2016-11-02

Flat Is Justice! Flatter Code for PowerShell



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:
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:
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:

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 code

A 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.