Monthly Archive

PowerShell Basics

String startswith method

If you look at the methods available on a string one of them is StartsWith(). It tests if a given string starts with another string

 

£> 'frgrughrugu'.StartsWith

OverloadDefinitions
-------------------
bool StartsWith(string value)
bool StartsWith(string value, System.StringComparison comparisonType)
bool StartsWith(string value, bool ignoreCase, cultureinfo culture)

 

 

The first option is the easiest to use and the most common scenario

 

£> 'frgrughrugu'.StartsWith('frg')
True
£> 'frgrughrugu'.StartsWith('xya')
False

 

Unlike most things in PowerShell this comparison is NOT case insensitive

£> 'frgrughrugu'.StartsWith('fRg')
False

 

The second option helps by letting you use the members of the System.StringComparison enumeration to control the way the comparison is performed

 

£> $ct = [System.StringComparison]::CurrentCultureIgnoreCase

 

£> 'frgrughrugu'.StartsWith('frg', $ct)
True
£> 'frgrughrugu'.StartsWith('fRg', $ct)
True

 

The final option allows you to use a culture to control the way the comparison is performed. $null implies use the current culture. Note how the boolean controlling the case sensitivity works:

£> 'frgrughrugu'.StartsWith('frg', $true, $null)
True
£> 'frgrughrugu'.StartsWith('fRg', $true, $null)
True
£> 'frgrughrugu'.StartsWith('fRg', $false, $null)
False
£> 'frgrughrugu'.StartsWith('frg', $false, $null)
True

CIM filters

I was looking up Win32_SystemDriver on the MSDN site and noticed there was some PowerShell example code

 

Get-WmiObject -Class Win32_SystemDriver |
Where-Object -FilterScript {$_.State -eq "Running"} |
Where-Object -FilterScript {$_.StartMode -eq "Manual"} |
Format-Table -Property Name,DisplayName

 

A better way to write this would be:

Get-WmiObject -Class Win32_SystemDriver -Filter "State='Running' AND StartMode='Manual'" | Format-Table -Property Name, DisplayName –AutoSize

 

or

 

Get-CimInstance -ClassName Win32_SystemDriver -Filter "State='Running' AND StartMode='Manual'" | Format-Table -Property Name, DisplayName -AutoSize

 

Do the filtering in the CIM call – especially if you’re running this against a number of remote machines. That way you limit the network traffic you’re returning

Multiple expands

PowerShell outputs objects but sometimes you need just the values. The –Expandproperty parameter of select-object can pull the values from a property.  Compare:

£> Get-VM | select Name

Name
----
Arista
SphinxLx01
W12R2DSC
W12R2OD01
W12R2SCDC01
W12R2SUS
W12R2TGT
W12R2Web01
W12R2Web02

 

with

 

£> Get-VM | select -ExpandProperty Name
Arista
SphinxLx01
W12R2DSC
W12R2OD01
W12R2SCDC01
W12R2SUS
W12R2TGT
W12R2Web01
W12R2Web02

 

In the first you get an object with just a name property.  In the second you get just the name.

This is good BUT you can only expand a single property in one pipeline.

 

If you need to expand multiple properties you need to do them individually and combine the results into a new object.  For instance to drill down into a 2012 r2 Hyper-V VM and get the IP addresses and the disk size

Get-VM |
foreach {
$props = [ordered]@{
  Name = $($psitem.Name)
  IPAddresses =  $psitem | select -ExpandProperty NetworkAdapters | select -ExpandProperty IPAddresses
  DiskSize = [math]::Round((Get-ChildItem -Path ($psitem | select -ExpandProperty HardDrives | select -ExpandProperty path) | select -ExpandProperty Length) / 1GB, 2)
}
New-Object -TypeName PSObject -Property $props
}

 

The IP addresses are a simple double expansion.  The disk size you have to expand the harddrives, then the path  - get the file length and re-calculate the size to GB.

 

NOTE – I know all my VMs only have a single disk. If you have multiple disks you’ll need to build a loop to get all the sizes

Playing with the range operator

The range operator allows you to reference a range of numbers

1..10

 

is equivalent to

1,2,3,4,5,6,7,8,9,10

 

If you want anything other than numbers you’re stuck as the range operator only works with integers

though you can have a decrementing list

10..1

 

65..74 | foreach {[char]$psitem}

would be A – J

 

If you want A-Z

65..90 | foreach {[char]$psitem}

 

For lowercase letters (a – z)  use

97..122 | foreach {[char]$psitem}

 

You can even work from an array of values

$data = 'value1','value2','value3','value4','value5','value6','value7','value8','value9','value10'

$data[3..6]
$data[6..3]

Typing variables

I was recently asked a question about typing variables after thinking about it came up with this demonstration.

Create a variable with an integer value

£> $a = 2
£> $a.GetType()

IsPublic IsSerial Name
-------- -------- ----
True     True     Int32

 

AS you would expect – you get an integer type.

If you do this

£> $a = '123'
£> $a.GetType()

IsPublic IsSerial Name
-------- -------- ----
True     True     String

 

It changes to a string. Which means you can also do this.

£> $a = 'gdyegf'
£> $a.GetType()

IsPublic IsSerial Name
-------- -------- ----
True     True     String

 

PowerShell variables will adapt their type to the data they contain.

However if you type the variable:

£> [int32]$b = 2
£> $b.GetType()

IsPublic IsSerial Name
-------- -------- ----
True     True     Int32

 

You start with an integer as expected

 

If you use a string that can be converted to an integer – that will happen and your type is still an integer.
£> $b = '123'
£> $b
123
£> $b.GetType()

IsPublic IsSerial Name
-------- -------- ----
True     True     Int32

 

If you try to put a string in the variable

£> $b = 'effAG'
Cannot convert value "effAG" to type "System.Int32". Error: "Input string was not in a correct format."
At line:1 char:1
+ $b = 'effAG'
+ ~~~~~~~~~~~~
    + CategoryInfo          : MetadataError: (:) [], ArgumentTransformationMetadataException
    + FullyQualifiedErrorId : RuntimeException

 

It fails because you can’t convert  'effAG' to an integer.

Untyped PowerShell variables can change their type. If you want to ensure the variable always contains a specific type then force that by typing the variable.

Modifying text

I needed to modify some text somewhere in a file. The file looks like this

##  start file
This is some text.

 

I want to change something.

 

But not this.
##  end file

 

I was playing around with various options.  The simplest I found was this:

£> $txt = Get-Content .\test.txt
£> $txt = $txt.Replace("I want", "I need")
£> Set-Content -Value $txt -Path C:\Test\test.txt
£> Get-Content .\test.txt
##  start file
This is some text.

 

I need to change something.

 

But not this.
##  end file

 

You could simplify to

£> $txt = (Get-Content .\test.txt).Replace("I want", "I need")
£> Set-Content -Value $txt -Path C:\Test\test.txt -PassThru

##  start file
This is some text.

 

I need to change something.

 

But not this.
##  end file

 

The passthru parameter displays the file contents you’ve set.

 

Or if you are a fan of convoluted one liners

£> Set-Content -Value ((Get-Content .\test.txt).Replace("I want", "I need")) -Path C:\Test\test.txt -PassThru
##  start file
This is some text.

 

I need to change something.

 

But not this.
##  end file

 

If you take this approach just make sure your text is uniquely identified otherwise you may change more than you thought.

Variable select

I was working on some code that  accesses a SQL database this afternoon. I only needed to pull back a single column from a single row but which column to pull back is variable depending on other data.

That’s OK

$query = “SELECT $colname FROM tablename WHERE x = ‘y’”

Invoke-SQLcmd –server <server> –database <database> –query $query

 

Now the problem hit me as I need to get the actual value from the object that invoke-sqlcmd returns

I normally do this:

Invoke-SQLcmd –server <server> –database <database> –query $query | select –expandproperty <columnname>

 

And then it dawned on me that I have the column name in $colname so this works

Invoke-SQLcmd –server <server> –database <database> –query $query | select –expandproperty $colname

 

I got so used to explicitly stating the properties I need that I forgot you could use a variable.  If you want an example to try on any system

Get-Service | select -First 1 | select -ExpandProperty $p1

 

or you could try

$p1 = 'Status'
Get-Service | select Name, $p1

 

and change to

$p1 = 'DisplayName'
Get-Service | select Name, $p1

 

Not something you want to do every day but a useful trick when you need it

Awkward file and folder names

Spent some time today dealing with a situation where there were special characters – namely [ ] in folder a file names

£> Get-ChildItem -Path C:\Test

    Directory: C:\Test

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----        21/01/2015     17:58            Awkward [One]
d----        21/01/2015     17:59            Simple One

 

Each folder has 3 files:

File 1.txt
File 2.txt
File 3.txt

 

Get-ChildItem -Path 'C:\Test\Simple One'

will work and show you the contents. When I’m typing paths like this I let Tab completion do the work. Type c:\test\ and use the Tab key to cycle round the available folders.

 

This gives

£> Get-ChildItem -Path 'C:\Test\Awkward `[One`]'
Get-ChildItem : Cannot find path 'C:\Test\Awkward `[One`]' because it does not exist.
At line:1 char:1
+ Get-ChildItem -Path 'C:\Test\Awkward `[One`]'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\Test\Awkward `[One`]:String) [Get-ChildItem], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

 

Unfortunately, the construct produced by Tab completion doesn’t work.  You need to double up on the back ticks so that it functions as an escape character.

Get-ChildItem -Path 'C:\Test\Awkward ``[One``]'

 

But that only shows you the folder not the contents.

Get-ChildItem -Path 'C:\Test\Awkward ``[One``]\*'

OR

Get-ChildItem -Path 'C:\Test\Awkward ``[One``]' -Include *

 

Will show you the contents of the folder.

 

But bizarrely

Get-Content -Path 'C:\Test\Awkward `[One`]\File 1.txt'

Works. As does

Copy-Item -Path 'C:\Test\Awkward `[One`]\File 1.txt' -Destination c:\test2

 

By using back ticks and quotes you can get round most problems like this. Other characters that cause similar problems are commas and quote marks.

Best advice of all – don’t use those awkward characters in your file names if you can possibly avoid it.

Event log dates

You can use Get-EventLog to query the event logs on you system

Get-EventLog -LogName System

 

One frequent task is to check if events occurred during a specific timespan. You may feel that you need to use a where-object filter to do this but there is a simple method.

Get-EventLog -LogName System -After (Get-Date -Date '1/1/2015')

 

Will return all events after the given date. if you don’t give a time your results start at midnight.

Get-EventLog -LogName System  -Before (Get-Date -Date '10/1/2015')

 

Will return all events before 10 January 2015.

You ususally use –Before in conjunction with –After to specify a data range

Get-EventLog -LogName System -After (Get-Date -Date '1/1/2015') -Before (Get-Date -Date '10/1/2015')

 

You can make these ranges quite specific

Get-EventLog -LogName System -After (Get-Date -Date '10/1/2015 14:31:00') -Before (Get-Date -Date '10/1/2015 15:00:00')

foreach, pipelines and $_

I’ve recently seen a few questions where people have been using a pipeline inside a foreach loop and experienced problems when they’ve tried to access properties on the looping objects. To illustrate we’ll start with a CSV file containing emailaddresses and job titles.

£> Import-Csv -Path C:\Test\userdata.txt

emailaddress             title   
------------             -----   
gdreen@Manticore.org     Boss    
dbrown@Manticore.org     Underling
dwhite@Manticore.org     Underling
jdaven@Manticore.org     Minion  
fgreen@Manticore.org     Minion  
dgreensmth@Manticore.org Minion  
dgreenly@Manticore.org   Minion

This is definitely an employee friendly organisation Smile

At the moment AD doesn’t contain any job title information

£> $users = Import-Csv -Path C:\Test\userdata.txt

foreach ($user in $users){
$mail = $user.EmailAddress

Get-ADUser -Filter {EmailAddress -eq $mail} -Properties Title |
select Name, Title

}

Name                                      Title                                  
----                                      -----                                  
Dave Green                                                                       
Dave Brown                                                                       
Dave White                                                                       
Jo Daven                                                                         
Fred Green                                                                       
Dale Greensmith                                                                  
Dave Greenly

 

The approach that causes problems is this:

£> $users = Import-Csv -Path C:\Test\userdata.txt

foreach ($user in $users){
$mail = $user.EmailAddress

Get-ADUser -Filter {EmailAddress -eq $mail} -Properties Title |
foreach {Set-ADUser -Identity $_ -Title $_.Title} |

Get-ADUser -Filter {EmailAddress -eq $mail} -Properties Title |
select Name, Title
}

Name                                      Title                                  
----                                      -----                                  
Dave Green                                                                       
Dave Brown                                                                       
Dave White                                                                       
Jo Daven                                                                         
Fred Green                                                                       
Dale Greensmith                                                                  
Dave Greenly     

 

When you use foreach as a keyword the $_ and $psitem variables aren’t available. These variables represent the current object on the pipeline.  The foreach keyword loop doesn’t have a pipeline as such.

 

Inside the foreach a pipeline is created

Get-ADUser -Filter {EmailAddress -eq $mail} -Properties Title |
foreach {Set-ADUser -Identity $_ -Title $_.Title -PassThru} |
select Name, Title

 

$_ is used correctly to identify the object on which Set-ADUser is to work – its the current object on the pipeline.

 

The use of  $_.Title  to set the user’s job title is where the problem really bites.  $_.Title  refers to the Title property of the current object on the pipeline so you are setting the Title to its existing value.

 

You need to reach back to the $user object that represents the current object from the set you are looping through with foreach to get the correct value

£> $users = Import-Csv -Path C:\Test\userdata.txt

foreach ($user in $users){
$mail = $user.EmailAddress

Get-ADUser -Filter {EmailAddress -eq $mail} -Properties Title |
foreach {Set-ADUser -Identity $_ -Title $user.Title} |

Get-ADUser -Filter {EmailAddress -eq $mail} -Properties Title |
select Name, Title
}

Name                                      Title                                  
----                                      -----                                  
Dave Green                                Boss                                   
Dave Brown                                Underling                              
Dave White                                Underling                              
Jo Daven                                  Minion                                 
Fred Green                                Minion                                 
Dale Greensmith                           Minion                                 
Dave Greenly                              Minion  

 

You’ll see similar problems if you have nested foreach-object loops or a switch statement inside a foreach-object loop.  $_ always refers to the current context and you have to either reach back to the looping variable in the csae of a foreach or set variables on the data in the outer foreach before entering the nested foreach.