Monthly Archive

Categories

PowerShell Basics

Boolean in Where-Object filter

I was testing some code yesterday and realised there was a quirk in the way the original where syntax (with {}) worked and the way the newer syntax worked.

 

To demonstrate this I created a set of objects

$i = 0

$tests = while ($i -lt 25){
  New-Object -TypeName PSObject -Property @{
    Index = $i
    Current = if (-not($i % 2)){$true} else {$false}
  }
  $i++
}

 

Object properties are a numeric index and a boolean value

 

If you want just the $true values many people write this

$tests | where {$_.Current -eq $true}

 

or if using the newer syntax use this

$tests | where Current -eq $true

 

This is unnecessary typing as you can do this

$tests | where {$_.Current}

$tests | where Current

 

The reason is that the filter you are creating tests a property of the current object against your criteria and passes is if the result is true. A boolean property will by definition either be true of false so just need to test directly

 

if you want to double negative type test i.e. – not $true (which I don’t recommend as its very easy to get into  logic mess) then you have to do this

$tests | where {-not $_.Current}

 

as this fails
$tests | where –not Current

 

You could do this

$tests | where Current -ne $true

 

but it negates the whole code simplification objective

Months

It would be nice to be able to do this:

PS>  Get-Date -Day 25 -Month December -Year 2016
Get-Date : Cannot bind parameter 'Month'. Cannot convert value "December" to type "System.Int32".
Error: "Input string was not in a correct format."
At line:1 char:25
+ Get-Date -Day 25 -Month December -Year 2016
+                         ~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Get-Date], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.PowerShell.Commands.GetDateCo
   mmand

 

You have to do this:

PS>  Get-Date -Day 25 -Month 12 -Year 2016

25 December 2016 11:00:56

 

The time is set to current time automatically.

 

One of the gaps in .NET is an enum for the months of the year. You get an enum for days of the week:

PS>  [System.DayOfWeek]::Friday
Friday

 

PS>  0..6 | foreach {[System.DayOfWeek]$psitem}
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday

 

PowerShell v5 gives you the capability to make your own enums:

enum MonthsOfYear {
January = 1
February = 2
March = 3
April = 4
May = 5
June = 6
July = 7
August = 8
September = 9
October = 10
November = 11
December = 12
}

 

PS>  [MonthsOfYear]10
October

PS>  [MonthsOfYear]::April
April

 

PS>  1..12 | foreach {[MonthsoFYear]$PSItem}
January
February
March
April
May
June
July
August
September
October
November
December

 

PS>  [System.Enum]::GetNames([MonthsOfYear])
January
February
March
April
May
June
July
August
September
October
November
December

 

Now you can use the name of the month

PS>  Get-Date -Day 25 -Month ([MonthsOfYear]::December) -Year 2016

25 December 2016 11:19:38

 

Enums are one of those pieces of functionality that you don’t necessarily think about but are very useful. The ability to create your own in PowerShell is very useful and will become more widespread as people realise its available.

WMI Filters

A common mistake with WMI/CIM filters is:

PS>  Get-WmiObject -Class Win32_LogicalDisk -Filter "DeviceId=C:"
Get-WmiObject : Invalid query "select * from Win32_LogicalDisk where DeviceId=C:"
At line:1 char:1
+ Get-WmiObject -Class Win32_LogicalDisk -Filter "DeviceId=C:"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Get-WmiObject], ManagementException
    + FullyQualifiedErrorId : GetWMIManagementException,Microsoft.PowerShell.Commands.GetWmiObjectCo
   mmand

 

The clue is in the invalid query error message

When you use the –Filter parameter and are testing a property of type string the value you are testing against has to be in quotes

PS>  Get-WmiObject -Class Win32_LogicalDisk -Filter "DeviceId='C:'"

DeviceID     : C:
DriveType    : 3
ProviderName :
FreeSpace    : 108999528448
Size         : 248480002048
VolumeName   : Windows

 

The filter is defined as a string so you need to use single quotes inside the double quotes. You could mess around with all single quotes but then you have to escape the inner set of quotes – good luck with that – its an unnecessary exercise in frustration

 

How do you know the property is a string?

PS>  (Get-CimClass -ClassName Win32_LogicalDisk).CimClassProperties['DeviceId']

Name               : DeviceID
Value              :
CimType            : String
Flags              : Property, Key, ReadOnly, NullValue
Qualifiers         : {CIM_Key, read, key, MappingStrings...}
ReferenceClassName :

 

The same rules for –Filter apply to Get-CimInstance

Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceId='C:'"

32 or 64 and/or Administrator

When you run the PowerShell console (or ISE) the default icon runs a 32 or 64 bit version that matches your OS. On a 64 bit machine you have the option of running in 32bit (icons have a (x86) suffix on the title.

 

How can you tell whether you’re running in 32 or 64 bit mode?

 

One way is shown in this forum question - http://powershell.org/forums/topic/requires/#post-42226

 

I prefer the simpler:

if ([System.IntPtr]::Size -eq 8) {$size = '64 bit'}
else {$size = '32 bit'}

 

I don’t like automatically kicking into the required bit version if its wrong. I prefer to abort the processing with a message to run as 32 or 64 bit as appropriate

 

You can perform a similar test for administrator privileges (running elevated)

$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$secprin = New-Object Security.Principal.WindowsPrincipal $currentUser
if ($secprin.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator))
{$admin = 'Administrator'}
else {$admin = 'non-Administrator'}

 

Though on later versions of Powershell its easier to use

#requires –RunAsAdministrator

Dates in file and folder names

If you want to incorporate the date in a file or folder name you can’t use Get-Date directly

PS>  Get-Date

01 June 2016 20:52:03

 

The simplest answer is to use the –Format or –Uformat parameters:

PS>  Get-Date -Format yyyyMMdd
20160601

PS>  Get-Date -UFormat %Y%m%d
20160601

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