Plugins/PSProfile.PowerTools.ps1

[CmdletBinding()]
Param()
function Get-Definition {
    <#
    .SYNOPSIS
    Convenience function to easily get the defition of a function
 
    .DESCRIPTION
    Convenience function to easily get the defition of a function
 
    .PARAMETER Command
    The command or function to get the definition for
 
    .EXAMPLE
    Get-Definition Open-Code
 
    .EXAMPLE
    def Open-Code
 
    Uses the shorter alias to get the definition of the Open-Code function
    #>

    [CmdletBinding()]
    Param(
        [parameter(Mandatory,Position = 0)]
        [String]
        $Command
    )
    Process {
        try {
            $Definition = (Get-Command $Command -ErrorAction Stop).Definition
            "function $Command {$Definition}"
        }
        catch {
            throw
        }
    }
}

Set-Alias -Name def -Value Get-Definition -Option AllScope -Scope Global

Register-ArgumentCompleter -CommandName 'Get-Definition' -ParameterName 'Command' -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    (Get-Command "$wordToComplete*").Name | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}

function Open-Code {
    <#
    .SYNOPSIS
    A drop-in replacement for the Visual Studio Code CLI `code`. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked.
 
    .DESCRIPTION
    A drop-in replacement for the Visual Studio Code CLI `code`. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked.
 
    .PARAMETER Path
    The path of the file or folder to open with Code. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked.
 
    .PARAMETER Cookbook
    If you are using Chef and have your chef-repo folder in your GitPaths, this will allow you to specify a cookbook path to open from the Cookbooks subfolder.
 
    .PARAMETER AddToWorkspace
    If $true, adds the folder to the current Code workspace.
 
    .PARAMETER InputObject
    Pipeline input to display as a temporary file in Code. Temp files are automatically cleaned up after the file is closed in Code. No need to add the `-` after `code` to specify that pipeline input is expected.
 
    .PARAMETER Language
    The language or extension of the temporary file created from the pipeline input. This allows specifying a file type like 'powershell' or 'csv' or an extension like 'ps1', enabling opening of the temp file with the editor file language already set correctly.
 
    .PARAMETER Wait
    If $true, waits for the file to be closed in Code before returning to the prompt. If $false, opens the file using a background job to allow immediately returning to the prompt. Defaults to $false.
 
    .PARAMETER Arguments
    Any additional arguments to be passed directly to the Code CLI command, e.g. `Open-Code --list-extensions` or `code --list-extensions` will still work the same as expected.
 
    .EXAMPLE
    Get-Process | ConvertTo-Csv | Open-Code -Language csv
 
    Gets the current running processes, converts to CSV format and opens it in Code via background job as a CSV. Easy Out-GridView!
 
    .EXAMPLE
    def Update-PSProfileSetting | code -l ps1
 
    Using shorter aliases, gets the current function definition of the Update-PSProfileSetting function and opens it in Code as a PowerShell file to take advantage of immediate syntax highlighting.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Path')]
    Param (
        [parameter(Mandatory,Position = 0,ParameterSetName = 'Path')]
        [parameter(Position = 0,ParameterSetName = 'InputObject')]
        [String]
        $Path,
        [parameter(ParameterSetName = 'Path')]
        [parameter(ParameterSetName = 'Cookbook')]
        [Alias('add','a')]
        [Switch]
        $AddToWorkspace,
        [parameter(ValueFromPipeline,ParameterSetName = 'InputObject')]
        [Object]
        $InputObject,
        [parameter(ParameterSetName = 'InputObject')]
        [Alias('l','lang','Extension')]
        [String]
        $Language = 'txt',
        [parameter(ValueFromPipeline,ParameterSetName = 'InputObject')]
        [Alias('w')]
        [Switch]
        $Wait,
        [parameter(ValueFromRemainingArguments)]
        [Object]
        $Arguments
    )
    DynamicParam {
        if ($global:PSProfile.GitPathMap.ContainsKey('chef-repo')) {
            $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
            $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute
            $ParamAttrib.Mandatory = $true
            $ParamAttrib.ParameterSetName = 'Cookbook'
            $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
            $AttribColl.Add($ParamAttrib)
            $set = (Get-ChildItem (Join-Path $global:PSProfile.GitPathMap['chef-repo'] 'cookbooks') -Directory).Name
            $AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($set)))
            $AttribColl.Add((New-Object System.Management.Automation.AliasAttribute('c')))
            $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter('Cookbook',  [string], $AttribColl)
            $RuntimeParamDic.Add('Cookbook',  $RuntimeParam)
        }
        return  $RuntimeParamDic
    }
    Begin {
        if ($PSCmdlet.ParameterSetName -eq 'InputObject') {
            $collection = New-Object System.Collections.Generic.List[object]
            $extDict = @{
                txt        = 'txt'
                powershell = 'ps1'
                csv        = 'csv'
                sql        = 'sql'
                xml        = 'xml'
                json       = 'json'
                yml        = 'yml'
                csharp     = 'cs'
                fsharp     = 'fs'
                ruby       = 'rb'
                html       = 'html'
                css        = 'css'
                go         = 'go'
                jsonc      = 'jsonc'
                javascript = 'js'
                typescript = 'ts'
                less       = 'less'
                log        = 'log'
                python     = 'py'
                razor      = 'cshtml'
                markdown   = 'md'
            }
        }
    }
    Process {
        $code = (Get-Command code -All | Where-Object { $_.CommandType -notin @('Function','Alias') })[0].Source
        if ($PSCmdlet.ParameterSetName -eq 'InputObject') {
            $collection.Add($InputObject)
        }
        else {
            $target = switch ($PSCmdlet.ParameterSetName) {
                Path {
                    if ($PSBoundParameters['Path'] -eq '.') {
                        $PWD.Path
                    }
                    elseif ($null -ne $global:PSProfile.GitPathMap.Keys) {
                        if ($global:PSProfile.GitPathMap.ContainsKey($PSBoundParameters['Path'])) {
                            $global:PSProfile.GitPathMap[$PSBoundParameters['Path']]
                        }
                        else {
                            $PSBoundParameters['Path']
                        }
                    }
                    else {
                        $PSBoundParameters['Path']
                    }
                }
                Cookbook {
                    [System.IO.Path]::Combine($global:PSProfile.GitPathMap['chef-repo'],'cookbooks',$PSBoundParameters['Cookbook'])
                }
            }
            if ($AddToWorkspace) {
                Write-Verbose "Running command: code --add $($PSBoundParameters[$PSCmdlet.ParameterSetName]) $Arguments"
                & $code --add $target $Arguments
            }
            else {
                Write-Verbose "Running command: code $($PSBoundParameters[$PSCmdlet.ParameterSetName]) $Arguments"
                & $code $target $Arguments
            }
        }
    }
    End {
        if ($PSCmdlet.ParameterSetName -eq 'InputObject') {
            $ext = if ($extDict.ContainsKey($Language)) {
                $extDict[$Language]
            }
            else {
                $Language
            }
            $in = @{
                StdIn   = $collection
                TmpFile = [System.IO.Path]::Combine(([System.IO.Path]::GetTempPath()),"code-stdin-$(-join ((97..(97+25) | ForEach-Object {[char]$_}) | Get-Random -Count 3)).$ext")
            }
            $handler = {
                Param(
                    [hashtable]
                    $in
                )
                try {
                    $code = (Get-Command code -All | Where-Object { $_.CommandType -notin @('Function','Alias') })[0].Source
                    $in.StdIn | Set-Content $in.TmpFile -Force
                    & $code $in.TmpFile --wait
                }
                catch {
                    throw
                }
                finally {
                    if (Test-Path $in.TmpFile -ErrorAction SilentlyContinue) {
                        Remove-Item $in.TmpFile -Force
                    }
                }
            }
            if (-not $Wait) {
                Write-Verbose "Piping input to Code: `$in | Start-Job {code -}"
                Start-Job -ScriptBlock $handler -ArgumentList $in
            }
            else {
                Write-Verbose "Piping input to Code: `$in | code -"
                .$handler($in)
            }
        }
    }
}

