Monthly Archive

PowerShell Basics

Creating JSON from a hash table

A question on the forum was asking about creating a JSON structure for use with a REST API.

The original has table looked like this

$body = @{
auth_token = "$auth_token"
items = @{
label = "server1"
value = "OK"
label = "server2"
value = "OK"
}
}

 

This fails because you can’t have duplicate keys in a hash table.

 

One way to create the JSON is like this

$items = @()
$items += @{label = "server1"; value = "OK"}
$items += @{label = "server2"; value = "OK"}

$body = New-Object -TypeName PSOBject -Property @{
auth_token = "$auth_token"
Items = $items
}

ConvertTo-Json -InputObject $body

 

which gives:

{
    "Items":  [
                  {
                      "value":  "OK",
                      "label":  "server1"
                  },
                  {
                      "value":  "OK",
                      "label":  "server2"
                  }
              ],
    "auth_token":  ""
}

 

The items are created by adding hah tables to a pre-existing hash table.  You’re creating a hashtable of hash tables.

 

If you need to control the order of values you need an ordered hash table to use with New-Object

$items = @()
$items += @{label = "server1"; value = "OK"}
$items += @{label = "server2"; value = "OK"}

$props =  [ordered]@{
auth_token = "$auth_token"
Items = $items
}

$body = New-Object -TypeName PSOBject -Property $props

ConvertTo-Json -InputObject $body

 

which gives

{
    "auth_token":  "",
    "Items":  [
                  {
                      "value":  "OK",
                      "label":  "server1"
                  },
                  {
                      "value":  "OK",
                      "label":  "server2"
                  }
              ]
}

Passing data into Jobs

PowerShell jobs are a powerful tool for running processes in the background. You have to remember that a PowerShell job runs in a separate process that only exists for the duration of the job. The results are returned to your process when the job finishes.

 

A user on the forum asked about passing variables into jobs.  He’d done this:

$w = '1..10'
Invoke-Expression $w

and got the expected result

1
2
3
4
5
6
7
8
9
10

 

He then tried this

Start-Job -ScriptBlock { Invoke-Expression $w}

But the job said

Cannot bind argument to parameter 'Command' because it is null.
    + CategoryInfo          : InvalidData: (:) [Invoke-Expression], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.InvokeExpre
   ssionCommand
    + PSComputerName        : localhost

 

Irrespective of whether using Invoke-Expression is a good idea  and I’ll cover that in a future post the job fails because its failing to find $w because the job is running in a separate PowerShell process and $w doesn’t exist.

 

The simplest answer is to make the job self contained

Start-Job -ScriptBlock { $w = '1..10'; Invoke-Expression $w}

 

Everything is in the script block passed to the job and therefore passed to the new PowerShell process.

 

Alternatively, you can pass the variable $w into the script block

Start-Job -ScriptBlock { param($w) Invoke-Expression $w} -ArgumentList $w

 

You need to define a param block on the script block and pass the $w variable to it using the –Argumentlist parameter on start-job.

 

Another option introduced with PowerShell 3.0 us the $using scope modifier

Start-Job -ScriptBlock { Invoke-Expression $using:w}

 

Which is another way to say use the $w variable from the current scope

 

You can find out more on $using from about_scopes, about_Remote_Variables

Masking output

A question on the forum on how to stop the output from New-Item raises an important point. Many cmdlets produce output when run for example

£> New-Item -Type File -Name test.txt

    Directory: C:\Test2

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       08/08/2015     14:14              0 test.txt

 

There are times when you want to hide that output. The converse situation where you want output from cmdlets that don’t normally produce it can be resolved with the –Passthru parameter where present or by using –verbose.

 

You have a number of choices when trying to mask output

£> New-Item -Type File -Name test1.txt | Out-Null
£> [void](New-Item -Type File -Name test2.txt)
£> New-Item -Type File -Name test3.txt > $null
£> $nf = New-Item -Type File -Name test4.txt

 

Any of the above techniques will work. I prefer the first one as I can develop and test the code showing output and then pipe to out-null to mask it

Scripting Games July 12015 thoughts on the solution

The July 2015 puzzle can be found here:

http://powershell.org/wp/2015/07/04/2015-july-scripting-games-puzzle/

 

Write a one-liner that produces the following output (note that property values will be different from computer to computer; that’s fine).

PSComputerName ServicePackMajorVersion Version  BIOSSerial                               
--------------         ----------------------- -------  ----------
win81             0                       6.3.9600 VMware-56 4d 09 1 71 dd a9 d0 e6 46 9f

 

By definition, a one-liner is a single, long command or pipeline that you type, hitting Enter only at the very end. If it wraps to more than one physical line as you’re typing, that’s OK. But, in order to really test your skill with the parser, try to make your one-liner as short as technically possible while still running correctly.

Challenges:

•Try to use no more than one semicolon total in the entire one-liner

•Try not to use ForEach-Object or one of its aliases

•Write the command so that it could target multiple computers (no error handling needed) if desired

•Want to go obscure? Feel free to use aliases and whatever other shortcuts you want to produce a teeny-tiny one-liner.

By definition, a one-liner is a single, long command or pipeline that you type, hitting Enter only at the very end. If it wraps to more than one physical line as you’re typing, that’s OK. But, in order to really test your skill with the parser, try to make your one-liner as short as technically possible while still running correctly.

 

Initial thoughts.

One liner is a mis-nomer that has caused more problems than enough for the PowerShell community. The requirement is correctly stated as a single pipline.  One pipeline can spread over many lines but is called a one-liner. This is one line of code:

get-service; get-process;

but is not a one-liner because the semi-colon is a line termination character so you’ve combined 2 lines but into 1 but they’ll execute as 2 pipelines.

 

Obviously need to use CIM (WMI) to retrieve the data. Standard approach would be Win32_OperatingSystem & Win32_Bios

£> Get-CimClass *Bios

   NameSpace: ROOT/cimv2

CimClassName
------------
Win32_BIOS
Win32_SystemBIOS

£> Get-CimClass *OperatingSystem

   NameSpace: ROOT/cimv2

CimClassName
------------
CIM_OperatingSystem
Win32_OperatingSystem
Win32_SystemOperatingSystem

 

If you want to shave off a couple of characters you could use the CIM_OperatingSystem class as Boe suggests http://powershell.org/wp/2015/07/29/2015-july-scripting-games-wrap-up/

 

Be aware that the CIM_ classes adhere to the standard definition from the DMTF - http://www.dmtf.org/standards/cim and Win32_ classes are the Microsoft version from when WMI was introduced. The win32_ classes are often subtly different to the CIM_ classes so check carefully with Get-CimClass before using.

 

My first though was to use Get-CimInstance

Get-CimInstance -ClassName Win32_OperatingSystem |
select PSComputerName, ServicePackMajorVersion, Version,
@{N='BIOSSerial'; E={(Get-CimInstance -ClassName Win32_Bios).SerialNumber}}

 

But for the local machine that won’t return the PSComputerName attribute unless you use the –ComputerName parameter and point to the local machine or pipe the computer name to the cmdlet

Get-WmiObject doesn’t have that problem

Get-WmiObject -ClassName Win32_OperatingSystem |
select PSComputerName, ServicePackMajorVersion, Version,
@{N='BIOSSerial'; E={(Get-CimInstance -ClassName Win32_Bios).SerialNumber}}

 

The trick here is to use a calculated field to get the BIOS serial number

 

I prefer using Invoke-Command to get the data   from a  remote machine. I get the RunSpaceId as well but that can be filtered out.

 

The last part of the puzzle was to shrink the code to the smallest number of characters possible. I’ve never seen the point of that in production use so never bother – if I wanted to write something unreadable I’d use Perl. The time taken to shrink is never regained and I can use tab completion to get my code input quickly and working. Boe has done an excellent job of showing that could be done so I’ll point you  in that direction

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