Viewing entries tagged
getting started

PowerShell: Getting Started - Utilizing the Web: Part 4 (Controlling Internet Explorer via IE COM Object)

PowerShell: Getting Started - Utilizing the Web: Part 4 (Controlling Internet Explorer via IE COM Object)

PowerShell: Getting Started - Utilizing the Web: Part 4 (Controlling Internet Explorer via IE COM Object)

Why Control Internet Explorer with PowerShell?

I've covered using Invoke-WebRequest, as well as Invoke-RestMethod for basic and advanced tasks... why would we need anything else? Sometimes you'll try using Invoke-WebRequest, and you swear the data should be there, yet for some reason you just can't seem to parse it out. 

The reasons for this can vary. Typically, it is because the page renders something with a script, and you can only gather it with a browser being opened/controlled. If you're having a hard time parsing the data from Invoke-WebRequest, controlling IE can be a time saving solution. This can come in handy if you need to script up something quick, and then work on a long term solution after you know what's possible.

COM Object Instantiation

The first thing we will need to do is instantiate an instance of the Internet Explorer COM object. To do this, we'll use New-Object.

$ieObject = New-Object -ComObject 'InternetExplorer.Application'

$ieObject will store the object and contains the properties and methods we can work with. Let's take a peek:

$ieObject | Get-Member
gm.PNG

We will be using a few of these properties and methods as we work with controlling IE.

By default, the instance of Internet Explorer is not visible. For automation, that's great!
For demoing and testing, not so much!

Let's show the current instance of IE:

$ieObject.Visible = $true

There's our window.

After instantiating our object, we can use it to navigate to a website.

To do this, we will utilize the Navigate method, and pass along a string value to navigate to.

$ieObject.Navigate('http://www.tukui.org/forums/bb-login.php')

Now the browser should be at the website.

Form Elements/Information Gathering

Now that we have navigated to the site, we will need to login. To login, we'll first need do some some information gathering.

First, we will store the current page's document in a new variable named $currentDocument

Then, we will list out all of the tags named input, along with their associated types and names

$currentDocument = $ieObject.Document
$currentDocument.IHTMLDocument3_getElementsByTagName("input") | Select-Object Type,Name

This gives us enough information to proceed to the next section on filling out the form, however let's take a look at a couple more things.

What if we wanted to gather all the links?

$currentDocument.links | Select-Object outerText,href

If we wanted to get just the download URL, we could use something like:

$downloadLink = $currentDocument.links | Where-Object {$_.outerText -eq 'Download'} | Select-Object -ExpandProperty href
$downloadLink

Hmm... it appears that there are two identical links to the download page provided. To make sure we only grab one, we will need to use something like:

$downloadLink = $currentDocument.links | Where-Object {$_.outerText -eq 'Download'} | Select-Object -ExpandProperty href -First 1
$downloadLink

There we go!

Filling Out Forms and Clicking Buttons

Now that we have parsed out all of the input types, and have their names, we can proceed with filling out the form information.

Here they are for review:

Username

First let's set the username field. 

The username field is named user_login, so let's store that field in a variable named $userNameBox.

$userNameBox = $currentDocument.IHTMLDocument3_getElementsByTagName('input') | Where-Object {$_.name -eq 'user_login'}

To set the username, we can change the value property to our username. I have a credential object which contains my username, and I will use that to set it. You can pass along any string value, but I highly recommend using a credential object, or other secure method (more for the password, but it at also omits even showing the raw string for your username in the code).

$userNameBox.value = $myCredentials.UserName

You can see that this is set on the website now!

Password

Now to set the password. To do this we'll use the same logic as we did for the username, but specify the name password.

$passwordBox = $currentDocument.IHTMLDocument3_getElementsByTagName('input') | Where-Object {$_.name -eq 'password'}

The password object will take a string for the password (just as the username object did). I highly recommend using a credential object, and then using the GetNetworkCredential method, and then the Password property.

$passwordBox.value = $myCredentials.GetNetworkCredential().Password

Looks good!

Clicking Submit ("Log in »")

We've filled out our username and password, what now? Well... that button ain't gonna click itself! Let's click it via PowerShell.

There is no name set on the object for the button, so we'll use its type for uniqueness, and set the variable to store it that way ($submitButton).

$submitButton = $currentDocument.IHTMLDocument3_getElementsByTagName('input') | Where-Object {$_.type -eq 'submit'}

Now to click it! There is a method on this object named Click.

$submitButton | Get-Member -MemberType method

Ok but for real now, let's 'click' it!

$submitButton.click()

And if all goes well, you'll see the next page and be logged in.

Now that we're logged in, we could:

  • Set the $currentDocument variable to $ieObject.Document as the value is now different since a new page has been loaded.
  • Parse out information we need by exploring the webpage.
  • Automate looking for a specific post

It really all depends on what you want to do. 

One thing I have done is wrote up a script for a project where I had to rename user IDs. I had a list of old IDs and new IDs, and would iterate through all of them (sometimes 300+), until they were all renamed. All with logging and results output for later review and verification. Once you have the basics down you can really expand into the realm of full automation, and reap the benefits of what PowerShell can do.

!!**Note**!!

After each time you use the Navigate method, or click a button, you'll want to wait for the object's status to not be busy. 

Here is one way to accomplish that.

Store this function in memory:

function Invoke-IEWait {
    [cmdletbinding()]
    Param(
        [Parameter(
            Mandatory,
            ValueFromPipeLine
        )]
        $ieObject
    )

    While ($ieObject.Busy) {

        Start-Sleep -Milliseconds 10

    }

}

You can store it in the same script, or as part of a module. You can invoke this function whenever you need to now, and keep your code looking clean.

It's also good to always refresh your $currentDocument variable after a button click / page load. 

Here's some code for an example of when/how to use it:

#Set the URL we want to navigate to
$webURL = 'http://www.tukui.org/forums/bb-login.php'

#Create / store object invoked in a variable
$ieObject = New-Object -ComObject 'InternetExplorer.Application'

#By default it will not be visible, and this is likely how you'd want it set in your scripts
#But for demo purposes, lets set it to visible
$ieObject.Visible = $true

#Take a look at the object
$ieObject | Get-Member

#Navigate to the URL we set earlier
$ieObject.Navigate($webURL)

#Wait for the page to load
$ieObject | Invoke-IEWait

#Store current document in variable
$currentDocument = $ieObject.Document

Alright, time to clean things up.

Cleanup

Let's cleanup the mess we made.

First we will log out of the website:

$logoutURL = $currentDocument.links | Where-Object {$_.outerText -eq 'log out'} | Select-Object -ExpandProperty href -First 1
$logoutURL

Now that we've verified the URL (and only have one thanks to Select-Object along with -First 1), we can logout by navigating to it.

$ieObject.Navigate($logoutURL)

Now to verify...

Now to clean up the COM object we instantiated.

First we'll quit IE:

$ieObject.Quit()

Our IE window should have went poof.

If you really want to perform the best cleanup, you can follow that up with this (to release the COM object):

[void][Runtime.Interopservices.Marshal]::ReleaseComObject($ieObject)

Example Code

Here's some example code. I took what we accomplished above, and broke parts of it out into functions.

To use the code you'll need an account on the Tukui forums.

Here's what it does:

  • Prompts for credentials
  • Navigates to the site
    • Enters credentials
    • Clicks Submit
    • Parses links / text from posts
    • Outputs results to text files
  • Logs out of site
  • Quits IE
  • Releases IE COM object
function Invoke-IEWait { #Begin function Invoke-IEWait
    [cmdletbinding()]
    Param(
        [Parameter(
            Mandatory,
            ValueFromPipeLine
        )]
        $ieObject
    )

    While ($ieObject.Busy) {

        Start-Sleep -Milliseconds 10

    }

} #End function Invoke-IEWait

function Invoke-SiteLogon { #Begin function Invoke-SiteLogon
    [cmdletbinding()]
    param()

    #Set the URL we want to navigate to
    $webURL = 'http://www.tukui.org/forums/bb-login.php'

    #Create / store object invoked in a variable
    $ieObject = New-Object -ComObject 'InternetExplorer.Application'

    #By default it will not be visible, and this is likely how you'd want it set in your scripts
    #But for demo purposes, let's set it to visible
    $ieObject.Visible = $true

    #Navigate to the URL we set earlier
    $ieObject.Navigate($webURL)

    #Wait for the page to load
    $ieObject | Invoke-IEWait

    #Store current document in a variable
    $currentDocument = $ieObject.Document

    #Username field
    $userNameBox = $currentDocument.IHTMLDocument3_getElementsByTagName('input') | Where-Object {$_.name -eq 'user_login'}

    #Fill out username value
    $userNameBox.value = $myCredentials.UserName

    #Password field
    $passwordBox = $currentDocument.IHTMLDocument3_getElementsByTagName('input') | Where-Object {$_.name -eq 'password'}

    #Fill out password value
    $passwordBox.value = $myCredentials.GetNetworkCredential().Password

    #Submit button
    $submitButton = $currentDocument.IHTMLDocument3_getElementsByTagName('input') | Where-Object {$_.type -eq 'submit'}

    #Invoke click method on submit button
    $submitButton.click()

    #Wait for the page to load
    $ieObject | Invoke-IEWait

    #Return the object so we can work with it further in the script
    Return $ieObject

} #End function Invoke-SiteLogon

function Invoke-IECleanUp { #Begin function Invoke-IECleanUp
    [cmdletbinding()]
    param(
        [Parameter(
            Mandatory,
            ValueFromPipeLine
        )]
        $ieObject        
    )

    #Store logout URL
    $logoutURL = $currentDocument.links | Where-Object {$_.outerText -eq 'log out'} | Select-Object -ExpandProperty href -First 1

    #Use logout URL to logout via the Navigate method
    $ieObject.Navigate($logoutURL)

    #Wait for logout
    $ieObject | Invoke-IEWait

    #Clean up IE Object
    $ieObject.Quit()

    #Release COM Object
    [void][Runtime.Interopservices.Marshal]::ReleaseComObject($ieObject)

} #End function Invoke-IECleanUp

#Get credentials
$myCredentials = Get-Credential

#Login to site
$ieObject = Invoke-SiteLogon

#Wait in case it is still busy
$ieObject | Invoke-IEWait

#Set the current document variable 
$currentDocument = $ieObject.Document

#Get all elements that may have text values in a table, and store in a text file
$currentDocument.IHTMLDocument3_getElementsByTagName('td') | Select-Object OuterText | Out-File '.\siteStuff.txt'

#Get all links and select name/href, and store in a text file
$currentDocument.links | Select-Object outerText,href | Out-File '.\links.txt'

#Log out / clean up IE Object now that we're done
$ieObject | Invoke-IECleanUp

It will create the text files in the current working directory.

Homework

I've provided some links for hints.

I hope you've enjoyed this series.
As always, leave a comment if you have any feedback or questions!

[top]

PowerShell: Getting Started - Utilizing the Web: Part 3 (More Invoke-RestMethod)

PowerShell: Getting Started - Utilizing the Web: Part 3 (More Invoke-RestMethod)

Getting Started - Utilizing the Web: Part 3 (More Invoke-RestMethod)

Welcome to my Getting Started with Windows PowerShell series!

Continuing With Invoke-RestMethod

In part 2, we went over Invoke-RestMethod's basics, including:

In this post, I will be going over some more ways to use Invoke-RestMethod to authenticate to different APIs. I will also go over how to send information to the API, and work with the results we get back.

API Key In Header

Some APIs will require you to authenticate with a key in the header information. I happen to own a LIFX light, and their API uses that method of authentication.

If you own a LIFX light, and want to follow along, you can get a personal access token here: https://cloud.lifx.com/settings.

Information recap:

Now let's get to work! I have my key, which let's just say is: 'YouNeedYourOwnKey123321$'

Let's store that in a variable.

$apiKey          = 'YouNeedYourOwnKey123321$'

The next step here is to store that key in the header information (as a hashtable), with the key name of "Authorization".

$headers         =  @{Authorization=("Bearer {0}" -f $apiKey)}

Let's take a peek at $headers to ensure it contains what we need.

Looks good! Now to check it out. The URL in the documentation to get the lights available is 'https://api.lifx.com/v1/lights/all'. I like to store information like this in variables. 

$allURL          = 'https://api.lifx.com/v1/lights/all'

Now to put it all to use! We'll be utilizing Invoke-RestMethod with the following parameters/arguments:

  • Headers
    • Since we already have our headers stored in a nifty hashtable, we'll simply specify $headers
  • Uri
    • We'll pass along $allURL, and attempt to get a list of available lights

Here's all of the code so far. 

We'll be storing the results of the Invoke-RestMethod command in $ninjaLights.

$apiKey          = 'YouNeedYourOwnKey123321$'
$headers         =  @{Authorization=("Bearer {0}" -f $apiKey)}
$allURL          = 'https://api.lifx.com/v1/lights/all'

$ninjaLights     = Invoke-RestMethod -Headers $headers -Uri $allURL -ContentType 'application/json'

Success! 

Sending Data to a REST API via Body (LIFX API)

Now that we've authenticated to LIFX, we can work on sending information over, and control the light. Here are the steps I generally follow when using an API for the first time:

  • Review documentation
  • Review documentation (This can't be emphasized enough)
  • Test out a bunch of different ways to utilize the API
    • Get errors, handle errors, see what's expected, clean up the code, and build a function

Here is a snippet from their documentation:

What that tells us is how the text is to be formatted (JSON)and that we'll be using the PUT method.

Here are the parameters they accept:

Let's get started by ensuring we have working code, and then build a function around it.

The URL is 'https://api.lifx.com/v1/lights/all/state', which we'll store in $lightStateURL. We'll also setup some variables for the accepted parameters, as follows:

  • $state
    • on or off, we'll choose on
  • $color
    • We'll set this to red
  • $brightness
    • We'll set this to .5, or 50%
  • $duration
    • This will be set to .0, which is indefinitely

After setting up these variables, we'll use a here-string to build the payload, and finally use Invoke-RestMethod with the following parameters/arguments:

  • Uri
    • The $lightStateURL variable
  • Method
    • We'll be using PUT, per their documentation
  • Headers
  • Body
    • We will set this to $payloadBuilder (which is the here-string that contains the parameters we're passing to the LIFX API)

Here is the full code so far to test this:

$apiKey          = 'YouNeedYourOwnKey123321$'
$headers         =  @{Authorization=("Bearer {0}" -f $apiKey)}
$allURL          = 'https://api.lifx.com/v1/lights/all'

$lightStateURL = 'https://api.lifx.com/v1/lights/all/state'

$state      = 'on'
$color      = 'red'
$brightness = 0.5
$duration   = 0.0

$payload = [PSCustomObject] ${

    power      = $state,
    color      = $color,
    brightness = $brightness,
    duration   = $duration
}
$jsonPayload = $payload | ConvertTo-Json

$setResults = Invoke-RestMethod -Uri $lightStateURL -Method Put -Headers $headers -Body $jsonPayload -ContentType 'application/json'

Let's go ahead and run that to see if it worked:

This is an easy one to verify!

We can also take a look at the results property of $setResults.

Now that we know that it works, it can be used in a function to control the light.

Here is the code for an example of how to build a function with this information:

$apiKey          = 'YouNeedYourOwnKey123321'
$headers         =  @{Authorization=("Bearer {0}" -f $apiKey)}
$baseURL         = 'https://api.lifx.com/v1/lights'

function Set-LightState { #Begin function Set-LightState
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory)]
        $headers,
        [String]
        $state = 'on',
        [String]
        $color = 'White',
        [Double]
        $brightness = 0.5,
        [Double]
        $duration = 0.0
    )
    
    #Get lights
    $ninjaLights = Invoke-RestMethod -Headers $headers -Uri $allURL

    #If our light shows as connected...
    if ($ninjaLights.Connected) {

        #Set the selected as the ID of the light (per LIFX documentation)
        $selector = $ninjaLights.id
        
        #Construct the URL to include the selector and /state
        $fullURL  = "$baseurl/$selector/state"        

        #Build the payload
        $payload = [PSCustomObject] @{

            power      = $state
            color      = $color
            brightness = $brightness
            duration   = $duration
        }

        #Convert payload to JSON
        $jsonPayload = $payload | ConvertTo-Json

        Write-Host "Changing light from:" 
        Write-Host `t"State     : $($ninjaLights.Power)" 
        Write-Host `t"Color     : $($ninjaLights.Color)"
        Write-Host `t"Brightness: $($($ninjaLights.Brightness * 100))%"`n

        Write-Host "Changing light to:" 
        Write-Host `t"State     : $state" 
        Write-Host `t"Color     : $color"
        Write-Host `t"Brightness: $($brightness * 100)%" 
        Write-Host `t"Duration  : $duration"`n

        #Store results in $setResults
        $setResults = Invoke-RestMethod -Uri $fullURL -Method Put -Headers $headers -Body $jsonPayload -ContentType 'application/json'
    
        Write-Host "API status:"
        Write-Host `t"Light :" $setResults.results.label
        Write-Host `t"Status:" $setResults.results.status `n
    
    }

} #End function Set-LightState

Set-LightState -headers $headers -color blue

Let's see what happens when we run it! 

(NOTE) I use Write-Host here merely as a tool to give you a visual of the data you are working with. Ultimately it would be best to create some custom objects, and return the results as you need to with the code you're writing.

The information here is provided to show you what you can do, how you can build a payload, and then work with the data you are returned. You can do anything you put your mind to, from silly things (the next example), to even using it with another script (that may get weather information), and then flash the light red a few times if there is a weather alert.

For this next example, I will utilize a loop, and our new function, to confuse my girlfriend. Well, with all the testing I've been doing... maybe not anymore ;) 

Here's the code:

[array]$colors   = @('white','red','orange','yellow','cyan','green','blue','purple','pink')

$originalBrightness = $ninjaLights.Brightness
$originalColor      = $ninjalights.Color
$originalState      = $ninjalights.Power
$colorString        = "hue:" + $originalcolor.hue + " saturation:" + $originalcolor.saturation + " kelvin:" + $originalColor.Kelvin
        
$i = 0
        
While ($i -le 20) {
            
    $color              = Get-Random $colors
    [double]$brightness = "{0:n2}" -f (Get-Random -Maximum 1 -Minimum 0.00)
 
    Set-LightState -headers $headers -color $color -brightness $brightness -state $state
    Start-Sleep -seconds 1

    $i++
            
}
        
Set-LightState -headers $headers -state $originalState -color $colorString -brightness $originalBrightness

And off we go!

It worked! 

At the end it will set the light back to the value it was discovered with:

If you'd like a deeper dive into this, check out my post on using the Lifx API with PowerShell, here: http://www.gngrninja.com/script-ninja/2016/2/6/powershell-control-your-lifx-light

Username/Password in Request URL (PRTG API)

Some APIs authenticate you via including a username/password (hopefully eventually password hash) in the request URL. PRTG has one of those APIs. PRTG is one of my favorite monitoring tools, as not only is it great out of the box, but it also has great synergy with PowerShell.

PRTG information:

Let's start by getting the credentials of the account you want to use with the API. This account will be an account that has access to do what you need to do in PRTG. I will use the PRTG admin account, but you'll want to ensure you use one setup just to use with the API.

$prtgCredential = Get-Credential

If you're demoing PRTG, and are using a self-signed cert, you'll need the following code to allow Invoke-RestMethod to work with the self-signed cert. This code will only affect your current session.

Add-Type @"
using System.Net;
using System.Security.Cryptography.X509Certificates;

    public class TemporarySelfSignedCert : ICertificatePolicy {
        public TemporarySelfSignedCert() {}
        public bool CheckValidationResult(
            ServicePoint sPoint, X509Certificate cert,
            WebRequest wRequest, int certProb) {
                return true;
            }
    }
"@

[System.Net.ServicePointManager]::CertificatePolicy = New-Object TemporarySelfSignedCert

Now let's setup a variable that contains our PRTG hostname, and a custom object with information for the API URLs.

$hostName = 'localhost'
$prtgInfo = [PSCustomObject]@{

    BaseURL        = "https://$hostName/api"
    SensorCountURL = "https://$hostName/api/table.xml?content=sensors&columns=sensor"
    
}

Next, per PRTG's documenation, we'll construct the URL we need to use to get the password hash.

$getHashURL = "$($prtgInfo.BaseURL)/getpasshash.htm?username=$($prtgCredential.userName)&password=$($prtgCredential.GetNetworkCredential().Password)"

Now we can finally use Invoke-RestMethod to get the hash:

$getHash    = Invoke-RestMethod -Uri $getHashURL

Let's make sure $getHash has our password hash.

Alright, got it! Now we can create a new PS Credential object to store our username, and password hash (instead of our password).

First. we'll convert the hash we created into a secure string. Then, we will create a new PS Credential object using the username we specified in $prtgCredential, and the secure string we just created as the password.

$passwordHash        = ConvertTo-SecureString  $getHash -AsPlainText -Force
$prtgCredentialwHash = New-Object System.Management.Automation.PSCredential ($($prtgCredential.UserName), $passwordHash)

We can then verify the information by displaying the contents of $prtgCredentialwHash, and also using the GetNetworkCredential().Password method.

Now that we know $prtgCredentialwHash contains what we need, we can construct a URL to test out the API.

Let's set some variables up:

$prtgUser         = $prtgCredentialwHash.UserName
$prtgPass         = $prtgCredentialwHash.GetNetworkCredential().Password

$credentialAppend = "&username=$prtgUser&passhash=$prtgPass"

We can check the value of $credentialAppend to ensure it is correct:

Now to construct the URL, and test out the API via Invoke-RestMethod.

Invoke-RestMethod -uri ($prtgInfo.SensorCountURL + $credentialAppend)