Register-ArgumentCompleter -CommandName 'Open-Code' -ParameterName 'Path' -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    Get-PSProfileArguments -WordToComplete "GitPathMap.$wordToComplete" -FinalKeyOnly
}

Register-ArgumentCompleter -CommandName 'Open-Code' -ParameterName 'Language' -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    'txt','powershell','csv','sql','xml','json','yml','csharp','fsharp','ruby','html','css','go','jsonc','javascript','typescript','less','log','python','razor','markdown' | Sort-Object | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}

function Install-LatestModule {
    <#
    .SYNOPSIS
    A helper function to uninstall any existing versions of the target module before installing the latest one.
 
    .DESCRIPTION
    A helper function to uninstall any existing versions of the target module before installing the latest one. Defaults to CurrentUser scope when installing the latest module version from the desired repository.
 
    .PARAMETER Name
    The name of the module to install the latest version of
 
    .PARAMETER Repository
    The PowerShell repository to install the latest module from. Defaults to the PowerShell Gallery.
 
    .PARAMETER ConfirmNotImported
    If $true, safeguards module removal if the module you are trying to update is currently imported by throwing a terminating error.
 
    .EXAMPLE
    Install-LatestModule PSProfile
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [String[]]
        $Name,
        [Parameter()]
        [String]
        $Repository = 'PSGallery',
        [Parameter()]
        [Switch]
        $ConfirmNotImported
    )
    Process {
        foreach ($module in $Name) {
            if ($ConfirmNotImported -and (Get-Module $module)) {
                throw "$module cannot be loaded if trying to install!"
            }
            else {
                try {
                    Write-Verbose "Uninstalling all version of module: $module"
                    Get-Module $module -ListAvailable | Uninstall-Module
                    Write-Verbose "Installing latest module version from PowerShell Gallery"
                    Install-Module $module -Repository $Repository -Scope CurrentUser -AllowClobber -SkipPublisherCheck -AcceptLicense
                }
                catch {
                    throw
                }
            }
        }
    }
}

