Monthly Archive

PowerShell Basics

WMI dates

Dates as reported by WMI still seem to cause a lot of problems. If you use the WMI cmdlets

£> Get-WmiObject -Class Win32_OperatingSystem | select *date* | fl

InstallDate   : 20131205101649.000000+000
LocalDateTime : 20150728121320.002000+060

 

That format is year, month, day, hour, minute, second then fractions of a second after the decimal point with the final +nnn indicating an offset from Greenwich Mean Time  (UTC) for time zones and daylight saving time.

 

You can read the date presented by WMI but its not intuitive.

 

The PowerShell team added a ConvertToDateTime() to the object output by WMI so that you can easily perform date conversions

£> Get-WmiObject -Class Win32_OperatingSystem | select @{N='Install'; E={$_.ConvertToDateTime($_.Installdate)}}, @{N='Lo
calDate'; E={$_.ConvertToDateTime($_.LocalDateTime)}} | fl

Install   : 05/12/2013 10:16:49
LocalDate : 28/07/2015 12:16:26

 

Though my preferred solution these days is to use the CIM cmdlets as they convert the date for you without any extra effort

£> Get-CimInstance -ClassName Win32_OperatingSystem | select *date* | fl

InstallDate   : 05/12/2013 10:16:49
LocalDateTime : 28/07/2015 12:17:29

Using parameters instead of read-host when getting AD replication data

I’ve seen a lot of scripts recently that use Read-Host to get input data.  This is generally not best practice – I tend to only use Read-Host if I want to get a password and obscure the text on screen.

 

A better practice is to use parameters – either in a function or a script. As an example consider this function that gets AD replication metadata

function get-ADReplmetadata {
param (
[Parameter(Mandatory=$true)]
[string]$ldapfilter,
 
[Parameter(Mandatory=$true)]
[string]$attribute,

[string]$server = 'server02'
)

Get-ADObject -LDAPFilter "($ldapfilter)"  -Properties $attribute |
Get-ADReplicationAttributeMetadata -Server $server -Attribute $attribute

}

 

Get-ADReplicationAttributeMetadata  is awkward to use because it only accepts a distinguished name or a GUID for identifying the object you want to access. Remembering distinguished names or GUIDs  is a pain so I use get-AdObject with an LDAP filter and pipe it to Get-ADReplicationAttributeMetadata .

 

The $server parameter defaults to server02 but can be overridden if you want to use another domain controller

I make the ldapfilter and attributes mandatory so I get prompted if I forget

 

This example pulls back meta data for just the Name

get-ADReplmetadata -ldapfilter 'samAccountName=Richard' -attribute Name

 

This example pulls back all metadata

get-ADReplmetadata -ldapfilter 'samAccountName=Richard' -attribute *

Data for comparisons

A question on the forum asked about storing data used in comparisons, The example was based on a list if IP addresses where some were known to be good and the questioner wanted to filter out the known good ones so he just had to investigate the rest.

 

You could put the data in your script

Remove-Item -Path C:\TestScripts\unknownip.txt -Force
$safe = '127.0.0.1',
'10.10.54.199',
'10.10.54.200',
'192.168.0.1'

$ips = '127.0.0.1', '10.10.54.199', '172.16.5.1', '192.168.0.47', '10.10.54.200'

foreach ($ip in $ips){
if ($ip -notin $safe) {
   Out-File -InputObject $ip -FilePath C:\TestScripts\unknownip.txt -Append
}
}

cat C:\TestScripts\unknownip.txt

 

The advantage is that you only maintain one file. The disadvantage come sif you want to use the data in another script.

 

Another approach is to put the data in  a file

127.0.0.1
10.10.54.199
10.10.54.200
192.168.0.1

 

I saved it as safeips.txt

 

The script changes to

Remove-Item -Path C:\TestScripts\unknownip.txt -Force
$safe = Get-Content -Path C:\TestScripts\safeips.txt

$ips = '127.0.0.1', '10.10.54.199', '172.16.5.1', '192.168.0.47', '10.10.54.200'

foreach ($ip in $ips){
if ($ip -notin $safe) {
   Out-File -InputObject $ip -FilePath C:\TestScripts\unknownip.txt -Append
}
}

cat C:\TestScripts\unknownip.txt

 

In production you’d obviously read the IPs to test from a file of some kind rather than hard code in the script.

 

The second approach involves maintaining 2 files but gives greater flexibility – its the approach I prefer

Input validation on multiple regex

One of the things I like about writing advanced functions is the ability to validate the input. if you test the input immediately you can  often stop mistakes being made. Mistakes that could damage your system!

 

One of the validation options is to test the input against a regular expression. Not being a big fan of regular expressions I don’t use this often but one option that came up on the forum was testing against more than one regex.

 

DISCLAIMER – The regex I’m using aer for illustrative purposes only and I don’t claim they are the best way of doing this.

 

This function uses a simple regex to validate the input starts with a letter:

function Test-Pval
{
  [CmdletBinding()]
  Param
  (
    [ValidatePattern("^[a-z]")]
    [String]
    $parm1
  )
  Write-Host "Input was $parm1"
}

 

£> Test-Pval abcd
Input was abcd

 

£> Test-Pval Abcd
Input was Abcd

 

£> Test-Pval 2bcd
Test-Pval : Cannot validate argument on parameter 'parm1'. The argument "2bcd" does not match the "^[a-z]" pattern.
Supply an argument that matches "^[a-z]" and try the command again.
At line:1 char:11
+ Test-Pval 2bcd
+           ~~~~
    + CategoryInfo          : InvalidData: (:) [Test-Pval], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Test-Pval

 

Lets also assume that we want to be able to accept input starting with @ or #

function Test-Pval
{
  [CmdletBinding()]
  Param
  (
    #[ValidatePattern("^[a-z]")]
    [ValidatePattern("^[@,#]")]
    [String]
    $parm1
  )
  Write-Host "Input was $parm1"
}

 

£> Test-Pval 2bcd
Test-Pval : Cannot validate argument on parameter 'parm1'. The argument "2bcd" does not match the "^[@,#]" pattern.
Supply an argument that matches "^[@,#]" and try the command again.
At line:1 char:11
+ Test-Pval 2bcd
+           ~~~~
    + CategoryInfo          : InvalidData: (:) [Test-Pval], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Test-Pval
 

£> Test-Pval abcd
Test-Pval : Cannot validate argument on parameter 'parm1'. The argument "abcd" does not match the "^[@,#]" pattern.
Supply an argument that matches "^[@,#]" and try the command again.
At line:1 char:11
+ Test-Pval abcd
+           ~~~~
    + CategoryInfo          : InvalidData: (:) [Test-Pval], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Test-Pval
 

£> Test-Pval @bcd
Test-Pval : Cannot validate argument on parameter 'parm1'. The argument "" does not match the "^[@,#]" pattern.
Supply an argument that matches "^[@,#]" and try the command again.
At line:1 char:11
+ Test-Pval @bcd
+           ~~~~
    + CategoryInfo          : InvalidData: (:) [Test-Pval], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Test-Pval
 

£> Test-Pval '@bcd'
Input was @bcd

 

£> Test-Pval '#bcd'
Input was #bcd

 

£> Test-Pval '?bcd'
Test-Pval : Cannot validate argument on parameter 'parm1'. The argument "?bcd" does not match the "^[@,#]" pattern.
Supply an argument that matches "^[@,#]" and try the command again.
At line:1 char:11
+ Test-Pval '?bcd'
+           ~~~~~~
    + CategoryInfo          : InvalidData: (:) [Test-Pval], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Test-Pval

 

 

The previous acceptable input is now rejected and only input starting with @ or # is accepted.

To combine the 2 patterns we use the | symbol which is an OR operator in regex

function Test-Pval
{
  [CmdletBinding()]
  Param
  (
    [ValidatePattern("^[a-z | @,#]")]
    [String]
    $parm1
  )
  Write-Host "Input was $parm1"
}

function Test-Pval
{
  [CmdletBinding()]
  Param
  (
    [ValidatePattern("^[a-z | @,#]")]
    [String]
    $parm1
  )
  Write-Host "Input was $parm1"
}

 

Which when used give this

£> Test-Pval abcd
Input was abcd

 

£> Test-Pval Abcd
Input was Abcd

 

£> Test-Pval 2bcd
Test-Pval : Cannot validate argument on parameter 'parm1'. The argument "2bcd" does not match the "^[a-z | @,#]"
pattern. Supply an argument that matches "^[a-z | @,#]" and try the command again.
At line:1 char:11
+ Test-Pval 2bcd
+           ~~~~
    + CategoryInfo          : InvalidData: (:) [Test-Pval], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Test-Pval
 

£> Test-Pval '@bcd'
Input was @bcd

 

£> Test-Pval '#bcd'
Input was #bcd

 

£> Test-Pval '?bcd'
Test-Pval : Cannot validate argument on parameter 'parm1'. The argument "?bcd" does not match the "^[a-z | @,#]"
pattern. Supply an argument that matches "^[a-z | @,#]" and try the command again.
At line:1 char:11
+ Test-Pval '?bcd'
+           ~~~~~~
    + CategoryInfo          : InvalidData: (:) [Test-Pval], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Test-Pval

Expect to spend a long time figuring out the regex though Smile

Disk identification

A recent question on the forums regarded using the Win32_LogicalDisk class to retrieve disk size and free space data.  A filter based on the disk letter was being used. The main error with the code was that the filter was being expressed as

 

"DeviceId='E'"

rather than

"DeviceId='E:"

 

The colon is necessary as its part of the DeviceId data – if you are in doubt about the form of the data required by the filter then examine the full output of the class to see an example.

 

There were a couple of other basic issues.

 

Firstly always output objects.

 

Secondly use the size constants MB, GB etc rather than calculating refresh each time.

 

The final modified code looks like this

$computername = $env:COMPUTERNAME
$partition = 'C:'
$description = 'backup_server'

Get-WmiObject -Class Win32_LogicalDisk -Filter "DeviceId='$partition'"     -ComputerName $computername |
select PSComputerName,
@{Name='Partition'; Expression={$_.DeviceId}},
@{Name='Description'; Expression={$description}},
@{Name='Size(GB)';Expression={[math]::Round(($_.Size / 1GB), 2)}},
@{Name='FreeSpace(GB)';Expression={[math]::Round(($_.FreeSpace / 1GB), 2)}}

Number of working days

Need to know the number of working days left until a specific date?

 

$we = [System.DayOfWeek]::Saturday, [System.DayOfWeek]::Sunday

$date = Get-Date -Year 2015 -Month 8 -Day 28
$now = (Get-Date).AddDays(-1)

$workdays = 0

while ($now -le $date){
 
$now = $now.AddDays(1)

if ($now.DayOfWeek -notin $we ) {
  $workdays++
}

}
$workdays

 

Create a collection of days you don’t want counting – in my case Saturday & Sunday

Set the date you want to count to and current date – the –1 on current day is to set the variable for the loop

 

In the loop increment the date and test against you collection of excluded days. Increment your workday counter and loop

 

Now you can work out how many days until that holiday , course or whatever.  The end date is included in the count by the way

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]