What should you do when a cmdlet doesn’t do enough?

Now that I’ve been using PowerShell extensively for almost a year, I frequently discover opportunities for core cmdlets to provide a richer experience than they do out of the box.  Since PowerShell is a scripting language there are many ways to take advantage of these opportunities including writing new cmdlets, adding type data or format data for specific object types, writing specialized functions, etc.  Sometimes though, all I want to do is extend an existing cmdlet as is while adhering to the following rules:

  1. It improves on the functionality of the original version.
  2. It can be used interchangeably in place of the original cmdlet in any script with no modification required and no loss of functionality.

I just finished the first revision of a helper function that is required if you want to extend existing cmdlets in this way.  This helper function allows me to invoke the original cmdlet that I am modifying with the same parameters that my override received.  It’s called Invoke-Cmdlet, and its purpose is simple: retrieve all cmdlets from all loaded snapins that have a specific name and from those determine which one should be executed by following Microsoft’s name resolution rules as closely as possible.  In this particular function, the logic is simply to invoke the core cmdlet if there is a core cmdlet that has the same name; otherwise, the first non-core cmdlet that has the same name will be invoked.  Here is the PowerShell script defining the function:

Function Invoke-Cmdlet {
    param([
string]$cmdletName = $(throw “Cannot bind argument to parameter ‘CmdletName’ because it is empty.”
))

    $matchingCmdlets
= Microsoft.PowerShell.Core\Get-Command -CommandType cmdlet -Name $cmdletName
   
if (@($matchingCmdlets).Count -eq
0) {
       
throw
“The term ‘$cmdletName’ is not recognized as a cmdlet. Verify the term and try again.”
   
}
    $cmdletName = $null
   
foreach ($cmdlet in $matchingCmdlets
) {
       
if ($cmdlet
.PSSnapin.IsDefault) {
           
$cmdletName =
“$($cmdlet.PSSnapin.Name)\$($cmdlet.Name)”
           
break
       
} elseif (-not $cmdletName) {
            $cmdletName = “$($cmdlet.PSSnapin.Name)\$($cmdlet.Name)”
        }
    }


   
Invoke-Expression “$cmdletName $($passThruArgs = $args; for ($i = 0; $i -lt $passThruArgs.Count; $i++) { if ($passThruArgs[$i] -match ‘^-‘) { $passThruArgs[$i] } else { `”`$passThruArgs[$i]`” } }) | Write-Output”

}

Now that this function is written, what is an example where you could use this to extend a cmdlet while following the rules outlined above?  Right here.

Kirk out.

Technorati Tags: , ,

Advertisements

5 thoughts on “What should you do when a cmdlet doesn’t do enough?

  1. Thanks for this excellent post; I had been inching toward an ulcer trying to solve the problem or robustly passing through parameters in a generic fashion.

    A few comments:

    – At powershellcommunity.org you have a version that also correctly relays pipeline input; may be worth mentioning here.

    – There’s a tiny chance that the code that builds your expression string malfunctions: in case $OFS happens to be set to something other than space.

    – What’s missing is passing through errors from the invoked expression; for instance, if the invoked expression fails with a terminating error, you’d want Invoke-CmdLet to reflect that by having $? return $false. Similarly, in the version of Invoke-CmdLet that relays pipeline objects one by one, you’d want to stop further pipeline processing if the invoked expression encountered a terminating error.

    If you’re interested, find a modified version of your function below that tries to address these issues (sans the code that tries to find the fully qualified cmdlet name).

    ——————-
    Function Invoke-Cmdlet {
    param([string]$cmdletName = $(throw “Please specify the cmdlet to invoke.”))

    BEGIN {
    # We build a string containing an expression to pass to Invoke-Expression:
    # – Parameter *names* (e.g. ‘-recurse’;yes, those are also contained in $args) are copied to the string ‘as is’.
    # – Parameter *values* are represented by *variable references* to the elements of a *copy* of the original $args array;
    # using a copy of $args is crucial, as by the time Invoke-Expression evaluates the string, $args no longer contains *this* function’s parameters.
    # Using variable references in lieu of the actual parameter values saves us from having to worry about having to represent the values as strings
    # (quoting, …). Note that simply outputting the parameter names / variable referencs in a loop in a subexpression implicitly builds up an array
    # which is converted to a space-delimited list of tokens when expanded in the context of the overall expression string–which is exactly what we want.
    # However, to ensure that the subexpression array is converted to a *space*-delimited list, we must make sure that $OFS is either not defined or set to a space.
    # Example:
    # Original parameter list…
    # -filter *.reg
    # … is translated to expression string:
    # -filter $passThruArgs[1] # ‘*.reg’ replaced with a reference to it, contained in the 2nd element of the argument-array copy.
    if ($OFS) { $localOFSbefore = $local:OFS; $local:OFS = ‘ ‘ } # Ensure that $OFS is not defined or defined as a space, so that our subexpression array is converted to a *space*-delimited list.
    $passThruExpr = “$cmdletName $($passThruArgs = $args; for ($i = 0; $i -lt $passThruArgs.Count; $i++) { if ($passThruArgs[$i] -match ‘^-‘) { $passThruArgs[$i] } else { `”`$passThruArgs[$i]`” } })”
    if ($OFS) { if ($localOFSbefore) { $local:OFS = $localOFSbefore } else { Remove-Variable OFS -Scope Local } } # Restore original $OFS, if necessary.
    }

    PROCESS {

    # Count the number of errors before.
    # Note that Invoke-Expression will not relay errors that occur during execution of the expression; we’d have
    # to add the common ‘-ErrorVariable’ parameter to the *expression* to achieve that (right?), but we cannot assume that whatever we
    # invoke supports it.
    $errCountBefore = $Error.Count

    if ($_) {
    # We have pipeline input (‘$_’ is non-null), so we must include it in the expression to evaluate.
    Invoke-Expression “`$_ | $passThruExpr”
    } else {
    Invoke-Expression $passThruExpr
    }

    if ($Error.Count -ne $errCountBefore) {
    # A terminating or non-terminating error occurred during evaluation of the expression.
    # (Note: In theory, $Error.Clear() could have been called by the expression invoked, and $Error.Count could by sheer coincidence end up the same
    # as before, despite errors. However, this is too unlikely a scenario to worry about.)
    # To mimic the behavior of the command we’ve invoked, we’d have to
    # (a) determine if the error was terminating or not (we base this on whether the error record’s execption is derived from
    # [Microsoft.PowerShell.Commands.WriteErrorException], which is produced by calling ‘Write-Error’ when reporting NON-terminating errors).
    # and
    # (b), if so, abort silently, and make sure that $? returns $false to indicate failure.
    if (-not ($Error[0].Exception -is [Microsoft.PowerShell.Commands.WriteErrorException])) {
    # The following trick aborts silently, and cause $? to report $false, as desired: simply throw and catch a dummy exception, then break.
    # (Neither using ‘break’ nor ‘throw’ by itself is sufficient: ‘break’ doesn’t affect $?, and ‘throw’ invariably causes additional error output.)
    try { throw } catch { break }
    }
    }
    }
    END {}
    }

    Like

    1. Thanks Michael! All great feedback, and I’ll have to see what I can do to update this function myself. I’d be interested to know if you’re specifically looking for a v1 solution to this or if v2 is ok. If you’re doing this in v1, are you planning to upgrade to v2 once it is generally available or will it take a while before you can upgrade? That information will be useful when I’m looking at upgrading my function library, this function included.

      Like

  2. Thanks for responding. I have the luxury of being unencumbered by real-world constraints at the moment; I’m only using PowerShell on my personal machines, so I’m interested in v2 solutions.

    Like

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