Register-ArgumentCompleter -CommandName 'Install-LatestModule' -ParameterName 'Name' -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    (Get-Module "$wordToComplete*" -ListAvailable).Name | Sort-Object | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}

function Open-Item {
    <#
    .SYNOPSIS
    Opens the item specified using Invoke-Item. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked.
 
    .DESCRIPTION
    Opens the item specified using Invoke-Item. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked.
 
    .PARAMETER Path
    The path you would like to open. Supports anything that Invoke-Item normally supports, i.e. files, folders, URIs.
 
    .PARAMETER Cookbook
    If you are using Chef and have your chef-repo folder in your GitPaths, this will allow you to specify a cookbook path to open from the Cookbooks subfolder.
 
    .EXAMPLE
    Open-Item
 
    Opens the current path in Explorer/Finder/etc.
 
    .EXAMPLE
    open
 
    Uses the shorter alias to open the current path
 
    .EXAMPLE
    open MyWorkRepo
 
    Opens the folder for the Git Repo 'MyWorkRepo' in Explorer/Finder/etc.
    #>


    [CmdletBinding(DefaultParameterSetName = 'Path')]
    Param (
        [parameter(Position = 0,ParameterSetName = 'Path')]
        [String]
        $Path = $PWD.Path
    )
    DynamicParam {
        if ($global:PSProfile.GitPathMap.ContainsKey('chef-repo')) {
            $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
            $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute
            $ParamAttrib.Mandatory = $true
            $ParamAttrib.ParameterSetName = 'Cookbook'
            $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
            $AttribColl.Add($ParamAttrib)
            $set = (Get-ChildItem (Join-Path $global:PSProfile.GitPathMap['chef-repo'] 'cookbooks') -Directory).Name
            $AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($set)))
            $AttribColl.Add((New-Object System.Management.Automation.AliasAttribute('c')))
            $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter('Cookbook',  [string], $AttribColl)
            $RuntimeParamDic.Add('Cookbook',  $RuntimeParam)
        }
        return  $RuntimeParamDic
    }
    Begin {
        if (-not $PSBoundParameters.ContainsKey('Path')) {
            $PSBoundParameters['Path'] = $PWD.Path
        }
    }
    Process {
        $target = switch ($PSCmdlet.ParameterSetName) {
            Path {
                if ($PSBoundParameters['Path'] -eq '.') {
                    $PWD.Path
                }
                elseif ($null -ne $global:PSProfile.GitPathMap.Keys) {
                    if ($global:PSProfile.GitPathMap.ContainsKey($PSBoundParameters['Path'])) {
                        $global:PSProfile.GitPathMap[$PSBoundParameters['Path']]
                    }
                    else {
                        $PSBoundParameters['Path']
                    }
                }
                else {
                    $PSBoundParameters['Path']
                }
            }
            Cookbook {
                [System.IO.Path]::Combine($global:PSProfile.GitPathMap['chef-repo'],'cookbooks',$PSBoundParameters['Cookbook'])
            }
        }
        Write-Verbose "Running command: Invoke-Item $($PSBoundParameters[$PSCmdlet.ParameterSetName])"
        Invoke-Item $target
    }
}


Register-ArgumentCompleter -CommandName 'Open-Item' -ParameterName 'Path' -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    Get-PSProfileArguments -WordToComplete "GitPathMap.$wordToComplete" -FinalKeyOnly
}

New-Alias -Name open -Value 'Open-Item' -Scope Global -Option AllScope -Force

