Get-RoughDraftExtension.ps1
#region Piecemeal [ 0.4.1 ] : Easy Extensible Plugins for PowerShell # Install-Module Piecemeal -Scope CurrentUser # Import-Module Piecemeal -Force # Install-Piecemeal -ExtensionModule 'RoughDraft' -ExtensionModuleAlias 'rd' -ExtensionTypeName 'RoughDraft.Extension' -OutputPath '.\Get-RoughDraftExtension.ps1' function Get-RoughDraftExtension { <# .Synopsis Gets Extensions .Description Gets Extensions. RoughDraftExtensions can be found in: * Any module that includes -RoughDraftExtensionModuleName in it's tags. * The directory specified in -RoughDraftExtensionPath * Commands that meet the naming criteria .Example Get-RoughDraftExtension #> [OutputType('Extension')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "", Justification="PSScriptAnalyzer cannot handle nested scoping")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidAssignmentToAutomaticVariable", "", Justification="Desired for scenario")] param( # If provided, will look beneath a specific path for extensions. [Parameter(ValueFromPipelineByPropertyName)] [Alias('Fullname')] [string] $ExtensionPath, # If set, will clear caches of extensions, forcing a refresh. [switch] $Force, # If provided, will get RoughDraftExtensions that extend a given command [Parameter(ValueFromPipelineByPropertyName)] [Alias('ThatExtends', 'For')] [string[]] $CommandName, <# The name of an extension. By default, this will match any extension command whose name, displayname, or aliases exactly match the name. If the extension has an Alias with a regular expression literal (```'/Expression/'```) then the -RoughDraftExtensionName will be valid if that regular expression matches. #> [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string[]] $ExtensionName, <# If provided, will treat -RoughDraftExtensionName as a wildcard. This will return any extension whose name, displayname, or aliases are like the -RoughDraftExtensionName. If the extension has an Alias with a regular expression literal (```'/Expression/'```) then the -RoughDraftExtensionName will be valid if that regular expression matches. #> [Parameter(ValueFromPipelineByPropertyName)] [switch] $Like, <# If provided, will treat -RoughDraftExtensionName as a regular expression. This will return any extension whose name, displayname, or aliases match the -RoughDraftExtensionName. If the extension has an Alias with a regular expression literal (```'/Expression/'```) then the -RoughDraftExtensionName will be valid if that regular expression matches. #> [Parameter(ValueFromPipelineByPropertyName)] [switch] $Match, # If set, will return the dynamic parameters object of all the RoughDraftExtensions for a given command. [Parameter(ValueFromPipelineByPropertyName)] [switch] $DynamicParameter, # If set, will return if the extension could run. [Parameter(ValueFromPipelineByPropertyName)] [Alias('CanRun')] [switch] $CouldRun, # If set, will return if the extension could accept this input from the pipeline. [Alias('CanPipe')] [PSObject] $CouldPipe, # If set, will run the extension. If -Stream is passed, results will be directly returned. # By default, extension results are wrapped in a return object. [Parameter(ValueFromPipelineByPropertyName)] [switch] $Run, # If set, will stream output from running the extension. # By default, extension results are wrapped in a return object. [Parameter(ValueFromPipelineByPropertyName)] [switch] $Stream, # If set, will return the dynamic parameters of all RoughDraftExtensions for a given command, using the provided DynamicParameterSetName. # Implies -DynamicParameter. [Parameter(ValueFromPipelineByPropertyName)] [string] $DynamicParameterSetName, # If provided, will return the dynamic parameters of all RoughDraftExtensions for a given command, with all positional parameters offset. # Implies -DynamicParameter. [Parameter(ValueFromPipelineByPropertyName)] [int] $DynamicParameterPositionOffset = 0, # If set, will return the dynamic parameters of all RoughDraftExtensions for a given command, with all mandatory parameters marked as optional. # Implies -DynamicParameter. Does not actually prevent the parameter from being Mandatory on the Extension. [Parameter(ValueFromPipelineByPropertyName)] [Alias('NoMandatoryDynamicParameters')] [switch] $NoMandatoryDynamicParameter, # If set, will require a [Runtime.CompilerServices.Extension()] attribute to be considered an extension. [Parameter(ValueFromPipelineByPropertyName)] [switch] $RequireExtensionAttribute, # If set, will validate this input against [ValidateScript], [ValidatePattern], [ValidateSet], and [ValidateRange] attributes found on an extension. [Parameter(ValueFromPipelineByPropertyName)] [PSObject] $ValidateInput, # If set, will validate this input against all [ValidateScript], [ValidatePattern], [ValidateSet], and [ValidateRange] attributes found on an extension. # By default, if any validation attribute returned true, the extension is considered validated. [switch] $AllValid, # The name of the parameter set. This is used by -CouldRun and -Run to enforce a single specific parameter set. [Parameter(ValueFromPipelineByPropertyName)] [string] $ParameterSetName, # The parameters to the extension. Only used when determining if the extension -CouldRun. [Parameter(ValueFromPipelineByPropertyName)] [Collections.IDictionary] [Alias('Parameters','ExtensionParameter','ExtensionParameters')] $Parameter = [Ordered]@{}, # If set, will output a steppable pipeline for the extension. # Steppable pipelines allow you to control how begin, process, and end are executed in an extension. # This allows for the execution of more than one extension at a time. [switch] $SteppablePipeline, # If set, will output the help for the extensions [switch] $Help ) begin { $ExtensionPattern = '(?>extension|ext|ex|x)\.ps1$' $ExtensionModule = 'RoughDraft' $ExtensionModuleAlias = 'rd' $ExtensionTypeName = 'RoughDraft.Extension' #region Define Inner Functions function WhereExtends { param( [Parameter(Position=0)] [string[]] $Command, [Parameter(ValueFromPipeline)] [PSObject] $ExtensionCommand ) process { if ($ExtensionName) { $ExtensionCommandAliases = @($ExtensionCommand.Attributes.AliasNames) $ExtensionCommandAliasRegexes = @($ExtensionCommandAliases -match '^/' -match '/$') if ($ExtensionCommandAliasRegexes) { $ExtensionCommandAliases = @($ExtensionCommandAliases -notmatch '^/' -match '/$') } :CheckExtensionName do { foreach ($exn in $ExtensionName) { if ($like) { if (($extensionCommand -like $exn) -or ($extensionCommand.DisplayName -like $exn) -or ($ExtensionCommandAliases -like $exn)) { break CheckExtensionName } } elseif ($match) { if (($ExtensionCommand -match $exn) -or ($extensionCommand.DisplayName -match $exn) -or ($ExtensionCommandAliases -match $exn)) { break CheckExtensionName } } elseif (($ExtensionCommand -eq $exn) -or ($ExtensionCommand.DisplayName -eq $exn) -or ($ExtensionCommandAliases -eq $exn)) { break CheckExtensionName } elseif ($ExtensionCommandAliasRegexes) { foreach ($extensionAliasRegex in $ExtensionCommandAliasRegexes) { $extensionAliasRegex = [Regex]::New($extensionAliasRegex -replace '^/' -replace '/$', 'IgnoreCase,IgnorePatternWhitespace') if ($extensionAliasRegex -and $extensionAliasRegex.IsMatch($exn)) { break CheckExtensionName } } } } return } while ($false) } if ($Command -and $ExtensionCommand.Extends -contains $command) { $commandExtended = $ext return $ExtensionCommand } elseif (-not $command) { return $ExtensionCommand } } } function ConvertToExtension($toExtension) { process { $in = if ($toExtension) { $toExtension } else { $_ } $extCmd = if ($in -is [Management.Automation.CommandInfo]) { $in } elseif ($in -is [IO.FileInfo]) { if ($in.LastWriteTime -gt $script:RoughDraftExtensionsFileTimes[$in.Fullname]) { $script:RoughDraftExtensionsFileTimes[$in.Fullname] = $in.LastWriteTime $script:RoughDraftExtensionsFromFiles[$in.Fullname] = $ExecutionContext.SessionState.InvokeCommand.GetCommand($in.fullname, 'ExternalScript,Application') $script:RoughDraftExtensionsFromFiles[$in.Fullname] } elseif ($script:RoughDraftExtensionsFromFiles[$in.Fullname]) { return $script:RoughDraftExtensionsFromFiles[$in.Fullname] } } else { $ExecutionContext.SessionState.InvokeCommand.GetCommand($in, 'Alias,Function,ExternalScript,Application') } $extMethods = $extCmd.PSObject.Methods $extProperties = $extCmd.PSObject.Properties #region .GetExtendedCommands if (-not $extMethods['GetExtendedCommands']) { $extMethods.Add([psscriptmethod]::new('GetExtendedCommands', { param([Management.Automation.CommandInfo[]]$CommandList) $extendedCommandNames = @( foreach ($attr in $this.ScriptBlock.Attributes) { if ($attr -isnot [Management.Automation.CmdletAttribute]) { continue } ( ($attr.VerbName -replace '\s') + '-' + ($attr.NounName -replace '\s') ) -replace '^\-' -replace '\-$' } ) if (-not $extendedCommandNames) { $this.PSObject.Properties.Add([psnoteproperty]::new('.Extends', @()), $true) $this.PSObject.Properties.Add([psnoteproperty]::new('.ExtensionCommands', @()), $true) return } if (-not $CommandList) { $commandList = $ExecutionContext.SessionState.InvokeCommand.GetCommands('*','Function,Alias,Cmdlet', $true) } $extends = @{} :nextCommand foreach ($loadedCmd in $commandList) { foreach ($extensionCommandName in $extendedCommandNames) { if ($extensionCommandName -and $loadedCmd.Name -match $extensionCommandName) { $loadedCmd $extends[$loadedCmd.Name] = $loadedCmd continue nextCommand } } } if (-not $extends.Count) { $extends = $null } $this.PSObject.Properties.Add([psnoteproperty]::new('.Extends', @($extends.Keys)), $true) $this.PSObject.Properties.Add([psnoteproperty]::new('.ExtensionCommands', @($extends.Values)), $true) }), $true) } #endregion .GetExtendedCommands #region .Extends if (-not $extProperties['Extends']) { $extProperties.Add([psscriptproperty]::new('Extends', { if (-not $this.'.Extends') { $this.GetExtendedCommands( $ExecutionContext.SessionState.InvokeCommand.GetCommands('*','Function,Alias,Cmdlet', $true) ) } return $this.'.Extends' }),$true) } #endregion .Extends #region .ExtensionCommands if (-not $extProperties['ExtensionCommands']) { $extProperties.Add([psscriptproperty]::new('ExtensionCommands', { if (-not $this.'.ExtensionCommands') { $this.GetExtendedCommands( $ExecutionContext.SessionState.InvokeCommand.GetCommands('*','Function,Alias,Cmdlet', $true) ) } return $this.'.ExtensionCommands' }), $true) } #endregion .ExtensionCommands $inheritanceLevel = [ComponentModel.InheritanceLevel]::Inherited #region .BlockComments if(-not $extProperties['BlockComments']) { $extProperties.Add([psscriptproperty]::New('BlockComments', { [Regex]::New(" \<\# # The opening tag (?<Block> (?:.|\s)+?(?=\z|\#>) # anything until the closing tag ) \#\> # the closing tag ", 'IgnoreCase,IgnorePatternWhitespace', '00:00:01').Matches($this.ScriptBlock) }), $true) } #endregion .BlockComments #region .GetHelpField if (-not $extMethods['GetHelpField']) { $extMethods.Add([psscriptmethod]::New('GetHelpField', { param([Parameter(Mandatory)]$Field) $fieldNames = 'synopsis','description','link','example','inputs', 'outputs', 'parameter', 'notes' foreach ($block in $this.BlockComments) { foreach ($match in [Regex]::new(" \.(?<Field>$Field) # Field Start [\s-[\r\n]]{0,} # Optional Whitespace [\r\n]+ # newline (?<Content>(?:.|\s)+?(?= ( [\r\n]{0,}\s{0,}\.(?>$($fieldNames -join '|'))| \#\>| \z ))) # Anything until the next .field or end of the comment block ", 'IgnoreCase,IgnorePatternWhitespace', [Timespan]::FromSeconds(1)).Matches( $block.Value )) { $match.Groups["Content"].Value -replace '[\s\r\n]+$' } } }), $true) } #endregion .GetHelpField #region .InheritanceLevel if (-not $extProperties['InheritanceLevel']) { $extProperties.Add([PSNoteProperty]::new('InheritanceLevel', $inheritanceLevel), $true) } #endregion .InheritanceLevel #region .DisplayName if (-not $extProperties['DisplayName']) { $extProperties.Add([PSScriptProperty]::new( 'DisplayName', { if ($this.'.DisplayName') { return $this.'.DisplayName' } if ($this.ScriptBlock.Attributes) { foreach ($attr in $this.ScriptBlock.Attributes) { if ($attr -is [ComponentModel.DisplayNameAttribute]) { $this | Add-Member NoteProperty '.DisplayName' $attr.DisplayName -Force return $attr.DisplayName } } } $this | Add-Member NoteProperty '.DisplayName' $this.Name return $this.Name }, { $this | Add-Member NoteProperty '.DisplayName' $args -Force } ), $true) $extProperties.Add([PSNoteProperty]::new( '.DisplayName', "$($extCmd.Name -replace $extensionFullRegex)" ), $true) } #endregion .DisplayName #region .Attributes if (-not $extProperties['Attributes']) { $extProperties.Add([PSScriptProperty]::new( 'Attributes', {$this.ScriptBlock.Attributes} ), $true) } #endregion .Attributes #region .Category if (-not $extProperties['Category']) { $extProperties.Add([PSScriptProperty]::new( 'Category', { foreach ($attr in $this.ScriptBlock.Attributes) { if ($attr -is [Reflection.AssemblyMetaDataAttribute] -and $attr.Key -eq 'Category') { $attr.Value } elseif ($attr -is [ComponentModel.CategoryAttribute]) { $attr.Category } } } ), $true) } #endregion .Category #region .Rank if (-not $extProperties['Rank']) { $extProperties.Add([PSScriptProperty]::new( 'Rank', { foreach ($attr in $this.ScriptBlock.Attributes) { if ($attr -is [Reflection.AssemblyMetaDataAttribute] -and $attr.Key -in 'Order', 'Rank') { return $attr.Value -as [int] } } return 0 } ), $true) } #endregion .Rank #region .Metadata if (-not $extProperties['Metadata']) { $extProperties.Add([psscriptproperty]::new( 'Metadata', { $Metadata = [Ordered]@{} foreach ($attr in $this.ScriptBlock.Attributes) { if ($attr -is [Reflection.AssemblyMetaDataAttribute]) { if ($Metadata[$attr.Key]) { $Metadata[$attr.Key] = @($Metadata[$attr.Key]) + $attr.Value } else { $Metadata[$attr.Key] = $attr.Value } } } return $Metadata } ), $true) } #endregion .Metadata #region .Description if (-not $extProperties['Description']) { $extProperties.Add([PSScriptProperty]::new( 'Description', { @($this.GetHelpField("Description"))[0] -replace '^\s+' } ), $true) } #endregion .Description #region .Synopsis if (-not $extProperties['Synopsis']) { $extProperties.Add([PSScriptProperty]::new( 'Synopsis', { @($this.GetHelpField("Synopsis"))[0] -replace '^\s+' }), $true) } #endregion .Synopsis #region .Examples if (-not $extProperties['Examples']) { $extProperties.Add([PSScriptProperty]::new( 'Examples', { $this.GetHelpField("Example") }), $true) } #endregion .Examples #region .Links if (-not $extProperties['Links']) { $extProperties.Add([PSScriptProperty]::new( 'Links', { $this.GetHelpField("Link") }), $true ) } #endregion .Links #region .Validate if (-not $extProperties['Validate']) { $extMethods.Add([psscriptmethod]::new('Validate', { param( # input being validated [PSObject]$ValidateInput, # If set, will require all [Validate] attributes to be valid. # If not set, any input will be valid. [switch]$AllValid ) foreach ($attr in $this.ScriptBlock.Attributes) { if ($attr -is [Management.Automation.ValidateScriptAttribute]) { try { $_ = $this = $psItem = $ValidateInput $isValidInput = . $attr.ScriptBlock if ($isValidInput -and -not $AllValid) { return $true} if (-not $isValidInput -and $AllValid) { if ($ErrorActionPreference -eq 'ignore') { return $false } elseif ($AllValid) { throw "'$ValidateInput' is not a valid value." } } } catch { if ($AllValid) { if ($ErrorActionPreference -eq 'ignore') { return $false } else { throw } } } } elseif ($attr -is [Management.Automation.ValidateSetAttribute]) { if ($ValidateInput -notin $attr.ValidValues) { if ($AllValid) { if ($ErrorActionPreference -eq 'ignore') { return $false } else { throw "'$ValidateInput' is not a valid value. Valid values are '$(@($attr.ValidValues) -join "','")'" } } } elseif (-not $AllValid) { return $true } } elseif ($attr -is [Management.Automation.ValidatePatternAttribute]) { $matched = [Regex]::new($attr.RegexPattern, $attr.Options, [Timespan]::FromSeconds(1)).Match("$ValidateInput") if (-not $matched.Success) { if ($allValid) { if ($ErrorActionPreference -eq 'ignore') { return $false } else { throw "'$ValidateInput' is not a valid value. Valid values must match the pattern '$($attr.RegexPattern)'" } } } elseif (-not $AllValid) { return $true } } elseif ($attr -is [Management.Automation.ValidateRangeAttribute]) { if ($null -ne $attr.MinRange -and $validateInput -lt $attr.MinRange) { if ($AllValid) { if ($ErrorActionPreference -eq 'ignore') { return $false } else { throw "'$ValidateInput' is below the minimum range [ $($attr.MinRange)-$($attr.MaxRange) ]" } } } elseif ($null -ne $attr.MaxRange -and $validateInput -gt $attr.MaxRange) { if ($AllValid) { if ($ErrorActionPreference -eq 'ignore') { return $false } else { throw "'$ValidateInput' is above the maximum range [ $($attr.MinRange)-$($attr.MaxRange) ]" } } } elseif (-not $AllValid) { return $true } } } if ($AllValid) { return $true } else { return $false } }), $true) } #endregion .Validate #region .HasValidation if (-not $extProperties['HasValidation']) { $extProperties.Add([psscriptproperty]::new('HasValidation', { foreach ($attr in $this.ScriptBlock.Attributes) { if ($attr -is [Management.Automation.ValidateScriptAttribute] -or $attr -is [Management.Automation.ValidateSetAttribute] -or $attr -is [Management.Automation.ValidatePatternAttribute] -or $attr -is [Management.Automation.ValidateRangeAttribute]) { return $true } } return $false }), $true) } #endregion .HasValidation #region .GetDynamicParameters if (-not $extMethods['GetDynamicParameters']) { $extMethods.Add([PSScriptMethod]::new('GetDynamicParameters', { param( [string] $ParameterSetName, [int] $PositionOffset, [switch] $NoMandatory, [string[]] $commandList ) $ExtensionDynamicParameters = [Management.Automation.RuntimeDefinedParameterDictionary]::new() $Extension = $this $ExtensionMetadata = $Extension -as [Management.Automation.CommandMetaData] if (-not $ExtensionMetadata) { return $ExtensionDynamicParameters } :nextDynamicParameter foreach ($in in @(($Extension -as [Management.Automation.CommandMetaData]).Parameters.Keys)) { $attrList = [Collections.Generic.List[Attribute]]::new() $validCommandNames = @() foreach ($attr in $extension.Parameters[$in].attributes) { if ($attr -isnot [Management.Automation.ParameterAttribute]) { # we can passthru any non-parameter attributes $attrList.Add($attr) if ($attr -is [Management.Automation.CmdletAttribute] -and $commandList) { $validCommandNames += ( ($attr.VerbName -replace '\s') + '-' + ($attr.NounName -replace '\s') ) -replace '^\-' -replace '\-$' } } else { # but parameter attributes need to copied. $attrCopy = [Management.Automation.ParameterAttribute]::new() # (Side note: without a .Clone, copying is tedious.) foreach ($prop in $attrCopy.GetType().GetProperties('Instance,Public')) { if (-not $prop.CanWrite) { continue } if ($null -ne $attr.($prop.Name)) { $attrCopy.($prop.Name) = $attr.($prop.Name) } } $attrCopy.ParameterSetName = if ($ParameterSetName) { $ParameterSetName } else { $defaultParamSetName = foreach ($extAttr in $Extension.ScriptBlock.Attributes) { if ($extAttr.DefaultParameterSetName) { $extAttr.DefaultParameterSetName break } } if ($attrCopy.ParameterSetName -ne '__AllParameterSets') { $attrCopy.ParameterSetName } elseif ($defaultParamSetName) { $defaultParamSetName } elseif ($this -is [Management.Automation.FunctionInfo]) { $this.Name } elseif ($this -is [Management.Automation.ExternalScriptInfo]) { $this.Source } } if ($NoMandatory -and $attrCopy.Mandatory) { $attrCopy.Mandatory = $false } if ($PositionOffset -and $attr.Position -ge 0) { $attrCopy.Position += $PositionOffset } $attrList.Add($attrCopy) } } if ($commandList -and $validCommandNames) { :CheckCommandValidity do { foreach ($vc in $validCommandNames) { if ($commandList -match $vc) { break CheckCommandValidity } } continue nextDynamicParameter } while ($false) } $ExtensionDynamicParameters.Add($in, [Management.Automation.RuntimeDefinedParameter]::new( $Extension.Parameters[$in].Name, $Extension.Parameters[$in].ParameterType, $attrList )) } $ExtensionDynamicParameters }), $true) } #endregion .GetDynamicParameters #region .IsParameterValid if (-not $extMethods['IsParameterValid']) { $extMethods.Add([PSScriptMethod]::new('IsParameterValid', { param([Parameter(Mandatory)]$ParameterName, [PSObject]$Value) if ($this.Parameters.Count -ge 0 -and $this.Parameters[$parameterName].Attributes ) { foreach ($attr in $this.Parameters[$parameterName].Attributes) { $_ = $value if ($attr -is [Management.Automation.ValidateScriptAttribute]) { $result = try { . $attr.ScriptBlock } catch { $null } if ($result -ne $true) { return $false } } elseif ($attr -is [Management.Automation.ValidatePatternAttribute] -and (-not [Regex]::new($attr.RegexPattern, $attr.Options, '00:00:05').IsMatch($value)) ) { return $false } elseif ($attr -is [Management.Automation.ValidateSetAttribute] -and $attr.ValidValues -notcontains $value) { return $false } elseif ($attr -is [Management.Automation.ValidateRangeAttribute] -and ( ($value -gt $attr.MaxRange) -or ($value -lt $attr.MinRange) )) { return $false } } } return $true }), $true) } #endregion .IsParameterValid #region .CouldPipe if (-not $extMethods['CouldPipe']) { $extMethods.Add([PSScriptMethod]::new('CouldPipe', { param([PSObject]$InputObject) :nextParameterSet foreach ($paramSet in $this.ParameterSets) { if ($ParameterSetName -and $paramSet.Name -ne $ParameterSetName) { continue } $params = @{} $mappedParams = [Ordered]@{} # Create a collection of mapped parameters # Walk thru each parameter of this command :nextParameter foreach ($myParam in $paramSet.Parameters) { # If the parameter is ValueFromPipeline if ($myParam.ValueFromPipeline) { $potentialPSTypeNames = @($myParam.Attributes.PSTypeName) -ne '' if ($potentialPSTypeNames) { foreach ($potentialTypeName in $potentialPSTypeNames) { if ($potentialTypeName -and $InputObject.pstypenames -contains $potentialTypeName) { $mappedParams[$myParam.Name] = $params[$myParam.Name] = $InputObject continue nextParameter } } } # and we have an input object elseif ($null -ne $inputObject -and ( # of the exact type $myParam.ParameterType -eq $inputObject.GetType() -or # (or a subclass of that type) $inputObject.GetType().IsSubClassOf($myParam.ParameterType) -or # (or an inteface of that type) ($myParam.ParameterType.IsInterface -and $InputObject.GetType().GetInterface($myParam.ParameterType)) ) ) { # then map the parameter. $mappedParams[$myParam.Name] = $params[$myParam.Name] = $InputObject } } } # Check for parameter validity. foreach ($mappedParamName in @($mappedParams.Keys)) { if (-not $this.IsParameterValid($mappedParamName, $mappedParams[$mappedParamName])) { $mappedParams.Remove($mappedParamName) } } if ($mappedParams.Count -gt 0) { return $mappedParams } } }), $true) } #endregion .CouldPipe #region .CouldPipeType if (-not $extMethods['CouldPipeType']) { $extMethods.Add([PSScriptMethod]::new('CouldPipeType', { param([Type]$Type) foreach ($paramSet in $this.ParameterSets) { if ($ParameterSetName -and $paramSet.Name -ne $ParameterSetName) { continue } # Walk thru each parameter of this command foreach ($myParam in $paramSet.Parameters) { # If the parameter is ValueFromPipeline if ($myParam.ValueFromPipeline -and ( $myParam.ParameterType -eq $Type -or # (or a subclass of that type) $Type.IsSubClassOf($myParam.ParameterType) -or # (or an inteface of that type) ($myParam.ParameterType.IsInterface -and $Type.GetInterface($myParam.ParameterType)) ) ) { return $true } } return $false } }), $true) } #endregion .CouldPipeType #region .CouldRun if (-not $extMethods['CouldRun']) { $extMethods.Add([PSScriptMethod]::new('CouldRun', { param([Collections.IDictionary]$params, [string]$ParameterSetName) :nextParameterSet foreach ($paramSet in $this.ParameterSets) { if ($ParameterSetName -and $paramSet.Name -ne $ParameterSetName) { continue } $mappedParams = [Ordered]@{} # Create a collection of mapped parameters $mandatories = # Walk thru each parameter of this command @(foreach ($myParam in $paramSet.Parameters) { if ($params.Contains($myParam.Name)) { # If this was in Params, $mappedParams[$myParam.Name] = $params[$myParam.Name] # then map it. } else { foreach ($paramAlias in $myParam.Aliases) { # Otherwise, check the aliases if ($params.Contains($paramAlias)) { # and map it if the parameters had the alias. $mappedParams[$myParam.Name] = $params[$paramAlias] break } } } if ($myParam.IsMandatory) { # If the parameter was mandatory, $myParam.Name # keep track of it. } }) # Check for parameter validity. foreach ($mappedParamName in @($mappedParams.Keys)) { if (-not $this.IsParameterValid($mappedParamName, $mappedParams[$mappedParamName])) { $mappedParams.Remove($mappedParamName) } } foreach ($mandatoryParam in $mandatories) { # Walk thru each mandatory parameter. if (-not $mappedParams.Contains($mandatoryParam)) { # If it wasn't in the parameters. continue nextParameterSet } } return $mappedParams } return $false }), $true) } #endregion .CouldRun # Decorate our return (so that it can be uniquely extended) if (-not $ExtensionTypeName) { $ExtensionTypeName = 'Extension' } if ($extCmd.pstypenames -notcontains $ExtensionTypeName) { $extCmd.pstypenames.insert(0,$ExtensionTypeName) } $extCmd } } function OutputExtension { begin { $allDynamicParameters = [Management.Automation.RuntimeDefinedParameterDictionary]::new() } process { $extCmd = $_ # When we're outputting an extension, we start off assuming that it is valid. $IsValid = $true if ($ValidateInput) { # If we have a particular input we want to validate try { # Check if it is valid if (-not $extCmd.Validate($ValidateInput, $AllValid)) { $IsValid = $false # and then set IsValid if it is not. } } catch { Write-Error $_ # If we encountered an exception, write it out $IsValid = $false # and set is $IsValid to false. } } # If we're requesting dynamic parameters (and the extension is valid) if ($IsValid -and ($DynamicParameter -or $DynamicParameterSetName -or $DynamicParameterPositionOffset -or $NoMandatoryDynamicParameter)) { # Get what the dynamic parameters of the extension would be. $extensionParams = $extCmd.GetDynamicParameters($DynamicParameterSetName, $DynamicParameterPositionOffset, $NoMandatoryDynamicParameter, $CommandName) # Then, walk over each extension parameter. foreach ($kv in $extensionParams.GetEnumerator()) { # If the $CommandExtended had a built-in parameter, we cannot override it, so skip it. if ($commandExtended -and ($commandExtended -as [Management.Automation.CommandMetaData]).Parameters.$($kv.Key)) { continue } # If already have this dynamic parameter if ($allDynamicParameters.ContainsKey($kv.Key)) { # check it's type. if ($kv.Value.ParameterType -ne $allDynamicParameters[$kv.Key].ParameterType) { # If the types are different, make it a PSObject (so it could be either). Write-Verbose "Extension '$extCmd' Parameter '$($kv.Key)' Type Conflict, making type PSObject" $allDynamicParameters[$kv.Key].ParameterType = [PSObject] } foreach ($attr in $kv.Value.Attributes) { if ($allDynamicParameters[$kv.Key].Attributes.Contains($attr)) { continue } $allDynamicParameters[$kv.Key].Attributes.Add($attr) } } else { $allDynamicParameters[$kv.Key] = $kv.Value } } } elseif ($IsValid -and ($CouldPipe -or $CouldRun)) { if (-not $extCmd) { return } $extensionParams = [Ordered]@{} $pipelineParams = @() if ($CouldPipe) { $couldPipeExt = $extCmd.CouldPipe($CouldPipe) if (-not $couldPipeExt) { return } $pipelineParams += $couldPipeExt.Keys if (-not $CouldRun) { $extensionParams += $couldPipeExt } else { foreach ($kv in $couldPipeExt.GetEnumerator()) { $Parameter[$kv.Key] = $kv.Value } } } if ($CouldRun) { $couldRunExt = $extCmd.CouldRun($Parameter, $ParameterSetName) if (-not $couldRunExt) { return } $extensionParams += $couldRunExt } [PSCustomObject][Ordered]@{ ExtensionCommand = $extCmd CommandName = $CommandName ExtensionInputObject = if ($CouldPipe) { $CouldPipe } else { $null } ExtensionParameter = $extensionParams PipelineParameters = $pipelineParams } } elseif ($IsValid -and $SteppablePipeline) { if (-not $extCmd) { return } if ($Parameter) { $couldRunExt = $extCmd.CouldRun($Parameter, $ParameterSetName) if (-not $couldRunExt) { $sb = {& $extCmd } $sb.GetSteppablePipeline() | Add-Member NoteProperty ExtensionCommand $extCmd -Force -PassThru | Add-Member NoteProperty ExtensionParameters $couldRunExt -Force -PassThru | Add-Member NoteProperty ExtensionScriptBlock $sb -Force -PassThru } else { $sb = {& $extCmd @couldRunExt} $sb.GetSteppablePipeline() | Add-Member NoteProperty ExtensionCommand $extCmd -Force -PassThru | Add-Member NoteProperty ExtensionParameters $couldRunExt -Force -PassThru | Add-Member NoteProperty ExtensionScriptBlock $sb -Force -PassThru } } else { $sb = {& $extCmd } $sb.GetSteppablePipeline() | Add-Member NoteProperty ExtensionCommand $extCmd -Force -PassThru | Add-Member NoteProperty ExtensionParameters @{} -Force -PassThru | Add-Member NoteProperty ExtensionScriptBlock $sb -Force -PassThru } } elseif ($IsValid -and $Run) { if (-not $extCmd) { return } $couldRunExt = $extCmd.CouldRun($Parameter, $ParameterSetName) if (-not $couldRunExt) { return } if ($extCmd.InheritanceLevel -eq 'InheritedReadOnly') { return } if ($Stream) { & $extCmd @couldRunExt } else { [PSCustomObject][Ordered]@{ CommandName = $CommandName ExtensionCommand = $extCmd ExtensionOutput = & $extCmd @couldRunExt Done = $extCmd.InheritanceLevel -eq 'NotInherited' } } return } elseif ($IsValid -and $Help) { $getHelpSplat = @{Full=$true} if ($extCmd -is [Management.Automation.ExternalScriptInfo]) { Get-Help $extCmd.Source @getHelpSplat } elseif ($extCmd -is [Management.Automation.FunctionInfo]) { Get-Help $extCmd @getHelpSplat } } elseif ($IsValid) { return $extCmd } } end { if ($DynamicParameter) { return $allDynamicParameters } } } #endregion Define Inner Functions $extensionFullRegex = [Regex]::New($( if ($ExtensionModule) { "\.(?>$(@(@($ExtensionModule) + $ExtensionModuleAlias) -join '|'))\." + "(?>$($ExtensionPattern -join '|'))" } else { "(?>$($ExtensionPattern -join '|'))" } ), 'IgnoreCase,IgnorePatternWhitespace', '00:00:01') #region Find Extensions $loadedModules = @(Get-Module) $myInv = $MyInvocation $myModuleName = if ($ExtensionModule) { $ExtensionModule } else { $MyInvocation.MyCommand.Module.Name } if ($myInv.MyCommand.Module -and $loadedModules -notcontains $myInv.MyCommand.Module) { $loadedModules = @($myInv.MyCommand.Module) + $loadedModules } $getCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand if ($Force) { $script:RoughDraftExtensions = $null $script:RoughDraftExtensionsByName = $null $script:AllCommands = @() } if (-not $script:RoughDraftExtensions) { $script:RoughDraftExtensionsFromFiles = [Ordered]@{} $script:RoughDraftExtensionsFileTimes = [Ordered]@{} $script:RoughDraftExtensionsByName = [Ordered]@{} $script:RoughDraftExtensionsByDisplayName = [Ordered]@{} $script:RoughDraftExtensionsByPattern = [Ordered]@{} $script:RoughDraftExtensions = @(@( #region Find RoughDraftExtensions in Loaded Modules foreach ($loadedModule in $loadedModules) { # Walk over all modules. if ( # If the module has PrivateData keyed to this module $loadedModule.PrivateData.$myModuleName ) { # Determine the root of the module with private data. $thisModuleRoot = [IO.Path]::GetDirectoryName($loadedModule.Path) # and get the extension data $extensionData = $loadedModule.PrivateData.$myModuleName if ($extensionData -is [Hashtable]) { # If it was a hashtable foreach ($ed in $extensionData.GetEnumerator()) { # walk each key $extensionCmd = if ($ed.Value -like '*.ps1') { # If the key was a .ps1 file $getCmd.Invoke( # treat it as a relative path to the .ps1 [IO.Path]::Combine($thisModuleRoot, $ed.Value), 'ExternalScript' ) } else { # Otherwise, treat it as the name of an exported command. $loadedModule.ExportedCommands[$ed.Value] } if ($extensionCmd) { # If we've found a valid extension command ConvertToExtension $extensionCmd # return it as an extension. } } } } elseif ($loadedModule.PrivateData.PSData.Tags -contains $myModuleName -or $loadedModule.Name -eq $myModuleName) { $loadedModuleRoot = Split-Path $loadedModule.Path if ($loadedModuleRoot) { foreach ($fileInModule in Get-ChildItem -Path $loadedModuleRoot -Recurse -File -Filter *.ps1) { if ($fileInModule.Name -notmatch $extensionFullRegex) { continue } ConvertToExtension $fileInModule } } } } #endregion Find RoughDraftExtensions in Loaded Modules #region Find RoughDraftExtensions in Loaded Commands $ExecutionContext.SessionState.InvokeCommand.GetCommands('*', 'Function,Alias',$true) -match $extensionFullRegex #endregion Find RoughDraftExtensions in Loaded Commands ) | Select-Object -Unique | Sort-Object Rank, Name) foreach ($extCmd in $script:RoughDraftExtensions) { if (-not $script:RoughDraftExtensionsByName[$extCmd.Name]) { $script:RoughDraftExtensionsByName[$extCmd.Name] = $extCmd } else { $script:RoughDraftExtensionsByName[$extCmd.Name] = @($script:RoughDraftExtensionsByName[$extCmd.Name]) + $extCmd } if ($extCmd.DisplayName) { if (-not $script:RoughDraftExtensionsByDisplayName[$extCmd.DisplayName]) { $script:RoughDraftExtensionsByDisplayName[$extCmd.DisplayName] = $extCmd } else { $script:RoughDraftExtensionsByDisplayName[$extCmd.DisplayName] = @($script:RoughDraftExtensionsByDisplayName[$extCmd.DisplayName]) + $extCmd } } $ExtensionCommandAliases = @($extCmd.Attributes.AliasNames) $ExtensionCommandAliasRegexes = @($ExtensionCommandAliases -match '^/' -match '/$') $ExtensionCommandNormalAliases = @($ExtensionCommandAliases -notmatch '^/') if ($ExtensionCommandAliasRegexes) { foreach ($extensionAliasRegex in $ExtensionCommandAliasRegexes) { $regex = [Regex]::New($extensionAliasRegex -replace '^/' -replace '/$', 'IgnoreCase,IgnorePatternWhitespace') if (-not $script:RoughDraftExtensionsByPattern[$regex]) { $script:RoughDraftExtensionsByPattern[$regex] = $extCmd } else { $script:RoughDraftExtensionsByPattern[$regex] = @($script:RoughDraftExtensionsByPattern[$regex]) + $extCmd } } } if ($ExtensionCommandNormalAliases) { foreach ($extensionAlias in $ExtensionCommandNormalAliases) { if (-not $script:RoughDraftExtensionsByName[$extensionAlias]) { $script:RoughDraftExtensionsByName[$extensionAlias] = $extCmd } else { $script:RoughDraftExtensionsByName[$extensionAlias] = @($script:RoughDraftExtensionsByName[$extensionAlias]) + $extCmd } } } } } #endregion Find Extensions } process { if ($ExtensionPath) { @(foreach ($_ in Get-ChildItem -Recurse:$($ExtensionPath -notmatch '^\.[\\/]') -Path $ExtensionPath -File) { if ($_.Name -notmatch $extensionFullRegex) { continue } if ($CommandName -or $ExtensionName) { ConvertToExtension $_ | . WhereExtends $CommandName } else { ConvertToExtension $_ } }) | #region Install-Piecemeal -WhereObject # This section can be updated by using Install-Piecemeal -WhereObject #endregion Install-Piecemeal -WhereObject Sort-Object Rank, Name | OutputExtension #region Install-Piecemeal -ForeachObject # This section can be updated by using Install-Piecemeal -ForeachObject #endregion Install-Piecemeal -ForeachObject } elseif ($CommandName -or $ExtensionName) { if (-not $CommandName -and -not $like -and -not $Match) { foreach ($exn in $ExtensionName) { if ($script:RoughDraftExtensionsByName[$exn]) { $script:RoughDraftExtensionsByName[$exn] | OutputExtension } if ($script:RoughDraftExtensionsByDisplayName[$exn]) { $script:RoughDraftExtensionsByDisplayName[$exn] | OutputExtension } if ($script:RoughDraftExtensionsByPattern.Count) { foreach ($patternAndValue in $script:RoughDraftExtensionsByPattern.GetEnumerator()) { if ($patternAndValue.Key.IsMatch($exn)) { $patternAndValue.Value | OutputExtension } } $script:RoughDraftExtensionsByDisplayName[$exn] } } } else { $script:RoughDraftExtensions | . WhereExtends $CommandName | OutputExtension } } else { $script:RoughDraftExtensions | OutputExtension } } } #endregion Piecemeal [ 0.4.1 ] : Easy Extensible Plugins for PowerShell |