Monthly Archive

Categories

PowerShell

Code reviews and Iron Scripter

Many organisations use code reviews to help ensure a quality outcome. At the recent PowerShell Summit we ran a scripting competition – Iron Scripter. I’ve had a thought on how code reviews and Iron Scripter have something in common. Something that could lead to a better code review.

Iron Scripter involved the attendees splitting into 3 factions – each with its own philosophy:

Battle faction produce code that’s good enough to do the job then move onto the next problem.

Daybreak faction produce beautiful code that is easy to read and understand

Flawless faction produces code that “never” fails – it has all of the error handling and validation that you could wish for.

 

How does this help your code reviews?

Get people to adopt one of the above points of view (POV) either by choosing what comes naturally to them or by assigning roles. They then review the code from that POV:

Battle faction – does it get the job done

Daybreak faction – is it easy to understand and therefore maintain

Flawless faction – are errors handled

 

You can add other POV as desired and you don’t have to use the faction names. By getting people to review the code from differing POV you will have a better code review and therefore a better product.

Positional parameters

Positional parameters allow you go use a function of cmdlet without specifying the parameter names. The values you supply are assigned to the correct parameters based on their position.

If you look at the documentation for PowerShell you’ll see some confusion as to whether position 0 or position 1 is the first assigned position. Hopefully, these experiments will explain it all.

 

By default parameters are positional. So values are assigned to parameters based on the order of parameter definition.

function pa {
param (

[int] $x,

[int] $y
)

"`$x = $x"
"`$y = $y"

$x * $y

}

PS> pa -x 2 -y 3
$x = 2
$y = 3
6

PS> pa 2 -y 3
$x = 2
$y = 3
6

PS> pa 2 3
$x = 2
$y = 3
6

PS> pa -x 2 3
$x = 2
$y = 3
6

 

When you assign a position to one of more parameters those parameters without a positional assignment become non-positional and need to be explicitly defined

PS> pb -x 2 -y 3
$x = 2
$y = 3
6

PS> pb 2 -y 3
$x = 2
$y = 3
6

PS> pb 2 3
pb : A positional parameter cannot be found that accepts argument '3'.
At line:1 char:1
+ pb 2 3
+ ~~~~~~
+ CategoryInfo : InvalidArgument: (:) [pb], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,pb

PS> pb -x 2 3
pb : A positional parameter cannot be found that accepts argument '3'.
At line:1 char:1
+ pb -x 2 3
+ ~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [pb], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,pb

 

Alternatively

function pc {
param (

[Parameter(Position=1)]
[int] $x,

[int] $y
)

"`$x = $x"
"`$y = $y"

$x * $y

}

PS> pc -x 2 -y 3
$x = 2
$y = 3
6

PS> pc 2 -y 3
$x = 2
$y = 3
6

PS> pc 2 3
pc : A positional parameter cannot be found that accepts argument '3'.
At line:1 char:1
+ pc 2 3
+ ~~~~~~
+ CategoryInfo : InvalidArgument: (:) [pc], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,pc

PS> pc -x 2 3
pc : A positional parameter cannot be found that accepts argument '3'.
At line:1 char:1
+ pc -x 2 3
+ ~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [pc], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,pc

 

So far it looks as position 0 or position 1 can be used as the first assigned position. What happens if we use both:

function pd {
param (

[Parameter(Position=0)]
[int] $x,

[Parameter(Position=1)]
[int] $y
)

"`$x = $x"
"`$y = $y"

$x * $y

}

PS> pd -x 2 -y 3
$x = 2
$y = 3
6

PS> pd 2 -y 3
$x = 2
$y = 3
6

PS> pd 2 3
$x = 2
$y = 3
6

 

So position 0 takes precedence over position 1

What if we have gaps in the position order:

function pe {
param (

[Parameter(Position=0)]
[int] $x,

[Parameter(Position=2)]
[int] $y
)

"`$x = $x"
"`$y = $y"

$x * $y

}

PS> pe -x 2 -y 3
$x = 2
$y = 3
6

PS> pe 2 -y 3
$x = 2
$y = 3
6

PS> pe 2 3
$x = 2
$y = 3
6

 

Numerical position order is used.

In the next case position 1 is assigned to the first parameter

function pf {
param (

[Parameter(Position=1)]
[int] $x,

[Parameter(Position=2)]
[int] $y
)

"`$x = $x"
"`$y = $y"

$x * $y

}

PS> pf -x 2 -y 3
$x = 2
$y = 3
6

PS> pf 2 -y 3
$x = 2
$y = 3
6

PS> pf 2 3
$x = 2
$y = 3
6

 

Note the order of positions

function pg {
param (

[Parameter(Position=2)]
[int] $x,

[Parameter(Position=1)]
[int] $y
)

"`$x = $x"
"`$y = $y"

$x * $y

}

PS> pg -x 2 -y 3
$x = 2
$y = 3
6

PS> pg 2 -y 3
$x = 2
$y = 3
6

PS> pg 2 3
$x = 3
$y = 2
6

 

x is assigned the first positional parameter because y is explicitly defined.

In the next case no parameter is given position 0 or 1

function ph {
param (

[Parameter(Position=4)]
[int] $x,

[Parameter(Position=2)]
[int] $y
)

"`$x = $x"
"`$y = $y"

$x * $y

}

PS> ph -x 2 -y 3
$x = 2
$y = 3
6

PS> ph 2 -y 3
$x = 2
$y = 3
6

PS> ph 2 3
$x = 3
$y = 2
6

PS> ph -x 2 3
$x = 2
$y = 3
6

 

When y is defined then x gets the first available positional value. When both x and y aren’t defined y gets the first positional parameter.

 

From the above.

Positional parameters are assigned a numeric position.

Defined parameters explicitly overrides positional order

Values are assigned in order of position

If a parameter isn’t defined it gets a value based on its position

Position ranking can start at any number – 0 and 1 are the defaults.

Lowest ranked undefined parameter gets the first positional value

PowerShell join

I looked at PowerShell split a few posts back. This time I’ll look at the opposite action – PowerShell join.

Lets look at a simple string

PS> $str = 'PowerShell for Windows and Linux'

and split it

PS> $strs = -split $str
PS> $strs
PowerShell
for
Windows
and
Linux

 

You end up with an array of strings.

Now lets put it back together again

You can just use the operator

PS> -join $strs
PowerShellforWindowsandLinux

 

but there’s no delimiter (default) between the elements as they’re joined. You can define a delimiter

PS> $strs -join ' '
PowerShell for Windows and Linux
PS> $strs -join '_'
PowerShell_for_Windows_and_Linux

 

And that’s it for join.

Beware that you can use variable substitution to effect a join

PS> "$strs"
PowerShell for Windows and Linux

 

Also be aware that if you give the join operator a comma-separated list the join will fail

PS> -join 'PowerShell', 'for', 'Windows', 'and', 'Linux'
PowerShell
for
Windows
and
Linux

 

because join has a higher operator precedence than comma

You need to wrap the list in parentheses

PS> -join ('PowerShell', 'for', 'Windows', 'and', 'Linux')
PowerShellforWindowsandLinux

or use a variable as you saw earlier
PS> $x = 'PowerShell', 'for', 'Windows', 'and', 'Linux'
PS> -join $x
PowerShellforWindowsandLinux

 

Using a delimiter also works

PS> 'PowerShell', 'for', 'Windows', 'and', 'Linux' -join ' '
PowerShell for Windows and Linux
PS> ('PowerShell', 'for', 'Windows', 'and', 'Linux') -join ' '
PowerShell for Windows and Linux

Iron Scripter 2108

Iron Scripter 2018 is the scripting competition we’re holding at the PowerShell Summit on 12 April 2018.

 

The task for the main event is still under wraps but even if you can’t make the Summit you can benefit from the competition.

 

I’ve posted 10 prequel puzzles on powershell.org. Each puzzle revolves around a task you may need to perform. There is a commentary available for each puzzle (the commentary for puzzle 1o will be published on 1 April). If you’ve not looked at the puzzles there are some very interesting parts of PowerShell you need to dig into to solve them. The commentary discusses how I solved them.

 

Starting on Sunday 8 April we’ll be publishing 4 preludes to the iron Scripter event on ironscripter.us. These are shorter puzzles that may (or may not) help you solve the main event. We won’t be publishing any commentary on them but there is the iron Scripter forum on powershell.org

 

Even if you’re not attending Summit get in touch with someone who is and offer remote assistance for the competition itself. Its on 12 April 3-4pm PDT (11-midnight BST).

PowerShell split

The PowerShell split operator is used to split strings based on a delimiter.

 

The default delimiter is white space

PS> $str = 'AA BB CC DD EE FF GG'

PS> -split $str
AA
BB
CC
DD
EE
FF
GG

 

You can define a delimiter

PS> $str = 'AA-B-CC-DD-EE-FF-GG'
PS> $str -split '-'
AA
B
CC
DD
EE
FF
GG

 

Notice that the position of the operator has changed. The first case using the default delimiter is a special case that’s equivalent to:

PS> $str = 'AA BB CC DD EE FF GG'
PS> $str -split ' '
AA
BB
CC
DD
EE
FF
GG

 

You can define the items into which the string is split:

PS> $str = 'AA BB CC DD EE FF GG'
PS> $str -split ' ', 3
AA
BB
CC DD EE FF GG

 

In this case you get the first two splits and the rest as one big string

The delimiter is normally removed from the results but you can keep it

PS> $str = 'AA-B-CC-DD-EE-FF-GG'
PS> $str -split '(-)'
AA
-
B
-
CC
-
DD
-
EE
-
FF
-
GG

 

You have a number of options you can use

"SimpleMatch [,IgnoreCase]"

or

"[RegexMatch] [,IgnoreCase] [,CultureInvariant]
[,IgnorePatternWhitespace] [,ExplicitCapture]
[,Singleline | ,Multiline]"

which are all documented in about_split

Set active hours

Last time time you saw how to get the current active hours. This is how you set the active hours.

$sb = {
Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings -Name ActiveHoursStart -Value 10 -PassThru
Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings -Name ActiveHoursEnd -Value 22 -PassThru
}

Get-ADComputer -Filter * -Properties OperatingSystem |
where OperatingSystem -like "*Server*" |
select -ExpandProperty Name |
foreach {

Invoke-Command -ComputerName $psitem -ScriptBlock $sb -ArgumentList $psitem -HideComputerName |
select -Property * -ExcludeProperty RunSpaceId
}

 

The script block uses Set-ItemProperty to set the start and end of active hours. On Windows server 2016 you’re restricted to a 12 hour span for your active hours. Later Windows 10 builds allow up to 18 hours. I’ve used 10 for the start and 22 for the end to give me the best time spread that matches my activity – you can choose your own hours of course.

 

Getting the list of systems from AD and running the command remotely is as previously.

Get Active Hours

Windows 10 and Server 2016 (and later) have the concept of active hours. These are the hours you define as working hours in effect. This is how you get active hours for a system

$sb = {
param([string]$computerName)

$ahs = Get-Item -Path HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings

$props = [ordered]@{
ComputerName = $computerName
ActiveHoursStart = $ahs.GetValue('ActiveHoursStart')
ActiveHoursEnd = $ahs.GetValue('ActiveHoursEnd')
}

New-Object -TypeName PSobject -Property $props

}

Get-ADComputer -Filter * -Properties OperatingSystem |
where OperatingSystem -like "*Server*" |
select -ExpandProperty Name |
foreach {

Invoke-Command -ComputerName $psitem -ScriptBlock $sb -ArgumentList $psitem -HideComputerName |
select -Property * -ExcludeProperty RunSpaceId
}

 

The script block reads the registry key that contains the active hours information and outputs and object that contains the computer name, and start and end (in 24 hour clock) of the active hours.

 

I’m getting the information for all servers in the domain – use the OperatingSystem property on the computer to deselect non-servers. use Invoke-Command to run the command against the remote computer – hide the automatic computer name and runspaceid properties.