function Push-Path {
    <#
    .SYNOPSIS
    Pushes your current location to the path specified. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked. Use Pop-Path to return to the location pushed from, as locations pushed from this function are within the module scope.
 
    .DESCRIPTION
    Pushes your current location to the path specified. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked. Use Pop-Path to return to the location pushed from, as locations pushed from this function are within the module scope.
 
    .PARAMETER Path
    The path you would like to push your location to.
 
    .PARAMETER Cookbook
    If you are using Chef and have your chef-repo folder in your GitPaths, this will allow you to specify a cookbook path to push your location to from the Cookbooks subfolder.
 
    .EXAMPLE
    Push-Path MyWorkRepo
 
    Changes your current directory to your Git Repo named 'MyWorkRepo'.
 
    .EXAMPLE
    push MyWorkRepo
 
    Same as the first example but using the shorter alias.
    #>


    [CmdletBinding()]
    Param(
        [parameter(Mandatory,Position = 0,ParameterSetName = 'Path')]
        [String]
        $Path
    )
    DynamicParam {
        $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        if ($global:PSProfile.GitPathMap.ContainsKey('chef-repo')) {
            $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute
            $ParamAttrib.Mandatory = $true
            $ParamAttrib.ParameterSetName = 'Cookbook'
            $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
            $AttribColl.Add($ParamAttrib)
            $set = (Get-ChildItem (Join-Path $global:PSProfile.GitPathMap['chef-repo'] 'cookbooks') -Directory).Name
            $AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($set)))
            $AttribColl.Add((New-Object System.Management.Automation.AliasAttribute('c')))
            $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter('Cookbook',  [string], $AttribColl)
            $RuntimeParamDic.Add('Cookbook',  $RuntimeParam)
        }
        return  $RuntimeParamDic
    }
    Process {
        $target = switch ($PSCmdlet.ParameterSetName) {
            Path {
                if ($global:PSProfile.GitPathMap.ContainsKey($PSBoundParameters['Path'])) {
                    $global:PSProfile.GitPathMap[$PSBoundParameters['Path']]
                }
                else {
                    $PSBoundParameters['Path']
                }
            }
            Cookbook {
                [System.IO.Path]::Combine($global:PSProfile.GitPathMap['chef-repo'],'cookbooks',$PSBoundParameters['Cookbook'])
            }
        }
        Write-Verbose "Pushing location to: $($target.Replace($env:HOME,'~'))"
        Push-Location $target
    }
}

New-Alias -Name push -Value Push-Path -Option AllScope -Scope Global -Force

function Pop-Path {
    <#
    .SYNOPSIS
    Pops your location back the path you Push-Path'd from.
 
    .DESCRIPTION
    Pops your location back the path you Push-Path'd from.
 
    .EXAMPLE
    Pop-Path
    #>

    [CmdletBinding()]
    Param ()
    Process {
        Pop-Location
    }
}

New-Alias -Name pop -Value Pop-Path -Option AllScope -Scope Global -Force

Register-ArgumentCompleter -CommandName 'Push-Path' -ParameterName 'Path' -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    Get-PSProfileArguments -WordToComplete "GitPathMap.$wordToComplete" -FinalKeyOnly
}

function Get-Gist {
    <#
    .SYNOPSIS
    Gets a GitHub Gist's contents using the public API
 
    .DESCRIPTION
    Gets a GitHub Gist's contents using the public API
 
    .PARAMETER Id
    The ID of the Gist to get
 
    .PARAMETER File
    The specific file from the Gist to get. If excluded, gets all of the files as an array of objects.
 
    .PARAMETER Sha
    The SHA of the specific Gist to get, if desired.
 
    .PARAMETER Metadata
    Any additional metadata you want to include on the resulting object, e.g. for identifying what the Gist is, add notes, etc.
 
    .PARAMETER Invoke
    If $true, invokes the Gist contents. If the Gist contains any PowerShell functions, it will adjust the scope to Global before invoking so the function remains available in the session after Get-Gist finishes. Useful for loading functions directly from a Gist.
 
    .EXAMPLE
    Get-Gist -Id f784228937183a1cf8105351872d2f8a -Invoke
 
    Gets the Update-Release and Test-GetGist functions from the following Gist URL and loads them into the current session for subsequent use: https://gist.github.com/scrthq/f784228937183a1cf8105351872d2f8a
    #>


    [CmdletBinding()]
    Param (
        [parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName,Position = 0)]
        [String]
        $Id,
        [parameter(ValueFromPipelineByPropertyName)]
        [Alias('Files')]
        [String[]]
        $File,
        [parameter(ValueFromPipelineByPropertyName)]
        [String]
        $Sha,
        [parameter(ValueFromPipelineByPropertyName)]
        [Object]
        $Metadata,
        [parameter()]
        [Switch]
        $Invoke
    )
    Process {
        $Uri = [System.Collections.Generic.List[string]]@(
            'https://api.github.com'
            '/gists/'
            $PSBoundParameters['Id']
        )
        if ($PSBoundParameters.ContainsKey('Sha')) {
            $Uri.Add("/$($PSBoundParameters['Sha'])")
            Write-Verbose "[$($PSBoundParameters['Id'])] Getting gist info @ SHA '$($PSBoundParameters['Sha'])'"
        }
        else {
            Write-Verbose "[$($PSBoundParameters['Id'])] Getting gist info"
        }
        $gistInfo = Invoke-RestMethod -Uri ([Uri](-join $Uri)) -Verbose:$false
        $fileNames = if ($PSBoundParameters.ContainsKey('File')) {
            $PSBoundParameters['File']
        }
        else {
            $gistInfo.files.PSObject.Properties.Name
        }
        foreach ($fileName in $fileNames) {
            Write-Verbose "[$fileName] Getting gist file content"
            $fileInfo = $gistInfo.files.$fileName
            $content = if ($fileInfo.truncated) {
                (Invoke-WebRequest -Uri ([Uri]$fileInfo.raw_url)).Content
            }
            else {
                $fileInfo.content
            }
            $lines = ($content -split "`n").Count
            if ($Invoke) {
                Write-Verbose "[$fileName] Parsing gist file content ($lines lines)"
                $noScopePattern = '^function\s+(?<Name>[\w+_-]{1,})\s+\{'
                $globalScopePattern = '^function\s+global\:'
                $noScope = [RegEx]::Matches($content, $noScopePattern, "Multiline, IgnoreCase")
                $globalScope = [RegEx]::Matches($content,$globalScopePattern,"Multiline, IgnoreCase")
                if ($noScope.Count -ge $globalScope.Count) {
                    foreach ($match in $noScope) {
                        $fullValue = ($match.Groups | Where-Object { $_.Name -eq 0 }).Value
                        $funcName = ($match.Groups | Where-Object { $_.Name -eq 'Name' }).Value
                        Write-Verbose "[$fileName::$funcName] Updating function to global scope to ensure it imports correctly."
                        $content = $content.Replace($fullValue, "function global:$funcName {")
                    }
                }
                Write-Verbose "[$fileName] Invoking gist file content"
                $ExecutionContext.InvokeCommand.InvokeScript(
                    $false,
                    ([scriptblock]::Create($content)),
                    $null,
                    $null
                )
            }
            [PSCustomObject]@{
                File     = $fileName
                Sha      = $Sha
                Lines    = $lines
                Metadata = $Metadata
                Content  = $content -join "`n"
            }
        }
    }
}

