Powershell

One-liners vs. reusable scripts and proper indentation

I mentioned before, Powershell is a great tool for IT/SysAdmins, for Cloud Engineers (Service Engineers, SRE etc…) and in my opinion even developers, if used to the full extent of its capabilities. Everyone can find their own dimension using Powershell: you can fire up the shell and type away commands and one-liners, or write quick scripts ready to go next time around, or you can transform those scripts into Advanced Functions and combine them into Modules for reuse and distribution among your colleagues and maybe share online.

All these different uses allow (I think almost call for) different writing styles: if I’m at the interactive console (other languages would call it REPL) I use all sort of shortcuts and aliases to save time and typing. For example let’s take one of the commands I used in Length, Count and arrays:

(Get-ChildItem -Directory | Where-Object Name -match 'automation' | Select-Object -ExpandProperty 'Name').Count

At the console I would instead use:

(dir -di | ? name -m automation | select -exp name).count

Here’s the breakdown:

  • “dir” is an alias for Get-ChildItem
  • “-di” is a contraction for “-Directory” (I want to list only folders, not files)
  • “?” is again an alias for Where-Object
  • “-m” is a contraction for -Match
  • “select” is an alias for Select-Object
  • “-exp” is a contraction for -ExpandProperty

You can get a list of available aliases running Get-Alias and you can add your own (take a look at about_aliases for details). Common parameters also have shortcuts (see about_CommonParameters).

Parameter names and Operators can be contracted as long as they are not ambiguous, in which case Powershell would not be able to decide which possible parameter combination to use. For example:

PS >_ Get-ChildItem *aut* | Select-Object -ex name
Select-Object: Parameter cannot be processed because the parameter name 'ex' is ambiguous. Possible matches include: -ExcludeProperty -ExpandProperty.
At line:1 char:20
Get-ChildItem *aut* | Select-Object -ex name
~~~ CategoryInfo : InvalidArgument: (:) [Select-Object], ParameterBindingException
FullyQualifiedErrorId : AmbiguousParameter,Microsoft.PowerShell.Commands.SelectObjectCommand

The error is pretty clear: “-ex” marches two Get-ChildItem parameters (ExpandProperty and ExcludeProperty) and Powershell does not know which one to use.

PS >_ Get-ChildItem *aut* | Select-Object -exp name
Microsoft.Authorization
Microsoft.Automation
Microsoft.Authorization.txt
Microsoft.Automation.txt

In this second example I just added an additional “p” (-exp vs. -ex), that is enough to tell Powershell I want to use -ExpandProperty and I get back the list of files and folders as expected.

This is great for interactive use and it definitely helps in certain situations but it’s definitely not the best approach for reusable scripts and modules, not because of technical limitations but purely for readability and maintainability. I have written a few modules and a good number of scripts I shared with my colleagues, so it is almost guaranteed that at some point some else will have to read/update/maintain that code: I cannot assume that person will be familiar with all those Powershell contractions and aliases and even if they were, it may still require some extra effort for them to fully understand what the script/module is doing. For shared code it is good practice to stay away from contractions and aliases and use full names and proper, clear form even if it takes a bit more typing, an inconvenience easily alleviated using advanced editors such as Visual Studio Code and the Powershell Extension: TAB completion and Shift+Alt+E on Windows (or Shift+Option+E on macOS) make a breeze to expand all aliases.

Speaking of readability, code indentation is a hot discussion topic (just search on StackOverflow/Reddit); the Powershell extension for VSCode allows for different indentation styles and apparently there is no real winner (see There is no One True Brace Style). Again, it is not a technical problem (Powershell does not care about indentation and you can even run multiple commands on one line as long as they are separated by semi-colons “;”), but trying to read and update a 100 lines script (let alone 1000+ lines) with all code aligned on the left is a quick way to get a screaming headache and a nice shortcut to bugs and syntax errors. The human brain is very good at spotting patterns, proper indentation really helps in figuring out at a glance where an “if” statements begins and ends, what goes into a “foreach” loop and what is in a “try…catch” block (or where you need one).

For example, here’s an excerpt from an old test script:

