New-Extension.ps1

function New-Extension
{
    <#
    .Synopsis
        Creates an Extension
    .Description
        Creates an Extension
    .Example
        New-Extension -ExtensionName MyExtension -CommandName New-Extension -ScriptBlock {
            [ValidateScript({return $true})]
            param()
            "Hello World"
        }
    .Link
        Get-Extension
    .Notes
        At this time, New-Extension assumes that it is not generating an indented script.
    #>

    param(
    # The name of the extension
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('Name')]
    [string]
    $ExtensionName,

    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('ScriptContents', 'Definition')]
    [ScriptBlock]
    $ScriptBlock,

    # One or more commands being extended.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $CommandName,

    # The extension module. If provided, this will have to prefix the ExtensionNameRegex
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $ExtensionModule,

    # A collection of Extension Parameters.
    # The key is the name of the parameter. The value is any parameter type or attributes.
    [Parameter(ValueFromPipelineByPropertyName)]
    [Collections.IDictionary]
    $ExtensionParameter = [Ordered]@{},

    # A collection of Extension Parameter Help.
    [Parameter(ValueFromPipelineByPropertyName)]
    [Collections.IDictionary]
    $ExtensionParameterHelp = [Ordered]@{},

    # The type of the extension command. By default, this is a script.
    [Parameter(ValueFromPipelineByPropertyName)]
    [ValidateSet('Script','Function', 'Filter')]
    [string]
    $ExtensionCommandType = 'Script',

    # The synopsis used for the extension.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $ExtensionSynopsis = 'Script',

    # Any help links for the extension.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $ExtensionLink,

    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $ExtensionExample
    )

    dynamicParam {
        $myCmd = $MyInvocation.MyCommand
        Get-Extension -CommandName $myCmd -DynamicParameter  # Get any extensions to New-Extension
    }

    process {

        # New-Extension works by combining several script blocks together into one output.
        # Therefore, we start by creating a few lists to keep track of all of the combined parts of the final script.
        $combinedBegin   = @()
        $combinedParam   = @()
        $combinedProcess = @()
        $combinedEnd     = @()

        #region Run Extensions
        # Next, we create a copy of the bound parameters
        $myParams = @{} + $PSBoundParameters
        # and use it to see which extensions we -CouldRun.
        $couldRunExtensions  = Get-Extension -CouldRun -CommandName $myCmd -Parameter $myParams
        # If we could run any extensions, run them and assign their output.
        $ExtensionOutput     = if ($couldRunExtensions) { $couldRunExtensions | Get-Extension -Run }


        $combinedExtensionOutput = # Walk over each extension that ran.
            foreach ($extOut in $ExtensionOutput) {
                # If the extension signaled it was done, output directly. if ($extOut.Done) { $extOut.ExtensionOutput; continue }
                # Otherwise, walk over each output from the extension.
                foreach ($extOutItem in $extOut.ExtensionOutput) {
                    if ($extOutItem -is [ScriptBlock]) {  # If the output was a ScriptBlock, combine it's parts.
                        $extOutAst = $extOut.ExtensionOutput.Ast
                        $combinedParam   += $extOutItem.ParamBlock
                        $combinedBegin   += $extOutItem.BeginBlock.Statements
                        $combinedProcess += $extOutItem.ProcessBlock.Statements
                        $combinedEnd     += $extOutItem.EndBlock.Statements
                    }
                    if ($extOutItem -is [Collections.IDictionary]) { # If the output was a dictionary
                        # walk thru the dictionary
                        foreach ($extKey in $extOutItem.GetEnumerator()) {
                            # (skip any keys that are not parameters of this function).
                            if (-not $myCmd.Parameters[$extKey.Key]) { continue }
                            # If the parameter was an array, add the value to the array.
                            if ($myCmd.Parameters[$extKey.Key].ParameterType.IsSubclassOf([Array])) {
                                $ExecutionContext.SessionState.PSVariable.Set(
                                    $extKey.Key,
                                    $ExecutionContext.SessionState.PSVariable.Get($extKey.Key).Value + $extKey.Value
                                )
                            }
                            # If it the parameter and value were dictionaries
                            elseif ($myCmd.Parameters[$extKey.Key].ParameterType.GetInterface([Collections.IDictionary]) -and
                                $extKey.Value -is [Collections.IDictionary]) {
                                # update the dictionary (overwriting values where found)
                                $dict = $ExecutionContext.SessionState.PSVariable.Get($extKey.Key).Value
                                foreach ($kv in $extKey.Value.GetEnumerator()) {
                                    $dict[$kv.Key] = $kv.Value
                                }
                            }
                            else {
                                # Otherwise, overwrite the parameter value.
                                $ExecutionContext.SessionState.PSVariable.Set($extKey.Key, $extKey.Value)
                            }
                        }
                    }
                }
            }


        # If there was direct extension output, return it.
        if ($combinedExtensionOutput) { return $combinedExtensionOutput }
        #endregion Run Extensions

        #region construct Script
        if ($ExtensionParameter.Count) {
            # If extension parameters were passed, construct a parameter block for each
            foreach ($ep in $ExtensionParameter.GetEnumerator()) {
                $paramLines = @(
                    'param('

                    " " + $ep.Value
                    " `$$($ep.Key -replace '^\$')"
                    ')'
                )
                $paramScriptBlock = [ScriptBlock]::Create($paramLines -join [Environment]::NewLine)
                $combinedParam += $paramScriptBlock.Ast.ParamBlock # and then add them to the combined output.
            }
        }

        if ($ScriptBlock) {  # If a -ScriptBlock was passed
            $ast = $ScriptBlock.Ast
            if ($ast.Body) {
                $ast= $ast.Body
            }
            # add it's parts to the combined output.
            $combinedParam   += $ast.ParamBlock
            $combinedBegin   += $ast.BeginBlock.Statements
            $combinedProcess += $ast.ProcessBlock.Statements
            $combinedEnd     += $ast.EndBlock.Statements
        }

        $extensionLines = @(

            # If the extension command type was function or filter, write that first
            if ($ExtensionCommandType -in 'Function', 'Filter') {
                $ExtensionCommandType -in 'Function', 'Filter'
                $ExtensionCommandType.ToLower() + " $ExtensionName" + '{'
            }

            # If the extension synopsis and description were provided, add help.
            if ($ExtensionSynopsis -and $extensionDescription) {
                "<#"
                ".SYNOPSIS"
                $ExtensionSynopsis
                ".DESCRIPTION"
                $ExtensionDescription
                if ($extensionExample) { # If there were examples, include them.
                    foreach ($example in $extensionExample) {
                        ".EXAMPLE"
                        $example
                    }
                }
                if ($extensionLink) {  # If there were links, include them.
                    foreach ($extLink in $extensionLink) {
                        ".LINK"
                        $extLink
                    }
                }
                "#>"
            }

            if ($CommandName) {  # If one or more command names have provided
                foreach ($extCmdName in $CommandName) {
                    $extVerb, $extNoun  = $extCmdName -split '-'
                    if (-not $extNoun) { $extNoun = ' '}
                    # add a cmdlet attribute
                    "[Management.Automation.Cmdlet('$extVerb','$extNoun')]"
                }
            }

            if ($combinedParam) { # If any combined params have attributes
                foreach ($combined in $combinedParam) {
                    if ($combinedParam.Attributes) {
                        foreach ($attr in $combinedParam.Attributes) {
                            $attr.Extent.ToString() # include them before the param block.
                        }
                    }
                }
            }
            # Start the param block
            "param("
            if ($combinedParam) {
                @(foreach ($combined in $combinedParam) {
                    # Include any existing parameters
                    foreach ($parameter in $combined.parameters) {
                        $parameterName = $parameter.Name.VariablePath.ToString()
                        @(
                            # If the parameter had help, include it
                            if ($ExtensionParameterHelp[$parameterName]) {
                                $lineCount = @([Regex]::Matches($ExtensionParameterHelp[$parameterName], '\n')).Count
                                if ($lineCount) { # (multiline help goes in block comments,
                                    "<#"
                                    $ExtensionParameterHelp[$parameterName]
                                    "#>"
                                } else {
                                    # singleline help goes in line comments)
                                    "# " + $ExtensionParameterHelp[$parameterName]
                                }
                            }
                            $parameter.Extent.ToString()
                        ) -join [Environment]::NewLine
                    }
                }) -join (',' + ([Environment]::NewLine * 2))
            }
            ")"


            if ($combinedBegin) {  # If there were begin blocks,
                "begin {"          # create a begin block
                @(foreach ($combined in $combinedBegin) {
                    "$combined"    # combine the statements with two newlines.
                }) -join ([Environment]::NewLine * 2)
                "}"                # and close the block.
            }

            if ($combinedProcess) {  # If there were process blocks
                "process {"          # create a process block
                @(foreach ($combined in $combinedProcess) {
                    "$combined"      # combine the statements with two newlines.
                }) -join ([Environment]::NewLine * 2)
                "}"                  # and close the block.
            }

            if ($combinedEnd) {
                # If there were end blcoks declared
                $beginOrProcess = $combinedParam -or $combinedBegin
                if ($beginOrProcess) { # create an end block (if there was a begin or process block),
                    "end {"
                }
                @(foreach ($combined in $combinedEnd) {
                    "$combined" # combine all of the statements with two newlines
                }) -join ([Environment]::NewLine * 2)
                if ($beginOrProcess) {
                    "}" # and close the block (if needed).
                }
            }

            # If the command type was a function or a filter, close the command.
            if ($ExtensionCommandType -in 'Function', 'Filter') {
                $ExtensionCommandType -in 'Function', 'Filter'
                '}'
            }
        )

        # Combine all of the lines, and voila, new extension.
        $ExtensionScript = $extensionLines -join [Environment]::NewLine
        $ExtensionScript
        #endregion construct Script
    }
}