function Enter-CleanEnvironment {
    <#
    .SYNOPSIS
    Enters a clean environment with -NoProfile and sets a couple helpers, e.g. a prompt to advise you are in a clean environment and some PSReadline helper settings for convenience.
 
    .DESCRIPTION
    Enters a clean environment with -NoProfile and sets a couple helpers, e.g. a prompt to advise you are in a clean environment and some PSReadline helper settings for convenience.
 
    .PARAMETER Engine
    The engine to open the clean environment with between powershell, pwsh, and pwsh-preview. Defaults to the current engine the clean environment is opened from.
 
    .PARAMETER ImportModule
    If $true, imports the module found in the BuildOutput folder if present. Useful for quickly testing compiled modules after building in a clean environment to avoid assembly locking and other gotchas.
 
    .EXAMPLE
    Enter-CleanEnvironment
 
    Opens a clean environment from the current path.
 
    .EXAMPLE
    cln
 
    Does the same as Example 1, but using the shorter alias 'cln'.
 
    .EXAMPLE
    cln -ipmo
 
    Enters the clean environment and imports the built module in the BuildOutput folder, if present.
    #>

    [CmdletBinding()]
    Param (
        [parameter(Position = 0)]
        [ValidateSet('powershell','pwsh','pwsh-preview')]
        [Alias('E')]
        [String]
        $Engine = $(if ($PSVersionTable.PSVersion.ToString() -match 'preview') {
                'pwsh-preview'
            }
            elseif ($PSVersionTable.PSVersion.Major -ge 6) {
                'pwsh'
            }
            else {
                'powershell'
            }),
        [Parameter()]
        [Alias('ipmo','Import')]
        [Switch]
        $ImportModule
    )
    Begin {
        $parsedEngine = if ($Engine -eq 'pwsh-preview' -and ($PSVersionTable.PSVersion.Major -le 5 -or $IsWindows)) {
            "& '{0}'" -f (Resolve-Path ([System.IO.Path]::Combine((Split-Path (Get-Command pwsh-preview).Source -Parent),'..','pwsh.exe'))).Path
        }
        else {
            $Engine
        }
    }
    Process {
        $verboseMessage = "Creating clean environment...`n Engine : $Engine"
        $command = "$parsedEngine -NoProfile -NoExit -C `"```$global:CleanNumber = 0;if (```$null -ne (Get-Module PSReadline)) {Set-PSReadLineKeyHandler -Chord Tab -Function MenuComplete;Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward;Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward;Set-PSReadLineKeyHandler -Chord 'Ctrl+W' -Function BackwardKillWord;Set-PSReadLineKeyHandler -Chord 'Ctrl+z' -Function MenuComplete;Set-PSReadLineKeyHandler -Chord 'Ctrl+D' -Function KillWord};"
        if ($ImportModule) {
            if (($modName = (Get-ChildItem .\BuildOutput -Directory).BaseName)) {
                $modPath = '.\BuildOutput\' + $modName
                $verboseMessage += "`n Module : $modName"
                $command += "Import-Module '$modPath' -Verbose:```$false;Get-Module $modName;"
            }
        }
        $newline = '`n'
        $command += "function global:prompt {```$global:CleanNumber++;'[CLN#' + ```$global:CleanNumber + '] [' + [Math]::Round((Get-History -Count 1).Duration.TotalMilliseconds,0) + 'ms] ' + (Get-Location).Path.Replace(```$env:Home,'~') + '$newline[PS ' + ```$PSVersionTable.PSVersion.ToString() + ']>> '}`""
        $verboseMessage += "`n Command : $command"
        Write-Verbose $verboseMessage
        Invoke-Expression $command
    }
}