Note: The full URL that is constructed is (I cut out my actual password hash on purpose, you'll see your hash after the = if all went well):

If all went well, you will see the results of the request (if not, you'll see an unauthorized message).

Success!

Sending Data via URL (PRTG API)

The PRTG API accepts data in the URL of the request. The below example will pause a sensor for a specific duration, with a specific message:

#Set the $duration (in minutes)
$duration = 10
#Set $sensorID to the sensor we want to pause
$sensorID = 45
#Set the pause message
$message  = "Paused via PS PRTG"

#Construct the $pauseURL
$pauseURL     = "$($prtgInfo.baseURL)/pauseobjectfor.htm?id=$sensorID&pausemsg=$message&duration=$duration"

#Use Invoke-RestMethod with the $pauseURL + $credentialAppend
#We will always append $credentialAppend as this is how the API accepts authentication
Invoke-RestMethod -Uri ($pauseURL + $credentialAppend)

Awesome, it worked!

Mega Example With Concept Code

This last example contains some concept code for a project I'm working on. Feel free to judge and use it as you wish, however I will note now that it is nowhere near finalized. I'm still in the exploration, see what's possible, and try to get it all to work phase. 

Requirements for concept code to work:

  • PRTG Installaction
  • Folder structure
  • Module
    • NinjaLogging.psm1 (included in ZIP file below)

NOTE: I have not fully cleaned up or officially released any of the code yet. That includes the logging module. 

Here is my TODO list for the code:

  • Upload it to GitHub
  • Clean up/add error handling
  • Create readme files
  • Add parameter sets to some of the psprtg.ps1 functions
  • Fully convert psprtg.ps1 to a module
  • Add functionality to psprtg.ps1 to include an 'oh crap!' undo feature
    • This will include exporting a custom object with the device ID and action taken

With that said, here is a link to download it (or you can skip to the code if that's all you want to see):

psPRTG download

How to get it working/examples:

  • Import the script as a module via Import-Module .\psprtg.ps1
    • The first time you do this, it will ask for your PRTG credentials. The module will then export the credential object to the input folder, and then use that exported credential the next time the module is used
  • Contents of machine/user encrypted credential file stored in the input directory:

 

  • Find a device and pause it via Invoke-SensorModification -findDevice
    • Note: You can also specify -Duration and -Message
  • Use a text file list of devices to find and take an action on

Pausing example:

Resuming example:

Log File Contents:

Code

psprtg.ps1

#Setup
$scriptPath      = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
$moduleDir       = "$scriptPath\modules"
$logDir          = "$scriptPath\logs"
$outputDir       = "$scriptPath\output"
$inputDir        = "$scriptPath\input"
$hashFile        = "$inputDir\prtgCredentialwHash.xml"
$hostName        = 'localhost'
$modules         = ('ninjaLogging')

$prtgInfo = [PSCustomObject]@{

    BaseURL        = "https://$hostName/api"
    SensorCountURL = "https://$hostName/api/table.xml?content=sensors&columns=sensor"
    
}

function Invoke-ModuleImport { #Begin function Invoke-ModuleImport
    [cmdletbinding()]
    param($modules)

    Push-Location

    ForEach ($module in $modules) {

        if (Get-Module -ListAvailable -Name $module) {
    
            Import-Module $module
    
        } elseif (!(Get-Module -Name $module)) {
              
            Write-Verbose "Importing module: [$module] from: [$moduleDir\$module]" 
            Import-Module "$moduleDir\$module"

        } Else {

            Write-Verbose "Module [$module] already loaded!"

        }

    }

    Pop-Location

} #End function Invoke-ModuleImport

function Invoke-CredentialCheck { #Begin function Invoke-CredentialCheck
    [cmdletbinding()]
    Param()

    Try { #Begin try for credential path existence

        #Check if our hashed credential object exists
        if (Test-Path $hashFile) { #Begin if for testing credential path existence 

            #Import credentials from hash file

            Write-LogFile -logPath $logFile -logValue "Importing credentials from [$hashFile]" -Verbose

            $prtgCredentialwHash = Import-Clixml $hashFile

            #Add to info hash

            $prtgInfo | Add-Member -Type NoteProperty -Name Credentials -Value $prtgCredentialwHash

            #create credential string for URLs
            $prtgUser         = $prtgCredentialwHash.UserName
            $prtgPass         = $prtgCredentialwHash.GetNetworkCredential().Password

            $credentialAppend = "&username=$prtgUser&passhash=$prtgPass"
            
            Return $credentialAppend

        #If it doesn't exist, attempt to create it
        } else {

            Write-Host "We need to get your hashed password to use with the API!" `n
            Write-Host "Please enter your PRTG credentials"`n

            #Store credential in $prtgCredential
            $prtgCredential = Get-Credential

            #Get the password hash via ConvertTo-SecureString and the Get-PRTGPasswordHash function.
            #Note: If you are not using a self-signed certificate (hopefully not, but if it is a demo/test install you may be): use -selfSignedCert:$True
            $passwordHash   = ConvertTo-SecureString  "$(Get-PRTGPasswordHash -PRTGCredential $prtgCredential -selfSignedCert:$True)" -AsPlainText -Force

            #Create new credential object and export it to the input folder
            #This allows us to use it when the script runs
            $prtgCredentialwHash = New-Object System.Management.Automation.PSCredential ($($prtgCredential.UserName), $passwordHash)

            Write-LogFile -logPath $logFile -logValue "Exporting hashed credentials to [$hashFile]." -Verbose

            #Export to file
            $prtgCredentialwHash | Export-Clixml $hashFile

            #create credential string for URLs
            $prtgUser         = $prtgCredentialwHash.UserName
            $prtgPass         = $prtgCredentialwHash.GetNetworkCredential().Password

            $credentialAppend = "&username=$prtgUser&passhash=$prtgPass"

            Return $credentialAppend

        }  #End if for testing credential path existence

    } #End try for credential path existence

    Catch {

        $errorMessage = $_.Exception.Message

        Write-LogFileError -logPath $logFile -errorDesc "Error while checking for credentials/importing credentials: [$errorMessage]!"
    
        #Resolve the log file
        Resolve-LogFile -logPath $logFile

        Break

    }

} #End function Invoke-CredentialCheck

function Get-PRTGPasswordHash { #Begin function Get-PRTGPasswordHash
    [cmdletbinding()]
    param(
        [Parameter(Mandatory)]
        [PSCredential]
        $PRTGCredential,

        [Parameter()]
        [Boolean]
        $selfSignedCert = $false
    )

    if ($selfSignedCert) {

        Try {

            Add-Type @"
                using System.Net;
                using System.Security.Cryptography.X509Certificates;

                    public class TemporarySelfSignedCert : ICertificatePolicy {
                    public TemporarySelfSignedCert() {}
                    public bool CheckValidationResult(
                        ServicePoint sPoint, X509Certificate cert,
                        WebRequest wRequest, int certProb) {
                        return true;
                    }
                }
"@

            [System.Net.ServicePointManager]::CertificatePolicy = New-Object TemporarySelfSignedCert

    }

        Catch {
        
            $errorMessage = $_.Exception.Message

            Write-LogFileError -logPath $logFile -errorDesc "Error while allowing self signed certs: [$errorMessage]" -ForegroundColor Red -BackgroundColor DarkBlue
            
            #Resolve the log file
            Resolve-LogFile -logPath $logFile

            Break


        }

    }

    Try {

        $getHashURL = "$($prtgInfo.BaseURL)/getpasshash.htm?username=$($prtgCredential.userName)&password=$($prtgCredential.GetNetworkCredential().Password)"
        $getHash    = Invoke-RestMethod -Uri $getHashURL 

        Return $getHash

    }

    Catch {

        $errorMessage = $_.Exception.Message

        Write-LogFileError -logPath $logFile -errorDesc "Error while getting hash: [$errorMessage]" -Verbose

        Break

    }

} #End function Get-PRTGPasswordHash

function Invoke-DeviceSearch { #Begin function Invoke-DeviceSearch
    [cmdletbinding()]
    Param(
        [Parameter(Mandatory)]
        $findMe
    )

    [System.Collections.ArrayList]$foundDeviceIDs = @()

    ForEach ($find in $findMe) {

        Try {

            $getDeviceURL       = "$($prtgInfo.BaseURL)/table.json?content=devices&output=json&columns=objid,probe,group,device,host,downsens,partialdownsens,downacksens,upsens,warnsens,pausedsens,unusualsens,undefinedsens"
            $devices            = Invoke-RestMethod -Uri ($getDeviceURL + $credentialAppend) 
            $findDeviceWildCard = '*' + $find + '*'

            ForEach ($device in $devices.devices) {

                Switch ($device.device) {

                    {$_ -like $findDeviceWildCard} {
                    
                        $foundDeviceIDs.Add($device.objid) | Out-Null
                    
                    }

                }

            } 
        
        }

        Catch {

            $errorMessage = $_.Exception.Message

            Write-LogFileError -logPath $logFile -errorDesc "Unable to get device list: [$errorMessage]" -Verbose

        }

    }

    Return $foundDeviceIDs

} #End function Invoke-DeviceSearch

function Invoke-SensorModification { #Begin function Invoke-SensorModification
    [cmdletbinding()]
    Param(
        [parameter(Mandatory)]
        [string]
        $action,
        [parameter()]
        [string]
        $findDevice,
        [Parameter()]
        [int]
        $sensorID,
        [Parameter()]
        [String]
        $message,
        [Parameter()]
        [int]
        $duration,
        [Parameter()]
        $List
    )

    $logFile = New-logFile -logPath $logDir -logName 'PSPRTG.log' -addDate:$true

    if (!$duration) {

        $duration = 30

    }

    if (!$message) {

        $message = "Sensor paused by: [$((Get-ChildItem Env:\USERNAME).Value)] via PS PRTG."    

    }

    if ($List) {
        
        Switch ($List | Get-Member | Select-Object -ExpandProperty TypeName -Unique) {

            {$_ -eq 'System.String'} {

                if ($list -like '*.txt') {
                
                    Write-Host "File detected" 

                    $deviceList = Get-Content $list

                    $foundDeviceIDs = Invoke-DeviceSearch -findMe $deviceList

                } else {

                    Write-Host 'Single string or array detected'

                    $deviceList = $list 

                    $foundDeviceIDs = Invoke-DeviceSearch -findMe $deviceList

                }

            }

            {$_ -eq 'System.Management.Automation.PSCustomObject'} {

                Write-Host 'Custom Object detected'

            }

        }

    }

    if ($findDevice) {

        Try {
            
            $foundDeviceIDs = Invoke-DeviceSearch -findMe $findDevice
        
        }

        Catch {

            $errorMessage = $_.Exception.Message

            Write-LogFileError -logPath $logFile -errorDesc "Unable to get device list: [$errorMessage]" -Verbose
            
            #Resolve the log file
            Resolve-LogFile -logPath $logFile

            Break
        
        }

    }

    Switch ($action) { #Begin switch for PRTG action

        {$_ -eq 'Pause'} {

            if ($findDevice -or $List) {

                ForEach ($id in $foundDeviceIDs) {

                    $sensorID     = $null
                    $sensorID     = $id

                    $pauseURL     = "$($prtgInfo.baseURL)/pauseobjectfor.htm?id=$sensorID&pausemsg=$message&duration=$duration"
        
                    Write-LogFile -logPath $logFile -Value "Attempting to pause sensor ID: [$sensorID], for duration of [$duration], with message [$message]" -Verbose
            
                    $pauseAttempt = Invoke-WebRequest -Uri ($pauseURL + $credentialAppend)   
                
                } 

                #Resolve the log file
                Resolve-LogFile -logPath $logFile 

            } 
            
            Else { 

                $pauseURL     = "$($prtgInfo.baseURL)/pauseobjectfor.htm?id=$sensorID&pausemsg=$message&duration=$duration"
        
                Write-LogFile -logPath $logFile -Value "Attempting to pause sensor ID: [$sensorID], for duration of [$duration], with message [$message]" -Verbose
            
                $pauseAttempt = Invoke-WebRequest -Uri ($pauseURL + $credentialAppend)

                #Resolve the log file
                Resolve-LogFile -logPath $logFile 
            

            }

        }

        {$_ -eq 'Resume'} {

            if ($findDevice -or $List) {

                ForEach ($id in $foundDeviceIDs) {

                    $sensorID     = $null
                    $sensorID     = $id

                    $resumeURL     = "$($prtgInfo.baseURL)/pause.htm?id=$sensorID&action=1"
        
                    Write-LogFile -logPath $logFile -Value "Attempting to resume sensor ID: [$sensorID]." -Verbose
            
                    $resumeAttempt =  Invoke-WebRequest -Uri ($resumeURL + $credentialAppend)
                
                }
                
                #Resolve the log file
                Resolve-LogFile -logPath $logFile 

            } 
            
            Else { 

                $resumeURL     = "$($prtgInfo.baseURL)/pause.htm?id=$sensorID&action=1"
        
                Write-LogFile -logPath $logFile -Value "Attempting to resume sensor ID: [$sensorID]." -Verbose
            
                $resumeAttempt =  Invoke-WebRequest -Uri ($resumeURL + $credentialAppend)
            
                #Resolve the log file
                Resolve-LogFile -logPath $logFile 

            }

        }

        Default {

            Write-LogFileError -logPath $logFile -errorDesc "Unable to perform action [$action], as it does not match any valid actions!" -Verbose

            Resolve-LogFile -logPath $logFile 

            Break

        }

    } #End switch for PRTG action

} #End function Invoke-SensorModification

#Test API
Try{

    #Import modules 
    ForEach ($module in $modules) {Invoke-ModuleImport -modules $module}

    #Create log file
    $logFile = New-logFile -logPath $logDir -logName 'PSPRTG.log' -addDate:$true

    #Attempt to import/set credentials and get the returned string to append to the request for authentication
    $credentialAppend = Invoke-CredentialCheck
    
    #Attempt to get information as a test to see if the API will work
    Invoke-RestMethod -uri ($prtgInfo.SensorCountURL + $credentialAppend) | Out-Null

    #If it works, set TestPassed as true
    $prtgInfo | Add-Member -MemberType NoteProperty -Name TestPassed -Value $true
    
}

Catch {

    $errorMessage = $_.Exception.Message
    
    Write-LogFileError -logPath $logFile -errorDesc "Error while attempting to use API: [$errorMessage]" -Verbose

    #If it doesn't work, set TestPassed as false and break out
    $prtgInfo | Add-Member -MemberType NoteProperty -Name TestPassed -Value $false

    #Resolve the log file
    Resolve-LogFile -logPath $logFile

    Break

}

NinjaLogging.psm1

Set-StrictMode -Version Latest

$scriptPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition

Switch ($MyInvocation.PSCommandPath) {

    {$_ -match '\\\\'} {
    
    
        $scriptName = $_.SubString($_.LastIndexOf('\')+1)
    
    }

    Default { 
    
        $scriptName = Get-ChildItem $MyInvocation.PSCommandPath | Select-Object -ExpandProperty BaseName
        
    }

}

if ('Count' -in ($scriptName.psobject.Members.Name)) {
    
    $scriptName = 'ScriptLog'
    
}

function New-LogFile {
<#
.SYNOPSIS
   New-LogFile will create a log file.

.DESCRIPTION
   New-LogFile will create a log file. 

   You can specify different paramaters to change the file's name, and where it is stored.
   By default it will attempt to get the name of the calling function or script via $scriptName = (Get-ChildItem $MyInvocation.PSCommandPath | Select-Object -ExpandProperty BaseName).
   It will also attempt to get the path via $scriptPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition.
   You can also specify the path and name, as well as if you'd like to append the date in the following format: MM-dd-yy_HHmm.

   Use the -Verbose parameter to display what is happening to the host.

.PARAMETER logPath
    Alias: Path
    Type : String

    Specify the path to the logfile

.PARAMETER logName    
    Alias: Name
    Type : String

    Specify the name of the log file. Be sure to include the extension if specifying the name.

.PARAMETER scriptVersion
    Type : Double

    Specify the version of your script being run. If left blank, will default to 0.1

.PARAMETER addDate
    Type : Boolean

    Specify if you'd like to add the date to the file name.  If you're specifying logName, you can use addDate to append the current date/time in the format: MM-dd-yy_HHmm.

.NOTES
    Name: New-LogFile
    Version: 1.0
    Author: Ginger Ninja (Mike Roberts)
    DateCreated: 5/11/16
    

.LINK
    http://www.gngrninja.com

.EXAMPLE
    $logFile = New-LogFile 
    -----------------------------
    
    gngrNinja> $logFile
    C:\PowerShell\logs\ScriptLog_05-11-16_1612.log

.EXAMPLE
    $logFile = New-LogFile -Verbose
    -----------------------------
    
    VERBOSE: No path specified. Using: C:\PowerShell\logs
    VERBOSE:
    VERBOSE: No log name specified. Setting log name to: ScriptLog.log and adding date.
    VERBOSE:
    VERBOSE: Adding date to log file with an extension! New file name:
    ScriptLog_05-11-16_1613.log
    VERBOSE:
    VERBOSE: Created C:\PowerShell\logs\ScriptLog_05-11-16_1613.log
    VERBOSE:
    VERBOSE: File C:\PowerShell\logs\ScriptLog_05-11-16_1613.log created and verified to
    exist.
    VERBOSE:
    VERBOSE: Adding the following information to:
    C:\PowerShell\logs\ScriptLog_05-11-16_1613.log
    VERBOSE:
    VERBOSE: -----------------------------------------------------------------
    VERBOSE: Started logging at [05/11/2016 16:13:11]
    VERBOSE: Script (Version 0.1) executed by: [thegn] on computer: [GINJA10]
    VERBOSE: -----------------------------------------------------------------
    VERBOSE:
    gngrNinja>

.EXAMPLE
    $logfile = New-LogFile -Name 'testName.log' -path  'c:\temp' -addDate $true -Verbose
    -----------------------------

    VERBOSE: Adding date to log file with an extension! New file name:
    testName_05-11-16_1615.log
    VERBOSE:
    VERBOSE: Created c:\temp\testName_05-11-16_1615.log
    VERBOSE:
    VERBOSE: File C:\temp\testName_05-11-16_1615.log created and verified to exist.
    VERBOSE:
    VERBOSE: Adding the following information to: C:\temp\testName_05-11-16_1615.log
    VERBOSE:
    VERBOSE: -----------------------------------------------------------------
    VERBOSE: Started logging at [05/11/2016 16:15:13]
    VERBOSE: Script (Version 0.1) executed by: [thegn] on computer: [GINJA10]
    VERBOSE: -----------------------------------------------------------------
    VERBOSE:

.OUTPUTS
    Full path to the log file created.
#>
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $false,
                   Position  = 0)]
        [Alias('Path')]
        [string]
        $logPath,
        [Parameter(Mandatory = $false,
                   Position  = 1)]
        [Alias('Name')]
        [string]
        $logName,
        [Parameter(Mandatory = $false,
                   Position  = 2)]
        [double]
        $scriptVersion = 0.1,
        [Parameter(Mandatory = $false,
                   Position  = 3)]
        [boolean]
        $addDate = $false
    )

#Check if file/path are set
    if (!$logPath) {
        
        $logPath = "$scriptPath\logs"
        
        Write-Verbose "No path specified. Using: $logPath"
        Write-Verbose ""
        
    }
    
    if (!$logName) {
        
        $logName = $scriptName + '.log'
        $addDate = $true
        
        Write-Verbose "No log name specified. Setting log name to: $logName and adding date."
        Write-Verbose ""
      
    }

    #Check if $addDate is $true, take action if so
    if ($addDate) {
        
        if ($logName.Contains('.')) {
            
            $logName = $logName.SubString(0,$logName.LastIndexOf('.')) + "_{0:MM-dd-yy_HHmm}" -f (Get-Date) + $logName.Substring($logName.LastIndexOf('.'))
            
            Write-Verbose "Adding date to log file with an extension! New file name: $logName"
            Write-Verbose ""
           
        } else {
            
            $logName = $logName + "_{0:MM-dd-yy_HHmm}" -f (Get-Date)
            
            Write-Verbose "Adding date to log file. New file name: $logName"
            Write-Verbose ""
            
        }
         
    }
    
    #Variable set up
    $time     = Get-Date
    $fullPath = $logPath + '\' + $logName
    $curUser  = (Get-ChildItem Env:\USERNAME).Value
    $curComp  = (Get-ChildItem Env:\COMPUTERNAME).Value
    
    #Checking paths / Creating directory if needed
    
    if (!(Test-Path $logPath)) {
        
        Try {
            
            New-Item -Path $logPath -ItemType Directory -ErrorAction Stop | Out-Null
            
            Write-Verbose "Folder $logPath created as it did not exist."
            Write-Verbose ""
            
        }
        
        Catch {
            
            $message = $_.Exception.Message
            
            Write-Output "Could not create folder due to an error. Aborting. (See error details below)"
            Write-Error $message
            
            Break
          
        }
    
    }
    
    #Checking to see if a file with the name name exists, renaming it if so.
    if (Test-Path $fullPath) {
        
        Try {
            
            $renFileName = ($fullPath + (Get-Random -Minimum ($time.Second) -Maximum 999) + 'old')
            
            Rename-Item $fullPath -NewName ($renFileName.Substring($renFileName.LastIndexOf('\')+1)) -Force -ErrorAction Stop | Out-Null
            
            Write-Verbose "Renamed $fullPath to $($renFileName.Substring($renFileName.LastIndexOf('\')+1))"
            Write-Verbose ""
            
        }
        
        Catch {
            
            $message = $_.Excetion.Message
            
            Write-Output "Could not rename existing file due to an error. Aborting. (See error details below)"
            Write-Error $message
            
            Break
            
        }
        
    }
    
    #File creation
    Try {
        
        New-Item -Path $fullPath -ItemType File -ErrorAction Stop | Out-Null
        
        Write-Verbose "Created $fullPath"
        Write-Verbose ""
        
    } 
    
    Catch {
        
        $message = $_.Exception.Message
        
        Write-Output "Could not create directory due to an error. Aborting. (See error details below)"
        Write-Error $message
        
        Break
        
    }
    
    #Get the full path in case of dot sourcing
    $fullPath = (Get-ChildItem $fullPath).FullName
    
    if (Test-Path $fullPath) {
        
        $flairLength = ("Script (Version $scriptVersion) executed by: [$curUser] on computer: [$curComp]").Length + 1
        
        Write-Verbose "File $fullPath created and verified to exist."
        Write-Verbose ""
        Write-Verbose "Adding the following information to: $fullPath"
        Write-Verbose ""
        Write-Verbose ('-'*$flairLength)
        Write-Verbose "Started logging at [$time]"
        Write-Verbose "Script (Version $scriptVersion) executed by: [$curUser] on computer: [$curComp]"
        Write-Verbose ('-'*$flairLength)
        Write-Verbose ""
        
        Add-Content -Path $fullPath -Value ('-'*$flairLength)
        Add-Content -Path $fullPath -Value "Started logging at [$time]"
        Add-Content -Path $fullPath -Value "Script (Version $scriptVersion) executed by: [$curUser] on computer: [$curComp]"
        Add-Content -Path $fullPath -Value ('-'*$flairLength)
        Add-Content -Path $fullPath -Value ""
        
        Return [string]$fullPath
         
    } else {
        
        Write-Error "File $fullPath does not exist. Aborting script."

        Break
        
    }
       
}

function Write-LogFile {
<#
.SYNOPSIS
   Write-LogFile will add information to a log file created with New-LogFile.

.DESCRIPTION
   Write-LogFile will add information to a log file created with New-LogFile.

   By default additions to the log file will include a timestamp, unless you specify -addTimeStamp $false.
   This function accepts values from the pipeline, as demonstrated in an example.

   Use the -Verbose parameter to display what is being logged to the host.
   
.PARAMETER logPath
    Alias: Path
    Type : String

    Specify the full path to the log file, including the name.

.PARAMETER logValue
    Alias: Value
    Type : String

    Specify the value(s) you'd like logged.

.PARAMETER addTimeStamp
    Type : Boolean

    Defaults to true, set to false if you'd like to omit the timestamp.

.NOTES
    Name: Write-LogFile
    Version: 1.0
    Author: Ginger Ninja (Mike Roberts)
    DateCreated: 5/11/16

.LINK
    http://www.gngrninja.com

.EXAMPLE
    For this example we'll assume you use:
    $logFile = New-LogFile 

    Write-LogFile -logPath $logFile -logValue 'test log value!'
    -----------------------------

    gngrNinja> more $logfile
    -----------------------------------------------------------------
    Started logging at [05/11/2016 16:19:37]
    Script (Version 0.1) executed by: [thegn] on computer: [GINJA10]
    -----------------------------------------------------------------

    [05-11-16 16:23:24] test log value!

.EXAMPLE
    For this example we'll assume you use:
    $logFile = New-LogFile 

    Write-LogFile -logPath $logFile -logValue 'test log value!' -Verbose
    -----------------------------
    
    VERBOSE: Adding [05-11-16 16:25:19] test log value! to
    C:\PowerShell\logs\ScriptLog_05-11-16_1619.log
    VERBOSE:

.EXAMPLE
    For this example we'll assume you use:
    $logFile = New-LogFile 

    Get-Process | Write-LogFile $logFile -Verbose
    -----------------------------

    ...
    VERBOSE: Adding [05-11-16 16:26:51] System.Diagnostics.Process (wininit) to
    C:\PowerShell\logs\ScriptLog_05-11-16_1619.log
    VERBOSE:
    VERBOSE: Adding [05-11-16 16:26:51] System.Diagnostics.Process (winlogon) to
    C:\PowerShell\logs\ScriptLog_05-11-16_1619.log
    VERBOSE:
    VERBOSE: Adding [05-11-16 16:26:51] System.Diagnostics.Process (WmiPrvSE) to
    C:\PowerShell\logs\ScriptLog_05-11-16_1619.log
    VERBOSE:
    VERBOSE: Adding [05-11-16 16:26:51] System.Diagnostics.Process (WmiPrvSE) to
    C:\PowerShell\logs\ScriptLog_05-11-16_1619.log
    VERBOSE:
    VERBOSE: Adding [05-11-16 16:26:51] System.Diagnostics.Process (WUDFHost) to
    C:\PowerShell\logs\ScriptLog_05-11-16_1619.log
    VERBOSE:
    ...

.EXAMPLE
    For this example we'll assume you use:
    $logFile = New-LogFile 

    Write-LogFile -logPath $logFile -logValue 'test without timestamp' -addTimeStamp $false -Verbose
    -----------------------------

    VERBOSE: Adding test without timestamp to C:\PowerShell\logs\ScriptLog_05-11-16_1631.log
    VERBOSE:
#>
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true,
                   Position  = 0)]
        [Alias('Path')]
        [string]
        $logPath,
        [Parameter(Mandatory                       = $true,
                   ValueFromPipeline               = $true,
                   ValueFromPipelineByPropertyName = $true,
                   Position                        = 1)]
        [Alias('Value')]
        [string]
        $logValue,
        [Parameter(Mandatory = $false,
                   Position  = 2)]
        [boolean]
        $addTimeStamp = $true
    )
    
    Begin {

        if (!(Test-Path $logPath)) {
        
            Write-Error "Unable to access $logPath"

            Break
        
        } 

    }

    Process {

        ForEach ($value in $logValue) {
        
            $timeStamp = "[{0,0:MM}-{0,0:dd}-{0,0:yy} {0,0:HH}:{0,0:mm}:{0,0:ss}]" -f (Get-Date)

            if ($addTimeStamp) {
            
                $value = "$($timeStamp + ' ' + $value)"
           
            }
        
            Write-Verbose "Adding $value to $logPath"
            Write-Verbose ""
        
            Add-Content -Path $logPath -Value $value
            Add-Content -Path $logPath -Value ''

        }
         
    }

}

function Write-LogFileError {
<#
.SYNOPSIS
   Write-LogFileError will add information to a log file created with New-LogFile. The information will be prepended with [ERROR].

.DESCRIPTION
   Write-LogFileError will add information to a log file created with New-LogFile. The information will be prepended with [ERROR].

   By default additions to the log file will include a timestamp, unless you specify -addTimeStamp $false.
   This function accepts values from the pipeline, as demonstrated in an example.

   Use the -Verbose parameter to display what is being logged to the host.
   
.PARAMETER logPath
    Alias: Path
    Type : String

    Specify the full path to the log file, including the name.

.PARAMETER errorDesc
    Alias: Value
    Type : String

    Specify the value(s) you'd like logged as errors.

.PARAMETER addTimeStamp
    Type : Boolean

    Defaults to true, set to false if you'd like to omit the timestamp.

.PARAMETER exitScript
    Alias: Exit
    Type : Boolean

    This parameter let's you specify $true if you'd like to exit the script after the error is logged. 
    It defaults to $false.
    
.NOTES
    Name: Write-LogFileError
    Version: 1.0
    Author: Ginger Ninja (Mike Roberts)
    DateCreated: 5/11/16

.LINK
    http://www.gngrninja.com

.EXAMPLE
    For this example we'll assume you use:
    $logFile = New-LogFile 

    Write-LogFileError -logPath $logFile -errorDesc 'test log value error!'
    -----------------------------

    gngrNinja> more $logFile
    -----------------------------------------------------------------
    Started logging at [05/11/2016 16:31:44]
    Script (Version 0.1) executed by: [thegn] on computer: [GINJA10]
    -----------------------------------------------------------------

    [05-11-16 16:31:51] [ERROR ENCOUNTERED]: test log value error!
#>
    [CmdletBinding()]
        param(
            [Parameter(Mandatory = $true,
                       Position  = 0)]
            [Alias('Path')]
            [string]
            $logPath,
            [Parameter(Mandatory                       = $true,
                       ValueFromPipeline               = $true,
                       ValueFromPipelineByPropertyName = $true,
                       Position                        = 1)]
            [string]
            $errorDesc,
            [Parameter(Mandatory = $false,
                       Position  = 2)]
            [boolean]
            $addTimeStamp = $true,
            [Parameter(Mandatory = $false,
                       Position  = 3)]
            [Alias('Exit')]           
            [boolean]
            $exitScript = $false
        )
    
    Begin {

        if (!(Test-Path $logPath)) {
    
            Write-Error "Unable to access $logPath"
            Break

        }

    }

    Process {
     
        ForEach ($value in $errorDesc) { 

            $timeStamp = "[{0,0:MM}-{0,0:dd}-{0,0:yy} {0,0:HH}:{0,0:mm}:{0,0:ss}]" -f (Get-Date)

            $value     = "[ERROR ENCOUNTERED]: $value"
        
            if ($addTimeStamp) {
            
                $value = "$($timeStamp + ' ' + $value)"
           
            }
        
            Write-Verbose "Adding $value to $logPath"
            Write-Verbose ""
        
            Add-Content -Path $logPath -Value $value
            Add-Content -Path $logPath -Value ''

        }

    }

    End {
         
         if ($exitScript) {
            
            Write-Verbose "Performing log file close command: Resolve-LogFile -logPath $logPath -exitonCompletion $true"
            Write-Verbose ""
            
            Resolve-LogFile -logPath $logPath -exitScript $true
            
        }

    }
 
    
    
}

function Write-LogFileWarning {
<#
.SYNOPSIS
   Write-LogFileWarning will add information to a log file created with New-LogFile. The information will be prepended with [ERROR].

.DESCRIPTION
   Write-LogFileWarning will add information to a log file created with New-LogFile. The information will be prepended with [ERROR].

   By default additions to the log file will include a timestamp, unless you specify -addTimeStamp $false.
   This function accepts values from the pipeline, as demonstrated in an example.

   Use the -Verbose parameter to display what is being logged to the host.
   
.PARAMETER logPath
    Alias: Path
    Type : String

    Specify the full path to the log file, including the name.

.PARAMETER warningDesc
    Alias: Value
    Type : String

    Specify the value(s) you'd like logged as errors.

.PARAMETER addTimeStamp
    Type : Boolean

    Defaults to true, set to false if you'd like to omit the timestamp.

.PARAMETER exitScript
    Alias: Exit
    Type : Boolean

    This parameter let's you specify $true if you'd like to exit the script after the error is logged. 
    It defaults to $false.

.NOTES
    Name: Write-LogFileWarning
    Version: 1.0
    Author: Ginger Ninja (Mike Roberts)
    DateCreated: 5/11/16

.LINK
    http://www.gngrninja.com

.EXAMPLE
    For this example we'll assume you use:
    $logFile = New-LogFile 

    Write-LogFileWarning -logPath $logFile -warningDesc 'test log value warning!'
    -----------------------------

    gngrNinja> more $logFile
    -----------------------------------------------------------------
    Started logging at [05/11/2016 16:38:29]
    Script (Version 0.1) executed by: [thegn] on computer: [GINJA10]
    -----------------------------------------------------------------

    [05-11-16 16:38:48] [WARNING]: test log value warning!
#>
    [CmdletBinding()]
        param(
            [Parameter(Mandatory = $true,
                       Position  = 0)]
            [Alias('Path')]
            [string]
            $logPath,
            [Parameter(Mandatory                       = $true,
                       ValueFromPipeline               = $true,
                       ValueFromPipelineByPropertyName = $true,
                       Position                        = 1)]
            [string]
            $warningDesc,
            [Parameter(Mandatory = $false,
                       Position  = 2)]
            [boolean]
            $addTimeStamp = $true,
            [Parameter(Mandatory = $false,
                       Position  = 3)]
            [Alias('Exit')]           
            [boolean]
            $exitScript = $false
        )
    
    Begin {

        if (!(Test-Path $logPath)) {
    
            Write-Error "Unable to access $logPath"

            Break

        }

    }

    Process {
     
        ForEach ($value in $warningDesc) { 

            $timeStamp = "[{0,0:MM}-{0,0:dd}-{0,0:yy} {0,0:HH}:{0,0:mm}:{0,0:ss}]" -f (Get-Date)

            $value     = "[WARNING]: $value"
        
            if ($addTimeStamp) {
            
                $value = "$($timeStamp + ' ' + $value)"
           
            }
        
            Write-Verbose "Adding $value to $logPath"
            Write-Verbose ""
        
            Add-Content -Path $logPath -Value $value
            Add-Content -Path $logPath -Value ''

        }

    }

    End {
         
         if ($exitScript) {
            
            Write-Verbose "Performing log file close command: Resolve-LogFile -logPath $logPath -exitonCompletion $true"
            Write-Verbose ""
            
            Resolve-LogFile -logPath $logPath -exitScript $true
            
        }

    }
    
}

function Resolve-LogFile {
<#
.SYNOPSIS
   Resolve-LogFile will resolve a created log file.

.DESCRIPTION
   Resolve-LogFile will resolve a created log file.

   Use the -Verbose parameter to display what is happening to the host.

.PARAMETER logPath
    Alias: Path
    Type : String

    Specify the full path, including name, to the log file to be resolved.

.PARAMETER logName    
    Alias: Name
    Type : String

    Specify the name of the log file. Be sure to include the extension if specifying the name.

.PARAMETER exitScript
    Alias: Exit
    Type : Boolean

    Specify $true if you'd like to exit the script after the log file is resolved. 
    It defaults to $false.

.NOTES
    Name: Resolve-LogFile
    Version: 1.0
    Author: Ginger Ninja (Mike Roberts)
    DateCreated: 5/11/16
    
.LINK
    http://www.gngrninja.com

.EXAMPLE
    $logFile = New-LogFile 
    -----------------------------
    
    gngrNinja> $logFile
    C:\PowerShell\logs\ScriptLog_05-11-16_1612.log

.EXAMPLE

    $logFile = New-LogFile 
    Get-Process | Write-LogFile $logFile

    Resolve-LogFile $logFile
    -----------------------------
    
    ...
    [05-11-16 16:43:58] System.Diagnostics.Process (wininit)

    [05-11-16 16:43:58] System.Diagnostics.Process (winlogon)

    [05-11-16 16:43:58] System.Diagnostics.Process (WmiPrvSE)

    [05-11-16 16:43:58] System.Diagnostics.Process (WmiPrvSE)

    [05-11-16 16:43:58] System.Diagnostics.Process (WUDFHost)

    ---------------------------------------------
    Ended logging at [05/11/2016 16:44:01]
    ---------------------------------------------
#>
    [cmdletbinding()]
    param(
        [parameter(Mandatory = $true,
                   Position  = 0)]
        [Alias('Path')]
        [string]
        $logPath,
        [Parameter(Mandatory = $false,
                   Position  = 1)]
        [Alias('Exit')]
        [boolean]
        $exitScript = $false
    )
    
    $time = Get-Date
    
    if (Test-Path $logPath) {
        
        $flairLength = ("Finished processing at [$time]").Length + 1
        
        Write-Verbose "Adding the following content to: $logPath"
        Write-Verbose ('-'*$flairLength)
        Write-Verbose "Ended logging at [$time]"
        Write-Verbose ('-'*$flairLength)
        Write-Verbose ""
        
        Add-Content -Path $logPath -Value ('-'*$flairLength)
        Add-Content -Path $logPath -Value "Ended logging at [$time]"
        Add-Content -Path $logPath -Value ('-'*$flairLength)
   
    } else {
        
        Write-Error "Unable to access $logPath"

        Break
        
    }
    
    if ($exitScript) {
        
        Write-Verbose "Exiting on completion specified, exiting..."
        
        Exit
        
    } 
   
}

function Out-LogFile {
<#
.SYNOPSIS
   Out-LogFile will create, add to, and resolve a logfile.

.DESCRIPTION
   Out-LogFile will create, add to, and resolve a logfile.

   Value from the pipeline is accepted.

   Use the -Verbose parameter to display what is happening to the host.

.PARAMETER logPath
    Alias: Path
    Type : String

    Specify the path to the logFile you'd like created.

.PARAMETER logName    
    Alias: Name
    Type : String

    Specify the name of the log file. Be sure to include the extension if specifying the name.

.PARAMETER logValue
    Alias: Value
    Type : String

    Specify the value(s) you'd like logged.

.PARAMETER addTimeStamp
    Type : Boolean

    Defaults to true, set to false if you'd like to omit the timestamp.

.NOTES
    Name: Out-LogFile
    Version: 1.0
    Author: Ginger Ninja (Mike Roberts)
    DateCreated: 5/11/16
    
.LINK
    http://www.gngrninja.com

.EXAMPLE
    $outLog = Get-Process | Out-LogFile -logPath c:\temp -logName 'outlog.log' -Verbose 
    -----------------------------
    
    VERBOSE: Created c:\temp\outlog.log
    VERBOSE:
    VERBOSE: File C:\temp\outlog.log created and verified to exist.
    VERBOSE:
    VERBOSE: Adding the following information to: C:\temp\outlog.log
    VERBOSE:
    VERBOSE: -----------------------------------------------------------------
    VERBOSE: Started logging at [05/11/2016 16:58:43]
    VERBOSE: Script (Version 0.1) executed by: [thegn] on computer: [GINJA10]
    VERBOSE: -----------------------------------------------------------------
    VERBOSE:
    VERBOSE: Adding [05-11-16 16:58:43] System.Diagnostics.Process (AdobeUpdateService) to
    C:\temp\outlog.log
    VERBOSE:

.EXAMPLE
    $outLog = Get-Process | Out-LogFile -Verbose
    -----------------------------
    
    gngrNinja> more $outLog
    ...
    [05-11-16 16:56:02] System.Diagnostics.Process (winlogon)

    [05-11-16 16:56:02] System.Diagnostics.Process (WmiPrvSE)

    [05-11-16 16:56:02] System.Diagnostics.Process (WmiPrvSE)

    [05-11-16 16:56:02] System.Diagnostics.Process (WUDFHost)

    ---------------------------------------------
    Ended logging at [05/11/2016 16:56:02]
    ---------------------------------------------

gngrNinja>
#>
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $false,
                   Position  = 0)]
        [Alias('Path')]
        [string]
        $logPath,
        [Parameter(Mandatory = $false,
                   Position  = 1)]
        [Alias('Name')]
        [string]
        $logName,
        [Parameter(Mandatory = $true,
                   ValueFromPipeLine               = $true,
                   ValueFromPipelineByPropertyName = $true,
                   Position                        = 2)]
        [Alias('Value')]
        [string]
        $logValue,
        [Parameter(Mandatory = $false,
                   Position  = 3)]
        [boolean]
        $addTimeStamp = $true
    )
    
    Begin {
        
        $logFile = New-LogFile -logPath $logPath -logName $logName

    }
    
    Process {

        ForEach ($value in $logValue) {
        
            Write-LogFile -logPath $logFile -logValue $value -addTimeStamp $addTimeStamp
        
        }


    }
    
    End {
    
        Resolve-LogFile $logFile 

        Return $logFile

    }
    
}

function Send-LogEmail {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $To,
        [Parameter(Mandatory=$true)]
        [string]
        $Subject,
        [Parameter(Mandatory=$true)]
        [string]
        $Body,
        [Parameter(Mandatory=$true)]
        [string]
        $emailFrom,
        [Parameter(Mandatory=$true)]
        [string]
        $emailUser,
        [Parameter(Mandatory=$false)]
        [string]
        $provider = 'gmail',
        [Parameter(Mandatory=$true)]
        $password = (Read-Host "Password?" -AsSecureString)
    )

    if (!$to)        {Write-Error "No recipient specified";break}
    if (!$subject)   {Write-Error "No subject specified";break}
    if (!$body)      {Write-Error "No body specified";break}
    if (!$emailFrom) {$emailFrom = 'Ninja_PS_Logging@gngrninja.com'}
   
    Switch ($provider) {

        {$_ -eq 'gmail'} {
            
            $SMTPServer   = "smtp.gmail.com"
            $SMTPPort = 587

        }

        {$_ -eq 'custom'} {

            $SMTPServer = 'Your.SMTP.Server'
            $SMTPPort   = 'Your.SMTP.Server.Port'

        }

    }

    $gmailCredential = New-Object System.Management.Automation.PSCredential($emailUser,$password)
    Send-MailMessage -To $to -From $emailFrom -Body $body -BodyAsHtml:$true -Subject $Subject -SmtpServer $smtpServer -Port $smtpPort -UseSsl -Credential $gmailCredential

}

Notes:

  • I've spent some time on the help with the NinjaLogging.psm1 module
    • Feel free to browse through and use it as you wish, let me know if you have any problems!
  • I have not finished the email sending feature
  • This code will eventually find its way to Github, and be a lot more polished

Homework

  • Find an API for an application you'd like control with PowerShell
    • Study up on the documentation, and find ways to send data to it
    • Automate something using what you've learned today

[Back to top]

PowerShell: Getting Started - Utilizing the Web: Part 2 (Invoke-RestMethod)

PowerShell: Getting Started - Utilizing the Web: Part 2 (Invoke-RestMethod)

Getting Started - Utilizing the Web: Part 2 (Invoke-RestMethod)

Invoke-RestMethod

If Invoke-WebRequest had a brother, it would be Invoke-RestMethod. Well… brother, cousin, sister… you get the idea! They are related. Invoke-RestMethod allows us to send requests to REST web services (Representational State Transfer), and then returns to us a lovely object to use as we need to.

