loading...
Cover image for Foundational PowerShell For Developers — Part 2

Foundational PowerShell For Developers — Part 2

sharpninja profile image The Sharp Ninja ・6 min read

In the previous entry in this series we looked at variables, piping objects between commands, and the Get-Item and Get-ChildItem commands. This time we will look at types, control flow, and finally we’ll put it all together to perform an essential dev task: performing a brute-force clean operation on a source tree.

Types

PowerShell has two “type systems.” Fundamentally, everything is a .NET object, and all .NET types are valid types in PowerShell. You can instantiate any .NET type with a public constructor using the New-Object command and assign them to strongly or dynamically typed variables. You can also use factory methods to create objects.

PS C:\Users> [string]$line = New-Object System.String -ArgumentList '-', 5
PS C:\Users> $line
-----
PS C:\Users> [Guid]$guid = [System.Guid]::NewGuid()
PS C:\Users> $guid

Guid
----
3f7ff402-edaf-48ab-9d03-ac046edcf7db

PowerShell Collections

PowerShell has built in Arrays and Hashtables. They are used as the normal input and output of collections by PowerShell commands. Manipulating Arrays is very simple.

PS C:\Users> $list = @()
PS C:\Users> $list += "first Value"
PS C:\Users> $list += 23
PS C:\Users> $list
first Value
23
PS C:\Users>

As you can see, you can put any type of data into a list and modify it.

PS C:\Users> $list[1]+=10
PS C:\Users> $list
first Value
33
PS C:\Users>

Hashtables are just as easy to work with.

PS C:\Users> $table=@{}
PS C:\Users> $table+=@{"Item"=1}
PS C:\Users> $table

Name                           Value
----                           -----
Item                           1

PS C:\Users> $table+=@{"Another"=2}
PS C:\Users> $table

Name                           Value
----                           -----
Another                        2
Item                           1

PS C:\Users>

The full range of features of Arrays and Hashtables in PowerShell is well documented by Michel Sorens of Red Gate.

Control Flow Code

The logic code of PowerShell looks like any other Von Neuman based language. Conditionals and Loops and pretty standard, but logic operators do not use symbols, rather are more expressive, using textual mnemonics instead. Some common Boolean operators are:

PS C:\Users> $a=$true; $b=$false
PS C:\Users> -not $a
False
PS C:\Users> $a -eq $b  #Equals
False
PS C:\Users> $a -ne $b  #Not Equals
True
PS C:\Users> $c=0; $d=1
PS C:\Users> $c -lt $d  #Less Than
True
PS C:\Users> $c -le $d  #Less Than Or Equals
True
PS C:\Users> $c -gt $d  #Greater Than
False
PS C:\Users> $c -ge $d  #Greater Than or Equals
False
PS C:\Users>
Boolean values can be chained.
PS C:\Users> ($a -ne $b) -and ($c -ge $d) # Logical AND
False
PS C:\Users> ($a -ne $b) -or ($c -ge $d) # Logical OR
True
PS C:\Users> -not (($a -ne $b) -or ($c -ge $d)) # Logical NOR
False
PS C:\Users>

The if statement can execute branches of code based on Boolean conditions.

PS C:\Users> if(($a -ne $b) -or ($c -ge $d)) { "It was true" } else { "It was false" }
It was true
PS C:\Users> if(($a -ne $b) -and ($c -ge $d)) { "It was true" } else { "It was false" }
It was false
PS C:\Users>

Looping Statements

The most common loops are foreach loops which iterate a collection.

PS C:\Users> foreach($item in $list) { $item }
first Value
33
PS C:\Users> foreach($item in $table) { $item }

Name                           Value
----                           -----
Another                        2
Item                           1

PS C:\Users> foreach($item in $table) { $item.Keys }
Another
Item
PS C:\Users> foreach($item in $table) { $item.Values }
2
1
PS C:\Users>

Another type of loop is the ForEach-Object command, which iterates over piped input.

PS C:\Users> $list | ForEach-Object -Process { $_ }
first Value
33
PS C:\Users>

Here we are introduced into a very important built-in variable in PowerShell. $_ contains the current context returned from certain commands and statements, such as catch. Since nesting can create new values for $_, it is wise to capture it in a variable.

PS C:\Users> $table | ForEach-Object -Process { $f=$_; $_.Keys | ForEach-Object -Process { Write-Host ($_+": "+$f[$_]) } }
Another: 2
Item: 1
PS C:\Users>

Now we are ready to put it all together and force clean a source directory tree. I’ve downloaded and built the ProjectIrony from GitHub and want to see a list of all of the MSBuild obj and bin folders created.

PS C:\github\Irony> get-childitem obj,bin -Recurse | ForEach-Object -Process { $_.FullName }
C:\github\Irony\Irony\obj
C:\github\Irony\Irony.GrammarExplorer\obj
C:\github\Irony\Irony.Interpreter\obj
C:\github\Irony\Irony.Samples\obj
C:\github\Irony\Irony.Samples.Console\obj
C:\github\Irony\Irony.Tests\obj
C:\github\Irony\Irony.WinForms\obj
C:\github\Irony\Languages\Refal\obj
C:\github\Irony\Languages\Refal.UnitTests\obj
C:\github\Irony\Irony\bin
C:\github\Irony\Irony.GrammarExplorer\bin
C:\github\Irony\Irony.Interpreter\bin
C:\github\Irony\Irony.Samples\bin
C:\github\Irony\Irony.Samples.Console\bin
C:\github\Irony\Irony.Tests\bin
C:\github\Irony\Irony.WinForms\bin
C:\github\Irony\Languages\Refal\bin
C:\github\Irony\Languages\Refal.UnitTests\bin
PS C:\github\Irony>

I could have left off the ForEach-Object, but that would have listed a separate table for each parent folder containing a bin or obj folder. I can make a slight modification and delete the folders if I’m happy with the list.

PS C:\github\Irony> get-childitem obj,bin -Recurse | Remove-Item -Force -Recurse

Now let’s see if it worked.

PS C:\github\Irony> get-childitem obj,bin -Recurse | ForEach-Object -Process { $_.FullName }
PS C:\github\Irony>

Let’s say we wanted to preserve the output in Samples folders. We would use the -Exclude parameter to pattern match the child items of the C:\GitHub\Irony folder. The resulting list would then be iterated. The list will have both files and directories, so we need to ensure we only perform the nested operations on directories by checking if they are an instance of System.IO.DirectoryInfo. Finally, we recursively check these directories for obj and bin folders and deleted them.

PS C:\github\Irony> get-childitem -Exclude *Samples* | foreach-object -process { if($_ -is [System.IO.DirectoryInfo]) { get-childitem -Include obj,bin -Path
 $_ -Recurse | Remove-Item -Recurse -Force }}
PS C:\github\Irony> get-childitem obj,bin -Recurse | ForEach-Object -Process { $_.FullName }
C:\github\Irony\Irony.Samples\obj
C:\github\Irony\Irony.Samples.Console\obj
C:\github\Irony\Irony.Samples\bin
C:\github\Irony\Irony.Samples.Console\bin
PS C:\github\Irony>

What If?

When creating potentially destructive PowerShell, you might want to “try before you buy”. Fortunately, there is a handy -WhatIf parameter that when added to a command like Remove-Item will tell you exactly what would happen without actually doing it.

PS C:\github\Irony> Remove-Item * -Recurse -Force -WhatIf
What if: Performing the operation "Remove Directory" on target "C:\github\Irony\.git".
What if: Performing the operation "Remove Directory" on target "C:\github\Irony\Irony".
What if: Performing the operation "Remove Directory" on target "C:\github\Irony\Irony.GrammarExplorer".
What if: Performing the operation "Remove Directory" on target "C:\github\Irony\Irony.Interpreter".
What if: Performing the operation "Remove Directory" on target "C:\github\Irony\Irony.Samples".
What if: Performing the operation "Remove Directory" on target "C:\github\Irony\Irony.Samples.Console".
What if: Performing the operation "Remove Directory" on target "C:\github\Irony\Irony.Tests".
What if: Performing the operation "Remove Directory" on target "C:\github\Irony\Irony.WinForms".
What if: Performing the operation "Remove Directory" on target "C:\github\Irony\Languages".
What if: Performing the operation "Remove Directory" on target "C:\github\Irony\Nuget".
What if: Performing the operation "Remove Directory" on target "C:\github\Irony\packages".
What if: Performing the operation "Remove File" on target "C:\github\Irony\.editorconfig".
What if: Performing the operation "Remove File" on target "C:\github\Irony\.gitignore".
What if: Performing the operation "Remove File" on target "C:\github\Irony\appveyor.yml".
What if: Performing the operation "Remove File" on target "C:\github\Irony\Irony.sln".
What if: Performing the operation "Remove File" on target "C:\github\Irony\LICENSE".
What if: Performing the operation "Remove File" on target "C:\github\Irony\ReadMe.md".
PS C:\github\Irony>

Conclusion

Using a combination of command and language/shell features allows us to create very powerful PowerShell code. Here we created a command to force clean a source tree and selectively clean it as well. Next time we’ll put together a script with parameters and assign it to an alias.

Discussion

markdown guide