New-Alias -Name cln -Value Enter-CleanEnvironment -Option AllScope -Scope Global -Force

function Start-BuildScript {
    <#
    .SYNOPSIS
    For those using the typical build.ps1 build scripts for PowerShell projects, this will allow invoking the build script quickly from wherever folder you are currently in using a child process.
 
    .DESCRIPTION
    For those using the typical build.ps1 build scripts for PowerShell projects, this will allow invoking the build script quickly from wherever folder you are currently in using a child process. Any projects in the ProjectPaths list that were discovered during PSProfile load and have a build.ps1 file will be able to be tab-completed for convenience. Temporarily sets the path to the build folder, invokes the build.ps1 file, then returns to the original path that it was invoked from.
 
    .PARAMETER Project
    The path of the project to build. Allows tab-completion of PSBuildPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked.
 
    .PARAMETER Task
    The list of Tasks to specify to the build.ps1 script.
 
    .PARAMETER Engine
    The engine to open the clean environment with between powershell, pwsh, and pwsh-preview. Defaults to the current engine the clean environment is opened from.
 
    .PARAMETER NoExit
    If $true, does not exit the child process once build.ps1 has completed and imports the built module in BuildOutput (if present to allow testing of the built project in a clean environment.
 
    .PARAMETER NoRestore
    If $true, sets $env:NoNugetRestore to $false to prevent NuGet package restoration (if applicable).
 
    .EXAMPLE
    Start-BuildScript MyModule -NoExit
 
    Changes directories to the repo root of MyModule, invokes build.ps1, imports the compiled module in a clean child process and stops before exiting to allow testing of the newly compiled module.
 
    .EXAMPLE
    bld MyModule -ne
 
    Same experience as Example 1 but uses the shorter alias 'bld' to call. Also uses the parameter alias `-ne` instead of `-NoExit`
    #>

    [CmdletBinding(PositionalBinding = $false)]
    Param (
        [Parameter(Position = 0)]
        [Alias('p')]
        [String]
        $Project,
        [Parameter(Position = 1)]
        [Alias('t')]
        [String[]]
        $Task,
        [Parameter(Position = 2)]
        [Alias('e')]
        [String]
        $Engine,
        [parameter()]
        [Alias('ne','noe')]
        [Switch]
        $NoExit,
        [parameter()]
        [Alias('nr','nor')]
        [Switch]
        $NoRestore
    )
    DynamicParam {
        $bldFolder = if ([String]::IsNullOrEmpty($PSBoundParameters['Project']) -or $PSBoundParameters['Project'] -eq '.') {
            $PWD.Path
        }
        elseif ($Global:PSProfile.PSBuildPathMap.ContainsKey($PSBoundParameters['Project'])) {
            Get-LongPath -Path $PSBoundParameters['Project']
        }
        else {
            (Resolve-Path $PSBoundParameters['Project']).Path
        }
        $bldFile = if ($bldFolder -like '*.ps1') {
            $bldFolder
        }
        else {
            Join-Path $bldFolder "build.ps1"
        }
        Copy-Parameters -From $bldFile -Exclude Project,Task,Engine,NoExit,NoRestore
    }
    Process {
        if (-not $PSBoundParameters.ContainsKey('Project')) {
            $PSBoundParameters['Project'] = '.'
        }
        if (-not $PSBoundParameters.ContainsKey('Engine')) {
            $PSBoundParameters['Engine'] = switch ($PSVersionTable.PSVersion.Major) {
                5 {
                    'powershell'
                }
                default {
                    if ($PSVersionTable.PSVersion.PreReleaseLabel) {
                        'pwsh-preview'
                    }
                    else {
                        'pwsh'
                    }
                }
            }
        }
        $parent = switch ($PSBoundParameters['Project']) {
            '.' {
                $PWD.Path
            }
            default {
                $global:PSProfile.PSBuildPathMap[$PSBoundParameters['Project']]
            }
        }
        $command = "$($PSBoundParameters['Engine']) -NoProfile -C `"```$env:NoNugetRestore = "
        if ($NoRestore) {
            $command += "```$true;"
        }
        else {
            $command += "```$false;"
        }
        $command += "Set-Location '$parent'; . .\build.ps1"
        $PSBoundParameters.Keys | Where-Object {$_ -notin @('Project','Engine','NoExit','NoRestore','Debug','ErrorAction','ErrorVariable','InformationAction','InformationVariable','OutBuffer','OutVariable','PipelineVariable','WarningAction','WarningVariable','Verbose','Confirm','WhatIf')} | ForEach-Object {
            if ($PSBoundParameters[$_].ToString() -in @('True','False')) {
                $command += " -$($_):```$$($PSBoundParameters[$_].ToString())"
            }
            else {
                $command += " -$($_) '$($PSBoundParameters[$_] -join "','")'"
            }
        }
        $command += '"'
        Write-Verbose "Invoking expression: $command"
        Invoke-Expression $command
        if ($NoExit) {
            Push-Location $parent
            Enter-CleanEnvironment -Engine $PSBoundParameters['Engine'] -ImportModule
            Pop-Location
        }
    }
}

Register-ArgumentCompleter -CommandName Start-BuildScript -ParameterName Project -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    Get-PSProfileArguments -WordToComplete "PSBuildPathMap.$wordToComplete" -FinalKeyOnly
}

Register-ArgumentCompleter -CommandName Start-BuildScript -ParameterName Task -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $bldFolder = if ([String]::IsNullOrEmpty($fakeBoundParameter.Project) -or $fakeBoundParameter.Project -eq '.') {
        $PWD.Path
    }
    elseif ($Global:PSProfile.PSBuildPathMap.ContainsKey($fakeBoundParameter.Project)) {
        Get-LongPath -Path $fakeBoundParameter.Project
    }
    else {
        (Resolve-Path $fakeBoundParameter.Project).Path
    }
    $bldFile = if ($bldFolder -like '*.ps1') {
        $bldFolder
    }
    else {
        Join-Path $bldFolder "build.ps1"
    }
    $set = if (Test-Path $bldFile) {
        ((([System.Management.Automation.Language.Parser]::ParseFile(
            $bldFile, [ref]$null, [ref]$null
        )).ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'Task' }).Attributes | Where-Object { $_.TypeName.Name -eq 'ValidateSet' }).PositionalArguments.Value
    }
    else {
        @('Clean','Build','Import','Test','Deploy')
    }
    $set | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}

New-Alias -Name bld -Value Start-BuildScript -Option AllScope -Scope Global -Force

function Format-Syntax {
    <#
    .SYNOPSIS
    Formats a command's syntax in an easy-to-read view.
 
    .DESCRIPTION
    Formats a command's syntax in an easy-to-read view.
 
    .PARAMETER Command
    The command to get the syntax of.
 
    .EXAMPLE
    Format-Syntax Get-Process
 
    Gets the formatted syntax by parameter set for Get-Process
 
    .EXAMPLE
    syntax Get-Process
 
    Same as Example 1, but uses the alias 'syntax' instead.
    #>

    [CmdletBinding()]
    param (
        $Command
    )
    $check = Get-Command -Name $Command
    $params = @{
        Name   = if ($check.CommandType -eq 'Alias') {
            Get-Command -Name $check.Definition
        }
        else {
            $Command
        }
        Syntax = $true
    }
    (Get-Command @params) -replace '(\s(?=\[)|\s(?=-))', "`r`n "
}

New-Alias -Name syntax -Value Format-Syntax -Option AllScope -Scope Global -Force