If we get an RSS feed as a response, we’ll get XML as the returned data type. When working with APIs, and using Invoke-RestMethod, you’ll get a custom object back. This is because more times than not we'll get the information back as JSON (JavaScript Object Notation). PowerShell automatically converts that into the custom object, which you can then dive into the properties of.

Here are the different methods we can send:

  • Default
  • Delete
  • Get
  • Head
  • Merge
  • Options
  • Patch
  • Post
  • Put
  • Trace

 

Whether we’re working with an RSS feed, or an API of sorts, Invoke-RestMethod allows us to do so with ease. After learning how to use it, it has quickly become one of my favorite commands in PowerShell.

Using Invoke-RestMethod

Let's go into some examples of how to use Invoke-RestMethod. To start out, we will use it to get some information via RSS, and then dive a little deeper into APIs.

RSS Feeds

Invoke-RestMethod can be used to gather information from RSS feeds. For this example, we'll get the feed for http://www.reddit.com/r/powershell. To do that, we simply append .rss to the URL.

Here's the command:

$redditRSS = Invoke-RestMethod 'http://www.reddit.com/r/PowerShell.rss'

Let's pipe our variable to Get-Member, and see what's up.

$redditRSS | Get-Member

It looks like we indeed have an XML object, and its associated methods/properties. 

Let's see what the data looks like!

$redditRSS

The object we have here has a lot of properties that we can dive into. 

For instance, let's say we want the author information for the first element in the array...

$redditRSS[0].author

Under the author property there are more properties that contain the information we're looking for. To access those, we need to drill down to them.

$redditRSS[0].author.name
$redditRSS[0].author.name.uri

This is part of the discovery you'll want to do when you receive an object back after using Invoke-RestMethod. Dig around, check documentation from the website if there is any, and discover all that you can. There is a lot of information returned, and the more you know, the more things you can do!

Let's dig into the content of the first array element.

$redditRSS[0].content

It looks like we want the '#text' property to see the actual returned content.

$redditRSS[0].content.'#text'

The content we received is in HTML. With some parsing we could make it more readable in the console.

Here's one way to clean it up, by stripping out the HTML tags:

$redditRSS[0].content.'#text' | ForEach-Object { $_ -replace '<[^>]+>','' }

With this object ($redditRSS[0]), we can also get the title and URL to the actual post.

$redditRSS[0].title
$redditRSS[0].link.href

The link value was buried in the href property under link.

What can we do with this information? Well... whatever you need/want to! That's the awesome part about PowerShell. If you need it to do something, there's a way to do it! We could setup a script that watches for a certain post with a specific title, and then have it email you or send you a text. 

You can even make a make-shift post explorer, wherein you can check out the posts and comments of a specific subreddit. I have some example code for doing that as the conclusion to this section.

Code for subreddit exploration:

function Invoke-RedditBrowsing { #Begin function Invoke-RedditBrowsing
    [cmdletbinding()]
    Param(
        [Parameter(Mandatory)]
        [String]
        $subReddit
    )

    #Get the RSS feed for the subreddit requested
    $redditRSS = Invoke-RestMethod "http://www.reddit.com/r/$subReddit.rss"

    #This is a hashtable for the text we'll replace in order to make the comments more readable
    $textToReplace = [hashtable]@{

        '<[^>]+>' = ''
        '&quot;'  = '"'
        '&#39;'   = "'"
        '&#32;'   = ' '

    }

    #Make sure there is content before we proceed
    if ($redditRSS) { #Begin if for $redditRSS existence

        #Use the evil Write-Host to display information on what to do next
        Write-Host 'Select a [#] from the options below!' `n -ForegroundColor Black -BackgroundColor Green

        #Set $i to 0 so our loop iterates and displays $i correctly
        $i = 0 

        #ForEach $post in the subReddit's feed...
        ForEach ($post in $redditRSS) {

            #Use evil Write-Host to display the current value of $i and the title, then the author's name and post updated date
            Write-Host "[$i] -> $($post.Title)"
            Write-Host `t"By: [$($post.author.name)], Updated [$($post.updated)]"`n`n

            #Iterate $i by 1
            $i++

        }

        #Write-Host (new line) to make the next line look prettier
        Write-Host `n

        #Try / Catch to make sure we can convert the input to an integer. If not, we error out
        Try {
        
            #Ask for the post selection so we can proceed
            [int]$selection = Read-Host 'Which [#]? (any invalid option quits)' -ErrorAction SilentlyContinue

        }

        Catch {

            #If we can't make it an int... quit!
            Write-Host "Invalid option, quitting!" -ForegroundColor Red -BackgroundColor DarkBlue

            Break

        }
        
        #Switch statement for $selection. This makes sure it is in bounds for the array of posts.
        Switch ($selection) { #Begin Switch for the $selection value

            #if $i is less than or equal to the total number of values in the array $redditRSS...
            {$_ -le ($i - 1)} { #Begin option for valid selection

                #Get the comments RSS feed from the link provided
                $redditComments = Invoke-RestMethod -Uri "$($redditRSS[$_].link.href).rss"
                
                #Write-Host the title we'll be viewing comments for
                Write-Host `n
                Write-Host "Title: [$($redditRSS[$_].Title)]" -ForegroundColor Black -BackgroundColor Green

                #ForEach comment in the comments feed
                ForEach ($comment in $redditComments) {

                    #Null out anything set in $commentText
                    $commentText = $null

                    #Set the comment text as the property which contains the actual comment
                    $commentText = $comment.Content.'#text'

                    #Go through each key in the hashtable we created earlier
                    ForEach ($text in $textToReplace.Keys) {
            
                        #Re-create the $commentText variable while replace the key ($text), with the value ($textToReplace.$text)
                        #For example: it will match '<[^>]+>', then replace it with ''
                        $commentText = $commentText -replace ($text,$textToReplace.$text) 

                    }

                    #Use Write-Host to write out the author/comment text we cleaned up
                    Write-Host `n`n
                    Write-Host "By: [$($comment.author.name)]" -ForegroundColor Black -BackgroundColor Green
                    Write-Host `t"Comment: [$commentText]"

                }

            } #End option for valid selection

            #If the number does not match a value that is valid, quit!
            Default {

                Write-Host "Invalid option, quitting!" -ForegroundColor Red -BackgroundColor DarkBlue
        
                Break

            }

        } #End Switch for the $selection value

    #If there is nothing in $redditRSS... quit!
    } else {

        Write-Host `n"Unable to get RSS feed for [$subReddit]." -ForegroundColor Red -BackgroundColor DarkBlue

    } #End if $redditRSS

} #End function Invoke-RedditBrowsing

Invoke-RedditBrowsing

Now to run it! It will prompt for the subreddit, which can be any subreddit. I will go with PowerShell.

browse1.PNG

The code will then run, and get the list of posts:

I will choose option [0], which is the first post.

There you have it, one example of what you can do with the data returned via Invoke-RestMethod. The primary motivation for doing something like this, at least for me, is that I like to visualize the data. 

Using Write-Host is not suitable for automation, as it will not be seen by anyone. There are better options as well when you want to make specific things verbose, and others specifically for debug / errors. So why use it? Well, when visualizing the data you have available, it really can help certain bits of text stand out. 

With a visual representation of what's available, it can lead to even more discovery and experimenting, which can in turn lead to more script ideas and ways to handle the data later.

Invoke-RestMethod and APIs

Invoke-RestMethod allows us to work with REST APIs with relative ease. When working with APIs, the most important thing is to familiarize yourself with their documentation. I'll link to the documentation for the different APIs I'll be using here.

Example 1: No Authentication

For this example we will use Weather Underground's Autocomplete API. This API returns a list of locations that best match the input it receives. It can be used in tandem with their full API to get the weather information.

Let's try the following:

$city          = 'Chicago'

$lookupResults = Invoke-RestMethod -Uri "http://autocomplete.wunderground.com/aq?query=$city"

$lookupResults

Looks like we have an object we can explore now! 

$lookupResults | Get-Member

It is a custom object with one property (RESULTS). Let's check it out.

$lookupResults

Quite a few results! We can access the first result by using $lookupResults.RESULTS[0].

The 'l' property is something we will actually use later when getting a forecast for Chicago.

Let's wrap this up in a nice little function.

function Get-WeatherCity { #Begin function Get-WeatherCity
    [cmdletbinding()]
    param(
        [parameter(Mandatory=$true)]
        [string]
        $city
    )

    Try {
        
        $lookupResults = Invoke-RestMethod -Uri "http://autocomplete.wunderground.com/aq?query=$city"

    }

    Catch {
        
        $errorMessage = $_.Exception.Message

        Write-Host "Error looking up [$city]: [$errorMessage]"

        Return

    }

    if ($lookupResults.results) {

        Return $lookupResults.Results[0]

    } else {

        Write-Host "Unable to find a result for: [$city]" -ForegroundColor Red -BackgroundColor DarkBlue

    }
        
} #End function Get-WeatherCity

Now we can use (with that function in memory): 

Get-WeatherCity -city Chicago

Example 2: API With a Key

For this example I will use Weather Underground's free API (for developers). To sign up for a key, go here. For the API's documentation, go here.

We'll use the following base code along side this example: 

function Get-WeatherCity { #Begin function Get-WeatherCity
    [cmdletbinding()]
    param(
        [parameter(Mandatory=$true)]
        [string]
        $city
    )

    Try {
        
        $lookupResults = Invoke-RestMethod -Uri "http://autocomplete.wunderground.com/aq?query=$city"

    }

    Catch {
        
        $errorMessage = $_.Exception.Message

        Write-Host "Error looking up [$city]: [$errorMessage]"

        Return

    }

    if ($lookupResults.results) {

        Return $lookupResults.Results[0]

    } else {

        Write-Host "Unable to find a result for: [$city]" -ForegroundColor Red -BackgroundColor DarkBlue

    }
        
} #End function Get-WeatherCity

With that function in memory, let's get a returned object for Chicago.

$cityInfo = Get-WeatherCity Chicago

Now for the setup to use the API to get weather information.

This code is one time code to keep my key secure so somebody reading the script can't see it in plain text. 

To do this I will:

  • Set the API key as a secure string in a variable
  • Create a credential object to store the key
  • Export that credential object via Export-Clixml
$apiKey          = 'EnterYourKeyHere' | ConvertTo-SecureString -AsPlainText -Force
$apiCredential   = New-Object System.Management.Automation.PSCredential('apiKey',$apiKey)
$apiCredential | Export-Clixml .\apiCred.xml

With that one time setup out of the way, you can then use:

$apiKey = (Import-Clixml .\apiCred.xml).GetNetworkCredential().Password

The key will be available to the script (as plain text), but it is stored in the $apiKey variable.

With all the setup done, here's the block of code we will be running:

$apiKey          = (Import-Clixml .\apiCred.xml).GetNetworkCredential().Password
$baseURL         = 'http://api.wunderground.com/api/'
$cityInfo        = Get-WeatherCity Chicago
$fullURL         = $baseURL + $apiKey + '/features/conditions/hourly/forecast/webcams/astronomy/alerts' + "$($cityInfo.l).json"
$weatherForecast = Invoke-RestMethod -Uri $fullURL

With the above code executed, $weatherForecast will now contain a plethora of data for us to work with.

$weatherForecast | Get-Member

This is one rich object! For alerts, you can use: $weatherForecast.alerts.

You can do a lot with the information available. I wrote a script that uses it to email a forecast (just the way we want it) to my significant other and myself. I took a parts of the script, and heavily modified them to fit this example. In the following example we will:

  • Setup the API information needed
  • Set a limit on use during the script's execution to 10
  • Declare 3 cities we want the forecast for
  • Create a PowerShell class (PowerShell version 5 only!)
    • This class will have a method that formats the forecast in HTML
  • Lookup the city information
  • Pass the city information to a function that gathers the API data
  • Create an object using the class
  • Loop through the cities, get the information, update the object we created with the class, and export the HTML forecast after using the object's method to create it

Here's the code:

$apiKey          = (Import-Clixml .\apiCred.xml).GetNetworkCredential().Password
$baseURL         = 'http://api.wunderground.com/api/'
$cityInfo        = Get-WeatherCity Chicago
$fullURL         = $baseURL + $apiKey + '/features/conditions/hourly/forecast/webcams/astronomy/alerts' + "$($cityInfo.l).json"
$weatherForecast = Invoke-RestMethod -Uri $fullURL
$citiesToGet     = ('Chicago','San Francisco','Portland')
$script:apiLimit = 10

Class Weather { #Begin Weather Class

    [pscustomobject]$apiData

    [string[]]formatForecastHTML() { #Begin method formatForecastHTML

        [System.Collections.ArrayList]$alertText    = @()
        [System.Collections.ArrayList]$forecastText = @() 
        $phases                                     = $null
        $todayForecast                              = $this.apiData.forecast.simpleforecast.forecastday
        $city                                       = $this.apiData.current_observation.display_location.full
        $selCam                                     = Get-Random $this.apiData.webcams.count
        $camImg                                     = $this.apiData.webcams[$selCam].CURRENTIMAGEURL
        $camName                                    = $this.apiData.webcams[$selCam].linktext
        $camLink                                    = $this.apiData.webcams[$selCam].link
        $curAlerts                                  = $this.apiData.alerts 
            
        if ($curAlerts) {

        foreach ($alert in $CurAlerts) {
            $typeName   = $null
            $typeName   = Get-WeatherInfo -Weather 'alert' -value $alert.type

            $alertText.Add(@"
<p><b><font color=`"red`">Weather Alert! ($typeName)</font></b></p>
<p>Date: $($alert.date) Expires: $($alert.Expires)</p>
<p>$($alert.Message)</p>
"@) | Out-Null              
                            
        } 

        }    
        
        $phases = @"
<p><b>Sun Information</b></p><p></p>
<p>Sunrise: $($this.apiData.sun_phase.sunrise.hour):$($this.apiData.sun_phase.sunrise.minute)</p>
<p>Sunset: $($this.apiData.sun_phase.sunset.hour):$($this.apiData.sun_phase.sunset.minute)</p>
<p></p>
<p><b>Moon Information</b></p><p></p>
<p>Moonrise: $($this.apiData.moon_phase.moonrise.hour):$($this.apiData.moon_phase.moonrise.minute)</p>
<p>Moonset: $($this.apiData.moon_phase.moonset.hour):$($this.apiData.moon_phase.moonset.minute)</p>
<p>Age: $($this.apiData.moon_phase.ageOfMoon) Days</p>
<p>Phase: $($this.apiData.moon_phase.phaseofMoon)</p>
<p>Illumination: $($this.apiData.moon_phase.percentIlluminated)%</p>
"@

        foreach ($day in $todayForecast) {
   
            $dayImg          = $day.icon_url
            $dayMonth        = $day.date.monthname
            $dayDay          = $day.date.day
            $dayName         = $day.date.weekday
            $dayHigh         = $day.high.fahrenheit  
            $dayLow          = $day.low.fahrenheit
            $maxWind         = $day.maxwind.mph
            $aveWind         = $day.avewind.mph
            $aveHum          = $day.avehumidity
            $conditions      = $day.conditions
            [int]$dayPrecip  = $day.pop
                
            $popText = Get-WeatherInfo -Weather 'preciptext' -value $dayPrecip
            
            $forecastText.Add(@"
<p></p>
<p></p>
<p><b>$dayName, $dayMonth $dayDay</b></p>
<p><img src=`"$dayImg`">$conditions</p>
<p>Chance of precipitation: $dayPrecip% / $popText</p>
<p>High: $dayHigh`F Low: $dayLow`F</p>
<p>Ave Winds: $aveWind`mph Max Winds: $maxWind`mph</p>
<p>Humidity: $aveHum%</p>
"@) | Out-Null                           

        }

        $body = @"
<p></p>
<p>Here is your 4 day forecast!</p>
<p>Random webcam shot from: <a href=`"$camLink`">$camName</a></p>
<p><img src=`"$camImg`"></p>
$($phases       | Out-String)
$($alertText    | Out-String)
$($forecastText | Out-String)
"@

        Return $body 

    } #End method formatForecastHTML

    [string[]]formatHourlyForecastHTML() { #Begin Class format4DayForecast

        [System.Collections.ArrayList]$alertText    = @()
        [System.Collections.ArrayList]$forecastText = @() 
        $phases                                     = $null
        $todayForecast                              = $this.apiData.forecast.simpleforecast.forecastday
        $city                                       = $this.apiData.current_observation.display_location.full
        $selCam                                     = Get-Random $this.apiData.webcams.count
        $camImg                                     = $this.apiData.webcams[$selCam].CURRENTIMAGEURL
        $camName                                    = $this.apiData.webcams[$selCam].linktext
        $camLink                                    = $this.apiData.webcams[$selCam].link
        $curAlerts                                  = $this.apiData.alerts 
        $hourlyForecast                             = $this.apiData.hourly_forecast
            
        if ($curAlerts) {

            foreach ($alert in $CurAlerts) {
            
                $typeName   = $null
                $typeName   = Get-WeatherInfo -Weather 'alert' -value $alert.type
            
                $alertText.Add(
@"
<p><b><font color=`"red`">Weather Alert! ($typeName)</font></b></p>
<p>Date: $($alert.date) Expires: $($alert.Expires)</p>
<p>$($alert.Message)</p>
"@) | Out-Null              
                            
            } 

        }  
         
        $phases = @"
<p><b>Sun Information</b></p><p></p>
<p>Sunrise: $($this.apiData.sun_phase.sunrise.hour):$($this.apiData.sun_phase.sunrise.minute)</p>
<p>Sunset: $($this.apiData.sun_phase.sunset.hour):$($this.apiData.sun_phase.sunset.minute)</p>
<p></p>
<p><b>Moon Information</b></p><p></p>
<p>Moonrise: $($this.apiData.moon_phase.moonrise.hour):$($this.apiData.moon_phase.moonrise.minute)</p>
<p>Moonset: $($this.apiData.moon_phase.moonset.hour):$($this.apiData.moon_phase.moonset.minute)</p>
<p>Age: $($this.apiData.moon_phase.ageOfMoon) Days</p>
<p>Phase: $($this.apiData.moon_phase.phaseofMoon)</p>
<p>Illumination: $($this.apiData.moon_phase.percentIlluminated)%</p>

"@            
        foreach ($hour in $hourlyForecast) {

            $prettyTime       = $hour.fcttime.pretty
            $hourTemp         = $hour.temp.english  
            $hourImg          = $hour.icon_url
            $hourChill        = $hour.windchill.english
            if ($hourChill -eq -9999) {
                
                $hourChill = 'N/A'
                    
            } else {
                    
                $hourChill = $hourChill + 'F'
                
            }

            $hourWind         = $hour.wspd.english
            $windDir          = $hour.wdir.dir
            $hourUV           = $hour.uvi
            $dewPoint         = $hour.dewpoint.english
            $hourFeels        = $hour.feelslike.english
            $hourHum          = $hour.humidity
            $conditions       = $hour.condition
            [int]$hourPrecip  = $hour.pop
                
            $popText = Get-WeatherInfo -Weather 'preciptext' -value $hourPrecip
            $forecastText.Add(
@"
<p></p>
<p></p>
<p><b>$prettyTime</b></p>
<p><img src=`"$hourImg`">$conditions</p>
<p>Chance of precipitation: $hourPrecip% / $popText</p>
<p>Current Temp: $hourTemp`F Wind Chill: $hourChill Feels Like: $hourFeels`F</p>
<p>Dew Point: $dewPoint</p>
<p>Wind Speed: $hourWind`mph Direction: $windDir</p>
<p>Humidity: $hourHum%</p>
<p>UV Index: $hourUV     
"@) | Out-Null                

        }

        $body = 
@"
<p></p>
<p>Here is your hourly forecast!</p>
<p>Random webcam shot from: <a href=`"$camLink`">$camName</a></p>
<p><img src=`"$camImg`"></p>
<p>$city Radar:</p>
<p><img src=`"$($this.radarURL)`"></p>
$($phases       | Out-String)
$($alertText    | Out-String)
$($forecastText | Out-String)
"@
        return $body 

    } #End method formatHourlyForecastHTML

    Weather([String]$apiURL, [String]$radarURL, [PSCustomObject]$apiData) {
        
        $this.radarURL = $radarURL
        $this.apiData  = $apiData

    }

    Weather() {}

} #End class Weather

function Get-WeatherCity { #Begin function Get-WeatherCity
    [cmdletbinding()]
    param(
        [parameter(Mandatory=$true)]
        [string]
        $city
    )

    Try {
        
        $lookupResults = Invoke-RestMethod -Uri "http://autocomplete.wunderground.com/aq?query=$city"

    }

    Catch {
        
        $errorMessage = $_.Exception.Message

        Write-Host "Error looking up [$city]: [$errorMessage]"

        Return

    }

    if ($lookupResults.results) {

        Return $lookupResults.Results[0]

    } else {

        Write-Host "Unable to find a result for: [$city]" -ForegroundColor Red -BackgroundColor DarkBlue

    }
        
} #End function Get-WeatherCity

function Get-WeatherAPIData { #Begin function Get-WeatherAPIData
    [cmdletbinding()]
    param(
        [parameter(Mandatory)]
        [PSCustomObject]
        $city
    )
    
    [System.Collections.ArrayList]$weatherCity = @()
    $weatherProperty                           = $null
    $weatherO                                  = $null

    $fullURL  = $baseURL + $apiKey + '/features/conditions/hourly/forecast/webcams/astronomy/alerts' + "$($city.l).json"
    $radarURL = "http://api.wunderground.com/api/$apiKey/animatedradar/animatedsatellite" + "$($city.l).gif?num=6&delay=50&interval=30"

    Try {

        if ($script:apiCount -le $apiLimit) {

            Write-Host "Attempting to get API information for: [$($city.Name)]."  
                  
            $weatherForecast = Invoke-RestMethod -Uri $fullURL 
                
            $script:apiCount++

            $weatherForecast | Export-Clixml ("$outputDir\$($city.Name)APIData{0:MM-dd-yy_HHmm}.clixml" -f (Get-Date)) 

            Write-Host "The API has been used [$script:apiCount] times, against the limit of [$apiLimit]"

        } else {

            Write-Host "The API limit of [$apiLimit] has been reached!" -ForegroundColor Red -BackgroundColor DarkBlue

            Break

        }

    }

    Catch {

        $errorMessager = $_.Exception.Message 

        Write-Host "Unabled to get API data from Weather Underground: [$($_.Exception.Message)]." -ForegroundColor Red -BackgroundColor DarkBlue

    }

    if ($weatherForecast.forecast) {

        Write-Host "API information found for: [$($city.Name)]"

        $weatherO = [PSCustomObject]@{

            APIData  = $weatherForecast
            RadarURL = $radarURL 

        }

        $weatherCity.Add($weatherO) | Out-Null

        Return $weatherCity

    } else {

        Write-Host "API information not found for: [$($city.Name)]" -ForegroundColor Red -BackgroundColor DarkBlue

    }
  
} #End function Get-WeatherAPIData

function Get-WeatherInfo { #Begin function Get-WeatherInfo
    [cmdletbinding()]
    param(
        [string]
        $weather,
        $value       
    )
    
    Switch ($weather) {
        
        {$_ -eq 'preciptext'} {
        
            Switch ($value) {
                
                {$_ -lt 20} {
                    
                    $popText = 'No mention'
                    
                }
                
                {$_ -eq 20} {
                    
                    $popText = 'Slight Chance'
                    
                }
                
                {($_ -lt 50 -and $_ -gt 20)} {
                    
                    $popText = 'Chance'
                    
                }
                
                {$_ -eq 50} {
                    
                    $popText = 'Good chance'
                    
                }
                
                {($_ -lt 70 -and $_ -gt 50)} {
                    
                    $popText = 'Likely'
                    
                }
                
                {$_ -ge 70} {
                    
                    $popText = 'Extremely likely'
                    
                }
                
            }
            
            Return $popText
            
        }   
        
        {$_ -eq 'alert'}    {
            
            Switch ($value) {
                
                'HEA' {$typeName = 'Heat Advisory'}
                'TOR' {$typeName = 'Tornado Warning'}
                'TOW' {$typeName = 'Tornado Watch'}
                'WRN' {$typeName = 'Severe Thunderstorm Warning'}
                'SEW' {$typeName = 'Severe Thunderstorm Watch'}
                'WIN' {$typeName = 'Winter Weather Advisory'}
                'FLO' {$typeName = 'Flood Warning'}
                'WAT' {$typeName = 'Flood Watch / Statement'}
                'WND' {$typeName = 'High Wind Advisory'}
                'SVR' {$typeName = 'Severe Weather Statement'}
                'HEA' {$typeName = 'Heat Advisory'}
                'FOG' {$typeName = 'Dense Fog Advisory'}
                'SPE' {$typeName = 'Special Weather Statement'}
                'FIR' {$typeName = 'Fire Weather Advisory'}
                'VOL' {$typeName = 'Volcanic Activity Statement'}
                'HWW' {$typeName = 'High Wind Warning'}
                'REC' {$typeName = 'Record Set'}
                'REP' {$typeName = 'Public Reports'}
                'PUB' {$typeName = 'Public Information Statement'}
                    
            }
                
            Return $typeName
    
        }    
    }
    
} #End function Get-WeatherInfo

$weatherObject = [Weather]::New()

Write-Host `n`n

ForEach ($cityName in $citiesToGet) {

    $findCity    = Get-WeatherCity -city $cityName
    $weatherCity = Get-WeatherAPIData -city $findCity -ErrorAction Stop    

    if ($weatherCity) {

        $weatherObject.apiData  = $null
        $weatherObject.apiData  = $weatherCity.APIData

        $foreCastFile = $null
        $foreCastFile = ".\$cityName.html"

        Write-Host `n"Exporting HTML formatted forecast to [$foreCastFile]."`n

        $weatherObject.formatHourlyForecastHTML() | Out-File ".\$cityName.html"

    } else {

        Write-Host "No data in [`$weatherCity] variable, halting execution on [$($userEmail.FirstName) $($userEmail.LastName)]." -ForegroundColor Red -BackgroundColor DarkBlue
                
        Break

    }

}

Write-Host `n`n

There should be 3 files in the folder now.

There they are. Let's take a look at Chicago's forecast.

You can do anything you put your mind to with the data received.

Homework

  • Explore the class created here to see how the information in the HTML generated forecast is created.
    • A lot of this information is transferable to functions and other methods of creating the HTML formatted forecast if you do not have PowerShell version 5.
  • See if a web service you use often has an API, read up on the documentation, and see if there's something you can automate doing with PowerShell!

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Back to top]

PowerShell: Getting Started - Utilizing the Web: Part 1 (Invoke-WebRequest)

PowerShell: Getting Started - Utilizing the Web: Part 1 (Invoke-WebRequest)

Getting Started - Utilizing the Web: Part 1 (Invoke-WebRequest)

Welcome to my Getting Started with Windows PowerShell series!

Why Work With the Web in PowerShell?

PowerShell can do quite a bit with web sites, and web services. 

Some of what we can do includes:

  • Checking if a website is up
  • Downloading files
  • Logging in to webpages
  • Parsing the content we receive
  • Utilizing REST methods

Learning how to use web services with PowerShell can expand your realm of possibility when scripting.

Using Invoke-WebRequest

Invoke-WebRequest is a command that allows us to retrieve content from web pages. The methods supported when using Invoke-WebRequest are:

  • Trace
  • Put
  • Post
  • Options
  • Patch
  • Merge
  • Delete
  • Default
  • Head
  • Get (Default)

Let's take a look at some different ways to utilize Invoke-WebRequest

Downloading a File

Let's download a file! In this particular example, we will download an addon for World of Warcraft.

The page we'll be looking at is http://www.tukui.org/dl.php.

In particular, let's get the ElvUI download.

PowerShell setup:

$downloadURL     = 'http://www.tukui.org/dl.php'
$downloadRequest = Invoke-WebRequest -Uri $downloadURL

Here I set the $downloadURL variable to store the main page we want to go to, and then use $downloadRequest to store the results of Invoke-WebRequest (using the $downloadURL as the URI).

Let's take a look at what's in $downloadRequest.

We'll go over some of the other properties later, and focus on Links for now. The Links property returns all the links found on the web site. $downloadRequest.Links

The list continues, but you get the idea. This is one of my favorite things about the Invoke-WebRequest command. The way it returns links is very easy to parse through.

Let's hunt for the ElvUI download link. To do that we'll pipe $downloadRequest.Links to Where-Object, and look for links that contain a phrase like Elv and Download.

$downloadRequest.Links | Where-Object {$_ -like '*elv*' -and $_ -like '*download*'}

Got it! Now let's store that href property in a variable.

$elvLink         = ($downloadRequest.Links | Where-Object {$_ -like '*elv*' -and $_ -like '*download*'}).href

That variable will now contain the download link.

Next we'll use Invoke-WebRequest again to download the file. There are two ways we can get the file:

  • Using Invoke-WebRequest to store the results in a variable,  and then write all the bytes to a file using the Contents property (which is a byte array).
  • Using Invoke-WebRequest with the -OutFile parameter set as the full path of the download. With this option we'll want to use -PassThru so we can still get the results from Invoke-WebRequest (otherwise success = empty result/no object returned).

Using the Contents property and writing the bytes out

$fileName        = $elvLink.Substring($elvLink.LastIndexOf('/')+1)
$downloadRequest = Invoke-WebRequest -Uri $elvLink 
$fileContents    = $downloadRequest.Content

The above code takes the link we stored, and gets the file name from it using LastIndexOf, and SubString.

It then stores the download request results in $downloadRequest

Finally, we get the contents (which is a byte array, if all went well), and store that in $fileContents.

The last thing we'll need to do is write the bytes out. To do that we'll use [io.file]WriteAllBytes(path,contents) to write the byte array to a file.

[io.file]::WriteAllBytes("c:\download\$fileName",$fileContents)

Let's run the code now, and see what happens!

Now we should have a file in "C:\download\"...

There it is! 

The $downloadRequest variable stores the results from the request, which you can use to validate accordingly. 

Using Invoke-WebRequest with -OutFile

Another way to download the file would be to use the -OutFile parameter with Invoke-WebRequest. We'll want to set the filename first:

$fileName        = $elvLink.Substring($elvLink.LastIndexOf('/')+1)

This is the same code from the previous example, and it uses LastIndexOf, and SubString.

Here's the code to download the file:

$downloadRequest  =  Invoke-WebRequest -Uri $elvLink -OutFile "C:\download\$fileName" -PassThru

Note that we used -PassThru as well. That is so we can still see the results of the request in the variable $downloadRequest. Otherwise a successful result would return no object, and your variable would be empty.

Let's see if that worked!

It did, and it was a bit easier than the previous example.

Downloading Files With a Redirect

Let's take a look at downloading files from sites that have a redirect. For this example I will use downloading WinPython from https://sourceforge.net/projects/winpython/files/latest/download?source=frontpage&position=4. Note: Invoke-WebRequest is typically great at following redirected links. This example is here to show you how to retrieve redirected links, as well as a cleaner way to get the file.

Here is what a typical page that has a redirected download looks like:

Invoke-WebRequest includes a parameter that will force the maximum number of times it will accept redirection (-MaximumRedirection). We'll set that to 0, which will error it out, and use -ErrorAction SilentlyContinue to allow the error to be ignored. I will also use the parameter -UserAgent to send along the user agent string for FireFox. If we do not do this, the download will not work on this website.

Here's the initial code:

I set the URL we want to download from and store it in $downloadURL

$downloadURL     = 'https://sourceforge.net/projects/winpython/files/latest/download?source=frontpage&position=4'

Then I store the result of our Invoke-WebRequest command in $downloadRequest.

$downloadRequest = Invoke-WebRequest -Uri $downloadURL -MaximumRedirection 0 -UserAgent [Microsoft.PowerShell.Commands.PSUserAgent]::FireFox -ErrorAction SilentlyContinue

Let's take a closer look at the Invoke-WebRequest line.

  • -MaximumRedirection 0
    • This ensures we do not get automatically redirected. Setting it to 0 forces no re-direction, and allows us to manually collect the redirection data.
  • -ErrorAction SilentlyContinue
    • This tells PowerShell to ignore the redirection error message. The downside to this is that it will be hard to capture any other errors. This is the only way I was able to find that keeps information in the variable $downloadRequest.
  • -UserAgent [Microsoft.PowerShell.Commands.PSUserAgent]::FireFox
    • This sends the user agent string for FireFox along in the request. We can see what [Microsoft.PowerShell.Commands.PSUserAgent]::FireFox resolves to by simply typing(or pasting) it in the console.

Now we have some helpful information in our $downloadRequest variable, assuming all went well.

$downloadRequest.StatusDescription should be "Found".

Good! The redirect link is stored in the header information, accessible via $downloadRequest.Headers.Location.

I did some digging in the Content property, and found the string that matches the file name. I then added some code for the $fileName variable that looks for a string that matches the file name, and selects the matched value.

$fileName        = (Select-String -Pattern 'WinPython-.+exe' -InputObject $downloadRequest.Content -AllMatches).Matches.Value

Now that we have this information, we're ready to continue! I used a couple Switch statements to add some logic, in case the responses aren't what we expected.

Here's the full code for this example:

$downloadURL     = 'https://sourceforge.net/projects/winpython/files/latest/download?source=frontpage&position=4'
$downloadRequest = Invoke-WebRequest -Uri $downloadURL -MaximumRedirection 0 -UserAgent [Microsoft.PowerShell.Commands.PSUserAgent]::FireFox -ErrorAction SilentlyContinue
$fileName        = (Select-String -Pattern 'WinPython-.+exe' -InputObject $downloadRequest.Content -AllMatches).Matches.Value

Switch ($downloadRequest.StatusDescription) {

    'Found' {
        
        Write-Host "Status Description is [Found], downloading from redirect URL [$($downloadRequest.Headers.Location)]."`n
        $downloadRequest = Invoke-WebRequest -Uri $downloadRequest.Headers.Location -UserAgent [Microsoft.PowerShell.Commands.PSUserAgent]::FireFox 

    }

    Default {

        Write-Host "Status Descrption is not [Found], received: [$($downloadRequest.StatusDescription)]. Perhaps something went wrong?" -ForegroundColor Red -BackgroundColor DarkBlue

    }

}

Switch ($downloadRequest.BaseResponse.ContentType) {

    'application/octet-stream' {

        Write-Host 'Application content type found!'`n
        Write-Host "Writing [$fileName] to [C:\download]"`n

        [io.file]::WriteAllBytes("c:\download\$fileName",$downloadRequest.Content)

    }

    Default {

        Write-Host "Content type is not an application! Not extracting file." -ForegroundColor Red -BackgroundColor DarkBlue

    }

}

The first Switch statement ensures that the StatusDescription is "Found", then sets $downloadRequest as the result of the Invoke-WebRequest command that now points to the redirect URL. If the StatusDescription is not found, you'll see a message stating that something went wrong.

We then use a Switch statement that ensures our downloaded content (in $downloadRequest) has the Content Type of "application/octet-stream". If it is, we write the file out using [io.file]WriteAllBytes(path,contents).

Let's run the code, and then look in "C:\download\" to verify the results!

While it downloads, this progress indicator is displayed (Sometimes it will not match the actual progress):

Looks like everything worked! One last place to check.

We got it. All 277MB downloaded and written to the appropriate location.

Parsing Content

Using Invoke-WebRequest, the content of the request is returned to us in the object. There are many ways to go through the data. In this example I will demonstrate gathering the titles and their associated links from the PowerShell subreddit.

Here's the setup:

$parseURL    = 'http://www.reddit.com/r/powershell'
$webRequest  = Invoke-WebRequest -Uri $parseURL

Now let's take a look at the $webRequest variable.

  • RawContent
    • This is the content returned as it was received, including header data. 
  • Forms
    • This property contains any discovered forms. We'll go over this portion in more detail when we log in to a website.
  • Headers
    • This property contains just the returned header information.
  • Images
    • This property contains any images that were able to be discovered.
  • InputFields
    • This property returns discovered input fields on the website.
  • Links
    • This returns the links found on the website, in an easy format to iterate through.
  • ParsedHTML
    • This property allows you to access the DOM of the web site. DOM is short for Document Object Model. Think of DOM as a structured representation of the data on the website.

As always, if you want to see what other properties were returned, and any methods available, pipe $webRequest to Get-Member.

As you can see there are a few more properties that exist, but we'll be focusing on the ones described above in this article.

Now to get the title text from the current posts at http://www.reddit.com/r/powershell.

The fastest way to narrow it down, is to launch a browser and take a look at the DOM explorer. In Edge I used [F12] to launch the developer tools, and then used the [Select Element] option in the [DOM Explorer] tab. I then selected one of the posts to see what it looked like. 

It looks like the link is under a class named title, and the tag <p>. 

Let's use the ParsedHTML property to access the DOM, and look for all instances of <p> where the class is title.

$webRequest.ParsedHTML.getElementsByTagName('p') | Where-Object {$_.ClassName -eq 'title'}

The results should look similar to this (quite a lot of text will scroll by):

To verify if this is just the title information, let's pipe the above command to | Select-Object -ExpandProperty OuterText

$webRequest.ParsedHTML.getElementsByTagName('p') | Where-Object {$_.ClassName -eq 'title'} | Select-Object -ExpandProperty OuterText

Awesome, looks like what we want to see!

Let's store all those titles (minus the (text)) at the end, in $titles.

The biggest problem I encountered was just getting the title names (minus the text after at the very end such as: (self.PowerShell)), while also not omitting results that had (text) in other places. Here is the solution I came up with to store all the post titles in the variable $titles.

$titles      = $webRequest.ParsedHTML.getElementsByTagName('p') | 
               Where-Object {$_.ClassName -eq 'title'}          |                    
               ForEach-Object {
               
                    $splitTitle = $null
                    $splitCount = $null
                    $fixedTitle = $null

                    $splitTitle = $_.OuterText.Split(' ')
                    $splitCount = $splitTitle.Count

                    $splitTitle[($splitCount - 1)] = $null

                    $fixedTitle = ($splitTitle -join ' ').Trim()

                    Return $fixedTitle

               }

In the above command, I piped our title results to ForEach-Object, and then used some string manipulation to split the title into an array, null out the last entry in the array, join the array back, and finally trim it so there is no extra white space.

Now let's take a look at our $titles variable.

Perfect! The next step is matching the titles up with the property Links in our $webRequest variable. Remember that $webRequest.Links contains all the links on the web site.

After some digging, I found that the link property of outerText matches the titles in our $titles variable. Now we can iterate through all the titles in $titles, find the links that match, and create a custom object to store an index, title, and link.

We will need to do some more string manipulation to get the link out of the outerHTML property. It's all doable, though! 

Finally, we'll store the custom object in an array of custom objects. 

Here is the full code (with comments to explain what is happening):

#Ensure i is set to 0 so it iterates properly in the loop below
$i = 0
#Create the .Net ArrayList object to store our custom objects
[System.Collections.ArrayList]$prettyLinks = @()
#Iterate through all the titles
ForEach ($title in $titles) {

    #Search through the Links property of $webRequest and take action where the outerText equals the title
    #For each of the found requests (each link that matches)...
    $webRequest.Links  | Where-Object {$_.outerText -eq $title} | ForEach-Object {
    
    #Null out variables used in this loop
    $linkObject = $null
    $linkURL    = $null
    $titleText  = $null

    #Set the link URL as $link using the outerHTML property of the Link
    $link       = $_.outerHTML
    #Set the title text as $titleText using the outerText property of the Link
    $titleText  = $_.outerText

    #Split the link where there are spaces, and keep the part that matches href
    $link       = $link.Split(' ') | Where-Object {$_ -match '^href'}
    #Manipulate the link to get just the url, and trim and extra quotes
    $link       = $link.SubString($link.IndexOf('"')).Trim('"')

    #If the link is a link to an external source (starts with http:// or https://), then it is a full link, set $linkURL to $link
    if ($link -match '^https?://.+') {

        $linkURL = $link
    
    #Else it is a link to this subreddit as a self post, $linkURL prepends 'http://www.reddit.com' to the $link (which is /r/powershell/...)
    } else {

    
        $linkURL = ('http://www.reddit.com' + $link)

    }
    
    #Create a custom object to store the index ($i), the title, and link
    $linkObject = [PSCustomObject]@{

        Index = $i
        Title = $titleText
        Link  = $linkURL

    }

    #Add the object to our object array
    $prettyLinks.Add($linkObject) | Out-Null

    #Add 1 to $i
    $i++    
   
    }  

}

Let's run the code, and then take a look at our $prettyLinks variable.

That looks good, and the object is at our disposal for whatever we'd like to do with the information.

For an example on how the code can be used, check this out!

$browsing = $true

While ($browsing) {

    $selection = $null

    Write-Host "Select a [#] from the titles below!"`n -ForegroundColor Black -BackgroundColor Green

    ForEach ($pretty in $prettyLinks) {

        Write-Host "[$($pretty.Index)] $($pretty.Title)"`n

    }

    Try {
        
        [int]$selection = Read-Host 'Which [#]? "q" quits'

        if ($selection -le ($prettyLinks.Count -1)) {

            Start-Process $prettyLinks[$selection].Link 

        } else {

            $browsing = $false

            Write-Host '"q" or invalid option selected, browsing aborted!' -ForegroundColor Red -BackgroundColor DarkBlue
            
        }

    }

    Catch {

        $browsing = $false

        Write-Host '"q" or invalid option selected, browsing aborted!' -ForegroundColor Red -BackgroundColor DarkBlue        

    }

}

The above code creates a loop until the user inputs "q", or an invalid option. It will list out all of the titles, and then ask you for the number of the one you want to look at. Once you input the number, it will launch your default browser to the title's associated link.

This is but one of many examples of how the data can be used. Check out these screenshots to see the code in action.

Let's select 17.

Here's what happens if you put "q".

Working With Forms

Invoke-WebRequest can also work with form data. We can get the forms from the current request, manipulate the data, and then submit them.

Let's take a look at a simple one, searching Reddit.

$webRequest = Invoke-WebRequest 'http://www.reddit.com'

Let's take a look at $webRequest.Forms:

Now that we know that the search form is the first array value, let's declare $searchForm as $webRequest.Forms[0].

$searchForm = $webRequest.Forms[0]

Now $searchForm will contain the form we care about.

Here are the properties we see at a glance:

  • Method
    • This is the method we'll use when sending the request with the form data.
  • Action
    • This is the URL used with the request. Sometimes it is the full URL, other times it is part of a URL that we need to concatenate together with the main URL.
  • Fields
    • This is a hash table that contains the fields to be submitted in the request.

Here are the values in $searchForm.Fields

Let's set the value of "q" to what we'd like to search for. I will set it to: "PowerShell".

$searchForm.Fields.q = 'PowerShell'

It's always good to verify things are as they should be. 

$searchForm.Fields

That looks good! Now to format our next request, and search Reddit!

$searchReddit = Invoke-WebRequest -Uri $searchForm.Action -Method $searchform.Method -Body $searchForm.Fields

In this request, the following parameters are set:

  • -Uri
    • We use $searchForm.Action  for this as that contains the full URL we need to use.
  • -Method
    • We use $searchForm.Method for this. Technically it would default to using Get, but that is not the case for all forms. It is good to use what the form states to use.
  • -Body
    • We use $searchForm.Fields for this, and the body is submitted as the hash table's key/value pairs. In this case that is "q" = "PowerShell". 

Now that we have the results in $searchReddit, we can validate the data by taking a look at the links.

$searchReddit.Links | Where-Object {$_.Class -eq 'search-title may-blank'} | Select-Object InnerText,Href

Now that we've validated it worked, you could also parse the contents to get what you want out of it!

Full code for this example:

$webRequest          = Invoke-WebRequest 'http://www.reddit.com'
$searchForm          = $webRequest.Forms[0]
$searchForm.Fields.q = 'PowerShell'
$searchReddit        = Invoke-WebRequest -Uri $searchForm.Action -Method $searchform.Method -Body $searchForm.Fields

$searchReddit.Links | Where-Object {$_.Class -eq 'search-title may-blank'} | Select-Object InnerText,Href

Logging In To Web Sites

We can also use Invoke-WebRequest to log in to web sites. To do this we'll need to be sure to do the following:

  • Set the userAgent to Firefox.
    • This may not be required, but it is generally safe to do.
  • Use the sessionVariable parameter to create a variable.
    • This will be used to maintain the session, and store cookies.
  • Populate the correct form with login details.
    • We'll store the credentials in $credential via Get-Credential.

We'll start by storing my credentials for Reddit in $credential, setting $uaString to the FireFox user agent string, and finally using Invoke-WebRequest to initiate our session.

$credential = Get-Credential
$uaString   = [Microsoft.PowerShell.Commands.PSUserAgent]::FireFox
$webRequest = Invoke-WebRequest -Uri 'www.reddit.com' -SessionVariable webSession -UserAgent $uaString

!!NOTE!! When setting the parameter -SessionVariable, do not include the "$" in the variable name.

$webRequest.Forms contains all the forms.

The Id of the form we need is "login_login-main". Knowing this, we can use the following line to get just the form we need:

$loginForm               = $webRequest.Forms | Where-Object {$_.Id -eq 'login_login-main'}

Now to check $loginForm.Fields to be sure it is what we need, and to see what the properties we need to set are.

Let's set the fields "user" and "passwd" using the $credential variable we created earlier.

$loginForm.Fields.user   = $credential.UserName
$loginForm.Fields.passwd = $credential.GetNetworkCredential().Password

!!NOTE!! The $loginForms.Fields.passwd property will store the password as plain text.

Alright! Now that our setup is complete, we can use the following command to attempt to log in:

$webRequest = Invoke-WebRequest -Uri $loginForm.Action -Method $loginForm.Method -Body $loginForm.Fields -WebSession $webSession -UserAgent $uaString

This request contains the following information:

  • -Uri $loginForm.Action
    • This uses the URL provided in the property Action from the login form.
  • -Method $loginForm.Method
    • This uses the Method provided in the property Method from the login form.
  • -Body $loginForm.Fields
    • This sends along the hash table (which includes our username and password) along with the request.
  • -WebSession $webSession
    • This tells Invoke-WebRequest to use the SessionVariable we created for cookie storage. We use $webSession this time, as the initial request was creating the variable, and we are utilizing it now.
  • -UserAgent $uaString
    • This sends along the FireFox user agent string we set earlier.

We can now use the following code to verify if we've succeeded in logging in:

if ($webRequest.Links | Where-Object {$_ -like ('*' + $credential.UserName + '*')}) {

    Write-Host "Login verified!"

} else {

    Write-Host 'Login unsuccessful!' -ForegroundColor Red -BackgroundColor DarkBlue

}

It worked! This verification check works by doing a wildcard search for the username that is stored in the credential object $credential in any of the web site's links.

Now that you have an authenticated session, you can browse/use Reddit with it by using the parameter -WebSession, and the value $webSession.

Full code for this example:

#Store the credentials you are going to use!
#Note: if you want to securely store your credentials, you can use $credential | Export-CliXML .\credential.xml
#Then you can import it by using $credential = Import-CliXML .\credential.xml
$credential = Get-Credential
#Set $uaString as the FireFox user agent string
$uaString   = [Microsoft.PowerShell.Commands.PSUserAgent]::FireFox
#Store the initial WebRequest data in $webRequest, create the session variable $webSession
$webRequest = Invoke-WebRequest -Uri 'www.reddit.com' -SessionVariable webSession -UserAgent $uaString

#Gather and set login form details
$loginForm               = $webRequest.Forms | Where-Object {$_.Id -eq 'login_login-main'}
$loginForm.Fields.user   = $credential.UserName
#NOTE: This will store the password in the hash table as plain text
$loginForm.Fields.passwd = $credential.GetNetworkCredential().Password

#Attempt to log in using the Web Session $webSession, with the information provided in $loginForm
$webRequest = Invoke-WebRequest -Uri $loginForm.Action -Method $loginForm.Method -Body $loginForm.Fields -WebSession $webSession -UserAgent $uaString

#Validate if the login succeeded, then take action accordingly.
if ($webRequest.Links | Where-Object {$_ -like ('*' + $credential.UserName + '*')}) {

    Write-Host "Login verified!"

} else {

    Write-Host 'Login unsuccessful!' -ForegroundColor Red -BackgroundColor DarkBlue

}

Homework

  • What error checking could we have applied to some of these examples?
  • Think of things you could automate when using Invoke-WebRequest that you use a browser for every day.
  • What better way could we have validated the data in the final example in the parsing data section?
  • How could we prompt the user for input as to what to search Reddit for?

Keep an eye out for Parts 2 and 3, coming in the next couple weeks!

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Back to top]

PowerShell: Getting Started - Importing, Using, and Exporting Data

PowerShell: Getting Started - Importing, Using, and Exporting Data

Getting Started - Importing, Using, and Exporting Data

Updated to reflect changes in what list type you should use.

Example:

Do use:

[Collections.Generic.List[PSCustomObject]]$resultsArray = @()

Don’t use:

[System.Collections.ArrayList]$resultsArray = @()

The reasoning behind this can be read, here: https://docs.microsoft.com/en-us/dotnet/api/system.collections.arraylist?view=netframework-4.8

Importing Data Into PowerShell

PowerShell offers a variety of ways to import data. I will be going over just a few of the ways in this article, but a lot of the concepts stay the same no matter which way you need to use. Remember that at the end of the day, the best way is the one that gets the job done for you!

The general guideline I follow is:

  • Import the data

  • Validate the data

  • Iterate through the data and take actions

  • Export data when needed

Text Files

While this method isn't as structured as others, it can really be handy in a pinch. You can copy and paste a list of users, computers, or anything else you want to iterate through, and then save it as a text file. Once you have a file, you can use PowerShell to import the contents.

For this example, I will import the contents of Cities.txt, seen here:

I'll use Get-Content to store the contents of the file in the variable $Cities.

$Cities = Get-Content .\Cities.txt

Let's take a look at the $Cities variable.

The contents have been successfully imported. We can see what type of object we're working with by piping $Cities to Get-Member.

$Cities | Get-Member

Looks like we're working with an array of strings. We can see this by using the Count property, as well as taking a look at the first value in the array, which would be reflected as $Cities[0].

CSV Files

PowerShell works nicely with CSV files. This method of importing data is a bit more structured, and overall is very clean. The headers of the CSV file will be imported as properties, and we'll take a look at that after we import the contents. 

For this example I will import a CSV file that contains a list of users that HR wants us to create in our environment. I will use the command Import-CSV, and store the contents in the variable $Users.

Here is what the CSV file looks like:

Let's go ahead and import the CSV now.

$Users = Import-CSV .\ADUsers.csv

Now let's see what $Users contains.

Awesome! We even have some properties now.  Let's see what we have when we pipe $Users to Get-Member.

Looks like CSVs are imported into a custom object, which we can then use as needed!

We can use the Count property to get the number of users imported, as well as take a look at the first value ($Users[0]) of the array. 

CliXML Files

CliXML files in PowerShell are a special type of XML file that represent object(s) that have been exported. This is extremely handy when you need to export an object that you'll later want to import into PowerShell.  You are also able to export information as CliXML that contains credentials, securely. 

Using CliXML is the best way to export/import objects that you are going to use directly in PowerShell.

For this example, I will import a CliXML file I exported, which contains the results of Get-Process.

Here is the file:

Another benefit of CliXML is it maintains hierarchies. Let's import this content into the variable $Processes, via Import-Clixml.

$Processes = Import-Clixml .\Processes.xml

Success! We can see the objects stored in the $Processes variable.

Let's pipe $Processes to Get-Member and see what we have.

$Processes | Get-Member

Our object TypeName will have the saved object type appended to "Deserialized", and in our case that is "Deserialized.System.Diagnostics.Process".

We can use the Count property to get the number of objects in $Processes, as well as look at the first value by using $Processes[0].

Validation

After we import the information into PowerShell, validating it is the next important step. I went over some basic examples in my article on error handling. In this article we will be going further into how to validate data.

Text Files

There are a few different ways to validate information in text files. Using our example of importing cities from above, here is what our $Cities variable contains:

What problems can you spot with this? I see two! 

  • $3477|3 is not a city

  • There seem to be some blank spaces at the end (Carriage returns)

Blank spaces can do some not-so-awesome things in scripts.

The easy fix for this script, since we're looking for city names, is to use the following regular expression, with the -match comparison operator: [a-z]. If you're not familiar with regular expressions, they can seem confusing at first. This basically says match any characters a-z. 

Visit http://regexone.com/ to learn more about Regex, I've found it to be a great resource.

Let's re-declare our variable, using itself, Where-Object, and -match

$Cities = $Cities | Where-Object {$_ -match '[a-z]'}

Now let's see the contents of $Cities.

That looks much better!

CSV/CliXML

Validating data from importing CSVs or CliXML files is a similar process. Since we'll have properties to work with, and values for each of those properties, the logic can become a bit more complex.

One way to handle it would be to not handle it up front, but to use some error handling, and catch any errors if they occur due to incomplete/invalid data. 

While that will technically work... you can catch invalid or incomplete data another way. 

In this example we'll be using the CSV file from earlier, and it's contents (with one addition, a test user with some missing information).

Here's our imported test data:

I'll now use the following code to validate the data:

#Iterate through each user
ForEach ($user in $users) {

    #Declaring variables up top
    $userProperties = $null
    $skipUser       = $null

    #Setting the $userProperties variable to contain the properties of the current $user object
    $userProperties = $user | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
    #Setting $skipUser to $false by default 
    $skipUser       = $false

    #Now we will iterate through each property
    ForEach ($property in $userProperties) {
    
        #For the current $property, we're using a switch statement to evaulute the $user object's current value
        #We do this by accessing $user.$property
        Switch ($user.$property) {

            #If there are any non-alphanumeric characters, minus spaces, set $skipUser to $true
            #We then break out of the switch
            {$_ -match '\W' -and $_ -notmatch '\s'} {
        
                $skipUser = $true

                Break

            }

            #If any of the values are blank, set $skipUser to $true
            #We then break out of the switch
            {$_ -eq ''} {

                $skipUser = $true

                Break

            }

        }
        
    }

    #If $skipUser is $false, perform this action
    if (!$skipUser) {

        Write-Host "Processing $($user | Out-String)"
        #Processing code here

    #Else (basically if $skipUser is $true), perform this action
    } else {

        Write-Host "Skipping $($user | Out-String)"
        #Skipping code here

    }

}

I use a couple different loops, a switch statement, regex, and an if statement to validate the data. I've added comments to the above code so you can better understand what is happening.

Let's take a look at the results:

Perfect! The two that we'd expect to be skipped, are indeed skipped.

Iterating Through Data

Iterating through data in PowerShell typically involves using loops. While I have gone over loops in a previous article, I will cover how they apply to our examples above.

Text Files

Let's expand upon our example from validating Text Files above. We have the following cities in $Cities:

Let's iterate through them, and just for fun, return some information from Weather Underground's free autocomplete API for each one.

[Collections.Generic.List[PSCustomObject]]$apiDataArray = @()

ForEach ($city in $Cities) {

    $apiURL         = $null
    $cityName       = $null
    $apiData        = $null
    $cityDataObject = $null

    $cityName       = $city
    $apiURL         = "http://autocomplete.wunderground.com/aq?query=$cityName"
    $apiData        = Invoke-RestMethod -Uri $apiURL

    $cityDataObject = [PSCustomObject]@{

        CityName = $cityName
        APIData  = $apiData

    }

    $apiDataArray.Add($cityDataObject) | Out-Null

}

In the above example I use a ForEach loop, Invoke-RestMethod, and a PS Custom Object.

Let's take a look at the results in $apiDataArray!

We now have an array of cities, and their associated API data. Let's dig into the data for Chicago.

Hmm, looks like we'll need to access the RESULTS property.

The API returned the results in an array for what it found when we gave it the value of Chicago for the city name. The first object in the array is the closest match. Let's access that!

There we go, data we can use.

CSV/CliXML Files

We'll be using the CSV information we validated earlier in this example. We pretty much demonstrated iterating through it when we validated it. However, I'd like to take that example further to demonstrate a few things, and put a lot of the concepts I've been going over together!

For this example I will iterate through the contents of the CSV file, and perform the following actions:

  • Create an array to store objects in

  • Validate information

  • Attempt to create the user in my Active Directory lab environment

  • Capture all results in an object

  • Add the object to the array

  • Return the object array

Here is the code (I've added comments so you can understand what is happening, every step of the way):

#Import users and create an array to store the results in
$Users                                                  = Import-CSV .\ADUsers.csv
[Collections.Generic.List[PSCustomObject]]$resultsArray = @()

#if $Users exists
if ($Users) {

    #Iterate through each user
    ForEach ($user in $users) {

        #Declaring variables up top
        $userProperties = $null
        $skipUser       = $null
        $samAccountName = $null
        $createUser     = $null
        $userName       = $null
        $resultsObject  = $null
        $csvValidation  = $null

        #Setting the $userProperties variable to contain the properties of the current $user object
        $userProperties = $user | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name

        #Setting $skipUser to $false by default 
        $skipUser       = $false

        #Now we will iterate through each property
        ForEach ($property in $userProperties) {
    
            #For the current $property, we're using a switch statement to evaulute the $user object's current value
            #We do this by accessing $user.$property
            Switch ($user.$property) {

                #If there are any non-alphanumeric characters, minus spaces, set $skipUser to $true
                #Set $csvValidation to the property we found an issue with
                #We then break out of the switch
                {$_ -match '\W' -and $_ -notmatch '\s'} {
        
                    $skipUser      = $true
                    $csvValidation = "Invalid characters in [$property]"

                    Break

                }

                #If any of the values are blank, set $skipUser to $true
                #Set $csvValidation to the property we found an issue with
                #We then break out of the switch
                {$_ -eq ''} {

                    $skipUser      = $true
                    $csvValidation = "Empty value for [$property]"

                    Break

                }

            }
        
        }

        #If $skipUser is $false, perform this action
        if (!$skipUser) {
        
            #Set the name of the user in $userName using first and last name from CSV
            $userName = "$($user.'First Name') $($user.'Last Name')"

            #Set $adServer with the hostname of a domain controller
            $adServer = (Get-ADDomainController).HostName

            Write-Host "Creating AD account for $userName..."`n

            #Set $samAccountName variable with first initial+lastName.
            $samAccountName = ($user.'First Name'.Substring(0,1) + $user.'Last Name').ToLower()
     
            Try {

                #Store created AD User object results in $createUser via New-ADUser with -PassThru, then use that with the other AD commands
                $createUser = New-ADUser -Company:$user.Company -Department:$user.Department -DisplayName:$userName -GivenName:$user.'First Name' -Name:$userName -Path:"OU=Users,OU=Accounts,DC=gngrninja,DC=com" -SamAccountName:$samAccountName -Server:$adServer -Surname:"$($user.'Last Name')" -Type:"user" -UserPrincipalName:"$samAccountName@gngrninja.com" -PassThru
                
                #Set the password for the account. You would want to use a more secure method than this for the password, however :). Flag for reset on new logon
                Set-ADAccountPassword -Identity:$createUser -NewPassword:('p@$$w0rd' | ConvertTo-SecureString -AsplainText -Force) -Reset:$true -Server:$adServer
                
                #Set more password options
                Set-ADAccountControl  -Identity:$createUser -AccountNotDelegated:$false -AllowReversiblePasswordEncryption:$false -CannotChangePassword:$false -DoesNotRequirePreAuth:$false -PasswordNeverExpires:$false -Server:$adServer -UseDESKeyOnly:$false
                
                #Enable the account
                Enable-ADAccount      -Identity:$createUser -Server:$adServer
                
                #Perform action if account exists in AD
                if (Get-ADUser $createUser -Server $adServer) {

                    Write-Host "User [$($createUser.Name)] verified to exist in AD!"`n -ForegroundColor Green -BackgroundColor Black

                    $resultsObject = [PSCustomObject]@{

                        UserName         = $createUser.Name
                        AccountCreated   = $true 
                        ErrorMessage     = 'N/A'

                    }

                #Perform action if account is not found in AD
                } else {

                    Write-Host "User [$userName] not found in AD... something went wrong!"`n -ForegroundColor Red -BackgroundColor DarkBlue

                    $resultsObject = [PSCustomObject]@{

                        UserName         = $userName
                        AccountCreated   = $false
                        ErrorMessage     = 'Account not found in AD!'

                    }

                }

                #Add results object to $resultsArray
                $resultsArray.Add($resultsObject) | Out-Null

            } 
            
            #Catch account already exists exception
            Catch [Microsoft.ActiveDirectory.Management.ADIdentityAlreadyExistsException] {

                Write-Host "User [$userName] already exists in AD!"`n -ForegroundColor Red -BackgroundColor DarkBlue
                
                $resultsObject = [PSCustomObject]@{

                        UserName         = $userName
                        AccountCreated   = $false
                        ErrorMessage     = 'Account already exists in AD!'

                }

                #Add results object to $resultsArray
                $resultsArray.Add($resultsObject) | Out-Null

            }

            Catch {

                Write-Host "Error [$($_.Exception.Message)] occured when attempting to create [$userName]!"`n -ForegroundColor Red -BackgroundColor DarkBlue
                
                $resultsObject = [PSCustomObject]@{

                        UserName         = $userName
                        AccountCreated   = $false
                        ErrorMessage     = $_.Exception.Message

                }

                #Add results object to $resultsArray
                $resultsArray.Add($resultsObject) | Out-Null 
                               
            }

        #Else (basically if $skipUser is $true), perform this action
        } else {

            $userName = "$($user.'First Name') $($user.'Last Name')"
            Write-Host "Skipping $userName..."

            $resultsObject = [PSCustomObject]@{

                    UserName         = $userName
                    AccountCreated   = $false
                    ErrorMessage     = "Error validating data: [$csvValidation]!"

             }

             #Add results object to $resultsArray
             $resultsArray.Add($resultsObject) | Out-Null
        
        }

    }

}

#Return the $resultsArray array of objects
Return $resultsArray

Let's run this on a remote session to my lab's Domain Controller, and see what happens! I've saved the script as Invoke-NewUserCreation.ps1, in a directory that also contains a CSV of the users.

Since I return the object array of results, I will use the following command to run the script and store the results:

$createUsers = .\Invoke-NewUserCreation.ps1

There you can see some of what is happening, because I used the evil Write-Host command to display information usefully :)

However, the real power comes with the object we have now (which has detailed results). Let's take a look at $createUsers.

$createUsers | Format-Table -AutoSize

Here you can see all the information regarding what happened. We can even see where the first validation errors occurred for the users that were skipped.

Here is a screenshot of Active Directory Users and Computers on that Domain, showing the results:

Let's run the script again, to see what happens!

$createUsers = .\Invoke-NewUserCreation.ps1

Those errors were to be expected! Now let's see the object...

$createUsers | Format-Table -AutoSize

Now the object contains the error message stating the user account already exists.

Exporting Data

We've gone over importing, validating, and iterating through data. Now let's see what we need to do to export it!

Text Files

This is the simplest of the bunch. In this example I will use Get-Process to get a list of process names, and then export that to Processes.txt.

Get-Process | Select-Object -ExpandProperty Name | Out-File .\Processes.txt

Let's make sure the file exists, and that it has content!

Sure enough, it does! 

CSV Files

Exporting to CSV allows for better readability, and also works to import the data into other systems. The readability can also be used for reporting purposes.

Here is the $createUsers object we created earlier:

Let's export that to a CSV via the Export-Csv command. We will want to be sure to use the parameter -NoTypeInformation, as that will prevent PowerShell from also outputting the object's top information on top of the CSV (which we don't want!).  

$createUsers | Export-Csv -NoTypeInformation (".\ADresults{0:MMddyy_HHmm}.csv" -f (Get-Date))

Let's see if the file exists...

There it is! Let's take a look at the contents.

We have data, and it is ours to pretty up.

CliXML Files

I love CliXML files. I use them a lot, and they can really save you some time. We'll use the $createUsers object we used earlier, which contains the following information:

Now I will export that to an XML file via Export-Clixml.

$createUsers | Export-Clixml (".\ADresults{0:MMddyy_HHmm}.xml" -f (Get-Date))

Let's ensure the file exists:

For good measure, let's import the Clixml file and then compare the original object and imported object side-by-side:

$createUsersCopy = Import-Clixml .\ADresults062716_2041.xml

There you have it, they are identical! The CliXML commands provide an excellent method for storing PowerShell objects.

Homework

  • How could we have automatically made the CSV iteration example export the results to a CSV?

    • How about to CliXML?

  • What ways can you benefit from validation in your code?

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Back to Top]

PowerShell: Getting Started - Creating Custom Objects

PowerShell: Getting Started - Creating Custom Objects

Getting Started - Creating Custom Objects

Welcome to my Getting Started with Windows PowerShell series!

What Are Custom Objects in PowerShell?

Everything we work with in PowerShell is an object in one way or another. A simple string is an object with the type of [System.String], using Get-Process returns objects with the type of [System.Diagnostics.Process], and using Get-ChildItem returns objects with the type [System.IO.FileInfo]

We can see that, here.

Custom objects are objects we create to collect data we want to collect. They can be comprised of objects from other datasets, arrays, and commands. Anything we can do in PowerShell can be fed into a custom object.

While we can create any type of object, from COM objects to objects based on .NET classes, we'll be focusing on the PSCustomObject. PSCustomObject is a .NET class [System.Management.Automation.PSCustomObject].

Why Create Custom Objects?

So that's nice... we can create custom objects, but why do it? Simple... output. In PowerShell it is best to keep your output from scripts and functions as objects you create or handle.  This allows for better readability and re-use. 

That way you can collect information, and iterate through it with built in commands such as Select-Object and Where-Object.

Since we can add anything to a custom object, from any other object/source in PowerShell, it can also be used as a method to organize information.

Let's take a look at a few example of creating custom objects.

Creating Custom Objects

Below I will be creating custom objects using various methods. It is important to understand that there isn't necessarily a best way to do anything in PowerShell. At the end of the day, the best way is the way that gets the job done for you.

However, I tend to gravitate towards methods that are easy for people looking at my scripts to interpret. This applies even when I'm looking at my own scripts! If I'm tired, and looking at some of my code, the last thing I need to do is try to figure out what I was doing based on code that isn't that easy to read.

Feel free to follow along! Open up the ISE and save a file to C:\PowerShell named part12.ps1. I will use the following code each time an example executes:

$adminPasswordStatus = $null
$thermalState        = $null
$osInfo              = Get-CimInstance Win32_OperatingSystem
$computerInfo        = Get-CimInstance Win32_ComputerSystem
$diskInfo            = Get-CimInstance Win32_LogicalDisk

Switch ($computerInfo.AdminPasswordStatus) {

    0 {$adminPasswordStatus = 'Disabled'}
     
    1 {$adminPasswordStatus = 'Enabled'}

    2 {$adminPasswordStatus = 'Not Implemented'} 

    3 {$adminPasswordStatus = 'Unknown'}

    Default {$adminPasswordStatus = 'Unable to determine'}

}

Switch ($computerInfo.ThermalState) {

    1 {$thermalState = 'Other'}

    2 {$thermalState = 'Unknown'}

    3 {$thermalState = 'Safe'}

    4 {$thermalState = 'Warning'} 

    5 {$thermalState = 'Critical'}

    6 {$thermalState = 'Non-recoverable'}

    Default {$thermalState = 'Unable to determine'}

}

Be sure to include the code above with any example you want to try in the ISE.

New-Object With Add-Member

With this method the first thing we'll do is create the object with the New-Object command, and store the object in a variable. Then we'll pipe the object to the Add-Member command. Add-Member will add the properties or methods we specify to the object we create.

We'll be using the MemberType of NoteProperty, and then give names to the properties (the names are whatever we want them to be). It's good to make them make sense! Then, finally, we'll define the Value as whatever expression we need to. In this case we'll use the some of the variables above. If you're following along and didn't catch what we have in the variables, check the summary below.

$computerInfo contains information from Get-CimInstance Win32_ComputerSystem.
$osInfo contains information from Get-CimInstance Win32_OperatingSystem.
$diskInfo contains information from Get-CimInstance Win32_LogicalDisk.

Here's the code:

$ourObject = New-Object -TypeName psobject 

$ourObject | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computerInfo.Name
$ourObject | Add-Member -MemberType NoteProperty -Name OS -Value $osInfo.Caption
$ourObject | Add-Member -MemberType NoteProperty -Name 'OS Version' -Value $("$($osInfo.Version) Build $($osInfo.BuildNumber)")
$ourObject | Add-Member -MemberType NoteProperty -Name Domain -Value $computerInfo.Domain
$ourObject | Add-Member -MemberType NoteProperty -Name Workgroup -Value $computerInfo.Workgroup
$ourObject | Add-Member -MemberType NoteProperty -Name DomainJoined -Value $computerInfo.Workgroup
$ourObject | Add-Member -MemberType NoteProperty -Name Disks -Value $diskInfo
$ourObject | Add-Member -MemberType NoteProperty -Name AdminPasswordStatus -Value $adminPasswordStatus
$ourObject | Add-Member -MemberType NoteProperty -Name ThermalState -Value $thermalState

Now let's run that, and then type $ourObject to see what our object contains.

That worked! We created an object, which contains information from all three sources. We can type $ourObject.Disks to see what is contained there.

Hashtable Using Add Method

Now let's take a look at using a hashtable to add our properties and values to the custom object. A hashtable is a collection of keys that have names with associated values.

For this example I will create the hashtable, and then call its Add method to add the pairs of data (separated by a comma). With this method we will actually create the object last, as we'll be adding the hashtable of properties and values to it when we create it.

If you're following along and didn't catch what we have in the variables, check the summary below.

$computerInfo contains information from Get-CimInstance Win32_ComputerSystem.
$osInfo contains information from Get-CimInstance Win32_OperatingSystem.
$diskInfo contains information from Get-CimInstance Win32_LogicalDisk.

Here's the code:

[hashtable]$objectProperty = @{}

$objectProperty.Add('ComputerName',$computerInfo.Name)
$objectProperty.Add('OS',$osInfo.Caption)
$objectProperty.Add('OS Version',$("$($osInfo.Version) Build $($osInfo.BuildNumber)"))
$objectProperty.Add('Domain',$computerInfo.Domain)
$objectProperty.Add('Workgroup',$computerInfo.Workgroup)
$objectProperty.Add('DomainJoined',$computerInfo.PartOfDomain)
$ObjectProperty.Add('Disks',$diskInfo)
$objectProperty.Add('AdminPasswordStatus',$adminPasswordStatus)
$objectProperty.Add('ThermalState',$thermalState)

$ourObject = New-Object -TypeName psobject -Property $objectProperty

Now let's see what $ourObject contains.

The code looks cleaner, but notice the order of the properties! It's not in the order we created the hashtable in. With Add-Member we were able to add each property, and it would keep the order we added them in.

That's not really a big deal, though! Since PowerShell has the nifty Select-Object command, we have an easy way to re-order the object.  

Let's try:

$ourObject | Select-Object ComputerName,OS,'OS Version',Domain,Workgroup,DomainJoined,AdminPasswordStatus,ThermalState,Disks

There we go, the order is as we want it now. Note that this will work to re-order any object you pass to it, and this can really come in handy.

Ordered Hashtable

!NOTE! Using this method is only supported in PowerShell version 3.0+

In this example I will declare the hashtable all at once, and then add the hashtable to the custom object we create. I will also use an ordered hashtable, so we can see how those work. That should allow us to keep the order of the properties correctly in the object. 

Here's the code:

$objectProperty = [ordered]@{

    ComputerName        = $computerInfo.Name
    OS                  = $osInfo.Caption
    'OS Version'        = $("$($osInfo.Version) Build $($osInfo.BuildNumber)")
    Domain              = $computerInfo.Domain
    Workgroup           = $computerInfo.Workgroup
    DomainJoined        = $computerInfo.PartOfDomain
    Disks               = $diskInfo
    AdminPasswordStatus = $adminPasswordStatus
    ThermalState        = $thermalState

}

$ourObject = New-Object -TypeName psobject -Property $objectProperty

$ourObject

Let's run this [F5], and see what happens!

Looks good, and it is indeed in the right order!

Running the script from the console shows the object in the correct order as well.

PSCustomObject Type Adapter

!NOTE! Using this method is only supported in PowerShell version 3.0+

We can also create custom objects using the [PSCustomObject] type adapter.

If you're following along and didn't catch what we have in the variables, check the summary below.

$computerInfo contains information from Get-CimInstance Win32_ComputerSystem.
$osInfo contains information from Get-CimInstance Win32_OperatingSystem.
$diskInfo contains information from Get-CimInstance Win32_LogicalDisk.

Here's the code:

$ourObject = [PSCustomObject]@{

    ComputerName        = $computerInfo.Name
    OS                  = $osInfo.Caption
    'OS Version'        = $("$($osInfo.Version) Build $($osInfo.BuildNumber)")
    Domain              = $computerInfo.Domain
    Workgroup           = $computerInfo.Workgroup
    DomainJoined        = $computerInfo.PartOfDomain
    Disks               = $diskInfo
    AdminPasswordStatus = $adminPasswordStatus
    ThermalState        = $thermalState

}

Let's take a look at $ourObject.

Nice! It also kept our order both in the ISE, and the console.

Here are the results from running part12.ps1 in the console:

Same result. Now let's look at how we can use this code to create an array of custom objects.

Creating a Custom Object Array

Why would we want to do such a thing?

Let's look at the following scenario:

You have a list of computers that you need to get information from. You want to capture any error messages that may come up. You also would like to capture what the computer name in the file was (which we will as ComputerInput), as well as the computer name returned when querying for information. We'll also want to gather the operating system and disk information.

To do this, we can use a ForEach loop, which calls a function, and utilize Try/Catch for error handling.

We'll also use a lot of what we learned today about creating custom objects. 

Setup

There is some setup to this, which is not included in the code. 

$credential contains a [System.Management.Automation.PSCredential] object, which stores the credentials to access my laptop for this example.

$computers contains an array of computer names to look up. For this example, I created a text file, and used Get-Content to import it into the $computers variable.

Looks like there are some potential script breakers in there!

Let's look at the code:

I've commented out the code to go over each and every step along the way. 

function Get-ComputerInformation { #Begin function Get-ComputerInformation
    [cmdletbinding()]
    param (
        [Parameter(
            Mandatory = $true
        )]
        [String]
        $computerName
    )

    Try { #Begin Try
        
        #Using Write-Host as Verbose is too chatty with the other commands
        Write-Host `n"Looking up information for: [$computerName]"`n

        #Null out variables used in the function
        #This isn't needed, but I like to declare variables up top
        $adminPasswordStatus = $null
        $thermalState        = $null
        $cimSession          = $null
        $computerObject      = $null
        $errorMessage        = $null

        #Except this guy, he gets a value that we'll use when errors are encountered
        $unableMsg           = 'Unable to determine'

        #Switch out the incoming $computerName
        #If it's localhost, we wont need to use our $credential variable when creating our CimSession
        #This creates the $cimSession variable, which encapsulates the object created with New-CimSession pointed at the target $computerName
        Switch ($computerName) {

            'localhost' {$cimSession = New-CimSession -ComputerName $computerName -ErrorAction Stop}

             Default    {$cimSession = New-CimSession -Credential $credential -ComputerName $computerName -ErrorAction Stop}

        }

        #Gather information using Get-CimInstance, pointed to our $cimSession variable
        #ErrorAction is set to Stop here, so we can catch any errors
        $osInfo       = Get-CimInstance Win32_OperatingSystem -CimSession $cimSession -ErrorAction Stop
        $computerInfo = Get-CimInstance Win32_ComputerSystem  -CimSession $cimSession -ErrorAction Stop
        $diskInfo     = Get-CimInstance Win32_LogicalDisk     -CimSession $cimSession -ErrorAction Stop

        #Use a switch to get the text value based on the number in $computerInfo.AdminPasswordStatus
        Switch ($computerInfo.AdminPasswordStatus) {

                  0 {$adminPasswordStatus = 'Disabled'}
     
                  1 {$adminPasswordStatus = 'Enabled'}

                  2 {$adminPasswordStatus = 'Not Implemented'}

                  3 {$adminPasswordStatus = 'Unknown'}

            Default {$adminPasswordStatus = 'Unable to determine'}

        }

        #Use a switch to get the text value based on the number in $computerInfo.ThermalState
        Switch ($computerInfo.ThermalState) {

                  1 {$thermalState = 'Other'}

                  2 {$thermalState = 'Unknown'}

                  3 {$thermalState = 'Safe'}

                  4 {$thermalState = 'Warning'} 
             
                  5 {$thermalState = 'Critical'}

                  6 {$thermalState = 'Non-recoverable'}

            Default {$thermalState = 'Unable to determine'}

        }

        #Create the object, cleanly!
        $computerObject = [PSCustomObject]@{

            ComputerInput       = $computerName
            ComputerName        = $computerInfo.Name
            OS                  = $osInfo.Caption
            'OS Version'        = $("$($osInfo.Version) Build $($osInfo.BuildNumber)")
            Domain              = $computerInfo.Domain
            Workgroup           = $computerInfo.Workgroup
            DomainJoined        = $computerInfo.PartOfDomain
            Disks               = $diskInfo
            AdminPasswordStatus = $adminPasswordStatus
            ThermalState        = $thermalState
            Error               = $false
            ErrorMessage        = $null

        }

        #Close the CimSession
        Remove-CimSession -CimSession $cimSession -ErrorAction Stop

        #Return the object created
        Return $computerObject
            
    } #End Try

    Catch { #Begin Catch

        #Capture the exception message in the $errorMessage variable
        $errorMessage = $_.Exception.Message    
        
        #Create our custom object with the error message     
        $computerObject = [PSCustomObject]@{

            ComputerInput       = $computerName
            ComputerName        = $unableMsg
            OS                  = $unableMsg
            'OS Version'        = $unableMsg
            Domain              = $unableMsg
            Workgroup           = $unableMsg
            DomainJoined        = $unableMsg
            Disks               = $unableMsg
            AdminPasswordStatus = $unableMsg
            ThermalState        = $unableMsg
            Error               = $true
            ErrorMessage        = $errorMessage
              
        }

        #Keeping Write-Host here for the colors! Hate all you want ;)
        Write-Host `n"Error encountered [$errorMessage]!"`n -ForegroundColor Red -BackgroundColor DarkBlue

        #Return the object created
        Return $computerObject 
            
        #Stop processing commands
        Break

    } #End Catch

} #End function Get-ComputerInformation

#Create the array we'll add the objects to
[System.Collections.ArrayList]$computerArray = @()

#Iterate through each computer in $computers with a ForEach loop
ForEach($computer in $computers) {

    #Use the Add method of the ArrayList to add the returned object from the Get-ComputerInformation function 
    #Piping this to Out-Null is important to suppress the result output from adding the object to the array
    $computerArray.Add((Get-ComputerInformation -computerName $computer)) | Out-Null

}

#Display the contents of the array.
$computerArray

Let's fire up the console, run the script, and take a look at the results!

It looks like the returned object is indeed an array of objects we've created in the function.

To do this I created an array before I called the ForEach loop, and then used the Add method to call the function with the current $computerName in the loop. This returns the object as a result, when each loop iteration runs.

We can also create a variable to store the results of the script via:

$computerArray = .\part12.ps1

You can see that using $computerArray[0] returned the first object in the array.

Want to export this to a CSV? Use this command:

$computerArray | Export-CSV .\results.csv -noTypeInformation

Let's open the file, and see if we were successful...

Now we can format it as we see fit!

!Note! Enumerating the disks and exporting them to the CSV is a bit more tricky as it is actually an object within an object.

This is but one of many ways to use objects! Again, there is no one right way. The right way is whatever works for you and your use case! 

Homework

  • How could you create a custom object that contains the computer name, along with network adapter, and IP information?

  • What interesting ways could you combine error handling with custom objects?

  • How can we properly export the disk information as well (from the custom object array example)?

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Back to top]

PowerShell: Getting Started - Error Handling

PowerShell: Getting Started - Error Handling

Getting Started - Error Handling

Welcome to my Getting Started with Windows PowerShell series!

Why Handle Errors?

Error handling may take some extra time to create, however, it can really save you time in the long run. Let's say you have a script that performs the following tasks:

  • Takes information from a database / file and stores it in $userAccounts

  • Uses a loop to perform processing on each $user in $userAccounts

    • Update a user's AD information if the user is in the list

    • Disable any accounts that are not in the list

The script runs, and seems to be working great for a few weeks. Then, all of a sudden, the database server goes down, and $userAccounts is empty. The script runs, doesn't see any users, and what likely happens next is all user accounts are deleted. While this example is a little extreme, it is something that can happen. There are also a lot of in-between cases that can make error handling worth it. It's a good mindset to have when you're writing scripts as you can write to incorporate it off the bat.

Errors in PowerShell

Errors in PowerShell are stored in the automatic variable $error. You can see how many errors you've encountered in your session by using:

$error.count

This is essentially an array of errors, to access the first value you can use:

$error[0]

Let's take a look at $error[0] more closely, via:

$error[0] | Get-Member

We can view information on the command that raised the error via:

$error[0].InvocationInfo

It looks like the line property contains the full command we used when the error was encountered.

$error[0].InvocationInfo.Line

The exception that raised the error can be accessed via:

$error[0].Exception

We can also get more information about the exception via:

$error[0].Exception | Get-Member

Here you can see the TypeName: [System.UnauthorizedAccessException], and the various methods and properties of the exception. The exception TypeName is used later when we catch specific errors.

Looks like to get the exception's message in string format we'd use:

$error[0].Exception.Message

Now that we've dug into the automatic variable $error, let's move on to the different types of errors we can encounter.

Terminating Errors

Terminating errors in PowerShell mean that the script can no longer continue to run with the information it has encountered, or has been given. If you do not have a way to catch the error, it will likely display the nasty red error text you've likely seen before. Terminating errors halt anything running in the pipeline. This terminates output, aside from the error message of the terminating error. 

Here's an example of a terminating error that was caused by calling a command that does not exist:

Get-TerminatingError

Non-Terminating Errors

Non-terminating errors in PowerShell do not stop the pipeline from executing. These errors are handled internally by the Cmdlet, and are not able to be captured via error handling. There are ways, however, to make PowerShell treat non-terminating errors like terminating errors. That way we can use error handling to capture what's going on.

Here's an example of a non-terminating error (access denied to a subfolder), when attempting to list all folders and subfolders in "C:\Windows\appcompat\".

Get-ChildItem -Path 'C:\Windows\appcompat' -Recurse

Force Non-Terminating Errors to Terminate

You can force non-terminating errors to become terminating errors in PowerShell. There are a couple ways you can do this. Why would you want to do this, you ask? Since non-terminating errors cannot be handled by us, we need to force them to terminate, and gain the functionality of handling the error ourselves. This is not always needed, but it is good to know that you can do it if you come across a use case for yourself.

$errorActionPreference

You can do it at a global way for the session via the $ErrorActionPreference variable. This is a special variable in PowerShell that allows you to control what happens when a non-terminating error is encountered.

Here are the values for $ErrorActionPreference.

  • Stop

    • Display error, and stop execution.

  • Inquire

    • Display error, and ask to continue.

  • Continue (Default)

    • This is the default setting. Display error, then continue execution.

  • Suspend

    • This one is for workflows. A workflow job is suspended to investigate what happened, then the workflow can be resumed.

  • SilentlyContinue

    • No error is displayed, execution is continued.

Let's see this in action.

Get-ChildItem -Path 'C:\Windows\appcompat' -Recurse;Write-Host 'Test'

As you can see, with a non-terminating error, the next command in the sequence is executed. Therefore we see 'Test'. Let's set the $errorActionPreference automatic variable to Stop, and re-run the same command.

$ErrorActionPreference = 'Stop'
Get-ChildItem -Path 'C:\Windows\appcompat' -Recurse;Write-Host 'Test'

It worked! This would also let us use error handling with this error in PowerShell now. As you can see, we do not see the output of the next command, as it never ran.

Use the Command's -ErrorAction Parameter

Cmdlet's and functions/scripts/modules that use [cmdletbinding()] enable utilization of the -ErrorAction common parameter. This parameter allows you to specify different actions to take when an error is encountered. 

  • Stop

    • Display error, and stop execution.

  • Inquire

    • Display error, and ask to continue.

  • Continue (Default)

    • This is the default setting. Display error, then continue execution.

  • Suspend

    • This one is for workflows. A workflow job is suspended to investigate what happened, then the workflow can be resumed.

  • SilentlyContinue

    • No error is displayed, execution is continued.

  • Ignore

    • The same as SilentlyContinue, but as SilentlyContinue still adds the message to the $error automatic variable, Ignore does not do this.

Let's set our $errorActionPreference to Continue, and then look at using the Cmdlet Get-ChildItem's -ErrorAction parameter.

We're setting $errorActionPreference back to Continue as earlier we set it to Stop for our other example. Continue is the default value.

$ErrorActionPreference = 'Continue'
Get-ChildItem -Path 'C:\Windows\appcompat' -Recurse -ErrorAction Stop;Write-Host 'Test'

Error Handling

There are a few different ways to to handle errors in PowerShell. The best way of course, is to never let them happen in the first place! To do that, it is good to have some data validation in place via an if statement, or whichever method suites your needs.

If a terminating error occurs, we can use Try/Catch/Finally blocks to perform different actions. If it is non-terminating, we can force it to become a terminating error, and then choose how to continue.

Validation

The simplest method of validation is the if statement. 

If you'd like to run these examples yourself, go ahead and fire up the PowerShell ISE. Save a file in C:\PowerShell as part11.ps1. 

An if statement is constructed as follows:

if (condition -eq $true) {

Do-Stuff

} else {

Do-Other-Stuff

}

The condition can be anything that resolves to a true value. That includes running a command that output (other than and error) occurs! Check this out:

if (Get-ChildItem Z:\ -ErrorAction SilentlyContinue) {

    Write-Host 'I can list the contents of Z:!'

} else {

    Write-Host 'I cannot list the contents of Z:!'

}

This does not return true, as expected, and thus we see the message from the code in the else portion of the if statement. We use the -ErrorAction common parameter with the value SilentlyContinue to suppress the error from being displayed to the end user of the script. No need in this case, as we're simply using the command for validation.

You can also use variables with the if statement to see if they are blank. 

$myVariable = $null

if ($myVariable) {

    Write-Host "We have information! Let's do stuff."

} else {

    Write-Host "`$myVariable is empty :("

}

The variable is empty, so the code in the else portion of the if statement is executed.

Try/Catch/Finally

The Try, Catch, and Finally blocks in PowerShell allow us to capture terminating errors. 

The Try block contains the code you'd like to execute, and catch any potential errors that happen.

The Catch block contains the code you'd like to execute after a terminating error has occurred. The current error will be accessible via the automatic variable $_.

The Finally block contains the code you'd like to run after the event has occurred. This is good for cleanup tasks. 

It is worth noting that finally block is not required.

Here is an example using Try/Catch/Finally:

Try {
    
    $command = 'Invoke-FakeCommand'

    Write-Host "Attempting to run: [Invoke-Expression -Command $command]"`n

    Invoke-Expression -Command $command

}

Catch {

    Write-Host $_.Exception.Message`n

}

Finally {

    Write-Host "Clean up: `$commmand = `$null"`n

    $commmand = $null

}

The code in the Try block executes and we see the output of Write-Host. We then see the error message that occurs, as our Catch block is writing out $_.Exception.Message. As we learned earlier, that is the string value of the exception that raised the error.

We then see the output from the Write-Host command in our Finally block. We use the finally block to free up the $command variable.

This version of Try/Catch will catch any terminating errors that are raised. To capture specific exceptions, you'll want to use the exception's TypeName.

Catch Specific Errors

Let's take a look at the following:

Try {

    Get-ThisWontWork

}

Catch [System.Management.Automation.CommandNotFoundException] {
    
    Write-Host "Command not found!"`n -ForegroundColor Red 
    Write-Host "Message: [$($_.Exception.Message)"] -ForegroundColor Red -BackgroundColor DarkBlue
    
}

The exception was caught, and the code in the Catch block was executed.

Now let's try this...

Try {
    
    Get-ChildItem -Path Z:\ -ErrorAction Stop
    Get-ThisWontWork

}

Catch [System.Management.Automation.CommandNotFoundException] {
    
    Write-Host "Command not found!"`n -ForegroundColor Red 
    Write-Host "Message: [$($_.Exception.Message)"] -ForegroundColor Red -BackgroundColor DarkBlue
    
}

Ahh! An error that wasn't handled! Luckily we can add multiple catch blocks.

Let's add another Catch block.

Catch {

Write-Host $_.Exception.Message

}
Try {
    
    Get-ChildItem -Path Z:\ -ErrorAction Stop
    Get-ThisWontWork

}

Catch [System.Management.Automation.CommandNotFoundException] {
    
    Write-Host "Command not found!"`n -ForegroundColor Red 
    Write-Host "Message: [$($_.Exception.Message)"] -ForegroundColor Red -BackgroundColor DarkBlue
    
}


Catch {

   Write-Host $_.Exception.Message

}

Now the exception from Get-ChildItem command is caught since the catch-all Catch block code is executed.

Getting Error Information

It can be handy to have a shortcut that shows you error information, which you can use to create specific Catch blocks. To do this, I created a function that I call in the Catch block which utilizes some validation itself! Here is the function:

function Get-ErrorInformation {
    [cmdletbinding()]
    param($incomingError)

    if ($incomingError -and (($incomingError| Get-Member | Select-Object -ExpandProperty TypeName -Unique) -eq 'System.Management.Automation.ErrorRecord')) {

        Write-Host `n"Error information:"`n
        Write-Host `t"Exception type for catch: [$($IncomingError.Exception | Get-Member | Select-Object -ExpandProperty TypeName -Unique)]"`n 

        if ($incomingError.InvocationInfo.Line) {
        
            Write-Host `t"Command                 : [$($incomingError.InvocationInfo.Line.Trim())]"
        
        } else {

            Write-Host `t"Unable to get command information! Multiple catch blocks can do this :("`n

        }

        Write-Host `t"Exception               : [$($incomingError.Exception.Message)]"`n
        Write-Host `t"Target Object           : [$($incomingError.TargetObject)]"`n
    
    }

    Else {

        Write-Host "Please include a valid error record when using this function!" -ForegroundColor Red -BackgroundColor DarkBlue

    }

}

Here is the full code I'll be running for this example:

function Get-ErrorInformation {
    [cmdletbinding()]
    param($incomingError)

    if ($incomingError -and (($incomingError| Get-Member | Select-Object -ExpandProperty TypeName -Unique) -eq 'System.Management.Automation.ErrorRecord')) {

        Write-Host `n"Error information:"`n
        Write-Host `t"Exception type for catch: [$($IncomingError.Exception | Get-Member | Select-Object -ExpandProperty TypeName -Unique)]"`n 

        if ($incomingError.InvocationInfo.Line) {
        
            Write-Host `t"Command                 : [$($incomingError.InvocationInfo.Line.Trim())]"
        
        } else {

            Write-Host `t"Unable to get command information! Multiple catch blocks can do this :("`n

        }

        Write-Host `t"Exception               : [$($incomingError.Exception.Message)]"`n
        Write-Host `t"Target Object           : [$($incomingError.TargetObject)]"`n
    
    }

    Else {

        Write-Host "Please include a valid error record when using this function!" -ForegroundColor Red -BackgroundColor DarkBlue

    }

}

Try {
   
    Get-ChildItem -Path Z:\ -ErrorAction Stop

    Get-ThisWontWork

}

Catch [System.Management.Automation.CommandNotFoundException] {

    Write-Host 'Command not found Catch block executed!' 

}

Catch {

   Get-ErrorInformation -incomingError $_

}

So now, if we wanted to catch this exception on it's own, we would need to add a catch block for [System.Management.Automation.DriveNotFoundException].

Catch [System.Management.Automation.CommandNotFoundException] {

Write-Host 'Command not found Catch block executed!' 

}

Let's add that to our code and run the following:

function Get-ErrorInformation {
    [cmdletbinding()]
    param($incomingError)

    if ($incomingError -and (($incomingError| Get-Member | Select-Object -ExpandProperty TypeName -Unique) -eq 'System.Management.Automation.ErrorRecord')) {

        Write-Host `n"Error information:"`n
        Write-Host `t"Exception type for catch: [$($IncomingError.Exception | Get-Member | Select-Object -ExpandProperty TypeName -Unique)]"`n 

        if ($incomingError.InvocationInfo.Line) {
        
            Write-Host `t"Command                 : [$($incomingError.InvocationInfo.Line.Trim())]"
        
        } else {

            Write-Host `t"Unable to get command information! Multiple catch blocks can do this :("`n

        }

        Write-Host `t"Exception               : [$($incomingError.Exception.Message)]"`n
        Write-Host `t"Target Object           : [$($incomingError.TargetObject)]"`n
    
    }

    Else {

        Write-Host "Please include a valid error record when using this function!" -ForegroundColor Red -BackgroundColor DarkBlue

    }

}

Try {
   
    Get-ChildItem -Path Z:\ -ErrorAction Stop

    Get-ThisWontWork

}

Catch [System.Management.Automation.CommandNotFoundException] {

    Write-Host 'Command not found Catch block executed!' 

}

Catch [System.Management.Automation.DriveNotFoundException] {

    Write-Host 'Get-ChildItem drive not found Catch block executed!'

}

Catch {

   Get-ErrorInformation -incomingError $_

}

There we go! Now all our errors are handled, minus the ones we don't know about yet.

Homework

  • Figure out why when multiple Catch blocks are used, it doesn't pass along the execution information (unable to get command/line).

    • Let me know why this is!

  • Where else could I have added error handling in any of these examples?

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Back to Top]

PowerShell: Getting Started - Jobs

PowerShell: Getting Started - Jobs

Getting Started - Jobs

PowerShell Background Jobs

Jobs in PowerShell allow you to run commands in the background. You use a command to start the job, and then various other commands to check in and receive information from it. Jobs can be started as 64-bit or 32-bit, regardless of what you launch your session as. You can also start jobs that run as a different user.  

Why Use PowerShell Jobs?

PowerShell jobs are handy for a lot of different uses. One could be running a script that contains multiple jobs that will perform different Active Directory tasks. You can run those in parallel, and use a While loop to check in on the jobs as they execute.

In this post you'll see how to start jobs, see running jobs, and get information as jobs complete. 

Creating Jobs

The command to start a job in PowerShell is Start-Job.

Let's use this command to start a job that will wait for one minute, and then execute the command Get-Process.

Start-Job -Name SleepProcess -ScriptBlock {Start-Sleep -Seconds 60; Get-Process}

The output of the job started is displayed after we execute the Start-Job command.

If you wanted to wait for this job to complete (which would make the console unusable until the job completes), you can use the Wait-Job command. With Wait-Job you'll need to specify the Job Name, Id, or simply pipe the Start-Job command to Wait-Job.

Start-Job -Name Wait -ScriptBlock {Start-Sleep -Seconds 60} | Wait-Job

And now we wait for the console to free up after 60 seconds have passed.

Monitoring Jobs

To keep an eye on the running jobs in PowerShell, we'll use the command Get-Job

Get-Job

Get-Job will show us all running jobs by default. We can also narrow it down to just our job via the Job Name or Job Id. Let's try getting this Job via the Job Id.

Get-Job -Id 1

If there were more Jobs running, we'd still only see this Job as we specified that we only want to see the Job with the Id value of 1.

Collecting Job Information

There are few different ways to collect information from running jobs in PowerShell. The official command to do it is Receive-Job. You'll need to specify the Job Name or Id when you use this command. Let's receive the information from this job, and specify the -Keep parameter so we can receive the information again later.

Receive-Job -Id 1 -Keep

You can see it displayed the output of Get-Process. Let's store the job in an object, and look at another way to receive the information.

This is a bit interesting. PowerShell stores most of the job information in a child job for each job you run. To access this information, you must access the ChildJobs property for the first child job, or 0. Let's take a look.

$ourOutput = Start-Job -Name GetOutput -ScriptBlock {Get-Process}

Here we started a job, and stored the object in the $ourOutput variable.

Now we can use that variable to take a look at the results.

$ourOutput

Let's see the available methods and properties for $ourOutput.

$ourOutput | Get-Member

Hey, there's the ChildJobs property! Let's take a look at it's properties and methods as well.

$ourOutput.ChildJobs[0] | Get-Member

Both $ourOutput and $ourOutput.ChildJobs[0] have the Output property. However, since our job is actually spawned as child job, this property will be empty on the initial object, and have a value only on the child object. 

$ourOutput.Output
$ourOutput.ChildJobs[0].Output

Error Handling

Error handling in jobs can be tricky, as sometimes a job state will show as completed, but the output of the job is actually a failed command. There are other times where a job will state that is actually has failed. In each case, there are two main spots to check for error information when a job fails.

Let's take a look at the two examples. I can replicate different states with the New-Item command and toggling the -ErrorAction parameter to Stop.

Job State Failed

$failJob = Start-Job -Name FailJob -ScriptBlock {New-Item -Path 'Z:\' -Name 'test' -ItemType Directory -ErrorAction Stop}

Let's look at the $failJob object to make sure it has indeed failed...

$failJob

Alright... now here is the weird part. Normally you'd look at the ChildJob's Error property to get the error message. That... or Receive-Job would show you an error. I have tried all of those approaches. It looks like Receive-Job will display the error, but you're unable to store it in a variable or parse the message in any way. Not helpful! 

$jobError = Receive-Job $failJob
$failJob.ChildJobs[0].Error
$jobError

So how do we see what happened? We need to look at the property $failJob.ChildJobs[0].JobStateInfo.Reason.Message to get the message of what happened (as a string). 

$failJob.ChildJobs[0].JobStateInfo.Reason.Message
$jobError = $failJob.ChildJobs[0].JobStateInfo.Reason.Message
$jobError

There we go, got the error! 

Job State Completed

There are times when the job will complete, and the error message will indeed be in the ChildJob's Error property. Let's take the -ErrorAction Stop off the New-Item command and start the job again.

$failJob = Start-Job -Name FailJob -ScriptBlock {New-Item -Path 'Z:\' -Name 'test' -ItemType Directory}

Now let's verify it shows as completed.

$failJob

This job has essentially the same error message as the other error example above. The difference is the error is stored in the ChildJob property of Error. This is one of the most confusing aspects of PowerShell jobs.  Receive-Job will display the error, but also like above not let you store it.  Not good for automation!

Here's how we get the error information this time.

$failJob.ChildJobs[0].Error
$jobError = $failJob.ChildJobs[0].Error
$jobError

Job Cleanup

Now that we've gone over how to create jobs, receive their information, and perform some error handling... let's move on to cleanup. 

The Remove-Job command will remove jobs from PowerShell. You can see this from start to finish with this example. 

$ourJob = Start-Job -Name CleanUp -ScriptBlock {Get-Process}
$ourJob
Get-Job

You can see the CleanUp job name amidst all of the other jobs I still have from this post. 

To remove this job only, you can use Remove-Job with the Job Name or Id. We can also pipe the variable we stored our job object in ($ourJob) to Remove-Job.

$ourJob | Remove-Job
Get-Job

And it's gone! What if we wanted to remove all of these jobs? You can actually pipe Get-Job to Remove-Job.

Get-Job | Remove-Job
Get-Job

As you can see, all of the jobs are now cleaned up.

Multiple Job Example

The glory of PowerShell Jobs is being able to run multiple commands at the same time. This example code will start up multiple jobs, and then use a While loop to monitor them. As they complete, the output and/or error messages will be output to a text file with the job name and date stamp as the file name. The output folder is C:\PowerShell\part10\output. 

Copy and paste the code in the ISE, and ensure you can write to C:\PowerShell\Part10. If you can't, you may see some nasty error messages, as I have not added any error handling to this example. Save the script as C:\PowerShell\Part10\part10.ps1.

Code

#Set the jobs variable to $true so the while loop processes at least once
$jobs         = $true
#Set the output folder path
$outputFolder = 'C:\PowerShell\part10\output'

#Create some jobs to monitor. One for each error handling example, and then one success that takes a minute to complete.
Start-Job -Name SleepProcess -ScriptBlock {Start-Sleep -Seconds 60; Get-Process}
Start-Job -Name FailJob -ScriptBlock {New-Item -Path 'Z:\' -Name 'test' -ItemType Directory -ErrorAction Stop} 
Start-Job -Name FailCompletedJob -ScriptBlock {New-Item -Path 'Z:\' -Name 'test' -ItemType Directory} 

#If the folder doesn't exist, create it
If (!(Test-Path $outputFolder)) {

    New-Item -Path $outputFolder -ItemType Directory

}

#While $jobs = $true...
While ($jobs) { #Begin $jobs While Loop

    #Store the jobs in $ourJobs
    $ourJobs = Get-Job

    Write-Host "Checking for jobs..."

    #Use a ForEach loop to iterate through the jobs
    foreach ($jobObject in $ourJobs) { #Begin $ourJobs ForEach loop
        
        #Null out variables used in this loop cycle
        $jobResults   = $null
        $errorMessage = $null
        $jobFile      = $null
        $jobCommand   = $null

        #Store the command used in the job to display later
        $jobCommand   = $jobObject.Command

        #Use the Switch statement to take different actions based on the job's state value
        Switch ($jobObject.State) { #Begin Job State Switch

            #If the job state is running, display the job info
            {$_ -eq 'Running'} {

                Write-Host "Job: [$($jobObject.Name)] is still running..."`n
                Write-Host "Command: $jobCommand"`n

            }

            #If the job is completed, create the job file, say it's been completed, and then perform an error check
            #Then display different information if an error is found, versus successful completion
            #Use a here-string to create the file contents, then add the contents to the file
            #Finally use Remove-Job to remove the job
            {$_ -eq 'Completed'} {
                
                #Create file
                $jobFile = New-Item -Path $outputFolder -Name ("$($jobObject.Name)_{0:MMddyy_HHmm}.txt" -f (Get-Date)) -ItemType File

                Write-Host "Job [$($jobObject.Name)] has completed!"

                #Begin completed but with error checking...
                if ($jobObject.ChildJobs[0].Error) {

                    #Store error message in $errorMessage
                    $errorMessage = $jobObject.ChildJobs[0].Error | Out-String

                    Write-Host "Job completed with an error!"`n
                    Write-Host "$errorMessage"`n -ForegroundColor Red -BackgroundColor DarkBlue

                    #Here-string that contains file contents
                    $fileContents = @"
Job Name: $($jobObject.Name)

Job State: $($jobObject.State)

Command:

$jobCommand

Error:

$errorMessage
"@

                    #Add the content to the file
                    Add-Content -Path $jobFile -Value $fileContents

                } else {
                    
                    #Get job result and store in $jobResults
                    $jobResults = Receive-Job $jobObject.Name

                    Write-Host "Job completed without errors!"`n
                    Write-Host ($jobResults | Out-String)`n

                    #Here-string that contains file contents
                    $fileContents = @"
Job Name: $($jobObject.Name)

Job State: $($jobObject.State)

Command: 

$jobCommand

Output:

$($jobResults | Out-String)
"@

                    #Add content to file
                    Add-Content -Path $jobFile -Value $fileContents

                }

                #Remove the job
                Remove-Job $jobObject.Name
             
            }

            #If the job state is failed, state that it is failed and then create the file
            #Add the error message to the file contents via a here-string
            #Then use Remove-Job to remove the job
            {$_ -eq 'Failed'} {

                #Create the file
                $jobFile    = New-Item -Path $outputFolder -Name ("$($jobObject.Name)_{0:MMddyy_HHmm}.txt" -f (Get-Date)) -ItemType File
                #Store the failure reason in $failReason
                $failReason = $jobObject.ChildJobs[0].JobStateInfo.Reason.Message 

                Write-Host "Job: [$($jobObject.Name)] has failed!"`n
                Write-Host "$failReason"`n -ForegroundColor Red -BackgroundColor DarkBlue
                
                #Here-string that contains file contents
                $fileContents = @"
Job Name: $($jobObject.Name)

Job State: $($jobObject.State)

Command: 

$jobCommand

Error:

$failReason
"@
                #Add content to file
                Add-Content -Path $jobFile -Value $fileContents

                #Remove the job
                Remove-Job $jobObject.Name
            }


        } #End Job State Switch
     
    } #End $ourJobs ForEach loop

    #Clear the $ourJobs variable
    $ourJobs = $null

    #Get the new list of jobs as it may have changed since we did some cleanup for failed/completed jobs
    $ourJobs = Get-Job 

    #If jobs exists, keep the loop running by setting $jobs to $true, else set it to $false
    if ($ourJobs) {$jobs = $true} else {$jobs = $false}

    #Wait 10 seconds to check for jobs again
    Start-Sleep -Seconds 10

} #End $jobs While Loop

Here's the code in action...

So there's the output to the console. Let's check out the C:\PowerShell\Part10\Output folder.

That looks good. Let's take a peek at each file's contents.

And there you have it! You can do whatever you'd like with the job information in the Switch statement.

Homework

  • Jobs are capable of being run in different ways, including remotely. There are also a handful of WMI command that can be started as jobs. To see more ways jobs can be used, check out these resources.
  • Add some error handling to the script example in this post. 
    • Post ideas in the comments!

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Back to Top]

PowerShell: Getting Started - All About Strings!

PowerShell: Getting Started - All About Strings!

Getting Started - All About Strings!

Welcome to part 9 of my series in getting started with Windows PowerShell!

In case you missed the earlier posts, you can check them out here:

We will be exploring:

What's a string?

Back in Part 1, I mentioned that everything in PowerShell is an object. That's true! Today I'll be going over one object type, and that's System.String. A string in PowerShell is simply an object with that type.

Let's start by using the command Get-Location, and using the variable $ourPath to hold that object. We'll then get the value of the variable, and see what object type is returned.

$ourPath = Get-Location

Let's see what $ourPath returns.

Now let's pipe $ourPath to Get-Member and see what object type, methods, and properties are returned.

$ourPath | Get-Member

The object TypeName is System.Management.Automation.PathInfo. To get the string returned, we need to drill down into the Path property.

$ourPath.Path should return a TypeName of System.String.

There it is! Now that we know that, we can move on to string manipulation. 

Manipulating Strings

Let's re-create the $ourPath variable and set it to become just the Path property of Get-Location. If you would like to follow along, open PowerShell and navigate to C:\PowerShell. Create a directory there named part9, and then navigate inside of that folder. 

You can use the following sequence of commands to create the folder if it doesn't exist, and then navigate to it.

$path = 'C:\PowerShell\part9'

New-Item -Path $path.SubString(0,$path.LastIndexOf('\')+1) -ItemType Directory -Name $path.SubString($path.LastIndexOf('\')+1)

Set-Location $path

Now that we're inside of the part9 folder, we can use the following command to store the path in the $ourPath variable.

$ourPath = (Get-Location).Path

Note: the $ourPath variable should match the $path variable. 

Now that we have our string stored in $ourPath, we can start manipulating it. I will be using the SubString and LastIndexOf methods. 

What if you wanted to just get the value of the child folder you're currently in? In this case that folder name would be part9.

We can use both SubString and LastIndexOf to perform this task.

SubString

The SubString method accepts two overload arguments. They are separated by a comma, and are numerical. Value 1 (left) is the value of where you'd like to start the SubString, and value 2 is how many characters you'd like to move to the right of where you started. For example, if we run the following command:

$ourPath.SubString(0,8)

You can see that the values of (0,8) with the string 'C:\PowerShell\part9' returns C:\Power. That is because we start at the first character (0), and end after the value (8) is reached.  Notice that from left to right, the letter 'r' is the eighth letter in the sequence.

Now that we know this, if we wanted to just return 'part9', we can use:

$ourPath.SubString(14,5)

There we go! We start out at (14), or 'p', and then end at (5), which puts us from p -> 9, returning part9.

For this example that's great, but what if we needed to make something auto-magically get the value of the child folder?

LastIndexOf

LastIndexOf accepts an overload argument that allows you to specify the character you'd like to look for. It will return the numerical value of its location in the string, the last time it is seen. That is to say, if we use the value '\' in a path, it will return the value of the where the very last occurrence of '\' is.

Let's take a look at:

$ourPath.LastIndexOf('\')

Awesome! This is handy as it will allow us to automatically get the name of the child folder, if need be. We would just need to start one value out from the LastIndexOf  '\'.

Now we can combine SubString, and LastIndexOf. We'll leave the second value of SubString blank, which will essentially tell SubString where to start, and then to give us everything after that. We will add +1 after the LastIndexOf method is called to add one to the value returned.

$ourPath.Substring($ourPath.LastIndexOf('\')+1)

Let's take a look at another example of this in use. I will get a list of folders on my F:\ drive, and then use a ForEach-Object loop to iterate through and only display the folder names. You can do this with Get-ChildItem as well, but that wouldn't help this example :)

$folders = Get-ChildItem F:\ -Directory
$folders | ForEach-Object { $_.FullName }

Now let's make that loop happen and only display the folder names.

$folders | ForEach-Object { $_.FullName.SubString($_.FullName.LastIndexOf('\')+1)}

While this has some practical uses, if you're using Get-ChildItem, it actually returns the Name property for you. We can use the following code to get the same result:

Get-ChildItem F:\ -Directory | Select-Object -ExpandProperty Name

Expanded Strings

An expanded string in PowerShell allows you to expand the values of variables inside of the string before it is executed and displayed. Expanded strings are surrounded in "double quotes". Let's take a look at this using the following code:

$test = 'This is a test'
Write-Host "Test: [$test]"

You can see that the Write-Host displayed the value of the $test variable inside of the expanded string.

Let's take a look at another example, this time using Get-Process, looking for Chrome.

$process = Get-Process Chrome

Write-Host "Process: $process"

Well that doesn't look nice! That's because there are both a lot of processes, as well as different properties of each process returned. The default isn't very pretty. If you want to expand an object's property inside of an expanded string, you'll need to encapsulate it inside of a sub-expression. Let's create a ForEach-Object loop, and then display the property name of each process in it.

$process | ForEach-Object {

    Write-Host "Process: $($_.Name)"

}

That looks much better!

What if we wanted to display the actual variable name in the output, and not its value? To do this, we'll need to use the escape character, or `

Let's take a look at that with the $test variable.

Write-Host "The value of `$test is: [$test]"

Literal Strings

Literal strings in PowerShell do not allow variable expansion, and will simply display what is inside of the quotes as-is. Literal strings are surrounded in 'single quotes'. Let's take a look at an example:

$test        = 'This is a test'
Write-Host 'This is a literal string: [$test]'

The -f Operator

The -f operator in PowerShell allows you to format strings in various predefined ways. You can apply formatting to characters and numbers. Here is one example of using the operator:

[array]$formatArray = @('you','values','them.')
$user               = (Get-ChildItem Env:\USERNAME).Value
$date               = Get-Date
"Your user name is {0}, and the time is [{1:HH}:{1:mm}:{1:ss}]" -f $user,$date

For more information on how to use the format operator, check out this post:

Converting To and From Strings

In PowerShell, you can use the ToString method to convert an object to a string. You can also declare a variable, cast as [string]. Either way works. Let's look at the following code: 

[int]$number = 10

$number | Get-Member

As expected, the object type for $number is Int32. Let's try converting that to a string via ToString.

$numberString = $number.ToString()

$numberString | Get-Member

Aha! Now the type is a String!

Now, finally, let's look at casting the variable with explicit types.

[string]$number | Get-Member

We can also make it an int again, just as easily.

[int]$number | Get-Member

Building a ScriptBlock

We can use expanded strings to build a scriptblock, which we can then use for other PowerShell commands. Let's start by declaring the variable $findProcess with the process we'd like to find. Then we can create the expression via the [scriptblock] member, and the Create method. We'll then set that overload to the command we'd like to run.

Then we'll use the command Start-Job to start a PowerShell job, and run that command. 

Here's the code:

$findProcess = 'chrome'

$expression = [scriptblock]::Create("Get-Process $findProcess | Select-Object ProcessName, CPU, Path | Format-List")

Start-Job -Name "$findProcess`Process" -ScriptBlock $expression

We used an expanded string to create the code being run in the script block, as well as to define the name of the job. We'll now use the following code to receive and clean up the job:

Receive-Job "$findProcess`Process"

Get-Job "$findProcess`Process" | Remove-Job

As you can see the job is received, and we have the output of the command we created and executed via PowerShell jobs. 

We used the expanded string to ensure we received and cleaned up the right job, and did so dynamically.

PowerShell jobs will be covered in a later portion of this series!

Homework

  • Play around with all the different methods and properties of strings in PowerShell!
  • Get a head start learning about PowerShell Jobs.
    • Get-Help about_jobs

[Back to Top]

PowerShell: Getting Started - Accepting Pipeline Input

PowerShell: Getting Started - Accepting Pipeline Input

Getting Started - Accepting Pipeline Input

Pipeline Review

The pipeline in PowerShell is what commands use when separated with the pipeline operator '|'. It allows commands to pass the output values to the next command in the sequence. You can pipe as many commands together as necessary to complete your task. 

Pipeline structure:

Command1 (output) -> | Command2 (output) -> | Command3

Using the pipeline

Let's start with Get-Process.

Get-Process

This command will give you a list of all running processes, and some information for each process.

What if we just cared about the first 10 processes, sorted by CPU usage?

Let's try:

Get-Process | Sort-Object CPU -Descending | Select-Object -First 10

The above command takes Get-Process, then pipes it to Sort-Object (which we have sorting by CPU, descending), and then pipes that to Select-Object so we can specify that we want to see the First 10 results only. 

We can use the same command, but specify -Last 10 to see the last 10 results.

Get-Process | Sort-Object CPU -Descending | Select-Object -Last 10

Pretty handy! What if we wanted to see this in a different way? You can take the above command, and then pipe it to Out-GridView.

Get-Process | Sort-Object CPU -Descending | Select-Object -Last 10 | Out-GridView

Accepting pipeline input

So now that we know how to work with the pipeline in PowerShell, how do we accept pipeline input in our code?

Accepting pipeline input is possible via parameter attributes available when using [cmdletbinding()].

There are two different parameter attributes we can use that will accept input from the pipeline. 

ValueFromPipeline

Let's take a look at ValueFromPipeline. This parameter attribute will take all values passed to it from the pipeline. To get started with this example, open the ISE and paste in the following code:

function Write-PipeLineInfoValue {
[cmdletbinding()]
param(
    [parameter(
        Mandatory         = $true,
        ValueFromPipeline = $true)]
    $pipelineInput
)

    Begin {

        Write-Host `n"The begin {} block runs once at the start, and is good for setting up variables."
        Write-Host "-------------------------------------------------------------------------------"

    }

    Process {

        ForEach ($input in $pipelineInput) {

            Write-Host "Process [$($input.Name)] information"

            if ($input.Path) {
        
                Write-Host "Path: $($input.Path)"`n
        
            } else {

                Write-Host "No path found!"`n -ForegroundColor Red

            }

        }

    }

    End {

        Write-Host "-------------------------------------------------------------------------------"
        Write-Host "The end {} block runs once at the end, and is good for cleanup tasks."`n

    }

}

Get-Process | Select-Object -First 10 | Write-PipeLineInfoValue

Go ahead and save it to C:\PowerShell\part8.ps1, and run the code.

Here are the results:

When accepting pipeline input, the input is handled in the Process {} block within the function. You can also use the Begin {} and End {} blocks as well. The Process block is mandatory, however Begin and End blocks are not.

The code in the begin block runs once when the function is invoked. It is where you can setup variables at the start of the code execution.

Begin {

        Write-Host `n"The begin {} block runs once at the start, and is good for setting up variables."
        Write-Host "-------------------------------------------------------------------------------"

    }

The code in the process block will process the pipeline input. It is cleanest, in my opinion, to handle each element of the pipeline (as it will typically come in as an array), in a ForEach loop. That way you can iterate through each item, and take action on it. 

Process {

        ForEach ($input in $pipelineInput) {

            Write-Host "Process [$($input.Name)] information"

            if ($input.Path) {
        
                Write-Host "Path: $($input.Path)"`n
        
            } else {

                Write-Host "No path found!"`n -ForegroundColor Red

            }

        }

    }

The code in the end block will run after all the elements in the pipeline are processed. This is a good place to cleanup variables, or anything else you need to do once, after the pipeline input is handled.

End {

        Write-Host "-------------------------------------------------------------------------------"
        Write-Host "The end {} block runs once at the end, and is good for cleanup tasks."`n

    }

ValueFromPipelineByPropertyName

This parameter attribute is a bit different than the one above. It will only accept the input from the pipeline if the incoming property name matches the parameter name. In this example, I will use the parameter name of $name to match any incoming property values of the property 'name'.

Go ahead and paste this code under the code we used earlier in part8.ps1

function Write-PipeLineInfoPropertyName {
[cmdletbinding()]
param(
    [parameter(
        Mandatory                       = $true,
        ValueFromPipelineByPropertyName = $true)]
    [string[]]
    $Name
)

    Begin {

        Write-Host `n"The begin {} block runs once at the start, and is good for setting up variables."
        Write-Host "-------------------------------------------------------------------------------"

    }

    Process {

        ForEach ($input in $name) {

            Write-Host "Value of input's Name property: [$($input)]"

        }

    } 

    End {

        Write-Host "-------------------------------------------------------------------------------"
        Write-Host "The end {} block runs once at the end, and is good for cleanup tasks."`n

    }

}

Get-Process | Select-Object -First 10 | Write-PipeLineInfoPropertyName

Let's run this code, and take a look at the results:

In this example we did not have to expand upon the incoming object's values, as it only gave us the property name values to begin with. That is, we didn't have to use $input.Name, we just had to use $input.

We can also run the above code with Get-Service in place of Get-Process, as it also returns a property name of 'name'.

Get-Service | Select-Object -First 10 | Write-PipeLineInfoPropertyName

Uses

There are many uses for accepting pipeline input. Let's say you need to get a list of specific computers that you need to perform actions on. You could simply pipe that to a function you created that would perform actions on each one.

If you create a logging function, you can also take bulk messages or commands and the pipe them to a function that creates or logs to a log file.

As with most things in PowerShell, there can be many ways to complete the same task. Sometimes piping commands together is cleaner than other methods, and sometimes it isn't. In the end it's up for you to decide.

Homework

  • Learn more about the pipeline!
    • Get-Help about_Pipelines
  • Try passing other commands to the functions we created today.

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Back to top]

PowerShell: Getting Started - Help!

PowerShell: Getting Started - Help!

Getting Started - Help!

Adding help

Adding help to your code is a great way to help people learn how to use it! It will also be useful for yourself if you have scripts you may use down the road, but do not need to use them all the time. It is easy to add, and maintain. The method of help I will be demonstrating is accessible via the Get-Help command. 

Comment block creation

To create comment based help in PowerShell, you'll need the following basic layout:

<#   
.SYNOPSIS   
   A brief description of what your code does.
.DESCRIPTION 
   A full description of what your code does.
.NOTES   
    Name: Script/function/module name
    Author: Your Name
    DateCreated: The current date
#>

It is important to note that the comment section begins with '<#', and ends with '#>'.

This block should go above all of the code you are going to use, and is the only thing in PowerShell allowed above the [cmdletbinding()] statement.

For this basic example we will display the synopsisdescription, and notes keywords. There are more keywords, and I will be going over those later in the accessing keywords section. All of the keyword identifiers must start with a period '.' for PowerShell to recognize them.

Here are some examples of the help block in action.

Script

Get-Help .\part7example.ps1

Function

To see this one we will need to import the module containing the function.

Import-Module .\part7examplemodule.psm1

Now we can use Get-Help, along with the function name, to display the help block's contents.

Get-Help Get-HelpExample

Accessing various keywords

You can access various keywords using different parameters with Get-Help. For this example I will be adding help to our function Write-Pretty from Part 6.

We'll be using the following comment based help block:

<#   
.SYNOPSIS   
   This function will output text in different ways depending on the arguments received.
.DESCRIPTION 
   Write-Pretty will output text in the following ways:

    ------
   |Random|
    ------
    Random will add some spacing and split your text into an array using a space. It will take each part of the array and apply a random color to it.

    [Random] will be prepended to the text.

    -----
   |Error|
    -----
    Error will output the text as red.

    [Error] will be prepended to the text.

    -------
   |Warning|
    -------
    Warning will output the text as yellow.

    [Warning] will be prepended to the text.

    ----
   |Info|
    ----
    Info will output the text as as white, as is the default option if you do not specify any parameters. 

    [Info] will be prepended to the text.
   
.PARAMETER prettyText

    Alias: Text
    Argument: This parameter is the text you'd like to affect the look of.

    This parameter accepts value from the pipeline.
   
.PARAMETER textType
    Alias: Type
    Argument: This parameter affects the type of text you'd like to display.

    Valid types:
    Random
    Error
    Warning
    Info

.NOTES   
    Name: part7.psm1
    Author: Ginger Ninja (Mike Roberts)
    DateCreated: 5/8/16

.LINK  
    http://www.gngrninja.com/script-ninja/2016/5/8/powershell-getting-started-part-7-help  
       
.EXAMPLE   
    Write-Pretty -Text 'This is some pretty text.' -Type Random
    ---------------------------------------------------------------

    [Random] This is some pretty text

.EXAMPLE   
    Write-Pretty -Text 'This is some pretty text, and it is now an error.' -Type Error
    ---------------------------------------------------------------

    [Error] This is some pretty text, and it is now an error

.EXAMPLE   
    Write-Pretty -Text 'Warning, this will output warning text!' -Type Warning
    ---------------------------------------------------------------

    [Warning] Warning, this will output warning text!

.EXAMPLE   
    Write-Pretty -Text 'This will output info text.' -Type Info
    ---------------------------------------------------------------

    [Info] This will output info text.

.EXAMPLE  
    Write-Pretty "I wonder what happens if we don't specify an option..."
    ---------------------------------------------------------------

    [Info] I wonder what happens if we don't specify an option...

.EXAMPLE
    Get-Process | Select-Object -ExpandProperty Name -First 5 | Sort-Object Name | Write-Pretty
    ---------------------------------------------------------------

    [Info] Battlenet
    [Info] Battlenet Helper
    [Info] AGSService
    [Info] AdobeUpdateService
    [Info] Agent
#>

I've added some more keywords so we can use Get-Help in various ways with the function Write-Pretty. The link keyword allows you to add a link for use with the -online parameter. The parameter keyword allows you to display help information for each parameter via the -parameter <name> parameter and argument. I also added some examples of the code being used, and expected output via the examples keyword. This keyword can be access via the -examples parameter with Get-Help.

Let's take a look at the full code for this post. If you'd like to try it out for yourself, open the ISE and save the code as C:\PowerShell\part7.psm1.

#Begin function Write-Pretty
function Write-Pretty {
<#   
.SYNOPSIS   
   This function will output text in different ways depending on the arguments received.
.DESCRIPTION 
   Write-Pretty will output text in the following ways:

    ------
   |Random|
    ------
    Random will add some spacing and split your text into an array using a space. It will take each part of the array and apply a random color to it.

    [Random] will be prepended to the text.

    -----
   |Error|
    -----
    Error will output the text as red.

    [Error] will be prepended to the text.

    -------
   |Warning|
    -------
    Warning will output the text as yellow.

    [Warning] will be prepended to the text.

    ----
   |Info|
    ----
    Info will output the text as as white, as is the default option if you do not specify any parameters. 

    [Info] will be prepended to the text.
   
.PARAMETER prettyText

    Alias: Text
    Argument: This parameter is the text you'd like to affect the look of.

    This parameter accepts value from the pipeline.
   
.PARAMETER textType
    Alias: Type
    Argument: This parameter affects the type of text you'd like to display.

    Valid types:
    Random
    Error
    Warning
    Info

.NOTES   
    Name: part7.psm1
    Author: Ginger Ninja (Mike Roberts)
    DateCreated: 5/8/16

.LINK  
    http://www.gngrninja.com/script-ninja/2016/5/8/powershell-getting-started-part-7-help  
       
.EXAMPLE   
    Write-Pretty -Text 'This is some pretty text.' -Type Random
    ---------------------------------------------------------------

    [Random] This is some pretty text

.EXAMPLE   
    Write-Pretty -Text 'This is some pretty text, and it is now an error.' -Type Error
    ---------------------------------------------------------------

    [Error] This is some pretty text, and it is now an error

.EXAMPLE   
    Write-Pretty -Text 'Warning, this will output warning text!' -Type Warning
    ---------------------------------------------------------------

    [Warning] Warning, this will output warning text!

.EXAMPLE   
    Write-Pretty -Text 'This will output info text.' -Type Info
    ---------------------------------------------------------------

    [Info] This will output info text.

.EXAMPLE  
    Write-Pretty "I wonder what happens if we don't specify an option..."
    ---------------------------------------------------------------

    [Info] I wonder what happens if we don't specify an option...

.EXAMPLE
    Get-Process | Select-Object -ExpandProperty Name -First 5 | Sort-Object Name | Write-Pretty
    ---------------------------------------------------------------

    [Info] Battlenet
    [Info] Battlenet Helper
    [Info] AGSService
    [Info] AdobeUpdateService
    [Info] Agent
#>  
    [cmdletbinding()]
    param(
    [Parameter(
                Mandatory         = $True,
                ValueFromPipeline = $True
               )]
    [Alias('Text')]
    $prettyText,
    [Parameter(Mandatory=$false)]
    [Alias('Type')]
    $textType
    )

    Begin {
    
        #Create a space before the text is displayed.
        Write-Host `n 

    }

    Process { #Begin process for Write-Pretty function

        ForEach ($textItem in $prettyText) { #Begin ForEach loop to handle prettyText input (normal and via pipeline)

            Switch ($textType) { #Begin switch for textType argument

                {$_ -eq 'Random'} {

                    Write-Host -NoNewline "[" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15) 
                    Write-Host -NoNewline "R" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15)
                    Write-Host -NoNewline "andom" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15)
                    Write-Host -NoNewline "]" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15)

                    #Split the text into an array, split by spaces. (Also turns it into a string before the split).
                    #We needed to turn it into a string in case the type wasn't string when it was received. Or else the .Split() method wouldn't work
                    $writeText  = $textItem.ToString().Split(' ')

                    #Change the text color for each element in the array.
                    ForEach ($text in $writeText) {

                        Write-Host -NoNewLine " $text" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15)

                    }

                    Write-Host `n
            
                }

                {$_ -eq 'Error'} {

                    Write-Host -NoNewline "[" -ForegroundColor White 
                    Write-Host -NoNewline "Error" -ForegroundColor Red -BackgroundColor DarkBlue
                    Write-Host -NoNewline "]" -ForegroundColor White 
                    Write-Host " $textItem" -ForegroundColor Red 

                }


                {$_ -eq 'Warning'} {

                    Write-Host -NoNewline "[" -ForegroundColor White
                    Write-Host -NoNewline "Warning" -ForegroundColor Yellow -BackgroundColor Blue
                    Write-Host -NoNewline "]" -ForegroundColor White
                    Write-Host " $textItem" -ForegroundColor Yellow


                }

                {$_ -eq 'Info' -or $_ -eq $null} {

                    Write-Host -NoNewline "[" -ForegroundColor White
                    Write-Host -NoNewline "Info" -ForegroundColor Green -BackgroundColor Black
                    Write-Host -NoNewline "]" -ForegroundColor White
                    Write-Host " $textItem" -ForegroundColor White

                }

                #The default option will simply write the text with no changes. This is if you do not specify a valid option for textType.
                Default { 
        
                    Write-Host $textItem
        
                }

            } #End switch for textType argument

        } #End ForEach loop to handle prettyText input (normal and via pipeline)

    } #End process for Write-Pretty function

    End {
    
        Write-Host `n

    }

} #End function Write-Pretty

#Display this message when the module is imported.
Write-Pretty -Text 'Part7 module loaded!' -textType Random

Now we'll need to import the module...

Import-Module .\part7.psm1

Now that it is imported, let's look at the various ways to access the help for the Write-Pretty function!

Get-Help

Get-Help Write-Pretty

Detailed

This will show us all of the available keywords.

Get-Help Write-Pretty -Detailed

Online

This will send us to the website (URI) defined in the link keyword.

Get-Help Write-Pretty -Online

Parameters

This one will display the information defined in the specified parameter keyword.

Get-Help Write-Pretty -Parameter prettyText
Get-Help Write-Pretty -Parameter textType

Examples

Get-Help Write-Pretty -Examples

This will display all the examples defined via the example keyword.

Adding comments to your code

Comments in your code provide messages to anyone editing the file. It can be useful for others that are reviewing your code (to see what it's doing, or more importantly, WHY it is doing what it is doing). You can add comments by simply starting the line with '#'

#This is an example comment. It will not be executed by PowerShell, but can be read by someone editing the script.

I like to add comments to the following areas of my code:

  • Code that performs a specific task that may seem out of place.
  • The beginning and end of functionsloops, ifs,  and switches
  • Any statements or block of code that are long, and it may be hard to see where they begin and end.

I added comments to the code we used in this post. Here is a snippet, which encompasses the Process {} statement in the Write-Pretty function. There are also some other comments inside this example as well.

Process { #Begin process for Write-Pretty function

        ForEach ($textItem in $prettyText) { #Begin ForEach loop to handle prettyText input (normal and via pipeline)

            Switch ($textType) { #Begin switch for textType argument

                {$_ -eq 'Random'} {

                    Write-Host -NoNewline "[" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15) 
                    Write-Host -NoNewline "R" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15)
                    Write-Host -NoNewline "andom" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15)
                    Write-Host -NoNewline "]" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15)

                    #Split the text into an array, split by spaces. (Also turns it into a string before the split).
                    #We needed to turn it into a string in case the type wasn't string when it was received. Or else the .Split() method wouldn't work
                    $writeText  = $textItem.ToString().Split(' ')

                    #Change the text color for each element in the array.
                    ForEach ($text in $writeText) {

                        Write-Host -NoNewLine " $text" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15)

                    }

                    Write-Host `n
            
                }

                {$_ -eq 'Error'} {

                    Write-Host -NoNewline "[" -ForegroundColor White 
                    Write-Host -NoNewline "Error" -ForegroundColor Red -BackgroundColor DarkBlue
                    Write-Host -NoNewline "]" -ForegroundColor White 
                    Write-Host " $textItem" -ForegroundColor Red 

                }


                {$_ -eq 'Warning'} {

                    Write-Host -NoNewline "[" -ForegroundColor White
                    Write-Host -NoNewline "Warning" -ForegroundColor Yellow -BackgroundColor Blue
                    Write-Host -NoNewline "]" -ForegroundColor White
                    Write-Host " $textItem" -ForegroundColor Yellow


                }

                {$_ -eq 'Info' -or $_ -eq $null} {

                    Write-Host -NoNewline "[" -ForegroundColor White
                    Write-Host -NoNewline "Info" -ForegroundColor Green -BackgroundColor Black
                    Write-Host -NoNewline "]" -ForegroundColor White
                    Write-Host " $textItem" -ForegroundColor White

                }

                #The default option will simply write the text with no changes. This is if you do not specify a valid option for textType.
                Default { 
        
                    Write-Host $textItem
        
                }

            } #End switch for textType argument

        } #End ForEach loop to handle prettyText input (normal and via pipeline)

    } #End process for Write-Pretty function

Homework

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Top]

PowerShell: Getting Started - Modules

PowerShell: Getting Started - Modules

Getting Started - Modules

Welcome to my Getting Started with Windows PowerShell series!

In case you missed the earlier posts, you can check them out here:

We will be exploring:

Note: I reccommend getting used to / switching to Visual Studio Code if you can. It is lightweight, has GREAT extensions/themes, and can be used for much, much more than just PowerShell. Check it out here: https://code.visualstudio.com/docs/languages/powershell

What are modules?

Modules in PowerShell are a way to aggregate functions into one file and then re-use them later. There are various different module types, but we'll be focusing on Script Modules. If you find yourself reusing functions here and there, it may be time to create a module in PowerShell. 

If you are here, already know all of this, and want a framework for creating a standard PowerShell module, check out this project on GitHub: https://github.com/devblackops/Stucco.

Script modules require these basic items:

  • A file with code in it (for script modules this will be a .psm1 file)

  • Any required modules / scripts that the module itself requires

  • A folder that has the module name stored where PowerShell can import the module

  • This isn't exactly required, but is is immensely helpful: a manifest file that contains metadata (version/author/file info).

List available modules

In PowerShell, you can see what modules are available to you by using the following command:

Get-Module -ListAvailable

This command will list the modules available to you, and show you which folder they are located in.

To  see what modules you are currently using in your session, type:

Get-Module

If you 'd like to see what commands a module has available, use:

Get-Command -Module DnsClient

The above command will show us the commands available in the DnsClient module.

Let's try to use one of the commands from the DnsClient module.

Clear-DnsClientCache

By all appearances it looks like that worked! Interesting. Let's have a look at seeing what modules are installed again.

Get-Module

It looks like the module was automatically imported into PowerShell when we used one of its commands. That is because starting in PowerShell version 3.0, modules that are available via Get-Module -ListAvailable will be automatically imported if one of their commands are used.

Creating modules

PowerShell modules available via Get-Module -ListAvailable are stored in folders listed in the environment variable PSModulePath. Use the following commands to see where the folders are:

(Get-ChildItem Env:\PSModulePath).Value.Split(';')

The folders:

  • C:\Program Files\WindowsPowerShell\Modules

  • C:\Windows\system32\WindowsPowerShell\v1.0\Modules

Contain modules that will be available to anybody using PowerShell on the local machine.

Let's keep the scope to not require administrative rights (which those folders require to change at all), and create the Documents\WindowsPowerShell\Modules folder.

The full path for my instance of PowerShell can be found by using:

(Get-ChildItem Env:\PSModulePath).Value.Split(';')[0]

Find out if that matches your Documents folder as well. If it does not, iterate through the array by using [1] or [2] at the end of the Get-ChildItem command.

When you find the one that matches, use the following command to create the folder:

New-Item -Path (Get-ChildItem Env:\PSModulePath).Value.Split(';')[0] -ItemType Directory

Navigate to that location via:

Set-Location (Get-ChildItem Env:\PSModulePath).Value.Split(';')[0]

Create a folder for our module named Part6. Note: This is what PowerShell will display the module name as.

New-Item 'Part6' -ItemType Directory

Now launch the ISE, and paste the following code in:

function Write-Pretty {
    [cmdletbinding()]
    param(
    [Parameter(
                Mandatory         = $True,
                ValueFromPipeline = $True
               )]
    [Alias('Text')]
    $prettyText,
    [Parameter(Mandatory=$false)]
    [Alias('Type')]
    $textType
    )

    Begin {
    
        Write-Host `n 

    }

    Process {

        ForEach ($textItem in $prettyText) {

            Switch ($textType) {

                {$_ -eq 'Random'} {

                    Write-Host -NoNewline "[" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15) 
                    Write-Host -NoNewline "R" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15)
                    Write-Host -NoNewline "andom" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15)
                    Write-Host -NoNewline "]" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15)

                    $writeText  = $textItem.ToString().Split(' ')

                    ForEach ($text in $writeText) {

                        Write-Host -NoNewLine " $text" -ForegroundColor $(Get-Random -Minimum 1 -Maximum 15)

                    }

                    Write-Host `n
            
                }

                {$_ -eq 'Error'} {

                    Write-Host -NoNewline "[" -ForegroundColor White 
                    Write-Host -NoNewline "Error" -ForegroundColor Red -BackgroundColor DarkBlue
                    Write-Host -NoNewline "]" -ForegroundColor White 
                    Write-Host " $textItem" -ForegroundColor Red 

                }


                {$_ -eq 'Warning'} {

                    Write-Host -NoNewline "[" -ForegroundColor White
                    Write-Host -NoNewline "Warning" -ForegroundColor Yellow -BackgroundColor Blue
                    Write-Host -NoNewline "]" -ForegroundColor White
                    Write-Host " $textItem" -ForegroundColor Yellow


                }

                {$_ -eq 'Info' -or $_ -eq $null} {

                    Write-Host -NoNewline "[" -ForegroundColor White
                    Write-Host -NoNewline "Info" -ForegroundColor Green -BackgroundColor Black
                    Write-Host -NoNewline "]" -ForegroundColor White
                    Write-Host " $textItem" -ForegroundColor White

                }

                Default { 
        
                    Write-Host $textItem
        
                }

            }

        }

    }

    End {
    
        Write-Host `n

    }

}

Write-Pretty -Text 'Part6 module loaded!' -textType Random

Once you have the content in the ISE, save the file in the Part6 folder we created as part6.psm1.

Congratulations! You just created your first module file.

Navigate to the folder containing Part6.psm1 in PowerShell.

cd .\Part6\

Use the following command to create the module manifest file:

New-ModuleManifest -Path .\part6.psd1 -NestedModules 'part6.psm1'

Use Get-ChildItem to list out the files. You should see part6.psm1 and part6.psd1.

The module manifest file contains the metadata for your module. Check out this link for more details on manifest files: https://msdn.microsoft.com/en-us/library/dd878337(v=vs.85).aspx

Import a module

Let's import the module we created!

Make sure you see it via:

Get-Module -ListAvailable

Now that we see it, let's manually import it via:

Import-Module Part6

If all goes well, you should see a confirmation that the module was installed.

With this module, we get a new command: Write-Pretty.

You can use:

Write-Pretty Test
Write-Pretty Test -textType Random
Write-Pretty Test -textType Error
Write-Pretty Test -textType Warning

You can also pipe a command to Write-Pretty. Let's try:

Get-Process | Write-Pretty

What about...

Get-Process | Write-Pretty -textType Random

Remove a module

To remove the module, simply run:

Remove-Module Part6

When developing your own modules, Import-Module and Remove-Module will be your friends. When you import a module, it is imported into memory in its current state at that moment. That means if you make any changes after importing, you will have to remove and then re-import the module.

Homework

  • Go over the code we used in the module!

    • How did we accept pipeline input?

    • What method did we use to iterate through each element received by the pipeline?

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Back to top]

 

 

 

PowerShell: Getting Started - Loop-the-Loop

PowerShell: Getting Started - Loop-the-Loop

Getting Started - Loop-the-Loop

Loop used in Part 4

We actually ended up using a loop in Part 4 to iterate through each disk and display information regarding the disk.

Code for loop used in Part 4:

Foreach ($disk in $disks) {

                Write-Host `n"Device ID    : $($disk.DeviceID)"
                Write-Host ("Free Space   : {0:N2} GB" -f $($disk.FreeSpace / 1GB))
                Write-Host ("Total Size   : {0:N2} GB" -f $($disk.Size / 1GB))
                Write-Host ("% Free       : {0:P0}   " -f $($disk.FreeSpace / $disk.Size))
                Write-Host "Volume Name  : $($disk.VolumeName)"
                Write-Host "Drive Type   : $(Get-WMIInfo -lookup 'drivenumber' -typeNumber $disk.DriveType)"`n

            }

This is called a ForEach loop and what it does is iterates through and array of objects, allowing you to declare the item as well as the array or collection.

ForEach ($item in $collection) { $item.Property }

In this case the item we declare is $disk, from the array/collection $disks.

What this allowed us to do was go through each disk, and for each disk display that current item's information individually. 

Handling individual items

Since $disk represents the current item in the loop, we can use $disk.Property to display the properties we want. The last line in this example uses the Get-WMIInfo function to assign the drive type text to the number we supply. That happens once for each item in the loop, therefore different drives (CD-ROM, HDD, etc) will return as different types.

The many different loops in PowerShell

There are many different types of loops in PowerShell. You can actually use any of them to perform the same task. There is usually a "best loop" for any given task, however. 

To get started, let's create and save part5.ps1 in C:\PowerShell (or wherever you prefer).

Note

If you run code that performs an infinite loop in PowerShell, use CTRL+C to break out of the loop.

The ForEach loop

What's it do?

The ForEach loop in PowerShell iterates through an array/collection. It allows you to define the element or item you wish to use as the placeholder for the current object in the loop.

Loop setup

The ForEach loop is setup as follows:

ForEach (item/element in array/collection) {

Do-Stuff

}

Element can be anything you specify. It makes sense to make it reflect what is being handled as the current item, however. 
Array is the object/array/collection you want to iterate through.

Example

Copy and paste the following into part5.ps1 (or the PowerShell console):

$processes = Get-Process

$i = 1
ForEach ($process in $processes) {

    Write-Host "Process[$i]'s Name is $($process.Name)"

    $i++

}

Here's the output of the code above:

In this example we declared $processes to hold the results of Get-Process$i as 1, then our ForEach loop with $process as the placeholder, and finally $processes as the item to iterate through.

The action we take is to Write-Host the current value of $i, and the current element's ($process)'s Name via $($process.Name).

We then take the final action of iterating $i by 1 via $i++.  $i++ is simply a shortcut that tells PowerShell to add one to the current value of $i

ForEach-Object

There's a pipeline variation of the ForEach loop named ForEach-Object.

To use it you would simply run the command you want, and then pipe it to ForEach-Object. You would then use the special placeholder variable in PowerShell ($_) to take action on the current element in the array. 

Here is an example:

Copy and paste the following into part5.ps1 (or the PowerShell console):
Run the code (starting with $i = 1) as a selection, rather than the whole script.

$i = 1
Get-Process | ForEach-Object {

    Write-Host "Process[$i]'s Name is $($_.Name)"
    $i++

}

This will perform the same actions as the other loop, but in a different way.

I generally like to store commands I'm calling into variables, and because of that I use the ForEach loop more. ForEach-Object does have it's place though, especially when you're running advanced commands and you need to perform actions from piped in input.

When to use a ForEach loop

I use a ForEach loop when there is an action I want to take on an array of items. The action could be anything from deleting specific files, or merely displaying information.

The for loop

What's it do?

The for loop performs actions until a specific condition is met.

Loop setup

The for loop is setup as follows:

for (init;condition;repeat) {

Do-Stuff

}

Here's what the init, condition, and repeat represent:

init = The statement/action that runs first (once). Typically it is the variable to be initialized and used in the loop.
condition = Statement that must be true for the loop to continue running
repeat = Action to take after a loop cycle

Examples

Copy and paste the following into part5.ps1 (or the PowerShell console):
Run the code (starting with for ($i=1;$i -le 15;$i++) {) as a selection, rather than the whole script.

for ($i=1;$i -le 15;$i++) {

    Write-Host "This is the color for [$i]" -ForegroundColor $i

}

You should see the following output:

For this example we set the init as $i=1. This initializes $i with the value of 1.

We then set the condition as $i -le 15. What that states is the loop will run as long as $i is less than (or equal to) 15.

Finally, we set the repeat to $i++. 

Write-Host takes 1-15 as values for the ForeGround color, which this loop iterates through.

For loop flexibility

You can actually specify any or none of the conditions for initcondition, and/or repeat.

For example, this works:

for () {Write-Host "Wheeeeeeeeeeeeeeee!"}

Which results in:

I'll note again that CTRL+C is your friend when your loop is infinite and you want to break out of it in PowerShell.

You can also specify the various elements outside of the loop statement declaration.

Copy and paste the following into part5.ps1 (or the PowerShell console):
Run the code (starting with $i = 0) as a selection, rather than the whole script.

$i = 0

for (;$i -le 20;) {

    Write-Host "`$i is [$i] iterations old!"

    $i++

}

Note: In this example it is important to understand that there are semicolons around the condition. That is to tell PowerShell there is no init specified in the for loop statement, that there is a condition, and finally that there is no repeat defined.

Here's the output:

When to use a for loop

I generally use for loops when I want to run the same set of code multiple times for various reasons. 

Remember the ForEach loop with Get-Process earlier?

One of the awesome things about PowerShell is you can do one thing many different ways.

Here's a for loop iterating through $processes its own way:

$processes = Get-Process
for ($i = 0;$i -le $processes.count;$i++) {Write-Host "Process[$i]'s Name is $($processes[$i].Name)"}

We set the the condition to be when $i is less than (or equal to) $processes.count.

We then modify the Write-Host statement a bit to reflect the current element in the $processes array via $processes[$i]. All arrays in PowerShell start with 0, which is why we set the init as $i = 0.  

The While loop

What's it do?

The While loop in PowerShell performs actions while a condition is true.

Loop setup

While (condition) {

Do-Stuff

}

The setup is easy for this one! Condition reflects any statement / expression that is true.

Example

For this example we'll be using Notepad.exe. If you have any Notepad windows open, close them!

Copy and paste the following into part5.ps1 (or the PowerShell console):
Run the code (starting with $notepad = Get-Process Notepad) as a selection, rather than the whole script.

$notepad = Get-Process Notepad

While ($notepad.Count -le 5) {

    Write-Host "Starting Notepad, current count is: $($notepad.Count + 1)"

    Start-Process Notepad.exe

    $notepad = Get-Process Notepad
    
}

Here is the result:

In this example we declare $notepad as the result of Get-Process Notepad. We then start the loop with the condition that $notepad.Count is less than (or equal to) 5

When the condition is true we use Write-Host to display the current number of Notepad windows open, open another Notepad window via Start-Process, and then declare $notepad as the result of Get-Process Notepad. That last step is perhaps the most important. If we did not do that, we'd have an infinite loop, and not a fun one.

Note:

We will use Notepad as an example in another loop. To close all open Notepad windows at once, run:

Get-Process Notepad | Stop-Process

BE SURE TO INCLUDE NOTEPAD IN GET-PROCESS!

When to use a While loop

I generally use while loops to perform actions I need to perform, while a specific condition is true. This could be while a specific process exists that you would like to close, or while PowerShell jobs exist. We haven't covered jobs yet, but we will do that in a later post!

The Do While loop

What's it do?

The Do While loop in PowerShell performs actions while a condition is true. The difference between Do While and While is that even if the condition is not true, the Do actions still run at least once. Whereas with the While loop the actions do not ever run if the condition is not true.

Loop setup

Do {

Do-Stuff

} While (condition) 

For the Do While loop we declare Do { Do-Stuff } for our action, and then finally the While condition afterwards.

Examples

Here is an example demonstrating that the Do actions always run before the condition is evaluated.

Copy and paste the following into part5.ps1 (or the PowerShell console):
Run the code (starting with $i = 14) as a selection, rather than the whole script.

$i = 14

Do {

    Write-Host "This will still run once! `$i = $i"

} While ($i -gt 15)

$i is never greater than 15, yet the Do code still executes at least once.

Here's another example of a Do While loop in action:

Copy and paste the following into part5.ps1 (or the PowerShell console):
Run the code (starting with $i = 0) as a selection, rather than the whole script.

$i = 0

Do {

    Write-Host "Now we're cookin'! `$i = $i"
    $i++

} While ($i -le 15)

This will add one to $i while $i is less than (or equal to) 15.

When to use the Do While loop

I use the Do While loop when I want to have the While loop going, yet no matter what need to perform the actions of the Do statement at least once.

The Do Until loop

What's it do?

The Do Until loop performs actions UNTIL a condition is true. This is the opposite of what we're doing with the Do While loop. Like the Do While loop, however, the Do statements are run at least once.

Loop setup

Do {

Do-Stuff

} Until (condition)

The setup for this is very similar to the Do While loop. The only difference here is we're using Until, not While

Example

