Monthly Archive

Categories

PowerShell Basics

PowerShell pause

PowerShell pause – how can you pause a PowerShell script?

Two ways come to mind.

First if you just want the script to pause for a specified time period then you can use Start-Sleep

1..10 |
foreach {
  $PSItem
  if ($PSItem -eq 5) {
    Write-Warning -Message "Starting sleep"
    Start-Sleep -Seconds 5
  }
}

Run this and you’ll see the numbers 1-5 output then then warning message. After the delay you’ll see the numbers 6-10 output.

But what if you want to control the pause manually? Not sure if there are advantages to this approach but if you do need to do this you can use Read-Host

1..10 |
foreach {
  $PSItem
  if ($PSItem -eq 5) {
    Read-Host -Prompt "Press Enter key to continue"
  }
}

You’ll see the numbers 1-5 output then the message

Press Enter key to continue:

After pressing the enter key the script continues and outputs 6-10

Don’t know why you’d want to do this in an automation scenario but the technique is there if you need it – I don’t recommend the approach.

There are also a few cmdlets that can be used under specific circumstances:

Wait-Event
Wait-Job
Wait-Process
Wait-VM

Also check the –wait parameter on Restart-Computer

PowerShell for loop

Loops are a construction seen in most scripting and programming languages. A loop is used to repeat a set of statements a set number of times or until a specific criterion is met or while a specific criterion is true. In this post I’ll describe the PowerShell for loop.

For loops are found in many languages. A for loop is sometimes referred to as a counting loop as it will have a counter that starts at a pre-set value and counts up to a specific value. The counter is usually incremented by 1 for each iteration of the loop.

A PowerShell for loop looks like this

for ($i=1; $i -le 10; $i++){$i}

The counter – $i – is initialised to 1. The loop will execute while $i is less than or equal to 10 and $i is incremented by 1 for each turn round the loop. In this case the loop lists the value of the counter.  You can see the results like this

PS> $results = for ($i=1; $i -le 10; $i++){$i}
PS> "$results"
1 2 3 4 5 6 7 8 9 10

You can also run loops where the counter decreases

PS> $results = for ($i=10; $i -ge 1; $i--){$i}
PS> "$results"
10 9 8 7 6 5 4 3 2 1

A for loop is great when you need to perform the loop and exact number of times but if your loop depends on a specific criterion you’re better off using a while loop or a do loop which I’ll cover in another post

Create a directory

PowerShell enables you to work with the file system on your machine – one question that often comes up is how to create a directory.

When working interactively you can use md

PS> md c:\testf1


    Directory: C:\


 Mode                LastWriteTime         Length Name 
 ----                -------------         ------ ---- 
 d-----       19/08/2017     14:24                testf1

md doesn’t look like a PowerShell command – more like an old style DOS command.

Its actually an alias for mkdir

PS> Get-Command md

CommandType     Name                                               Version    Source 
 -----------     ----                                               -------    ------ 
 Alias           md –> mkdir

Which raises the question – what’s mkdir?

PS> Get-Command mkdir

CommandType     Name                                               Version    Source 
 -----------     ----                                               -------    ------ 
 Function        mkdir

Its a function that PowerShell creates for you

Digging into the function

PS> Get-ChildItem -Path function:\mkdir | select  -ExpandProperty  Definition

<# 
 .FORWARDHELPTARGETNAME New-Item 
 .FORWARDHELPCATEGORY Cmdlet 
 #>

[CmdletBinding(DefaultParameterSetName='pathSet', 
    SupportsShouldProcess=$true, 
    SupportsTransactions=$true, 
    ConfirmImpact='Medium')] 
    [OutputType([System.IO.DirectoryInfo])] 
param( 
    [Parameter(ParameterSetName='nameSet', Position=0, ValueFromPipelineByPropertyName=$true)] 
    [Parameter(ParameterSetName='pathSet', Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] 
    [System.String[]] 
    ${Path},

    [Parameter(ParameterSetName='nameSet', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 
    [AllowNull()] 
    [AllowEmptyString()] 
    [System.String] 
    ${Name},

    [Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] 
    [System.Object] 
    ${Value},

    [Switch] 
    ${Force},

    [Parameter(ValueFromPipelineByPropertyName=$true)] 
    [System.Management.Automation.PSCredential] 
    ${Credential} 
 )

begin {

    try { 
        $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('New-Item', [System.Management.Automation.CommandTypes] 
 ::Cmdlet) 
        $scriptCmd = {& $wrappedCmd -Type Directory @PSBoundParameters } 
        $steppablePipeline = $scriptCmd.GetSteppablePipeline() 
        $steppablePipeline.Begin($PSCmdlet) 
    } catch { 
        throw 
    }

}

shows that its based on New-Item

PS> New-Item -Path c:\ -Name testf2 -ItemType Directory


    Directory: C:\


 Mode                LastWriteTime         Length Name 
 ----                -------------         ------ ---- 
 d-----       19/08/2017     14:32                testf2

The default for New-Item in the filesystem is to create a file so you need to use –ItemType Directory to create the folder.

If the folder you’re creating is a subfolder of a non-existent folder you can create the hierarchy is one go

PS> New-Item -Path c:\ -Name testf3\tests1 -ItemType Directory


    Directory: C:\testf3


 Mode                LastWriteTime         Length Name 
 ----                -------------         ------ ---- 
 d-----       19/08/2017     14:33                tests1


 PS> Get-ChildItem -Path c:\testf3 -Recurse


    Directory: C:\testf3


 Mode                LastWriteTime         Length Name 
 ----                -------------         ------ ---- 
 d-----       19/08/2017     14:33                tests1

This can get complicated if you try to nest too many levels so I recommend explicitly creating each level of your folder hierarchy. Its much easier to maintain and modify

PowerShell foreach

PowerShell has a number of ways to perform a loop – I recently counted seven distinct methods. If you can’t list them all don’t worry one is very esoteric and unexpected. I’ll enumerate them in a future post. For now I want to concentrate on a source of confusion – especially to newcomers to PowerShell – namely the PowerShell foreach statements.

The confusion arises because there are effectively two foreach statements. One is a PowerShell keyword that initiates a loop and the other is an alias for  a cmdlet.

Lets start with the foreach loop.

$numbers = 1..10
foreach ($number in $numbers){
  [math]::Pow($number, 2)
}

foreach in this case is used to iterate over a collection. In the example above $numbers is an array of numbers 1 to 10. Foreach number in the array it is raised to the power 2 – squared.

Remember that PowerShell is unique among shells in that you can use pipelines in many places that other languages insist on variables so you could change the example to

foreach ($number in 1..10){
  [math]::Pow($number, 2)
}

The array is generated and then iterated over as earlier.

If you see foreach as the first command on a line you’re dealing with the foreach keyword and therefore a loop.

On the other hand if you see foreach in a pipeline

1..10 | foreach {
  [math]::Pow($_, 2)
}

or

1..10 | foreach {
  [math]::Pow($psitem, 2)
}

you’re dealing with the cmdlet. $_ or $psitem denote the object currently on the pipeline. foreach is an alias for the Foreach-Object cmdlet and you’re using –Process as a position parameter for the scriptblock. Written in full you’re doing this

1..10 | ForEach-Object -process {
  [math]::Pow($_, 2)
}

or

1..10 | ForEach-Object -process {
  [math]::Pow($psitem, 2)
}

Just to add to the confusion you also have the option to use the foreach method on the collection

(1..10).foreach({[math]::Pow($psitem, 2)})

This isn’t seen as much though it should be remembered as this is the fastest way to iterate over a collection.

In summary

foreach starting a line is the looping keyword. Faster than the pipeline but increases memory overheads as  the collection has to pre-generated

foreach on the pipeline is an alias for foreach-object. Lower memory requirements as the collection is passed down the pipeline but a bit slower

().foreach({}) is a method on the collection (we treat it as an operator in PowerShell in Action) and is fast but in terms of coding style may be more intuitive to developers than admins.

Modifying AD users in bulk

Modifying AD users in bulk involves either setting one or more properties to the same value for a set of users or reading in the values you need from a data source of some kind.

We prepared some test data in the last post so lets see how we use it.

$users = Import-Csv -Path .\users.csv
foreach ($user in $users){
Get-ADUser -Identity $user.Id |
Set-ADUser -Division $user.Division -EmployeeNumber $user.EmployeeNumber
}

The simplest way is to read in the data and store as a collection of objects. Use foreach to iterate through the set of user information. Get-ADUser gets the appropriate AD account which is piped to Set-ADUser. Set-ADUser is a great cmdlet because it has parameters for most of the user properties.

In this case though we know that some of the users don’t have employee numbers. This means a bit more work. Two approaches are possible – use splatting and the parameters used above or use the –Replace option

Lets look at splatting first

$users = Import-Csv -Path .\users.csv
foreach ($user in $users){
$params = @{
Division = $user.Division
EmployeeNumber = 0
}

if ($user.EmployeeNumber) {
$params.EmployeeNumber = $user.EmployeeNumber
}
else {
$params.Remove('EmployeeNumber')
}

Get-ADUser -Identity $user.Id |
Set-ADUser @params
}

As before read the user information into the $users variable. Iterate over the users with foreach. Create a hashtable for the parameters and their values. Division is always present so that can be set directly. Employeenumber should be tested and if  present the place holder value should be overwritten with the correct value otherwise Employeenumber is removed from the hashtable.

The user account is found and Set-ADUser sets the correct values. Notice how the hashtable is specified to the cmdlet.

Splatting is a great way to dynamically set the parameters you’re using on a particular cmdlet.

Set-ADUser has an alternative – the –Replace parameter.

$users = Import-Csv -Path .\users.csv
foreach ($user in $users){
$params = @{
division = $user.Division
employeeNumber = 0
}

if ($user.EmployeeNumber) {
$params.EmployeeNumber = $user.EmployeeNumber
}
else {
$params.Remove('EmployeeNumber')
}

Get-ADUser -Identity $user.Id |
Set-ADUser -Replace $params
}

This is very similar to the splatting example but instead of splatting the hashtable you use it as the value input to the Replace parameter. If you wrote  out the command it would look like this:

Set-ADUser –Replace @{division = ‘Division B’; employeeNumber  = 100}

With –Replace you’re using the LDAP names of the properties rather than the GUI or PowerShell name – there are differences for instance surname is sn in LDAP.

Modifying AD users in bulk is straightforward with PowerShell and its relatively easy to deal with missing values if you adopt one of the above ideas. Splatting is probably the easiest in this case.

Test data for bulk AD changes

I’ve had a number of questions about changing AD user data in bulk. If you need to do that you need some test data. The specific questions were around setting the Division property and the EmployeeNumber at the same time – but some accounts didn’t have an employee number.

First you need to generate some test data

$count =  1

Get-ADUser -Filter * -SearchBase 'OU=UserAccounts,DC=Manticore,DC=org' |
foreach {

$props = @{
Id = $psitem.SamAccountName
Division = ''
EmployeeNumber = $count
}
switch ($count % 3){
0 {$props.Division = 'Division A'}
1 {$props.Division = 'Division B'}
2 {$props.Division = 'Division C'}
}

if (-not ($count % 15)){$props.Remove('EmployeeNumber')}

New-Object -TypeName PSObject -Property $props

$count ++
} |
Export-Csv -Path users.csv –NoTypeInformation

You need a counter variable. Use Get-ADUser to get you test users and pipe to foreach. You can then create a hashtable for the properties – Id = samaccountname, Division is an empty string and EmployeeNumber is set equal to count.

A switch statement is used to modify the division. Modulo 3 on the $count variable can only give values of 0, 1 or 2.

Remove the EmployeeNumber for every 15th account

Create an object. Increment the counter.

Export the objects to a csv file.  You should end up with something like this:

Division   EmployeeNumber Id
--------   -------------- --
Division B 1              DonJones
Division C 2              DonSmith
Division A 3              DonBrown
Division B 4              DonBlack
Division C 5              DonWhite
Division A 6              DonGreen
Division B 7              DonWood
Division C 8              DonBell
Division A 9              DonHarris
Division B 10             DonFox
Division C 11             JamesJones
Division A 12             JamesSmith
Division B 13             JamesBrown
Division C 14             JamesBlack
Division A                JamesWhite
Division B 16             JamesGreen
Division C 17             JamesWood

etc.

Next time I’ll show you how to deal with the missing employee numbers when you modify the AD accounts.

Get-Content and Numbers

A common technique is to put a list of information into a text file and read that using Get-Content. The information is often server names. This works great when the data is strings but breaks down if you’re dealing with numbers.

Lets start with a text file containing some numbers:

PS> Get-Content -Path .\num.txt
1000
109
258
331
699
744
829

Let’s try and sort those numbers:

PS> Get-Content -Path .\num.txt | sort
1000
109
258
331
699
744
829

It may seem that nothing is happening but if you sort descending:

PS> Get-Content -Path .\num.txt | sort -Descending
829
744
699
331
258
109
1000

Things change but not quite in the way that you might think. The data isn’t being sorted numerically its being sorted as strings.

PS> Get-Content -Path .\num.txt | Get-Member
TypeName: System.String

If you read the help file for Get-Content it states that the cmdlet returns either strings or bytes depending on the content of the file.

So if Get-Content returns strings what can we do if we want to work with the file contents as numbers rather than text.

You have a few choices. You can force a type conversion (known in programming circles as a cast) in a number of ways.

First using the –as operator

PS> Get-Content -Path .\num.txt | foreach {$psitem -as [int]} | sort
109
258
331
699
744
829
1000

That’s more like a numeric sort

You can cut down on the code:

PS> Get-Content -Path .\num.txt | foreach {[int]$psitem} | sort
109
258
331
699
744
829
1000

Or you can use foreach to call  a method on the object

PS> Get-Content -Path .\num.txt | foreach ToInt32($psitem) | sort
109
258
331
699
744
829
1000

You can even use the foreach method on the array that Get-Content creates

PS> (Get-Content -Path .\num.txt).foreach({[int]$psitem})  | sort
109
258
331
699
744
829
1000

The last option is the fastest but is only available on PowerShell 4.0 and above.

Get-Content returns strings but its not difficult to turn them into the numbers you need

Filtering of Objects and Properties

Saw a post on the forum today that suggests people are still confused about how to perform filtering of objects and properties in PowerShell.

As with so much in PowerShell explanations are always better with examples.

 

Let’s start with the physical disks in a computer:

PS> Get-PhysicalDisk

FriendlyName               SerialNumber    CanPool OperationalStatus HealthStatus Usage            Size 
------------               ------------    ------- ----------------- ------------ -----            ---- 
Toshiba USB 2.0 Ext. HDD   WD-WCAMR3209671 False   OK                Healthy      Auto-Select 298.09 GB 
ST916082 1A                DEF10E8D9B36    False   OK                Healthy      Auto-Select 149.05 GB 
Samsung SSD 840 PRO Series S1AXNSAF329511V False   OK                Healthy      Auto-Select 476.94 GB

 

If you want to objects that match a specific criteria – for instance disk is larger than 300GB:

PS> Get-PhysicalDisk | Where-Object Size -gt 300GB

FriendlyName               SerialNumber    CanPool OperationalStatus HealthStatus Usage            Size 
------------               ------------    ------- ----------------- ------------ -----            ---- 
Samsung SSD 840 PRO Series S1AXNSAF329511V False   OK                Healthy      Auto-Select 476.94 GB

 

Where-Object is your friend.

 

What you’re actually doing – though very few people actually write it like this – is

Get-PhysicalDisk | Where-Object -Property Size -GT -Value 300GB

 

The help file for Where-Object lists the possible operators.

 

You can also show the original style syntax

Get-PhysicalDisk | Where-Object -FilterScript {$_.Size -gt 300GB}

 

Normal usage is to not write the –FilterScript parameter so it becomes

Get-PhysicalDisk | Where-Object {$_.Size -gt 300GB}

 

$_ represents the object currently on the pipeline. If you need to use multiply conditions in your filter you’ll need to use the older style syntax.

 

So far you’ve seen how reduce the number of objects on the pipeline. Where-Object filters out those that don’t match the given criteria.

 

If you want to reduce the number of properties that the objects on the pipeline possess you’ll need to use Select-Object

PS> Get-PhysicalDisk | Select-Object -Property FriendlyName, HealthStatus, Size

FriendlyName               HealthStatus         Size 
------------               ------------         ---- 
Toshiba USB 2.0 Ext. HDD   Healthy      320072933376 
ST916082 1A                Healthy      160041885696 
Samsung SSD 840 PRO Series Healthy      512110190592

 

More commonly written as

Get-PhysicalDisk | Select FriendlyName, HealthStatus, Size

 

PowerShell best practice is always to use the full cmdlet and parameter names in your scripts. The *-Object cmdlets and in particular Where-Object, Sort-Object and Select-Object are often abbreviated to Where, Sort and Select and the parameters only used where necessary.  This was the way I was advised to use them by Jeffrey Snover – who invented PowerShell -  when I wrote PowerShell in Practice. Good enough for me.

Append data to a file

A question on the forums - the user wanted to append data to a file. This is a common scenario when you’re creating a log file.

 

There’s 2 easy ways to do this.

 

Lets create a couple of variables with multi-line data

PS> $data = @'
>> This is
>> multiline data
>>
>> '@
PS> $data
This is
multiline data

 

PS> $data2 = @'
>> This is
>> more multiline
>> data
>> '@
PS> $data2
This is
more multiline
data

 

First you could use Out-File

PS> Out-File -FilePath of.txt -InputObject $data
PS> Out-File -FilePath of.txt -InputObject $data2 -Append
PS> Get-Content -Path of.txt
This is
multiline data

This is
more multiline
data

 

First time you call Out-File you don’t have to use –Appemd but you can. On subsequent calls use -Append to add the data – if you don’t the file will be overwritten with the new data.

 

Second option is one you don’t see so much – Add-Content. In earlier versions of PowerShell this was your only option

PS> Add-Content -Path ac.txt -Value $data
PS> Add-Content -Path ac.txt -Value $data2
PS> Get-Content -Path ac.txt
This is
multiline data

This is
more multiline
data

 

If the file doesn’t exist Add-Content will create it.

 

Two ways to append data to a file

Preserving property order

This is a very common pattern:

 

$os = Get-CimInstance -ClassName Win32_OperatingSystem
$comp = Get-CimInstance -ClassName Win32_ComputerSystem

$props = @{
OS = $os.Caption
InstallDate = $os.InstallDate
LastBoot = $os.LastBootUpTime
Make = $comp.Manufacturer
Model = $comp.Model
}

New-Object -TypeName PSObject -Property $props

 

Get some data – in this case a couple of WMI classes and create an output object.

Unfortunately, the output looks like this

 

Make        : Microsoft Corporation
Model       : Surface Pro 2
LastBoot    : 30/12/2016 09:41:52
OS          : Microsoft Windows 10 Pro Insider Preview
InstallDate : 08/12/2016 13:20:04

 

The property order is NOT preserved because you’re working with a hash table. If you absolutely have to preserve the property order use an ordered hash table

 

$os = Get-CimInstance -ClassName Win32_OperatingSystem
$comp = Get-CimInstance -ClassName Win32_ComputerSystem

$props = [ordered]@{
OS = $os.Caption
InstallDate = $os.InstallDate
LastBoot = $os.LastBootUpTime
Make = $comp.Manufacturer
Model = $comp.Model
}

New-Object -TypeName PSObject -Property $props

 

All you do is add [ordered] in front of the hashtable definition and your output becomes

 

OS          : Microsoft Windows 10 Pro Insider Preview
InstallDate : 08/12/2016 13:20:04
LastBoot    : 30/12/2016 09:41:52
Make        : Microsoft Corporation
Model       : Surface Pro 2

 

exactly what you defined