Add-Macro.ps1

function Add-Macro
{
    <#
    .Synopsis
        Adds a macro
    .Description
        Adds a macro function.
 
        Macro functions are quick commands that tend to start with an _. They can be a:
 
        * -Path (to a file or folder)
        * -URI (with optional replacement strings using curly brackets, braces or dollar signs)
        * -ScriptBlock
        * -Alias
        * -Command proxies (these can -RemoveParameter(s), have -OptionalParameter(s) and have -DefaultParameter(s))
    .Link
        Import-Macro
    .Link
        Export-Macro
    .Link
        Get-Macro
    .Link
        Remove-Macro
    #>

    param(
    # The name of the macro
    [Parameter(Mandatory,Position=0,ValueFromPipelineByPropertyName)]
    [string]
    $Name,

    # The name of the command macro will run.
    [Parameter(Mandatory,ParameterSetName='Alias',ValueFromPipelineByPropertyName)]
    [string]
    $Alias,

    # The script block the macro will run. This will define the macro as a function
    [Parameter(Mandatory,ParameterSetName='ScriptBlock',Position=1,ValueFromPipelineByPropertyName)]
    [ScriptBlock]
    $ScriptBlock,

    <#
    The path used by the macro.
 
    When a path macro is being assigned, it will return the file or folder info.
 
    If a path macro points to a folder, and we are not in that folder, we will Push-Location to the path.
 
    If a path macro points to a folder, and we are already in the folder, we will echo the path.
 
    If a path macro points to a file, it will be invoked with any positional arguments.
    #>

    [Parameter(Mandatory,ParameterSetName='Path')]
    [string]
    $Path,

    # The URI used by the macro.
    # This may contain replacement parameters, using brackets or curly braces to denote the name of the parameter.
    [Parameter(Mandatory,ParameterSetName='Uri')]
    [ValidatePattern('^http(?:s)?\:')]
    [Alias('URL')]
    [string]
    $Uri,

    # If command the macro will wrap.
    # Command wrappers are different than Aliases, in that they can -RemoveParameter, -DefaultParameter, and can whitelist parameter names with -OptionalParameter
    [Parameter(Mandatory,ParameterSetName='ProxyCommand')]
    [string]
    $Command,

    # The list of parameters in the URI that are optional
    [Parameter(ParameterSetName='Uri')]
    [Parameter(ParameterSetName='ProxyCommand')]
    [Alias('OptionalParameters')]
    [string[]]
    $OptionalParameter,

    # The default parameter values.
    # Providing a parameter default will mark make parameters that were mandatory optional.
    [Parameter(ParameterSetName='Uri')]
    [Parameter(ParameterSetName='ProxyCommand')]
    [Alias('DefaultParameters','DefaultValue','DefaultValues')]
    [Collections.IDictionary]
    $DefaultParameter,

    # A list of parameters to remove from a Proxy Command.
    [Parameter(ParameterSetName='ProxyCommand')]
    [Alias('RemoveParameters')]
    [string[]]
    $RemoveParameter,

    # The prefix added to a macro that does not have it.
    # The default prefix is an underscore _.
    [string]
    $Prefix = '_',

    # If set, will not alter the name you pass in.
    [switch]
    $NoPrefix,

    # If set, will output the created macro module.
    [switch]
    $PassThru
    )

    begin {
        if (-not $script:macroList) {
            $script:macroList = [Collections.Generic.List[PSObject]]::new()
        }

$REST_Variable = [Regex]::new(@'
(?> # A variable can be in a URL segment or subdomain
    (?<Start>[/\.]) # Match the <Start>ing slash|dot ...
    (?<IsOptional>\?)? # ... an optional ? (to indicate optional) ...
    (?:
        \{(?<Variable>\w+)\}| # ... A <Variable> name in {} OR
        \[(?<Variable>\w+)\]| # A <Variable> name in [] OR
        \$(?<Variable>\w+) | # A $ followed by a <Variable> OR
        \:(?<Variable>\w+) # A : followed by a <Variable>
    )
| # OR it can be in a query parameter:
    (?<Start>[?&]) # Match The <Start>ing ? or & ...
    (?<Query>[\w\-]+) # ... the <Query> parameter name ...
    = # ... an equals ...
    (?<IsOptional>\?)? # ... an optional ? (to indicate optional) ...
    (?:
        \{(?<Variable>\w+)\}| # ... A <Variable> name in {} OR
        \[(?<Variable>\w+)\]| # A <Variable> name in [] OR
        \$(?<Variable>\w+) | # A $ followed by a <Variable> OR
        \:(?<Variable>\w+) # A : followed by a <Variable>
    )
)
'@
, 'IgnoreCase,IgnorePatternWhitespace', '00:00:05')



    }

    process {
        if (-not $Name.StartsWith($Prefix, 'OrdinalIgnoreCase') -and -not $NoPrefix) {
            $name = "${Prefix}$name"
        }
        $moduleScript =
            if ($Alias)
            {
                "Set-Alias '$Name' '$($Alias)'; Export-ModuleMember -Alias '$Name'"
            }
            elseif ($ScriptBlock)
            {
                "function $Name {$ScriptBlock}"
            }
            elseif ($path)
            {
                "function $name {
begin {
    `$path = '$("$path".Replace("'", "''"))'
}
process {
${Path.Macro}
}
}"

            } elseif ($uri)
            {
                $variableNames =
                    foreach ($match in $REST_Variable.Matches($uri)) {
                        if ($match.Groups['IsOptional'].Value -eq '?') {
                            $OptionalParameter += $match.Groups['Variable'].Value
                        }
                        $match.Groups['Variable'].Value
                    }

                if ($DefaultParameter) {
                    $OptionalParameter += $DefaultParameter.Keys
                }

                $OptionalParameter = $OptionalParameter | Select-Object -Unique
                $position = 0

                $parameterDeclaration =
                    @(foreach ($v in $variableNames) {
                        @("[Parameter($(if ($OptionalParameter -notcontains $v) { "Mandatory,"})ParameterSetName='$uri',ValueFromPipelineByPropertyName,Position=$position)]"
                        "[string]"
                        "`$$v") -join [Environment]::NewLine
                        $position++
                    })

                $parameterDeclaration = $parameterDeclaration -join ",$([Environment]::NewLine)"
                $underylingCommand = 'Invoke-RestMethod'

                $underylingCommandMd =
                    [Management.Automation.CommandMetaData]$ExecutionContext.SessionState.InvokeCommand.GetCommand(
                        $underylingCommand, 'All'
                    )

                $null = $underylingCommandMd.Parameters.Remove('Uri'), $underylingCommandMd.Parameters.Remove('Url')
                $additionalParams = [Management.Automation.ProxyCommand]::GetParamBlock($underylingCommandMd)
                if ($additionalParams -and $additionalParams.contains('$')) {
                    $parameterDeclaration += ','
                    $parameterDeclaration += $additionalParams
                }
"
function $name {
[CmdletBinding(SupportsShouldProcess)]
param(
$parameterDeclaration
)
begin {
    `$url = '$("$uri".Replace("'", "''"))'
    `$underlyingCommand = `$executionContext.SessionState.InvokeCommand.GetCommand('$underylingCommand', 'All')
    `$underlyingParameters = @{}
    foreach (`$k in `$psBoundParameters.Keys) {
        if (`$underlyingCommand.Parameters.`$k) {
            `$underlyingParameters[`$k] = `$psBoundParameters[`$k]
        }
    }
    $(if ($DefaultParameter) {@"
    `$default = ConvertFrom-Json @'
$($defaultParameter | ConvertTo-Json -depth 100)
'@
    foreach (`$prop in `$default.psobject.properties) {
        if (-not `$psBoundParameters.ContainsKey(`$prop.Name)) {
            if (`$prop.Value -is [string] -and `$prop.Value.StartsWith('`$')) {
                `$executionContext.SessionState.PSVariable.Set(`$prop.Name, (
                    `$executionContext.SessionState.PSVariable.Get(`$prop.Value.TrimStart('$')).Value
                ))
            }
            elseif (`$prop.Value -is [ScriptBlock]) {
                `$executionContext.SessionState.PSVariable.Set(`$prop.Name, (& `$prop.Value))
            }
            else {
                `$executionContext.SessionState.PSVariable.Set(`$prop.Name, `$prop.Value)
            }
        }
    }
"@})
    `$urlReplaceRegex = [Regex]::new(@'
$REST_Variable
'@, 'IgnoreCase,IgnorePatternWhitespace')
    `$urlReplacer = {$ReplaceUrlVariable}
 
}
process {
    ${Url.Macro}
}
}
"


            }
            elseif ($Command)
            {
                $realCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand($command, 'All')
                if (-not $realCmd) { Write-Error "$Command not found"; return }
                $commandMetaData = [Management.Automation.CommandMetaData]$realCmd


                foreach ($rp in $RemoveParameter) {
                    $wasRemoved = $commandMetaData.Parameters.Remove($rp)
                    if (-not $wasRemoved) {
                        foreach ($parameterName in @($commandMetaData.Parameters.Keys)) {
                            if ($parameterName -like $rp -and $commandMetaData.Parameters.Remove($parameterName)) {
                                continue
                            }
                        }
                    }
                }

                $proxy =
                    if ($DefaultParameter) {
                        $proxy = [Management.Automation.ProxyCommand]::Create($commandMetaData)
                        $insertAt = $proxy.IndexOf(
                            '$scriptCmd = {& $wrappedCmd @PSBoundParameters }',[StringComparison]::OrdinalIgnoreCase)

                        $proxy.Insert($insertAt, @"
`$defaultJson = ConvertFrom-Json @'
$(ConvertTo-Json -Depth 100 -InputObject $DefaultParameter)
'@
foreach (`$prop in `$defaultJson.psobject.properties) {
    `$psBoundParameters[`$prop.Name] =
        if (`$prop.Value -is [string] -and `$prop.Value.StartsWith('`$')) {
            `$executionContext.SessionState.PSVariable.Get(`$prop.Value.TrimStart('$')).Value
        }
        elseif (`$prop.Value -is [ScriptBlock]) {
            & `$prop.Value
        }
        else {
            `$prop.Value
        }
 
}
"@
 + ([Environment]::NewLine) + (' ' * 8))


                    } else {
                        [Management.Automation.ProxyCommand]::Create($commandMetaData)
                    }

                "function $name {
$proxy
}
"

            }

        if (-not $moduleScript) { return }
        $moduleExists = try { Get-Module -ErrorAction SilentlyContinue -Name $name } catch {  Write-Verbose ($_ |Out-String)}
        if ($moduleExists) {
            if ($script:macroList.Contains($moduleExists)) {
                $null = $script:macroList.Remove($moduleExists)
            }
            $moduleExists | Remove-Macro
        }
        $newModule = New-Module -Name $Name -ScriptBlock ([ScriptBlock]::Create($moduleScript))
        $newModule.PrivateData = [PSCustomObject]([Ordered]@{IsPoshMacro=$true} + $PSBoundParameters)
        $importedNewModule = $newModule | Import-Module -Global -PassThru
        if (-not $importedNewModule) { return }
        $importedNewModule.PSTypenames.add('Macro.Module')
        $script:macroList.Add($importedNewModule)

        if ($PassThru) {$importedNewModule }
    }
}