function Get-LongPath {
    <#
    .SYNOPSIS
    Expands a short-alias from the GitPathMap to the full path
 
    .DESCRIPTION
    Expands a short-alias from the GitPathMap to the full path
 
    .PARAMETER Path
    The short path to expand
 
    .PARAMETER Subpaths
    Any subpaths to join to the main path before resolving.
 
    .EXAMPLE
    Get-LongPath MyWorkRepo
 
    Gets the full path to MyWorkRepo
 
    .EXAMPLE
    path MyWorkRepo
 
    Same as Example 1, but uses the short-alias 'path' instead.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Path')]
    Param (
        [parameter(Position = 0,ParameterSetName = 'Path')]
        [String]
        $Path = $PWD.Path,
        [parameter(ValueFromRemainingArguments,Position = 1,ParameterSetName = 'Path')]
        [String[]]
        $Subpaths
    )
    DynamicParam {
        if ($global:PSProfile.GitPathMap.ContainsKey('chef-repo')) {
            $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
            $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute
            $ParamAttrib.Mandatory = $true
            $ParamAttrib.ParameterSetName = 'Cookbook'
            $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
            $AttribColl.Add($ParamAttrib)
            $set = (Get-ChildItem (Join-Path $global:PSProfile.GitPathMap['chef-repo'] 'cookbooks') -Directory).Name
            $AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($set)))
            $AttribColl.Add((New-Object System.Management.Automation.AliasAttribute('c')))
            $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter('Cookbook',  [string], $AttribColl)
            $RuntimeParamDic.Add('Cookbook',  $RuntimeParam)
        }
        return  $RuntimeParamDic
    }
    Begin {
        if (-not $PSBoundParameters.ContainsKey('Path')) {
            $PSBoundParameters['Path'] = $PWD.Path
        }
    }
    Process {
        $target = switch ($PSCmdlet.ParameterSetName) {
            Path {
                if ($PSBoundParameters['Path'] -eq '.') {
                    $PWD.Path
                }
                elseif ($null -ne $global:PSProfile.GitPathMap.Keys) {
                    if ($global:PSProfile.GitPathMap.ContainsKey($PSBoundParameters['Path'])) {
                        $global:PSProfile.GitPathMap[$PSBoundParameters['Path']]
                    }
                    else {
                        (Resolve-Path $PSBoundParameters['Path']).Path
                    }
                }
                else {
                    (Resolve-Path $PSBoundParameters['Path']).Path
                }
            }
            Cookbook {
                [System.IO.Path]::Combine($global:PSProfile.GitPathMap['chef-repo'],'cookbooks',$PSBoundParameters['Cookbook'])
            }
        }
        if ($Subpaths) {
            Join-Path $target ($Subpaths -join [System.IO.Path]::DirectorySeparatorChar)
        }
        else {
            $target
        }
    }
}

Register-ArgumentCompleter -CommandName 'Get-LongPath' -ParameterName 'Path' -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    Get-PSProfileArguments -WordToComplete "GitPathMap.$wordToComplete" -FinalKeyOnly
}

New-Alias -Name path -Value 'Get-LongPath' -Scope Global -Option AllScope -Force

function Confirm-ScriptIsValid {
    <#
    .SYNOPSIS
    Uses the PSParser to check for any errors in a script file.
 
    .DESCRIPTION
    Uses the PSParser to check for any errors in a script file.
 
    .PARAMETER Path
    The path of the script to check for errors.
 
    .EXAMPLE
    Confirm-ScriptIsValid MyScript.ps1
 
    .EXAMPLE
    Get-ChildItem .\Scripts | Confirm-ScriptIsValid
    #>

    [CmdletBinding()]
    Param (
        [parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias("FullName")]
        [ValidateScript( { Test-Path $_ })]
        [String[]]
        $Path
    )
    Begin {
        $errorColl = @()
        $analyzed = 0
        $lenAnalyzed = 0
    }
    Process {
        foreach ($p in $Path | Where-Object { $_ -like '*.ps1' }) {
            $analyzed++
            $item = Get-Item $p
            $lenAnalyzed += $item.Length
            $contents = Get-Content -Path $item.FullName -ErrorAction Stop
            $errors = $null
            $null = [System.Management.Automation.PSParser]::Tokenize($contents, [ref]$errors)
            $obj = [PSCustomObject][Ordered]@{
                Name       = $item.Name
                FullName   = $item.FullName
                Length     = $item.Length
                ErrorCount = $errors.Count
                Errors     = $errors
            }
            $obj
            if ($errors.Count) {
                $errorColl += $obj
            }
        }
    }
    End {
        Write-Verbose "Total files analyzed: $analyzed"
        Write-Verbose "Total size of files analyzed: $lenAnalyzed ($([Math]::Round(($lenAnalyzed/1MB),2)) MB)"
        Write-Verbose "Files with errors:`n$($errorColl | Sort-Object FullName | Out-String)"
    }
}

function Test-RegEx {
    <#
    .SYNOPSIS
    Tests a RegEx pattern against a string and returns the results.
 
    .DESCRIPTION
    Tests a RegEx pattern against a string and returns the results.
 
    .PARAMETER Pattern
    The RegEx pattern to test against the string.
 
    .PARAMETER String
    The string to test.
 
    .EXAMPLE
    Test-RegEx -Pattern '^\w+' -String 'no spaces',' spaces in front'
 
    Matched Pattern Matches String
    ------- ------- ------- ------
       True ^\w+ {no} no spaces
      False ^\w+ spaces in front
    #>

    [CmdletBinding()]
    Param (
        [parameter(Mandatory,Position = 0)]
        [RegEx]
        $Pattern,
        [parameter(Mandatory,Position = 1,ValueFromPipeline)]
        [String[]]
        $String
    )
    Process {
        foreach ($S in $String) {
            $Matches = $null
            [PSCustomObject][Ordered]@{
                Matched = $($S -match $Pattern)
                Pattern = $Pattern
                Matches = $Matches.Values
                String  = $S
            }
        }
    }
}