Cmdlet parameters

I discovered another way to investigate cmdlet parameters.

If you dig into the output of Get-Command you’ll see it has a parameters property

PS> Get-Command Clear-RecycleBin | select parameters

Parameters
----------
{[DriveLetter, System.Management.Automation.ParameterMetadata], [Force, System.Management.Automation.ParameterMetada...

If you expand the parameters property:

PS> Get-Command Clear-RecycleBin | select -ExpandProperty  parameters

Key                 Value
---                 -----
DriveLetter         System.Management.Automation.ParameterMetadata
Force               System.Management.Automation.ParameterMetadata
Verbose             System.Management.Automation.ParameterMetadata
Debug               System.Management.Automation.ParameterMetadata
ErrorAction         System.Management.Automation.ParameterMetadata
WarningAction       System.Management.Automation.ParameterMetadata
InformationAction   System.Management.Automation.ParameterMetadata
ErrorVariable       System.Management.Automation.ParameterMetadata
WarningVariable     System.Management.Automation.ParameterMetadata
InformationVariable System.Management.Automation.ParameterMetadata
OutVariable         System.Management.Automation.ParameterMetadata
OutBuffer           System.Management.Automation.ParameterMetadata
PipelineVariable    System.Management.Automation.ParameterMetadata
WhatIf              System.Management.Automation.ParameterMetadata
Confirm             System.Management.Automation.ParameterMetadata

The really nice thing is that you get the common parameters listed as well.

If you want to dig into the individual parameters

PS> $params = Get-Command Clear-RecycleBin | select -ExpandProperty  parameters

PS> $params.DriveLetter

Name            : DriveLetter
ParameterType   : System.String[]
ParameterSets   : {[__AllParameterSets, System.Management.Automation.ParameterSetMetadata]}
IsDynamic       : False
Aliases         : {}
Attributes      : {__AllParameterSets, System.Management.Automation.ValidateNotNullOrEmptyAttribute}
SwitchParameter : False

or

PS> $params.Verbose

Name            : Verbose
ParameterType   : System.Management.Automation.SwitchParameter
ParameterSets   : {[__AllParameterSets, System.Management.Automation.ParameterSetMetadata]}
IsDynamic       : False
Aliases         : {vb}
Attributes      : {System.Management.Automation.AliasAttribute, __AllParameterSets}
SwitchParameter : True

You can see the parameter sets and aliases which is useful. The parameter type indicates the input data.

This is very useful where you haven’t installed the help files on a system

Finding special folders

Windows has a number of special folders. These can be accessed either directly through the file system – for example the Documents special folder is C:\<user>\Richard\Documents or through code. But how do you go about finding special folders.

The easiest way is to use this script

1..1000 |
ForEach-Object {
$shell = New-Object -ComObject Shell.Application
$sf = $shell.NameSpace($psitem)

if ($sf) {

$props = [ordered]@{
Value = $psitem
Name = $sf.Title
Path = $sf.Self.Path
}

New-Object -TypeName PSobject -Property $props
}
}

Looping through the values 1-1000 the Shell.Application COM object is used to find the namespace corresponding to that number. If it exists, the value, name and path are output.

In the Path property you’ll either get a normal looking path e.g. C:\Windows\System32 or something like this - ::{645FF040-5081-101B-9F08-00AA002F954E} - which is the CLSID or Windows Class Identifier

PowerShell while

PowerShell has a number of looping structures – do; while; for; foreach. This is how the PowerShell while loop works

The while statement has the form:

while (<condition>){<statement list>}

 

The while loop is probably the simplest of the PowerShell loops. For example:

$x = 0
while ($x -lt 5){
Write-Host "`$x is $x"
$x++
}

 

gives:

$x is 0
$x is 1
$x is 2
$x is 3
$x is 4

 

As long as the condition is true the statement list is executed

The condition is evaluated BEFORE the loop is executed meaning that it may never run

PS> $x = 10
while ($x -lt 5){
Write-Host "`$x is $x"
$x++
}

PS>

The value of $x is greater than 4 so the loop never executes.