Personalising PowerShell Commands, or what seemed like a good idea in Redmond isn’t necessarily the best option for me!

January 8, 2017 at 9:28 am Leave a comment

To be fair, when the PowerShell commands are being developed in Redmond they need to be able to work for all people across the globe in all situations as much as possible.  In particular this means that many of the default values for command parameters are very generic so at least they work.  A classic example of this is the parameter ComputerName having a default value of LocalHost.  This will work, but in most cases is not the best value for administrators.

As an example of how you can personalise a command, I will look at the ConvertTo-HTML command. My preference is rather than have to output look like:

converttohtmlsansborders

is to have it look like:

converttohtmlborders

There are a couple of ways to personalise how these commands work so that you can get the default values that make sense for you and your environment.  A very straightforward option is to use the $PSDefaultParameterValues variable that you can set in your profile.  This enables you to define a value for a specific parameter on a specific command.  For example, I can use:

 $PSDefaultParameterValues.Add("ConvertTo-HTML:Head",$style)

in my profile.ps1 file to set the default value for the Head parameter in my ConvertTo-HTML to embed a css specification for the table layout as defined in the $style variable.

Ref: Get-Help about_PSDefaultParameterValues -ShowWindow

This approach works very well but has the limitation that it is set for me on one computer.  If there are commands that are commonly used by administrators in your organisation and you all have a common need it would be better to have an organisational solution.

My approach to this is to create a wrapper or proxy command.  What I mean by this is that you effectively write your own version of the standard command and set you own default values and/or other functionality.  This command can then be put in to an organisational module (you can set PowerShell to look for modules on a UNC path so that it is shared by all staff.)

Ref: https://bchallis.wordpress.com/2015/11/14/where-does-powershell-look-for-modules/

One way to do this is to write your own command collecting all the parameters used in the original command and then use these values to make the call to the original command.  An easier way is to use PowerShell to do it for you.  I have a command New-CCCommandWrapper which is:

function New-CCCommandWrapper
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $True)]
        [String]$CommandName,
        [String]$NounPrefix = "CC",
        [Switch]$CopyToClipboard,
        [Switch]$RawCode
    )
    [System.Text.StringBuilder]$sb = New-Object System.Text.StringBuilder | Out-Null
    $functionName = $CommandName.Replace("-","-$NounPrefix")
    $sb = "function " + $functionName + "`n{`n`t"
    $command = Get-Command $CommandName
    $metadata = New-Object System.Management.Automation.CommandMetaData($command)
    [String]$code = [System.Management.Automation.ProxyCommand]::Create($metadata)
    [String]$formattedCode = $code.Replace("`n","`n`t")
    if ($formattedCode.EndsWith("`t"))
    {
        $sb.Append($formattedCode.Substring(0,($formattedCode.Length - 1))) | Out-Null
    }
    else
    {
        $sb.Append($formattedCode) | Out-Null
    }
    
    $sb.Append("}") | Out-Null
    
    if ($RawCode)
    {
        $codeToReturn = $code
    }
    else
    {
        $codeToReturn = $sb.ToString()
    }
    if ($CopyToClipboard)
    {
        Set-Clipboard -Value $codeToReturn
        Write-Verbose "$functionName copied to clipboard"
    }
    else
    {
        Write-Output $codeToReturn
    }
}
As an aside, I like the way Sapien’s PowerShell Studio creates Comment Based Help by reading the parameter definition to produce a useful template.

ref: https://www.sapien.com/software/powershell_studio

psstudiogeneratecommenthelp

Using this gave me a template for the help that provides tags for all of the parameters.

New-CCComandWrapper writes the following code:

function ConvertTo-CCHtml
{
    [CmdletBinding(DefaultParameterSetName = 'Page', HelpUri = 'http://go.microsoft.com/fwlink/?LinkID=113290', RemotingCapability = 'None')]
    param (
        [Parameter(ValueFromPipeline = $true)]
        [psobject]${InputObject},
        [Parameter(Position = 0)]
        [System.Object[]]${Property},
        [Parameter(ParameterSetName = 'Page', Position = 3)]
        [string[]]${Body},
        [Parameter(ParameterSetName = 'Page', Position = 1)]
        [string[]]${Head},
        [Parameter(ParameterSetName = 'Page', Position = 2)]
        [ValidateNotNullOrEmpty()]
        [string]${Title},
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Table', 'List')]
        [string]${As},
        [Parameter(ParameterSetName = 'Page')]
        [Alias('cu', 'uri')]
        [ValidateNotNullOrEmpty()]
        [uri]${CssUri},
        [Parameter(ParameterSetName = 'Fragment')]
        [ValidateNotNullOrEmpty()]
        [switch]${Fragment},
        [ValidateNotNullOrEmpty()]
        [string[]]${PostContent},
        [ValidateNotNullOrEmpty()]
        [string[]]${PreContent})
    
    begin
    {
        try
        {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
            {
                $PSBoundParameters['OutBuffer'] = 1
            }
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\ConvertTo-Html', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = { & $wrappedCmd @PSBoundParameters }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        }
        catch
        {
            throw
        }
    }
    
    process
    {
        try
        {
            $steppablePipeline.Process($_)
        }
        catch
        {
            throw
        }
    }
    
    end
    {
        try
        {
            $steppablePipeline.End()
        }
        catch
        {
            throw
        }
    }
    <#          .ForwardHelpTargetName Microsoft.PowerShell.Utility\ConvertTo-Html     .ForwardHelpCategory Cmdlet          #>
}

The wrapper function uses the $PSBoundParameters dictionary object to feed the values provided when the command is called to the wrapped command.  My code checks to see if there is a value for the Head parameter, and if not, passes the default table style in to that parameter at the start of the begin block to produce the following command:

function ConvertTo-CCHTML
{
    [CmdletBinding(DefaultParameterSetName = 'Page', HelpUri = 'http://go.microsoft.com/fwlink/?LinkID=113290', RemotingCapability = 'None')]
    param
    (
        [Parameter(ValueFromPipeline = $true)]
        [psobject]${InputObject},
        [Parameter(Position = 0)]
        [System.Object[]]${Property},
        [Parameter(ParameterSetName = 'Page', Position = 3)]
        [string[]]${Body},
        [Parameter(ParameterSetName = 'Page', Position = 1)]
        [string[]]${Head},
        [Parameter(ParameterSetName = 'Page', Position = 2)]
        [ValidateNotNullOrEmpty()]
        [string]${Title},
        [ValidateSet('Table', 'List')]
        [ValidateNotNullOrEmpty()]
        [string]${As},
        [Parameter(ParameterSetName = 'Page')]
        [Alias('cu', 'uri')]
        [ValidateNotNullOrEmpty()]
        [uri]${CssUri},
        [Parameter(ParameterSetName = 'Fragment')]
        [ValidateNotNullOrEmpty()]
        [switch]${Fragment},
        [ValidateNotNullOrEmpty()]
        [string[]]${PostContent},
        [ValidateNotNullOrEmpty()]
        [string[]]${PreContent}
    )
    
    begin
    {
        try
        {
            $defaultTableStyle = '<style>
            th {
                  VERTICAL-ALIGN:  TOP; 
                  COLOR:  #018AC0; 
                  TEXT-ALIGN:  left;
                  background-color:LightSteelBlue;
                  color:Black;
                  BORDER: 1px  solid black;
                  }
            table, td 
            {
                border: 1px solid black;
            }
            td 
            {
                padding: 5px;
            }
            tr:nth-child(even) {background-color:White;}
            tr:nth-child(odd) {background-color:AliceBlue;}
            </style>'
            if ([String]::IsNullOrWhiteSpace($Head))
            {
                $PSBoundParameters['Head'] = $defaultTableStyle
            }
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
            {
                $PSBoundParameters['OutBuffer'] = 1
            }
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\ConvertTo-Html', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = { & $wrappedCmd @PSBoundParameters }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        }
        catch
        {
            throw
        }
    }
    
    process
    {
        try
        {
            $steppablePipeline.Process($_)
        }
        catch
        {
            throw
        }
    }
    
    end
    {
        try
        {
            $steppablePipeline.End()
        }
        catch
        {
            throw
        }
    }
}

This approach means I can include my version of the command in a module that can be deployed as widely as needed.

I have included the code for both commands here commands.

In my next post I will discuss two of the other command wrappers that I have created which alter the functionality of the commands they wrap.

Advertisements

Entry filed under: PowerShell.

Where does PowerShell look for Modules Splitting command lines into command and arguments

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

Trackback this post  |  Subscribe to the comments via RSS Feed


Calendar

January 2017
M T W T F S S
« Nov   Feb »
 1
2345678
9101112131415
16171819202122
23242526272829
3031  

Most Recent Posts


%d bloggers like this: