Monthly Archive

PowerShell Basics

Counting members

If you have a collection of objects

$proc = get-process

 

you can get the number of members using the Length property

PS>  $proc.Length
71

$proc is of type System.Array

 

PS>  $proc.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

 

MSDN documenattion is at https://msdn.microsoft.com/en-us/library/system.array(v=vs.110).aspx

 

PowerShell also adds a Count property that is an alias of Length

 

if you are after the number of members in a collection via the pipeline you need to use the Measure-Object cmdlet

PS>  $proc | Measure-Object

Count    : 71
Average  :
Sum      :
Maximum  :
Minimum  :
Property :

 

or measure directly

PS>  Get-Process | Measure-Object

Count    : 71
Average  :
Sum      :
Maximum  :
Minimum  :
Property :

 

Notice that Measure-Object can also be used to discover other statistics – the Average, Sum, Maximum and Minimum. These only work for numeric properties.

Loading assemblies

PowerShell is .NET based but doesn’t load all available .NET assemblies when it starts.

 

Many people still use something  like

[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')

 

to load additional assemblies.  This is a hang over from PowerShell v1 when there wasn’t another way to perform the load.

The LoadWithPartialName method has been deprecated - https://msdn.microsoft.com/en-us/library/12xc5368(v=vs.110).aspx – and shouldn’t be used.

 

Your alternatives are:

Add-Type -AssemblyName System.Windows.Forms

 

or in PowerShell v5

using assembly System.Windows.Forms

using namespace System.Windows.Forms

 

can be used

New variables with the variable cmdlets

 

So you have some data in csv format:

column1 column2 column3 column4
------- ------- ------- -------
a1      b1      c1      d1
a1      b2      c1      d1
a1      b3      c1      d1
a2      b3      c1      d1
a2      b4      c1      d1
a2      b3      c1      d1
a3      b5      c1      d1
a3      b6      c1      d1
a3      b7      c1      d1
a4      b5      c1      d1

 

In a variable $cd

 

You want colum1 and column2 in a new variable

 

Simplest way is probably

$new1 = $cd | select Column1, column2

 

The *-Variable cmdlets don’t get out much so I thought examples using them would be useful

You could also use the New-Variable cmdlet

New-Variable -Name new2 -Value ($cd | select Column1, column2)

 

Set-Variable also works

Set-Variable -Name new3 -Value ($cd | select Column1, column2)

Folder creation dates from WMI

A question on the powershell.org about finding the creation date of folders raises some interesting points

 

To find a folder’s creation date use:

Get-WmiObject -Class Win32_Directory -Filter "Drive='C:' AND Path = '\\users\\$user\\'" | select Name, @{N='Creation date'; E={$_.ConvertToDateTime($_.CreationDate)}}

 

OR

 

Get-CimInstance -ClassName Win32_Directory -Filter "Drive='C:' AND Path = '\\users\\$user\\'" | select Name, CreationDate

 

If you use Get-WmiObject the date is returned in the form

20160128110039.938756+000

 

Which is why you need to perform the conversion using the ConvetToDateTime method that PowerShell adds to every WMI object.

 

Get-CimInstance automatically performs the conversion for you.

 

The other interesting part is the filter

"Drive='C:' AND Path = '\\users\\$user\\'"

 

Note that it’s wrapped in double quotes. Each of the values is a string so HAS to be in single quotes. Also note that you need to double the \ characters as WMI treats a single \ as an escape character so you have to escape the escape character.

Monitor Info

A question on the forum about combining information from 2 CIM classes produced this:

 

function Get-MonitorInfo {
    [CmdletBinding()]
    param(
        $computername = $env:COMPUTERNAME
    )

    $cs = New-CimSession -ComputerName $computername
   
    $monitors =  Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorId -Filter "Active = '$true'" -CimSession $cs
   
    foreach ($monitor in $monitors) {
       
        $in = ($monitor.InstanceName).Replace('\', '\\')
        Write-Verbose -Message $in
        $dp = Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorBasicDisplayParams -Filter "InstanceName = '$in'" -CimSession $cs
       
        $name = ''

        foreach ($c in $monitor.UserFriendlyName){
            if ($c -ne '00'){$name += [char]$c}
        }

        $type = 'Unknown'
        switch ($dp.VideoInputType){
            0 {$type = 'Analog'}
            1 {$type = 'Digital'}
        }
       
        New-Object -TypeName PSObject -Property @{
            Name = $name
            Type = $type
        }
    }
   
    Remove-CimSession -CimSession $cs
}

 

Create a CIM session to the computer. Get the instances of the WmiMonitorId class. Iterate through them and find the matching WmiMonitorBasicDisplayParams class instance.

 

The InstanceName of the monitor will look like this:

DISPLAY\GSM598F\4&19086f00&0&UID200195_0

you need to replace \ by \\ to use the value in a CIM query because \ is treated as the escape character and you have to escape it to use it

 

Translate the UserFriendly name by converting the byte array to a string and determine the VideoInputType using the switch.

 

Create an object and output

Comparing lists

An interesting question on the forum regarding how you compare the contents of 2 collections.  The question revolved around comparing the contents of the Machine property of

HKLM:\System\CurrentControlSet\Control\SecurePipeServers\Winreg\AllowedExactPaths

 

with

$Machine = @(
'System\CurrentControlSet\Control\ProductOptions',
'System\CurrentControlSet\Control\Server Applications',
'Software\Microsoft\Windows NT\CurrentVersion'
)

 

Start by getting the data from the registry

$p = Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\SecurePipeServers\Winreg\AllowedExactpaths' -Name Machine

 

$p.Machine
System\CurrentControlSet\Control\ProductOptions
System\CurrentControlSet\Control\Server Applications
Software\Microsoft\Windows NT\CurrentVersion

 

So the 2 look identical but how do we do that programmatically.

 

You can’t do

$machine -eq $p.machine

 

you get nothing back.

You have to use Compare-Object:

 

Compare-Object $machine $p.machine

 

You’ll get nothing back so check  everything is equal:

PS> Compare-Object $machine $p.machine -IncludeEqual

InputObject                                                                     SideIndicator
-----------                                                                         -------------
System\CurrentControlSet\Control\ProductOptions        ==
System\CurrentControlSet\Control\Server Applications ==
Software\Microsoft\Windows NT\CurrentVersion            ==

 

Compare-Object returns nothing if the 2 objects match so your comparison ends up as

PS> if ( -not (Compare-Object $machine $p.machine)){'yay'}else{'nay'}
yay

 

You can test it works

$m2 = 'item1', 'item2', 'item3'

PS> Compare-Object $machine $m2

InputObject                                          SideIndicator
-----------                                          -------------
item1                                                =>
item2                                                =>
item3                                                =>
System\CurrentControlSet\Control\ProductOptions      <=
System\CurrentControlSet\Control\Server Applications <=
Software\Microsoft\Windows NT\CurrentVersion         <=

 

PS> if ( -not (Compare-Object $machine $m2)){'yay'}else{'nay'}
nay

CSV file with [] in headers

With the PowerShell Summit registration deadline rapidly approaching I wanted to see how registrations were going. I can down load a CSV file from the event website that lists attendees.

Great. Just need to do some sorting and grouping and I’m golden.

 

I’m running PowerShell 5.0 on Windows 10 Insider build 14271

 

First off I discovered that the headers of the CSV file contain [] i.e. they look like this for example:

[UserName]
[REGISTRATIONSTATUS]
[AttendeeType]
[ConfirmationCode]

 

Sorting by attendee type

 

Import-Csv -Path C:\test1\AttendeeReport.csv |
Sort-Object -Property [AttendeeType]

works but if I add a check for complete registrations

 

Import-Csv -Path C:\test1\AttendeeReport.csv |
Where-Object [REGISTRATIONSTATUS] -eq 'Complete'|
Sort-Object -Property [AttendeeType]

 

I get nothing back.

 

Import-Csv -Path C:\test1\AttendeeReport.csv |
Where-Object '[REGISTRATIONSTATUS]' -eq 'Complete'|
Sort-Object -Property [AttendeeType]

 

Is no better. You need to revert to old style where-object syntax

Import-Csv -Path C:\test1\AttendeeReport.csv |
Where-Object {$_.'[REGISTRATIONSTATUS]' -eq 'Complete'}|
Sort-Object -Property [AttendeeType]

 

Now I want to group on attendee type

PS> Import-Csv -Path C:\test1\AttendeeReport.csv |
>> Where-Object {$_.'[REGISTRATIONSTATUS]' -eq 'Complete'}|
>> Sort-Object -Property [AttendeeType] |
>> Group-Object -Property [AttendeeType] -NoElement
Group-Object : Wildcard characters are not allowed in "[AttendeeType]".
At line:4 char:1
+ Group-Object -Property [AttendeeType] -NoElement
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Group-Object], NotSupportedException
    + FullyQualifiedErrorId : ExpressionGlobbing2,Microsoft.PowerShell.Commands.GroupObjectCommand

 

This partially works

Import-Csv -Path C:\test1\AttendeeReport.csv |
Where-Object {$_.'[REGISTRATIONSTATUS]' -eq 'Complete'}|
Sort-Object -Property [AttendeeType] |
Group-Object -Property '`[AttendeeType`]' -NoElement

 

But just gives me a count of the total number of attendees.

 

I tried a number of ways of dealing with the [] in the headers and therefore in the property names. In the end I decided it was going to be easier to completely reset the headers in the csv file:

Import-Csv -Path C:\test1\AttendeeReport.csv -Header  'UserName', 'REGISTRATIONSTATUS', 'AttendeeType', 'ConfirmationCode', 'PaymentAuthorization', 'RegisteredByEmail', 'PromotionCode', 'BioName', 'BioTitle', 'BioEmail', 'Created', 'Updated', 'LastName', 'Dietary', 'Alumni', 'FirstName', 'Twitter', 'BestEmail'

 

This means I have to skip the first record because it looks like this:

UserName             : [UserName]
REGISTRATIONSTATUS   : [REGISTRATIONSTATUS]
AttendeeType         : [AttendeeType]
ConfirmationCode     : [ConfirmationCode]
PaymentAuthorization : [PaymentAuthorization]
RegisteredByEmail    : [RegisteredByEmail]
PromotionCode        : [PromotionCode]
BioName              : [BioName]
BioTitle             : [BioTitle]
BioEmail             : [BioEmail]
Created              : [Created]
Updated              : [Updated]
LastName             : [LastName]
Dietary              : [Dietary]
Alumni               : [Alumni]
FirstName            : [FirstName]
Twitter              : [Twitter]
BestEmail            : [BestEmail]

 

My grouping script is now much simpler

Import-Csv -Path C:\test1\AttendeeReport.csv -Header  'UserName', 'REGISTRATIONSTATUS', 'AttendeeType', 'ConfirmationCode', 'PaymentAuthorization', 'RegisteredByEmail', 'PromotionCode', 'BioName', 'BioTitle', 'BioEmail', 'Created', 'Updated', 'LastName', 'Dietary', 'Alumni', 'FirstName', 'Twitter', 'BestEmail' |
Select-Object -Skip 1 |
Where-Object REGISTRATIONSTATUS -eq 'Complete'|
Sort-Object -Property AttendeeType |
Group-Object -Property AttendeeType –NoElement

 

If I come across other CSV files with [] in the headers I’m going to go for immediate replacement of the headers as the way to get the job done.

Scripting Game puzzle – – January 2016

Here’s how I’d solve the puzzle

function get-starttime {
    [CmdletBinding()]
    param(
        [parameter(
                ValueFromPipeline=$true,
                ValueFromPipelineByPropertyName=$true)]
        [Alias('CN', 'Computer')]
        [ValidateNotNullOrEmpty()] 
        [string[]]$computername = $env:COMPUTERNAME
    )
   
    PROCESS {
   
        foreach ($computer in $computername){
            $props = [ordered]@{
                ComputerName = $computer
                StartTime = ''
                'UpTime (Days)' = 0.0
                Status = 'OFFLINE'
            }
   
            if (Test-WSMan -ComputerName $computer -ErrorAction SilentlyContinue) {
                $lbt = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $computer -ErrorAction SilentlyContinue
               
                if ($lbt) {
                
                    $props['StartTime'] = $lbt.LastBootUpTime
           
                    $upt = [math]::round(((Get-Date) - $lbt.LastBootUpTime).TotalDays, 1)
                    $props['UpTime (Days)'] = $upt
               
                    $props['Status'] = 'OK'
                }
                else {
                    $props['Status'] = 'ERROR'
                }
           
            } ## endif
           
            New-Object -TypeName PSObject -Property $props
       
        } ## end foreach
   
    } ## end PROCESS
}

Create an advanced function. Yes I know I’ve used lower case for the function name. I always do to visually separate my code from cmdlets and other functions.

 

Use the [parameter] decorator to enable pipeline input. Only a single parameter so don’t need to bother woth positional parameters. Function is supposed to default to local machien so can’t make parameter mandatory.

 

Requirement to process multiple computers at once presumably means the computername parameter has to take an array – sumultaneous processing implies a work flow which negates the initial requirement to create a function

 

Use the PROCESS block to run a foreach loop that iterates over the collection of computernames.

 

Create a hash table for the results – I’ve used an ordered hash table to preserve the property order. Set the values to a failed connection.

 

use Test-Wsman to see if can reach the computer. If can’t the output object is created. If you can reach the machine then run Get-CimInstance - preferred over Get-WmiObject because it returns the date ready formatted

 

Assuming that works set the start time and status properties. Calculate the uptime in days. I’d prefer to see  just an integer here – tenths of days doesn’t mean anything to most people

 

If the call to Get-CimInstance  fails then set the status to ERROR

Output the object.

 

The requirement to add a proeprty for patching is not clear but I’m assuming it means if the machine has been up for more than 30 days with the 1/10 month as a typo

if you want to add that then

 

Add a property

MightNeedPatching = $false

to the hash table when you create it

 

and add this line

if ($upt -ge 30){$props['MightNeedPatching'] = $true}

after

$upt = [math]::round(((Get-Date) - $lbt.LastBootUpTime).TotalDays, 1)
$props['UpTime (Days)'] = $upt

FQDN

How do you find the FQDN of the machine you’re using. 

The simplest way is to combine a couple of environmental variables:

PS> "$env:COMPUTERNAME.$env:USERDNSDOMAIN"
SERVER02.MANTICORE.ORG

 

If you like using CIM (and who doesn’t) you can try this

PS> Get-CimInstance -ClassName Win32_ComputerSystem |
>> select @{N='FQDN'; E={"$($_.DNSHostName).$($_.Domain)"}}
>>

FQDN
----
server02.Manticore.org

 

This could easily be used for remote machines as well by adding the –ComputerName parameter to Get-CimInstance

 

If you want to go down the .NET route you have:

PS> [System.Net.Dns]::GetHostByName('').HostName
server02.Manticore.org

Testing against an arrays contents

You may need to test if a value is a member of an array. PowerShell provides 2 operators for testing array membership  - -   -in and –contains.

Simple usage is like this

 

PS> $colours = 'red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'

 

PS> 'blue' -in $colours
True

PS> $colours -contains 'blue'
True

 

Note the order of value and array changes between the operators.

 

You  can also use these operators in Where-Object

 

PS> $testcolours = 'blue', 'pink', 'yellow'

 

PS> $testcolours | where {$_ -in $colours}
blue
yellow

PS> $testcolours | where {$colours -contains $_}
blue
yellow

 

Often the value we want is a property of an object

 

PS> $to = New-Object -TypeName PSObject -Property @{Colour = 'green'}
PS> $to1 = New-Object -TypeName PSObject -Property @{Colour = 'pink'}

 

PS> $to, $to1 | where Colour -in $colours

Colour
------
green

PS> $to, $to1 | where {$colours -contains $_.Colour}

Colour
------
green

 

The –in operator can be used in the simplified Where-Object syntax (it was introduced for that purpose) but –contains has to use the full, original syntax

 

For testing non-membership you also get –notin and –notcontains

 

PS> $to, $to1 | where Colour -notin $colours

Colour
------
pink

PS> $to, $to1 | where {$colours -notcontains $_.Colour}

Colour
------
pink