Last Update: Sep 04, 2024 | Published: Jun 22, 2015
After you have been using PowerShell for a while, you’ll begin finding the need to create your own tools and commands. Often, your commands are centered on a PowerShell command that you want to adjust to meet your requirements. Perhaps you need to add a new parameter, modify an existing parameter, or take results from a command and combine them with something else. This can be a very tedious process if you attempt to build your own command from scratch. As you have probably surmised, I’m here to help. First, let’s look at two approaches on how to create your own PowerShell command.
A proxy command is often used to customize the behavior of a given cmdlet, often by removing parameters. This is most often done in scenarios where you have created a delegated and restricted remote-endpoint, and you not only want to limit what commands can be run, but also what parameters can be used. Often the proxy command uses the same name as the original command and passes parameters to it.
You can create a proxy function manually by first creating a CommandMetaData object.
This object is a snapshot of the command, its parameters, and settings. This object has a method that will generate a proxy command.
The proxy code looks like this:
[Parameter(ParameterSetName='Default', Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [Alias('ServiceName')] [string[]] ${Name}, [Parameter(ValueFromPipelineByPropertyName=$true)] [Alias('Cn')] [ValidateNotNullOrEmpty()] [string[]] ${ComputerName}, [Alias('DS')] [switch] ${DependentServices}, [Alias('SDO','ServicesDependedOn')] [switch] ${RequiredServices}, [Parameter(ParameterSetName='DisplayName', Mandatory=$true)] [string[]] ${DisplayName}, [ValidateNotNullOrEmpty()] [string[]] ${Include}, [ValidateNotNullOrEmpty()] [string[]] ${Exclude}, [Parameter(ParameterSetName='InputObject', ValueFromPipeline=$true)] [ValidateNotNullOrEmpty()] [System.ServiceProcess.ServiceController[]] ${InputObject}) begin { try { $outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) { $PSBoundParameters['OutBuffer'] = 1 } $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-Service', [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 Get-Service .ForwardHelpCategory Cmdlet #>
All you have to do is wrap it in a function scriptblock and modify as necessary. Personally, I like to clean up parameter definitions and remove the { } characters from the parameter variables.
By contrast, a wrapper function typically focuses on a single cmdlet, but it can be customized to meet a specific need. For example, you might want to adjust parameters, or you might want to further process the results. Many of the tools you will build will most likely rely on wrapper functions.
Here is a wrapper function I wrote for Get-History.
Param( [Parameter(Position=0, ValueFromPipeline=$true)] [ValidateRange(1, 9223372036854775807)] [long[]]$Id, [Parameter(Position=1)] [ValidateRange(0, 32767)] [int]$Count ) Begin { Write-Verbose "Starting $($MyInvocation.Mycommand)" Write-Verbose "Using parameter set $($PSCmdlet.ParameterSetName)" Write-Verbose ($PSBoundParameters | out-string) } #begin Process { Get-History @PSBoundParameters | Select-Object -Unique | Select ID,Commandline,StartExecutionTime,EndExecutionTime } #process End { Write-Verbose "Ending $($MyInvocation.Mycommand)" } #end } #end function Get-MyHistory
I wanted something that would give me history but weed out the duplicates. However, I want my function to be as complete as possible with help and all of the same parameters as Get-History. But I don’t want to have to manually create all of that content. Instead, I make PowerShell do the work for me.
I created a function I call Copy-Command, which grew out of earlier efforts to streamline the creation of proxy functions. You can use the ProxyCommand class to get other types of information from a command, where you can even grab individual elements if you wish.
This means that I can construct a new command on the fly, including copying the original help as a comment block.
I created Copy-Command, so I could easily create a wrapper command or a proxy function.
<# copy a PowerShell command including parameters and help to turn it into a wrapper or a proxy function run this in the PowerShell ISE for best results #> Function Copy-Command { <# .Synopsis Copy a PowerShell command .Description This command will copy a PowerShell command, including parameters and help to a new user-specified command. You can use this to create a "wrapper" function or to easily create a proxy function. The default behavior is to create a copy of the command complete with the original comment-based help block. For best results, run this in the PowerShell ISE so that the copied command will be opened in a new tab. .Parameter Command The name of a PowerShell command, preferably a cmdlet but that is not a requirment. You can specify an alias and it will be resolved. .Parameter NewName Specify a name for your copy of the command. If no new name is specified, the original name will be used. .Parameter IncludeDynamic The command will only copy explicitly defined parameters unless you specify to include any dynamic parameters as well. If you copy a command and it seems to be missing parameters, re-copy and include dynamic parameters. .Parameter AsProxy Create a traditional proxy function. .Parameter UseForwardHelp By default the copy process will create a comment-based help block with the original command's help which you can then edit to meet your requirements. Or you can opt to retain the forwarded help links to the original command. .Example PS C:> Copy-Command Get-Process Get-MyProcess Create a copy of Get-Process called Get-MyProcess. .Example PS C:> Copy-Command Get-Eventlog -asproxy -useforwardhelp Create a proxy function for Get-Eventlog and use forwarded help links. .Example PS C:> Copy-Command Get-ADComputer Get-MyADComputer -includedynamic Create a wrapper function for Get-ADComputer called Get-MyADComputer. Due to the way the Active Directory cmdlets are written, most parameters appear to be dynamic so you need to include dynamic parameters otherwise there will be no parameters in the final function. .Notes Last Updated: 5/18/2015 Version : 0.9 Learn more about PowerShell:Essential PowerShell Learning Resources**************************************************************** * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * **************************************************************** .Link Get-Command .Inputs None .Outputs [string[]] #> [cmdletbinding()] Param( [Parameter(Position=0,Mandatory=$True,HelpMessage="Enter the name of a PowerShell command")] [ValidateNotNullorEmpty()] [string]$Command, [Parameter(Position=1,HelpMessage="Enter the new name for your command using Verb-Noun convention")] [ValidateNotNullorEmpty()] [string]$NewName, [switch]$IncludeDynamic, [switch]$AsProxy, [switch]$UseForwardHelp ) Try { Write-Verbose "Getting command metadata for $command" $gcm = Get-Command -Name $command -ErrorAction Stop #allow an alias or command name if ($gcm.CommandType -eq 'Alias') { $cmdName = $gcm.ResolvedCommandName } else { $cmdName = $gcm.Name } Write-Verbose "Resolved to $cmdName" $cmd = New-Object System.Management.Automation.CommandMetaData $gcm } Catch { Write-Warning "Failed to create command metadata for $command" Write-Warning $_.Exception.Message } if ($cmd) { #create the metadata if ($NewName) { $Name = $NewName } else { $Name = $cmd.Name } #define a metadata comment block $myComment = @" <# This is a copy of: $(($gcm | format-table -AutoSize | out-string).trim()) Created: $((Get-Date).ToShortDateString()) Author : $env:username **************************************************************** * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * **************************************************************** #> "@ #define the beginning of text for the new command #dynamically insert the command's module if one exists $text = @" #requires -version $($PSVersionTable.psversion) $(if ($gcm.modulename -AND $gcm.modulename -notmatch "Microsoft.PowerShell.w+") { "#requires -module $($gcm.modulename)" }) $myComment Function $Name { "@ #manually copy parameters from original command if param block not found #this can happen with dynamic parameters like those in the AD cmdlets if (-Not [System.Management.Automation.ProxyCommand]::GetParamBlock($gcm)) { Write-Verbose "No param block detected. Looking for dynamic parameters" $IncludeDynamic = $True } if ($IncludeDynamic) { Write-Verbose "Adding dynamic parameters" $params = $gcm.parameters.GetEnumerator() | where { $_.value.IsDynamic} foreach ($p in $params) { $cmd.Parameters.add($p.key,$p.value) } } if ($UseForwardHelp) { #define a regex to pull forward help from a proxy command [regex]$rx = ".ForwardHelp.*s+.ForwardHelp.*" $help = $rx.match([System.Management.Automation.ProxyCommand]::Create($cmd)).Value } else { #if not using the default Forwardhelp links, get comment based help instead #get help as a comment block $help = [System.Management.Automation.ProxyCommand]::GetHelpComments((get-help $Command)) #substitute command name $help = $help -replace $Command,$NewName #remove help link $cmd.HelpUri = $null } Write-Verbose "Adding Help" $Text += @" <# $help #> "@ #cmdletbinding $Text += [System.Management.Automation.ProxyCommand]::GetCmdletBindingAttribute($cmd) #get parameters $NewParameters = [System.Management.Automation.ProxyCommand]::GetParamBlock($cmd) Write-Verbose "Cleaning up parameter names" [regex]$rx= ']rs+${(?<var>w+)}' #replace the {variable-name} with just variable-name and joined to type name $NewParameters = $rx.Replace($NewParameters,']$$${var}') #Insert parameters $Text += @" Param( $NewParameters ) Begin { Write-Verbose "Starting `$(`$MyInvocation.Mycommand)" Write-Verbose "Using parameter set `$(`$PSCmdlet.ParameterSetName)" Write-Verbose (`$PSBoundParameters | out-string) "@ Write-Verbose "Adding Begin" if ($AsProxy) { $Text += [System.Management.Automation.ProxyCommand]::GetBegin($cmd) } $Text += @" } #begin Process { "@ Write-Verbose "Adding Process" if ($AsProxy) { $Text += [System.Management.Automation.ProxyCommand]::GetProcess($cmd) } else { $Text += @" $($cmd.name) @PSBoundParameters "@ } $Text += @" } #process End { Write-Verbose "Ending `$(`$MyInvocation.Mycommand)" "@ Write-Verbose "Adding End" If ($AsProxy) { $Text += [System.Management.Automation.ProxyCommand]::GetEnd($cmd) } $Text += @" } #end "@ #insert closing text $Text += @" } #end function $Name "@ if ($host.Name -match "PowerShell ISE") { #open in a new ISE tab $tab = $psise.CurrentPowerShellTab.Files.Add() Write-Verbose "Opening new command in a new ISE tab" $tab.editor.InsertText($Text) #jump to the top $tab.Editor.SetCaretPosition(1,1) } else { #just write the new command to the pipeline $Text } } Write-Verbose "Ending $($MyInvocation.MyCommand)" }#end Copy-Command Set-Alias -Name cc -Value Copy-Command
Another reason I wrote this is because I was having an issue creating proxy commands of the Active Directory cmdlets like Get-ADUser. It turns out that in those cmdlets many of the parameters are defined as dynamic parameters. These type of parameters are not detected when generating a proxy command. My solution was to retrieve the dynamic parameters from Get-Command and add them to my $cmd object, which would eventually be copied to my new command.
The function's default behavior is to copy and paste the help as a comment, which you can then edit. If you prefer, you can tell the function to use the forwarded help links. The whole purpose of this command is to duplicate an existing command with minimal effort. The function works best in the ISE as your new command will automatically be opened in a new tab.
All I have to do now is refine the function to do what I need. All of the grunt work of typing help, parameters, and scriptblocks is automatically done for me. I hope you'll let me know what you think about this. If I'm doing something you don't quite understand, please post a question in the comments. I will be using this in a number of upcoming articles to create new commands to solve a few problems. I hope you'll stay tuned.