New-PipeScript.ps1
function New-PipeScript { <# .Synopsis Creates new PipeScript. .Description Creates new PipeScript and PowerShell ScriptBlocks. .EXAMPLE New-PipeScript -Parameter @{a='b'} .EXAMPLE New-PipeScript -Parameter ([Net.HttpWebRequest].GetProperties()) -ParameterHelp @{ Accept=' HTTP Accept. HTTP Accept indicates what content types the web request will accept as a response. ' } .EXAMPLE New-PipeScript -Parameter @{"bar"=@{ Name = "foo" Help = 'Foobar' Attributes = "Mandatory","ValueFromPipelineByPropertyName" Aliases = "fubar" Type = "string" }} #> [Alias('New-ScriptBlock')] param( # Defines one or more parameters for a ScriptBlock. # Parameters can be defined in a few ways: # * As a ```[Collections.Dictionary]``` of Parameters # * As the ```[string]``` name of an untyped parameter. # * As a ```[ScriptBlock]``` containing only parameters. [Parameter(ValueFromPipelineByPropertyName)] [ValidateScript({ if ($_ -isnot [ScriptBlock]) { return $true } $statementCount = 0 $statementCount += $_.Ast.DynamicParamBlock.Statements.Count $statementCount += $_.Ast.BeginBlock.Statements.Count $statementCount += $_.Ast.ProcessBlock.Statements.Count $statementCount += $_.Ast.EndBlock.Statements.Count if ($statementCount) { throw "ScriptBlock should have no statements" } else { return $true } })] [ValidateScript({ $validTypeList = [System.Collections.IDictionary],[System.String],[System.Object[]],[System.Management.Automation.ScriptBlock],[System.Reflection.PropertyInfo],[System.Reflection.PropertyInfo[]],[System.Reflection.ParameterInfo],[System.Reflection.ParameterInfo[]],[System.Reflection.MethodInfo],[System.Reflection.MethodInfo[]] $thisType = $_.GetType() $IsTypeOk = $(@( foreach ($validType in $validTypeList) { if ($_ -as $validType) { $true;break } })) if (-not $isTypeOk) { throw "Unexpected type '$(@($thisType)[0])'. Must be 'System.Collections.IDictionary','string','System.Object[]','scriptblock','System.Reflection.PropertyInfo','System.Reflection.PropertyInfo[]','System.Reflection.ParameterInfo','System.Reflection.ParameterInfo[]','System.Reflection.MethodInfo','System.Reflection.MethodInfo[]'." } return $true })] $Parameter, # The dynamic parameter block. [Parameter(ValueFromPipelineByPropertyName)] [ValidateScript({ if ($_ -isnot [ScriptBlock]) { return $true } if ($_.Ast.DynamicParamBlock -or $_.Ast.BeginBlock -or $_.Ast.ProcessBlock) { throw "ScriptBlock should not have any named blocks" } return $true })] [ValidateScript({ if ($_ -isnot [ScriptBlock]) { return $true } if ($_.Ast.ParamBlock.Parameters.Count) { throw "ScriptBlock should not have parameters" } return $true })] [Alias('DynamicParameterBlock')] [ScriptBlock] $DynamicParameter, # The begin block. [Parameter(ValueFromPipelineByPropertyName)] [ValidateScript({ if ($_ -isnot [ScriptBlock]) { return $true } if ($_.Ast.DynamicParamBlock -or $_.Ast.BeginBlock -or $_.Ast.ProcessBlock) { throw "ScriptBlock should not have any named blocks" } return $true })] [ValidateScript({ if ($_ -isnot [ScriptBlock]) { return $true } if ($_.Ast.ParamBlock.Parameters.Count) { throw "ScriptBlock should not have parameters" } return $true })] [Alias('BeginBlock')] [ScriptBlock] $Begin, # The process block. [Parameter(ValueFromPipelineByPropertyName)] [ValidateScript({ if ($_ -isnot [ScriptBlock]) { return $true } if ($_.Ast.DynamicParamBlock -or $_.Ast.BeginBlock -or $_.Ast.ProcessBlock) { throw "ScriptBlock should not have any named blocks" } return $true })] [ValidateScript({ if ($_ -isnot [ScriptBlock]) { return $true } if ($_.Ast.ParamBlock.Parameters.Count) { throw "ScriptBlock should not have parameters" } return $true })] [Alias('ProcessBlock')] [ScriptBlock] $Process, # The end block. [Parameter(ValueFromPipelineByPropertyName)] [ValidateScript({ if ($_ -isnot [ScriptBlock]) { return $true } if ($_.Ast.DynamicParamBlock -or $_.Ast.BeginBlock -or $_.Ast.ProcessBlock) { throw "ScriptBlock should not have any named blocks" } return $true })] [ValidateScript({ if ($_ -isnot [ScriptBlock]) { return $true } if ($_.Ast.ParamBlock.Parameters.Count) { throw "ScriptBlock should not have parameters" } return $true })] [Alias('EndBlock')] [ScriptBlock] $End, # The script header. [Parameter(ValueFromPipelineByPropertyName)] [string] $Header, # If provided, will automatically create parameters. # Parameters will be automatically created for any unassigned variables. [Alias('AutoParameterize','AutoParameters')] [switch] $AutoParameter, # The type used for automatically generated parameters. # By default, ```[PSObject]```. [type] $AutoParameterType = [PSObject], # If provided, will add inline help to parameters. [Collections.IDictionary] $ParameterHelp, <# If set, will weakly type parameters generated by reflection. 1. Any parameter type implements IList should be made a [PSObject[]] 2. Any parameter that implements IDictionary should be made an [Collections.IDictionary] 3. Booleans should be made into [switch]es 4. All other parameter types should be [PSObject] #> [Alias('WeakType', 'WeakParameters', 'WeaklyTypedParameters', 'WeakProperties', 'WeaklyTypedProperties')] [switch] $WeaklyTyped, # The name of the function to create. [string] $FunctionName, # The type or namespace the function to create. This will be ignored if -FunctionName is not passed. # If the function type is not function or filter, it will be treated as a function namespace. [Alias('FunctionNamespace','CommandNamespace')] [string] $FunctionType = 'function', # A description of the script's functionality. If provided with -Synopsis, will generate help. [string] $Description, # A short synopsis of the script's functionality. If provided with -Description, will generate help. [string] $Synopsis, # A list of examples to use in help. Will be ignored if -Synopsis and -Description are not passed. [Alias('Examples')] [string[]] $Example, # A list of links to use in help. Will be ignored if -Synopsis and -Description are not passed. [Alias('Links')] [string[]] $Link, # A list of attributes to declare on the scriptblock. [string[]] $Attribute, # If set, will not transpile the created code. [switch] $NoTranspile ) begin { $ParametersToCreate = [Ordered]@{} $parameterScriptBlocks = @() $allDynamicParameters = @() $allBeginBlocks = @() $allEndBlocks = @() $allProcessBlocks = @() $allHeaders = @() filter embedParameterHelp { if ($_ -notmatch '^\s\<\#' -and $_ -notmatch '^\s\#') { $commentLines = @($_ -split '(?>\r\n|\n)') if ($commentLines.Count -gt 1) { '<#' + [Environment]::NewLine + "$_".Trim() + [Environment]::newLine + '#>' } else { "# $_" } } else { $_ } } } process { if ($Synopsis -and $Description) { function indentHelpLine { foreach ($line in $args -split '(?>\r\n|\n)') { (' ' * 4) + $line.TrimEnd() } } $helpHeader = @( "<#" ".Synopsis" indentHelpLine $Synopsis ".Description" indentHelpLine $Description foreach ($helpExample in $Example) { ".Example" indentHelpLine $helpExample } foreach ($helplink in $Link) { ".Link" indentHelpLine $helplink } "#>" ) -join [Environment]::Newline $allHeaders += $helpHeader } if ($Attribute) { $allHeaders += $Attribute } # If -Parameter was passed, we will need to define parameters. if ($parameter) { # this will end up populating an [Ordered] dictionary, $parametersToCreate. # However, for ease of use, -Parameter can be very flexible. # The -Parameter can be a dictionary of parameters. if ($Parameter -is [Collections.IDictionary]) { $parameterType = '' # If it is, walk thur each parameter in the dictionary foreach ($EachParameter in $Parameter.GetEnumerator()) { # Continue past any parameters we already have if ($ParametersToCreate.Contains($EachParameter.Key)) { continue } # If the parameter is a string and the value is not a variable if ($EachParameter.Value -is [string] -and $EachParameter.Value -notlike '*$*') { $parameterName = $EachParameter.Key $ParametersToCreate[$EachParameter.Key] = @( if ($parameterHelp -and $parameterHelp[$eachParameter.Key]) { $parameterHelp[$eachParameter.Key] | embedParameterHelp } $parameterAttribute = "[Parameter(ValueFromPipelineByPropertyName)]" $parameterType '$' + $parameterName ) -ne '' } # If the value is a string and the value contains variables elseif ($EachParameter.Value -is [string]) { # embed it directly. $ParametersToCreate[$EachParameter.Key] = $EachParameter.Value } # If the value is a ScriptBlock elseif ($EachParameter.Value -is [ScriptBlock]) { # Embed it $ParametersToCreate[$EachParameter.Key] = # If there was a param block on the script block if ($EachParameter.Value.Ast.ParamBlock) { # embed the parameter block (except for the param keyword) $EachParameter.Value.Ast.ParamBlock.Extent.ToString() -replace '^[\s\r\n]{0,}param\(' -replace '\)[\s\r\n]{0,}$' } else { # Otherwise '[Parameter(ValueFromPipelineByPropertyName)]' + ( $EachParameter.Value.ToString() -replace "\`$$($eachParameter.Key)[\s\r\n]$" -replace # Replace any trailing variables 'param\(\)[\s\r\n]{0,}$' # then replace any empty param blocks. ) } } # If the value was an array elseif ($EachParameter.Value -is [Object[]]) { $ParametersToCreate[$EachParameter.Key] = # join it's elements by newlines $EachParameter.Value -join [Environment]::Newline } elseif ($EachParameter.Value -is [Collections.IDictionary] -or $EachParameter.Value -is [PSObject]) { $parameterMetadata = $EachParameter.Value $parameterName = $EachParameter.Key if ($parameterMetadata.Name) { $parameterName = $parameterMetadata.Name } $parameterAttributeParts = @() $ParameterOtherAttributes = @() $attrs = if ($parameterMetadata.Attributes) { $parameterMetadata.Attributes } elseif ($parameterMetadata.Attribute) { $parameterMetadata.Attribute } $aliases = if ($parameterMetadata.Alias) { $parameterMetadata.Alias } elseif ($parameterMetadata.Aliases) { $parameterMetadata.Aliases } $parameterHelp = if ($parameterMetadata.Help) { $parameterMetadata.Help} $aliasAttribute = @(foreach ($alias in $aliases) { $alias -replace "'","''" }) -join "','" if ($aliasAttribute) { $aliasAttribute = "[Alias('$aliasAttribute')]" } foreach ($attr in $attrs) { if ($attr -notmatch '^\[') { $parameterAttributeParts += $attr } else { $ParameterOtherAttributes += $attr } } $parameterType = if ($parameterMetadata.Type) {$parameterMetadata.Type } elseif ($parameterMetadata.ParameterType) {$parameterMetadata.ParameterType } $ParametersToCreate[$parameterName] = @( if ($ParameterHelp) { $ParameterHelp | embedParameterHelp } if ($parameterAttributeParts) { "[Parameter($($parameterAttributeParts -join ','))]" } if ($aliasAttribute) { $aliasAttribute } if ($parameterType -as [type]) { "[$(($parameterType -as [type]).FullName -replace '^System\.')]" } elseif ($parameterType) { "[PSTypeName('$($parameterType -replace '^System\.')')]" } if ($ParameterOtherAttributes) { $ParameterOtherAttributes } '$' + ($parameterName -replace '^$') ) -join [Environment]::newLine } } } # If the parameter was a string elseif ($Parameter -is [string]) { # treat it as parameter name $ParametersToCreate[$Parameter] = @( if ($parameterHelp -and $parameterHelp[$Parameter]) { $parameterHelp[$Parameter] | embedParameterHelp } "[Parameter(ValueFromPipelineByPropertyName)]" "`$$Parameter" ) -join [Environment]::NewLine } # If the parameter is a [ScriptBlock] elseif ($parameter -is [scriptblock]) { # add it to a list of parameter script blocks. $parameterScriptBlocks += if ($parameter.Ast.ParamBlock) { $parameter } } # If the -Parameter was provided via reflection elseif ($parameter -is [Reflection.PropertyInfo] -or $parameter -as [Reflection.PropertyInfo[]] -or $parameter -is [Reflection.ParameterInfo] -or $parameter -as [Reflection.ParameterInfo[]] -or $parameter -is [Reflection.MethodInfo] -or $parameter -as [Reflection.MethodInfo[]] ) { # check to see if it's a method if ($parameter -is [Reflection.MethodInfo] -or $parameter -as [Reflection.MethodInfo[]]) { $parameter = @(foreach ($methodInfo in $parameter) { $methodInfo.GetParameters() # if so, reflect the method's parameters }) } # Walk over each parameter foreach ($prop in $Parameter) { # If it is a property info that cannot be written, skip. if ($prop -is [Reflection.PropertyInfo] -and -not $prop.CanWrite) { continue } # Determine the reflected parameter type. $paramType = if ($prop.ParameterType) { $prop.ParameterType } elseif ($prop.PropertyType) { $prop.PropertyType } else { [PSObject] } $ParametersToCreate[$prop.Name] = @( if ($parameterHelp -and $parameterHelp[$prop.Name]) { $parameterHelp[$prop.Name] | embedParameterHelp } $parameterAttribute = "[Parameter(ValueFromPipelineByPropertyName)]" $parameterAttribute if ($paramType -eq [boolean]) { "[switch]" } elseif ($WeaklyTyped) { if ($paramType.GetInterface([Collections.IDictionary])) { "[Collections.IDictionary]" } elseif ($paramType.GetInterface([Collections.IList])) { "[PSObject[]]" } else { "[PSObject]" } } else { "[$($paramType -replace '^System\.')]" } '$' + $prop.Name ) -ne '' } } } # If there is header content, if ($header) { $allHeaders += $Header } # dynamic parameters, if ($DynamicParameter) { $allDynamicParameters += $DynamicParameter } # begin, if ($Begin) { $allBeginBlocks += $begin } # process, if ($process) { $allProcessBlocks += $process } # or end blocks. if ($end) { # accumulate them. $allEndBlocks += $end } # If -AutoParameter was passed if ($AutoParameter) { # Find all of the variable expressions within -Begin, -Process, and -End $variableDefinitions = $Begin, $Process, $End | Where-Object { $_ } | Search-PipeScript -AstType VariableExpressionAST | Select-Object -ExpandProperty Result foreach ($var in $variableDefinitions) { # Then, see where those variables were assigned $assigned = $var.GetAssignments() # (if they were assigned, keep moving) if ($assigned) { continue } # If there were not assigned $varName = $var.VariablePath.userPath.ToString() # add it to the list of parameters to create. $ParametersToCreate[$varName] = @( @( "[Parameter(ValueFromPipelineByPropertyName)]" "[$($AutoParameterType.FullName -replace '^System\.')]" "$var" ) -join [Environment]::NewLine ) } } } end { # Take all of the accumulated parameters and create a parameter block $newParamBlock = "param(" + [Environment]::newLine + $(@(foreach ($toCreate in $ParametersToCreate.GetEnumerator()) { $toCreate.Value -join [Environment]::NewLine }) -join (',' + [Environment]::NewLine)) + [Environment]::NewLine + ')' # If any parameters were passed in as ```[ScriptBlock]```s, if ($parameterScriptBlocks) { $parameterScriptBlocks += [ScriptBlock]::Create($newParamBlock) # join them with the new parameter block. $newParamBlock = $parameterScriptBlocks | Join-PipeScript } # If we provided a -FunctionName, we'll be declaring a function. $functionDeclaration = # If the -FunctionType is function or filter if ($functionName -and $functionType -in 'function', 'filter') { # we declare it naturally. "$functionType $FunctionName {" } elseif ($FunctionName) { # Otherwise, we declare it as a command namespace "$functionType function $functionName {" # (which means we have to transpile). $NoTranspile = $false } # Create the script block by combining together the provided parts. $createdScriptBlock = [scriptblock]::Create("$(if ($functionDeclaration) { "$functionDeclaration"}) $($allHeaders -join [Environment]::Newline) $newParamBlock $(if ($allDynamicParameters) { @(@("dynamicParam {") + $allDynamicParameters + '}') -join [Environment]::Newline }) $(if ($allBeginBlocks) { @(@("begin {") + $allBeginBlocks + '}') -join [Environment]::Newline }) $(if ($allProcessBlocks) { @(@("process {") + $allProcessBlocks + '}') -join [Environment]::Newline }) $(if ($allEndBlocks -and -not $allBeginBlocks -and -not $allProcessBlocks) { $allEndBlocks -join [Environment]::Newline } elseif ($allEndBlocks) { @(@("end {") + $allEndBlocks + '}') -join [Environment]::Newline }) $(if ($functionDeclaration) { '}'}) ") # If -NoTranspile was passed, if ($createdScriptBlock -and $NoTranspile) { $createdScriptBlock # output the script as-is } elseif ($createdScriptBlock) { # otherwise $createdScriptBlock | .>PipeScript # output the transpiled script. } } } |