Transpilers/Inherit.psx.ps1
<# .SYNOPSIS Inherits a Command .DESCRIPTION Inherits a given command. This acts similarily to inheritance in Object Oriented programming. By default, inheriting a function will join the contents of that function with the -ScriptBlock. Your ScriptBlock will come first, so you can override any of the behavior of the underlying command. You can "abstractly" inherit a command, that is, inherit only the command's parameters. Inheritance can also be -Abstract. When you use Abstract inheritance, you get only the function definition and header from the inherited command. You can also -Override the command you are inheriting. This will add an [Alias()] attribute containing the original command name. One interesting example is overriding an application .EXAMPLE Invoke-PipeScript { [inherit("Get-Command")] param() } .EXAMPLE { [inherit("gh",Overload)] param() begin { "ABOUT TO CALL GH"} end { "JUST CALLED GH" } }.Transpile() .EXAMPLE # Inherit Get-Transpiler abstractly and make it output the parameters passed in. { [inherit("Get-Transpiler", Abstract)] param() process { $psBoundParameters } }.Transpile() .EXAMPLE { [inherit("Get-Transpiler", Dynamic, Abstract)] param() } | .>PipeScript #> param( # The command that will be inherited. [Parameter(Mandatory,Position=0)] [Alias('CommandName')] [string] $Command, # If provided, will abstractly inherit a function. # This include the function's parameters, but not it's content # It will also define a variable within a dynamicParam {} block that contains the base command. [switch] $Abstract, # If provided, will set an alias on the function to replace the original command. [Alias('Overload')] [switch] $Override, # If set, will dynamic overload commands. # This will use dynamic parameters instead of static parameters, and will use a proxy command to invoke the inherited command. [switch] $Dynamic, # If set, will not generate a dynamic parameter block. This is primarily present so Abstract inheritance has a small change footprint. [switch] $NoDynamic, # If set, will always inherit commands as proxy commands. # This is implied by -Dynamic. [switch] $Proxy, # The Command Type. This can allow you to specify the type of command you are overloading. # If the -CommandType includes aliases, and another command is also found, that command will be used. # (this ensures you can continue to overload commands) [Alias('CommandTypes')] [string[]] $CommandType = 'All', # A list of block types to be excluded during a merge of script blocks. # By default, no blocks will be excluded. [ValidateSet('using', 'requires', 'help','header','param','dynamicParam','begin','process','end')] [Alias('SkipBlockType','SkipBlockTypes','ExcludeBlockTypes')] [string[]] $ExcludeBlockType, # A list of block types to include during a merge of script blocks. [ValidateSet('using', 'requires', 'help','header','param','dynamicParam','begin','process','end')] [Alias('BlockType','BlockTypes','IncludeBlockTypes')] [string[]] $IncludeBlockType = @('using', 'requires', 'help','header','param','dynamicParam','begin','process','end'), # A list of parameters to include. Can contain wildcards. # If -IncludeParameter is provided without -ExcludeParameter, all other parameters will be excluded. [string[]] $IncludeParameter, # A list of parameters to exclude. Can contain wildcards. # Excluded parameters with default values will declare the default value at the beginnning of the command. [string[]] $ExcludeParameter, # The ArgumentList parameter name # When inheriting an application, a parameter is created to accept any remaining arguments. # This is the name of that parameter (by default, 'ArgumentList') # This parameter is ignored when inheriting from anything other than an application. [Alias('ArgumentListParameter')] [string] $ArgumentListParameterName = 'ArgumentList', # The ArgumentList parameter aliases # When inheriting an application, a parameter is created to accept any remaining arguments. # These are the aliases for that parameter (by default, 'Arguments' and 'Args') # This parameter is ignored when inheriting from anything other than an application. [Alias('ArgumentListParameters','ArgumentListParameterNames')] [string[]] $ArgumentListParameterAlias = @('Arguments', 'Args'), # The ArgumentList parameter type # When inheriting an application, a parameter is created to accept any remaining arguments. # This is the type of that parameter (by default, '[string[]]') # This parameter is ignored when inheriting from anything other than an application. [type] $ArgumentListParameterType = [string[]], # The help for the argument list parameter. # When inheriting an application, a parameter is created to accept any remaining arguments. # This is the help of that parameter (by default, 'Arguments to $($InhertedApplication.Name)') # This parameter is ignored when inheriting from anything other than an application. [Alias('ArgumentListParameterDescription')] [string] $ArgumentListParameterHelp = 'Arguments to $($InhertedApplication.Name)', [Parameter(ValueFromPipeline,ParameterSetName='ScriptBlock')] [scriptblock] $ScriptBlock = {} ) process { # To start off with, let's resolve the command we're inheriting. $commandTypes = [Management.Automation.CommandTypes]$($CommandType -join ',') $resolvedCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand($Command, $commandTypes) # If it is an alias if ($resolvedCommand -is [Management.Automation.AliasInfo]) { # check for other commands by this name $otherCommandExists = $ExecutionContext.SessionState.InvokeCommand.GetCommand($Command, $commandTypes -bxor ([Management.Automation.CommandTypes]'Alias')) if ($otherCommandExists) { # and use those instead (otherwise, a command can only be overloaded once). Write-Verbose "Using $otherCommandExists instead of alias" $resolvedCommand = $otherCommandExists } } # If we could not resolve the command if (-not $resolvedCommand) { Write-Error "Could not resolve -Command '$Command'" # error out. return } $InheritTemplate = if ($resolvedCommand.Inherit) { $resolvedCommand.Inherit } elseif ($resolvedCommand.Module.Inherit) { $resolvedCommand.Module.Inherit } if ($InheritTemplate -is [Management.Automation.PSMethodInfo]) { return $InheritTemplate.Invoke(@([Ordered]@{} + $PSBoundParameters)) } elseif ($InheritTemplate -is [scriptblock]) { return (& $InheritTemplate @([Ordered]@{} + $PSBoundParameters) ) } # Prepare parameters for Join-ScriptBlock $joinSplat = @{} foreach ($key in 'IncludeBlockType', 'ExcludeBlockType') { if ($PSBoundParameters[$key]) { $joinSplat[$key] = $PSBoundParameters[$key] } } $joinInheritedSplat = @{} foreach ($key in 'IncludeParameter', 'ExcludeParameter') { if ($PSBoundParameters[$key]) { $joinInheritedSplat[$key] = $PSBoundParameters[$key] } } # and determine the name of the command as a variable $commandVariable = $Command -replace '\W' # If -Dynamic was passed if ($Dynamic) { $Proxy = $true # it implies -Proxy. } $InhertedApplication = if ($resolvedCommand -is [Management.Automation.ApplicationInfo]) { $resolvedCommand } elseif ( $resolvedCommand -is [Management.Automation.AliasInfo] -and $resolvedCommand.ResolvedCommand -is [Management.Automation.ApplicationInfo] ) { $resolvedCommand.ResolvedCommand } if ($InhertedApplication) { # In a couple of cases, we're inheriting an application. # In this scenario, we want to use the same wrapper [ScriptBlock] $paramHelp = foreach ($helpLine in $ExecutionContext.SessionState.InvokeCommand.ExpandString($ArgumentListParameterHelp) -split '(?>\r\n|\n)') { "# $HelpLine" } $applicationWrapper = New-PipeScript -Parameter @{ $ArgumentListParameterName = @( $paramHelp "[Parameter(ValueFromRemainingArguments)]" "[Alias('$($ArgumentListParameterAlias -join "','")')]" "[$ArgumentListParameterType]" "`$$ArgumentListParameterName" ) } -Process { & $baseCommand @ArgumentList } } # Now we get the script block that we're going to inherit. $resolvedScriptBlock = # If we're inheriting an application if ($resolvedCommand -is [Management.Automation.ApplicationInfo]) { # use our light wrapper. $applicationWrapper } elseif ($resolvedCommand -is [Management.Automation.CmdletInfo] -or $Proxy) { # If we're inheriting a Cmdlet or -Proxy was passed, inherit from a proxy command. .>ProxyCommand -CommandName $Command } elseif ( # If it's a function or script $resolvedCommand -is [Management.Automation.FunctionInfo] -or $resolvedCommand -is [management.Automation.ExternalScriptInfo] ) { # inherit from the ScriptBlock definition. $resolvedCommand.ScriptBlock } elseif ( # If we're inheriting an alias to something with a scriptblock $resolvedCommand -is [Management.Automation.AliasInfo] -and $resolvedCommand.ResolvedCommand.ScriptBlock) { # inherit from that ScriptBlock. $resolvedCommand.ResolvedCommand.ScriptBlock } elseif ( # And if we're inheriting from an alias that points to an application $resolvedCommand -is [Management.Automation.AliasInfo] -and $resolvedCommand.ResolvedCommand -is [Management.Automation.ApplicationInfo]) { # use our lite wrapper once more. $applicationWrapper } if (-not $resolvedCommand) { Write-Error "Could not resolve [ScriptBlock] to inheirt from Command: '$Command'" return } # Now we're passing a series of script blocks to Join-PipeScript $( # If we do not have a resolved command, if (-not $resolvedCommand) { {} # the first script block is empty. } else { # If we have a resolvedCommand, fully qualify it. $fullyQualifiedCommand = if ($resolvedCommand.Module) { "$($resolvedCommand.Module)\$command" } else { "$command" } # Then create a dynamicParam block that will set `$baseCommand, # as well as a `$script: scoped variable for the command name. if ($NotDynamic) # unless, of course -NotDynamic is passed { {} } else { [scriptblock]::create(@" dynamicParam { `$baseCommand = if (-not `$script:$commandVariable) { `$script:$commandVariable = `$executionContext.SessionState.InvokeCommand.GetCommand('$($command -replace "'", "''")','$($resolvedCommand.CommandType)') `$script:$commandVariable } else { `$script:$commandVariable } $( # Embed -IncludeParameter if ($IncludeParameter) { "`$IncludeParameter = '$($IncludeParameter -join "','")'" } else { "`$IncludeParameter = @()" }) $( # Embed -ExcludeParameter if ($ExcludeParameter) { "`$ExcludeParameter = '$($ExcludeParameter -join "','")'" } else { "`$ExcludeParameter = @()" }) } "@) } } ), # Next is our [ScriptBlock]. This will come before almost everything else. $scriptBlock, $( # If we are -Overriding, create an [Alias] if ($Override) { [ScriptBlock]::create("[Alias('$($command)')]param()") } else { {} } ),$( # Now we include the resolved script if ($Abstract -or $Dynamic) { # If we're using -Abstract or -Dynamic, we will strip out a few blocks first. $excludeFromInherited = 'begin','process', 'end', 'header','help' if ($Dynamic) { if (-not $Abstract) { $excludeFromInherited = 'param' } else { $excludeFromInherited += 'param' } } $resolvedScriptBlock | Join-PipeScript -ExcludeBlockType $excludeFromInherited @joinInheritedSplat } else { } ), $( if ($Dynamic) { # If -Dynamic was passed, generate dynamic parameters. { dynamicParam { $DynamicParameters = [Management.Automation.RuntimeDefinedParameterDictionary]::new() :nextInputParameter foreach ($paramName in ([Management.Automation.CommandMetaData]$baseCommand).Parameters.Keys) { if ($ExcludeParameter) { foreach ($exclude in $ExcludeParameter) { if ($paramName -like $exclude) { continue nextInputParameter} } } if ($IncludeParameter) { $shouldInclude = foreach ($include in $IncludeParameter) { if ($paramName -like $include) { $true;break} } if (-not $shouldInclude) { continue nextInputParameter } } $DynamicParameters.Add($paramName, [Management.Automation.RuntimeDefinedParameter]::new( $baseCommand.Parameters[$paramName].Name, $baseCommand.Parameters[$paramName].ParameterType, $baseCommand.Parameters[$paramName].Attributes )) } $DynamicParameters } } } else { {} } ) | Join-PipeScript @joinSplat # join the scripts together and return one [ScriptBlock] } |