Copy and paste the following into part5.ps1 (or the PowerShell console):
Run the code (starting with Do {) as a selection, rather than the whole script.

Do {

    $notepad = Get-Process Notepad

    Start-Process Notepad.exe

    Write-Host "Starting Notepad, current count is: $($notepad.Count + 1)"

    $notepad = Get-Process Notepad


} Until ($notepad.Count -eq 5)

This should execute Notepad exactly 5 times, given that it was not open before running the code.

Notice how we changed the condition to be -eq and not -le. So Until $notepad.Count equals 5, we're performing the loop actions of Do.

When to use the Do Until loop

I seldom have used the Do Until loop, but it has its place. You generally would use it when you need to run commands that should execute until a specific condition is true. The condition could be that a specific process is running, or a number you're keeping track of reaches a specific value.

Comparison Operators

I've use a few comparison operators in these examples. Comparison operators evaluate values you place on either side. Here are some examples.

-eq
The -eq comparison operator evaluates whether one value equals another. For example:

1 -eq 1

The above example would return True 

-ne
The -ne comparison operator is the opposite of the -eq comparison operator. For example:

1 -ne 1

The above example would return False

-gt
The -gt comparison operator evaluates values to see if one is greater than the other. For example:

2 -gt 1

The above example would return True

-ge
The -ge comparison operator evaluates values to see if one is greater than or equal to the other. For example:

2 -ge 2

The above example would return True

-lt
The -lt comparison operator evaluates values to see if one is less than the other. For example:

1 -lt 2

The above example would return True

-le
The -le comparison operator evaluates values to see if one is less than or equal to the other. For example:

2 -le 2

The above example would return True

-like
The -like comparison operator evaluates values to see if one is like the other, allowing a wildcard comparison to take place. For example:

'onetwothree' -like '*one*'

The above example would return True

-notlike
The -notlike comparison operator evaluates values to see if one is not like the other, allowing a wildcard comparison to take place. For example:

'onetwothree' -notlike '*one*'

The above example would return False

There's more!

For a deeper dive into comparison operators, use this command:

Get-Help About_Comparison_Operators

You could also check out this link: http://ss64.com/ps/syntax-compare.html

Homework

  • Pick one of the loop examples and get it working with each of the different loop types. (It's possible!)

  • Learn more about comparison operators

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Back to top]

PowerShell: Getting Started - A Deeper Dive Into Functions

PowerShell: Getting Started - A Deeper Dive Into Functions

Getting Started - A Deeper Dive Into Functions

Part 3 Review

At the end of Part 3, we created the following functions:

function Get-OSInfo {

    $osInfo = Get-WmiObject -Class Win32_OperatingSystem

    $versionText = Get-VersionText -version $osInfo.Version.Split('.')[0]

    Write-Host "You're running $versionText"

}


function Get-VersionText {
    param($version)

    Switch ($version) {

        10{$versionText = 'Windows 10'}

        6.3 {$versionText = 'Windows 8.1 or Server 2012 R2'}

        6.2 {$versionText = 'Windows 8 or Server 2012'}

        6.1 {$versionText = 'Windows 7 or Server 2008 R2'}

        6.0 {$versionText = 'Windows Vista or Server 2008'}

        5.2 {$versionText = 'Windows Server 2003/2003 R2'}

        5.1 {$versionText = 'Windows XP'}

        5.0 {$versionText = 'Windows 2000'}

    }

    Return $versionText

}

Get-OSInfo

Get-VersionText issue explored

Let's go ahead and run that script in the ISE (or copy and paste it into the console).

Here's the output I get:

Stay in the console pane if you're in the ISE, and let's run the following command:

Get-VersionText -version 6

Here's the output:

This presents a problem! The way we coded the version we send to Get-VersionText takes only the first part of the version number before the "." in the version returned from the Get-WMIObject command. To see this, run the following commands:

$osInfo = Get-WmiObject -Class Win32_OperatingSystem
$osInfo

The version we're sending the Get-VersionText function is gathered using the following code:

$osInfo.Version.Split('.')[0]

For me, running Windows 10, that is fine! But if you are running Windows 7, it would have just been 6! That would not have returned the correct version (as we saw above). The version we need to match correctly to display the text for Windows 7 is 6.1.

The fix

Let's look at a possible solution.  We need to get the numbers before the last decimal point in the Version property. There are two string manipulation methods that can help us out here. They are SubString and LastIndexOf.

SubString takes two arguments to display a specific part of the string based on the numerical values you specify. 

LastIndexOf takes one argument as the text to look for the last instance of. It then returns the numerical value of where it matches. 

Here is the solution to store the correct version number to look up:

$versionNumber = $osInfo.Version.SubString(0,$osInfo.Version.LastIndexOf('.'))

If you want to see what LastIndexOf does on its own (with value of '.') run:

$osInfo.Version.LastIndexOf('.')

So we're running $osInfo.Version.SubString(0,4) when we use  $osInfo.Version.SubString(0,$osInfo.Version.LastIndexOf('.')) **And the version is 10.x.xxx)**

That's why we use LastIndexOf and not a hard set numerical value. Let's look at setting up a test to see that in action with a different version number. 

$testVersion = '6.1.1337'
$testVersion.SubString(0,4)
$testVersion.SubString(0,$testVersion.LastIndexOf('.'))

You should see the following results:

Notice that if we used (0,4) we got an extra '.' at the end as a result. But if we use LastIndexOf, it returns the correct number for SubString to use every time for what we need.

Getting the functions to work

To get the code from Part 3 to work now, we'll have to modify the Get-OSInfo function to add the new line to get the version number, and then use that variable when calling the Get-VersionText function.

We'll also want to declare the parameter type in Get-VersionText for the version parameter to [double].

The reason we want to set the type for this parameter is so that it will take 10.0, and essentially make it 10. However, it will also take 10.1 or 6.1 and keep it at that value. To see that, use the following commands:

[double]10.0
[double]10.1

Perfect, that's just what we want to make sure our Switch statement works in the Get-VersionText function for Windows 10. Here's the code that will work:

function Get-OSInfo {

    $osInfo = Get-WmiObject -Class Win32_OperatingSystem

    $versionNumber = $osInfo.Version.SubString(0,$osInfo.Version.LastIndexOf('.'))
    $versionText = Get-VersionText -version $versionNumber

    Write-Host "You're running $versionText"

}


function Get-VersionText {
    param([double]$version)

    Switch ($version) {

        10{$versionText = 'Windows 10'}

        6.3 {$versionText = 'Windows 8.1 or Server 2012 R2'}

        6.2 {$versionText = 'Windows 8 or Server 2012'}

        6.1 {$versionText = 'Windows 7 or Server 2008 R2'}

        6.0 {$versionText = 'Windows Vista or Server 2008'}

        5.2 {$versionText = 'Windows Server 2003/2003 R2'}

        5.1 {$versionText = 'Windows XP'}

        5.0 {$versionText = 'Windows 2000'}

    }

    Return $versionText

}

Get-OSInfo

Go ahead and run that code! Looks like it worked for me.

Don't worry if you don't understand what exactly [double] means yet. I'll be going over types later in this post!

Adding CMDlet-like functionality to your functions

What if I told you there's this one weird trick to writing better functions? Yeah... I'd hate me too. There is one thing you can do, however, that will graduate a function from basic to advanced. That would be adding [cmdletbinding()] to the very top of of your scripts and/or functions. (It must be at the top, except for where there is comment based help).

When you use [cmdletbinding()] you need to also include the param() statement. This is true even if your function doesn't require parameters.

What does it do, exactly?

By simply adding [cmdletbinding()] you enable extra functionality for:

  • The ability to add -Verbose and -Debug when calling your functions/scripts.
  • Add the -whatif and -confirm options to your scripts (although, this requires a bit of extra work, but is doable).
  • Extra parameter options for using advanced parameters. These can really come in handy.

Simple advanced function example

function Invoke-SuperAdvancedFunctionality {
[cmdletbinding()]
param()

    Write-Verbose "This function has now graduated!"

}

Let's go ahead and enter that in PowerShell (run via ISE or paste into console), and then call it by using:

Invoke-SuperAdvancedFunctionality

Alright... nothing happened!

Well that's because the line we added is Write-Verbose, not Write-Output or Write-Host. This seems like a good time to go over...

Write-Verbose vs. Write-Debug vs. Write-Host vs. Write-Output

Write-Host

This command is generally frowned upon in the PowerShell community. Write-Host merely outputs text via the current running PowerShell host (be it the ISE or console). 

Pros

  • Easy way to work through scripts while outputting information to yourself.
  • Can use formatting features (`t for [TAB] and `n for new line to name a couple), and colors! 
  • Can present users of your scripts/functions with a nice way to have data presented to them

Cons

  • Some people in the PowerShell community may scoff at you.
  • Can be confused with Write-Output, which should be used differently

Example

Write-Host 'This is an example of Write-Host!' -ForegroundColor White -BackgroundColor DarkBlue

More information
Use the command: 

Get-Help Write-Host -Detailed

Write-Output

This command sends what you specify to the next command in the pipeline. What this means is that if it is the last command used, you'll see the value of Write-Output in the console. You can use Write-Output to return information from a function, or even to set a variable with a command.

Pros

  • Can be used to pass objects to the pipeline in PowerShell.

Cons

  • Can be used to pass objects to the pipeline in PowerShell! (yup can be a con too)
    • Be careful with this! If your intention is to merely display information, do not use Write-Output (especially in a function) as you may clutter the pipeline with random information that could cause some unintentional results.
  • Cannot format the text in any way (as it is an object and information passed to the pipeline).

Example

Write-Output "This is a test"
$test = Write-Output "This is a test"
$test

The first line will simply output the text to the console. The second line shows how it can be used to pass information to the pipeline. The third line will display the value of the $test variable.

Let's see what happens if we run the following commands with Write-Host...

$test = Write-Host "This is a test"
$test

Since Write-Host does not pass information to the pipeline, and merely outputs text to the host, it does not store the result in the variable. The command is simply executed and the output written to the console only.

More information

Use the command:

Get-Help Write-Output -Detailed

Write-Verbose

Write-Verbose is enabled when you use a function or script with [cmdletbinding()] specified at the top. You use it by calling the function or script with the -Verbose flag.

Let's go back to our function Invoke-SuperAdvancedFunctionality from earlier, and call it with the -Verbose flag.

Invoke-SuperAdvancedFunctionality -Verbose

There we go! Now you see the text from the Write-Verbose command we used in the function.

Write-Debug

Write-Debug allows you to step through your code with breakpoints. It is also enabled by adding [cmdletbinding()] to your script or function.

Let's add the following lines to our function:

$processes = Get-Process
Write-Debug "`$processes contains $($processes.Count) processes"

The function should now look like this:

function Invoke-SuperAdvancedFunctionality {
[cmdletbinding()]
param()

    Write-Verbose "This function has now graduated!"

    $processes = Get-Process

    Write-Debug "`$processes contains $($processes.Count) processes"

}

A quick note on strings and escape characters

In PowerShell, values surrounded by "double quotes" are called expanded strings. These allow variables to be resolved and displayed in the string. In this example I wanted to show what the actual variable name is (without resolving it), and also show the .Count property. To do this I first use the escape character of ` before $processes. The ` character tells PowerShell to not display the value of $processes, but to treat it like text.

I then wanted to show the .Count property. To get this to resolve you need to use $() to encapsulate the variable and property you're wanting to call. This is called a subexpression.

If you're just displaying text information in PowerShell, it's best to use single quotes.

Back to Write-Debug!

Now run:

Invoke-SuperAdvancedFunctionality -Verbose

The function will now show you the debugging information, and prompt you for options as to how to continue.

There are really no pros or cons to using Write-Verbose or Write-Debug. I wrote the above pros and cons for Write-Host and Write-Output slightly in jest.

The general use cases for Write-Verbose is when you want to display information. Typically it's to show someone else running your code that has added the -Verbose flag to derive more information.

As for Write-Debug, it's best used to troubleshoot and step through your scripts when you need to pause and check what a value is set to (or anything else you want to stop for). 

Advanced parameters and types

Let's start a new script in the ISE and save it as part4.ps1.

Add the following code:

function Get-WMIInfo {
    [cmdletbinding()]
    param(
    [parameter(Mandatory=$true)]
    [string]
    $lookup,
    [parameter(Mandatory=$false)]
    [double]
    $version,
    [parameter(Mandatory=$false)]
    [int]
    $typeNumber
    )

    Write-Debug "[$lookup] reference requested."

Now, with [cmdletbinding()], we can use some parameter options we couldn't before. One of those is (Mandatory=$true). What that does is requires that parameter to be set for the script or function to run. If it is not initially set, it will prompt you to set it when the code executes.

There are many other options available to you for advanced parameters. You can check those out here.

Types

You can specify type constraints in PowerShell. In the above code we set one for each parameter. 

[string]$lookup
[double]$version
[int]$typeNumber

What PowerShell does it looks for the values/arguments provided to match the datatypes. It they do not, you'll get a pretty error message. Here's an example error when you try to pass a string to the [int] type.

[int]$thisIsGoingToErrorOut = 'one is a number, right?'

Type list

[array]          -An array of values.
[bool]           - $true or $false values only.
[byte]           - An 8-bit unsigned integer.
[char]           - A unicode 16-bit character.
[decimal]     - A single-precision 32-bit floating point number.
[double]      - A double-precision 64-bit floating point number.
[hashtable] - This represents a hastable object in PowerShell. (keys paired with values)
[int]             - A 32-bit signed integer.
[single]       - A (lonely) single-precision 32-bit floating point number.
[string]       - A fixed-length string of Unicode characters.
[xml]           - This represents an XMLdocument object.
 

Using the Switch statement to handle parameter arguments

You can use a Switch statement to handle different arguments given to a parameter.

Let's finish the code for Get-WMIInfo. Paste the following after the Write-Debug command:

Switch ($lookup) {

        {$_ -eq 'osversion'} {

            Write-Debug "Looking up [$version]."

            Switch ($version) {

                10{$versionText = 'Windows 10'}

                6.3 {$versionText = 'Windows 8.1 or Server 2012 R2'}

                6.2 {$versionText = 'Windows 8 or Server 2012'}

                6.1 {$versionText = 'Windows 7 or Server 2008 R2'}

                6.0 {$versionText = 'Windows Vista or Server 2008'}

                5.2 {$versionText = 'Windows Server 2003/2003 R2'}

                5.1 {$versionText = 'Windows XP'}

                5.0 {$versionText = 'Windows 2000'}

                Default {$versionText = 'Unable to determine version!'}

            }

            Write-Debug "[$version] matched with text [$versionText]"

            Return $versionText

        }

        {$_ -eq 'drivenumber'} {


            Write-Debug "Looking up drive # [$typeNumber]"

            Switch ($typeNumber) {

                0 {$typeText = 'Unknown'}

                1 {$typeText = 'No Root Directory'}

                2 {$typeText = 'Removeable Disk'}

                3 {$typeText = 'Local Disk'}

                4 {$typeText = 'Network Drive'}

                5 {$typeText = 'Compact Disk'}

                6 {$typeText = 'RAM Disk'}

                Default {$typeText = "Invalid type number [$typeNumber]"}

            }

            Write-Debug "[$typeNumber] matched with text [$typeText]"

            Return $typeText

        }
    }
}

The first Switch statement evaluates $lookup. The value for $lookup is determined by what is set in the $lookup parameter. 

Let's look at the follow segment of code:

{$_ -eq 'osversion'} { 

What this does it is evaluates if $_ (in this case the value for lookup) matches 'osversion'.

If the condition is found to be true, it will run the code surrounded in {}'s.

In this case, the code would be the same code we used in Part 3 to lookup the OS version text. This would switch the value for $version,and attempt to match the version number.  

The default statement in the switch will execute if no values are matched. For the $version switch that would execute:

Default {$versionText = 'Unable to determine version!'}

The next evaluation point for $lookup is to see if it matches 'drivenumber'.

{$_ -eq 'drivenumber'} { 

If it matches drive number, then it proceeds to attempt to match the drive number received with the associated text.

Finishing the script for Part 4

Let's add the following code after the Get-WMIInfo function:

function Get-OSInfo {
    [cmdletbinding()]
    Param(
    [parameter(Mandatory=$false)]
    [boolean]
    $getDiskInfo = $false
    )

    Write-Verbose "Looking up OS Information..."

    $osInfo        = Get-WmiObject -Class Win32_OperatingSystem

    $versionNumber = $osInfo.Version.SubString(0,$osInfo.Version.LastIndexOf('.'))

    Write-Verbose "Looking up the matching windows edition for version #: [$versionNumber]"

    Write-Debug "Version number stored as [$versionNumber]"

    $versionText = Get-WMIInfo -lookup 'osversion' -version $versionNumber

    Write-Host `n"You're running $versionText"`n

    if ($getDiskInfo) {

        Write-Verbose "Gathing disk information via WMI..."

        $disks = Get-WmiObject -Class Win32_LogicalDisk

        if ($disks) {
            
            Write-Host `n"Disk information!"`n -ForegroundColor White -BackgroundColor Black
            
            Foreach ($disk in $disks) {

                Write-Host `n"Device ID    : $($disk.DeviceID)"
                Write-Host ("Free Space   : {0:N2} GB" -f $($disk.FreeSpace / 1GB))
                Write-Host ("Total Size   : {0:N2} GB" -f $($disk.Size / 1GB))
                Write-Host ("% Free       : {0:P0}   " -f $($disk.FreeSpace / $disk.Size))
                Write-Host "Volume Name  : $($disk.VolumeName)"
                Write-Host "Drive Type   : $(Get-WMIInfo -lookup 'drivenumber' -typeNumber $disk.DriveType)"`n

            }


        } else {

            Write-Host "Error getting disk info!"

        }

    }

}

Get-OSInfo -getDiskInfo $true

Execute the code, and you should see it both displays the OS information, as well as the information for any disks in your system.

Homework

  • After running the script we created, call the functions in different ways. Here's an example of one:

Get-OSInfo -getDiskInfo $true -Debug
Get-WMIInfo -lookup osversion -version 100
  • Review the code we used today.
    • What does the foreach statement do?
    • Take a look at the -f operator used to format a percentage (you can also see some PowerShell math in action)
Write-Host ("% Free : {0:P0} " -f $($disk.FreeSpace / $disk.Size))

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Back to Top]

PowerShell: Getting Started - Using the ISE and Creating Functions

PowerShell: Getting Started - Using the ISE and Creating Functions

Getting Started - Using the ISE and Creating Functions

PowerShell ISE

NOTE

The ISE is basically in maintenance mode now, and will likely only be getting security patches. Using Visual Studio Code is now the way to go.
Check out my post here on getting started with VS Code + PowerShell:

The ISE provides a way to use PowerShell more... powerfully. I generally prefer sticking to the shell if I'm using solid scripts and running random commands. However, once I get an idea for a script or start stringing different commands together, I turn to the ISE (or Visual Studio Code depending on what I'm doing). 

For the purpose of this getting started series, I will stick to using the ISE. The ISE was introduced with PowerShell 2.0, and follows relatively the same version outline as described in part one.

Features

  • IntelliSense (commands auto finish as you type them, and it shows available options)

  • Script debugging

  • Recent file list

  • Syntax highlighting

For a full list of features, check out this TechNet article.

Using the ISE

  • Launch the ISE by clicking the Start icon and searching for/typing "ISE".

  • You should see the following screen.

  • Go File -> Save As, and save this script as C:\PowerShell\part3.ps1 (Create the folder if it does not exist).

We are now ready to start using the ISE!

Customizing the ISE

As a preference I like to close the command pane on the right. 

You can also adjust the theme by going to Tools -> Options.

The script pane can be toggled in various ways by using the icons highlighted here:

Running Commands

Let's type in Get-Date, and then click the green arrow.

Here's the result:

The result is shown below in the script pane.

Now, let's add another command: 

Get-WMIObject -Class Win32_LogicalDisk

Highlight this command by clicking and dragging, and then click the icon next to the green arrow (smaller green arrow with paper behind it).

The result of this should show that the highlighted command ran. (it will display under the output of Get-Date)

The green arrow will run every command in the script, while the green arrow with paper behind it will only run the current selection. You can also use the shortcut keys F5 (to run the entire script) and F8 (to run just the selected text).

IntelliSense

Let's see what happens if you just type in Get-.

The ISE will show you a list of available command. Go ahead and choose Get-Alias.

On the same line as Get-Alias add a "-". 

You'll see that the ISE will display a list of available parameters and the type of input you can use when specifying an argument.

In this case it is showing that the Name parameter accepts an argument type of string array.

That means that either of these commands will work:

Get-Alias -Name ls

Get-Alias -Name ls,dir

The results of running each command should look similar to this:

The first command returns just the ls alias, as expected.

The second command will return both the ls, and dir output of the command. This is because PowerShell interprets the comma separated values as an array.

Let's move on to creating some functions!

Functions

As described in part 2 of my series, functions are:

a series of commands that are grouped together to perform a specific task (or at least, that's the best practice). You can write functions and scripts that mimic the behavior of Cmdlets (via providing error checking and comment based help).

Go ahead and clear out any commands we've put in the ISE so we can start with a clean slate.

Creating a Function

Let's create a function that works by gathering information about the current OS of your machine.

When naming functions it is best practice to use approved verbs, and use PowerShell's Verb-Noun format.

Type the following in the ISE:

function Get-OSInfo {

 Write-Host "This function will display OS information"

}

Go ahead and run this in the ISE using F5 or the green arrow to run the entire script.

Let's see the result.

It looks like it ran, but nothing happened!

That's because the function is now stored in this PowerShell session. You have to call it to use it. To call it, add this line to the script:

Get-OSInfo

Go ahead and run the full script again.

In the above example you can see that the function was called via the script, and that you also can type the function name in the script pane manually.

You can see that Write-Host was executed and the text was displayed.

Replace the code inside of the function with the following code:

$osInfo = Get-WmiObject -Class Win32_OperatingSystem

Return $osInfo

Run the script again.

You can see that the function returns the variable we set within it (which contains the OS information). The following information is displayed:

SystemDirectory : C:\WINDOWS\system32
Organization: Microsoft
BuildNumber : 10586
RegisteredUser: Microsoft
SerialNumber: x
Version : 10.0.10586

Let's modify the Get-OSInfo function to reflect the following code:

function Get-OSInfo {

$osInfo = Get-WmiObject -Class Win32_OperatingSystem

$versionText = Get-VersionText -version $osInfo.Version.Split('.')[0]

Write-Host "You're running $versionText"

}

Finally, add this function below the first one (Get-VersionText):

function Get-VersionText {
param($version)

Switch ($version) {

10{$versionText = 'Windows 10'}

6.3 {$versionText = 'Windows 8.1 or Server 2012 R2'}

6.2 {$versionText = 'Windows 8 or Server 2012'}

6.1 {$versionText = 'Windows 7 or Server 2008 R2'}

6.0 {$versionText = 'Windows Vista or Server 2008'}

5.2 {$versionText = 'Windows Server 2003/2003 R2'}

5.1 {$versionText = 'Windows XP'}

5.0 {$versionText = 'Windows 2000'}

}

Return $versionText

}

The script should now look like this:

Functions should do one thing, and that one thing well. The second function adds a switch statement. The switch statement evaluates a variable's value and performs actions depending on what it receives. It also accepts one parameter, which is $version

In the first function we use the .split() method on the version, and the [0] to select the first element in the split array.

You can see the result of that method if you run the following lines individually:

$osInfo = Get-WmiObject -Class Win32_OperatingSystem

$osInfo.Version.Split('.')[0]

That should return '10'. (if you're running Windows 10) 

If you'd like to see what the whole array looks like type

$osInfo.Version.Split('.')

You should see the following results (may vary depending on your version).

Let's get back to our script! Go ahead and run it!

If all goes well, it will display the returned text of the version you're running. 

What it does

At the bottom of the script the first function is called via the Get-OSInfo command. 

The function (Get-OSInfo) stores the WMI information for the  Win32_OperatingSystem class in the $osInfo variable.

Then we declare $versionText, which calls the function Get-VersionText  with the parameter -version and argument $osInfo.Version.Split('.')[0]

The Get-VersionText function then runs, and uses the Switch statement to determine how to set $versionText depending on what the value of $version is. This value is then returned into the $versionText variable called in the first function.

Finally, it uses Write-Host to display the text version of the version you're running that's stored in the $versionText variable.

After it executes, it will output "You're running Windows 10". (Or whichever version of Windows you're running).

 

That covers the basics of using functions in PowerShell. I will go over more advanced functions in part 4!

Homework

  • Use the Get-VersionText function with different numbers as the argument for the -version parameter.

  • Write your own functions! Maybe get some ideas from this post on Get-WMIObject.

  • Get a deeper understanding of the switch statement. How would you make it return a value in $versionText if none of the numbers specified are a match?

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Back to top]

PowerShell: Getting Started - Command Discovery

PowerShell: Getting Started - Command Discovery

Getting Started - Command Discovery

Welcome to my Getting Started with Windows PowerShell series!

Terminology Overview

This part may not be as exciting as the hands on stuff, but it is important to understand. Don't worry if you don't grasp a lot of it right away. It will all come together the more you use PowerShell. When your knowledge of it deepens you'll see more and more use cases for it yourself! We'll actually be building scripts, functions, and modules in subsequent posts! If you would like to skip to the hands-on stuff (and I don't blame you!) click here.

Cmdlets

Cmdlets are essentially snippets of .NET code or classes that perform a single function in PowerShell. The core functionality of PowerShell revolves around Cmdlets. Keep in mind that when you are creating functions, modules, or scripts that you aren't actually creating Cmdlets. If you'd like to look further into actually creating Cmdlets, check out this page. Please note that it is not actually writing PowerShell at that point.  It's .NET code that will interact, and provide functionality TO PowerShell.

Scripts

A PowerShell script is simply a file with a .ps1 extension that contains a series of PowerShell commands. The profile we created is actually an example of a PowerShell script.

Functions

A function in PowerShell is a series of commands that are grouped together to perform a specific task (or at least, that's the best practice). You can write functions and scripts that mimic the behavior of Cmdlets (via providing error checking and comment based help). Here's an example of a function I wrote in my post about using the LIFX API.

function Set-LightState { #Begin function Set-LightState
    [cmdletbinding()]
    param(
        [Parameter()]
        [ValidateSet('on','off')]
        [string]
        $state,
        [Parameter()]  
        [string]
        $color,
        [Parameter()]
        [ValidateRange(0.0,1)]
        [double]
        $brightness,
        [string]
        $selector
    )

    if ($lifxLight.Connected) {          

        $fullURL  = "$baseurl/$($lifxLight.id)/state"             
        $payload  = [PSCustomObject]@{

            power      = $state
            color      = $color
            brightness = $brightness

        }

        $payloadJson = $payload | ConvertTo-Json

        Write-Host "Attempting to Change light to:" -ForegroundColor $foregroundcolor
        Write-Host `t"State     :" $state
        Write-Host `t"Color     :" $color
        Write-Host `t"Brightness:" ($brightness * 100)"%" `n

        $stateReturn = Invoke-RestMethod -headers $headers -uri $fullURL -Method Put -Body $payloadJson -ContentType $acceptheader
    
        Write-Host "API status:" -ForegroundColor $foregroundcolor
        Write-Host `t"Light :" $stateReturn.results.label
        Write-Host `t"Status:" $stateReturn.results.status `n
    
    } else {
        
        $lightName = $lifxLight.label
        Write-Host "$lightName is offline or unreachable :("
        
    }    

} #End function Set-LightState

Here are some examples of that function in action.

Below you can see it being referenced in some code further on in the script.

Let's create a simple function to see how they work interactively.

function Get-RandomNameChaos {
param([string]$Name,
      [int]$Times
     )
    
    if ($Name) {
        $i = 1
        
        Do {

            Write-Host (Get-Random "`t","`n") [$i] [$Name] -foregroundColor (Get-Random -Minimum 1 -Maximum 15) -backgroundColor (Get-Random -Minimum 1 -Maximum 15)
        
            $i++
    
        } While ($i -lt ($times + 1)) 

    } else {

    Write-Host 'No name specified!' 
    
    }
}

This functions takes two arguments, -Name and -Time

You can get the function to work in PowerShell by:

  • Copy and pasting it in from above
  • Putting it in a script file and calling it within the script
  • Putting it in a module file and importing the module

I decided to paste it in and show you what happens as an example. I ran:

Get-RandomNameChaos -Name Mike -Times 3

Functions can take different arguments which are assigned to the parameters defined. 

This function we created has [int] next to the $Times parameter. This means that if you put anything other than an integer as a value, you will get an error message. 

Try running the function by calling it with different arguments. Maybe 100 for times, or even putting a non-integer value in just to see what it does.

Modules

Modules are PowerShell scripts with a .psm1 extension that contain a group of functions. Once you write a lot of functions you use over and over, it's generally a good time to create a module. You can import modules in PowerShell by using the Import-Module command. Here's an example of a logging module I created:

Above I imported my module to add logging functionality via specific functions I created. I then ran Get-Command -Module ninjalogging to get a list of commands it provides. 

Command structure

PowerShell Cmdlets (and well written functions) utilize the following format: Verb-Noun.

You can use the Get-Verb command to see a list of verbs that PowerShell uses and are officially approved. This can be handy when you're creating your own functions so you can use an approved verb that fits.

You can also run the following command which utilizes Get-CommandGroup-Object, and Sort-Object to list out the most commonly used verbs for all the PowerShell CMDlets you have access to.

Get-Command -CommandType Cmdlet | Group-Object Verb | Sort-Object Count -Descending | Format-Table -Autosize

If you have any questions about the above string of piped commands let me know in the comments, or try using Get-Help <command>.

As you can see a lot of the Cmdlets use only a few of the approved verbs. I find myself trying to use a lot of those verbs myself. There's Get, Set, and New just to name a few.

Aliases

There are aliases in PowerShell that make writing out one-liners and troubleshooting during a fire easier to manage. To see a list of aliases type Get-Alias.

You can even create you own aliases and use them in PowerShell. The alias I personally use the most is ls. To see what that Alias is linked to you can use:

Get-Alias -Name ls

ls is simply an alias for Get-ChildItem

Simple, but it provides a quick way to execute different Cmdlets and shorthand format.

You can check if an alias exists, and create your own with the following commands:

Get-Alias -name log
New-Alias -Name log -Value Out-LogFile -Description 'Link to Out-LogFile'
Get-Process | Out-String | log

The above example will not work for you as you do not have a function named Out-LogFile. What it does in my case is creates a text file logging the output of Get-Process. If you want to try to create your own alias, use Get-Process for the value and psax for the Name. Give it any Description you'd like.

You can then use:

Get-Alias -Name 'your alias name' 

to check on it. Here is the result for my example:

Command Discovery

The most straightforward command to use to discover commands is... Get-Command.

This command will display the commands as well as the command type, version, and its source.

You can narrow the scope of this command in various ways. One way is to specify the source:

Get-Command -Module Microsoft.PowerShell.Management

Let's say you wanted to discover all the commands available that had to do with processes. Let's try:

Get-Command -Name "*process*"

You could also run:

Get-Command | Where-Object {$_.Name -like "*process*"}

The $_. placeholder variable stands for the current element in the pipeline. In this case it would run through each line that is returned from Get-Command and look at the property Name ($_.Name). It then uses the comparison operator -like which allows us to specify wildcards. 

Either way the results are the same, and let's focus in on...

Get-Process

Here is the result of running Get-Process.

Formatting the results

You can also pipe the command to Format-List

Get-Process | Format-List

Here's an example of how to use Select-Object and Sort-Object. You can add more properties the the list if you want! Mess around with it to see what you can come up with that's useful to you. Here's what I ran:

Get-Process | Select-Object Name,CPU | Sort-Object CPU -Descending | Format-Table -AutoSize

Another way to output this information is to use Out-GridView. This can be a great way to get a visual of the returned items.

Get-Process | Out-GridView

In this instance I know that Get-Process is actually capable of displaying much more information. Let's try this command:

Get-Process | Select-Object * | Out-GridView

Well, that's a lot more information!

You can even add criteria to narrow it down.

I added one here (Use the [+ Add Criteria] button in the upper left) for VirtualMemorySize that islessthanorequalto 100000000.

Here's another one for Name contains chrome.

Exporting the Results

We can also use Get-Process with > and >>. These are very simple ways to output the results to a file.

Creates a new file every time (and will overwrite the file if it exists)
>> Appends the results to a file if it exists, or creates one if it doesn't

Let's look at:

Get-Process > processes.txt

Let's run the following command to see the contents of the file:

Start-Process .\processes.txt.

You can also use [TAB] completion. Try typing Start-Pro[TAB] .\pr[TAB]

Start-Process will take the input you give it and then run that with the associated application handler that's set. For example:

Start-Process www.google.com

The above command would open your default browser, and then navigate to www.google.com

Back to Get-Process!

If you were to use:

Get-Process >> processes.txt

It would append the results to the file that already exists.

Out-File

You can also use Out-File. I prefer this option as you have more control over what happens. You can specify the encoding, path, and other various options such as NoClobber.

Let's give it a try:

Get-Process | Out-File -FilePath C:\ninja\process.txt 

To append to this file using Out-File you would use:

Get-Process | Out-File -FilePath C:\ninja\process.txt -Append

Using Stop-Process with Get-Process

One of the handy things you can do with Get-Process is pipe it to Stop-Process. Just don't do this without specifying a process name, or you will very likely end up causing your computer to reboot!

This can, however, be a handy way to kill a hung process. Let's say you wanted to close any open notepad.exe instances. 

Get-Process notepad | Stop-Process

You wont see any information returned, and that usually means it found the process and stopped it as well. 

Profile Review

Below I have commented out the profile we created in part one with information on what each line is doing. 

#Set the variable $foregroundColor to be white. Global means it will be set in all scopes. 
$global:foregroundColor = 'white'

#Store the results of Get-Date in the $time variable
$time = Get-Date

#Store the current version in the $psVersion variable
$psVersion= $host.Version.Major

#Store the current user and computer in each variable 
$curUser= (Get-ChildItem Env:\USERNAME).Value
$curComp= (Get-ChildItem Env:\COMPUTERNAME).Value

#Write-Host writes to the host that's running the script. In this case, very likley your console!
#The following lines takes the variables above and uses them in the output in various ways.
Write-Host "Greetings, $curUser!" -foregroundColor $foregroundColor
Write-Host "It is: $($time.ToLongDateString())"
Write-Host "You're running PowerShell version: $psVersion" -foregroundColor Green
Write-Host "Your computer name is: $curComp" -foregroundColor Green
Write-Host "Happy scripting!" `n

#This replaces the default prompt function with our own!
function Prompt {

#We get the time again as this function runs every single time you hit enter.
$curtime = Get-Date

#Writing to the host, this time with -noNewLine.
#-NoNewLine means it will keep writing to the same line. 
#This allows us to use multiple colors on the same line.
Write-Host -NoNewLine "p" -foregroundColor $foregroundColor
Write-Host -NoNewLine "$" -foregroundColor Green
Write-Host -NoNewLine "[" -foregroundColor Yellow

#This line uses a string format operator (-f) and takes the results from Get-Date and formats them on the left.
#I'll be detailing more about how that works in a later post!

Write-Host -NoNewLine ("{0:HH}:{0:mm}:{0:ss}" -f (Get-Date)) -foregroundColor $foregroundColor

#Closing off the prompt 
Write-Host -NoNewLine "]" -foregroundColor Yellow
Write-Host -NoNewLine ">" -foregroundColor Red

#This variable controls the PowerShell window title. We set it to the current path everytime this function runs.
$host.UI.RawUI.WindowTitle = "PS >> User: $curUser >> Current DIR: $((Get-Location).Path)"

#This next line is needed so we can override the default prompt with our Write-Host above!
Return " "

}

Homework

  • Write your own profile using the one I gave you as a template for ideas.
  • Customize the Get-RandomNameChaos function some more!
  • Figure out just what the heck "{0}" -f (Get-Date) does and how it works. (Hint: String format operators)
  • Get Get-Process | Out-GridView to show you only properties you care about.
  • Learn what a calculated property is.

In part 3, we go over the ISE and creating functions!

-Ginger Ninja

[Back to top]

PowerShell: Getting Started - Customizing Your Environment!

PowerShell: Getting Started - Customizing Your Environment!

Getting Started - Basics and Environment Customization

Welcome to my Getting Started with Windows PowerShell series!

We will be exploring:

Update 4/24/19: Look for some of these series pages to get revamped. PowerShell Core is making waves! If you haven’t checked it out, you should.

Looking to get started with Visual Studio Code + PowerShell? Check out this post:

Just What is PowerShell?

PowerShell is a framework provided by Microsoft that provides a platform for automation, general scripting, and well just about anything you can imagine doing with it. It's based on .NET, and has hooks into pretty much anything Windows can do. There are a lot of new things introduced with PowerShell, and it is ever evolving. You can also still use old commands you're familiar with like ping, but you have much more powerful options at your finger tips (like Test-Connection).  If you'd like a deeper dive into what PowerShell is, check out Microsoft's Scripting Guy's post, here.

PowerShell is an object-oriented programming language with a ton of features. Everything we work with PowerShell is an object one way or another. What this means is we can get information from it via properties, and take actions with it via methods.

You can use PowerShell in two distinct ways.

PowerShell also makes it easy to create your own custom objects, more on that in another post!

PowerShell Versions

Here's the breakdown: 

  • Windows 7 comes with Windows PowerShell version 2.0 installed by default. If you're still running Windows 7, and want to try out the latest version of PowerShell (currently 5.0), you'll need to install the Windows Management Framework update

  • Windows 8 versioning is a bit strange

  • You can run PowerShell 2.0-3.0

    • You cannot run PowerShell 4.0+

  • Windows 8.1 let's you run version 5.0

  • Windows 10 comes with PowerShell version 5.0 installed by default, and if you're all updated (which it is hard not to be with how Windows 10 updates work...) version 5.1+ will be available

If for some reason you're using a machine running Windows XP, PowerShell 2.0 will work with this download.

PowerShell Core

PowerShell core heralds the open sourcing of PowerShell, check it out on Github! You can even run it on your Raspberry Pi.

How can I check what version I have?

Well, first you'll have to open PowerShell

  1. Hit the Windows key or click the Start icon/button.

  2. Type in 'PowerShell', and select 'Windows PowerShell'

I recommend pinning PowerShell to your taskbar as well. This makes it really easy to launch.

PowerShell stores the version in a special variable dedicated to host information. This variable is aptly named $host. To see the value of this variable, type $host into your console and press enter. You should see a similar result to this:

If you simply wanted to return the latest version, try typing $host.Version to display the Version property only.

Finally, if you want to just return the Major Version, use $host.Version.Major.

For a more detailed write up on versions and supported OSs, see this post at 4sysops.

How PowerShell Works

PowerShell works by executing commands, and then provides you a way to interpreting the results. Everything in PowerShell either is or becomes an object in one way or another. Think of an object in this instance as something you can take action on via methods, and get information from via properties.

Let's learn some more basics before we go about customizing our environment. Don't worry too much about grasping terminology! With PowerShell especially, learning by doing is where it's at.

'Hello World'

Even the simplest thing in PowerShell, such as 'Hello World',  becomes an object you can take action on. Go ahead and type the following command in PowerShell:

'hello world'.Length

The above example should return 11, as seen below.

Since 'Hello World' is an object, we can pipe it via "|" to Get-Member to see what we can do with it. Piping in PowerShell is the act of adding the "|" character to take the results of the input before it, and pass that over to the next command. Let's go ahead and run this:

'Hello World' | Get-Member

You should see the following:

You can see that the object Type is of System.String, and below that the various methods and properties. To use them, simply add a dot after 'Hello World' and specify the one you'd like to use. For instance, I wonder what ToUpper does. Let's see!

'Hello World'.ToUpper

Hmm... that looks a little weird. That's because to execute the method, you need to put a pair of parentheses after it. Sometimes you can include different values in the parentheses to include overload options. What we're seeing here is the definition of those options for .ToUpper(). For this example we can just use:

'Hello World'.ToUpper()

Get-Member will likely be one of the handiest cmdlets you will use. It lets you know what properties and methods the object contains.

Now that we've covered some basics, let's get back to checking out...

Ping vs Test-Connection

Let's ping google.com via PowerShell.

Ping Google.com

Alright, pretty standard! Let's see what Test-Connection Google.com does.

Test-Connection Google.com

Now that looks a little bit different. So what's so special about Test-Connection? To see what else Test-Connection can do and the options it provides, use:

Get-Help Test-Connection

Notice under REMARKS it states that I don't have the help files stored on my computer. To remedy this, right click your PowerShell icon on the taskbar, and go to Run as Administrator. Then use this command:

Update-Help

Now let's see the help for Test-Connection again!

Get-Help Test-Connection

Under the SYNTAX portion you can see that Test-Connection accepts the -ComputerName parameter. This is the one that Google.com was placed into by default. It then specifies what the input is expected to be. This parameter accepts a string, and string array. That is what the [] next to string means. Think of an array as a collection of values.

To see examples of how to run Test-Connection, type:

Get-Help Test-Connection -Examples

Variables

Let's take advantage of the fact that Test-Connection's -ComputerName parameter can accept a string array. To do this, we'll need to create a variable and add some values to it. The best way to create a string array is to use this command:

[System.Collections.ArrayList]$testArray = @()

This above code will create an empty array in the variable $testArray. Think of a variable as a container of objects.

Let's add some hosts to this array that we'll want to use with Test-Connection

$testArray.Add('192.168.1.1')
$testArray.Add('google.com')
$testArray.Add('qwertyuiop.asdf')
 

Arrays in PowerShell always start with 0, and when we use the .Add method on this array you can see it outputs the index of the element(value) we are adding. To add an element without seeing that, simply pipe $testArray.Add('yahoo.com') to Out-Null.

$testArray.Add('yahoo.com') | Out-Null

You can see it did not return the index number this time. To display the values in the array, type:

$testArray

OK! Now that we have our array setup, let's use:

Test-Connection -ComputerName $testArray

You can even use Test-Connection with conditional logic. 

if (Test-Connection Google.com) {Write-Host "Success!"} 

Since Test-Connection Google.com returned $true, it proceeds to perform the command in the script block {}.

I wonder what happens if you replace Google.com with 'qwertyuiop.asdf'...

Alright! Now that we've gone through some more of the basic concepts, it's time to...

Customize Your Environment

Open up your PowerShell console and Right Click the title bar. 

  1. Select Properties.

  2. Select the Font tab to adjust the font.

  3. Select the Colors tab to set the colors you want.

Customizing your profile

PowerShell uses profile files to automatically load a script when you start the PowerShell console.

Let's take a look at the file PowerShell uses for your current user profile on all hosts (meaning the ISE and console). We'll get into the different host types in a different post. The special variable we'll want to look at is $profile, and we'll want to see the CurrentUserAllHosts property.

$profile.CurrentUserAllHosts

It looks like the generic Dell account (my way to have a fresh instance of PowerShell) I'm using would have the profile stored in:

C:\Users\Dell\Documents\WindowsPowerShell\profile.ps1

Since the folder and file do not exist, let's use the New-Item cmdlet to create each. Be sure to change the values to match what your result was from the $profile.CurrentUserAllHosts value. Note: the file will still be profile.ps1, and only the user name should change.

New-Item -Path C:\Users\Dell\Documents\ -ItemType Directory -Name WindowsPowerShell
New-Item -Path C:\Users\Dell\Documents\WindowsPowerShell\ -ItemType File -Name profile.ps1

To auto-magically do this, you can use the following commands:

New-Item -Path "$((Get-ChildItem ENV:\UserProfile).Value)\Documents\" -ItemType Directory -Name WindowsPowerShell
New-Item -Path "$((Get-ChildItem ENV:\UserProfile).Value)\Documents\WindowsPowerShell" -ItemType File -Name profile.ps1

Now you should be able to use the Start-Process cmdlet(which opens a file with the associated handler in Windows automatically) to open and edit the profile file.

Start-Process $profile.CurrentUserAllHosts

You should now have a blank text file open with profile.ps1 as the name in the upper left.

Let's add the following code to the profile.ps1 file:
I will detail what this code does in the next post!

$foregroundColor = 'white'
$time = Get-Date
$psVersion= $host.Version.Major
$curUser= (Get-ChildItem Env:\USERNAME).Value
$curComp= (Get-ChildItem Env:\COMPUTERNAME).Value

Write-Host "Greetings, $curUser!" -foregroundColor $foregroundColor
Write-Host "It is: $($time.ToLongDateString())"
Write-Host "You're running PowerShell version: $psVersion" -foregroundColor Green
Write-Host "Your computer name is: $curComp" -foregroundColor Green
Write-Host "Happy scripting!" `n

function Prompt {

$curtime = Get-Date

Write-Host -NoNewLine "p" -foregroundColor $foregroundColor
Write-Host -NoNewLine "$" -foregroundColor Green
Write-Host -NoNewLine "[" -foregroundColor Yellow
Write-Host -NoNewLine ("{0}" -f (Get-Date)) -foregroundColor $foregroundColor
Write-Host -NoNewLine "]" -foregroundColor Yellow
Write-Host -NoNewLine ">" -foregroundColor Red

$host.UI.RawUI.WindowTitle = "PS >> User: $curUser >> Current DIR: $((Get-Location).Path)"

Return " "

}

Once you've added the content, save profile.ps1.
Now close and re-open your PowerShell console.

It should now look similar to this:

Each time you type a command the function named prompt executes and changes both the prompt (to include the current time), and the Window Title (to include the current user and directory).

In the next post I will be going over command discovery and formatting results. 

Homework

  • Try to figure out exactly how the string format operator works in the prompt function to format the time.

  • Use Get-Command to discover more PowerShell commands.

  • Find some commands to run (maybe Get-Date)? and Pipe them to Get-Member to see what properties and methods they contain.

  • Declare a variable and use that variable to use the methods and display the properties available. Hint: $time = Get-Date.

  • Further customize your profile to change up your prompt and title!

Let me know if you have any questions! Feedback is always appreciated.

-Ginger Ninja

[Back to top]