Design and Coding Considerations for PowerShell

Here a few coding considerations that might make you life easer when dealing with PowerShell (but may also hold true for other programming languages):

  1. Hide unwanted return information in your code
    In PowerShell your functions and CmdLets return everything that is output within the function – even if you use an explicit return statement. To avoid unwanted returns from there make sure every call you make to other CmdLets or .NET runtime is “saved” into a variable:

    [void] $aCounter[4].Increment();
    $null = Set-SomeFunction;
    
  2. If you want to scale measure your code before going into production
    Use performance timing of your code fragments when you want to your functions to run in a large scale. The following example gives you the time for each iteration in ms (use large $n to achieve more accurate results and repeat the test at least 10 times).

    PS > $n = 100000; $dat1=Get-Date; 1..$n | % {
     $null = Get-Date;
    };
    $dat2 = Get-Date; ($dat2-$dat1).TotalMilliseconds/$n;
    0.081638144
    
    1. Consider using .NET runtime directly when PowerShell functionality is much slower (compare times from previous example):
      $n = 100000; $dat1=Get-Date; 1..$n | % {
      $null = [datetime]::Now;
      }; $dat2 = Get-Date; ($dat2-$dat1).TotalMilliseconds/$n
      0.022982041
      
  3. Do not use the pipe when not really neccessary, especially not with Out-Null
    As strange as it might sound… it takes quite some time.

    PS> $n = 100000; $dat1=Get-Date; 1..$n | % {
    Get-Date | Out-Null;
    };  $dat2 = Get-Date; ($dat2-$dat1).TotalMilliseconds/$n;
    0.117125208
    
  4. Use try/catch and really check for the exception and produces a meaningful error message
    catch {
    if($gotoSuccess -eq $_.Exception.Message) {
    $fReturn = $true;
    } else {
    [string] $ErrorText = "catch [$($_.FullyQualifiedErrorId)]";
    $ErrorText += (($_ | fl * -Force) | Out-String);
    $ErrorText += (($_.Exception | fl * -Force) | Out-String);
    $ErrorText += (Get-PSCallStack | Out-String);
    
    if( ($_.Exception.InnerException) -and ([System.Net.WebException] -eq ($_.Exception.InnerException.GetType())) ) {
    Log-Critical $fn "Operation with UriServer '$UriServer' FAILED [$_].";
    Log-Debug $fn $ErrorText -fac 3;
    } # [System.Net.WebException]
    else {
    Log-Error $fn $ErrorText -fac 3;
    if($gotoFailure -ne $_.Exception.Message) { Write-Verbose ("$fn`n$ErrorText"); }
    } # other exceptions
    $fReturn = $false;
    $OutputParameter = $null;
    } # !$gotoSuccess
    } # catch
    
  5. Use finally too, to clean up resources!
  6. Be careful with pipe (|) and foreach (%) when you want to break out – as break does not give you the intended result. Use return instead.
  7. Use goto
    Maybe this might need a bit of explanation, but seriously – in certain cases when you are deep inside of some nested loop constructs to easily jump out after you found want you wanted makes programming much easier. I therefore define a custom exception type, throw it and evaluate it in the catch handler.

    try {
    { # some nested code
    if(someOperationSucceeded) { throw($gotoSuccess); }
    }
    } # try
    catch {
    if($gotoSuccess -eq $_.Exception.Message) {
    $fReturn = $true;
    } # gotoSuccess
    } # catch
    
  8. If you indend to re-throw document that in your code.
  9. Use PowerShell Modules to encapsulate and deploy your code
    A module can be any where in the file system as long as it is within $ENV:PSModulePath. Create a folder of your liking and place a psm1 file into it with the same base name as the folder. Eg folder: testmodule, file: testmodule.psm1. In that file place your PowerShell CmdLets (or variables) and export them with Export-ModuleMember -Function New-CmdLetModule -Alias My-Alias;.
  10. When using modules that need some kind of configuration data use a configuration file that is loaded on Import-Module. Name the configuration file with the same base name as the psm1 file and auto load it with Import-CliXml. You can then access the data easily via a hash table:
    # configuration file is $MODULE_NAME.xml
    Set-Variable MODULE_NAME -Option 'Constant' -Value 'biz.dfch.PS.System.Logging';
    $ENV:PSModulePath.Split(';') | % {
    [string] $ModuleDirectoryBase = Join-Path -Path $_ -ChildPath $MODULE_NAME;
    [string] $ModuleConfigFile = [string]::Format('{0}.xml', $MODULE_NAME);
    [string] $ModuleConfigurationPathAndFile = Join-Path -Path $ModuleDirectoryBase -ChildPath $ModuleConfigFile;
    if($true -eq (Test-Path -Path $ModuleConfigurationPathAndFile)) {
    if($true -ne (Test-Path variable:$($MODULE_NAME.Replace('.', '_')))) {
    Set-Variable -Name $MODULE_NAME.Replace('.', '_') -Value (Import-Clixml -Path $ModuleConfigurationPathAndFile) -Description "The array contains the public configuration properties of the module '$MODULE_NAME'.`n$MODULE_URI_BASE" ;
    } # if()  } # if()
    } # for()
    if($true -ne (Test-Path variable:$($MODULE_NAME.Replace('.', '_')))) {
    Write-Error "Could not find module configuration file '$ModuleConfigFile' in 'ENV:PSModulePath'.`nAborting module import...";
      break; # Aborts loading module.
    } # if()
    Export-ModuleMember -Variable $MODULE_NAME.Replace('.', '_');
    
  11. In addition to the previous item it can make sense to have a $PSDefaultParameterValues set up so you can have per-platform specific default parameter values with even less typing.
    To pick up the Send-MailMessage example from TechEd 2012 Advanced PowerShell Automation session you might want to define different default mail server and account parameters for you development, integration and production environment. I would then probably write a generic configuration module the would  be loaded with environment specific configuration options for every platform. That module would then load its values from a configuration file as described in the item above.
  12. Use parameter validation on input parameters.
    Nothing to explain here…
  13. $null, 0 or $false intialise your variables.
  14. Use object.Clone() when returning objects that should live independently of your CmdLet or if you return them in an array and you process more objects of the type.
  15. Have a consistent interface with your caller. Always return parameters in the same way. Eg $null on failure.
  16. Use CmdLet and function templates to have a constistent programming behaviour.
  17. If you want to depend on your code use a consistent logging framework as in biz.dfch.PS.System.Logging. And by the way have your users evaluate the log messages.
  18. Use a PSSessionConfiguration (with a session configuration file) if you want your administrators to execute commands for which additional permissions are needed.
    Suppose you have a vCenter/vCloud evironment where you want to use PowerCLI to access ressources. Your administrators probably need additional permissions to access the VMware environment and you do not want to give everyone full administrative access to it. Besides you do not need all (PowerCLI) CmdLets anyway. So in this case you create a new PSSessionConfiguration file, restrict it to the appropriate user group (your administrators) and specify a “RunAs”-Account for it. In addition, you restrict the available CmdLets your user group may execute.
  19. PowerShell and reference types
    When using references ([out] variables in .NET) you can use a reference (via the [ref] keyword). However, you cannot just pass a variable via [ref] to a method.  You first have to declare the variable and then pass its value to a PSReference type:

    PS > [string] $sLock = "biz.dfch.PS.MyLock";
    PS > [bool] $fCreated = $false;
    PS > $ref = [ref] $fCreated;
    PS > $ref.GetType();
    
    IsPublic IsSerial Name          BaseType
    -------- -------- ----          --------
    False    False    PSReference`1 System.Management.Automation.PSReference
    PS > $hLock = New-Object -TypeName System.Threading.Semaphore -ArgumentList 1, 1, $sLock, $ref;
    PS > $ref
    	Value
    	-----
    	True
    PS > $fCreated;
    True
    
  20. Be bold (and strict)
    Use advanced runtime checking and more proper variable handling via setting “StrictMode”:

    PS > Set-StrictMode -Version 3.0;
    PS >
    
  21. Use string formatter instead of writing variables inline a string for better readability adn safety:
    You can write

    Write-Host "This is the contents my variable s: '$s'.";

    to output “$s” on the the host. If you are using more complex data types (eg hashtables) you have to enclose the expression in parentheses to get the output. Otherwise you only output the data type:

    $ht = @{};
    $ht.s = "Some string";
    Write-Host "This is the contents of my variable ht.s: '$($ht.s)'";

    A better approach is to use “[string]::Format” or its “-f” shortcut:

    Write-Host ([string]::Format("This is the contents of my variable ht.s: '{0}'", $ht.s));
    Write-Host ("This is the contents of my variable ht.s: '{0}'", -f $ht.s);

    To get an overview of possible formatting options see “Windows PowerShell Tip of the Week” on “Formatting Numbers”.

  22. Use a semicolon (“;”) to terminate commands (and also comments)
    Whenever you have to paste code (and comments) you are ensuring that all commands are being interpreted as separate commands even if you leave out line delimiters. Furthermore with that you can have more than one command per line to save space. Eg:

    if($fReturn) { $status = 5; break; };
  23. More to come…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: