Private/New-ProxyCommand.ps1
Function New-ProxyCommand { <# .SYNOPSIS Generate the sourcecode for a ProxyCommand to call a base Cmdlet adding or removing functionality (parameters). .DESCRIPTION Generate the sourcecode for a ProxyCommand to call a base Cmdlet adding or removing functionality (parameters). This command generates a command which calls another command (a ProxyCommand). In doing so, it can add new parameters or remove existing parameters from the original command. Even you can add functionality or change the behavior of the command. If you ADD a parameter, you'll have to implement the semantics of that parameter in the code that gets generated. There are bits of background knowledge you need for proxy functions. 1. Command Precedence (See: Get-Help about_Command_Precedence) If you do not specify a path, Windows PowerShell uses the following precedence order when it runs commands: 1. Alias 2. Function 3. Cmdlet 4. Native Windows commands Aliases beat Functions, Functions beat Cmdlets. Cmdlets beat external scripts and programs. A function named "Get-ChildItem" will be called instead of a cmdlet named "Get-ChildItem" – meaning a function can replace a cmdlet simply by giving it the same name. That is the starting point for a Proxy function. 2. Steppable Pipeline A pipeline (Functions and Cmdlets) can have a 3 internal Named Scriptblocks with the keywords Begin{}, Process{} and End{}. The Begin{} block runs once and initialisize the command (it is recomended that the Begin{} block should not do any output!) The Begin{} block is intended to open needed rescources (databases), or to initialisize Variables inner nested functions. The Process{} block runs for each item passed via the pipeline (it is best to do command output here) The End{} block runs after the last item has passed through Process[} block (the End{} block can do output (eg. on a sort process)) The End{} block schould do the cleanup work to close rescources (databases) or to do tidy up work. Given a script block that contains a single pipeline, the GetSteppablePipeline() method returns a SteppablePipeline object that gives you control over the Begin, Process, and End stages of the pipeline. 3. Argument Splatting. Given a hashtable of names and values, PowerShell lets you pass the entire hashtable to a command. If you use the @ symbol to identify the hashtable variable name (rather than the $ symbol), PowerShell then treats each element of the hashtable as though it were a parameter to the command. 4. .NET classes to create a ProxyCommand 1. We expose the metadata. You can do a New-Object on System.Management.Automation.CommandMetaData passing it cmdletInfo and get it's metadata. Try this to create Metadate from the cmdlet "Get-Process": PS> New-Object System.Management.Automation.CommandMetaData (Get-Command Get-Process) 2. We make the metadata programmable. You can add/remove parameters, change the parameters, change the name, etc. 3. We use metadata to emit a script Cmdlet. PS> $metaData = New-Object System.Management.Automation.CommandMetaData (Get-Command Get-Process) PS> [System.Management.Automation.ProxyCommand]::create($MetaData) 5. A command can be invoked as moduleName\CommandName. If you have created a ProxyCommand to shadow a Cmdlet, the original Cmdlet can be called by use of the Cmdlet-Module path! If you have done a ProxyCommand to shadow the Cmdlet "Get-ChildItem", you can call the original Cmdlet like so: Microsoft.PowerShell.Management\Get-ChildItem See Link: Extending and/or Modifing Commands with Proxies http://blogs.msdn.com/b/powershell/archive/2009/01/04/extending-and-or-modifing-commands-with-proxies.aspx See Link: Customizing PowerShell, Proxy functions and a better Select-String http://jamesone111.wordpress.com/2012/02/04/customizing-powershell-proxy-functions-and-a-better-select-string/ .PARAMETER Name Name of the Cmdlet to proxy. You can Proxy cmdlets or functions The name of the command can be an module path like: Microsoft.Powershell.Management\Get-ChildItem .PARAMETER NewName Provide a new command name if you like to create a new function that is similar to an existing command. The command from the -Name parameter is taken as template for the command with this new name. This new name is used as function name and the old name from the -Name parameter is replaced by this new name in the sourcecode text. .PARAMETER Path Specifies the path to output the resulting sourcecode as a textfile. Note: If this function runs inside the PowerShell ISE, the resulting sourcecode is inserted in a new Editor-Tab by default. .PARAMETER CommandType Type of Command we are proxying. In general you dont' need to specify this but it becomes necessary if there is both a cmdlet and a function with the same name Valid arguments are 'Cmdlet' or 'function' the default is 'cmdlet' .PARAMETER AddParameter List of Parameters as type of 'System.Management.Automation.ParameterMetadata' you would like to add You can even simpy provide an list of Names as type of Strings NOTE: you have to edit the resultant code to implement the semantics of these parameters. ALSO - you need to remove them from $PSBoundParameters on a call of the origin command! You can use the New-Parameter Function to create a new ParameterMetadata Object .PARAMETER RemoveParameter List of Parameters as type of 'System.String' you would like to remove from the origin command. .PARAMETER ExcludeHelp Provide this Parameter if you do not want to add the helptext (comment based help) of the original command .PARAMETER NoIseInsert Provide this Parameter if you do not want to insert the sourcecode produced by this function into the PowerShell ISE. If this function runs inside the PowerShell ISE, the resulting sourcecode is inserted in a new Editor-Tab by default. .EXAMPLE New-ProxyCommand Get-Alias -AddParameter 'SortBy' Create an proxy function from the 'Get-Alias' cmdlet and add a new Parameter with Name 'SortBy' .EXAMPLE New-ProxyCommand -Name Get-Alias -AddParameter 'SortBy' -Path E:\temp\test\Get-MyAliasISE.ps1 Create an proxy function from the 'Get-Alias' cmdlet, add a new Parameter with Name 'SortBy' and save the resulting sourcecode into the file 'E:\temp\test\Get-MyAliasISE.ps1' .EXAMPLE New-ProxyCommand -Name Get-Alias -AddParameter 'SortBy' -Path E:\temp\test\Get-MyAlias.ps1 -NoIseInsert Create an proxy function from the 'Get-Alias' cmdlet, add a new Parameter with Name 'SortBy', save the resulting sourcecode into the file 'E:\temp\test\Get-MyAliasISE.ps1', and prevent outpot to the PowerShell ISE .OUTPUTS System.String The sourcecode of the command to proxy as System.String .NOTES #Requires PowerShell -Version 2.0 NAME: New-ProxyCommand AUTHOR: NTDEV\jsnover ToDo: Need to modify script to emit template help for the proxy command. Probably should add a -AsFunction switch LASTEDIT: 1/4/2009 8:53:35 AM See Link: Extending and/or Modifing Commands with Proxies http://blogs.msdn.com/b/powershell/archive/2009/01/04/extending-and-or-modifing-commands-with-proxies.aspx Edited by: Peter Kriegel Initial release: 18.June.2014 Version: 2.0.0 LASTEDIT: 22.June.2014 History: Switch from use of here Strings to Stringbuilder removed inner function added parameter -Newname and functionality to use a new command name added ability to extract the help from origin command added parameter ExcludeHelp and functionality #> [CmdletBinding( SupportsShouldProcess=$False, SupportsTransactions=$False, ConfirmImpact="None", DefaultParameterSetName="")] param( [Parameter(Position=0, Mandatory=$True)] [String]$Name, [String]$NewName, [String]$Path, [Alias("Type")] [ValidateSet('Cmdlet','Function')] [System.Management.Automation.CommandTypes]$CommandType='Cmdlet', [System.Management.Automation.ParameterMetadata[]]$AddParameter, [String[]]$RemoveParameter, [Switch]$ExcludeHelp, [Switch]$NoIseInsert ) # create a StringBuilder to glue the sourcecode # with StringBuilder we have better control over newline $stringBuilder = New-Object System.Text.StringBuilder # The name of the command can be an module path like: Microsoft.Powershell.Management\Get-ChildItem # so we split out only the leaf namen $OriginCommandName = Split-Path $Name -Leaf # If the original command is modified, or the original hcommant help is included, # we remove all links to the original command # create a flag for this $NoForwardHelp = $False If((-not [String]::IsNullOrEmpty($NewName)) -or (-not [String]::IsNullOrEmpty($RemoveParameter)) -or ($AddParameter.count -gt 0) -or (-Not $ExcludeHelp.IsPresent) ) { $NoForwardHelp = $True } # try to get the metadata from the command Try { $Cmd = Get-Command -Name $Name -CommandType $CommandType -ErrorAction stop } catch { Throw $_ # exit function Return } # if the command exist more then once throw an Error and exit function if (@($cmd).Count -ne 1) { Throw "Command exist more then once!`nAmbiguous reference [$Name : $CommandType]`n$($Cmd | Out-String)" # exit function Return } ## If a function already exists with this name (perhaps it's already been ## wrapped,) output a warning message if(Test-Path function:\$OriginCommandName) { Write-Warning "A Function with the Name: '$OriginCommandName' already exist!" } # get metadata from the command $MetaData = New-Object System.Management.Automation.CommandMetaData $cmd if ($RemoveParameter) { foreach ($ParameterName in @($RemoveParameter)) { # Remove Parameters by Name from the command metadata [Void]$MetaData.Parameters.Remove($ParameterName) } } # Add comment to the Output Text If([String]::IsNullOrEmpty($NewName)) { [void]$stringBuilder.AppendLine("# Begin of ProxyCommand for command: $OriginCommandName") } # Add the head of the function If([String]::IsNullOrEmpty($NewName)) { [void]$stringBuilder.AppendLine("Function $OriginCommandName {") } else { [void]$stringBuilder.AppendLine("Function $NewName {") } # Add Comment based help If(-Not $ExcludeHelp.IsPresent) { # create comment based helptext and reformat it line by line $IsCommendHelpKeyword = $False ("<#$([System.Management.Automation.ProxyCommand]::GetHelpComments((Get-Help $Name)))#>") -split "`n" | ForEach-Object { $Line = ([String]$_).Trim() If(-not [String]::IsNullOrEmpty($NewName)) { $Line = $Line -replace 'Get-ChildItem','Get-Popel' } If($Line -eq '') { $EmptyLineCounter++ } Else { $EmptyLineCounter = 0 } If($Line -like '.*' -or ($Line -eq '<#') -or ($Line -eq '#>')){ # return line without a tab in front # if it is a comment based help keyword or a comment indicator $IsCommendHelpKeyword = $True [void]$stringBuilder.AppendLine($Line) }Else{ # return line with a tab in front # do not return the empty line if they folow directly after a comment based help keyword If(-not ($IsCommendHelpKeyword -and ($Line -eq ''))) { # return line with a tab in front # if it is not a comment based help keyword If($EmptyLineCounter -lt 2) { # supress more than 1 empty lines [void]$stringBuilder.AppendLine("`t$Line") } } $IsCommendHelpKeyword = $False } } # append comment based help text [void]$stringBuilder.AppendLine($CommentHelpText) } If ($AddParameter) { [void]$stringBuilder.AppendLine('<#') [void]$stringBuilder.AppendLine('You are responsible for implementing the logic for added parameters. These ') [void]$stringBuilder.AppendLine('parameters are bound to $PSBoundParameters so if you pass them on the the ') [void]$stringBuilder.AppendLine('command you are proxying, it will almost certainly cause an error. This logic') [void]$stringBuilder.AppendLine('should be added to your BEGIN statement to remove any specified parameters ') [void]$stringBuilder.AppendLine('from $PSBoundParameters.') [void]$stringBuilder.AppendLine('') [void]$stringBuilder.AppendLine('In general, the way you are going to implement additional parameters is by') [void]$stringBuilder.AppendLine('modifying the way you generate the $scriptCmd variable. Here is an example') [void]$stringBuilder.AppendLine('of how you would add a -SORTBY parameter to a cmdlet:') [void]$stringBuilder.AppendLine('') [void]$stringBuilder.AppendLine(' if ($SortBy)') [void]$stringBuilder.AppendLine(' {') [void]$stringBuilder.AppendLine(' [Void]$PSBoundParameters.Remove("SortBy")') [void]$stringBuilder.AppendLine(' $scriptCmd = {& $wrappedCmd @PSBoundParameters |Sort-Object -Property $SortBy}') [void]$stringBuilder.AppendLine(' }else') [void]$stringBuilder.AppendLine(' {') [void]$stringBuilder.AppendLine(' $scriptCmd = {& $wrappedCmd @PSBoundParameters }') [void]$stringBuilder.AppendLine(' }') [void]$stringBuilder.AppendLine('') [void]$stringBuilder.AppendLine('################################################################################ ') [void]$stringBuilder.AppendLine('New ATTRIBUTES:') foreach ($ParameterMetadata in @($AddParameter)) { [Void]$MetaData.Parameters.Add($ParameterMetadata.Name, $ParameterMetadata) [void]$stringBuilder.AppendLine(" if (`$$($ParameterMetadata.Name))") [void]$stringBuilder.AppendLine(" {") [void]$stringBuilder.AppendLine(" [Void]`$PSBoundParameters.Remove($($ParameterMetadata.Name))") [void]$stringBuilder.AppendLine(" }") } [void]$stringBuilder.AppendLine('################################################################################') [void]$stringBuilder.AppendLine('#>') } # end If($AddParameter) [void]$stringBuilder.AppendLine() # create the command sourcecode from metadata $CommandText = [System.Management.Automation.ProxyCommand]::create($MetaData) If($NoForwardHelp) { # Regex to remove the help forwarding to the origin command $regex = New-Object Text.RegularExpressions.Regex "\<\#.*\.ForwardHelpTargetName.*\.ForwardHelpCategory.*\#\>", ('singleline','multiline','IgnoreCase') $CommandText = $regex.Replace($CommandText,'') # Regex to replace the HelpUri $CommandText = $CommandText -Replace ", HelpUri='http://.*?'", '' } # Add a Tab in front of each Line $CommandText = $CommandText -split "`n" | ForEach-Object { "`t$_`n" } [void]$stringBuilder.AppendLine($CommandText) #[void]$stringBuilder.AppendLine(([System.Management.Automation.ProxyCommand]::create($MetaData))) If([String]::IsNullOrEmpty($NewName)) { [void]$stringBuilder.AppendLine("} # End ProxyFunction for command: $OriginCommandName") }Else{ [void]$stringBuilder.AppendLine("} # End of function: $NewName") } $OutputText = $stringBuilder.ToString() # if we are in the PowerShell ISE we create a new File-Tab and # insert the sourcecode of the ProxyCommand in the Editor of the new created File-Tab If($psise -and (-not $NoIseInsert.IsPresent)) { # Create File to open in ISE if Filepath was given If(-not [String]::IsNullOrEmpty($Path)) { '' | Out-File -FilePath $Path # create a new File Tab in the ISE $File = $psise.PowerShellTabs.SelectedPowerShellTab.Files.Add($Path) } Else { # No filepath was given, open File without filepath # create a new File Tab in the ISE $File = $psise.PowerShellTabs.SelectedPowerShellTab.Files.Add() } # call Internal Function to set the Text of the ISE Editor Text in new File Tab $File.Editor.Text = $OutputText # scroll to first char $File.Editor.Select(1,1,1,1) } If(-not [String]::IsNullOrEmpty($Path)) { If($psise -and (-not $NoIseInsert.IsPresent)) { $File.SaveAs($Path) } Else { $OutputText | Out-File -FilePath $Path } } # always return the Text of the ProxyCommand $OutputText }# end function internal New-ProxyCommand -Name 'Get-ChildItem' -CommandType Cmdlet -NewName Get-Popel |