foreach ($sa in $ssStorageAccounts) {
    if ($sa -eq $ssStorageAccounts[-1]) { continue }

    $ssStorageAccountDecrypted = & $StashClient -env:prod sagetdecrypted -name:$sa | ConvertFrom-Csv -Delimiter "`t" -Header @('Name', 'Value')
    $kvSecretName = $sa.Replace('/', '-').Replace('.', '-').Replace('_', '-')
    $existingKvSecret = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name $kvSecretName -ErrorAction 'SilentlyContinue'
    $ssKey1 = $ssStorageAccountDecrypted.Where( { $_.Name -match 'key1' }).Value

    if ($existingKvSecret) {
        # the secret already exists, check if it matches Secret Store
        if ($ssKey1 -ceq $existingKvSecret.SecretValueText) {
            Write-Verbose "Secret Store $sa is equal to KeyVault secret $($existingKvSecret.Name), skipping"
        }
        else {
            # the KeyVault secret exists but the StorageAccount does not match Secret Store
            if ($force -or $PSCmdlet.ShouldProcess($sa, 'Overwrite existing Secret')) {
                if ($force -or $PSCmdlet.ShouldContinue($kvSecretName, 'Overwrite existing Secret?')) {
                    Write-Verbose "Updating $kvSecretName"
                    Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name $kvSecretName `
                        -SecretValue (ConvertTo-SecureString -AsPlainText -Force -String $ssKey1)
                    -Tag @{'Account' = "$sa" }
                }
            }
        }
    }
    else {
        # creat the secret in KeyVault
        if ($force -or $PSCmdlet.ShouldProcess($sa, 'Create Secret')) {
            Write-Verbose "Adding $kvSecretName"
            Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name $kvSecretName `
                -SecretValue (ConvertTo-SecureString -AsPlainText -Force -String $ssKey1)
            -Tag @{'Account' = "$sa" }
        }
    }
}

The same script without indentation:

foreach ($sa in $ssStorageAccounts) 
{
if ($sa -eq $ssStorageAccounts[-1]) { continue }

$ssStorageAccountDecrypted = & $StashClient -env:prod sagetdecrypted -name:$sa | ConvertFrom-Csv -Delimiter "`t" -Header @('Name', 'Value')
$kvSecretName = $sa.Replace('/', '-').Replace('.', '-').Replace('_', '-')
$existingKvSecret = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name $kvSecretName -ErrorAction 'SilentlyContinue'
$ssKey1 = $ssStorageAccountDecrypted.Where( { $_.Name -match 'key1' }).Value

if ($existingKvSecret) 
{
# the secret already exists, check if it matches Secret Store
if ($ssKey1 -ceq $existingKvSecret.SecretValueText) 
{
Write-Verbose "Secret Store $sa is equal to KeyVault secret $($existingKvSecret.Name), skipping"
}
else 
{
# the KeyVault secret exists but the StorageAccount does not match Secret Store
if ($force -or $PSCmdlet.ShouldProcess($sa, 'Overwrite existing Secret')) 
{
if ($force -or $PSCmdlet.ShouldContinue($kvSecretName, 'Overwrite existing Secret?')) 
{
Write-Verbose "Updating $kvSecretName"
Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name $kvSecretName `
-SecretValue (ConvertTo-SecureString -AsPlainText -Force -String $ssKey1)
-Tag @{'Account' = "$sa" }
}
}
}
}
else 
{
# creat the secret in KeyVault
if ($force -or $PSCmdlet.ShouldProcess($sa, 'Create Secret')) 
{
Write-Verbose "Adding $kvSecretName"
Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name $kvSecretName `
-SecretValue (ConvertTo-SecureString -AsPlainText -Force -String $ssKey1)
-Tag @{'Account' = "$sa" }
}
}
}

This is a simple block of code and it is still possible to infer some pattern (all those consecutive braces are a hint), still following the flow of the script is not as easy as with the first example. These are 43 lines, imagine how the script would look like when you have ten or twenty times that amount of code…

By the way, my preference goes to K&R (and https://github.com/PoshCode/PowerShellPracticeAndStyle/issues/81#issuecomment-465131592), very easy to enable in Visual Studio Code; again, proper indentation is just a keyboard shortcut away (Shirt+Alt+F on Windows and Shift+Option+F on macOS).


Doubt is not a pleasant condition, but certainty is absurd – Voltaire

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.