PowerShell for Non-N00bs: My Script Outline

This is more for me to know where to go looking for this.  I don't expect anyone else to laud it and worship it.  If you find it useful, great.  If you find holes in it, please let me know. At the end of this post will be a walkthrough.

#region #######################################################################

#

# An outline for my scripts

#

# version: 0.1

# last updated: 2009-08-20

# updated by: timid@msdn.com

#

#endregion ####################################################################

#region command line parameters ###############################################

param (

    [switch]$help,

    [switch]$force,

    [switch]$quiet,

    [switch]$verbose,

    [switch]$_debug

);

#endregion ####################################################################

#region help text #############################################################

if ($help) {

    $basename = (Split-Path -Leaf ((&{$myInvocation}).ScriptName)) `

-replace ".PS1$";

    & { # anonymous function context to filter all output through more

@"

NAME

    $basename

SYNOPSIS

    One line description. In this case, an outline for my scripts.

SYNTAX

    $basename [-help] [-force] [-quiet] [-verbose]

DESCRIPTION

    One or two short paragraph description. In this case, this handles my

    most common command line options for my scripts

"@;

        if ($verbose) {

@"

PARAMETERS

    -help

     Show basic help text (not including PARAMETERs, INPUTS, OUTPUTS, and

        NOTES sections) and exit.

    -force

        Script does not terminate if error encountered. Default is for script

        to terminate on first error.

        May include added functionality depending on script.

    -quiet

        Suppress -verbose and warning messages.

    -verbose

        If -help specified, show full help text and exit.

        If -help not specified, may include added functionality depending on

        script.

INPUTS

    None

OUTPUTS

    None

NOTES

    -------------------------- EXAMPLE 1 --------------------------

    C:\PS> $basename

    Description

    -----------

    Does nothing.

    -------------------------- EXAMPLE 2 --------------------------

    C:\PS> $basename -help

    Description

    -----------

    Shows basic help text.

    -------------------------- EXAMPLE 3 --------------------------

    C:\PS> $basename -help -verbose

    Description

    -----------

    Shows full help text.

"@;

        }

        if ($_debug) {

@"

    -------------------------- EXAMPLE 4 --------------------------

    C:\PS> . $basename

    Description

    -----------

    Only load function declarations and not execute any code. This is to

    allow for loading the functions for interactive use and/or testing.

ADVANCED USAGE

    Notes for power users go here.

"@;

        }

@"

RELATED LINKS

    None

"@;

        if (!$verbose) {

@"

REMARKS

    For more information, type: "$basename -help -verbose".

"@;

        }

    } | Out-Host -Paging;

    exit 0;

}

#endregion ####################################################################

#region functions #############################################################

#endregion ####################################################################

#region invoke-script #########################################################

function Invoke-SCRIPTNAME {

    #region house keeping

    function Stop-Script {

        if ($_tracing) { Set-PSDebug -Off; }

     }

     trap {

        $basename = (Split-Path -Leaf ((&{$myInvocation}).ScriptName)) `

            -replace ".PS1$";

        Write-Host -ForegroundColor Red ("$basename ERROR: {0}" `

            -f ($Error[0]).ToString());

        Stop-Script;

        if ($LASTEXITCODE) {

            exit $LASTEXITCODE;

        } else {

            exit 1;

        }

        break;

    }

    #endregion

    #region verbosity parameters

   

    if ($force) {

        $ErrorActionPreference = 'Continue';

    } else {

        $ErrorActionPreference = 'Stop';

    }

    if ($quiet) {

        $VerbosePreference = $WarningPreference = $DebugPreference = `

            'SilentlyContinue';

        $verbose = $false;

        if ($force) { $ErrorActionPreference = 'SilentlyContinue'; }

    }

    $_tracing = $false;

    if ($_debug) {

        $DebugPreference = 'Continue';

        if ($verbose) {

            Set-PSDebug -Trace 1;

            $_tracing = $true;

        }

        $verbose = $true;

    }

    if ($verbose) { $VerbosePreference = 'Continue'; }

    #endregion

    #region core fuctionality

    # body of script goes here

    Stop-Script;

    #endregion

}

if ( $myInvocation.Line -notmatch '^.\s') { Invoke-SCRIPTNAME; }

exit 0;

#endregion ####################################################################

#region design notes ##########################################################

@'

"Always code as if the next guy to work on it is a violent psychopath who knows

where you live." -- Author unknown, saw this taped to someone's monitor.

'@ | Out-Null;

#endregion ####################################################################

The "#region ... #endregion" comments are for PowerGUI's Script Editor block folding feature.  Num + and Num - expand and collapse the code respectively.  Incidentally, it's also how the script is syntax-coloured.  The script editor is surprisingly full-featured - check out the hotkey list.  And, it's free.

I like my scripts to always have -help, -force, -quiet, and -verbose flags.  This also means I don't have to remember if I need to add a comma after a new parameter or not (unlike Perl, PowerShell doesnt like having an empty element, i.e., none specified).  I also am adding an undocumented -_debug flag.

That said, I may as well put as much handling as I can in the script, so I have a large section for -help.  While I don't particularly like PowerShell's "we'll give you less than you want when you Get-Help, you have to Get-Help -full for the good stuff" mindset, I can see where it is appreciated, especially when the help text goes on and on.  Thus, I use -help for the basic text and -help -verbose for the full text.  Additionally, I'll display the write up for the undocumented features if -help -verbose -_debug is specified. After a few months, I expect I'll forget all the kooky stuff I've grafted on.

(Split-Path -Leaf ((&{$myInvocation}).ScriptName)) -replace ".PS1$" is a fancy way to say "take the script's name, not .ps1 extension, not the path".  This saves me from having to keep typing the script's name, and from having to update it if I ever change the name (or copy and paste chunks of this to another script.)

My ideal scripting practice is to have no actual code executing in the body of the script other than the help text and the penultimate line that invokes the equivalent to main().  This way, I can dot the script into my shell and have access to the functions. 

For example, my work does a lot with XML files.  PowerShell, while XML-aware, doesn't have any cmdlets to make working with XML easier, so I've written a few functions for basic jobs, such as taking a node from this doc and putting it into that doc.  The functions may be specialized and hopelessly tied to the specific schema I'm using, but they are still useful.  For one-off tasks, I'd love to just use the function instead of creating a new script and copying the requisite functions in.

Thus, the Invoke-SCRIPTNAME function.  All processing will take place in this function.  Within it I have:

  • A private function to handle the housekeeping after the script terminates.  It's in a function so the trap {} statement and the script can both call it upon both normal and abnormal termination.
  • The aforementioned trap{} catches all errors and pretty-prints them.  Sadly, it doesn't output them to STDERR, but at least it doesn't dump them to STDOUT.  I have a link about the plethora of different (and obscure) filehandles PowerShell uses, but that'll have to wait for later.
  • And if the trap catches some $LASTEXITCODE, then it will exit that.
  • Next, we handle the verbosity.  -quiet trumps all, including -_debug.
  • -force overrides the default setting of the script to stop on nonterminating errors.  Instead it will display the error, but continue.  If -quiet and -force are both specified, it will continue without displaying the error.
  • Next, -_debug only turns on $DebugActionPreference, but if -verbose is also specified, it turns on tracing.  After this point, it forces -verbose on.

if ( $myInvocation.Line -notmatch '^.\s') { Invoke-SCRIPTNAME; } is a fancy way of saying, "If the script is dotted in, don't run it."  This allows us to interactively utilize the functions.

Lastly is a 'scratch space'.  The @' .. '@ | Out-Null is effectively a multiline comment.  The interpreter never ges here because of the previous exit 0 line.  Thus, this can be where I put longer comments that pertain to the design, as opposed to the functionality and execution of individual sections of code.

In review, we learned a few things:

  • The PowerGUI Script Editor does syntax colouring, code block folding, and many more IDE features.  And, it's free.
  • A PowerShell script can have an enormous amount of UI functionality.  It's command-line, but it's still a UI.
  • We really have no excuse to write scripts that are lacking help text, poorly factored, or lack code documentation.

ScriptOutline.ps1