Intro
Before learning PowerShell, I was deep into learning Python.
At the time, I was managing slightly more Linux environments than I was Windows
environments. I was a big fan of PEP 8 and PEP 20 and I tried to take all those
lessons to heart while programming in Python. Just as I was gaining speed in
Python, my job switched to almost purely Microsoft products and I was plunged into the
world of PowerShell.
I love PowerShell! But there is much more diversity in the
standards people use in it than in the Python world. That’s not necessarily a
bad thing. After all, PowerShell is just as useful for an interactive shell
environment as it is for a full-bodied scripting language. That is bound to espouse some differences of
style.
PowerShell does have some Best Practices and a Style Guide,
but I feel it is not quite where Python is yet. There is one thing that I feel
Python got right that I’d like to see more of I the PowerShell world: Flat is
better than nested.
What is Flat Code?
Flat code is the opposite of Nested code. Nested code is
code blocks within code blocks within code blocks. Flat Code would have fewer
code blocks within code blocks.
In reality, all PowerShell code is deeply nested considering you have functions using other functions using cmdlets using .NET using functions using classes, etc. What Flat vs. Nested refers to in the context of this post is the code on the screen and the levels of indentation.
I hope everyone is using some form of proper indentation (Team 4-Space 4-Life!). That is, unless you are using the interactive shell, your code should properly indent a consistent amount within code blocks (ScriptBlock, Foreach, Foreach-Object, While, For, Function, Begin/Process/End, Switch, Try/Catch/Finally, etc.). Each level should have a new indentation.
Nested Code:
foreach ($Folder in $Folders) { if (Test-Path -Path $Folder) { foreach ($User in $Users) { if (Get-ADUser -Identity $User -ErrorAction SilentlyContinue) { if (-not (Test-Path "$Folder\$User")) { New-Item -Path $Folder -Name $User -ItemType Directory } foreach ($File in $Files) { if (Test-Path $File) { Copy-Item -Path $File -Destination "$Folder\$User" } Else { Write-Error "File '$File'' not found!" } } } Else { Write-Error "User '$User' not found!" } } } else { Write-Error "Folder '$Folder' not found!" } }
So, in the context of this post, Flat Code is code that has fewer levels of indentation than Nested Code.
Flat Code:
$FoundFolders = foreach ($Folder in $Folders) { if (-not (Test-Path -Path $Folder)) { Write-Error "Folder '$Folder' not found!" Continue } $Folder } $FoundUsers = foreach ($User in $Users) { if (-not (Get-ADUser -Identity $User -ErrorAction SilentlyContinue)) { Write-Error "User '$User' not found!" Continue } $User } $FoundFiles = foreach ($File in $Files) { if (Test-Path $File) { Write-Error "User '$User' not found!" Continue } $File } foreach ($Folder in $FoundFolders) { foreach ($User in $FoundUsers) { foreach ($File in $FoundFiles) { $Params = @{ Path = $Folder Name = $User ItemType = 'Directory' ErrorAction = 'SilentlyContinue' } New-Item @Params Copy-Item -Path $File -Destination "$Folder\$User" } } }
If you look at the two examples, you will see that the Nested Code has some considerable white space on the left hand side, especially by the time it gets to the Copy-Item code. There is still an unavoidable nesting of Foreach blocks in the flat code. The difference is that the testing logic has been separated and flattened allowing for the foreach nesting to become much more readable. This could be further flattened with some functions created instead, but I wanted to show a quick and dirty example.
What Tools Can Be Used to Flatten Code?
Many of the tools available in PowerShell to flatten code
are the same in other languages. In fact, I know of these tools from my time
with Python and PHP. I believe it is
just a matter of translation and examples.
Short Circuiting
What is Short Circuiting?
Short Circuiting is the act of terminating or skipping an iteration
of a code block early. In PowerShell, this is the use of Continue, Break, and
Return within loops and code blocks.
“But Mark, everyone says to never use Return!”
Well, I’m here to say those people are wrong. More accurately,
they are mostly-right. You should definitely avoid using Return to send items to
the pipeline in a Function or [ScriptBlock]. There is an exception to that rule
and that is Short Circuiting. Return is used to Short Circuit the following
code blocks and allow code after them to continue: Begin, Process, End, Foreach-Object, [ScriptBlock], and Simple
Functions (which you shouldn’t be using). You can use Break if you want the actions following the code to stop as well or you want the pipeline to die. But Return is the only way for the code to be flattened and gracefully continue. The Continue statement acts like Break for these blocks too. It took me a fair bit of playing around with Break, Continue, and Return to figure out the right combinations in the right contexts. That is something for a post all itself.
On advanced Functions using Begin, Process, and End blocks, using Return inside those blocks will not terminate the function. Return will only terminate the current block. A Return statement in the Begin block will not stop the Process block from executing. If you run into a show stopping issue in the Begin block (such as being unable to initialize a resource needed for the Process block) but still want the pipeline to continue, you will need to make use of an abort Boolean and test for its presence at the start of the other blocks and short circuit those blocks.
Example Short Circuiting Advanced Functions:
Function Get-JiggyWidIt { [cmdletbinding()] Param( [switch]$No ) Begin { Write-Verbose "Enter Begin" If($No){ $Abort = $true Return } Write-Host 'Gettin Jiggy Wid It!' } Process { Write-Verbose "Enter Process" if($Abort){ Return } Write-Host 'Na na na nana na na.' Write-Host 'Na na na nana na na.' } End { Write-Verbose "Enter End" if($Abort){ Return } Write-Host 'Gettin Jiggy Wid It!' } } Get-JiggyWidIt -No -Verbose
When to Short Circuit
Short Circuiting is done when a condition is met that
renders the rest of the code block useless. An example is a Foreach loop where
if the current item isn’t valid for the actions that will be done to the item
in the loop such as a computer being offline. The Nested Code version is to put
the entire block into an If statement thus increasing the depth of the Foreach block’s
body.
Traditional Nested Code:
Traditional Nested Code:
Foreach ($Computer in $Computers) { If (Test-Connection -ComputerName $Computer -Count 1 -Quiet){ #line 1 #line 2 #line 3 #... } }
The Flat Code way would be to Short Circuit the Foreach block
with a Continue statement and forcing the block to the next item.
Flat Code Short Circuit:
Flat Code Short Circuit:
Foreach ($Computer in $Computers) { If (-Not (Test-Connection -ComputerName $Computer -Count 1 -Quiet)){ Continue } #line 1 #line 2 #line 3 #... }
Inverse Conditionals
What is an Inverse Conditional?
An Inverse Conditional is a conditional statement where the opposite
expected result returns true. OK, that is a terrible explanation. How about:
In other words: in an If statement, instead of testing whether a computer is reachable via Test-Connection, test whether it is NOT reachable instead.
An inverse Conditional is one that begins with -Not.
In other words: in an If statement, instead of testing whether a computer is reachable via Test-Connection, test whether it is NOT reachable instead.
Standard Conditional:
If (Test-Connection -ComputerName $Computer -Count 1 -Quiet){ #Do something }
Inverse Conditional:
If (-Not (Test-Connection -ComputerName $Computer -Count 1 -Quiet)) { #Short Circuit }
#Do Something
When to Use Inverse Conditionals
You may notice that in the Inverse Conditional example,
instead of doing something when the condition is true, we are short circuiting.
You can also see this in the Flat Code example for Short Circuiting. Inverse
Conditionals work in conjunction with Short Circuiting by exiting a code block
or going to the next iteration when and evaluation comes back false. For example:
- A user does not exist
- A group does not exist
- A path does not exist
- A Computer is offline
These are common scenarios where the built-in tests return
false and often the remainder of your code in the block does not apply and you
would want to skip to the next item. Combining the Inverse Conditional with a Short Circuit
allows you to bypass the rest of the code in the code block without having to
wrap it all in an If statement. The only indentation increase is the error reporting (if needed) and the Short Circuit.
Else is a statement that often is a flag that an Inverse
Conditional and Short Circuit could be used. With an If/Else you are creating 2
blocks of code that are indented. By turning the Else block into an Inverse
Conditional of what is in your If statement and adding a Short Circuit, the code
that was in the If block can now be in the same parent block of code, saving
yourself a level of indentation. This is especially true for what I call a "Dangling Else". These are Else blocks that only contain one or 2 lines of code where the If block contain many lines of code.
Example Nested Else:
Foreach ($Widget in $Widgets){ If($Widget.IsForSale){ Submit-WidgetForSale $Widget } Else{ Write-Error "$($Widget.Name) is not for sale!" } }
Flattened:
Foreach ($Widget in $Widgets){ If(-Not $Widget.IsForSale){ Write-Error "$($Widget.Name) is not for sale!" Continue } Submit-WidgetForSale $Widget }
Collapsing Conditionals
What is Collapsing Conditionals?
Collapsing Conditionals is the process of unnesting If
statements. When you have an If within an If, often you can collapse these into
a single If statement and combine the conditions with an -And or -Or.
Collapsing Conditionals can also mean introducing Inverse
Conditionals and Short Circuiting. Nested conditional’s with Else’s should be
a red flag that something should be flattened and a short Circuit may be
needed.
Example Nested Conditionals:
Foreach ($Widget in $Widgets) { If($Widget.Length -lt 10){ If($Widget.IsForSale) { If($Widget.IsInStock){ Submit-WidgetForSale $Widget } Else { Write-Error "$($Widget.Name) is out of stock!" } } Else { Write-Error "$($Widget.Name) is not for sale!" } } }
Exampled Flattened Conditionals:
Foreach ($Widget in $Widgets) { If( -Not $Widget.IsForSale ) { Write-Error "$($Widget.Name) is not for sale!" Continue } If ( -Not $Widget.IsInStock ){ Write-Error "$($Widget.Name) is out of stock!" Continue } If($Widget.Length -lt 10){ Submit-WidgetForSale $Widget } }
Functions
What are Functions?
If you have not been introduced to Functions in PowerShell, I suggest you read about_Functions and about_Functions_Advanced for starters and then practice your Google-fu and read up some tutorials. Functions are the core of PowerShell methodology. One thing that PowerShell and Python have in common: the community highly recommends functions that do one thing only and using as many functions as possible to simplify codeA Function flattens code by moving nested code out of the current visible scope. You move the nested code from your current code into a function and then replace that nested code with the function name, thus reducing the indentation level in the current code.
Nested Code:
foreach ($Computer in $Computers) { Rename-Computer @Params Set-Item @RegistryHack01 Set-Item @RegistryHack02 foreach ($Folder in $Folders) { New-Item -ItemType Directory -Path "$Folder\$env:username" New-Item -ItemType Directory -Path "$Folder\$env:username\folder1" New-Item -ItemType Directory -Path "$Folder\$env:username\folder2" New-Item -ItemType Directory -Path "$Folder\$env:username\folder3" } }
Flattened with a Function:
foreach ($Computer in $Computers) { Rename-Computer @Params Set-Item @RegistryHack01 Set-Item @RegistryHack02 # Old Foreach is now just a function call Add-UserFolders -Folder $Folders }
When to use Functions
As often as possible. There are many reasons to move code to a Function that are outside the scope of Flat Code which are actually more compelling reasons to do so. The most prevalent of those is maintainability, reusability, and readability. If you create everything as functions that do one thing and return one type of thing, those functions can be separated into separate files and added to a module that can then be copied off an imported anywhere.More specific to Flat Code, any kind of looping and most especially Foreach loops are prime candidates for being separated off into a Function. This was demonstrated in the previous example. While loops can often be made into Wait- functions.
Nested:
$count = 0 while (-not (Test-Connection -ComputerName $Computer -Count 1 -Quiet) -and $count -lt 600) { $count++ Start-Sleep -Second 1 }
Flat:
$Computer | Wait-ComputerOnline -MaxSeconds 600
Complex validation or test logic is also a prime candidate for being made into a Function. Instead of having a bunch of If/Elseif/Else/Switch statements to verify the state of an object or test a resource's availability in your code, these can be moved out to Validate- or Test- functions. This is essentially combining Collapsing Conditionals with Functions.The nested conditional code example flat code could just as easily be the following with some tweaks to the logic:
Foreach ($Widget in $Widgets) { Validate-Widget $Widget Submit-WidgetForSale $Widget }
Complex multi-step filtering logic performed on large data sets can often be moved to Select- functions.Any time you are going beyond a simple Where-Object it might be better to move the code off to a Function. Essentially, if you have objects you are performing multiple checks against to see if they should be sent to the output stream or acted on, a Select- function will help flatten and simplify the code.
Nested:
Foreach ($Widget in $Widgets) { if ($Widget.IsForsale) { $Purhcased = Search-WidgetOrders $Widget.ID if (-not $Purhcased) { Submit-WidgetForSale $Widget } } }
Flattened with Select- Function:
$Widgets | Select-WdigetAvailable | Submit-WidgetForSale
Conclusion
The examples in this post don't really do Flat Code justice, in my opinion. It makes more sense when you can look at some truly deep nested code and see it flattened. In a future post I will cover some of the common nesting I see in PowerShell that can be flattened.In any case, if this post has done its job it at least introduced you to Flat Code if you were not already familiar with it and provided you with some available tools within PowerShell to assist in flattening code.