PSModuleDevelopment.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\PSModuleDevelopment.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName PSModuleDevelopment.Import.DoDotSource -Fallback $false
if ($PSModuleDevelopment_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName PSModuleDevelopment.Import.IndividualFiles -Fallback $false
if ($PSModuleDevelopment_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }

function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
         
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
             
            This provides a central location to react to files being imported, if later desired
         
        .PARAMETER Path
            The path to the file to load
         
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
     
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    param (
        [string]
        $Path
    )
    
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1"
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1"
    
    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
foreach ($resolvedPath in (Resolve-PSFPath -Path "$($script:ModuleRoot)\en-us\*.psd1"))
{
    $data = Import-PowerShellDataFile -Path $resolvedPath
    
    foreach ($key in $data.Keys)
    {
        [PSFramework.Localization.LocalizationHost]::Write('PSModuleDevelopment', $key, 'en-US', $data[$key])
    }
}

if ($IsLinux -or $IsMacOs)
{
    # Defaults to the first value in $Env:XDG_CONFIG_DIRS on Linux or MacOS (or $HOME/.local/share/)
    $fileUserShared = @($Env:XDG_CONFIG_DIRS -split ([IO.Path]::PathSeparator))[0]
    if (-not $fileUserShared) { $fileUserShared = Join-Path $HOME .local/share/ }
    
    $path_FileUserShared = Join-Path (Join-Path $fileUserShared $psVersionName) "PSFramework"
}
else
{
    # Defaults to $Env:AppData on Windows
    $path_FileUserShared = Join-Path $Env:AppData "$psVersionName\PSFramework\Config"
    if (-not $Env:AppData) { $path_FileUserShared = Join-Path ([Environment]::GetFolderPath("ApplicationData")) "$psVersionName\PSFramework\Config" }
}

# Store of registered build actions
$script:buildActions = @{ }
$script:buildArtifacts = @{ }


Set-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' -Value '' -Validation string -Initialize -Description 'Path of the selected build project. Used when running Invoke-PSMDBuildProject without specifying a build file.'

Set-PSFConfig -Module PSModuleDevelopment -Name 'Debug.ConfigPath' -Value (Join-Path -Path (Get-PSFPath -Name AppData) -ChildPath "InfernalAssociates/PowerShell/PSModuleDevelopment/config.xml") -Initialize -Validation string -Description 'The path to where the module debugging information is being stored. Used in the *-PSMDModuleDebug commands.'

Set-PSFConfig -Module PSModuleDevelopment -Name 'Script.StagingRepository' -Value PSGallery -Validation string -Initialize -Description 'Repository to use for modules that are then retrieved when deploying a script'
Set-PSFConfig -Module PSModuleDevelopment -Name 'Script.OutPath' -Value "$env:USERPROFILE\Desktop" -Validation string -Initialize -Description 'Default path where scripts are published to.'

# The parameter identifier used to detect and insert parameters
Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.Identifier' -Value 'þ' -Initialize -Validation 'string' -Description "The identifier used by the template system to detect and insert variables / scriptblock values"

# The default values for common parameters
Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.ParameterDefault.Author' -Value "$env:USERNAME" -Initialize -Validation 'string' -Description "The default value to set for the parameter 'Author'. This same setting can be created for any other parameter name."
Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.ParameterDefault.Company' -Value "MyCompany" -Initialize -Validation 'string' -Description "The default value to set for the parameter 'Company'. This same setting can be created for any other parameter name."

# The file extensions that will not be scanned for content replacement and will be stored as bytes
Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.BinaryExtensions' -Value @('.dll', '.exe', '.pdf', '.doc', '.docx', '.xls', '.xlsx','.png','.ico','.bmp','.jpg','.jpeg','.pdb') -Initialize -Description "When creating a template, files with these extensions will be included as raw bytes and not interpreted for parameter insertion."

# Define the default store. To add more stores, just add a similar setting with a different last name segment
Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.Store.Default' -Value (Join-Path -Path (Get-PSFPath -Name AppData) -ChildPath "WindowsPowerShell/PSModuleDevelopment/Templates") -Initialize -Validation "string" -Description "Path to the default directory where PSModuleDevelopment will store its templates. You can add additional stores by creating the same setting again, only changing the last name segment to a new name and configuring a separate path."
Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.Store.PSModuleDevelopment' -Value "$script:ModuleRoot/internal/templates" -Initialize -Validation "string" -Description "Path to the templates shipped in PSModuleDevelopment"

# Define the default path to create from templates in
Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.OutPath' -Value '.' -Initialize -Validation 'string' -Description "The path where new files & projects should be created from templates by default."

Set-PSFConfig -Module PSModuleDevelopment -Name 'Module.Path' -Value "" -Initialize -Validation "string" -Handler { } -Description "The path to the module currently under development. Used as default path by commnds that work within a module directory."
Set-PSFConfig -Module PSModuleDevelopment -Name 'Package.Path' -Value (Get-PSFPath -Name Temp) -Initialize -Validation "string" -Description "The default output path when exporting a module into a nuget package."
Set-PSFConfig -Module PSModuleDevelopment -Name 'Find.DefaultExtensions' -Value '^\.ps1$|^\.psd1$|^\.psm1$|^\.cs$' -Initialize -Validation string -Description 'The pattern to use to select files to scan when using Find-PSMDFileContent.'

Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.ParmsNotFound" -Value "Red" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the parameters that could not be found."
Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.CommandName" -Value "Green" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the command name extracted from the command text."
Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.MandatoryParam" -Value "Yellow" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the mandatory parameters from the commands parameter sets."
Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.NonMandatoryParam" -Value "DarkGray" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the non mandatory parameters from the commands parameter sets."
Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.FoundAsterisk" -Value "Green" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the asterisk that indicates a parameter has been filled / supplied."
Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.NotFoundAsterisk" -Value "Magenta" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the asterisk that indicates a mandatory parameter has not been filled / supplied."
Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.ParmValue" -Value "DarkCyan" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the parameter value."


#Set-PSFConfig -Module PSModuleDevelopment -Name 'Wix.profile.path' -Value "$env:APPDATA\WindowsPowerShell\PSModuleDevelopment\Wix" -Initialize -Validation "string" -Handler { } -Description "The path where the wix commands store and look for msi building profiles by default."
#Set-PSFConfig -Module PSModuleDevelopment -Name 'Wix.profile.default' -Value " " -Initialize -Validation "string" -Handler { } -Description "The default profile to build. If this is specified, 'Invoke-PSMDWixBuild' will build this profile when nothing else is specified."

#region Ensure Config path exists

# If the folder doesn't exist yet, create it
$root = Split-Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath')
if (-not (Test-Path $root)) { New-Item $root -ItemType Directory -Force | Out-Null }

# If the config file doesn't exist yet, create it
if (-not (Test-Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath'))) { Export-Clixml -InputObject @() -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') }

#endregion Ensure Config path exists

# Pass on the host UI to the library
[PSModuleDevelopment.Utility.UtilityHost]::RawUI = $host.UI.RawUI

function Export-PsmdBuildProjectFile {
<#
    .SYNOPSIS
        Exports a build project object to file.
     
    .DESCRIPTION
        Exports a build project object to file.
        Strips out all superfluous properties on steps to improve readability of output.
     
    .PARAMETER OutPath
        The path to write the file to.
     
    .PARAMETER ProjectObject
        The build project to export.
     
    .EXAMPLE
        PS C:\> $projectObject | Export-PsmdBuildProjectFile -OutPath $outPath
     
        Exports the specified build project object to file.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $OutPath,
        
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        $ProjectObject
    )
    
    process {
        $steps = foreach ($step in $ProjectObject.Steps) {
            $newStep = $step | ConvertTo-PSFHashtable -Include Name, Weight, Action
            if ($step.Dependency) { $newStep.Dependency = $step.Dependency }
            if ($step.Parameters) {
                $parameters = $step.Parameters | ConvertTo-PSFHashtable
                if ($parameters.Count -gt 0) { $newStep.Parameters = $parameters }
            }
            if ($step.Condition -and $step.ConditionSet) {
                $newStep.Condition = $step.Condition
                $newStep.ConditionSet = $step.ConditionSet
            }
            [PSCustomObject]$newStep
        }
        $ProjectObject.Steps = $steps | Sort-Object Weight
        $ProjectObject | ConvertTo-Json -Depth 10 | Set-Content -Path $OutPath -Encoding UTF8 -ErrorAction Stop
    }
}

function Get-PsmdTemplateStore
{
<#
    .SYNOPSIS
        Returns the configured template stores, usually only default.
     
    .DESCRIPTION
        Returns the configured template stores, usually only default.
        Returns null if no matching store is available.
     
    .PARAMETER Filter
        Default: "*"
        The returned stores are filtered by this.
     
    .EXAMPLE
        PS C:\> Get-PsmdTemplateStore
     
        Returns all stores configured.
     
    .EXAMPLE
        PS C:\> Get-PsmdTemplateStore -Filter default
     
        Returns the default store only
#>

    [CmdletBinding()]
    Param (
        [string]
        $Filter = "*"
    )
    
    process
    {
        Get-PSFConfig -FullName "PSModuleDevelopment.Template.Store.$Filter" | ForEach-Object {
            New-Object PSModuleDevelopment.Template.Store -Property @{
                Path  = $_.Value
                Name  = $_.Name -replace "^.+\."
            }
        }
    }
}

function Resolve-TemplateParameter {
    <#
    .SYNOPSIS
        Resolves the parameters to invoke a template with.
     
    .DESCRIPTION
        Resolves the parameters to invoke a template with.
 
        This processes parameters with the following, ascending priority:
 
        - From Configuration (if specified)
        - From PSMD Configuration files (if specified)
        - From being explicitly specified on invocation
 
        Explicitly bound parameters will thus always win.
     
    .PARAMETER Path
        Path in which the template is being invoked.
        If this parameter is specified, it will search the path and all parent paths for PSMDConfig.psd1 files.
        Then read the settings from them, starting at the root path.
        The deeper the path, the later the settings are loaded, overwriting settings from parent folders in case of conflict.
     
    .PARAMETER Configuration
        The Configuration settings specified by the user.
        These take precedence over anything else.
     
    .PARAMETER FromConfiguration
        Whether to load configuration settings from configuration.
     
    .EXAMPLE
        PS C:\> Resolve-TemplateParameter -Path $resolvedPath -Configuration $Configuration -FromConfiguration
 
        Resolves all parameters for the current template.
    #>

    [OutputType([hashtable])]
    [CmdletBinding()]
    param (
        [string]
        $Path,

        [hashtable]
        $Configuration = @{ },

        [switch]
        $FromConfiguration
    )
    process {
        $newConfiguration = @{ }

        if ($Path) {
            $currentPath = $Path
            $paths = while ($currentPath) {
                $currentPath
                $currentPath = Split-Path $currentPath
            }
            
            foreach ($rootPath in $paths | Sort-Object Length) {
                $configPath = Join-Path $rootPath 'PSMDConfig.psd1'
                if (-not (Test-Path -Path $configPath)) { continue }

                $cfg = Import-PSFPowerShellDataFile -Path $configPath
                if ($cfg -and $cfg -is [hashtable]) {
                    foreach ($pair in $cfg.GetEnumerator()) {
                        $newConfiguration[$pair.Key] = $pair.Value
                    }
                }
            }
        }

        foreach ($pair in $Configuration.GetEnumerator()) {
            $newConfiguration[$pair.Key] = $pair.Value
        }

        if ($FromConfiguration) {
            foreach ($config in Get-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.ParameterDefault.*') {
                $cfgName = $config.Name -replace '^.+\.([^\.]+)$', '$1'
                if (-not $newConfiguration.ContainsKey($cfgName)) {
                    $newConfiguration[$cfgName] = $config.Value
                }
            }
        }

        $newConfiguration
    }
}

function Expand-PSMDTypeName
{
<#
    .SYNOPSIS
        Returns the full name of the input object's type, as well as the name of the types it inherits from, recursively until System.Object.
     
    .DESCRIPTION
        Returns the full name of the input object's type, as well as the name of the types it inherits from, recursively until System.Object.
     
    .PARAMETER InputObject
        The object whose typename to expand.
     
    .EXAMPLE
        PS C:\> Expand-PSMDTypeName -InputObject "test"
     
        Returns the typenames for the string test ("System.String" and "System.Object")
#>

    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipeline = $true)]
        $InputObject
    )
    
    process
    {
        foreach ($item in $InputObject)
        {
            if ($null -eq $item) { continue }
            
            $type = $item.GetType()
            if ($type.FullName -eq "System.RuntimeType") { $type = $item }
            
            $type.FullName
            
            while ($type.FullName -ne "System.Object")
            {
                $type = $type.BaseType
                $type.FullName
            }
        }
    }
}

function Find-PSMDType
{
<#
    .SYNOPSIS
        Searches assemblies for types.
     
    .DESCRIPTION
        This function searches the currently imported assemblies for a specific type.
        It is not inherently limited to public types however, and can search interna just as well.
     
        Can be used to scan for dependencies, to figure out what libraries one needs for a given type and what dependencies exist.
     
    .PARAMETER Name
        Default: "*"
        The name of the type to search for.
        Accepts wildcards.
     
    .PARAMETER FullName
        Default: "*"
        The FullName of the type to search for.
        Accepts wildcards.
     
    .PARAMETER Assembly
        Default: (Get-PSMDAssembly)
        The assemblies to search. By default, all loaded assemblies are searched.
     
    .PARAMETER Public
        Whether the type to find must be public.
     
    .PARAMETER Enum
        Whether the type to find must be an enumeration.
         
    .PARAMETER Static
        Whether the type to find must be static.
     
    .PARAMETER Implements
        Whether the type to find must implement this interface
     
    .PARAMETER InheritsFrom
        The type must directly inherit from this type.
        Accepts wildcards.
     
    .PARAMETER Attribute
        The type must have this attribute assigned.
        Accepts wildcards.
     
    .EXAMPLE
        Find-PSMDType -Name "*String*"
     
        Finds all types whose name includes the word "String"
        (This will be quite a few)
     
    .EXAMPLE
        Find-PSMDType -InheritsFrom System.Management.Automation.Runspaces.Runspace
     
        Finds all types that inherit from the Runspace class
#>

    [Alias('ftype')]
    [CmdletBinding()]
    Param (
        [string]
        $Name = "*",
        
        [string]
        $FullName = "*",
        
        [Parameter(ValueFromPipeline = $true)]
        [System.Reflection.Assembly[]]
        $Assembly = (Get-PSMDAssembly),
        
        [switch]
        $Public,
        
        [switch]
        $Enum,
        
        [switch]
        $Static,
        
        [string]
        $Implements,
        
        [string]
        $InheritsFrom,
        
        [string]
        $Attribute
    )
    
    begin
    {
        $boundEnum = Test-PSFParameterBinding -ParameterName Enum
        $boundPublic = Test-PSFParameterBinding -ParameterName Public
        $boundStatic = Test-PSFParameterBinding -ParameterName Static
    }
    process
    {
        foreach ($item in $Assembly)
        {
            if ($boundPublic)
            {
                if ($Public) { $types = $item.ExportedTypes }
                else
                {
                    # Empty Assemblies will error on this, which is not really an issue and can be safely ignored
                    try { $types = $item.GetTypes() | Where-Object IsPublic -EQ $false }
                    catch { Write-PSFMessage -Message "Failed to enumerate types on $item" -Level InternalComment -Tag 'fail','assembly','type','enumerate' -ErrorRecord $_ }
                }
            }
            else
            {
                # Empty Assemblies will error on this, which is not really an issue and can be safely ignored
                try { $types = $item.GetTypes() }
                catch { Write-PSFMessage -Message "Failed to enumerate types on $item" -Level InternalComment -Tag 'fail', 'assembly', 'type', 'enumerate' -ErrorRecord $_ }
            }
            
            foreach ($type in $types)
            {
                if ($type.Name -notlike $Name) { continue }
                if ($type.FullName -notlike $FullName) { continue }
                if ($Implements -and ($type.ImplementedInterfaces.Name -notcontains $Implements)) { continue }
                if ($boundEnum -and ($Enum -ne $type.IsEnum)) { continue }
                if ($InheritsFrom -and ($type.BaseType.FullName -notlike $InheritsFrom)) { continue }
                if ($Attribute -and ($type.CustomAttributes.AttributeType.Name -notlike $Attribute)) { continue }
                if ($boundStatic -and ($Static -ne ($type.IsAbstract -and $type.IsSealed))) { continue }
                
                $type
            }
        }
    }
}

function Get-PSMDAssembly
{
    <#
        .SYNOPSIS
            Returns the assemblies currently loaded.
         
        .DESCRIPTION
            Returns the assemblies currently loaded.
         
        .PARAMETER Filter
            Default: *
            The name to filter by
         
        .EXAMPLE
            Get-PSMDAssembly
     
            Lists all imported libraries
     
        .EXAMPLE
            Get-PSMDAsssembly -Filter "Microsoft.*"
     
            Lists all imported libraries whose name starts with "Microsoft.".
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Filter = "*"
    )
    
    process
    {
        [appdomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like $Filter
    }
}

function Get-PSMDConstructor
{
    <#
        .SYNOPSIS
            Returns information on the available constructors of a type.
         
        .DESCRIPTION
            Returns information on the available constructors of a type.
            Accepts any object as pipeline input:
            - if it's a type, it will retrieve its constructors.
            - If it's not a type, it will retrieve the constructor from the type of object passed
     
            Will not duplicate constructors if multiple objects of the same type are passed.
            In order to retrieve the constructor of an array, wrap it into another array.
         
        .PARAMETER InputObject
            The object the constructor of which should be retrieved.
     
        .PARAMETER NonPublic
            Show non-public constructors instead.
         
        .EXAMPLE
            Get-ChildItem | Get-PSMDConstructor
     
            Scans all objects in the given path, than tries to retrieve the constructor for each kind of object returned
            (generally, this will return the constructors for file and folder objects)
     
        .EXAMPLE
            Get-PSMDConstructor $result
     
            Returns the constructors of objects stored in $result
    #>

    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipeline = $true)]
        $InputObject,
        
        [switch]
        $NonPublic
    )
    
    begin
    {
        $processedTypes = @()
    }
    process
    {
        foreach ($item in $InputObject)
        {
            if ($null -eq $item) { continue }
            if ($item -is [System.Type]) { $type = $item }
            else { $type = $item.GetType() }
            
            if ($processedTypes -contains $type) { continue }
            
            if ($NonPublic)
            {
                foreach ($constructor in $type.GetConstructors([System.Reflection.BindingFlags]'NonPublic, Instance'))
                {
                    New-Object PSModuleDevelopment.PsmdAssembly.Constructor($constructor)
                }
            }
            else
            {
                foreach ($constructor in $type.GetConstructors())
                {
                    New-Object PSModuleDevelopment.PsmdAssembly.Constructor($constructor)
                }
            }
            
            $processedTypes += $type
        }
    }
}

function Get-PSMDMember
{
<#
.ForwardHelpTargetName Microsoft.PowerShell.Utility\Get-Member
.ForwardHelpCategory Cmdlet
#>

    [CmdletBinding(HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=113322', RemotingCapability = 'None')]
    param (
        [Parameter(ValueFromPipeline = $true)]
        [psobject]
        $InputObject,
        
        [Parameter(Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Name,
        
        [Alias('Type')]
        [System.Management.Automation.PSMemberTypes]
        $MemberType,
        
        [System.Management.Automation.PSMemberViewTypes]
        $View,
        
        [string]
        $ArgumentType,
        
        [string]
        $ReturnType,
        
        [switch]
        $Static,
        
        [switch]
        $Force
    )
    
    begin
    {
        try
        {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
            {
                $PSBoundParameters['OutBuffer'] = 1
            }
            if ($ArgumentType) { $null = $PSBoundParameters.Remove("ArgumentType") }
            if ($ReturnType) { $null = $PSBoundParameters.Remove("ReturnType") }
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Get-Member', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = { & $wrappedCmd @PSBoundParameters }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($true)
        }
        catch
        {
            throw
        }
        
        function Split-Member
        {
            [CmdletBinding()]
            param (
                [Parameter(ValueFromPipeline = $true)]
                [Microsoft.PowerShell.Commands.MemberDefinition]
                $Member
            )
            
            process
            {
                if ($Member.MemberType -notlike "Method") { return $Member }
                
                if ($Member.Definition -notlike "*), *") { return $Member }
                
                foreach ($definition in $Member.Definition.Replace("), ", ")þþþ").Split("þþþ"))
                {
                    if (-not $definition) { continue }
                    New-Object Microsoft.PowerShell.Commands.MemberDefinition($Member.TypeName, $Member.Name, $Member.MemberType, $definition)
                }
            }
        }
        
    }
    
    process
    {
        try
        {
            $members = $steppablePipeline.Process($_) | Split-Member
            
            if ($ArgumentType)
            {
                $tempMembers = @()
                foreach ($member in $members)
                {
                    if ($member.MemberType -notlike "Method") { continue }
                    
                    if (($member.Definition -split "\(",2)[1] -match $ArgumentType) { $tempMembers += $member }
                }
                $members = $tempMembers
            }
            
            if ($ReturnType)
            {
                $members = $members | Where-Object Definition -match "^$ReturnType"
            }
            
            $members
        }
        catch
        {
            throw
        }
    }
    
    end
    {
        try
        {
            $steppablePipeline.End()
        }
        catch
        {
            throw
        }
    }
}

function Get-PSMDBuildAction {
<#
    .SYNOPSIS
        Get a list of registered build actions.
     
    .DESCRIPTION
        Get a list of registered build actions.
        Actions are the scriptblocks that are used to execute the build logic when running Invoke-PSMDBuildProject.
     
    .PARAMETER Name
        The name by which to filter the actions returned.
        Defaults to '*'
     
    .EXAMPLE
        PS C:\> Get-PSMDBuildAction
     
        Get a list of all registered build actions.
#>

    [CmdletBinding()]
    param (
        [PsfArgumentCompleter('PSModuleDevelopment.Build.Action')]
        [string]
        $Name = '*'
    )
    
    process {
        $script:buildActions.Values | Where-Object Name -Like $Name
    }
}


function Get-PSMDBuildArtifact {
<#
    .SYNOPSIS
        Retrieve an artifact during a build project's execution.
     
    .DESCRIPTION
        Retrieve an artifact during a build project's execution.
        These artifacts are usually created during such an execution and discarded once completed.
     
    .PARAMETER Name
        The name by which to search for artifacts.
        Defaults to '*'
     
    .PARAMETER Tag
        Search for artifacts by tag.
        Artifacts can receive tag for better categorization.
        When specifying multiple tags, any artifact containing at least one of them will be returned.
     
    .EXAMPLE
        PS C:\> Get-PSMDBuildArtifact
     
        List all available artifacts.
     
    .EXAMPLE
        PS C:\> Get-PSMDBuildArtifact -Name ReleasePath
     
        Returns the artifact named "ReleasePath"
     
    .EXAMPLE
        PS C:\> Get-PSMDBuildArtifact -Tag pssession
     
        Returns all artifacts with the tag "pssession"
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [CmdletBinding()]
    param (
        [string]
        $Name = '*',
        
        [string[]]
        $Tag
    )
    
    process {
        $artifacts = $script:buildArtifacts.Values | Where-Object Name -Like $Name | Where-Object {
            if (-not $Tag) { return $true }
            foreach ($tagName in $Tag) {
                if ($_.Tags -contains $Tag) { return $true }
            }
            return $false
        }
        $($artifacts)
    }
}


function Get-PSMDBuildProject {
<#
    .SYNOPSIS
        Reads & returns a build project.
     
    .DESCRIPTION
        Reads & returns a build project.
        A build project is a container including the steps executed during the build.
     
    .PARAMETER Path
        Path to the build project file.
        May target the folder, in which case the -Name parameter must be specified.
     
    .PARAMETER Name
        The name of the build project to read.
        Use together with the -Path parameter only.
        Absolute file path assumed will be: "<Path>\<Name>.build.json"
     
    .PARAMETER Selected
        Rather than specifying the path to read from, return the currently selected build project.
        Use Select-PSMDBuildProject to select a build project as the default ("selected") project.
     
    .EXAMPLE
        PS C:\> Get-PSMDBuildProject -Path 'C:\code\project' -Name project
     
        Will load the build project stored in the file "C:\code\project\project.build.json"
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [CmdletBinding(DefaultParameterSetName = 'Path')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
        [string]
        $Path,
        
        [Parameter(ParameterSetName = 'Path')]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Selected')]
        [switch]
        $Selected
    )
    
    process {
        if ($Path) {
            $importPath = $Path
            if ($Name) { $importPath = Join-Path -Path $Path -ChildPath "$Name.build.json" }
            if (-not (Test-Path -Path $importPath)) {
                $importPath = Join-Path -Path $Path -ChildPath "$Name.build.psd1"
            }
        }
        else {
            $importPath = Get-PSFConfigValue -FullName 'PSModuleDevelopment.Build.Project.Selected'
        }

        if (-not (Test-Path -Path $importPath)) {
            throw "Path not found: $importPath"
        }
        if ($importPath -like "*.psd1") { Import-PSFPowerShellDataFile -Path $importPath }
        else { Get-Content -Path $importPath -Encoding UTF8 | ConvertFrom-Json }
    }
}


function Get-PSMDBuildStep {
<#
    .SYNOPSIS
        Read the steps that are part of the specified build project.
     
    .DESCRIPTION
        Read the steps that are part of the specified build project.
     
    .PARAMETER Name
        The name by which to filter the steps returned.
        Defaults to '*'
     
    .PARAMETER BuildProject
        Path to the build project file to read from.
        Defaults to the currently selected project if available.
        Use Select-PSMDBuildProject to select a default project.
     
    .EXAMPLE
        PS C:\> Get-PSMDBuildStep
     
        Read all steps that are part of the default build project.
     
    .EXAMPLE
        PS C:\> Get-PSMDBuildStep -Name CreateSession -BuildProject C:\code\Project\Project.build.json
     
        Return the CreateSession step from the specified project file.
#>

    [CmdletBinding()]
    param (
        [string]
        $Name = '*',
        
        [string]
        $BuildProject
    )
    
    begin {
        $projectPath = $BuildProject
        if (-not $projectPath) { $projectPath = Get-PSFConfigValue -FullName 'PSModuleDevelopment.Build.Project.Selected' }
        if (-not $projectPath) { throw "No Project path specified and none selected!" }
        if (-not (Test-Path -Path $projectPath)) {
            throw "Project file not found: $projectPath"
        }
    }
    process {
        $projectObject = Get-PSMDBuildProject -Path $projectPath
        $projectObject.Steps | Where-Object Name -Like $Name
    }
}


function Invoke-PSMDBuildProject {
<#
    .SYNOPSIS
        Execute a build project.
     
    .DESCRIPTION
        Execute a build project.
        A build project is a configured chain of actions that have been configured in json.
        They will be processed in their specified order and allow manageable, configurable steps without having to reinvent the same action again and again.
         
        + Individual action types become available using Register-PSMDBuildAction.
        + Create new build projects using New-PSMDBuildProject
        + Set up steps taken during a build using Set-PSMDBuildStep
        + Select the default build project using Select-PSMDBuildProject
     
    .PARAMETER Path
        The path to the build project file to execute.
        Mandatory if no build project has been selected as the default project.
        Use the Select-PSMDBuildProject to define a default project (and optionally persist the choice across sessions)
     
    .PARAMETER InheritArtifacts
        Accept artifacts that were generated before ever executing this pipeline.
        By default, any artifacts previously provisioned are cleared on pipeline start.
     
    .PARAMETER RetainArtifacts
        Whether, after executing the project, its artifacts should be retained.
        By default, any artifacts created during a build project will be discarded upon project completion.
         
        Artifacts are similar to variables to the pipeline and can be used to pass data throughout the pipeline.
         
        + Use Publish-PSMDBuildArtifact to create a new artifact.
        + Use Get-PSMDBuildArtifact to access existing build artifacts.
     
    .EXAMPLE
        PS C:\> Invoke-PSMDBuildProject -Path .\VMDeployment.build.Json
         
        Execute the build file "VMDeployment.build.json" from the current folder
     
    .EXAMPLE
        PS C:\> build
         
        Execute the default build project.
#>

    [Alias('build')]
    [CmdletBinding()]
    param (
        [string]
        $Path,
        
        [switch]
        $InheritArtifacts,
        
        [switch]
        $RetainArtifacts
    )
    
    begin {
        if (-not $InheritArtifacts) {
            $script:buildArtifacts = @{ }
        }
        $buildStatus = @{ }
        
        $projectPath = $Path
        if (-not $projectPath) { $projectPath = Get-PSFConfigValue -FullName 'PSModuleDevelopment.Build.Project.Selected' }
        if (-not $projectPath) { throw "No Project path specified and none selected!" }
        if (-not (Test-Path -Path $projectPath)) {
            throw "Project file not found: $projectPath"
        }
        
        function Write-StepResult {
            [CmdletBinding()]
            param (
                [int]
                $Count,
                
                [ValidateSet('Success', 'Failed', 'ConditionNotMet', 'DependencyNotMet', 'BadAction')]
                [string]
                $Status,
                
                $StepObject,
                
                $Data,
                
                [hashtable]
                $BuildStatus,
                
                [string]
                $ContinueLabel
            )
            
            $BuildStatus[$StepObject.Name] = $Status -eq 'Success'
            
            $paramWritePSFMessage = @{
                Level         = 'Warning'
                String         = "Invoke-PSMDBuildProject.Step.$Status"
            }
            
            switch ($Status) {
                Failed { Write-PSFMessage @paramWritePSFMessage -StringValues $StepObject.Name, $StepObject.Action -ErrorRecord $Data }
                ConditionNotMet { Write-PSFMessage @paramWritePSFMessage -StringValues $StepObject.Name, $StepObject.Action, $StepObject.Condition }
                DependencyNotMet { Write-PSFMessage @paramWritePSFMessage -StringValues $StepObject.Name, $StepObject.Action, $Data }
                BadAction { Write-PSFMessage @paramWritePSFMessage -StringValues $StepObject.Name, $StepObject.Action }
            }
            
            [PSCustomObject]@{
                PSTypeName = 'PSModuleDevelopment.Build.StepResult'
                Count       = $Count
                Action       = $StepObject.Action
                Status       = $Status
                Step       = $StepObject.Name
                Data       = $Data
            }
            
            if ($ContinueLabel) {
                continue $ContinueLabel
            }
        }
    }
    process {
        $projectObject = Get-PSMDBuildProject -Path $projectPath
        $steps = $projectObject.Steps | Sort-Object { $_.Weight } # Might be a hashtable
        
        $count = 0
        $stepResults = :main foreach ($step in $steps) {
            $count++
            $resultDef = @{
                Count = $count
                StepObject = $step
                BuildStatus = $buildStatus
            }
            
            Write-PSFMessage -Level Host -String 'Invoke-PSMDBuildProject.Step.Executing' -StringValues $count, $step.Name, $step.Action
            
            #region Validation
            $actionObject = $script:buildActions[$step.Action]
            if (-not $actionObject) {
                Write-StepResult @resultDef -Status BadAction -ContinueLabel main
            }
            
            foreach ($dependency in $step.Dependency) {
                if (-not $buildStatus[$dependency]) {
                    Write-StepResult @resultDef -Status DependencyNotMet -Data $dependency -ContinueLabel main
                }
            }
            
            if ($step.Condition -and $step.ConditionSet) {
                $cModule, $cSetName = $step.ConditionSet -split " ", 2
                $conditionSet = Get-PSFFilterConditionSet -Module $cModule -Name $cSetName
                if (-not $conditionSet) {
                    Write-StepResult @resultDef -Status ConditionNotMet -ContinueLabel main
                }
                
                $filter = New-PSFFilter -Expression $step.Condition -ConditionSet $conditionSet
                if (-not $filter.Evaluate()) {
                    Write-StepResult @resultDef -Status ConditionNotMet -ContinueLabel main
                }
            }
            #endregion Validation
            
            #region Execution
            $parameters = @{
                RootPath = Split-Path -Path $projectPath
                Parameters = $step.Parameters | ConvertTo-PSFHashtable
                ProjectName = $projectObject.Name
                StepName = $step.Name
                ParametersFromArtifacts = $step.ParametersFromArtifacts | ConvertTo-PSFHashtable
            }
            if (-not $parameters.Parameters) { $parameters.Parameters = @{ } }
            if (-not $parameters.ParametersFromArtifacts) { $parameters.ParametersFromArtifacts = @{ } }
            
            # Resolve Parameters
            $parameters.Parameters = Resolve-PSMDBuildStepParameter -Parameters $parameters.Parameters -FromArtifacts $parameters.ParametersFromArtifacts -ProjectName $parameters.ProjectName -StepName $parameters.StepName
            
            try { $null = & $actionObject.Action $parameters }
            catch {
                Write-StepResult @resultDef -Status Failed -Data $_ -ContinueLabel main
            }
            Write-StepResult @resultDef -Status Success
            #endregion Execution
        }
        $stepResults
    }
    end {
        if (-not $RetainArtifacts) {
            $script:buildArtifacts = @{ }
        }
    }
}


function New-PSMDBuildProject {
<#
    .SYNOPSIS
        Create a new build project file.
     
    .DESCRIPTION
        Create a new build project file.
        Build projects are used to configure a repeatable, managed set of steps that make up a workflow.
        It is designed with software build processes in mind, but can be used for pretty much anything that works in separate steps.
     
        See the help on Invoke-PSMDBuildProject for more details.
     
        NOTE: This is not the tool or component to create new PowerShell _code_ projects / repositories!
        To create a new PowerShell module project, instead run:
         
            Invoke-PSMDTemplate PSFProject
     
    .PARAMETER Name
        The name of the build project.
     
    .PARAMETER Path
        The path to the folder in which the build project file is created.
        Final path will be: "<Path>\<Name>.build.json"
     
    .PARAMETER Condition
        A condition - a filter expression - that must be met in order for the build to proceed.
        For more details on filter conditions, see the PSFramework documentation on its feature:
        https://psframework.org/documentation/documents/psframework/filters.html
     
    .PARAMETER ConditionSet
        The name of the condition set to use.
        This is part of the PSFramework filter system:
        https://psframework.org/documentation/documents/psframework/filters.html
     
        Specify as "<module> <conditionsetname>" format.
        Default Value: PSFramework Environment
     
    .PARAMETER NoSelect
        Do not select the newly created build project as the default project for the current session.
        By default, the newly created build project will be set as default project, in order to facilitate adding steps to it.
        Use Select-PSMDBuildProject to explicitly set a default project file.
     
    .PARAMETER Register
        Persist the newly created build project as default build project beyond the current session.
        By default, the newly created build project will already be set as default project, in order to facilitate adding steps to it.
        But ONLY for the current session. This parameter makes it remember in new PowerShell sessions as well.
     
    .EXAMPLE
        PS C:\> New-PSMDBuildProject -Name 'VMDeployment' -Path 'C:\Code\VMDeployment'
     
        Create a new build project named 'VMDeployment' in the folder 'C:\Code\VMDeployment'
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding(DefaultParameterSetName = 'default')]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')]
        [string]
        $Path,
        
        [string]
        $Condition,
        
        [string]
        $ConditionSet = 'PSFramework Environment',
        
        [Parameter(ParameterSetName = 'NoSelect')]
        [switch]
        $NoSelect,
        
        [Parameter(ParameterSetName = 'Register')]
        [switch]
        $Register
    )
    
    process {
        $resolvedPath = Resolve-PSFPath -Path $Path
        $project = [pscustomobject]@{
            Name         = $Name
            Condition    = $Condition
            ConditionSet = $ConditionSet
            Steps         = @()
        }
        $outPath = Join-Path -Path $resolvedPath -ChildPath "$Name.build.Json"
        $project | Export-PsmdBuildProjectFile -OutPath $outPath -ErrorAction Stop
        if (-not $NoSelect) {
            Set-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' -Value $outPath
            if ($Register) { Register-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' }
        }
    }
}


function Publish-PSMDBuildArtifact {
<#
    .SYNOPSIS
        Create a new artifact for the current build pipeline.
     
    .DESCRIPTION
        Create a new artifact for the current build pipeline.
        Use this create artifacts that are accessible in later steps in the pipeline.
     
        Usually, artifacts are deleted at the end of a build process.
        They are always cleared at the beginning of a new one.
     
        Artifacts are NOT persisted across PowerShell sessions.
     
    .PARAMETER Name
        Name of the Artifact to create.
        Technically there are no limits to which character to chose, but we strongly encourage restricting yourself to letters, numbers, dash, underscore and dot.
     
    .PARAMETER Value
        The value to assign to the artifact.
     
    .PARAMETER Tag
        Any tags to add to an artifact.
        Tags can be searched for in order to bulk-operate against all artifacts of that tag.
        For example, the "remove-pssession" action can remove all remoting sessions for all artifacts tagged as "pssession".
     
    .EXAMPLE
        PS C:\> Publish-PSMDBuildArtifact -Name 'session' -Value $session -Tag 'pssession'
     
        Publishes an artifact named "session" containing the content of $session that is tagged as a PowerShell remoting session.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        $Value,
        
        [string[]]
        $Tag = @()
    )
    
    process {
        $script:buildArtifacts[$Name] = [pscustomobject]@{
            PSTypeName = 'PSModuleDevelopment.Build.Artifact'
            Name       = $Name
            Value       = $Value
            Tags       = $Tag
        }
    }
}


function Register-PSMDBuildAction {
<#
    .SYNOPSIS
        Register a new action usable in build projects.
     
    .DESCRIPTION
        Register a new action usable in build projects.
        Actions are the actual implementation logic that turns the configuration in a build project file into ... well, actions.
        Anyway, these are basically named scriptblocks with some metadata.
        This command is used to provide all the builtin actions and can be used to freely define your own actions.
         
        Whenever you use a "script" action in your build projects, consider ... would it make a good configurable option valuable for other builds?
        If so, that might just mark the birth of the next action!
     
    .PARAMETER Name
        The name of the action.
     
    .PARAMETER Action
        The actual code implementing the action.
        Each action scriptblock will receive exactly one .
     
    .PARAMETER Description
        A description explaining what the action is all about.
     
    .PARAMETER Parameters
        The parameters the action accepts.
        Provider a hashtable, with the keys being the parameter names and the values being a description of its parameter.
     
    .EXAMPLE
        PS C:\> Register-PSMDBuildAction -Name 'script' -Action $actionCode -Description 'Execute a custom scriptfile as part of your workflow' -Parameters $parameters
     
        Creates / registers the action "script".
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [ScriptBlock]
        $Action,
        
        [Parameter(Mandatory = $true)]
        [string]
        $Description,
        
        [Parameter(Mandatory = $true)]
        [hashtable]
        $Parameters
    )
    
    process {
        $script:buildActions[$Name] = [pscustomobject]@{
            PSTypeName  = 'PSModuleDevelopment.Build.Action'
            Name        = $Name
            Action        = $Action
            Description = $Description
            Parameters  = $Parameters
        }
    }
}


function Remove-PSMDBuildArtifact
{
<#
    .SYNOPSIS
        Removes an artifact from the build pipeline.
     
    .DESCRIPTION
        Removes an artifact from the build pipeline.
        Only interacts with the PSModuleDevelopment build system.
     
    .PARAMETER Name
        Name of the artifact to remove.
     
    .EXAMPLE
        PS C:\> Remove-PSMDBuildArtifact -Name 'session'
     
        Removes the artifact 'session' from the build pipeline.
     
    .EXAMPLE
        PS C:\> Get-PSMDBuildArtifact -Tag pssession | Remove-PSMDBuildArtifact
     
        Removes all artifacts with the 'pssession' tag from the build pipeline.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Name
    )
    
    process{
        foreach ($nameString in $Name) {
            $script:buildArtifacts.Remove($nameString)
        }
    }
}


function Resolve-PSMDBuildStepParameter {
<#
    .SYNOPSIS
        Resolves and consolidates the overall parameters of a given step.
     
    .DESCRIPTION
        Resolves and consolidates the overall parameters of a given step.
        This ensures that individual actions do not have to implement manual resolution and complex conditionals.
        Sources of parameters:
        - Explicitly defined parameter in the step
        - Value from Artifacts
        - Value from Configuration (only if not otherwise sourced)
        - Value from implicit artifact resolution: Any value that is formatted like this:
          "%!NameOfArtifact!%" will be replaced with the value of the artifact of the same name.
          This supports wildcard resolution, so "%!Session.*!%" will resolve to all artifacts with a name starting with "Session."
         
        Configuration-driven parameters follow this name scheme:
        "PSModuleDevelopment.BuildParam.<project>.<step>.<parameterName>"
 
        For example:
        "PSModuleDevelopment.BuildParam.Admf.connect.credential"
     
    .PARAMETER Parameters
        The hashtable containing the currently specified parameters from the step configuration within the build project file.
        Only settings not already defined there are taken from configuration.
     
    .PARAMETER FromArtifacts
        The hashtable mapping parameters from artifacts.
        This allows dynamically assigning artifacts to parameters.
     
    .PARAMETER ProjectName
        The name of the project being executed.
        Supplementary parameters taken from configuration will pick up settings based on this name:
        "PSModuleDevelopment.BuildParam.<ProjectName>.<StepName>.*"
     
    .PARAMETER StepName
        The name of the step being executed.
        Supplementary parameters taken from configuration will pick up settings based on this name:
        "PSModuleDevelopment.BuildParam.<ProjectName>.<StepName>.*"
     
    .EXAMPLE
        PS C:\> Resolve-PSMDBuildStepParameter -Parameters $actualParameters -ProjectName VMDeployment -StepName 'Create Session'
         
        Adds parameters provided through configuration.
#>

    [OutputType([hashtable])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [hashtable]
        $Parameters,
        
        [Parameter(Mandatory = $true)]
        [hashtable]
        $FromArtifacts,
        
        [Parameter(Mandatory = $true)]
        [string]
        $ProjectName,
        
        [Parameter(Mandatory = $true)]
        [string]
        $StepName
    )
    
    process {
        # Process parameters from Configuration
        $configObject = Select-PSFConfig -FullName "PSModuleDevelopment.BuildParam.$ProjectName.$StepName.*"
        foreach ($property in $configObject.PSObject.Properties) {
            if ($property.Name -in '_Name', '_FullName', '_Depth', '_Children') { continue }
            if ($Parameters.ContainsKey($property.Name)) { continue }
            $Parameters[$property.Name] = $property.Value
        }
        
        # Process parameters from Artifacts
        foreach ($pair in $FromArtifacts.GetEnumerator()) {
            $Parameters[$pair.Key] = (Get-PSMDBuildArtifact -Name $pair.Value).Value
        }
        
        # Resolve implicit artifact references
        foreach ($key in $($Parameters.Keys)) {
            if ($Parameters.$key -notlike '%!*!%') { continue }
            
            $artifactName = $Parameters.$key -replace '^%!(.+?)!%$', '$1'
            $Parameters[$Key] = (Get-PSMDBuildArtifact -Name $artifactName).Value
        }
        
        $Parameters
    }
}

function Select-PSMDBuildProject
{
<#
    .SYNOPSIS
        Set the specified build project as the default project.
     
    .DESCRIPTION
        Set the specified build project as the default project.
        This will have most other commands in this Component automatically use the specified project.
     
    .PARAMETER Path
        Path to the project file to pick.
     
    .PARAMETER Register
        Persist the choice as default build project file across PowerShell sessions.
     
    .EXAMPLE
        PS C:\> Select-PSMDBuildProject -Path 'c:\code\Project\Project.build.json'
     
        Sets the specified build project as the default project.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [switch]
        $Register
    )
    
    process
    {
        Invoke-PSFProtectedCommand -ActionString 'Select-PSMDBuildProject.Testing' -ActionStringValues $Path -ScriptBlock {
            $null = Get-PSMDBuildProject -Path $Path -ErrorAction Stop
        } -Target $Path -EnableException $true -PSCmdlet $PSCmdlet
        Set-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' -Value $Path
        if ($Register) { Register-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' }
    }
}


function Set-PSMDBuildStep {
<#
    .SYNOPSIS
        Create or update a step from a build project.
     
    .DESCRIPTION
        Create or update a step from a build project.
     
    .PARAMETER Name
        The name of the step.
        All step names must be unique within a single build project.
     
    .PARAMETER Weight
        The weight of the step.
        Weight determines processing order, the lower the number the earlier it is executed.
     
    .PARAMETER Action
        The name of the action to execute.
        Use Get-PSMDBuildAction to get a list of available actions.
     
    .PARAMETER Parameters
        The parameters this action should take.
        See the action object to see a description of parameters, including which must be provided and which can be skipped.
     
    .PARAMETER Condition
        A PSFramework filter condition that must apply for this action to be executed successfully.
        Example Conditions:
          Elevated
          PS7Plus -and OSWindows
        More Details: https://psframework.org/documentation/documents/psframework/filters.html
     
    .PARAMETER ConditionSet
        The name of the condition set to use.
        This is part of the PSFramework filter system:
        https://psframework.org/documentation/documents/psframework/filters.html
     
        Specify as "<module> <conditionsetname>" format.
        Default Value: PSFramework Environment
     
    .PARAMETER Dependency
        Any other steps that must successfully finished in order for this step to execute.
        ALL of the listed steps must have succeeded, skipped steps do not count.
     
    .PARAMETER BuildProject
        The build project file to work against.
        Specify the full path to the build project file.
        This parameter can be skipped if a default project file has been defined.
     
    .EXAMPLE
        PS C:\> Set-PSMDBuildStep -Name 'Create Session' -Action new-pssession -Parameters @{ VMName = 'labdc1'; CredentialPath = "%ProjectRoot%\creds\labdc1.cred"; }
     
        Defines a new step named 'Create Session' using the 'new-pssession'-action.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [int]
        $Weight,
        
        [PsfArgumentCompleter('PSModuleDevelopment.Build.Action')]
        [string]
        $Action,
        
        [hashtable]
        $Parameters,
        
        [string]
        $Condition,
        
        [string]
        $ConditionSet,
        
        [string[]]
        $Dependency,
        
        [string]
        $BuildProject
    )
    
    begin {
        $projectPath = $BuildProject
        if (-not $projectPath) { $projectPath = Get-PSFConfigValue -FullName 'PSModuleDevelopment.Build.Project.Selected' }
        if (-not $projectPath) { throw "No Project path specified and none selected!" }
        if (-not (Test-Path -Path $projectPath)) {
            throw "Project file not found: $projectPath"
        }
    }
    process {
        $projectObject = Get-PSMDBuildProject -Path $projectPath | ConvertTo-PSFHashtable
        $stepObject = $projectObject.Steps | Where-Object Name -EQ $Name | ConvertTo-PSFHashtable
        if (-not $stepObject) {
            $stepObject = [pscustomobject]@{
                PSTypeName = 'PSModuleDevelopment.Build.Step'
                Name       = $Name
                Weight       = 50
                Action       = ''
                Parameters = @{ }
                Condition  = ''
                ConditionSet = 'PSFramework Environment'
                Dependency = @()
            }
        }
        if (Test-PSFParameterBinding -ParameterName Weight) { $stepObject.Weight = $Weight }
        if (Test-PSFParameterBinding -ParameterName Action) { $stepObject.Action = $Action }
        if (Test-PSFParameterBinding -ParameterName Parameters) { $stepObject.Parameters = $Parameters }
        if (Test-PSFParameterBinding -ParameterName Condition) { $stepObject.Condition = $Condition }
        if (Test-PSFParameterBinding -ParameterName ConditionSet) { $stepObject.ConditionSet = $ConditionSet }
        if (Test-PSFParameterBinding -ParameterName Dependency) { $stepObject.Dependency = $Dependency }
        
        if (-not $stepObject.Action) {
            throw "Failed to save Build Step $Name : No Action defined!"
        }
        $projectObject.Steps = @($projectObject.Steps | Where-Object Name -ne $Name) + @($stepObject)
        $projectObject | Export-PsmdBuildProjectFile -OutPath $projectPath -ErrorAction Stop
    }
}

function New-PSMDFormatTableDefinition
{
<#
    .SYNOPSIS
        Generates a format XML for the input type.
     
    .DESCRIPTION
        Generates a format XML for the input type.
        Currently, only tables are supported.
         
        Note:
        Loading format files has a measureable impact on module import PER FILE.
        For the sake of performance, you should only generate a single file for an entire module.
         
        You can generate all items in a single call (which will probably be messy on many types at a time)
        Or you can use the -Fragment parameter to create individual fragments, and combine them by passing
        those items again to this command (the final time without the -Fragment parameter).
     
    .PARAMETER InputObject
        The object that will be used to generate the format XML for.
        Will not duplicate its work if multiple object of the same type are passed.
        Accepts objects generated when using the -Fragment parameter, combining them into a single document.
     
    .PARAMETER IncludeProperty
        Only properties in this list will be included.
     
    .PARAMETER ExcludeProperty
        Only properties not in this list will be included.
     
    .PARAMETER IncludePropertyAttribute
        Only properties that have the specified attribute will be included.
     
    .PARAMETER ExcludePropertyAttribute
        Only properties that do NOT have the specified attribute will be included.
     
    .PARAMETER Fragment
        The function will only return a partial Format-XML object (an individual table definition per type).
     
    .PARAMETER DocumentName
        Adds a name to the document generated.
        Purely cosmetic.
     
    .PARAMETER SortColumns
        Enabling this will cause the command to sort columns alphabetically.
        Explicit order styles take precedence over alphabetic sorting.
     
    .PARAMETER ColumnOrder
        Specify a list of properties in the order they should appear.
        For properties with labels: Labels take precedence over selected propertyname.
     
    .PARAMETER ColumnOrderHash
        Allows explicitly defining the order of columns on a per-type basis.
        These hashtables need to have two properties:
        - Type: The name of the type it applies to (e.g.: "System.IO.FileInfo")
        - Properties: The list of properties in the order they should appear.
        Example: @{ Type = "System.IO.FileInfo"; Properties = "Name", "Length", "LastWriteTime" }
        This parameter takes precedence over ColumnOrder in instances where the
        processed typename is explicitly listed in a hashtable.
     
    .PARAMETER ColumnTransformations
        A complex parameter that allows customizing columns or even adding new ones.
        This parameter accepts a hashtable that can ...
        - Set column width
        - Set column alignment
        - Add a script column
        - Assign a label to a column
        It can be targeted by typename as well as propertyname. Possible properties (others will be ignored):
        Content | Type | Possible Hashtable Keys
        Filter: Typename | string | T / Type / TypeName / FilterViewName
        Filter: Property | string | C / Column / Name / P / Property / PropertyName
        Append | bool | A / Append
        ScriptBlock | script | S / Script / ScriptBlock
        Label | string | L / Label
        Width | int | W / Width
        Alignment | string | Align / Alignment
         
        Notes:
        - Append needs to be specified if a new column should be added if no property to override was found.
          Use this to add a completely new column with a ScriptBlock.
        - Alignment: Expects a string, can be any choice of "Left", "Center", "Right"
         
        Example:
        $transform = @{
            Type = "System.IO.FileInfo"
            Append = $true
            Script = { "{0} | {1}" -f $_.Extension, $_.Length }
            Label = "Ext.Length"
            Align = "Left"
        }
     
    .EXAMPLE
        PS C:\> Get-ChildItem | New-PSMDFormatTableDefinition
         
        Generates a format xml for the objects in the current path (files and folders in most cases)
     
    .EXAMPLE
        PS C:\> Get-ChildItem | New-PSMDFormatTableDefinition -IncludeProperty LastWriteTime, FullName
         
        Creates a format xml that only includes the columns LastWriteTime, FullName
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
    [OutputType([PSModuleDevelopment.Format.Document], ParameterSetName = "default")]
    [OutputType([PSModuleDevelopment.Format.TableDefinition], ParameterSetName = "fragment")]
    [CmdletBinding(DefaultParameterSetName = "default")]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        $InputObject,
        
        [string[]]
        $IncludeProperty,
        
        [string[]]
        $ExcludeProperty,
        
        [string]
        $IncludePropertyAttribute,
        
        [string]
        $ExcludePropertyAttribute,
        
        [Parameter(ParameterSetName = "fragment")]
        [switch]
        $Fragment,
        
        [Parameter(ParameterSetName = "default")]
        [string]
        $DocumentName,
        
        [switch]
        $SortColumns,
        
        [string[]]
        $ColumnOrder,
        
        [hashtable[]]
        $ColumnOrderHash,
        
        [PSModuleDevelopment.Format.ColumnTransformation[]]
        $ColumnTransformations
    )
    
    begin
    {
        $typeNames = @()
        
        $document = New-Object PSModuleDevelopment.Format.Document
        $document.Name = $DocumentName
    }
    process
    {
        foreach ($object in $InputObject)
        {
            #region Input Type Processing
            if ($object -is [PSModuleDevelopment.Format.TableDefinition])
            {
                if ($Fragment)
                {
                    $object
                    continue
                }
                else
                {
                    $document.Views.Add($object)
                    continue
                }
            }
            
            if ($object.PSObject.TypeNames[0] -in $typeNames) { continue }
            else { $typeNames += $object.PSObject.TypeNames[0] }
            
            $typeName = $object.PSObject.TypeNames[0]
            #endregion Input Type Processing
            
            #region Process Properties
            $propertyNames = $object.PSOBject.Properties.Name
            if ($IncludeProperty)
            {
                $propertyNames = $propertyNames | Where-Object { $_ -in $IncludeProperty }
            }
            
            if ($ExcludeProperty)
            {
                $propertyNames = $propertyNames | Where-Object { $_ -notin $ExcludeProperty }
            }
            if ($IncludePropertyAttribute)
            {
                $listToInclude = @()
                $object.GetType().GetMembers([System.Reflection.BindingFlags]("FlattenHierarchy, Public, Instance")) | Where-Object { ($_.MemberType -match "Property|Field") -and ($_.CustomAttributes.AttributeType.Name -like $IncludePropertyAttribute) } | ForEach-Object { $listToInclude += $_.Name }
                
                $propertyNames = $propertyNames | Where-Object { $_ -in $listToInclude }
            }
            if ($ExcludePropertyAttribute)
            {
                $listToExclude = @()
                $object.GetType().GetMembers([System.Reflection.BindingFlags]("FlattenHierarchy, Public, Instance")) | Where-Object { ($_.MemberType -match "Property|Field") -and ($_.CustomAttributes.AttributeType.Name -like $ExcludePropertyAttribute) } | ForEach-Object { $listToExclude += $_.Name }
                
                $propertyNames = $propertyNames | Where-Object { $_ -notin $listToExclude }
            }
            
            $table = New-Object PSModuleDevelopment.Format.TableDefinition
            $table.Name = $typeName
            $table.ViewSelectedByType = $typeName
            
            foreach ($name in $propertyNames)
            {
                $column = New-Object PSModuleDevelopment.Format.Column
                $column.PropertyName = $name
                $table.Columns.Add($column)
            }
            
            foreach ($transform in $ColumnTransformations)
            {
                $table.TransformColumn($transform)
            }
            #endregion Process Properties
            
            #region Sort Columns
            if ($SortColumns) { $table.Columns.Sort() }
            
            $appliedOrder = $false
            foreach ($item in $ColumnOrderHash)
            {
                if (($item.Type -eq $typeName) -and ($item.Properties -as [string[]]))
                {
                    [string[]]$props = $item.Properties
                    $table.SetColumnOrder($props)
                    $appliedOrder = $true
                }
            }
            
            if ((-not $appliedOrder) -and ($ColumnOrder))
            {
                $table.SetColumnOrder($ColumnOrder)
            }
            #endregion Sort Columns
            
            $document.Views.Add($table)
            if ($Fragment) { $table }
        }
    }
    end
    {
        $document.Views.Sort()
        if (-not $Fragment) { $document }
    }
}

Function Get-PSMDHelp
{
    <#
        .SYNOPSIS
            Displays localized information about Windows PowerShell commands and concepts.
     
        .DESCRIPTION
            The Get-PSMDHelp function is a wrapper around get-help that allows localizing help queries.
            This is especially useful when developing modules with help in multiple languages.
     
        .PARAMETER Category
            Displays help only for items in the specified category and their aliases. Valid values are Alias, Cmdlet,
            Function, Provider, Workflow, and HelpFile. Conceptual topics are in the HelpFile category.
 
        .PARAMETER Component
            Displays commands with the specified component value, such as "Exchange." Enter a component name. Wildcards are permitted.
 
            This parameter has no effect on displays of conceptual ("About_") help.
 
        .PARAMETER Detailed
            Adds parameter descriptions and examples to the basic help display.
 
            This parameter is effective only when help files are for the command are installed on the computer. It has no effect on displays of conceptual ("About_") help.
 
        .PARAMETER Examples
            Displays only the name, synopsis, and examples. To display only the examples, type "(Get-PSMDHelpEx <cmdlet-name>).Examples".
 
            This parameter is effective only when help files are for the command are installed on the computer. It has no effect on displays of conceptual ("About_") help.
 
        .PARAMETER Full
            Displays the entire help topic for a cmdlet, including parameter descriptions and attributes, examples, input and output object types, and additional notes.
 
            This parameter is effective only when help files are for the command are installed on the computer. It has no effect on displays of conceptual ("About_") help.
 
        .PARAMETER Functionality
            Displays help for items with the specified functionality. Enter the functionality. Wildcards are permitted.
 
            This parameter has no effect on displays of conceptual ("About_") help.
 
        .PARAMETER Name
            Gets help about the specified command or concept. Enter the name of a cmdlet, function, provider, script, or
            workflow, such as "Get-Member", a conceptual topic name, such as "about_Objects", or an alias, such as "ls".
            Wildcards are permitted in cmdlet and provider names, but you cannot use wildcards to find the names of
            function help and script help topics.
 
            To get help for a script that is not located in a path that is listed in the Path environment variable, type
            the path and file name of the script .
 
            If you enter the exact name of a help topic, Get-Help displays the topic contents. If you enter a word or word
            pattern that appears in several help topic titles, Get-Help displays a list of the matching titles. If you
            enter a word that does not match any help topic titles, Get-Help displays a list of topics that include that
            word in their contents.
 
            The names of conceptual topics, such as "about_Objects", must be entered in English, even in non-English versions of Windows PowerShell.
     
        .PARAMETER Language
            Set the language of the help returned. Use 5-digit language codes such as "en-us" or "de-de".
            Note: If PowerShell does not have help in the language specified, it will either return nothing or default back to English
     
        .PARAMETER SetLanguage
            Sets the language of the current and all subsequent help queries. Use 5-digit language codes such as "en-us" or "de-de".
            Note: If PowerShell does not have help in the language specified, it will either return nothing or default back to English
 
        .PARAMETER Online
            Displays the online version of a help topic in the default Internet browser. This parameter is valid only for
            cmdlet, function, workflow and script help topics. You cannot use the Online parameter in Get-Help commands in
            a remote session.
 
            For information about supporting this feature in help topics that you write, see about_Comment_Based_Help
            (http://go.microsoft.com/fwlink/?LinkID=144309), and "Supporting Online Help"
            (http://go.microsoft.com/fwlink/?LinkID=242132), and "How to Write Cmdlet Help"
            (http://go.microsoft.com/fwlink/?LinkID=123415) in the MSDN (Microsoft Developer Network) library.
 
        .PARAMETER Parameter
            Displays only the detailed descriptions of the specified parameters. Wildcards are permitted.
 
            This parameter has no effect on displays of conceptual ("About_") help.
 
        .PARAMETER Path
            Gets help that explains how the cmdlet works in the specified provider path. Enter a Windows PowerShell provider path.
 
            This parameter gets a customized version of a cmdlet help topic that explains how the cmdlet works in the
            specified Windows PowerShell provider path. This parameter is effective only for help about a provider cmdlet
            and only when the provider includes a custom version of the provider cmdlet help topic in its help file. To
            use this parameter, install the help file for the module that includes the provider.
 
            To see the custom cmdlet help for a provider path, go to the provider path location and enter a Get-Help
            command or, from any path location, use the Path parameter of Get-Help to specify the provider path. You can
            also find custom cmdlet help online in the provider help section of the help topics. For example, you can find
            help for the New-Item cmdlet in the Wsman:\*\ClientCertificate path
            (http://go.microsoft.com/fwlink/?LinkID=158676).
 
            For more information about Windows PowerShell providers, see about_Providers
            (http://go.microsoft.com/fwlink/?LinkID=113250).
 
        .PARAMETER Role
            Displays help customized for the specified user role. Enter a role. Wildcards are permitted.
 
            Enter the role that the user plays in an organization. Some cmdlets display different text in their help files
            based on the value of this parameter. This parameter has no effect on help for the core cmdlets.
 
        .PARAMETER ShowWindow
            Displays the help topic in a window for easier reading. The window includes a "Find" search feature and a
            "Settings" box that lets you set options for the display, including options to display only selected sections
            of a help topic.
 
            The ShowWindow parameter supports help topics for commands (cmdlets, functions, CIM commands, workflows,
            scripts) and conceptual "About" topics. It does not support provider help.
 
            This parameter is introduced in Windows PowerShell 3.0.
     
        .EXAMPLE
            PS C:\> Get-PSMDHelp Get-Help "en-us" -Detailed
     
            Gets the detailed help text of Get-Help in English
    #>

    [Alias('hex')]
    [CmdletBinding(DefaultParameterSetName = "AllUsersView")]
    Param (
        [Parameter(ParameterSetName = "Parameters", Mandatory = $true)]
        [System.String]
        $Parameter,
        
        [Parameter(ParameterSetName = "Online", Mandatory = $true)]
        [System.Management.Automation.SwitchParameter]
        $Online,
        
        [Parameter(ParameterSetName = "ShowWindow", Mandatory = $true)]
        [System.Management.Automation.SwitchParameter]
        $ShowWindow,
        
        [Parameter(ParameterSetName = "AllUsersView")]
        [System.Management.Automation.SwitchParameter]
        $Full,
        
        [Parameter(ParameterSetName = "DetailedView", Mandatory = $true)]
        [System.Management.Automation.SwitchParameter]
        $Detailed,
        
        [Parameter(ParameterSetName = "Examples", Mandatory = $true)]
        [System.Management.Automation.SwitchParameter]
        $Examples,
        
        [ValidateSet("Alias", "Cmdlet", "Provider", "General", "FAQ", "Glossary", "HelpFile", "ScriptCommand", "Function", "Filter", "ExternalScript", "All", "DefaultHelp", "Workflow", "DscResource", "Class", "Configuration")]
        [System.String[]]
        $Category,
        
        [System.String[]]
        $Component,
        
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true)]
        [System.String]
        $Name,
        
        [Parameter(Position = 1)]
        [System.String]
        $Language,
        
        [System.String]
        $SetLanguage,
        
        [System.String]
        $Path,
        
        [System.String[]]
        $Functionality,
        
        [System.String[]]
        $Role
    )
    
    Begin
    {
        if (Test-PSFParameterBinding -ParameterName "SetLanguage") { $script:set_language = $SetLanguage }
        if (Test-PSFParameterBinding -ParameterName "Language")
        {
            try { [System.Threading.Thread]::CurrentThread.CurrentUICulture = $Language }
            catch { Write-PSFMessage -Level Warning -Message "Failed to set language" -ErrorRecord $_ -Tag 'fail','language' }
        }
        elseif ($script:set_language)
        {
            try { [System.Threading.Thread]::CurrentThread.CurrentUICulture = $script:set_language }
            catch { Write-PSFMessage -Level Warning -Message "Failed to set language" -ErrorRecord $_ -Tag 'fail', 'language' }
        }
        
        # Prepare Splat for splatting a steppable pipeline
        $splat = $PSBoundParameters | ConvertTo-PSFHashtable -Exclude Language, SetLanguage
        
        try
        {
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-Help', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = { & $wrappedCmd @splat }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline()
            $steppablePipeline.Begin($PSCmdlet)
        }
        catch {    throw }
    }
    Process
    {
        try { $steppablePipeline.Process($_) }
        catch { throw }
    }
    End
    {
        try { $steppablePipeline.End() }
        catch { throw }
    }
}

function Get-PSMDModuleDebug
{
    <#
        .SYNOPSIS
            Retrieves module debugging configurations
         
        .DESCRIPTION
            Retrieves a list of all matching module debugging configurations.
         
        .PARAMETER Filter
            Default: "*"
            A string filter applied to the module name. All modules of matching name (using a -Like comparison) will be returned.
         
        .EXAMPLE
            PS C:\> Get-PSMDModuleDebug -Filter *net*
     
            Returns the module debugging configuration for all modules with a name that contains "net"
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [CmdletBinding()]
    Param (
        [string]
        $Filter = "*"
    )
    
    process
    {
        Import-Clixml -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') | Where-Object {
            ($_.Name -like $Filter) -and ($_.Name.Length -gt 0)
        }
    }
}

function Import-PSMDModuleDebug
{
    <#
        .SYNOPSIS
            Invokes the preconfigured import of a module.
         
        .DESCRIPTION
            Invokes the preconfigured import of a module.
         
        .PARAMETER Name
            The exact name of the module to import using the specified configuration.
         
        .EXAMPLE
            PS C:\> Import-PSMDModuleDebug -Name 'cPSNetwork'
     
            Imports the cPSNetwork module as it was configured to be imported using Set-ModuleDebug.
    #>

    [Alias('ipmod')]
    [CmdletBinding()]
    param (
        [string]
        $Name
    )
    
    process
    {
        # Get original module configuration
        $____module = $null
        $____module = Import-Clixml -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') | Where-Object Name -eq $Name
        if (-not $____module) { throw "No matching module configuration found" }
        
        # Process entry
        if ($____module.DebugMode) { Set-Variable -Scope Global -Name "$($____module.Name)_DebugMode" -Value $____module.DebugMode -Force }
        if ($____module.PreImportAction)
        {
            [System.Management.Automation.ScriptBlock]::Create($____module.PreImportAction).Invoke()
        }
        Import-Module -Name $____module.Name -Scope Global
        if ($____module.PostImportAction)
        {
            [System.Management.Automation.ScriptBlock]::Create($____module.PostImportAction).Invoke()
        }
    }
}

function Remove-PSMDModuleDebug
{
    <#
        .SYNOPSIS
            Removes module debugging configurations.
         
        .DESCRIPTION
            Removes module debugging configurations.
         
        .PARAMETER Name
            Name of modules whose debugging configuration should be removed.
     
        .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
         
        .PARAMETER WhatIf
            If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
         
        .EXAMPLE
            PS C:\> Remove-PSMDModuleDebug -Name "cPSNetwork"
     
            Removes all module debugging configuration for the module cPSNetwork
         
        .NOTES
            Version 1.0.0.0
            Author: Friedrich Weinmann
            Created on: August 7th, 2016
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    Param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0, Mandatory = $true)]
        [string[]]
        $Name
    )
    
    Begin
    {
        $allModules = Import-Clixml -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath')
    }
    Process
    {
        foreach ($nameItem in $Name)
        {
            ($allModules) | Where-Object { $_.Name -like $nameItem } | ForEach-Object {
                if (Test-PSFShouldProcess -Target $_.Name -Action 'Remove from list of modules configured for debugging' -PSCmdlet $PSCmdlet)
                {
                    $Module = $_
                    $allModules = $allModules | Where-Object { $_ -ne $Module }
                }
            }
        }
    }
    End
    {
        Export-Clixml -InputObject $allModules -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') -Depth 99
    }
}

function Set-PSMDModuleDebug
{
    <#
        .SYNOPSIS
            Configures how modules are handled during import of this module.
         
        .DESCRIPTION
            This module allows specifying other modules to import during import of this module.
            Using the Set-PSMDModuleDebug function it is possible to configure, which module is automatically imported, without having to edit the profile each time.
            This import occurs at the end of importing this module, thus setting this module in the profile as automatically imported is recommended.
         
        .PARAMETER Name
            The name of the module to configure for automatic import.
            Needs to be an exact match, the first entry found using "Get-Module -ListAvailable" will be imported.
         
        .PARAMETER AutoImport
            Setting this will cause the module to be automatically imported at the end of importing the PSModuleDevelopment module.
            Even when set to false, the configuration can still be maintained and the debug mode enabled.
         
        .PARAMETER DebugMode
            Setting this will cause the module to create a global variable named "<ModuleName>_DebugMode" with value $true during import of PSModuleDevelopment.
            Modules configured to use this variable can determine the intended import mode using this variable.
         
        .PARAMETER PreImportAction
            Any scriptblock that should run before importing the module.
            Only used when importing modules using the "Invoke-ModuleDebug" funtion, as is used for modules set to auto-import.
         
        .PARAMETER PostImportAction
            Any scriptblock that should run after importing the module.
            Only used when importing modules using the "Invoke-ModuleDebug" funtion, as his used for modules set to auto-import.
     
        .PARAMETER Priority
            When importing modules in a debugging context, they are imported in the order of their priority.
            The lower the number, the sooner it is imported.
         
        .PARAMETER AllAutoImport
            Changes all registered modules to automatically import on powershell launch.
         
        .PARAMETER NoneAutoImport
            Changes all registered modules to not automatically import on powershell launch.
     
        .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
         
        .PARAMETER WhatIf
            If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
         
        .EXAMPLE
            PS C:\> Set-PSMDModuleDebug -Name 'cPSNetwork' -AutoImport
             
            Configures the module cPSNetwork to automatically import after importing PSModuleDevelopment
         
        .EXAMPLE
            PS C:\> Set-PSMDModuleDebug -Name 'cPSNetwork' -AutoImport -DebugMode
             
            Configures the module cPSNetwork to automatically import after importing PSModuleDevelopment using debug mode.
         
        .EXAMPLE
            PS C:\> Set-PSMDModuleDebug -Name 'cPSNetwork' -AutoImport -DebugMode -PreImportAction { Write-Host "Was done before importing" } -PostImportAction { Write-Host "Was done after importing" }
             
            Configures the module cPSNetwork to automatically import after importing PSModuleDevelopment using debug mode.
            - Running a scriptblock before import
            - Running another scriptblock after import
             
            Note: Using Write-Host is generally - but not always - bad practice
            Note: Verbose output during module import is generally discouraged (doesn't apply to tests of course)
    #>

    [Alias('smd')]
    [CmdletBinding(DefaultParameterSetName = "Name", SupportsShouldProcess = $true)]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Name", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('n')]
        [string]
        $Name,
        
        [Parameter(ParameterSetName = 'Name')]
        [Alias('ai')]
        [switch]
        $AutoImport,
        
        [Parameter(ParameterSetName = 'Name')]
        [Alias('dbg')]
        [switch]
        $DebugMode,
        
        [Parameter(ParameterSetName = 'Name')]
        [AllowNull()]
        [System.Management.Automation.ScriptBlock]
        $PreImportAction,
        
        [Parameter(ParameterSetName = 'Name')]
        [AllowNull()]
        [System.Management.Automation.ScriptBlock]
        $PostImportAction,
        
        [Parameter(ParameterSetName = 'Name')]
        [int]
        $Priority = 5,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'AllImport')]
        [Alias('aai')]
        [switch]
        $AllAutoImport,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'NoneImport')]
        [Alias('nai')]
        [switch]
        $NoneAutoImport
    )
    
    process
    {
        #region AllAutoImport
        if ($AllAutoImport)
        {
            $allModules = Import-Clixml (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath')
            if (Test-PSFShouldProcess -Target ($allModules.Name -join ", ") -Action "Configuring modules to automatically import" -PSCmdlet $PSCmdlet)
            {
                foreach ($module in $allModules)
                {
                    $module.AutoImport = $true
                }
                Export-Clixml -InputObject $allModules -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath')
            }
            return
        }
        #endregion AllAutoImport
        
        #region NoneAutoImport
        if ($NoneAutoImport)
        {
            $allModules = Import-Clixml -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath')
            if (Test-PSFShouldProcess -Target ($allModules.Name -join ", ") -Action "Configuring modules to not automatically import" -PSCmdlet $PSCmdlet)
            {
                foreach ($module in $allModules)
                {
                    $module.AutoImport = $false
                }
                Export-Clixml -InputObject $allModules -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath')
            }
            return
        }
        #endregion NoneAutoImport
        
        #region Name
        # Import all module-configurations
        $allModules = Import-Clixml -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath')
        
        # If a configuration already exists, change only those values that were specified
        if ($module = $allModules | Where-Object Name -eq $Name)
        {
            if (Test-PSFParameterBinding -ParameterName "AutoImport") { $module.AutoImport = $AutoImport.ToBool() }
            if (Test-PSFParameterBinding -ParameterName "DebugMode") { $module.DebugMode = $DebugMode.ToBool() }
            if (Test-PSFParameterBinding -ParameterName "PreImportAction") { $module.PreImportAction = $PreImportAction }
            if (Test-PSFParameterBinding -ParameterName "PostImportAction") { $module.PostImportAction = $PostImportAction }
            if (Test-PSFParameterBinding -ParameterName "Priority") { $module.Priority = $Priority }
        }
        # If no configuration exists yet, create a new one with all parameters as specified
        else
        {
            $module = [pscustomobject]@{
                Name       = $Name
                AutoImport = $AutoImport.ToBool()
                DebugMode  = $DebugMode.ToBool()
                PreImportAction = $PreImportAction
                PostImportAction = $PostImportAction
                Priority   = $Priority
            }
        }
        
        # Add new module configuration to all (if any) other previous configurations and export it to config file
        $newModules = @(($allModules | Where-Object Name -ne $Name), $module)
        
        if (Test-PSFShouldProcess -Target $name -Action "Changing debug settings for module" -PSCmdlet $PSCmdlet)
        {
            Export-Clixml -InputObject $newModules -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath')
        }
        #endregion Name
    }
}

function Measure-PSMDCommand
{
    <#
        .SYNOPSIS
            Measures command performance with consecutive tests.
         
        .DESCRIPTION
            This function measures the performance of a scriptblock many consective times.
     
            Warning: Running a command repeatedly may not yield reliable information, since repeated executions may benefit from caching or other performance enhancing features, depending on the script content.
            This is best suited for measuring the performance of tasks that will later be run repeatedly as well.
            It also is useful for mitigating local performance fluctuations when comparing performances.
     
        .PARAMETER ScriptBlock
            The scriptblock whose performance is to be measure.
     
        .PARAMETER Iterations
            How many times should this performance test be repeated.
     
        .PARAMETER TestSet
            Accepts a hashtable, mapping a name to a specific scriptblock to measure.
            This will generate a result grading the performance of the various sets offered.
         
        .EXAMPLE
            PS C:\> Measure-PSMDCommand -ScriptBlock { dir \\Server\share } -Iterations 100
     
            This tries to use Get-ChildItem on a remote directory 100 consecutive times, then measures performance and reports common performance indicators (Average duration, Maximum, Minimum, Total)
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,
        
        [int]
        $Iterations = 1,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Set')]
        [hashtable]
        $TestSet
    )
    
    Process
    {
        #region Running an individual testrun
        if ($ScriptBlock)
        {
            [System.Collections.ArrayList]$results = @()
            $count = 0
            while ($count -lt $Iterations)
            {
                $null = $results.Add((Measure-Command -Expression $ScriptBlock))
                $count++
            }
            $measured = $results | Measure-Object -Maximum -Minimum -Average -Sum -Property Ticks
            [pscustomobject]@{
                PSTypeName = 'PSModuleDevelopment.Performance.TestResult'
                Results = $results.ToArray()
                Max        = (New-Object System.TimeSpan($measured.Maximum))
                Sum        = (New-Object System.TimeSpan($measured.Sum))
                Min        = (New-Object System.TimeSpan($measured.Minimum))
                Average = (New-Object System.TimeSpan($measured.Average))
            }
        }
        #endregion Running an individual testrun
        
        #region Performing a testset
        if ($TestSet)
        {
            $setResult = @{ }
            foreach ($testName in $TestSet.Keys)
            {
                $setResult[$testName] = Measure-PSMDCommand -ScriptBlock $TestSet[$testName] -Iterations $Iterations
            }
            $fastestResult = $setResult.Values | Sort-Object Average | Select-Object -First 1
            
            $finalResult = foreach ($setName in $setResult.Keys)
            {
                $resultItem = $setResult[$setName]
                [pscustomobject]@{
                    PSTypeName = 'PSModuleDevelopment.Performance.TestSetItem'
                    Name = $setName
                    Efficiency = $resultItem.Average.Ticks / $fastestResult.Average.Ticks
                    Average    = $resultItem.Average
                    Result       = $resultItem
                    
                }
            }
            $finalResult | Sort-Object Efficiency
        }
        #endregion Performing a testset
    }
}

function Convert-PSMDMessage
{
<#
    .SYNOPSIS
        Converts a file's use of PSFramework messages to strings.
     
    .DESCRIPTION
        Converts a file's use of PSFramework messages to strings.
     
    .PARAMETER Path
        Path to the file to convert.
     
    .PARAMETER OutPath
        Folder in which to generate the output ps1 and psd1 file.
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .EXAMPLE
        PS C:\> Convert-PSMDMessage -Path 'C:\Scripts\logrotate.ps1' -OutPath 'C:\output'
     
        Converts all instances of writing messages in logrotate.ps1 to use strings instead.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')]
        [string]
        $Path,
        
        [Parameter(Mandatory = $true, Position = 1)]
        [PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')]
        [string]
        $OutPath,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        #region Utility Functions
        function Get-Text
        {
            [OutputType([string])]
            [CmdletBinding()]
            param (
                $Value
            )
            
            if (-not $Value.NestedExpressions) { return $Value.Extent.Text }
            
            $expressions = @{ }
            $expIndex = 0
            
            $builder = [System.Text.StringBuilder]::new()
            $baseIndex = $Value.Extent.StartOffset
            $astIndex = 0
            
            foreach ($nestedExpression in $Value.NestedExpressions)
            {
                $null = $builder.Append($Value.Extent.Text.SubString($astIndex, ($nestedExpression.Extent.StartOffset - $baseIndex - $astIndex)).Replace("{", "{{").Replace('}', '}}'))
                $astIndex = $nestedExpression.Extent.EndOffset - $baseIndex
                
                if ($expressions.ContainsKey($nestedExpression.Extent.Text)) { $effectiveIndex = $expressions[$nestedExpression.Extent.Text] }
                else
                {
                    $expressions[$nestedExpression.Extent.Text] = $expIndex
                    $effectiveIndex = $expIndex
                    $expIndex++
                }
                
                $null = $builder.Append("{$effectiveIndex}")
            }
            
            $null = $builder.Append($Value.Extent.Text.SubString($astIndex).Replace("{", "{{").Replace('}', '}}'))
            $builder.ToString()
        }
        
        function Get-Insert
        {
            [OutputType([string])]
            [CmdletBinding()]
            param (
                $Value
            )
            
            if (-not $Value.NestedExpressions) { return "" }
            
            $processed = @{ }
            $elements = foreach ($nestedExpression in $Value.NestedExpressions)
            {
                if ($processed[$nestedExpression.Extent.Text]) { continue }
                else { $processed[$nestedExpression.Extent.Text] = $true }
                
                if ($nestedExpression -is [System.Management.Automation.Language.SubExpressionAst])
                {
                    if (
                        ($nestedExpression.SubExpression.Statements.Count -eq 1) -and
                        ($nestedExpression.SubExpression.Statements[0].PipelineElements.Count -eq 1) -and
                        ($nestedExpression.SubExpression.Statements[0].PipelineElements[0].Expression -is [System.Management.Automation.Language.MemberExpressionAst])
                    ) { $nestedExpression.SubExpression.Extent.Text }
                    else { $nestedExpression.Extent.Text.SubString(1) }
                }
                else { $nestedExpression.Extent.Text }
            }
            $elements -join ", "
        }
        #endregion Utility Functions
        
        $parameterMapping = @{
            'Message' = 'String'
            'Action'  = 'ActionString'
        }
        $insertMapping = @{
            'String' = '-StringValues'
            'Action' = '-ActionStringValues'
        }
    }
    process
    {
        $ast = (Read-PSMDScript -Path $Path).Ast
        
        #region Parse Input
        $functionName = (Get-Item $Path).BaseName
        
        $commandAsts = $ast.FindAll({
                if ($args[0] -isnot [System.Management.Automation.Language.CommandAst]) { return $false }
                if ($args[0].CommandElements[0].Value -notmatch '^Invoke-PSFProtectedCommand$|^Write-PSFMessage$|^Stop-PSFFunction$|^Test-PSFShouldProcess$') { return $false }
                if (-not ($args[0].CommandElements.ParameterName -match '^Message$|^Action$')) { return $false }
                $true
            }, $true)
        if (-not $commandAsts)
        {
            Write-PSFMessage -Level Host -String 'Convert-PSMDMessage.Parameter.NonAffected' -StringValues $Path
            return
        }
        #endregion Parse Input
        
        #region Build Replacements table
        $currentCount = 1
        $replacements = foreach ($command in $commandAsts)
        {
            $parameter = $command.CommandElements | Where-Object ParameterName -in 'Message', 'Action'
            $paramIndex = $command.CommandElements.IndexOf($parameter)
            $parameterValue = $command.CommandElements[$paramIndex + 1]
            
            [PSCustomObject]@{
                OriginalText = $parameterValue.Value
                Text         = Get-Text -Value $parameterValue
                Inserts         = Get-Insert -Value $parameterValue
                String         = "$($functionName).Message$($currentCount)"
                StartOffset  = $parameter.Extent.StartOffset
                EndOffset    = $parameterValue.Extent.EndOffset
                OldParameterName = $parameter.ParameterName
                NewParameterName = $parameterMapping[$parameter.ParameterName]
                Parameter    = $parameter
                ParameterValue = $parameterValue
            }
            $currentCount++
        }
        #endregion Build Replacements table
        
        #region Calculate new text body
        $fileText = [System.IO.File]::ReadAllText((Resolve-PSFPath -Path $Path))
        $builder = [System.Text.StringBuilder]::new()
        $index = 0
        foreach ($replacement in $replacements)
        {
            $null = $builder.Append($fileText.Substring($index, ($replacement.StartOffset - $index)))
            $null = $builder.Append("-$($replacement.NewParameterName) '$($replacement.String)'")
            if ($replacement.Inserts) { $null = $builder.Append(" $($insertMapping[$replacement.NewParameterName]) $($replacement.Inserts)") }
            $index = $replacement.EndOffset
        }
        $null = $builder.Append($fileText.Substring($index))
        $newDefinition = $builder.ToString()
        $testResult = Read-PSMDScript -ScriptCode ([Scriptblock]::create($newDefinition))
        
        if ($testResult.Errors)
        {
            Stop-PSFFunction -String 'Convert-PSMDMessage.SyntaxError' -StringValues $Path -Target $Path -EnableException $EnableException
            return
        }
        #endregion Calculate new text body
        
        $resolvedOutPath = Resolve-PSFPath -Path $OutPath
        $encoding = [System.Text.UTF8Encoding]::new($true)
        $filePath = Join-Path -Path $resolvedOutPath -ChildPath "$functionName.ps1"
        [System.IO.File]::WriteAllText($filePath, $newDefinition, $encoding)
        $stringsPath = Join-Path -Path $resolvedOutPath -ChildPath "$functionName.psd1"
        $stringsText = @"
@{
$($replacements | Format-String "`t'{0}' = {1} # {2}" -Property String, Text, Inserts | Join-String -Separator "`n")
}
"@

        [System.IO.File]::WriteAllText($stringsPath, $stringsText, $encoding)
    }
}

function Export-PSMDString
{
<#
    .SYNOPSIS
        Parses a module that uses the PSFramework localization feature for strings and their value.
 
    .DESCRIPTION
        Parses a module that uses the PSFramework localization feature for strings and their value.
        This command can be used to generate and update the language files used by the module.
        It is also used in automatic tests, ensuring no abandoned string has been left behind and no key is unused.
 
    .PARAMETER ModuleRoot
        The root of the module to process.
        Must be the root folder where the psd1 file is stored in.
 
    .EXAMPLE
        PS C:\> Export-PSMDString -ModuleRoot 'C:\Code\Github\MyModuleProject\MyModule'
 
        Generates the strings data for the MyModule module.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('ModuleBase')]
        [string]
        $ModuleRoot
    )
    
    process
    {
        #region Find Language Files : $languageFiles
        $languageFiles = @{ }
        $languageFolders = Get-ChildItem -Path $ModuleRoot -Directory | Where-Object Name -match '^\w\w-\w\w$'
        foreach ($languageFolder in $languageFolders)
        {
            $languageFiles[$languageFolder.Name] = @{ }
            foreach ($file in (Get-ChildItem -Path $languageFolder.FullName -Filter *.psd1))
            {
                $languageFiles[$languageFolder.Name] += Import-PSFPowerShellDataFile -Path $file.FullName
            }
        }
        #endregion Find Language Files : $languageFiles
        
        #region Find Keys : $foundKeys
        $foundKeys = foreach ($file in (Get-ChildItem -Path $ModuleRoot -Recurse | Where-Object Extension -match '^\.ps1$|^\.psm1$'))
        {
            $ast = (Read-PSMDScript -Path $file.FullName).Ast
            #region Command Parameters
            $commandAsts = $ast.FindAll({
                    if ($args[0] -isnot [System.Management.Automation.Language.CommandAst]) { return $false }
                    if ($args[0].CommandElements[0].Value -notmatch '^Invoke-PSFProtectedCommand$|^Write-PSFMessage$|^Stop-PSFFunction$|^Test-PSFShouldProcess$|^Get-PSFLocalizedString$') { return $false }
                    if (-not ($args[0].CommandElements.ParameterName -match '^String$|^ActionString$|^Name$')) { return $false }
                    $true
                }, $true)
            
            foreach ($commandAst in $commandAsts)
            {
                $stringParam = $commandAst.CommandElements | Where-Object ParameterName -match '^String$|^ActionString$|^Name$'
                $stringParamValue = $commandAst.CommandElements[($commandAst.CommandElements.IndexOf($stringParam) + 1)].Value
                
                $stringValueParam = $commandAst.CommandElements | Where-Object ParameterName -match '^StringValues$|^ActionStringValues$|^Name$'
                if ($stringValueParam)
                {
                    $stringValueParamValue = $commandAst.CommandElements[($commandAst.CommandElements.IndexOf($stringValueParam) + 1)].Extent.Text
                }
                else { $stringValueParamValue = '' }
                [PSCustomObject]@{
                    PSTypeName = 'PSModuleDevelopment.String.ParsedItem'
                    File       = $file.FullName
                    Line       = $commandAst.Extent.StartLineNumber
                    CommandName = $commandAst.CommandElements[0].Value
                    String       = $stringParamValue
                    StringValues = $stringValueParamValue
                }
            }
            #endregion Command Parameters
            
            #region Splatted Variables
            $splattedVariables = $ast.FindAll({
                    if ($args[0] -isnot [System.Management.Automation.Language.VariableExpressionAst]) { return $false }
                    if (-not ($args[0].Splatted -eq $true)) { return $false }
                    try { if ($args[0].Parent.CommandElements[0].Value -notmatch '^Invoke-PSFProtectedCommand$|^Write-PSFMessage$|^Stop-PSFFunction$|^Test-PSFShouldProcess$') { return $false } }
                    catch { return $false }
                    $true
                }, $true)
            
            foreach ($splattedVariable in $splattedVariables)
            {
                $splatParamName = $splattedVariable.VariablePath.UserPath
                
                $splatAssignmentAsts = $ast.FindAll({
                        if ($args[0] -isnot [System.Management.Automation.Language.AssignmentStatementAst]) { return $false }
                        if ($args[0].Left.VariablePath.userPath -ne $splatParamName) { return $false }
                        if ($args[0].Operator -ne 'Equals') { return $false }
                        if ($args[0].Right.Expression -isnot [System.Management.Automation.Language.HashtableAst]) { return $false }
                        $keys = $args[0].Right.Expression.KeyValuePairs.Item1.Value
                        if (($keys -notcontains 'String') -and ($keys -notcontains 'ActionString')) { return $false }
                        
                        $true
                    }, $true)
                
                foreach ($splatAssignmentAst in $splatAssignmentAsts)
                {
                    $splatHashTable = $splatAssignmentAst.Right.Expression
                    
                    $splatParam = $splathashTable.KeyValuePairs | Where-Object Item1 -in 'String', 'ActionString'
                    $splatValueParam = $splathashTable.KeyValuePairs | Where-Object Item1 -in 'StringValues', 'ActionStringValues'
                    if ($splatValueParam)
                    {
                        $splatValueParamValue = $splatValueParam.Item2.Extent.Text
                    }
                    else { $splatValueParamValue = '' }
                    
                    [PSCustomObject]@{
                        PSTypeName = 'PSModuleDevelopment.String.ParsedItem'
                        File       = $file.FullName
                        Line       = $splatHashTable.Extent.StartLineNumber
                        CommandName = $splattedVariable.Parent.CommandElements[0].Value
                        String       = $splatParam.Item2.Extent.Text.Trim("'").Trim('"')
                        StringValues = $splatValueParamValue
                    }
                }
            }
            #endregion Splatted Variables
            
            #region Attributes
            $validateAsts = $ast.FindAll({
                    if ($args[0] -isnot [System.Management.Automation.Language.AttributeAst]) { return $false }
                    if ($args[0].TypeName -notmatch '^PsfValidateScript$|^PsfValidatePattern$') { return $false }
                    if (-not ($args[0].NamedArguments.ArgumentName -eq 'ErrorString')) { return $false }
                    $true
                }, $true)
            
            foreach ($validateAst in $validateAsts)
            {
                [PSCustomObject]@{
                    PSTypeName = 'PSModuleDevelopment.String.ParsedItem'
                    File       = $file.FullName
                    Line       = $commandAst.Extent.StartLineNumber
                    CommandName = '[{0}]' -f $validateAst.TypeName
                    String       = (($validateAst.NamedArguments | Where-Object ArgumentName -eq 'ErrorString').Argument.Value -split "\.", 2)[1] # The first element is the module element
                    StringValues = '<user input>, <validation item>'
                }
            }
            #endregion Attributes
        }
        #endregion Find Keys : $foundKeys
        
        #region Report Findings
        $totalResults = foreach ($languageFile in $languageFiles.Keys)
        {
            #region Phase 1: Matching parsed strings to language file
            $results = @{ }
            foreach ($foundKey in $foundKeys)
            {
                if ($results[$foundKey.String])
                {
                    $results[$foundKey.String].Entries += $foundKey
                    continue
                }
                
                $results[$foundKey.String] = [PSCustomObject] @{
                    PSTypeName = 'PSmoduleDevelopment.String.LanguageFinding'
                    Language   = $languageFile
                    Surplus    = $false
                    String       = $foundKey.String
                    StringValues = $foundKey.StringValues
                    Text       = $languageFiles[$languageFile][$foundKey.String]
                    Line       = "'{0}' = '{1}' # {2}" -f $foundKey.String, $languageFiles[$languageFile][$foundKey.String], $foundKey.StringValues
                    Entries    = @($foundKey)
                }
            }
            $results.Values
            #endregion Phase 1: Matching parsed strings to language file
            
            #region Phase 2: Finding unneeded strings
            foreach ($key in $languageFiles[$languageFile].Keys)
            {
                if ($key -notin $foundKeys.String)
                {
                    [PSCustomObject] @{
                        PSTypeName   = 'PSmoduleDevelopment.String.LanguageFinding'
                        Language     = $languageFile
                        Surplus         = $true
                        String         = $key
                        StringValues = ''
                        Text         = $languageFiles[$languageFile][$key]
                        Line         = ''
                        Entries         = @()
                    }
                }
            }
            #endregion Phase 2: Finding unneeded strings
        }
        $totalResults | Sort-Object String
        #endregion Report Findings
    }
}

function Format-PSMDParameter
{
    <#
        .SYNOPSIS
            Formats the parameter block on commands.
         
        .DESCRIPTION
            Formats the parameter block on commands.
            This function will convert legacy functions that have their parameters straight behind their command name.
            It also fixes missing CmdletBinding attributes.
     
            Nested commands will also be affected.
         
        .PARAMETER FullName
            The file to process
         
        .PARAMETER DisableCache
            By default, this command caches the results of its execution in the PSFramework result cache.
            This information can then be retrieved for the last command to do so by running Get-PSFResultCache.
            Setting this switch disables the caching of data in the cache.
     
        .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
         
        .PARAMETER WhatIf
            If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
         
        .EXAMPLE
            PS C:\> Get-ChildItem .\functions\*\*.ps1 | Set-PSMDCmdletBinding
     
            Updates all commands in the module to have a cmdletbinding attribute.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $FullName,
        
        [switch]
        $DisableCache
    )
    
    begin
    {
        #region Utility functions
        function Invoke-AstWalk
        {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
            [CmdletBinding()]
            param (
                $Ast,
                
                [string[]]
                $Command,
                
                [string[]]
                $Name,
                
                [string]
                $NewName,
                
                [bool]
                $IsCommand,
                
                [bool]
                $NoAlias
            )
            
            #Write-PSFMessage -Level Host -Message "Processing $($Ast.Extent.StartLineNumber) | $($Ast.Extent.File) | $($Ast.GetType().FullName)"
            $typeName = $Ast.GetType().FullName
            
            switch ($typeName)
            {
                "System.Management.Automation.Language.FunctionDefinitionAst"
                {
                    #region Has no param block
                    if ($null -eq $Ast.Body.ParamBlock)
                    {
                        $baseIndent = $Ast.Extent.Text.Split("`n")[0] -replace "^(\s{0,}).*", '$1'
                        $indent = $baseIndent + "`t"
                        
                        # Kill explicit parameter section behind name
                        $startIndex = "function ".Length + $Ast.Name.Length
                        $endIndex = $Ast.Extent.Text.IndexOf("{")
                        Add-FileReplacement -Path $ast.Extent.File -Start ($Ast.Extent.StartOffset + $startIndex) -Length ($endIndex - $startIndex) -NewContent "`n"
                        
                        $baseParam = @"
$($indent)[CmdletBinding()]
$($indent)param (
{0}
$($indent))
"@

                        $parameters = @()
                        $paramIndent = $indent + "`t"
                        foreach ($parameter in $Ast.Parameters)
                        {
                            $defaultValue = ""
                            if ($parameter.DefaultValue) { $defaultValue = " = $($parameter.DefaultValue.Extent.Text)" }
                            $values = @()
                            foreach ($attribute in $parameter.Attributes)
                            {
                                $values += "$($paramIndent)$($attribute.Extent.Text)"
                            }
                            $values += "$($paramIndent)$($parameter.Name.Extent.Text)$($defaultValue)"
                            $parameters += $values -join "`n"
                        }
                        $baseParam = $baseParam -f ($parameters -join ",`n`n")
                        
                        Add-FileReplacement -Path $ast.Extent.File -Start $Ast.Body.Extent.StartOffset -Length 1 -NewContent "{`n$($baseParam)"
                    }
                    #endregion Has no param block
                    
                    #region Has a param block, but no cmdletbinding
                    if (($null -ne $Ast.Body.ParamBlock) -and (-not ($Ast.Body.ParamBlock.Attributes | Where-Object TypeName -Like "CmdletBinding")))
                    {
                        $text = [System.IO.File]::ReadAllText($Ast.Extent.File)
                        
                        $index = $Ast.Body.ParamBlock.Extent.StartOffset
                        while (($index -gt 0) -and ($text.Substring($index, 1) -ne "`n")) { $index = $index - 1 }
                        
                        $indentIndex = $index + 1
                        $indent = $text.Substring($indentIndex, ($Ast.Body.ParamBlock.Extent.StartOffset - $indentIndex))
                        Add-FileReplacement -Path $Ast.Body.ParamBlock.Extent.File -Start $indentIndex -Length ($Ast.Body.ParamBlock.Extent.StartOffset - $indentIndex) -NewContent "$($indent)[CmdletBinding()]`n$($indent)"
                    }
                    #endregion Has a param block, but no cmdletbinding
                    
                    Invoke-AstWalk -Ast $Ast.Body -Command $Command -Name $Name -NewName $NewName -IsCommand $false
                }
                default
                {
                    foreach ($property in $Ast.PSObject.Properties)
                    {
                        if ($property.Name -eq "Parent") { continue }
                        if ($null -eq $property.Value) { continue }
                        
                        if (Get-Member -InputObject $property.Value -Name GetEnumerator -MemberType Method)
                        {
                            foreach ($item in $property.Value)
                            {
                                if ($item.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast")
                                {
                                    Invoke-AstWalk -Ast $item -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand
                                }
                            }
                            continue
                        }
                        
                        if ($property.Value.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast")
                        {
                            Invoke-AstWalk -Ast $property.Value -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand
                        }
                    }
                }
            }
        }
        
        function Add-FileReplacement
        {
            [CmdletBinding()]
            param (
                [string]
                $Path,
                
                [int]
                $Start,
                
                [int]
                $Length,
                
                [string]
                $NewContent
            )
            Write-PSFMessage -Level Verbose -Message "Change Submitted: $Path | $Start | $Length | $NewContent" -Tag 'update', 'change', 'file'
            
            if (-not $globalFunctionHash.ContainsKey($Path))
            {
                $globalFunctionHash[$Path] = @()
            }
            
            $globalFunctionHash[$Path] += New-Object PSObject -Property @{
                Content = $NewContent
                Start   = $Start
                Length  = $Length
            }
        }
        
        function Apply-FileReplacement
        {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")]
            [CmdletBinding()]
            param (
                
            )
            
            foreach ($key in $globalFunctionHash.Keys)
            {
                $value = $globalFunctionHash[$key] | Sort-Object Start
                $content = [System.IO.File]::ReadAllText($key)
                
                $newString = ""
                $currentIndex = 0
                
                foreach ($item in $value)
                {
                    $newString += $content.SubString($currentIndex, ($item.Start - $currentIndex))
                    $newString += $item.Content
                    $currentIndex = $item.Start + $item.Length
                }
                
                $newString += $content.SubString($currentIndex)
                
                [System.IO.File]::WriteAllText($key, $newString)
                #$newString
            }
        }
        
        function Write-Issue
        {
            [CmdletBinding()]
            param (
                $Extent,
                
                $Data,
                
                [string]
                $Type
            )
            
            New-Object PSObject -Property @{
                Type = $Type
                Data = $Data
                File = $Extent.File
                StartLine = $Extent.StartLineNumber
                Text = $Extent.Text
            }
        }
        #endregion Utility functions
    }
    process
    {
        foreach ($path in $FullName)
        {
            $globalFunctionHash = @{ }
            
            $tokens = $null
            $parsingError = $null
            $ast = [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$parsingError)
            
            Write-PSFMessage -Level VeryVerbose -Message "Ensuring Cmdletbinding for all functions in $path" -Tag 'start' -Target $Name
            $issues += Invoke-AstWalk -Ast $ast -Command $Command -Name $Name -NewName $NewName -IsCommand $false
            
            Set-PSFResultCache -InputObject $issues -DisableCache $DisableCache
            if ($PSCmdlet.ShouldProcess($path, "Set CmdletBinding attribute"))
            {
                Apply-FileReplacement
            }
            $issues
        }
    }
}

function Get-PSMDFileCommand
{
<#
    .SYNOPSIS
        Parses a scriptfile and returns the contained/used commands.
     
    .DESCRIPTION
        Parses a scriptfile and returns the contained/used commands.
        Use this to determine, what command resources are being used.
     
    .PARAMETER Path
        The path to the scriptfile to parse.
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .EXAMPLE
        PS C:\> Get-PSMDFileCommand -Path './task_usersync.ps1'
     
        Parses the scriptfile task_usersync.ps1 for commands used.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfValidateScript('PSModuleDevelopment.Validate.Path', ErrorString = 'PSModuleDevelopment.Validate.Path')]
        [string[]]
        $Path,
        
        [switch]
        $EnableException
    )
    
    process
    {
        foreach ($pathItem in $Path)
        {
            # Skip Folders
            if (-not (Test-Path -Path $pathItem -PathType Leaf)) { continue }
            
            $parsedCode = Read-PSMDScript -Path $pathItem
            if ($parsedCode.Errors)
            {
                Stop-PSFFunction -String 'Get-PSMDFileCommand.SyntaxError' -StringValues $pathItem -EnableException $EnableException -Continue
            }
            
            $results = @{ }
            $commands = $parsedCode.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true)
            $internalCommands = $parsedCode.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true).Name
            
            foreach ($command in $commands)
            {
                if (-not $results[$command.CommandElements[0].Value])
                {
                    $commandInfo = Get-Command $command.CommandElements[0].Value -ErrorAction Ignore
                    $module = $commandInfo.Module
                    if (-not $module) { $module = $commandInfo.PSSnapin }
                    $results[$command.CommandElements[0].Value] = [pscustomobject]@{
                        PSTypeName = 'PSModuleDevelopment.File.Command'
                        File       = Get-Item $pathItem
                        Name       = $command.CommandElements[0].Value
                        Parameters = @{ }
                        Count       = 0
                        AstObjects = @()
                        CommandInfo = $commandInfo
                        Module       = $module
                        Internal   = $command.CommandElements[0].Value -in $internalCommands
                        Path       = $pathItem
                    }
                }
                $object = $results[$command.CommandElements[0].Value]
                $object.Count = $object.Count + 1
                $object.AstObjects += $command
                foreach ($parameter in $command.CommandElements.Where{ $_ -is [System.Management.Automation.Language.CommandParameterAst] })
                {
                    if (-not $object.Parameters[$parameter.ParameterName]) { $object.Parameters[$parameter.ParameterName] = 1 }
                    else { $object.Parameters[$parameter.ParameterName] = $object.Parameters[$parameter.ParameterName] + 1 }
                }
            }
            
            $results.Values
        }
    }
}

function Read-PSMDScript
{
<#
    .SYNOPSIS
        Parse the content of a script
     
    .DESCRIPTION
        Uses the powershell parser to parse the content of a script or scriptfile.
     
    .PARAMETER ScriptCode
        The scriptblock to parse.
     
    .PARAMETER Path
        Path to the scriptfile to parse.
        Silently ignores folder objects.
     
    .EXAMPLE
        PS C:\> Read-PSMDScript -ScriptCode $ScriptCode
     
        Parses the code in $ScriptCode
     
    .EXAMPLE
        PS C:\> Get-ChildItem | Read-PSMDScript
     
        Parses all script files in the current directory
#>

    [Alias('parse')]
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, ParameterSetName = 'Script', Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        $ScriptCode,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'File', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [string[]]
        $Path
    )
    
    begin
    {
        Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param'
    }
    process
    {
        foreach ($file in $Path)
        {
            Write-PSFMessage -Level Verbose -Message "Processing $file" -Target $file
            $item = Get-Item $file
            if ($item.PSIsContainer)
            {
                Write-PSFMessage -Level Verbose -Message "is folder, skipping $file" -Target $file
                continue
            }
            
            $tokens = $null
            $errors = $null
            $ast = [System.Management.Automation.Language.Parser]::ParseFile($item.FullName, [ref]$tokens, [ref]$errors)
            [pscustomobject]@{
                PSTypeName = 'PSModuleDevelopment.Meta.ParseResult'
                Ast           = $ast
                Tokens       = $tokens
                Errors       = $errors
                File       = $item.FullName
            }
        }
        
        if ($ScriptCode)
        {
            $tokens = $null
            $errors = $null
            $ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptCode, [ref]$tokens, [ref]$errors)
            [pscustomobject]@{
                PSTypeName = 'PSModuleDevelopment.Meta.ParseResult'
                Ast           = $ast
                Tokens       = $tokens
                Errors       = $errors
                Source       = $ScriptCode
            }
        }
    }
}

function Rename-PSMDParameter
{
    <#
        .SYNOPSIS
            Renames a parameter of a function.
         
        .DESCRIPTION
            This command is designed to rename the parameter of a function within an entire module.
            By default it will add an alias for the previous command name.
             
            In order for this to work you need to consider to have the command / module imported.
            Hint: Import the psm1 file for best results.
             
            It will then search all files in the specified path (hint: Specify module root for best results), and update all psm1/ps1 files.
            At the same time it will force all commands to call the parameter by its new standard, even if they previously used an alias for the parameter.
             
            While this command was designed to work with a module, it is not restricted to that:
            You can load a standalone function and specify a path with loose script files for the same effect.
             
            Note:
            You can also use this to update your scripts, after a foreign module introduced a breaking change by renaming a parameter.
            In this case, import the foreign module to see the function, but point it at the base path of your scripts to update.
            The loaded function is only used for alias/parameter alias resolution
         
        .PARAMETER Path
            The path to the root folder where all the files are stored.
            It will search the folder recursively and ignore hidden files & folders.
         
        .PARAMETER Command
            The name of the function, whose parameter should be changed.
            Most be loaded into the current runtime.
         
        .PARAMETER Name
            The name of the parameter to change.
         
        .PARAMETER NewName
            The new name for the parameter.
            Do not specify "-" or the "$" symbol
         
        .PARAMETER NoAlias
            Avoid creating an alias for the old parameter name.
            This may cause a breaking change!
     
        .PARAMETER WhatIf
            Prevents the command from updating the files.
            Instead it will return the strings of all its changes.
         
        .PARAMETER EnableException
            Replaces user friendly yellow warnings with bloody red exceptions of doom!
            Use this if you want the function to throw terminating errors you want to catch.
         
        .PARAMETER DisableCache
            By default, this command caches the results of its execution in the PSFramework result cache.
            This information can then be retrieved for the last command to do so by running Get-PSFResultCache.
            Setting this switch disables the caching of data in the cache.
         
        .EXAMPLE
            PS C:\> Rename-PSMDParameter -Path 'C:\Scripts\Modules\MyModule' -Command 'Get-Test' -Name 'Foo' -NewName 'Bar'
             
            Renames the parameter 'Foo' of the command 'Get-Test' to 'Bar' for all scripts stored in 'C:\Scripts\Modules\MyModule'
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSupportsShouldProcess", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [Parameter(Mandatory = $true)]
        [string[]]
        $Command,
        
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [string]
        $NewName,
        
        [switch]
        $NoAlias,
        
        [switch]
        $WhatIf,
        
        [switch]
        $EnableException,
        
        [switch]
        $DisableCache
    )
    
    # Global Store for pending file updates
    # Exempt from Scope Boundary violation rule, since only accessed using dedicated helper function
    $globalFunctionHash = @{ }
    
    #region Helper Functions
    function Invoke-AstWalk
    {
        [CmdletBinding()]
        Param (
            $Ast,
            
            [string[]]
            $Command,
            
            [string[]]
            $Name,
            
            [string]
            $NewName,
            
            [bool]
            $IsCommand,
            
            [bool]
            $NoAlias
        )
        
        #Write-PSFMessage -Level Host -Message "Processing $($Ast.Extent.StartLineNumber) | $($Ast.Extent.File) | $($Ast.GetType().FullName)"
        $typeName = $Ast.GetType().FullName
        
        switch ($typeName)
        {
            "System.Management.Automation.Language.CommandAst"
            {
                Write-PSFMessage -Level Verbose -Message "Line $($Ast.Extent.StartLineNumber): Processing Command Ast: <c='em'>$($Ast.Extent.ToString())</c>"
                
                $commandName = $Ast.CommandElements[0].Value
                $resolvedCommand = $commandName
                
                if (Test-Path function:\$commandName)
                {
                    $resolvedCommand = (Get-Item function:\$commandName).Name
                }
                if (Test-Path alias:\$commandName)
                {
                    $resolvedCommand = (Get-Item alias:\$commandName).ResolvedCommand.Name
                }
                
                if ($resolvedCommand -in $Command)
                {
                    $parameters = $Ast.CommandElements | Where-Object { $_.GetType().FullName -eq "System.Management.Automation.Language.CommandParameterAst" }
                    
                    foreach ($parameter in $parameters)
                    {
                        if ($parameter.ParameterName -in $Name)
                        {
                            Write-PSFMessage -Level SomewhatVerbose -Message "Found parameter: <c='em'>$($parameter.ParameterName)</c>"
                            Update-CommandParameter -Ast $parameter -NewName $NewName
                        }
                    }
                    
                    $splatted = $Ast.CommandElements | Where-Object Splatted
                    
                    if ($splatted)
                    {
                        foreach ($splat in $splatted)
                        {
                            Write-PSFMessage -Level Warning -FunctionName Rename-PSMDParameter -Message "Splat detected! Manually verify $($splat.Extent.Text) at line $($splat.Extent.StartLineNumber) in file $($splat.Extent.File)" -Tag 'splat','fail','manual'
                            Write-Issue -Extent $splat.Extent -Data $Ast -Type "SplattedParameter"
                        }
                    }
                }
                
                foreach ($element in $Ast.CommandElements)
                {
                    if ($element.GetType().FullName -ne "System.Management.Automation.Language.CommandParameterAst")
                    {
                        Invoke-AstWalk -Ast $element -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand -NoAlias $NoAlias
                    }
                }
            }
            "System.Management.Automation.Language.FunctionDefinitionAst"
            {
                if ($Ast.Name -In $Command)
                {
                    foreach ($parameter in $Ast.Body.ParamBlock.Parameters)
                    {
                        if ($Name[0] -ne $parameter.Name.VariablePath.UserPath) { continue }
                        
                        $stringExtent = $parameter.Extent.ToString()
                        $lines = $stringExtent.Split("`n")
                        $multiLine = $lines -gt 1
                        $indent = 0
                        $indentStyle = "`t"
                        
                        if ($multiLine)
                        {
                            if ($lines[1][0] -eq " ") { $indentStyle = " " }
                            $indent = $lines[1].Length - $lines[1].Trim().Length
                        }
                        
                        $aliases = @()
                        foreach ($attribute in $parameter.Attributes)
                        {
                            if ($attribute.TypeName.FullName -eq "Alias") { $aliases += $attribute }
                        }
                        
                        $aliasNames = $aliases.PositionalArguments.Value
                        if ($aliasNames -contains $NewName) { $aliasNames = $aliasNames | Where-Object { $_ -ne $NewName } }
                        if (-not $NoAlias) { $aliasNames += $Name }
                        $aliasNames = $aliasNames | Select-Object -Unique | Sort-Object
                        
                        if ($aliasNames)
                        {
                            if ($aliases)
                            {
                                $newAlias = "[Alias($("'" + ($aliasNames -join "','")+ "'"))]"
                                Add-FileReplacement -Path $aliases[0].Extent.File -Start $aliases[0].Extent.StartOffset -Length ($aliases[0].Extent.EndOffset - $aliases[0].Extent.StartOffset) -NewContent $newAlias
                                Add-FileReplacement -Path $parameter.Name.Extent.File -Start $parameter.Name.Extent.StartOffset -Length ($parameter.Name.Extent.EndOffset - $parameter.Name.Extent.StartOffset) -NewContent "`$$NewName"
                            }
                            else
                            {
                                if ($multiLine)
                                {
                                    $newAliasAndName = "[Alias($("'" + ($aliasNames -join "','") + "'"))]`n$($indentStyle * $indent)`$$NewName"
                                }
                                else
                                {
                                    $newAliasAndName = "[Alias($("'" + ($aliasNames -join "','") + "'"))]`$$NewName"
                                }
                                Add-FileReplacement -Path $parameter.Name.Extent.File -Start $parameter.Name.Extent.StartOffset -Length ($parameter.Name.Extent.EndOffset - $parameter.Name.Extent.StartOffset) -NewContent $newAliasAndName
                            }
                        }
                        else
                        {
                            Add-FileReplacement -Path $parameter.Name.Extent.File -Start $parameter.Name.Extent.StartOffset -Length ($parameter.Name.Extent.EndOffset - $parameter.Name.Extent.StartOffset) -NewContent "`$$NewName"
                        }
                    }
                    
                    if ($Ast.Body.DynamicParamBlock) { Invoke-AstWalk -Ast $Ast.Body.DynamicParamBlock -Command $Command -Name $Name -NewName $NewName -IsCommand $true -NoAlias $NoAlias }
                    if ($Ast.Body.BeginBlock) { Invoke-AstWalk -Ast $Ast.Body.BeginBlock -Command $Command -Name $Name -NewName $NewName -IsCommand $true -NoAlias $NoAlias }
                    if ($Ast.Body.ProcessBlock) { Invoke-AstWalk -Ast $Ast.Body.ProcessBlock -Command $Command -Name $Name -NewName $NewName -IsCommand $true -NoAlias $NoAlias }
                    if ($Ast.Body.EndBlock) { Invoke-AstWalk -Ast $Ast.Body.EndBlock -Command $Command -Name $Name -NewName $NewName -IsCommand $true -NoAlias $NoAlias }
                    
                    Update-CommandParameterHelp -FunctionAst $Ast -ParameterName $Name[0] -NewName $NewName
                }
                else
                {
                    Invoke-AstWalk -Ast $Ast.Body -Command $Command -Name $Name -NewName $NewName -IsCommand $false -NoAlias $NoAlias
                }
            }
            "System.Management.Automation.Language.VariableExpressionAst"
            {
                if ($IsCommand -and ($Ast.VariablePath.UserPath -eq $Name))
                {
                    Add-FileReplacement -Path $Ast.Extent.File -Start $Ast.Extent.StartOffset -Length ($Ast.Extent.EndOffset - $Ast.Extent.StartOffset) -NewContent "`$$NewName"
                }
            }
            "System.Management.Automation.Language.IfStatementAst"
            {
                foreach ($clause in $Ast.Clauses)
                {
                    Invoke-AstWalk -Ast $clause.Item1 -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand -NoAlias $NoAlias
                    Invoke-AstWalk -Ast $clause.Item2 -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand -NoAlias $NoAlias
                }
                if ($Ast.ElseClause)
                {
                    Invoke-AstWalk -Ast $Ast.ElseClause -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand -NoAlias $NoAlias
                }
            }
            default
            {
                foreach ($property in $Ast.PSObject.Properties)
                {
                    if ($property.Name -eq "Parent") { continue }
                    if ($null -eq $property.Value) { continue }
                    
                    if (Get-Member -InputObject $property.Value -Name GetEnumerator -MemberType Method)
                    {
                        foreach ($item in $property.Value)
                        {
                            if ($item.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast")
                            {
                                Invoke-AstWalk -Ast $item -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand -NoAlias $NoAlias
                            }
                        }
                        continue
                    }
                    
                    if ($property.Value.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast")
                    {
                        Invoke-AstWalk -Ast $property.Value -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand -NoAlias $NoAlias
                    }
                }
            }
        }
    }
    
    function Update-CommandParameter
    {
        [CmdletBinding()]
        Param (
            [System.Management.Automation.Language.CommandParameterAst]
            $Ast,
            
            [string]
            $NewName
        )
        
        $name = $NewName
        if ($name -notlike "-*") { $name = "-$name" }
        
        $length = $Ast.Extent.EndOffset - $Ast.Extent.StartOffset
        if ($null -ne $Ast.Argument) { $length = $Ast.Argument.Extent.StartOffset - $Ast.Extent.StartOffset - 1 }
        
        Add-FileReplacement -Path $Ast.Extent.File -Start $Ast.Extent.StartOffset -Length $length -NewContent $name
    }
    
    function Update-CommandParameterHelp
    {
        [CmdletBinding()]
        Param (
            [System.Management.Automation.Language.FunctionDefinitionAst]
            $FunctionAst,
            
            [string]
            $ParameterName,
            
            [string]
            $NewName
        )
        
        function Get-StartIndex
        {
            [CmdletBinding()]
            Param (
                [System.Management.Automation.Language.FunctionDefinitionAst]
                $FunctionAst,
                
                [string]
                $ParameterName,
                
                [int]
                $HelpEnd
            )
            
            if ($HelpEnd -lt 1) { return -1 }
            
            $index = -1
            $offset = 0
            
            while ($FunctionAst.Extent.Text.SubString(0, $HelpEnd).IndexOf(".PARAMETER $ParameterName", $offset, [System.StringComparison]::InvariantCultureIgnoreCase) -ne -1)
            {
                $tempIndex = $FunctionAst.Extent.Text.SubString(0, $HelpEnd).IndexOf(".PARAMETER $ParameterName", $offset, [System.StringComparison]::InvariantCultureIgnoreCase)
                $endOfLineIndex = $FunctionAst.Extent.Text.SubString(0, $HelpEnd).IndexOf("`n", $tempIndex, [System.StringComparison]::InvariantCultureIgnoreCase)
                if ($FunctionAst.Extent.Text.SubString($tempIndex, ($endOfLineIndex - $tempIndex)).Trim() -eq ".PARAMETER $ParameterName")
                {
                    return $tempIndex
                }
                $offset = $endOfLineIndex
            }
            
            return $index
        }
        
        $startIndex = $FunctionAst.Extent.StartOffset
        $endIndex = $FunctionAst.Body.ParamBlock.Extent.StartOffset
        foreach ($attribute in $FunctionAst.Body.ParamBlock.Attributes)
        {
            if ($attribute.Extent.StartOffset -lt $endIndex) { $endIndex = $attribute.Extent.StartOffset }
        }
        
        $index1 = Get-StartIndex -FunctionAst $FunctionAst -ParameterName $ParameterName -HelpEnd ($endIndex - $startIndex)
        if ($index1 -eq -1)
        {
            Write-PSFMessage -Level Warning -Message "Could not find Comment Based Help for parameter '$ParameterName' of command '$($FunctionAst.Name)' in '$($FunctionAst.Extent.File)'" -Tag 'cbh', 'fail' -FunctionName Rename-PSMDParameter
            Write-Issue -Extent $FunctionAst.Extent -Type "ParameterCBHNotFound" -Data "Parameter Help not found"
            return
        }
        $index2 = $FunctionAst.Extent.Text.SubString(0, ($endIndex - $startIndex)).IndexOf("$ParameterName", $index1, [System.StringComparison]::InvariantCultureIgnoreCase)
        
        Add-FileReplacement -Path $FunctionAst.Extent.File -Start ($index2 + $startIndex) -Length $ParameterName.Length -NewContent $NewName
    }
    
    function Add-FileReplacement
    {
        [CmdletBinding()]
        Param (
            [string]
            $Path,
            
            [int]
            $Start,
            
            [int]
            $Length,
            
            [string]
            $NewContent
        )
        Write-PSFMessage -Level Verbose -Message "Change Submitted: $Path | $Start | $Length | $NewContent" -Tag 'update','change','file'
        
        if (-not $globalFunctionHash.ContainsKey($Path))
        {
            $globalFunctionHash[$Path] = @()
        }
        
        $globalFunctionHash[$Path] += New-Object PSObject -Property @{
            Content  = $NewContent
            Start      = $Start
            Length    = $Length
        }
    }
    
    function Apply-FileReplacement
    {
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")]
        [CmdletBinding()]
        Param (
            [bool]
            $WhatIf
        )
        
        foreach ($key in $globalFunctionHash.Keys)
        {
            $value = $globalFunctionHash[$key] | Sort-Object Start
            $content = [System.IO.File]::ReadAllText($key)
            
            $newString = ""
            $currentIndex = 0
            
            foreach ($item in $value)
            {
                $newString += $content.SubString($currentIndex, ($item.Start - $currentIndex))
                $newString += $item.Content
                $currentIndex = $item.Start + $item.Length
            }
            
            $newString += $content.SubString($currentIndex)
            
            if ($WhatIf) { $newString }
            else { [System.IO.File]::WriteAllText($key, $newString) }
        }
    }
    
    function Write-Issue
    {
        [CmdletBinding()]
        Param (
            $Extent,
            
            $Data,
            
            [string]
            $Type
        )
        
        New-Object PSObject -Property @{
            Type  = $Type
            Data   = $Data
            File     = $Extent.File
            StartLine = $Extent.StartLineNumber
            Text = $Extent.Text
        }
    }
    #endregion Helper Functions
    
    foreach ($item in $Command)
    {
        try { $com = Get-Item function:\$item -ErrorAction Stop }
        catch
        {
            Stop-PSFFunction -Message "Could not find command, please import the module using the psm1 file before starting a refactor" -EnableException $EnableException -Category ObjectNotFound -ErrorRecord $_ -OverrideExceptionMessage -Tag "fail", "input"
            return
        }
    }
    
    $files = Get-ChildItem -Path $Path -Recurse | Where-Object Extension -Match "\.ps1|\.psm1"
    
    $issues = @()
    
    foreach ($file in $files)
    {
        $tokens = $null
        $parsingError = $null
        $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parsingError)
        
        Write-PSFMessage -Level VeryVerbose -Message "Replacing <c='sub'>$Command / $Name</c> with <c='em'>$NewName</c> | Scanning $($file.FullName)" -Tag 'start' -Target $Name
        $issues += Invoke-AstWalk -Ast $ast -Command $Command -Name $Name -NewName $NewName -IsCommand $false -NoAlias $NoAlias
    }
    
    Set-PSFResultCache -InputObject $issues -DisableCache $DisableCache
    Apply-FileReplacement -WhatIf $WhatIf
    $issues
}

function Set-PSMDCmdletBinding
{
    <#
        .SYNOPSIS
            Adds cmdletbinding attributes in bulk
         
        .DESCRIPTION
            Searches the specified file(s) for functions that ...
            - Do not have a cmdlet binding attribute
            - Do have a param block
            and inserts a cmdletbinding attribute for them.
     
            Will not change files where functions already have this attribute. Will also update internal functions.
         
        .PARAMETER FullName
            The file to process
         
        .PARAMETER DisableCache
            By default, this command caches the results of its execution in the PSFramework result cache.
            This information can then be retrieved for the last command to do so by running Get-PSFResultCache.
            Setting this switch disables the caching of data in the cache.
     
        .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
         
        .PARAMETER WhatIf
            If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
         
        .EXAMPLE
            PS C:\> Get-ChildItem .\functions\*\*.ps1 | Set-PSMDCmdletBinding
     
            Updates all commands in the module to have a cmdletbinding attribute.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $FullName,
        
        [switch]
        $DisableCache
    )
    
    begin
    {
        #region Utility functions
        function Invoke-AstWalk
        {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
            [CmdletBinding()]
            Param (
                $Ast,
                
                [string[]]
                $Command,
                
                [string[]]
                $Name,
                
                [string]
                $NewName,
                
                [bool]
                $IsCommand,
                
                [bool]
                $NoAlias
            )
            
            #Write-PSFMessage -Level Host -Message "Processing $($Ast.Extent.StartLineNumber) | $($Ast.Extent.File) | $($Ast.GetType().FullName)"
            $typeName = $Ast.GetType().FullName
            
            switch ($typeName)
            {
                "System.Management.Automation.Language.FunctionDefinitionAst"
                {
                    #region Has a param block, but no cmdletbinding
                    if (($null -ne $Ast.Body.ParamBlock) -and (-not ($Ast.Body.ParamBlock.Attributes | Where-Object TypeName -Like "CmdletBinding")))
                    {
                        $text = [System.IO.File]::ReadAllText($Ast.Extent.File)
                        
                        $index = $Ast.Body.ParamBlock.Extent.StartOffset
                        while (($index -gt 0) -and ($text.Substring($index, 1) -ne "`n")) { $index = $index - 1 }
                        
                        $indentIndex = $index + 1
                        $indent = $text.Substring($indentIndex, ($Ast.Body.ParamBlock.Extent.StartOffset - $indentIndex))
                        Add-FileReplacement -Path $Ast.Body.ParamBlock.Extent.File -Start $indentIndex -Length ($Ast.Body.ParamBlock.Extent.StartOffset - $indentIndex) -NewContent "$($indent)[CmdletBinding()]`n$($indent)"
                    }
                    #endregion Has a param block, but no cmdletbinding
                    
                    Invoke-AstWalk -Ast $Ast.Body -Command $Command -Name $Name -NewName $NewName -IsCommand $false
                }
                default
                {
                    foreach ($property in $Ast.PSObject.Properties)
                    {
                        if ($property.Name -eq "Parent") { continue }
                        if ($null -eq $property.Value) { continue }
                        
                        if (Get-Member -InputObject $property.Value -Name GetEnumerator -MemberType Method)
                        {
                            foreach ($item in $property.Value)
                            {
                                if ($item.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast")
                                {
                                    Invoke-AstWalk -Ast $item -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand
                                }
                            }
                            continue
                        }
                        
                        if ($property.Value.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast")
                        {
                            Invoke-AstWalk -Ast $property.Value -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand
                        }
                    }
                }
            }
        }
        
        function Add-FileReplacement
        {
            [CmdletBinding()]
            Param (
                [string]
                $Path,
                
                [int]
                $Start,
                
                [int]
                $Length,
                
                [string]
                $NewContent
            )
            Write-PSFMessage -Level Verbose -Message "Change Submitted: $Path | $Start | $Length | $NewContent" -Tag 'update', 'change', 'file'
            
            if (-not $globalFunctionHash.ContainsKey($Path))
            {
                $globalFunctionHash[$Path] = @()
            }
            
            $globalFunctionHash[$Path] += New-Object PSObject -Property @{
                Content        = $NewContent
                Start        = $Start
                Length        = $Length
            }
        }
        
        function Apply-FileReplacement
        {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")]
            [CmdletBinding()]
            Param (
                
            )
            
            foreach ($key in $globalFunctionHash.Keys)
            {
                $value = $globalFunctionHash[$key] | Sort-Object Start
                $content = [System.IO.File]::ReadAllText($key)
                
                $newString = ""
                $currentIndex = 0
                
                foreach ($item in $value)
                {
                    $newString += $content.SubString($currentIndex, ($item.Start - $currentIndex))
                    $newString += $item.Content
                    $currentIndex = $item.Start + $item.Length
                }
                
                $newString += $content.SubString($currentIndex)
                
                [System.IO.File]::WriteAllText($key, $newString)
                #$newString
            }
        }
        
        function Write-Issue
        {
            [CmdletBinding()]
            Param (
                $Extent,
                
                $Data,
                
                [string]
                $Type
            )
            
            New-Object PSObject -Property @{
                Type     = $Type
                Data     = $Data
                File     = $Extent.File
                StartLine = $Extent.StartLineNumber
                Text     = $Extent.Text
            }
        }
        #endregion Utility functions
    }
    process
    {
        foreach ($path in $FullName)
        {
            $globalFunctionHash = @{ }
            
            $tokens = $null
            $parsingError = $null
            $ast = [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$parsingError)
            
            Write-PSFMessage -Level VeryVerbose -Message "Ensuring Cmdletbinding for all functions in $path" -Tag 'start' -Target $Name
            $issues += Invoke-AstWalk -Ast $ast -Command $Command -Name $Name -NewName $NewName -IsCommand $false
            
            Set-PSFResultCache -InputObject $issues -DisableCache $DisableCache
            if ($PSCmdlet.ShouldProcess($path, "Set CmdletBinding attribute"))
            {
                Apply-FileReplacement
            }
            $issues
        }
    }
}

function Set-PSMDEncoding
{
<#
    .SYNOPSIS
        Sets the encoding for the input file.
     
    .DESCRIPTION
        This command reads the input file using the default encoding interpreter.
        It then writes the contents as the specified enconded string back to itself.
     
        There is no inherent encoding conversion enacted, so special characters may break.
        This is a tool designed to reformat code files, where special characters shouldn't be used anyway.
     
    .PARAMETER Path
        Path to the files to be set.
        Silently ignores folders.
     
    .PARAMETER Encoding
        The encoding to set to (Defaults to "UTF8 with BOM")
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Get-ChildItem -Recurse | Set-PSMDEncoding
     
        Converts all files in the current folder and subfolders to UTF8
#>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [string[]]
        $Path,
        
        [PSFEncoding]
        $Encoding = (Get-PSFConfigValue -FullName 'psframework.text.encoding.defaultwrite' -Fallback 'utf-8'),
        
        [switch]
        $EnableException
    )
    
    process
    {
        foreach ($pathItem in $Path)
        {
            Write-PSFMessage -Level VeryVerbose -Message "Processing $pathItem" -Target $pathItem
            try { $pathResolved = Resolve-PSFPath -Path $pathItem -Provider FileSystem }
            catch { Stop-PSFFunction -Message " " -EnableException $EnableException -ErrorRecord $_ -Target $pathItem -Continue }
            
            foreach ($resolvedPath in $pathResolved)
            {
                if ((Get-Item $resolvedPath).PSIsContainer) { continue }
                
                Write-PSFMessage -Level Verbose -Message "Setting encoding for $resolvedPath" -Target $pathItem
                try
                {
                    if (Test-PSFShouldProcess -PSCmdlet $PSCmdlet -Target $resolvedPath -Action "Set encoding to $($Encoding.Encoding.EncodingName)")
                    {
                        $text = [System.IO.File]::ReadAllText($resolvedPath)
                        [System.IO.File]::WriteAllText($resolvedPath, $text, $Encoding)
                    }
                }
                catch
                {
                    Stop-PSFFunction -Message "Failed to access file! $resolvedPath" -EnableException $EnableException -ErrorRecord $_ -Target $pathItem -Continue
                }
            }
        }
    }
}

function Set-PSMDParameterHelp
{
    <#
        .SYNOPSIS
            Sets the content of a CBH parameter help.
         
        .DESCRIPTION
            Sets the content of a CBH parameter help.
            This command will enumerate all files in the specified folder and subfolders.
            Then scan all files with extension .ps1 and .psm1.
            In each of these files it will check out function definitions, see whether the name matches, then update the help for the specified parameter if present.
     
            In order for this to work, a few rules must be respected:
            - It will not work with help XML, only with CBH xml
            - It will not work if the help block is above the function. It must be placed within.
            - It will not ADD a CBH, if none is present yet. If there is no help for the specified parameter, it will simply do nothing, but report the fact.
         
        .PARAMETER Path
            The base path where all the files are in.
         
        .PARAMETER CommandName
            The name of the command to update.
            Uses wildcard matching to match, so you can do a global update using "*"
         
        .PARAMETER ParameterName
            The name of the parameter to update.
            Must be an exact match, but is not case sensitive.
         
        .PARAMETER HelpText
            The text to insert.
            - Do not include indents. It will pick up the previous indents and reuse them
            - Do not include an extra line, it will automatically add a separating line to the next element
         
        .PARAMETER DisableCache
            By default, this command caches the results of its execution in the PSFramework result cache.
            This information can then be retrieved for the last command to do so by running Get-PSFResultCache.
            Setting this switch disables the caching of data in the cache.
         
        .EXAMPLE
            Set-PSMDParameterHelp -Path "C:\PowerShell\Projects\MyModule" -CommandName "*" -ParameterName "Foo" -HelpText @"
            This is some foo text
            For a truly foo-some result
            "@
     
            Scans all files in the specified path.
            - Considers every function found
            - Will only process the parameter 'Foo'
            - And replace the current text with the one specified
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [Parameter(Mandatory = $true)]
        [string]
        $CommandName,
        
        [Parameter(Mandatory = $true)]
        [string]
        $ParameterName,
        
        [Parameter(Mandatory = $true)]
        [string]
        $HelpText,
        
        [switch]
        $DisableCache
    )
    
    # Global Store for pending file updates
    # Exempt from Scope Boundary violation rule, since only accessed using dedicated helper function
    $globalFunctionHash = @{ }
    
    #region Utility Functions
    function Invoke-AstWalk
    {
        [CmdletBinding()]
        Param (
            $Ast,
            
            [string]
            $CommandName,
            
            [string]
            $ParameterName,
            
            [string]
            $HelpText
        )
        
        #Write-PSFMessage -Level Host -Message "Processing $($Ast.Extent.StartLineNumber) | $($Ast.Extent.File) | $($Ast.GetType().FullName)"
        $typeName = $Ast.GetType().FullName
        
        switch ($typeName)
        {
            "System.Management.Automation.Language.FunctionDefinitionAst"
            {
                if ($Ast.Name -like $CommandName)
                {
                    Update-CommandParameterHelp -FunctionAst $Ast -ParameterName $ParameterName -HelpText $HelpText
                    
                    if ($Ast.Body.DynamicParamBlock) { Invoke-AstWalk -Ast $Ast.Body.DynamicParamBlock -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText }
                    if ($Ast.Body.BeginBlock) { Invoke-AstWalk -Ast $Ast.Body.BeginBlock -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText }
                    if ($Ast.Body.ProcessBlock) { Invoke-AstWalk -Ast $Ast.Body.ProcessBlock -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText }
                    if ($Ast.Body.EndBlock) { Invoke-AstWalk -Ast $Ast.Body.EndBlock -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText }
                }
                else
                {
                    Invoke-AstWalk -Ast $Ast.Body -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText
                }
            }
            default
            {
                foreach ($property in $Ast.PSObject.Properties)
                {
                    if ($property.Name -eq "Parent") { continue }
                    if ($null -eq $property.Value) { continue }
                    
                    if (Get-Member -InputObject $property.Value -Name GetEnumerator -MemberType Method)
                    {
                        foreach ($item in $property.Value)
                        {
                            if ($item.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast")
                            {
                                Invoke-AstWalk -Ast $item -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText
                            }
                        }
                        continue
                    }
                    
                    if ($property.Value.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast")
                    {
                        Invoke-AstWalk -Ast $property.Value -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText
                    }
                }
            }
        }
    }
    
    function Update-CommandParameterHelp
    {
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
        [CmdletBinding()]
        Param (
            [System.Management.Automation.Language.FunctionDefinitionAst]
            $FunctionAst,
            
            [string]
            $ParameterName,
            
            [string]
            $HelpText
        )
        
        #region Find the starting position
        function Get-StartIndex
        {
            [OutputType([System.Int32])]
            [CmdletBinding()]
            Param (
                [System.Management.Automation.Language.FunctionDefinitionAst]
                $FunctionAst,
                
                [string]
                $ParameterName,
                
                [int]
                $HelpEnd
            )
            
            if ($HelpEnd -lt 1) { return -1 }
            
            $index = -1
            $offset = 0
            
            while ($FunctionAst.Extent.Text.SubString(0, $HelpEnd).IndexOf(".PARAMETER $ParameterName", $offset, [System.StringComparison]::InvariantCultureIgnoreCase) -ne -1)
            {
                $tempIndex = $FunctionAst.Extent.Text.SubString(0, $HelpEnd).IndexOf(".PARAMETER $ParameterName", $offset, [System.StringComparison]::InvariantCultureIgnoreCase)
                $endOfLineIndex = $FunctionAst.Extent.Text.SubString(0, $HelpEnd).IndexOf("`n", $tempIndex, [System.StringComparison]::InvariantCultureIgnoreCase)
                if ($FunctionAst.Extent.Text.SubString($tempIndex, ($endOfLineIndex - $tempIndex)).Trim() -eq ".PARAMETER $ParameterName")
                {
                    return $tempIndex
                }
                $offset = $endOfLineIndex
            }
            
            return $index
        }
        
        $startIndex = $FunctionAst.Extent.StartOffset
        $endIndex = $FunctionAst.Body.ParamBlock.Extent.StartOffset
        foreach ($attribute in $FunctionAst.Body.ParamBlock.Attributes)
        {
            if ($attribute.Extent.StartOffset -lt $endIndex) { $endIndex = $attribute.Extent.StartOffset }
        }
        
        $index1 = Get-StartIndex -FunctionAst $FunctionAst -ParameterName $ParameterName -HelpEnd ($endIndex - $startIndex)
        if ($index1 -eq -1)
        {
            Write-PSFMessage -Level Warning -Message "Could not find Comment Based Help for parameter '$ParameterName' of command '$($FunctionAst.Name)' in '$($FunctionAst.Extent.File)'" -Tag 'cbh', 'fail' -FunctionName Rename-PSMDParameter
            Write-Issue -Extent $FunctionAst.Extent -Type "ParameterCBHNotFound" -Data "Parameter Help not found"
            return
        }
        $index2 = $FunctionAst.Extent.Text.SubString(0, ($endIndex - $startIndex)).IndexOf("$ParameterName", $index1, [System.StringComparison]::InvariantCultureIgnoreCase) + $ParameterName.Length
        $goodIndex = $FunctionAst.Extent.Text.SubString($index2).IndexOf("`n") + 1 + $index2
        #endregion Find the starting position
        
        #region Find the ending position
        $lines = $FunctionAst.Extent.Text.SubString(0, ($endIndex - $startIndex)).Substring($goodIndex).Split("`n")
        
        $goodLines = @()
        $badLine = ""
        
        foreach ($line in $lines)
        {
            if ($line -notmatch "^#{0,1}[\s`t]{0,}\.|^#>") { $goodLines += $line }
            else
            {
                $badLine = $line
                break
            }
        }
        
        if (($goodLines.Count -eq 0) -or ($goodLines.Count -eq $lines.Count))
        {
            Write-PSFMessage -Level Warning -Message "Could not parse the Comment Based Help for parameter '$ParameterName' of command '$($FunctionAst.Name)' in '$($FunctionAst.Extent.File)'" -Tag 'cbh', 'fail' -FunctionName Rename-PSMDParameter
            Write-Issue -Extent $FunctionAst.Extent -Type "ParameterCBHBroken" -Data "Parameter Help cannot be parsed"
            return
        }
        
        $badIndex = $FunctionAst.Extent.Text.SubString(0, ($endIndex - $startIndex)).IndexOf($badLine, $index2) - 1
        #endregion Find the ending position
        
        #region Find the indent and create the text to insert
        $indents = @()
        foreach ($line in $goodLines)
        {
            if ($line.Trim(" ^t#$([char]13)").Length -gt 0)
            {
                $line | Select-String "^(#{0,1}[\s`t]+)" | ForEach-Object { $indents += $_.Matches[0].Groups[1].Value }
            }
        }
        if ($indents.Count -eq 0) { $indent = "`t`t" }
        else
        {
            $indent = $indents | Sort-Object -Property Length | Select-Object -First 1
        }
        $indent = $indent.Replace([char]13, [char]9)
        
        $newHelpText = ($HelpText.Split("`n") | ForEach-Object { "$($indent)$($_)" }) -join "`n"
        $newHelpText += "`n$($indent)"
        #endregion Find the indent and create the text to insert
        
        Add-FileReplacement -Path $FunctionAst.Extent.File -Start ($goodIndex + $startIndex) -Length ($badIndex - $goodIndex) -NewContent $newHelpText
    }
    
    function Add-FileReplacement
    {
        [CmdletBinding()]
        Param (
            [string]
            $Path,
            
            [int]
            $Start,
            
            [int]
            $Length,
            
            [string]
            $NewContent
        )
        Write-PSFMessage -Level Verbose -Message "Change Submitted: $Path | $Start | $Length | $NewContent" -Tag 'update', 'change', 'file'
        
        if (-not $globalFunctionHash.ContainsKey($Path))
        {
            $globalFunctionHash[$Path] = @()
        }
        
        $globalFunctionHash[$Path] += New-Object PSObject -Property @{
            Content    = $NewContent
            Start       = $Start
            Length       = $Length
        }
    }
    
    function Apply-FileReplacement
    {
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")]
        [CmdletBinding()]
        Param (
            
        )
        
        foreach ($key in $globalFunctionHash.Keys)
        {
            $value = $globalFunctionHash[$key] | Sort-Object Start
            $content = [System.IO.File]::ReadAllText($key)
            
            $newString = ""
            $currentIndex = 0
            
            foreach ($item in $value)
            {
                $newString += $content.SubString($currentIndex, ($item.Start - $currentIndex))
                $newString += $item.Content
                $currentIndex = $item.Start + $item.Length
            }
            
            $newString += $content.SubString($currentIndex)
            
            [System.IO.File]::WriteAllText($key, $newString)
            #$newString
        }
    }
    
    function Write-Issue
    {
        [CmdletBinding()]
        Param (
            $Extent,
            
            $Data,
            
            [string]
            $Type
        )
        
        New-Object PSObject -Property @{
            Type    = $Type
            Data    = $Data
            File    = $Extent.File
            StartLine = $Extent.StartLineNumber
            Text    = $Extent.Text
        }
    }
    #endregion Utility Functions
    
    $files = Get-ChildItem -Path $Path -Recurse | Where-Object Extension -Match "\.ps1|\.psm1"
    
    $issues = @()
    
    foreach ($file in $files)
    {
        $tokens = $null
        $parsingError = $null
        $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parsingError)
        
        Write-PSFMessage -Level VeryVerbose -Message "Updating help for <c='sub'>$CommandName / $ParameterName</c> | Scanning $($file.FullName)" -Tag 'start' -Target $Name
        $issues += Invoke-AstWalk -Ast $ast -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText
    }
    
    Set-PSFResultCache -InputObject $issues -DisableCache $DisableCache
    Apply-FileReplacement
    $issues
}

function Split-PSMDScriptFile
{
    <#
        .SYNOPSIS
            Parses a file and exports all top-level functions from it into a dedicated file, just for the function.
         
        .DESCRIPTION
            Parses a file and exports all top-level functions from it into a dedicated file, just for the function.
            The original file remains unharmed by this.
     
            Note: Any comments outside the function definition will not be copied.
         
        .PARAMETER File
            The file(s) to extract functions from.
         
        .PARAMETER Path
            The folder to export to
         
        .PARAMETER Encoding
            Default: UTF8
            The output encoding. Can usually be left alone.
         
        .EXAMPLE
            PS C:\> Split-PSMDScriptFile -File ".\module.ps1" -Path .\files
     
            Exports all functions in module.ps1 and puts them in individual files in the folder .\files.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipeline = $true)]
        [string[]]
        $File,
        
        [string]
        $Path,
        
        $Encoding = "UTF8"
    )
    
    process
    {
        foreach ($item in $File)
        {
            $a = $null
            $b = $null
            $ast = [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $item), [ref]$a, [ref]$b)
            
            foreach ($functionAst in ($ast.EndBlock.Statements | Where-Object { $_.GetType().FullName -eq "System.Management.Automation.Language.FunctionDefinitionAst" }))
            {
                $ast.Extent.Text.Substring($functionAst.Extent.StartOffset, ($functionAst.Extent.EndOffset - $functionAst.Extent.StartOffset)) | Set-Content "$Path\$($functionAst.Name).ps1" -Encoding $Encoding
            }
        }
    }
}

function Publish-PSMDScriptFile
{
    <#
    .SYNOPSIS
        Packages a script with all dependencies and "publishes" it as a zip package.
 
    .DESCRIPTION
        Packages a script with all dependencies and "publishes" it as a zip package.
        By default, it will be published to the user's desktop.
        All modules it uses will be parsed from the script:
        - Commands that cannot be resolved will trigger a warning.
        - Modules that are installed in the Windows folder (such as the ActiveDirectory module or other modules associated with server roles) will be ignored.
        - PSSnapins will be ignored
        - All other modules determined by the commands used will be provided from a repository, packaged in a subfolder and included in the zip file.
 
        If needed, the scriptfile will be modified to add the new modules folder to its list of known folders.
        (The source file itself will never be modified)
 
        Use Set-PSMDStagingRepository to create / use a local path for staging modules to provide that way.
        This gives you better control over the versions used and better performance.
        Also the ability to use this with non-public modules.
        Use Publish-PSMDStagedModule to transfer modules from path or another repository into your registered staging repository.
 
    .PARAMETER Path
        Path to the scriptfile to publish.
        The scriptfile is expected to be UTF8 encoded with BOM, otherwise some characters may end up broken.
 
    .PARAMETER OutPath
        The path to the folder where the output zip file will be created.
        Defaults to the user's desktop.
 
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
 
    .EXAMPLE
        PS C:\> Publish-PSMDScriptFile -Path 'C:\scripts\logrotate.ps1'
 
        Creates a delivery package for the logrotate.ps1 scriptfile and places it on the desktop
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PsfValidateScript('PSModuleDevelopment.Validate.File', ErrorString = 'PSModuleDevelopment.Validate.File')]
        [string]
        $Path,

        [PsfValidateScript('PSModuleDevelopment.Validate.Path', ErrorString = 'PSModuleDevelopment.Validate.Path')]
        [string]
        $OutPath = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Script.OutPath'),

        [switch]
        $EnableException
    )

    begin
    {
        #region Utility Functions
        function Get-Modifier
        {
            [CmdletBinding()]
            param (
                [Parameter(Mandatory = $true)]
                [string]
                $Path
            )

            $help = Get-Help $Path
            $modifiers = $help.alertSet.alert.Text -split "`n" | Where-Object { $_ -like "PSMD: *" } | ForEach-Object { $_ -replace '^PSMD: ' }

            foreach ($modifier in $modifiers)
            {
                $operation, $values = $modifier -split ":"
                switch ($operation)
                {
                    'Include'
                    {
                        foreach ($module in $values.Split(",").ForEach{ $_.Trim() })
                        {
                            [pscustomobject]@{
                                Type = 'Include'
                                Name = $module
                            }
                        }
                    }
                    'Exclude'
                    {
                        foreach ($module in $values.Split(",").ForEach{ $_.Trim() })
                        {
                            [pscustomobject]@{
                                Type = 'Exclude'
                                Name = $module
                            }
                        }
                    }
                    'IgnoreUnknownCommand'
                    {
                        foreach ($commandName in $values.Split(",").ForEach{ $_.Trim() })
                        {
                            [pscustomobject]@{
                                Type = 'IgnoreCommand'
                                Name = $commandName
                            }
                        }
                    }
                }
            }
        }

        function Add-PSModulePath
        {
            [CmdletBinding()]
            param (
                [string]
                $Path
            )

            $psmodulePathCode = @'
 
# Ensure modules are available
$modulePath = "$PSScriptRoot\Modules"
if (-not $env:PSModulePath.Contains($modulePath)) { $env:PSModulePath = "$($env:PSModulePath);$($modulePath)" }
 
 
'@


            $parsedFile = Read-PSMDScript -Path $Path
            $assignment = $parsedFile.Ast.FindAll({
                    $args[0] -is [System.Management.Automation.Language.AssignmentStatementAst] -and
                    $args[0].Left.VariablePath.UserPath -eq 'env:PSModulePath'
                }, $true)
            if ($assignment) { return }
            if ($parsedFile.Ast.ParamBlock.Extent)
            {
                $paramExtent = $parsedFile.Ast.ParamBlock.Extent
                $text = [System.IO.File]::ReadAllText($Path)
                $newText = $text.Substring(0, $paramExtent.EndOffset) + $psmodulePathCode + $text.Substring($paramExtent.EndOffset)
                $encoding = [System.Text.UTF8Encoding]::new($true)
                [System.IO.File]::WriteAllText($Path, $newText, $encoding)
            }
            else
            {
                $extent = $parsedFile.Ast.EndBlock.Statements[0].Extent
                $text = [System.IO.File]::ReadAllText($Path)
                $textBefore = ""
                $textAfter = $text
                if ($extent.StartOffset -gt 0)
                {
                    $textBefore = $text.Substring(0, $extent.StartOffset)
                    $textAfter = $text.Substring($extent.StartOffset)
                }
                $newText = $textBefore + $psmodulePathCode + $textAfter
                $encoding = [System.Text.UTF8Encoding]::new($true)
                [System.IO.File]::WriteAllText($Path, $newText, $encoding)
            }
        }
        #endregion Utility Functions

        $modulesToProcess = @{
            IgnoreCommand = @()
            Include       = @()
            Exclude       = @()
        }
    }
    process
    {
        #region Prepare required Modules
        # Scan help-notes for explicit directives
        $modifiers = Get-Modifier -Path $Path
        foreach ($modifier in $modifiers)
        {
            $modulesToProcess.$($modifier.Type) += $modifier.Name
        }

        # Detect modules needed and store them
        try { $parsedCommands = Get-PSMDFileCommand -Path $Path -EnableException }
        catch
        {
            Stop-PSFFunction -String 'Publish-PSMDScriptFile.Script.ParseError' -StringValues $Path -EnableException $EnableException -ErrorRecord $_
            return
        }
        foreach ($command in $parsedCommands)
        {
            Write-PSFMessage -Level Verbose -String 'Publish-PSMDScriptFile.Script.Command' -StringValues $command.Name, $command.Count, $command.Module
            if ($modulesToProcess.IgnoreCommand -contains $command.Name) { continue }

            if (-not $command.Module -and -not $command.Internal)
            {
                Write-PSFMessage -Level Warning -String 'Publish-PSMDScriptFile.Script.Command.NotKnown' -StringValues $command.Name, $command.Count
                continue
            }
            if ($modulesToProcess.Exclude -contains "$($command.Module)") { continue }
            if ($modulesToProcess.Include -contains "$($command.Module)") { continue }
            if ($command.Module -is [System.Management.Automation.PSSnapInInfo]) { continue }
            if ($command.Module.ModuleBase -like 'C:\Windows\System32\WindowsPowerShell\v1.0*') { continue }
            if ($command.Module.ModuleBase -like 'C:\Program Files\PowerShell\7*') { continue }
            $modulesToProcess.Include += "$($command.Module)"
        }

        $tempPath = Get-PSFPath -Name Temp
        $newPath = New-Item -Path $tempPath -Name "PSMD_$(Get-Random)" -ItemType Directory -Force
        $modulesFolder = New-Item -Path $newPath.FullName -Name 'Modules' -ItemType Directory -Force

        foreach ($moduleLabel in $modulesToProcess.Include | Select-Object -Unique)
        {
            if (-not $moduleLabel) { continue }
            Invoke-PSFProtectedCommand -ActionString 'Publish-PSMDScriptFile.Module.Saving' -ActionStringValues $moduleLabel, (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Script.StagingRepository') -Scriptblock {
                Save-Module -Name $moduleLabel -Repository (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Script.StagingRepository') -Path $modulesFolder.FullName -ErrorAction Stop
            } -EnableException $EnableException -PSCmdlet $PSCmdlet -Target $moduleLabel
            if (Test-PSFFunctionInterrupt) { return }
        }
        #endregion Prepare required Modules

        # Copy script file
        $newScript = Copy-Item -Path $Path -Destination $newPath.FullName -PassThru

        # Update script to set PSModulePath
        Add-PSModulePath -Path $newScript.FullName

        # Zip result & move to destination
        Compress-Archive -Path "$($newPath.FullName)\*" -DestinationPath ('{0}\{1}.zip' -f $OutPath, $newScript.BaseName) -Force
        Remove-Item -Path $newPath.FullName -Recurse -Force -ErrorAction Ignore
    }
}

function Publish-PSMDStagedModule
{
<#
    .SYNOPSIS
        Publish a module to your staging repository.
     
    .DESCRIPTION
        Publish a module to your staging repository.
        Always publishes the latest version available when specifying a name.
     
    .PARAMETER Name
        The name of the module to publish.
     
    .PARAMETER Path
        The path to the module to publish.
     
    .PARAMETER Repository
        The repository from which to withdraw the module to then publish to the staging repository.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Publish-PSMDStagedModule -Name 'PSFramework'
     
        Publishes the latest version of PSFramework found on the local machine.
     
    .EXAMPLE
        PS C:\> Publish-PSMDStagedModule -Name 'Microsoft.Graph' -Repository PSGallery
     
        Publishes the entire kit of 'Microsoft.Graph' modules from the PSGallery to the staging repository.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
        [PsfValidateScript('PSModuleDevelopment.Validate.Path', ErrorString = 'PSModuleDevelopment.Validate.Path')]
        [string]
        $Path,
        
        [Parameter(ParameterSetName = 'Name')]
        [string]
        $Repository,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        $tempPath = Get-PSFPath -Name Temp
    }
    process
    {
        #region Explicit Path specified
        if ($Path)
        {
            try { Publish-Module -Path $Path -Repository (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Script.StagingRepository') -ErrorAction Stop }
            catch
            {
                if ($_.FullyQualifiedErrorId -like '*ModuleVersionIsAlreadyAvailableInTheGallery*')
                {
                    Write-PSFMessage -Level Warning -String 'Publish-PSMDStagedModule.Module.AlreadyPublished' -StringValues $moduleToPublish.Name, $moduleToPublish.Version -ErrorRecord $_
                    return
                }
                
                Stop-PSFFunction -String 'Publish-PSMDStagedModule.Module.PublishError' -StringValues $Name, $folder.Name -ErrorRecord $_ -EnableException $EnableException
                return
            }
            return
        }
        #endregion Explicit Path specified
        
        #region Deploy from source repository
        if ($Repository)
        {
            $workingDirectory = Join-Path -Path $tempPath -ChildPath "psmd_$(Get-Random)"
            $null = New-Item -Path $workingDirectory -ItemType Directory -Force
            
            Save-Module -Name $Name -Repository $Repository -Path $workingDirectory
            
            foreach ($folder in Get-ChildItem -Path $workingDirectory | Sort-Object -Property LastWriteTime)
            {
                $subFolder = Get-ChildItem -Path $folder.FullName | Sort-Object -Property Name -Descending | Select-Object -First 1
                
                try { Publish-Module -Path $subFolder.FullName -Repository (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Script.StagingRepository') -ErrorAction Stop }
                catch
                {
                    if ($_.FullyQualifiedErrorId -like '*ModuleVersionIsAlreadyAvailableInTheGallery*') { continue }
                    
                    Remove-Item -Path $workingDirectory -Force -Recurse -ErrorAction Ignore
                    Stop-PSFFunction -String 'Publish-PSMDStagedModule.Module.PublishError' -StringValues $Name, $folder.Name -ErrorRecord $_ -EnableException $EnableException
                    return
                }
            }
            
            Remove-Item -Path $workingDirectory -Force -Recurse -ErrorAction Ignore
        }
        #endregion Deploy from source repository
        
        #region Deploy from local computer installation
        else
        {
            $modules = Get-Module -Name $Name -ListAvailable
            if (-not $modules)
            {
                Stop-PSFFunction -String 'Publish-PSMDStagedModule.Module.NotFound' -StringValues $Name -EnableException $EnableException
            }
            $moduleToPublish = $modules | Sort-Object -Property Version -Descending | Select-Object -First 1
            try { Publish-Module -Path $moduleToPublish.ModuleBase -Repository (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Script.StagingRepository') -ErrorAction Stop }
            catch
            {
                if ($_.FullyQualifiedErrorId -like '*ModuleVersionIsAlreadyAvailableInTheGallery*')
                {
                    Write-PSFMessage -Level Warning -String 'Publish-PSMDStagedModule.Module.AlreadyPublished' -StringValues $moduleToPublish.Name, $moduleToPublish.Version -ErrorRecord $_
                    return
                }
                
                Stop-PSFFunction -String 'Publish-PSMDStagedModule.Module.PublishError' -StringValues $Name, $folder.Name -ErrorRecord $_ -EnableException $EnableException
                return
            }
        }
        #endregion Deploy from local computer installation
    }
}

function Set-PSMDStagingRepository
{
<#
    .SYNOPSIS
        Define the repository to use for deploying modules along with scripts.
     
    .DESCRIPTION
        Define the repository to use for deploying modules along with scripts.
        By default, modules are deployed using the PSGallery, which may be problematic:
        - Offline computers do not have access to it
        - Low performance compared to a local mirror
     
    .PARAMETER Path
        The local path to use. Will configure that path as a PSRepository.
        The new repository will be named "PSMDStaging".
     
    .PARAMETER Repository
        The name of an existing repository to use
     
    .EXAMPLE
        PS C:\> Set-PSMDStagingRepository -Path 'C:\PowerShell\StagingRepo'
     
        Registers the local path 'C:\PowerShell\StagingRepo' as a repository and will use it for deploying modules along with scripts.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
        [PsfValidateScript('PSModuleDevelopment.Validate.Path', ErrorString = 'PSModuleDevelopment.Validate.Path')]
        [string]
        $Path,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Repository')]
        [string]
        $Repository
    )
    
    process
    {
        if ($Path)
        {
            if (Get-PSRepository -Name PSMDStaging -ErrorAction Ignore)
            {
                Unregister-PSRepository -Name PSMDStaging
            }
            Register-PSRepository -Name PSMDStaging -SourceLocation $Path -PublishLocation $Path -InstallationPolicy Trusted
            Set-PSFConfig -Module PSModuleDevelopment -Name 'Script.StagingRepository' -Value PSMDStaging -PassThru | Register-PSFConfig
        }
        else
        {
            Set-PSFConfig -Module PSModuleDevelopment -Name 'Script.StagingRepository' -Value $Repository -PassThru | Register-PSFConfig
        }
    }
}

function Get-PSMDTemplate
{
<#
    .SYNOPSIS
        Search for templates to create from.
     
    .DESCRIPTION
        Search for templates to create from.
     
    .PARAMETER TemplateName
        The name of the template to search for.
        Templates are filtered by this using wildcard comparison.
        Defaults to "*" (everything).
     
    .PARAMETER Store
        The template store to retrieve tempaltes from.
        By default, all stores are queried.
     
    .PARAMETER Path
        Instead of a registered store, look in this path for templates.
     
    .PARAMETER Tags
        Only return templates with the following tags.
     
    .PARAMETER Author
        Only return templates by this author.
     
    .PARAMETER MinimumVersion
        Only return templates with at least this version.
     
    .PARAMETER RequiredVersion
        Only return templates with exactly this version.
     
    .PARAMETER All
        Return all versions found.
        By default, only the latest matching version of a template will be returned.
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .EXAMPLE
        PS C:\> Get-PSMDTemplate
     
        Returns all templates
     
    .EXAMPLE
        PS C:\> Get-PSMDTemplate -TemplateName module
     
        Returns the latest version of the template named module.
#>

    [CmdletBinding(DefaultParameterSetName = 'Store')]
    Param (
        [Parameter(Position = 0)]
        [string]
        $TemplateName = "*",
        
        [Parameter(ParameterSetName = 'Store')]
        [string]
        $Store = "*",
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
        [string]
        $Path,
        
        [string[]]
        $Tags,
        
        [string]
        $Author,
        
        [version]
        $MinimumVersion,
        
        [version]
        $RequiredVersion,
        
        [switch]
        $All,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        $prospects = @()
    }
    process
    {
        #region Scan folders
        if (Test-PSFParameterBinding -ParameterName "Path")
        {
            $templateInfos = Get-ChildItem -Path $Path -Filter "$($TemplateName)-*.Info.xml" | Where-Object { ($_.Name -replace "-\d+(\.\d+){0,3}.Info.xml$") -like $TemplateName }
            
            foreach ($info in $templateInfos)
            {
                $data = Import-PSFClixml $info.FullName
                $data.Path = $info.FullName -replace '\.Info\.xml$','.xml'
                $prospects += $data
            }
        }
        #endregion Scan folders
        
        #region Search Stores
        else
        {
            $stores = Get-PsmdTemplateStore -Filter $Store
            
            foreach ($item in $stores)
            {
                if ($item.Ensure())
                {
                    $templateInfos = Get-ChildItem -Path $item.Path -Filter "$($TemplateName)-*-Info.xml" | Where-Object { ($_.Name -replace "-\d+(\.\d+){0,3}-Info.xml$") -like $TemplateName }
                    
                    foreach ($info in $templateInfos)
                    {
                        $data = Import-PSFClixml $info.FullName
                        $data.Path = $info.FullName -replace '-Info\.xml$', '.xml'
                        $data.Store = $item.Name
                        $prospects += $data
                    }
                }
                # If the user asked for a specific store, it should error out on him
                elseif ($item.Name -eq $Store)
                {
                    Stop-PSFFunction -Message "Could not find store $Store" -EnableException $EnableException -Category OpenError -Tag 'fail','template','store','open'
                    return
                }
            }
        }
        #endregion Search Stores
    }
    end
    {
        $filteredProspects = @()
        
        #region Apply filters
        foreach ($prospect in $prospects)
        {
            if ($Author)
            {
                if ($prospect.Author -notlike $Author) { continue }
            }
            if (Test-PSFParameterBinding -ParameterName MinimumVersion)
            {
                if ($prospect.Version -lt $MinimumVersion) { continue }
            }
            if (Test-PSFParameterBinding -ParameterName RequiredVersion)
            {
                if ($prospect.Version -ne $RequiredVersion) { continue }
            }
            if ($Tags)
            {
                $test = $false
                foreach ($tag in $Tags)
                {
                    if ($prospect.Tags -contains $tag)
                    {
                        $test = $true
                        break
                    }
                }
                if (-not $test) { continue }
            }
            
            $filteredProspects += $prospect
        }
        #endregion Apply filters
        
        #region Return valid templates
        if ($All) { return $filteredProspects | Sort-Object Type, Name, Version }
        
        $prospectHash = @{ }
        foreach ($prospect in $filteredProspects)
        {
            if ($prospectHash.Keys -notcontains $prospect.Name)
            {
                $prospectHash[$prospect.Name] = $prospect
            }
            elseif ($prospectHash[$prospect.Name].Version -lt $prospect.Version)
            {
                $prospectHash[$prospect.Name] = $prospect
            }
        }
        $prospectHash.Values | Sort-Object Type, Name
        #endregion Return valid templates
    }
}

function Invoke-PSMDTemplate {
    <#
    .SYNOPSIS
        Creates a project/file from a template.
     
    .DESCRIPTION
        This function takes a template and turns it into a finished file&folder structure.
        It does so by creating the files and folders stored within, replacing all parameters specified with values provided by the user.
         
        Missing parameters will be prompted for.
     
    .PARAMETER Template
        The template object to build from.
        Accepts objects returned by Get-PSMDTemplate.
     
    .PARAMETER TemplateName
        The name of the template to build from.
        Warning: This does wildcard interpretation, don't specify '*' unless you like answering parameter prompts.
     
    .PARAMETER Store
        The template store to retrieve tempaltes from.
        By default, all stores are queried.
     
    .PARAMETER Path
        Instead of a registered store, look in this path for templates.
     
    .PARAMETER OutPath
        The path in which to create the output.
        By default, it will create in the current directory.
     
    .PARAMETER Name
        The name of the produced output.
        Automatically inserted for any name parameter specified on creation.
        Also used for creating a root folder, when creating a project.
     
    .PARAMETER NoFolder
        Skip automatic folder creation for project templates.
        By default, this command will create a folder to place files&folders in when creating a project.
     
    .PARAMETER Encoding
        The encoding to apply to text files.
        The default setting for this can be configured by updating the 'PSFramework.Text.Encoding.DefaultWrite' configuration setting.
        The initial default value is utf8 with BOM.
     
    .PARAMETER Parameters
        A Hashtable containing parameters for use in creating the template.
     
    .PARAMETER Raw
        By default, all parameters will be replaced during invocation.
        In Raw mode, this is skipped, reproducing mostly the original template input (dynamic scriptblocks will now be named scriptblocks)).
     
    .PARAMETER GenerateObjects
        By default, Invoke-PSMDTemplate generates files.
        In GenerateObjects mode, no file but objects are created.
     
    .PARAMETER Force
        If the target path the template should be written to (filename or folder name within $OutPath), then overwrite it.
        By default, this function will fail if an overwrite is required.
     
    .PARAMETER Silent
        This places the function in unattended mode, causing it to error on anything requiring direct user input.
 
    .PARAMETER NoConfigFile
        By default, this command will look in the execution path and above for files named "PSMDConfig.psd1" to populate template parameters from.
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Invoke-PSMDTemplate -TemplateName "module"
         
        Creates a project based on the module template in the current folder, asking for all details.
     
    .EXAMPLE
        PS C:\> Invoke-PSMDTemplate -TemplateName "module" -Name "MyModule"
         
        Creates a project based on the module template with the name "MyModule"
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [OutputType([PSModuleDevelopment.Template.TemplateResult])]
    [Alias('imt')]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'NameStore')]
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'NamePath')]
        [string]
        $TemplateName,
        
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Template')]
        [PSModuleDevelopment.Template.TemplateInfo[]]
        $Template,
        
        [Parameter(ParameterSetName = 'NameStore')]
        [string]
        $Store = "*",
        
        [Parameter(Mandatory = $true, ParameterSetName = 'NamePath')]
        [string]
        $Path,
        
        [Parameter(Position = 2)]
        [PSFramework.Validation.PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')]
        [string]
        $OutPath = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Template.OutPath' -Fallback "."),
        
        [Parameter(Position = 1)]
        [string]
        $Name,
        
        [PSFEncoding]
        $Encoding = (Get-PSFConfigValue -FullName 'PSFramework.Text.Encoding.DefaultWrite'),
        
        [switch]
        $NoFolder,
        
        [hashtable]
        $Parameters = @{ },
        
        [switch]
        $Raw,
        
        [switch]
        $GenerateObjects,
        
        [switch]
        $Force,
        
        [switch]
        $Silent,

        [switch]
        $NoConfigFile,
        
        [switch]
        $EnableException
    )
    
    begin {
        $resolvedPath = Resolve-PSFPath -Path $OutPath

        $templates = @()
        switch ($PSCmdlet.ParameterSetName) {
            'NameStore' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Store $Store }
            'NamePath' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Path $Path }
        }
        if ($TemplateName -and -not $templates) {
            Stop-PSFFunction -String 'Invoke-PSMDTemplate.Template.NotFound' -StringValues $TemplateName -EnableException $EnableException -Cmdlet $PSCmdlet
            return
        }
        
        #region Parameter Processing
        if (-not $Parameters) { $Parameters = @{ } }
        if ($Name) { $Parameters["Name"] = $Name }
        
        if ($NoConfigFile) { $paramCloned = Resolve-TemplateParameter -Configuration $Parameters -FromConfiguration}
        else { $paramCloned = Resolve-TemplateParameter -Path $resolvedPath -Configuration $Parameters -FromConfiguration }
        #endregion Parameter Processing
        
        #region Helper function
        function Invoke-Template {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [CmdletBinding()]
            param (
                [PSModuleDevelopment.Template.TemplateInfo]
                $Template,
                
                [string]
                $OutPath,
                
                [PSFEncoding]
                $Encoding,
                
                [bool]
                $NoFolder,
                
                [hashtable]
                $Parameters,
                
                [bool]
                $Raw,
                
                [switch]
                $GenerateObjects,
        
                [bool]
                $Silent
            )
            Write-PSFMessage -Level Verbose -Message "Processing template $($item)" -Tag 'template', 'invoke' -FunctionName Invoke-PSMDTemplate
            
            $templateData = Import-PSFClixml -Path $Template.Path -ErrorAction Stop
            #region Process Parameters
            foreach ($parameter in $templateData.Parameters) {
                if (-not $parameter) { continue }
                if (-not $Parameters.ContainsKey($parameter)) {
                    if ($Silent) { throw "Parameter not specified: $parameter" }
                    try {
                        $value = Read-Host -Prompt "Enter value for parameter '$parameter'" -ErrorAction Stop
                        $Parameters[$parameter] = $value
                    }
                    catch { throw }
                }
            }
            #endregion Process Parameters
            
            #region Scripts
            $scriptParameters = @{ }
            
            if (-not $Raw) {
                foreach ($scriptParam in $templateData.Scripts.Values) {
                    if (-not $scriptParam) { continue }
                    try { $scriptParameters[$scriptParam.Name] = "$([scriptblock]::Create($scriptParam.StringScript).Invoke())" }
                    catch {
                        if ($Silent) { throw (New-Object System.Exception("Scriptblock $($scriptParam.Name) failed during execution: $_", $_.Exception)) }
                        else {
                            Write-PSFMessage -Level Warning -Message "Scriptblock $($scriptParam.Name) failed during execution. Please specify a custom value or use CTRL+C to terminate creation" -ErrorRecord $_ -FunctionName "Invoke-PSMDTemplate" -ModuleName 'PSModuleDevelopment'
                            $scriptParameters[$scriptParam.Name] = Read-Host -Prompt "Value for script $($scriptParam.Name)"
                        }
                    }
                }
            }
            #endregion Scripts
            $createdTemplateItems = switch ($templateData.Type.ToString()) {
                #region File
                "File" {
                    foreach ($child in $templateData.Children) {
                        New-TemplateItem -Item $child -Path $OutPath -ParameterFlat $Parameters -ParameterScript $scriptParameters -Raw $Raw
                    }
                    if ($Raw -and $templateData.Scripts.Values) {
                        $templateData.Scripts.Values | Export-Clixml -Path (Join-Path $OutPath "_PSMD_ParameterScripts.xml")
                    }
                }
                #endregion File
                
                #region Project
                "Project" {
                    #region Resolve output folder
                    if (-not $NoFolder) {
                        if ($Parameters["Name"]) {
                            $projectName = $Parameters["Name"]
                            $projectFullName = Join-Path $OutPath $projectName
                            if ((Test-Path $projectFullName) -and (-not $Force)) {
                                throw "Project root folder already exists: $projectFullName"
                            }
                            $newFolder = New-Item -Path $OutPath -Name $Parameters["Name"] -ItemType Directory -ErrorAction Stop -Force
                        }
                        else {
                            throw "Parameter Name is needed to create a project without setting the -NoFolder parameter!"
                        }
                    }
                    else { $newFolder = Get-Item $OutPath }
                    #endregion Resolve output folder
                    
                    foreach ($child in $templateData.Children) {
                        New-TemplateItem -Item $child -Path $newFolder.FullName -ParameterFlat $Parameters -ParameterScript $scriptParameters -Raw $Raw
                    }
                    
                    #region Write Config File (Raw)
                    if ($Raw) {
                        $guid = [System.Guid]::NewGuid().ToString()
                        $optionsTemplate = @"
@{
    TemplateName = "$($Template.Name)"
    Version = ([Version]"$($Template.Version)")
    Tags = $(($Template.Tags | ForEach-Object { "'$_'" }) -join ",")
    Author = "$($Template.Author)"
    Description = "$($Template.Description)"
þþþPLACEHOLDER-$($guid)þþþ
}
"@

                        if ($params = $templateData.Scripts.Values) {
                            $list = @()
                            foreach ($param in $params) {
                                $list += @"
    $($param.Name) = {
        $($param.StringScript)
    }
"@

                            }
                            $optionsTemplate = $optionsTemplate -replace "þþþPLACEHOLDER-$($guid)þþþ", ($list -join "`n`n")
                        }
                        else {
                            $optionsTemplate = $optionsTemplate -replace "þþþPLACEHOLDER-$($guid)þþþ", ""
                        }
                        
                        [PSModuleDevelopment.Template.TemplateResult]@{
                            Name     = "PSMDTemplate.ps1"
                            Path     = $newFolder.FullName
                            FullPath = (Join-Path $newFolder.FullName "PSMDTemplate.ps1")
                            Content  = $optionsTemplate
                        }
                    }
                    #endregion Write Config File (Raw)
                }
                #endregion Project
            }
            If ($GenerateObjects) {
                return $createdTemplateItems
            }
            Write-TemplateResult -TemplateResult $createdTemplateItems -Encoding $Encoding
        }
        
        function New-TemplateItem {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [OutputType([PSModuleDevelopment.Template.TemplateResult])]
            [CmdletBinding()]
            param (
                [PSModuleDevelopment.Template.TemplateItemBase]
                $Item,
                
                [string]
                $Path,
                
                [hashtable]
                $ParameterFlat,
                
                [hashtable]
                $ParameterScript,
                
                [bool]
                $Raw
            )
            Write-PSFMessage -Level Verbose -Message "Creating Template-Item: $($Item.Name) ($($Item.RelativePath))" -FunctionName Invoke-PSMDTemplate -ModuleName PSModuleDevelopment -Tag 'create', 'template'
            
            $identifier = $Item.Identifier
            $isFile = $Item.GetType().Name -eq 'TemplateItemFile'
            
            #region File
            if ($isFile) {
                $fileName = $Item.Name
                if (-not $Raw) {
                    foreach ($param in $Item.FileSystemParameterFlat) {
                        $fileName = [PSModuleDevelopment.Utility.UtilityHost]::Replace($fileName, "$($identifier)$($param)$($identifier)", $ParameterFlat[$param], $false)
                    }
                    foreach ($param in $Item.FileSystemParameterScript) {
                        $fileName = [PSModuleDevelopment.Utility.UtilityHost]::Replace($fileName, "$($identifier)$($param)$($identifier)", $ParameterScript[$param], $false)
                    }
                }
                $destPath = Join-Path $Path $fileName
                
                if ($Item.PlainText) {
                    $text = $Item.Value
                    if (-not $Raw) {
                        foreach ($param in $Item.ContentParameterFlat) {
                            $text = [PSModuleDevelopment.Utility.UtilityHost]::Replace($text, "$($identifier)$($param)$($identifier)", $ParameterFlat[$param], $false)
                        }
                        foreach ($param in $Item.ContentParameterScript) {
                            $text = [PSModuleDevelopment.Utility.UtilityHost]::Replace($text, "$($identifier)!$($param)!$($identifier)", $ParameterScript[$param], $false)
                        }
                    }
                    return [PSModuleDevelopment.Template.TemplateResult]@{
                        Name     = $fileName
                        Path     = $Path
                        FullPath = $destPath
                        Content  = $text
                    }
                }
                else {
                    $bytes = [System.Convert]::FromBase64String($Item.Value)
                    return [PSModuleDevelopment.Template.TemplateResult]@{
                        Name     = $fileName
                        Path     = $Path
                        FullPath = $destPath
                        Content  = $bytes
                        IsText   = $false
                    }
                }
            }
            #endregion File
            
            #region Folder
            else {
                $folderName = $Item.Name
                if (-not $Raw) {
                    foreach ($param in $Item.FileSystemParameterFlat) {
                        $folderName = $folderName -replace "$($identifier)$([regex]::Escape($param))$($identifier)", $ParameterFlat[$param]
                    }
                    foreach ($param in $Item.FileSystemParameterScript) {
                        $folderName = $folderName -replace "$($identifier)!$([regex]::Escape($param))!$($identifier)", $ParameterScript[$param]
                    }
                }
                $folder = Join-Path -Path $Path -ChildPath $folderName

                # Return a folder object to make sure empty folders are not excluded
                [PSModuleDevelopment.Template.TemplateResult]@{
                    Name     = $folderName
                    Path     = $Path
                    FullPath = $folder
                    IsFolder = $true
                    IsText   = $false
                }
                
                foreach ($child in $Item.Children) {
                    New-TemplateItem -Item $child -Path $folder -ParameterFlat $ParameterFlat -ParameterScript $ParameterScript -Raw $Raw
                }
            }
            #endregion Folder
        }
        
        function Write-TemplateResult {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [CmdletBinding()]
            param (
                [PSModuleDevelopment.Template.TemplateResult[]]
                $TemplateResult,
                
                [PSFEncoding]
                $Encoding
            )
            $msgParam = @{ Level = 'Verbose'; FunctionName = 'Invoke-PSMDTemplate' }
            foreach ($item in $TemplateResult | Sort-Object { $_.FullPath.Length }) {
                Write-PSFMessage @msgParam -Message "Creating file: $($item.FullPath)" -Tag 'create', 'template'
                if (-not (Test-Path $item.Path)) {
                    Write-PSFMessage -Level Verbose -Message "Creating Folder $($item.Path)"
                    $null = New-Item -Path $item.Path -ItemType Directory
                }
                if ($item.IsFolder) {
                    if (-not (Test-Path $item.FullPath)) {
                        Write-PSFMessage  @msgParam -Message "Creating Folder $($item.FullPath)"
                        $null = New-Item -Path $item.FullPath -ItemType Directory
                    }
                    continue
                }
                if ($item.IsText) {
                    Write-PSFMessage @msgParam -Message "Creating as a Text-File"
                    [System.IO.File]::WriteAllText($item.FullPath, $item.Content, $Encoding)
                }
                else {
                    Write-PSFMessage @msgParam -Message "Creating as a Binary-File"
                    [System.IO.File]::WriteAllBytes($item.FullPath, $item.Content)
                }
            }
        }
        #endregion Helper function
    }
    process {
        if (Test-PSFFunctionInterrupt) { return }
        
        $invokeParam = @{
            Parameters      = $paramCloned
            OutPath         = $resolvedPath
            NoFolder        = $NoFolder
            Encoding        = $Encoding
            Raw             = $Raw
            Silent          = $Silent
            GenerateObjects = $GenerateObjects
        }
        
        foreach ($item in $Template) {
            Invoke-PSFProtectedCommand -ActionString 'Invoke-PSMDTemplate.Invoking' -ActionStringValues $item -Target $item -ScriptBlock {
                Invoke-Template @invokeParam -Template $item
            } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
        }
        foreach ($item in $templates) {
            Invoke-PSFProtectedCommand -ActionString 'Invoke-PSMDTemplate.Invoking' -ActionStringValues $item -Target $item -ScriptBlock {
                Invoke-Template @invokeParam -Template $item
            } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
        }
    }
}


function New-PSMDDotNetProject
{
<#
    .SYNOPSIS
        Wrapper function around 'dotnet new'
     
    .DESCRIPTION
        This function is a wrapper around the dotnet.exe application with the parameter 'new'.
        It can be used to create projects from templates, as well as to administrate templates.
     
    .PARAMETER TemplateName
        The name of the template to create
     
    .PARAMETER List
        List the existing templates.
     
    .PARAMETER Help
        Ask for help / documentation.
        Particularly useful when dealing with project types that have a lot of options.
     
    .PARAMETER Force
        Overwrite existing files.
     
    .PARAMETER Name
        The name of the project to create
     
    .PARAMETER Output
        The folder in which to create it.
        Note: This folder will automatically be te root folder of the project.
        If this folder doesn't exist yet, it will be created.
        When used with -Force, it will automatically purge all contents.
     
    .PARAMETER Install
        Install the specified template from the VS marketplace.
     
    .PARAMETER Uninstall
        Uninstall an installed template.
     
    .PARAMETER Arguments
        Additional arguments to pass to the application.
        Generally used for parameters when creating a project from a template.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> dotnetnew -l
     
        Lists all installed templates.
     
    .EXAMPLE
        PS C:\> dotnetnew mvc foo F:\temp\projects\foo -au Windows --no-restore
     
        Creates a new MVC project named "foo" in folder "F:\Temp\projects\foo"
        - It will set authentication to windows
        - It will skip the automatic restore of the project on create
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [Alias('dotnetnew')]
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Create')]
    Param (
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = 'Create')]
        [Parameter(Position = 0, ParameterSetName = 'List')]
        [string]
        $TemplateName,
        
        [Parameter(ParameterSetName = 'List')]
        [Alias('l')]
        [switch]
        $List,
        
        [Alias('h')]
        [switch]
        $Help,
        
        [switch]
        $Force,
        
        [Parameter(Position = 1, ParameterSetName = 'Create')]
        [Alias('n')]
        [string]
        $Name,
        
        [Parameter(Position = 2, ParameterSetName = 'Create')]
        [Alias('o')]
        [string]
        $Output,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Install')]
        [Alias('i')]
        [string]
        $Install,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Uninstall')]
        [Alias('u')]
        [string]
        $Uninstall,
        
        [Parameter(ValueFromRemainingArguments = $true)]
        [Alias('a')]
        [string[]]
        $Arguments
    )
    
    begin
    {
        $parset = $PSCmdlet.ParameterSetName
        Write-PSFMessage -Level InternalComment -Message "Active parameterset: $parset" -Tag 'start'
        
        if (-not (Get-Command dotnet.exe))
        {
            throw "Could not find dotnet.exe! This should automatically be available on machines with Visual Studio installed."
        }
        
        $dotNetArgs = @()
        switch ($parset)
        {
            'Create'
            {
                if (Test-PSFParameterBinding -ParameterName TemplateName) { $dotNetArgs += $TemplateName }
                if ($Help) { $dotNetArgs += "-h" }
                if (Test-PSFParameterBinding -ParameterName Name)
                {
                    $dotNetArgs += "-n"
                    $dotNetArgs += $Name
                }
                if (Test-PSFParameterBinding -ParameterName Output)
                {
                    $dotNetArgs += "-o"
                    $dotNetArgs += $Output
                }
                if ($Force) { $dotNetArgs += "--Force" }
            }
            'List'
            {
                if (Test-PSFParameterBinding -ParameterName TemplateName) { $dotNetArgs += $TemplateName }
                $dotNetArgs += '-l'
                if ($Help) { $dotNetArgs += "-h" }
            }
            'Install'
            {
                $dotNetArgs += '-i'
                $dotNetArgs += $Install
                if ($Help) { $dotNetArgs += '-h'}
            }
            'Uninstall'
            {
                $dotNetArgs += '-u'
                $dotNetArgs += $Uninstall
                if ($Help) { $dotNetArgs += '-h' }
            }
        }
        
        foreach ($item in $Arguments)
        {
            $dotNetArgs += $item
        }
        Write-PSFMessage -Level Verbose -Message "Resolved arguments: $($dotNetArgs -join " ")" -Tag 'argument','start'
    }
    process
    {
        if ($PSCmdlet.ShouldProcess("dotnet", "Perform action: $parset"))
        {
            if ($parset -eq 'Create')
            {
                if ($Output)
                {
                    if ((Test-Path $Output) -and $Force) { $null = New-Item $Output -ItemType Directory -Force -ErrorAction Stop }
                    if (-not (Test-Path $Output)) { $null = New-Item $Output -ItemType Directory -Force -ErrorAction Stop }
                }
            }
            Write-PSFMessage -Level Verbose -Message "Executing with arguments: $($dotNetArgs -join " ")" -Tag 'argument', 'start'
            & dotnet.exe new $dotNetArgs
        }
    }
}

function New-PSMDTemplate
{
<#
    .SYNOPSIS
        Creates a template from a reference file / folder.
     
    .DESCRIPTION
        This function creates a template based on an existing folder or file.
        It automatically detects parameters that should be filled in one creation time.
         
        # Template reference: #
        #---------------------#
        Project templates can be preconfigured by a special reference file in the folder root.
        This file must be named "PSMDTemplate.ps1" and will not be part of the template.
        It must emit a single hashtable with various pieces of information.
         
        This hashtable can have any number of the following values, in any desired combination:
        - Scripts: A Hashtable, of scriptblocks. These are scripts used for replacement parameters, the key is the name used on insertions.
        - TemplateName: Name of the template
        - Version: The version number for the template (See AutoIncrementVersion property)
        - AutoIncrementVersion: Whether the version number should be incremented
        - Tags: Tags to add to a template - makes searching and finding templates easier
        - Author: Name of the author of the template
        - Description: Description of the template
        - Exclusions: List of relative file/folder names to not process / skip.
        Each of those entries can also be overridden by specifying the corresponding parameter of this function.
         
        # Parameterizing templates: #
        #---------------------------#
        The script will pick up any parameter found in the files and folders (including the file/folder name itself).
        There are three ways to do this:
        - Named text replacement: The user will need to specify what to insert into this when creating a new project from this template.
        - Scriptblock replacement: The included scriptblock will be executed on initialization, in order to provide a text to insert. Duplicate scriptblocks will be merged.
        - Named scriptblock replacement: The template reference file can define scriptblocks, their value will be inserted here.
        The same name can be reused any number of times across the entire project, it will always receive the same input.
         
        Naming Rules:
        - Parameter names cannot include the characters '!', '{', or '}'
        - Parameter names cannot include the parameter identifier. This is by default 'þ'.
        This identifier can be changed by updating the 'psmoduledevelopment.template.identifier' configuration setting.
        - Names are not case sensitive.
         
        Examples:
        ° Named for replacement:
        "Test þnameþ" --> "Test <inserted text of parameter>"
         
        ° Scriptblock replacement:
        "Test þ{ $env:COMPUTERNAME }þ" --> "Test <Name of invoking computer>"
        - Important: No space between identifier and curly braces!
        - Scriptblock can have multiple lines.
         
        ° Named Scriptblock replacement:
        "Test þ!ClosestDomainController!þ" --> "Test <Result of script ClosestDomainController>"
        - Named Scriptblocks are created by using a template reference file (see section above)
     
    .PARAMETER ReferencePath
        Root path in which all files are selected for creating a template project.
        The folder will not be part of the template, only its content.
     
    .PARAMETER FilePath
        Path to a single file.
        Used to create a template for that single file, instead of a full-blown project.
        Note: Does not support template reference files.
     
    .PARAMETER TemplateName
        Name of the template.
     
    .PARAMETER Filter
        Only files matching this filter will be included in the template.
     
    .PARAMETER OutStore
        Where the template will be stored at.
        By default, it will push the template to the default store (A folder in appdata unless configuration was changed).
     
    .PARAMETER OutPath
        If the template should be written to a specific path instead.
        Specify a folder.
     
    .PARAMETER Exclusions
        The relative path of the files or folders to ignore.
        Ignoring folders will also ignore all items in the folder.
     
    .PARAMETER Version
        The version of the template.
     
    .PARAMETER Author
        The author of the template.
     
    .PARAMETER Description
        A description text for the template itself.
        This will be visible to the user before invoking the template and should describe what this template is for.
     
    .PARAMETER Tags
        Tags to apply to the template, making it easier to filter & search.
     
    .PARAMETER Force
        If the template in the specified version in the specified destination already exists, this will fail unless the Force parameter is used.
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .EXAMPLE
        PS C:\> New-PSMDTemplate -FilePath .\þnameþ.Test.ps1 -TemplateName functiontest
     
        Creates a new template named 'functiontest', based on the content of '.\þnameþ.Test.ps1'
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName = 'Project')]
    param (
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Project')]
        [string]
        $ReferencePath,
        
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'File')]
        [string]
        $FilePath,
        
        [Parameter(Position = 1, ParameterSetName = 'Project')]
        [Parameter(Position = 1, ParameterSetName = 'File', Mandatory = $true)]
        [string]
        $TemplateName,
        
        [string]
        $Filter = "*",
        
        [string]
        $OutStore = "Default",
        
        [string]
        $OutPath,
        
        [string[]]
        $Exclusions,
        
        [version]
        $Version = "1.0.0.0",
        
        [string]
        $Description,
        
        [string]
        $Author = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Template.ParameterDefault.Author' -Fallback $env:USERNAME),
        
        [string[]]
        $Tags,
        
        [switch]
        $Force,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        #region Insert basic meta-data
        $identifier = [regex]::Escape(( Get-PSFConfigValue -FullName 'psmoduledevelopment.template.identifier' -Fallback 'þ' ))
        $binaryExtensions = Get-PSFConfigValue -FullName 'PSModuleDevelopment.Template.BinaryExtensions' -Fallback @('.dll', '.exe', '.pdf', '.doc', '.docx', '.xls', '.xlsx')
        
        $template = New-Object PSModuleDevelopment.Template.Template
        $template.Name = $TemplateName
        $template.Version = $Version
        $template.Tags = $Tags
        $template.Description = $Description
        $template.Author = $Author
        
        if ($PSCmdlet.ParameterSetName -eq 'File')
        {
            $template.Type = 'File'
        }
        else
        {
            $template.Type = 'Project'
            
            $processedReferencePath = Resolve-Path $ReferencePath
            
            if (Test-Path (Join-Path $processedReferencePath "PSMDTemplate.ps1"))
            {
                $templateData = & (Join-Path $processedReferencePath "PSMDTemplate.ps1")
                foreach ($key in $templateData.Scripts.Keys)
                {
                    $template.Scripts[$key] = New-Object PSModuleDevelopment.Template.ParameterScript($key, $templateData.Scripts[$key])
                }
                if ($templateData.TemplateName -and (Test-PSFParameterBinding -ParameterName TemplateName -Not)) { $template.Name = $templateData.TemplateName }
                if ($templateData.Version -and (Test-PSFParameterBinding -ParameterName Version -Not)) { $template.Version = $templateData.Version }
                if ($templateData.Tags -and (Test-PSFParameterBinding -ParameterName Tags -Not)) { $template.Tags = $templateData.Tags }
                if ($templateData.Description -and (Test-PSFParameterBinding -ParameterName Description -Not)) { $template.Description = $templateData.Description }
                if ($templateData.Author -and (Test-PSFParameterBinding -ParameterName Author -Not)) { $template.Author = $templateData.Author }
                
                if (-not $template.Name)
                {
                    Stop-PSFFunction -Message "No template name detected: Make sure to specify it as parameter or include it in the 'PSMDTemplate.ps1' definition file!" -EnableException $EnableException
                    return
                }
                
                if ($templateData.AutoIncrementVersion)
                {
                    $oldTemplate = Get-PSMDTemplate -TemplateName $template.Name -WarningAction SilentlyContinue | Sort-Object Version | Select-Object -First 1
                    if (($oldTemplate) -and ($oldTemplate.Version -ge $template.Version))
                    {
                        $major = $oldTemplate.Version.Major
                        $minor = $oldTemplate.Version.Minor
                        $revision = $oldTemplate.Version.Revision
                        $build = $oldTemplate.Version.Build
                        
                        # Increment lowest element
                        if ($build -ge 0) { $build++ }
                        elseif ($revision -ge 0) { $revision++ }
                        elseif ($minor -ge 0) { $minor++ }
                        else { $major++ }
                        $template.Version = "$($major).$($minor).$($revision).$($build)" -replace "\.-1",''
                    }
                }
                
                if ($templateData.Exclusions -and (Test-PSFParameterBinding -ParameterName Exclusions -Not)) { $Exclusions = $templateData.Exclusions }
            }
            
            if ($Exclusions)
            {
                $oldExclusions = $Exclusions
                $Exclusions = @()
                foreach ($exclusion in $oldExclusions)
                {
                    $Exclusions += Join-Path $processedReferencePath $exclusion
                }
            }
        }
        #endregion Insert basic meta-data
        
        #region Validation
        #region Validate FilePath
        if ($FilePath)
        {
            if (-not (Test-Path $FilePath -PathType Leaf))
            {
                Stop-PSFFunction -Message "Filepath $FilePath is invalid. Ensure it exists and is a file" -EnableException $EnableException -Category InvalidArgument -Tag 'fail', 'argument', 'path'
                return
            }
        }
        #endregion Validate FilePath
        
        #region Validate & ensure output folder
        $fileName = "$($template.Name)-$($template.Version).xml"
        $infoFileName = "$($template.Name)-$($template.Version)-Info.xml"
        if ($OutPath) { $exportFolder = $OutPath }
        else { $exportFolder = Get-PsmdTemplateStore -Filter $OutStore | Select-Object -ExpandProperty Path -First 1 }
        
        if (-not $exportFolder)
        {
            Stop-PSFFunction -Message "Unable to resolve a path to create the template in. Verify a valid template store or path were specified." -Category InvalidArgument -EnableException $EnableException -Tag 'fail', 'argument', 'path'
            return
        }
        
        if (-not (Test-Path $exportFolder))
        {
            if ($Force)
            {
                try { $null = New-Item -Path $exportFolder -ItemType Directory -Force -ErrorAction Stop }
                catch
                {
                    Stop-PSFFunction -Message "Failed to create output path: $exportFolder" -ErrorRecord $_ -Tag 'fail', 'folder', 'create' -EnableException $EnableException
                    return
                }
            }
            else
            {
                Stop-PSFFunction -Message "Output folder does not exist. Use '-Force' to have this function automatically create it: $exportFolder" -Category InvalidArgument -EnableException $EnableException -Tag 'fail', 'argument', 'path'
                return
            }
        }
        
        if ((Test-Path (Join-Path $exportFolder $fileName)) -and (-not $Force))
        {
            Stop-PSFFunction -Message "Template already exists in the current version. Use '-Force' if you want to overwrite it!" -Category InvalidArgument -EnableException $EnableException -Tag 'fail', 'argument', 'path'
            return
        }
        #endregion Validate & ensure output folder
        #endregion Validation
        
        #region Utility functions
        function Convert-Item
        {
            [CmdletBinding()]
            param (
                [System.IO.FileSystemInfo]
                $Item,
                
                [PSModuleDevelopment.Template.TemplateItemBase]
                $Parent,
                
                [string]
                $Filter,
                
                [string[]]
                $Exclusions,
                
                [PSModuleDevelopment.Template.Template]
                $Template,
                
                [string]
                $ReferencePath,
                
                [string]
                $Identifier,
                
                [string[]]
                $BinaryExtensions
            )
            
            if ($Item.FullName -in $Exclusions) { return }
            
            #region Regex
            <#
                Fixed string Replacement pattern:
                "$($Identifier)([^{}!]+?)$($Identifier)"
             
                Named script replacement pattern:
                "$($Identifier)!([^{}!]+?)!$($Identifier)"
             
                Live script replacement pattern:
                "$($Identifier){(.+?)}$($Identifier)"
             
                Chained together in a logical or, in order to avoid combination issues.
            #>

            $pattern = "$($Identifier)([^{}!]+?)$($Identifier)|$($Identifier)!([^{}!]+?)!$($Identifier)|(?ms)$($Identifier){(.+?)}$($Identifier)"
            #endregion Regex
            
            $name = $Item.Name
            $relativePath = ""
            if ($ReferencePath)
            {
                $relativePath = ($Item.FullName -replace "^$([regex]::Escape($ReferencePath))","").Trim("\")
            }
            
            #region Folder
            if ($Item.GetType().Name -eq "DirectoryInfo")
            {
                $object = New-Object PSModuleDevelopment.Template.TemplateItemFolder
                $object.Name = $name
                $object.RelativePath = $relativePath
                
                foreach ($find in ([regex]::Matches($name, $pattern, 'IgnoreCase')))
                {
                    #region Fixed string replacement
                    if ($find.Groups[1].Success)
                    {
                        if ($object.FileSystemParameterFlat -notcontains $find.Groups[1].Value)
                        {
                            $null = $object.FileSystemParameterFlat.Add($find.Groups[1].Value)
                        }
                        if ($Template.Parameters -notcontains $find.Groups[1].Value)
                        {
                            $null = $Template.Parameters.Add($find.Groups[1].Value)
                        }
                    }
                    #endregion Fixed string replacement
                    
                    #region Named Scriptblock replacement
                    if ($find.Groups[2].Success)
                    {
                        $scriptName = $find.Groups[2].Value
                        if ($Template.Scripts.Keys -eq $scriptName)
                        {
                            $object.FileSystemParameterScript($scriptName)
                        }
                        else
                        {
                            throw "Unknown named scriptblock '$($scriptName)' in name of '$($Item.FullName)'. Make sure the named scriptblock exists in the configuration file."
                        }
                    }
                    #endregion Named Scriptblock replacement
                }
                
                foreach ($child in (Get-ChildItem -Path $Item.FullName -Filter $Filter))
                {
                    $paramConvertItem = @{
                        Item               = $child
                        Filter               = $Filter
                        Exclusions           = $Exclusions
                        Template           = $Template
                        ReferencePath       = $ReferencePath
                        Identifier           = $Identifier
                        BinaryExtensions   = $BinaryExtensions
                        Parent               = $object
                    }
                    
                    Convert-Item @paramConvertItem
                }
            }
            #endregion Folder
            
            #region File
            else
            {
                $object = New-Object PSModuleDevelopment.Template.TemplateItemFile
                $object.Name = $name
                $object.RelativePath = $relativePath
                
                #region File Name
                foreach ($find in ([regex]::Matches($name, $pattern, 'IgnoreCase')))
                {
                    #region Fixed string replacement
                    if ($find.Groups[1].Success)
                    {
                        if ($object.FileSystemParameterFlat -notcontains $find.Groups[1].Value)
                        {
                            $null = $object.FileSystemParameterFlat.Add($find.Groups[1].Value)
                        }
                        if ($Template.Parameters -notcontains $find.Groups[1].Value)
                        {
                            $null = $Template.Parameters.Add($find.Groups[1].Value)
                        }
                    }
                    #endregion Fixed string replacement
                    
                    #region Named Scriptblock replacement
                    if ($find.Groups[2].Success)
                    {
                        $scriptName = $find.Groups[2].Value
                        if ($Template.Scripts.Keys -eq $scriptName)
                        {
                            $null = $object.FileSystemParameterScript.Add($scriptName)
                        }
                        else
                        {
                            throw "Unknown named scriptblock '$($scriptName)' in name of '$($Item.FullName)'. Make sure the named scriptblock exists in the configuration file."
                        }
                    }
                    #endregion Named Scriptblock replacement
                }
                #endregion File Name
                
                #region File Content
                if (-not ($Item.Extension -in $BinaryExtensions))
                {
                    $text = [System.IO.File]::ReadAllText($Item.FullName)
                    foreach ($find in ([regex]::Matches($text, $pattern, 'IgnoreCase, Multiline')))
                    {
                        #region Fixed string replacement
                        if ($find.Groups[1].Success)
                        {
                            if ($object.ContentParameterFlat -notcontains $find.Groups[1].Value)
                            {
                                $null = $object.ContentParameterFlat.Add($find.Groups[1].Value)
                            }
                            if ($Template.Parameters -notcontains $find.Groups[1].Value)
                            {
                                $null = $Template.Parameters.Add($find.Groups[1].Value)
                            }
                        }
                        #endregion Fixed string replacement
                        
                        #region Named Scriptblock replacement
                        if ($find.Groups[2].Success)
                        {
                            $scriptName = $find.Groups[2].Value
                            if ($Template.Scripts.Keys -eq $scriptName)
                            {
                                $null = $object.ContentParameterScript.Add($scriptName)
                            }
                            else
                            {
                                throw "Unknown named scriptblock '$($scriptName)' in name of '$($Item.FullName)'. Make sure the named scriptblock exists in the configuration file."
                            }
                        }
                        #endregion Named Scriptblock replacement
                        
                        #region Live Scriptblock replacement
                        if ($find.Groups[3].Success)
                        {
                            $scriptCode = $find.Groups[3].Value
                            $scriptBlock = [ScriptBlock]::Create($scriptCode)
                            
                            if ($scriptBlock.ToString() -in $Template.Scripts.Values.StringScript)
                            {
                                $scriptName = ($Template.Scripts.Values | Where-Object StringScript -EQ $scriptBlock.ToString() | Select-Object -First 1).Name
                                if ($object.ContentParameterScript -notcontains $scriptName)
                                {
                                    $null = $object.ContentParameterScript.Add($scriptName)
                                }
                                $text = $text -replace ([regex]::Escape("$($Identifier){$($scriptCode)}$($Identifier)")), "$($Identifier)!$($scriptName)!$($Identifier)"
                            }
                            
                            else
                            {
                                do
                                {
                                    $scriptName = "dynamicscript_$(Get-Random -Minimum 100000 -Maximum 999999)"
                                }
                                until ($Template.Scripts.Keys -notcontains $scriptName)
                                
                                $parameter = New-Object PSModuleDevelopment.Template.ParameterScript($scriptName, ([System.Management.Automation.ScriptBlock]::Create($scriptCode)))
                                $Template.Scripts[$scriptName] = $parameter
                                $null = $object.ContentParameterScript.Add($scriptName)
                                $text = $text -replace ([regex]::Escape("$($Identifier){$($scriptCode)}$($Identifier)")), "$($Identifier)!$($scriptName)!$($Identifier)"
                            }
                        }
                        #endregion Live Scriptblock replacement
                    }
                    $object.Value = $text
                }
                else
                {
                    $bytes = [System.IO.File]::ReadAllBytes($Item.FullName)
                    $object.Value = [System.Convert]::ToBase64String($bytes)
                    $object.PlainText = $false
                }
                #endregion File Content
            }
            #endregion File
            
            # Set identifier, so that Invoke-PSMDTemplate knows what to use when creating the item
            # Needed for sharing templates between users with different identifiers
            $object.Identifier = $Identifier
            
            if ($Parent)
            {
                $null = $Parent.Children.Add($object)
            }
            else
            {
                $null = $Template.Children.Add($object)
            }
        }
        #endregion Utility functions
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        #region Parse content and produce template
        if ($ReferencePath)
        {
            foreach ($item in (Get-ChildItem -Path $processedReferencePath -Filter $Filter))
            {
                if ($item.FullName -in $Exclusions) { continue }
                if ($item.Name -eq "PSMDTemplate.ps1") { continue }
                Convert-Item -Item $item -Filter $Filter -Exclusions $Exclusions -Template $template -ReferencePath $processedReferencePath -Identifier $identifier -BinaryExtensions $binaryExtensions
            }
        }
        else
        {
            $item = Get-Item -Path $FilePath
            Convert-Item -Item $item -Template $template -Identifier $identifier -BinaryExtensions $binaryExtensions
        }
        #endregion Parse content and produce template
    }
    end
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        $template.CreatedOn = (Get-Date).Date
        
        $template | Export-Clixml -Path (Join-Path $exportFolder $fileName)
        $template.ToTemplateInfo() | Export-Clixml -Path (Join-Path $exportFolder $infoFileName)
    }
}

function Remove-PSMDTemplate
{
<#
    .SYNOPSIS
        Removes templates
     
    .DESCRIPTION
        This function removes templates used in the PSModuleDevelopment templating system.
     
    .PARAMETER Template
        A template object returned by Get-PSMDTemplate.
        Will clear exactly the version specified, from exactly its location.
     
    .PARAMETER TemplateName
        The name of the template to remove.
        Templates are filtered by this using wildcard comparison.
     
    .PARAMETER Store
        The template store to retrieve tempaltes from.
        By default, all stores are queried.
     
    .PARAMETER Path
        Instead of a registered store, look in this path for templates.
     
    .PARAMETER Deprecated
        Will delete all versions of matching templates except for the latest one.
        Note:
        If the same template is found in multiple stores, it will keep a single copy across all stores.
        To process by store, be sure to specify the store parameter and loop over the stores desired.
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Remove-PSMDTemplate -TemplateName '*' -Deprecated
     
        Remove all templates that have been superseded by a newer version.
     
    .EXAMPLE
        PS C:\> Get-PSMDTemplate -TemplateName 'module' -RequiredVersion '1.2.2.1' | Remove-PSMDTemplate
     
        Removes all copies of the template 'module' with exactly the version '1.2.2.1'
#>

    [CmdletBinding(DefaultParameterSetName = 'NameStore', SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Template')]
        [PSModuleDevelopment.Template.TemplateInfo[]]
        $Template,
        
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'NameStore')]
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'NamePath')]
        [string]
        $TemplateName,
        
        [Parameter(ParameterSetName = 'NameStore')]
        [string]
        $Store = "*",
        
        [Parameter(Mandatory = $true, ParameterSetName = 'NamePath')]
        [string]
        $Path,
        
        [Parameter(ParameterSetName = 'NameStore')]
        [Parameter(ParameterSetName = 'NamePath')]
        [switch]
        $Deprecated,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        $templates = @()
        switch ($PSCmdlet.ParameterSetName)
        {
            'NameStore' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Store $Store -All }
            'NamePath' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Path $Path -All }
        }
        if ($Deprecated)
        {
            $toKill = @()
            $toKeep = @{ }
            foreach ($item in $templates)
            {
                if ($toKeep.Keys -notcontains $item.Name) { $toKeep[$item.Name] = $item }
                elseif ($toKeep[$item.Name].Version -lt $item.Version)
                {
                    $toKill += $toKeep[$item.Name]
                    $toKeep[$item.Name] = $item
                }
                else { $toKill += $item}
            }
            $templates = $toKill
        }
        
        function Remove-Template
        {
        <#
            .SYNOPSIS
                Deletes the files associated with a given template.
             
            .DESCRIPTION
                Deletes the files associated with a given template.
                Takes objects returned by Get-PSMDTemplate.
             
            .PARAMETER Template
                The template to kill.
             
            .EXAMPLE
                PS C:\> Remove-Template -Template $template
             
                Removes the template stored in $template
        #>

            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [CmdletBinding()]
            Param (
                [PSModuleDevelopment.Template.TemplateInfo]
                $Template
            )
            
            $pathFile = $Template.Path
            $pathInfo = $Template.Path -replace '\.xml$', '-Info.xml'
            
            Remove-Item $pathInfo -Force -ErrorAction Stop
            Remove-Item $pathFile -Force -ErrorAction Stop
        }
    }
    process
    {
        foreach ($item in $Template)
        {
            Invoke-PSFProtectedCommand -ActionString 'Remove-PSMDTemplate.Removing.Template' -Target $item.Name -ScriptBlock {
                Remove-Template -Template $item
            } -EnableException $EnableException.ToBool() -PSCmdlet $PSCmdlet -Continue -ActionStringValues $item.Name, $item.Version, $item.Store
        }
        foreach ($item in $templates)
        {
            Invoke-PSFProtectedCommand -ActionString 'Remove-PSMDTemplate.Removing.Template' -Target $item.Name -ScriptBlock {
                Remove-Template -Template $item
            } -EnableException $EnableException.ToBool() -PSCmdlet $PSCmdlet -Continue -ActionStringValues $item.Name, $item.Version, $item.Store
        }
    }
}

function Find-PSMDFileContent
{
<#
    .SYNOPSIS
        Used to quickly search in module files.
     
    .DESCRIPTION
        This function can be used to quickly search files in your module's path.
        By using Set-PSMDModulePath (or Set-PSFConfig 'PSModuleDevelopment.Module.Path' '<path>') you can set the default path to search in.
        Using
          Register-PSFConfig -FullName 'PSModuleDevelopment.Module.Path'
        allows you to persist this setting across sessions.
     
    .PARAMETER Pattern
        The text to search for, can be any regex pattern
     
    .PARAMETER Extension
        The extension of files to consider.
        Only files with this extension will be searched.
     
    .PARAMETER Path
        The path to use as search base.
        Defaults to the path found in the setting 'PSModuleDevelopment.Module.Path'
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .EXAMPLE
        PS C:\> Find-PSMDFileContent -Pattern 'Get-Test'
     
        Searches all module files for the string 'Get-Test'.
#>

    [Alias('find')]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string]
        $Pattern,
        
        [string]
        $Extension = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Find.DefaultExtensions'),
        
        [string]
        $Path = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Module.Path'),
        
        [switch]
        $EnableException
    )
    
    begin
    {
        if (-not (Test-Path -Path $Path))
        {
            Stop-PSFFunction -Message "Path not found: $Path" -EnableException $EnableException -Category InvalidArgument -Tag "fail", "path", "argument"
            return
        }
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        Get-ChildItem -Path $Path -Recurse | Where-Object Extension -Match $Extension | Select-String -Pattern $Pattern
    }
}

function Get-PSMDArgumentCompleter
{
<#
    .SYNOPSIS
        Gets the registered argument completers.
 
    .DESCRIPTION
        This function can be used to serach the argument completers registered using either the Register-ArgumentCompleter command or created using the ArgumentCompleter attribute.
 
    .PARAMETER CommandName
        Filter the results to a specific command. Wildcards are supported.
 
    .PARAMETER ParameterName
        Filter results to a specific parameter name. Wildcards are supported.
 
    .EXAMPLE
        PS C:\> Get-PSMDArgumentCompleter
 
        Get all argument completers in use in the current PowerShell session.
 
#>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName)]
        [Alias('Name')]
        [String]
        $CommandName = '*',

        [String]
        $ParameterName = '*'
    )

    begin
    {
        $internalExecutionContext = [PSFramework.Utility.UtilityHost]::GetExecutionContextFromTLS()
        $customArgumentCompleters = [PSFramework.Utility.UtilityHost]::GetPrivateProperty('CustomArgumentCompleters', $internalExecutionContext)
    }
    process
    {
        foreach ($argumentCompleter in $customArgumentCompleters.Keys)
        {
            $name, $parameter = $argumentCompleter -split ':'

            if ($name -like $CommandName)
            {
                if ($parameter -like $ParameterName)
                {
                    [pscustomobject]@{
                        CommandName   = $name
                        ParameterName = $parameter
                        Definition    = $customArgumentCompleters[$argumentCompleter]
                    }
                }
            }
        }
    }
}

function Measure-PSMDLinesOfCode
{
<#
    .SYNOPSIS
        Measures the lines of code ina PowerShell scriptfile.
     
    .DESCRIPTION
        Measures the lines of code ina PowerShell scriptfile.
        This scan uses the AST to figure out how many lines contain actual functional code.
     
    .PARAMETER Path
        Path to the files to scan.
        Folders will be ignored.
     
    .EXAMPLE
        PS C:\> Measure-PSMDLinesOfCode -Path .\script.ps1
     
        Measures the lines of code in the specified file.
     
    .EXAMPLE
        PS C:\> Get-ChildItem C:\Scripts\*.ps1 | Measure-PSMDLinesOfCode
     
        Measures the lines of code for every single file in the folder c:\Scripts.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [string[]]
        $Path
    )
    
    begin
    {
        #region Utility Functions
        function Invoke-AstWalk
        {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
            [CmdletBinding()]
            param (
                $Ast,
                
                [string[]]
                $Command,
                
                [string[]]
                $Name,
                
                [string]
                $NewName,
                
                [bool]
                $IsCommand,
                
                [bool]
                $NoAlias,
                
                [switch]
                $First
            )
            
            #Write-PSFMessage -Level Host -Message "Processing $($Ast.Extent.StartLineNumber) | $($Ast.Extent.File) | $($Ast.GetType().FullName)"
            $typeName = $Ast.GetType().FullName
            
            switch ($typeName)
            {
                'System.Management.Automation.Language.StringConstantExpressionAst'
                {
                    $Ast.Extent.StartLineNumber .. $Ast.Extent.EndLineNumber
                }
                'System.Management.Automation.Language.IfStatementAst'
                {
                    $Ast.Extent.StartLineNumber
                    $Ast.Extent.EndLineNumber
                    
                    foreach ($clause in $Ast.Clauses)
                    {
                        Invoke-AstWalk -Ast $clause.Item1 -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand
                        Invoke-AstWalk -Ast $clause.Item2 -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand
                    }
                    if ($null -ne $Ast.ElseClause)
                    {
                        Invoke-AstWalk -Ast $Ast.ElseClause -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand
                    }
                }
                default
                {
                    if (-not $First)
                    {
                        $Ast.Extent.StartLineNumber
                        $Ast.Extent.EndLineNumber
                    }
                    
                    foreach ($property in $Ast.PSObject.Properties)
                    {
                        if ($property.Name -eq "Parent") { continue }
                        if ($null -eq $property.Value) { continue }
                        
                        if (Get-Member -InputObject $property.Value -Name GetEnumerator -MemberType Method)
                        {
                            foreach ($item in $property.Value)
                            {
                                if ($item.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast")
                                {
                                    Invoke-AstWalk -Ast $item -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand
                                }
                            }
                            continue
                        }
                        
                        if ($property.Value.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast")
                        {
                            Invoke-AstWalk -Ast $property.Value -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand
                        }
                    }
                }
            }
        }
        #endregion Utility Functions
    }
    process
    {
        #region Process Files
        foreach ($fileItem in $Path)
        {
            Write-PSFMessage -Level VeryVerbose -String MeasurePSMDLinesOfCode.Processing -StringValues $fileItem
            foreach ($resolvedPath in (Resolve-PSFPath -Path $fileItem -Provider FileSystem))
            {
                if ((Get-Item $resolvedPath).PSIsContainer) { continue }
                
                $parsedItem = Read-PSMDScript -Path $resolvedPath
                
                $object = New-Object PSModuleDevelopment.Utility.LinesOfCode -Property @{
                    Path    = $resolvedPath
                }
                
                if ($parsedItem.Ast)
                {
                    $object.Ast = $parsedItem.Ast
                    $object.Lines = Invoke-AstWalk -Ast $parsedItem.Ast -First | Sort-Object -Unique
                    $object.Count = ($object.Lines | Measure-Object).Count
                    $object.Success = $true
                }
                
                $object
            }
        }
        #endregion Process Files
    }
}

function New-PSMDHeader
{
<#
    .SYNOPSIS
        Generates a header wrapping around text.
     
    .DESCRIPTION
        Generates a header wrapping around text.
        The output is an object that contains the configuration options to generate a header.
        Use its ToString() method (or cast it to string) to generate the header.
     
    .PARAMETER Text
        The text to wrap into a header.
        Can handle multiline text.
        When passing a list of strings, each string will be wrapped into its own header.
     
    .PARAMETER BorderBottom
        The border used for the bottom of the frame. Use a single letter, such as "-"
     
    .PARAMETER BorderLeft
        The border used for the left side of the frame.
     
    .PARAMETER BorderRight
        The border used for the right side of the frame.
     
    .PARAMETER BorderTop
        The border used for the top of the frame. Use a single letter, such as "-"
     
    .PARAMETER CornerLB
        The symbol used for the left-bottom corner of the frame
     
    .PARAMETER CornerLT
        The symbol used for the left-top corner of the frame
     
    .PARAMETER CornerRB
        The symbol used for the right-bottom corner of the frame
     
    .PARAMETER CornerRT
        The symbol used for the right-top corner of the frame
     
    .PARAMETER MaxWidth
        Whether to align the frame's total width to the window width.
     
    .PARAMETER Padding
        Whether the text should be padded.
        Only applies to left/right aligned text.
     
    .PARAMETER TextAlignment
        Default: Center
        Whether the text should be aligned left, center or right.
     
    .PARAMETER Width
        Total width of the header.
        Defaults to entire screen.
     
    .EXAMPLE
        PS C:\> New-PSMDHeader -Text 'Example'
     
        Will create a header labeled 'Example' that spans the entire screen.
     
    .EXAMPLE
        PS C:\> New-PSMDHeader -Text 'Example' -Width 80
     
        Will create a header labeled 'Example' with a total width of 80:
         #----------------------------------------------------------------------------#
         # Example #
         #----------------------------------------------------------------------------#
     
    .EXAMPLE
        PS C:\> New-PSMDHeader -Text 'Example' -Width 80 -BorderLeft " |" -BorderRight "| " -CornerLB " \" -CornerLT " /" -CornerRB "/" -CornerRT "\"
     
        Will create a header labeled "Example with a total width of 80 and some custom border lines:
         /----------------------------------------------------------------------------\
         | Example |
         \----------------------------------------------------------------------------/
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string[]]
        $Text,
        
        [string]
        $BorderBottom = "-",
        
        [string]
        $BorderLeft = " #",
        
        [string]
        $BorderRight = "# ",
        
        [string]
        $BorderTop = "-",
        
        [string]
        $CornerLB = " #",
        
        [string]
        $CornerLT = " #",
        
        [string]
        $CornerRB = "# ",
        
        [string]
        $CornerRT = "# ",
        
        [switch]
        $MaxWidth,
        
        [int]
        $Padding = 0,
        
        [PSModuleDevelopment.Utility.TextAlignment]
        $TextAlignment = "Center",
        
        [int]
        $Width = $Host.UI.RawUI.WindowSize.Width
    )
    
    process
    {
        foreach ($line in $Text)
        {
            $header = New-Object PSModuleDevelopment.Utility.TextHeader($line)
            
            $header.BorderBottom = $BorderBottom
            $header.BorderLeft = $BorderLeft
            $header.BorderRight = $BorderRight
            $header.BorderTop = $BorderTop
            $header.CornerLB = $CornerLB
            $header.CornerLT = $CornerLT
            $header.CornerRB = $CornerRB
            $header.CornerRT = $CornerRT
            $header.Padding = $Padding
            $header.TextAlignment = $TextAlignment
            
            if ((Test-PSFParameterBinding -ParameterName Width) -and (Test-PSFParameterBinding -ParameterName MaxWidth -Not))
            {
                $header.MaxWidth = $false
                $header.Width = $Width
            }
            else
            {
                $header.MaxWidth = $MaxWidth
                $header.Width = $Width
            }
            
            $header
        }
    }
}

function New-PSMDModuleNugetPackage
{
<#
    .SYNOPSIS
        Creates a nuget package from a PowerShell module.
     
    .DESCRIPTION
        This function will take a module and wrap it into a nuget package.
        This is accomplished by creating a temporary local filesystem repository and using the PowerShellGet module to do the actual writing.
         
        Note:
        - Requires PowerShellGet module
        - Dependencies must be built first to the same folder
     
    .PARAMETER ModulePath
        Path to the PowerShell module you are creating a Nuget package from
     
    .PARAMETER PackagePath
        Path where the package file will be copied.
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .EXAMPLE
        New-PSMDModuleNugetPackage -PackagePath 'c:\temp\package' -ModulePath .\DBOps
         
        Packages the module stored in .\DBOps and stores the nuget file in 'c:\temp\package'
     
    .NOTES
        Author: Mark Wilkinson
        Editor: Friedrich Weinmann
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('ModuleBase')]
        [string[]]
        $ModulePath,
        
        [string]
        $PackagePath = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Package.Path' -Fallback "$env:TEMP"),
        
        [switch]
        $EnableException
    )
    
    begin
    {
        #region Input validation and prerequisites check
        try
        {
            $null = Get-Command Publish-Module -ErrorAction Stop
            $null = Get-Command Register-PSRepository -ErrorAction Stop
            $null = Get-Command Unregister-PSRepository -ErrorAction Stop
        }
        catch
        {
            $paramStopPSFFunction = @{
                Message                       = "Failed to detect the PowerShellGet module! The module is required in order to execute this function."
                EnableException               = $EnableException
                Category                   = 'NotInstalled'
                ErrorRecord                   = $_
                OverrideExceptionMessage   = $true
                Tag                           = 'fail', 'validation', 'prerequisites', 'module'
            }
            Stop-PSFFunction @paramStopPSFFunction
            return
        }
        
        if (-not (Test-Path $PackagePath))
        {
            Write-PSFMessage -Level Verbose -Message "Creating path: $PackagePath" -Tag 'begin', 'create', 'path'
            try { $null = New-Item -Path $PackagePath -ItemType Directory -Force -ErrorAction Stop }
            catch
            {
                Stop-PSFFunction -Message "Failed to create output path: $PackagePath" -ErrorRecord $_ -EnableException $EnableException -Tag 'fail', 'bgin', 'create', 'path'
                return
            }
        }
        $resolvedPath = (Get-Item -Path $PackagePath).FullName
        #endregion Input validation and prerequisites check
        
        #region Prepare local Repository
        try
        {
            if (Get-PSRepository | Where-Object Name -EQ 'PSModuleDevelopment_TempLocalRepository')
            {
                Unregister-PSRepository -Name 'PSModuleDevelopment_TempLocalRepository'
            }
            $paramRegisterPSRepository = @{
                Name                 = 'PSModuleDevelopment_TempLocalRepository'
                PublishLocation         = $resolvedPath
                SourceLocation         = $resolvedPath
                InstallationPolicy   = 'Trusted'
                ErrorAction             = 'Stop'
            }
            
            Register-PSRepository @paramRegisterPSRepository
        }
        catch
        {
            Stop-PSFFunction -Message "Failed to create temporary PowerShell Repository" -ErrorRecord $_ -EnableException $EnableException -Tag 'fail', 'bgin', 'create', 'path'
            return
        }
        #endregion Prepare local Repository
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        #region Process Paths
        foreach ($Path in $ModulePath)
        {
            Write-PSFMessage -Level VeryVerbose -Message "Starting to package: $Path" -Tag 'progress', 'developer' -Target $Path
            
            if (-not (Test-Path $Path))
            {
                Stop-PSFFunction -Message "Path not found: $Path" -EnableException $EnableException -Category InvalidArgument -Tag 'progress', 'developer', 'fail' -Target $Path -Continue
            }
            
            try { Publish-Module -Path $Path -Repository 'PSModuleDevelopment_TempLocalRepository' -ErrorAction Stop -Force }
            catch
            {
                Stop-PSFFunction -Message "Failed to publish module: $Path" -EnableException $EnableException -ErrorRecord $_ -Tag 'progress', 'developer', 'fail' -Target $Path -Continue
            }
            
            Write-PSFMessage -Level Verbose -Message "Finished processing: $Path" -Tag 'progress', 'developer' -Target $Path
        }
        #endregion Process Paths
    }
    end
    {
        Unregister-PSRepository -Name 'PSModuleDevelopment_TempLocalRepository' -ErrorAction Ignore
        if (Test-PSFFunctionInterrupt) { return }
    }
}

function New-PssModuleProject
{
    <#
        .SYNOPSIS
            Builds a Sapien PowerShell Studio Module Project from a regular module.
         
        .DESCRIPTION
            Builds a Sapien PowerShell Studio Module Project, either a clean one, or imports from a regular module.
            Will ignore all hidden files and folders, will also ignore all files and folders in the root folder that start with a dot (".").
     
            Importing from an existing module requires the module to have a valid manifest.
         
        .PARAMETER Name
            The name of the folder to create the project in.
            Will also be used to name a blank module project. (When importing a module into a project, the name will be taken from the manifest file).
         
        .PARAMETER Path
            The path to create the new module-project folder in. Will default to the PowerShell Studio project folder.
            The function will fail if PSS is not found on the system and no path was specified.
         
        .PARAMETER SourcePath
            The path to the module to import from.
            Specify the path the the root folder the actual module files are in.
         
        .PARAMETER Force
            Force causes the function to overwrite all stuff in the destination folder ($Path\$Name), if it already exists.
         
        .EXAMPLE
            PS C:\> New-PssModuleProject -Name 'Foo'
     
            Creates a new module project named "Foo" in your default project folder.
     
        .EXAMPLE
            PS C:\> New-PssModuleProject -Name dbatools -SourcePath "C:\Github\dbatools"
     
            Imports the dbatools github repo's local copy into a new PSS module project in your default project folder.
     
        .EXAMPLE
            PS C:\> New-PssModuleProject -name 'Northwind' -SourcePath "C:\Github\Northwind" -Path "C:\Projects" -Force
     
            Will create a new module project, importing from "C:\Github\Northwind" and storing it in "C:\Projects". It will overwrite any existing folder named "Northwind" in the destination folder.
         
        .NOTES
            Author: Friedrich Weinmann
            Editors: -
            Created on: 01.03.2017
            Last Change: 01.03.2017
            Version: 1.0
             
            Release 1.0 (01.03.2017, Friedrich Weinmann)
            - Initial Release
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName = "Vanilla")]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [ValidateScript({ Test-Path -Path $_ -PathType Container })]
        [string]
        $Path,
        
        [Parameter(Mandatory = $true, ParameterSetName = "Import")]
        [string]
        $SourcePath,
        
        [switch]
        $Force
    )
    
    if (Test-PSFParameterBinding -ParameterName "Path" -Not)
    {
        try
        {
            $pssRoot = (Get-ChildItem "HKCU:\Software\SAPIEN Technologies, Inc." -ErrorAction Stop | Where-Object Name -like "*PowerShell Studio*" | Select-Object -last 1 -ExpandProperty Name).Replace("HKEY_CURRENT_USER", "HKCU:")
            $Path = (Get-ItemProperty -Path "$pssRoot\Settings" -Name "DefaultProjectDirectory" -ErrorAction Stop).DefaultProjectDirectory
        }
        catch
        {
            throw "No local PowerShell Studio found and no path specified. Going to take a break now. Bye!"
        }
    }
    
    switch ($PSCmdlet.ParameterSetName)
    {
        #region Vanilla
        "Vanilla"
        {
            if ((-not $Force) -and (Test-Path (Join-Path $Path $Name)))
            {
                throw "There already is an existing folder in '$Path\$Name', cannot create module!"
            }
            
            $root = New-Item -Path $Path -Name $Name -ItemType Directory -Force:$Force
            $Guid = [guid]::NewGuid().Guid
            
            # Create empty .psm1 file
            Set-Content -Path "$($root.FullName)\$Name.psm1" -Value ""
            
            #region Create Manifest
            Set-Content -Path "$($root.FullName)\$Name.psd1" -Value @"
@{
     
    # Script module or binary module file associated with this manifest
    ModuleToProcess = '$Name.psm1'
     
    # Version number of this module.
    ModuleVersion = '1.0.0.0'
     
    # ID used to uniquely identify this module
    GUID = '$Guid'
     
    # Author of this module
    Author = ''
     
    # Company or vendor of this module
    CompanyName = ''
     
    # Copyright statement for this module
    Copyright = '(c) $((Get-Date).Year). All rights reserved.'
     
    # Description of the functionality provided by this module
    Description = 'Module description'
     
    # Minimum version of the Windows PowerShell engine required by this module
    PowerShellVersion = '2.0'
     
    # Name of the Windows PowerShell host required by this module
    PowerShellHostName = ''
     
    # Minimum version of the Windows PowerShell host required by this module
    PowerShellHostVersion = ''
     
    # Minimum version of the .NET Framework required by this module
    DotNetFrameworkVersion = '2.0'
     
    # Minimum version of the common language runtime (CLR) required by this module
    CLRVersion = '2.0.50727'
     
    # Processor architecture (None, X86, Amd64, IA64) required by this module
    ProcessorArchitecture = 'None'
     
    # Modules that must be imported into the global environment prior to importing
    # this module
    RequiredModules = @()
     
    # Assemblies that must be loaded prior to importing this module
    RequiredAssemblies = @()
     
    # Script files (.ps1) that are run in the caller's environment prior to
    # importing this module
    ScriptsToProcess = @()
     
    # Type files (.ps1xml) to be loaded when importing this module
    TypesToProcess = @()
     
    # Format files (.ps1xml) to be loaded when importing this module
    FormatsToProcess = @()
     
    # Modules to import as nested modules of the module specified in
    # ModuleToProcess
    NestedModules = @()
     
    # Functions to export from this module
    FunctionsToExport = '*' #For performanace, list functions explicity
     
    # Cmdlets to export from this module
    CmdletsToExport = '*'
     
    # Variables to export from this module
    VariablesToExport = '*'
     
    # Aliases to export from this module
    AliasesToExport = '*' #For performanace, list alias explicity
     
    # List of all modules packaged with this module
    ModuleList = @()
     
    # List of all files packaged with this module
    FileList = @()
     
    # Private data to pass to the module specified in ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
    PrivateData = @{
         
        #Support for PowerShellGet galleries.
        PSData = @{
             
            # Tags applied to this module. These help with module discovery in online galleries.
            # Tags = @()
             
            # A URL to the license for this module.
            # LicenseUri = ''
             
            # A URL to the main website for this project.
            # ProjectUri = ''
             
            # A URL to an icon representing this module.
            # IconUri = ''
             
            # ReleaseNotes of this module
            # ReleaseNotes = ''
             
        } # End of PSData hashtable
         
    } # End of PrivateData hashtable
}
"@

            #endregion Create Manifest
            
            #region Create project file
            Set-Content -Path "$($root.FullName)\$Name.psproj" -Value @"
<Project>
  <Version>2.0</Version>
  <FileID>$Guid</FileID>
  <ProjectType>1</ProjectType>
  <Folders />
  <Files>
    <File Build="2">$Name.psd1</File>
    <File Build="0">$Name.psm1</File>
  </Files>
</Project>
"@

            #endregion Create project file
        }
        #endregion Vanilla

        #region Import
        "Import"
        {
            $SourcePath = Resolve-Path $SourcePath
            if (-not (Test-Path $SourcePath))
            {
                throw "Source path was not detectable!"
            }
            
            if ((-not $Force) -and (Test-Path (Join-Path $Path $Name)))
            {
                throw "There already is an existing folder in '$Path\$Name', cannot create module!"
            }
            
            $items = Get-ChildItem -Path $SourcePath | Where-Object Name -NotLike ".*"
            $root = New-Item -Path $Path -Name $Name -ItemType Directory -Force:$Force
            
            $items | Copy-Item -Destination $root.FullName -Recurse -Force
            
            $items_directories = Get-ChildItem -Path $root.FullName -Recurse -Directory
            $items_psd = Get-Item "$($root.FullName)\*.psd1" | Select-Object -First 1
            
            if (-not $items_psd)
            {
                throw "no module manifest found!"
            }
            
            $ModuleName = $items_psd.BaseName
            $items_files = Get-ChildItem -Path $root.FullName -Recurse -File | Where-Object { ($_.FullName -ne $items_psd.FullName) -and ($_.FullName -ne $items_psd.FullName.Replace(".psd1",".psm1")) }
            
            $Guid = (Get-Content $items_psd.FullName | Select-String "GUID = '(.+?)'").Matches[0].Groups[1].Value
            
            $string_Files = ($items_files | Select-Object -ExpandProperty FullName | ForEach-Object { " <File Build=`"2`" Shared=`"True`">$(($_ -replace ([regex]::Escape(($root.FullName + "\"))), ''))</File>" }) -join "`n"
            $string_Directories = ($items_Directories | Select-Object -ExpandProperty FullName | ForEach-Object { " <Folder>$(($_ -replace ([regex]::Escape(($root.FullName + "\"))), ''))</Folder>" }) -join "`n"
            Set-Content -Path "$($root.FullName)\$ModuleName.psproj" -Value @"
<Project>
  <Version>2.0</Version>
  <FileID>$Guid</FileID>
  <ProjectType>1</ProjectType>
  <Folders>
    $($string_Directories)
  </Folders>
  <Files>
    <File Build="2">$ModuleName.psd1</File>
    <File Build="0">$ModuleName.psm1</File>
    $($string_Files)
  </Files>
</Project>
"@

        }
        #endregion Import
    }
}

function Restart-PSMDShell {
    <#
        .SYNOPSIS
            A swift way to restart the PowerShell console.
         
        .DESCRIPTION
            A swift way to restart the PowerShell console.
            - Allows increasing elevation
            - Allows keeping the current process, thus in effect adding a new PowerShell process
         
        .PARAMETER NoExit
            The current console will not terminate.
         
        .PARAMETER Admin
            The new PowerShell process will be run as admin.
     
        .PARAMETER NoProfile
            The new PowerShell process will not load its profile.
     
        .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
         
        .PARAMETER WhatIf
            If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
         
        .EXAMPLE
            PS C:\> Restart-PSMDShell
     
            Restarts the current PowerShell process.
     
        .EXAMPLE
            PS C:\> Restart-PSMDShell -Admin -NoExit
     
            Creates a new PowerShell process, run with elevation, while keeping the current console around.
    #>

    [Alias('rss', 'Restart-Shell')]
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    Param (
        [Switch]
        $NoExit,
        
        [Switch]
        $Admin,
        
        [switch]
        $NoProfile
    )
    
    begin {
        $process = Get-Process -Id $pid
        $powershellPath = $process.Path
        $isWindowsTerminal = $process.Parent.ProcessName -eq 'WindowsTerminal'
    }
    process {
        if (-not $PSCmdlet.ShouldProcess("Current shell", "Restart")) { return }

        if ($isWindowsTerminal) {
            $psVersionName = 'powershell'
            if ($PSVersionTable.PSVersion.Major -gt 5) { $psVersionName = 'pwsh' }

            $param = @{
                FilePath = 'wt'
                ArgumentList = @('-w', 0, 'nt','--title', $psVersionName, $powershellPath)
            }
            if ($NoProfile) { $param.ArgumentList = @('-w', 0, 'nt', '--title', $psVersionName, $powershellPath, '-NoProfile') }
            if ($Admin) { $param.Verb = 'RunAs' }
            Start-Process @param
        }
        else {
            $param = @{
                FilePath = $powershellPath
            }
            if ($NoProfile) { $param.ArgumentList = '-NoProfile' }
            if ($Admin) { $param.Verb = 'RunAs' }
            Start-Process @param
        }
    }
    end {
        if (-not $NoExit) { exit }
    }
}

function Search-PSMDPropertyValue
{
<#
    .SYNOPSIS
        Recursively search an object for property values.
     
    .DESCRIPTION
        Recursively search an object for property values.
        This can be useful to determine just where an object stores a given piece of information in scenarios, where objects either have way too many properties or a deeply nested data structure.
     
    .PARAMETER Object
        The object to search.
     
    .PARAMETER Value
        The value to search for.
     
    .PARAMETER Match
        Search by comparing with regex, rather than equality comparison.
     
    .PARAMETER Depth
        Default: 3
        How deep should the query recurse.
        The deeper, the longer it can take on deeply nested objects.
     
    .EXAMPLE
        PS C:\> Get-Mailbox Max.Mustermann | Search-PSMDPropertyValue -Object 'max.mustermann@contoso.com' -Match
     
        Searches all properties on the mailbox of Max Mustermann for his email address.
#>

    [CmdletBinding()]
    param (
        [AllowNull()]
        $Value,
        
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        $Object,
        
        [switch]
        $Match,
        
        [int]
        $Depth = 3
    )
    
    begin
    {
        function Search-Value
        {
            [CmdletBinding()]
            param (
                $Object,
                
                $Value,
                
                [bool]
                $Match,
                
                [int]
                $Depth,
                
                [string[]]
                $Elements,
                
                $InputObject
            )
            
            $path = $Elements -join "."
            Write-PSFMessage -Level Verbose -Message "Processing $path"
            
            foreach ($property in $Object.PSObject.Properties)
            {
                if ($Match)
                {
                    if ($property.Value -match $Value)
                    {
                        New-Object PSModuleDevelopment.Utility.PropertySearchResult($property.Name, $Elements, $property.Value, $InputObject)
                    }
                }
                else
                {
                    if ($Value -eq $property.Value)
                    {
                        New-Object PSModuleDevelopment.Utility.PropertySearchResult($property.Name, $Elements, $property.Value, $InputObject)
                    }
                }
                
                if ($Elements.Count -lt $Depth)
                {
                    $newItems = New-Object System.Object[]($Elements.Count)
                    $Elements.CopyTo($newItems, 0)
                    $newItems += $property.Name
                    Search-Value -Object $property.Value -Value $Value -Match $Match -Depth $Depth -Elements $newItems -InputObject $InputObject
                }
            }
        }
    }
    
    process
    {
        Search-Value -Object $Object -Value $Value -Match $Match.ToBool() -Depth $Depth -Elements @() -InputObject $Object
    }
}

function Set-PSMDModulePath
{
<#
    .SYNOPSIS
        Sets the path of the module currently being developed.
     
    .DESCRIPTION
        Sets the path of the module currently being developed.
        This is used by several utility commands in order to not require any path input.
         
        This is a wrapper around the psframework configuration system, the same action can be taken by running this command:
        Set-PSFConfig -Module PSModuleDevelopment -Name "Module.Path" -Value $Path
     
    .PARAMETER Module
        The module, the path of which to register.
     
    .PARAMETER Path
        The path to set as currently developed module.
     
    .PARAMETER Register
        Register the specified path, to have it persist across sessions
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .EXAMPLE
        Set-PSMDModulePath -Path "C:\github\dbatools"
         
        Sets the current module path to "C:\github\dbatools"
     
    .EXAMPLE
        Set-PSMDModulePath -Path "C:\github\dbatools" -Register
         
        Sets the current module path to "C:\github\dbatools"
        Then stores the setting in registry, causing it to be persisted acros multiple sessions.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Module')]
        [System.Management.Automation.PSModuleInfo]
        $Module,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
        [string]
        $Path,
        
        [switch]
        $Register,
        
        [switch]
        $EnableException
    )
    
    process
    {
        if ($Path)
        {
            $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem
            if (Test-Path -Path $resolvedPath)
            {
                if ((Get-Item $resolvedPath).PSIsContainer)
                {
                    Set-PSFConfig -Module PSModuleDevelopment -Name "Module.Path" -Value $resolvedPath
                    if ($Register) { Register-PSFConfig -Module 'PSModuleDevelopment' -Name 'Module.Path' }
                    return
                }
            }
            
            Stop-PSFFunction -Target $Path -Message "Could not validate/resolve path: $Path" -EnableException $EnableException -Category InvalidArgument
            return
        }
        else
        {
            Set-PSFConfig -Module PSModuleDevelopment -Name "Module.Path" -Value $Module.ModuleBase
            if ($Register) { Register-PSFConfig -Module 'PSModuleDevelopment' -Name 'Module.Path' }
        }
    }
}

function Show-PSMDSyntax
{
<#
    .SYNOPSIS
        Validate or show parameter set details with colored output
 
    .DESCRIPTION
        Analyze a function and it's parameters
 
        The cmdlet / function is capable of validating a string input with function name and parameters
 
    .PARAMETER CommandText
        The string that you want to analyze
 
        If there is parameter value present, you have to use the opposite quote strategy to encapsulate the string correctly
 
        E.g. for double quotes
        -CommandText 'New-Item -Path "c:\temp\newfile.txt"'
         
        E.g. for single quotes
        -CommandText "New-Item -Path 'c:\temp\newfile.txt'"
 
    .PARAMETER Mode
        The operation mode of the cmdlet / function
 
        Valid options are:
        - Validate
        - ShowParameters
 
    .PARAMETER Legend
        Include a legend explaining the color mapping
 
    .EXAMPLE
        PS C:\> Show-PSMDSyntax -CommandText "New-Item -Path 'c:\temp\newfile.txt'"
 
        This will validate all the parameters that have been passed to the Import-D365Bacpac cmdlet.
        All supplied parameters that matches a parameter will be marked with an asterisk.
         
    .EXAMPLE
        PS C:\> Show-PSMDSyntax -CommandText "New-Item" -Mode "ShowParameters"
 
        This will display all the parameter sets and their individual parameters.
 
    .NOTES
        Author: Mötz Jensen (@Splaxi)
        Twitter: https://twitter.com/splaxi
        Original github project: https://github.com/d365collaborative/d365fo.tools
 
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 1)]
        [string]
        $CommandText,
        
        [Parameter(Position = 2)]
        [ValidateSet('Validate', 'ShowParameters')]
        [string]
        $Mode = 'Validate',
        
        [switch]
        $Legend
    )
    
    $commonParameters = 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'Confirm', 'WhatIf'
    
    $colorParmsNotFound = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.ParmsNotFound"
    $colorCommandName = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.CommandName"
    $colorMandatoryParam = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.MandatoryParam"
    $colorNonMandatoryParam = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.NonMandatoryParam"
    $colorFoundAsterisk = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.FoundAsterisk"
    $colorNotFoundAsterisk = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.NotFoundAsterisk"
    $colParmValue = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.ParmValue"
    
    #Match to find the command name: Non-Whitespace until the first whitespace
    $commandMatch = ($CommandText | Select-String '\S+\s*').Matches
    
    if (-not $commandMatch)
    {
        Write-PSFMessage -Level Host -Message "The function was unable to extract a valid command name from the supplied command text. Please try again."
        Stop-PSFFunction -Message "Stopping because of missing command name."
        return
    }
    
    $commandName = $commandMatch.Value.Trim()
    
    $res = Get-Command $commandName -ErrorAction Ignore
    
    if (-not $res)
    {
        Write-PSFMessage -Level Host -Message "The function was unable to get the help of the command. Make sure that the command name is valid and try again."
        Stop-PSFFunction -Message "Stopping because command name didn't return any help."
        return
    }
    
    $sbHelp = New-Object System.Text.StringBuilder
    $sbParmsNotFound = New-Object System.Text.StringBuilder
    
    if (-not ($CommandText | Select-String '\s{1}[-]\S+' -AllMatches).Matches)
    {
        $Mode = 'ShowParameters'
    }
    
    switch ($Mode)
    {
        "Validate" {
            # Match to find the parameters: Whitespace Dash Non-Whitespace
            $inputParameterMatch = ($CommandText | Select-String '\s{1}[-]\S+' -AllMatches).Matches
            
            if ($inputParameterMatch)
            {
                $inputParameterNames = $inputParameterMatch.Value.Trim("-", " ")
                Write-PSFMessage -Level Verbose -Message "All input parameters - $($inputParameterNames -join ",")" -Target ($inputParameterNames -join ",")
            }
            else
            {
                Write-PSFMessage -Level Host -Message "The function was unable to extract any parameters from the supplied command text. Please try again."
                Stop-PSFFunction -Message "Stopping because of missing input parameters."
                return
            }
            
            $availableParameterNames = (Get-Command $commandName).Parameters.keys | Where-Object { $commonParameters -NotContains $_ }
            Write-PSFMessage -Level Verbose -Message "Available parameters - $($availableParameterNames -join ",")" -Target ($availableParameterNames -join ",")
            
            $inputParameterNotFound = $inputParameterNames | Where-Object { $availableParameterNames -NotContains $_ }
            
            if ($inputParameterNotFound.Length -gt 0)
            {
                $null = $sbParmsNotFound.AppendLine("Parameters that <c='em'>don't exists</c>")
                $inputParameterNotFound | ForEach-Object {
                    $null = $sbParmsNotFound.AppendLine("<c='$colorParmsNotFound'>$($_)</c>")
                }
            }
            
            foreach ($parmSet in (Get-Command $commandName).ParameterSets)
            {
                $sb = New-Object System.Text.StringBuilder
                $null = $sb.AppendLine("ParameterSet Name: <c='em'>$($parmSet.Name)</c> - Validated List")
                $null = $sb.Append("<c='$colorCommandName'>$commandName </c>")
                
                $parmSetParameters = $parmSet.Parameters | Where-Object name -NotIn $commonParameters
                
                foreach ($parameter in $parmSetParameters)
                {
                    $parmFoundInCommandText = $parameter.Name -In $inputParameterNames
                    
                    $color = "$colorNonMandatoryParam"
                    
                    if ($parameter.IsMandatory -eq $true) { $color = "$colorMandatoryParam" }
                    
                    $null = $sb.Append("<c='$color'>-$($parameter.Name)</c>")
                    
                    if ($parmFoundInCommandText)
                    {
                        $null = $sb.Append("<c='$colorFoundAsterisk'>* </c>")
                    }
                    elseif ($parameter.IsMandatory -eq $true)
                    {
                        $null = $sb.Append("<c='$colorNotFoundAsterisk'>* </c>")
                    }
                    else
                    {
                        $null = $sb.Append(" ")
                    }
                    
                    if (-not ($parameter.ParameterType -eq [System.Management.Automation.SwitchParameter]))
                    {
                        $null = $sb.Append("<c='$colParmValue'>PARAMVALUE </c>")
                    }
                }
                
                $null = $sb.AppendLine("")
                Write-PSFHostColor -String "$($sb.ToString())"
            }
            
            $null = $sbHelp.AppendLine("")
            $null = $sbHelp.AppendLine("<c='$colorParmsNotFound'>$colorParmsNotFound</c> = Parameter not found")
            $null = $sbHelp.AppendLine("<c='$colorCommandName'>$colorCommandName</c> = Command Name")
            $null = $sbHelp.AppendLine("<c='$colorMandatoryParam'>$colorMandatoryParam</c> = Mandatory Parameter")
            $null = $sbHelp.AppendLine("<c='$colorNonMandatoryParam'>$colorNonMandatoryParam</c> = Optional Parameter")
            $null = $sbHelp.AppendLine("<c='$colParmValue'>$colParmValue</c> = Parameter value")
            $null = $sbHelp.AppendLine("<c='$colorFoundAsterisk'>*</c> = Parameter was filled")
            $null = $sbHelp.AppendLine("<c='$colorNotFoundAsterisk'>*</c> = Mandatory missing")
        }
        
        "ShowParameters" {
            foreach ($parmSet in (Get-Command $commandName).ParameterSets)
            {
                # (Get-Command $commandName).ParameterSets | ForEach-Object {
                $sb = New-Object System.Text.StringBuilder
                $null = $sb.AppendLine("ParameterSet Name: <c='em'>$($parmSet.Name)</c> - Parameter List")
                $null = $sb.Append("<c='$colorCommandName'>$commandName </c>")
                
                $parmSetParameters = $parmSet.Parameters | Where-Object name -NotIn $commonParameters
                
                foreach ($parameter in $parmSetParameters)
                {
                    # $parmSetParameters | ForEach-Object {
                    $color = "$colorNonMandatoryParam"
                    
                    if ($parameter.IsMandatory -eq $true) { $color = "$colorMandatoryParam" }
                    
                    $null = $sb.Append("<c='$color'>-$($parameter.Name) </c>")
                    
                    if (-not ($parameter.ParameterType -eq [System.Management.Automation.SwitchParameter]))
                    {
                        $null = $sb.Append("<c='$colParmValue'>PARAMVALUE </c>")
                    }
                }
                
                $null = $sb.AppendLine("")
                Write-PSFHostColor -String "$($sb.ToString())"
            }
            
            $null = $sbHelp.AppendLine("")
            $null = $sbHelp.AppendLine("<c='$colorCommandName'>$colorCommandName</c> = Command Name")
            $null = $sbHelp.AppendLine("<c='$colorMandatoryParam'>$colorMandatoryParam</c> = Mandatory Parameter")
            $null = $sbHelp.AppendLine("<c='$colorNonMandatoryParam'>$colorNonMandatoryParam</c> = Optional Parameter")
            $null = $sbHelp.AppendLine("<c='$colParmValue'>$colParmValue</c> = Parameter value")
        }
        Default { }
    }
    
    if ($sbParmsNotFound.ToString().Trim().Length -gt 0)
    {
        Write-PSFHostColor -String "$($sbParmsNotFound.ToString())"
    }
    
    if ($Legend)
    {
        Write-PSFHostColor -String "$($sbHelp.ToString())"
    }
}

function Test-PSMDClmCompatibility {
    <#
    .SYNOPSIS
        Tests, whether the targeted file would have trouble executing under Constrained Language Mode.
     
    .DESCRIPTION
        Tests, whether the targeted file would have trouble executing under Constrained Language Mode (CLM).
 
        In CLM, various language features and commands are constrained in their ability to execute.
        This command uses the AST parser to scan for as many known issues as possible and gives a comprehensive report for concerns found.
 
        Detected Issues:
        - Custom Object creation using PSCustomObject
        - Calling methods on untrusted types
        - Converting to an untrusted type
        - Using Add-Type to load anything but trusted libraries
        - Using New-Object to instantiate an untrusted type
        - Assigning Values to properties*
 
        *This detection will likely have a large rate of false positives, due to inability to detect datatype of the object, the property of which is being set.
        Generally, assigning values to the properties of PSObjects is fine.
 
        Note:
        Many of the detections make allowances for "whitelisted types".
        In CLM, access to most types is constrained, except for a few, known to be trustworthy types.
        To get a full list of the constraints and what types are allowed, see the documentation:
 
        https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes?view=powershell-7.1#constrained-language-constrained-language
     
    .PARAMETER Path
        Path to the scriptfile to scan.
     
    .EXAMPLE
        PS C:\> Get-ChildItem C:\Scripts | Test-PSMDClmCompatibility
 
        Scans each file in C:\Scripts and returns any issues that might occur in CLM.
     
    .LINK
        https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes?view=powershell-7.1#constrained-language-constrained-language
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [string[]]
        $Path
    )

    begin {
        #region Safe Types
        $safeTypes = @(
            [System.Array]
            [System.Boolean]
            [System.Byte]
            [System.Char]
            [System.DateTime]
            [System.Decimal]
            [System.Double]
            [System.Single]
            [System.Guid]
            [System.Collections.Hashtable]
            [System.Int32]
            [System.Int16]
            [System.Int64]
            [System.Management.Automation.Language.NullString]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.PSListModifier]
            [System.Management.Automation.PSObject]
            [System.Management.Automation.PSPrimitiveDictionary]
            [System.Management.Automation.PSTypeNameAttribute]
            [System.Text.RegularExpressions.Regex]
            [System.SByte]
            [System.String]
            [System.Globalization.CultureInfo]
            [System.Net.IPAddress]
            [System.Net.Mail.MailAddress]
            [System.Numerics.BigInteger]
            [System.Security.SecureString]
            [System.TimeSpan]
            [System.UInt16]
            [System.UInt32]
            [System.UInt64]
            [System.Management.Automation.AliasAttribute]
            [System.Management.Automation.AllowEmptyCollectionAttribute]
            [System.Management.Automation.AllowEmptyStringAttribute]
            [System.Management.Automation.AllowNullAttribute]
            [System.Management.Automation.CmdletBindingAttribute]
            [System.DirectoryServices.DirectoryEntry]
            [System.DirectoryServices.DirectorySearcher]
            [System.Management.ManagementClass]
            [System.Management.ManagementObject]
            [System.Management.ManagementObjectSearcher]
            [System.Management.Automation.OutputTypeAttribute]
            [System.Management.Automation.ParameterAttribute]
            [System.Management.Automation.PSDefaultValueAttribute]
            [System.Management.Automation.PSReference]
            [System.Management.Automation.SupportsWildcardsAttribute]
            [System.Management.Automation.SwitchParameter]
        )
        #endregion Safe Types

        #region Utility Functions
        function Search-Ast {
            [CmdletBinding()]
            param (
                [System.Management.Automation.Language.Ast]
                $Ast,

                [ScriptBlock]
                $Filter,

                [string]
                $Type,

                [string]
                $Explanation
            )

            $results = $Ast.FindAll($Filter, $true)

            foreach ($result in $results) {
                [PSCustomObject]@{
                    Type        = $Type
                    Line        = $result.Extent.StartLineNumber
                    File        = $Ast.Extent.File
                    Data        = $result
                    Explanation = $Explanation
                }
            }
        }

        function Format-Result {
            [CmdletBinding()]
            param (
                [Parameter(ValueFromPipeline = $true)]
                $Result
            )

            begin {
                $defaultDisplaySet = 'Type', 'Line', 'File', 'Data'
                $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet(‘DefaultDisplayPropertySet’, [string[]]$defaultDisplaySet)
                $standardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)
            }
            process {
                $Result | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $standardMembers -PassThru
            }
        }

        function Find-PSCustomObject {
            [CmdletBinding()]
            param (
                [System.Management.Automation.Language.Ast]
                $Ast,

                $SafeTypes
            )

            $explanation = 'Custom object creation using PSCustomObject is not available in CLM. You can work around this issue by replacing it with "New-Object PSObject -Properties @{ ... }", which works in CLM.'
            Search-Ast -Ast $Ast -Type PSCustomObject -Explanation $explanation -Filter {
                if ($args[0] -isnot [System.Management.Automation.Language.ConvertExpressionAst]) { return }
                if ($args[0].Type.TypeName.Name -ne 'PSCustomObject') { return }

                $true
            } | Format-Result
        }

        function Find-MethodInvocation {
            [CmdletBinding()]
            param (
                [System.Management.Automation.Language.Ast]
                $Ast,

                $SafeTypes
            )

            $explanation = 'Cannot call methods on objects in CLM, other than ToString, unless the type is one of the few basic trusted types such as string or integer'
            Search-Ast -Ast $Ast -Type 'Method Invocation' -Explanation $explanation -Filter {
                if ($args[0] -isnot [System.Management.Automation.Language.InvokeMemberExpressionAst]) { return }
                if ($args[0].Expression.StaticType -in $SafeTypes) { return }
                if ($args[0].Member.Value -eq 'ToString') { return }
            
                $true
            } | Format-Result
        }
        
        function Find-TypeConversion {
            [CmdletBinding()]
            param (
                [System.Management.Automation.Language.Ast]
                $Ast,

                $SafeTypes
            )

            $explanation = 'Cannot convert to types not trusted in CLM. Trusted types are few, including very simple types such as string or integer.'
            Search-Ast -Ast $Ast -Type 'Type Conversion' -Explanation $explanation -Filter {
                if ($args[0] -isnot [System.Management.Automation.Language.ConvertExpressionAst]) { return }
                if ($args[0].StaticType -in $SafeTypes) { return }
                if ($args[0].Type.TypeName.Name -eq 'PSCustomObject' -and $args[0].Child.StaticType -eq [hashtable]) { return }

                $true
            } | Format-Result
        }

        function Find-AddType {
            [CmdletBinding()]
            param (
                [System.Management.Automation.Language.Ast]
                $Ast,

                $SafeTypes
            )

            $explanation = 'Add-Type can only load signed and trusted libraries. This includes core .NET assemblies loaded by name. If loading an assembly from file, this detection will trigger, as it does not verify the file referenced. If the targeted dll is signed and trusted, disregard this detection.'
            Search-Ast -Ast $Ast -Type 'Add-Type' -Explanation $explanation -Filter {
                if ($args[0] -isnot [System.Management.Automation.Language.CommandAst]) { return }
                if ($args[0].CommandElements[0].Value -ne 'Add-Type') { return }
                if ($args[0].CommandElements.ParameterName -contains 'AssemblyName') { return }

                $true
            } | Format-Result
        }

        function Find-NewObject {
            [CmdletBinding()]
            param (
                [System.Management.Automation.Language.Ast]
                $Ast,

                $SafeTypes
            )

            $explanation = 'New-Object cannot be used in CLM, except to create an object of one of a set of explicitly whitelisted types, such as strings, integers, DateTime, etc.'
            Search-Ast -Ast $Ast -Type 'New-Object' -Explanation $explanation -Filter {
                if ($args[0] -isnot [System.Management.Automation.Language.CommandAst]) { return }
                if ($args[0].CommandElements[0].Value -ne 'New-Object') { return }
                if ($args[0].CommandElements | Where-Object Value -In $SafeTypes.FullName) { return }
                if ($args[0].CommandElements | Where-Object Value -eq 'PSObject') { return }

                $true
            } | Format-Result
        }

        function Find-PropertyAssignment {
            [CmdletBinding()]
            param (
                [System.Management.Automation.Language.Ast]
                $Ast,

                $SafeTypes
            )

            $explanation = 'Under CLM, assigning values to properties doesn''t work, unless the type is explicitly whitelisted by the engine. Generic PSObject objects - such as returned by ConvertFrom-Json or Import-Csv - ARE whitelisted however, so this scan may have a few false positives, sorry.'
            Search-Ast -Ast $Ast -Type 'Property Assignment' -Explanation $explanation -Filter {
                if ($args[0] -isnot [System.Management.Automation.Language.AssignmentStatementAst]) { return }
                if ($args[0].Left -isnot [System.Management.Automation.Language.MemberExpressionAst]) { return }
                if ($args[0].CommandElements | Where-Object Value -In $SafeTypes.FullName) { return }

                $true
            } | Format-Result
        }

        function Find-ClassDefinition {
            [CmdletBinding()]
            param (
                [System.Management.Automation.Language.Ast]
                $Ast,

                $SafeTypes
            )

            $explanation = 'PowerShell classes are not supported in Constrained Language Mode.'
            Search-Ast -Ast $Ast -Type 'PowerShell Class' -Explanation $explanation -Filter {
                if ($args[0] -isnot [System.Management.Automation.Language.TypeDefinitionAst]) { return }
                if ($args[0].TypeAttributes -eq 'Enum') { return }
                
                $true
            } | Format-Result
        }
        #endregion Utility Functions
    }
    process {
        foreach ($file in ($Path | Resolve-Path).Path) {
            try { $ast = [System.Management.Automation.Language.Parser]::ParseFile($file, [ref]$null, [ref]$null) }
            catch {
                Write-PSFMessage -Level Warning -Message "Error parsing: $file" -ErrorRecord $_ -PSCmdlet $PSCmdlet -EnableException $true
                continue
            }

            $param = @{
                Ast       = $ast
                SafeTypes = $safeTypes
            }

            Find-PSCustomObject @param
            Find-MethodInvocation @param
            Find-TypeConversion @param
            Find-AddType @param
            Find-NewObject @param
            Find-PropertyAssignment @param
            Find-ClassDefinition @param
        }
    }
}

Set-PSFScriptblock -Name PSModuleDevelopment.Validate.Path -Scriptblock {
    Test-Path $_
}
Set-PSFScriptblock -Name PSModuleDevelopment.Validate.File -Scriptblock {
    Test-Path $_ -PathType Leaf
}

Register-PSFTeppScriptblock -Name 'PSModuleDevelopment.Build.Action' -ScriptBlock {
    (Get-PSMDBuildAction).Name
}

Register-PSFTeppScriptblock -Name PSMD_dotNetTemplates -ScriptBlock {
    if (-not (Test-Path "$env:USERPROFILE\.templateengine\dotnetcli")) { return }
    
    $folder = (Get-ChildItem "$env:USERPROFILE\.templateengine\dotnetcli" | Sort-Object Name | Select-Object -Last 1).FullName
    Get-Content -Path "$folder\templatecache.json" | ConvertFrom-Json | Select-Object -ExpandProperty TemplateInfo | Select-Object -ExpandProperty ShortName -Unique
}

Register-PSFTeppScriptblock -Name PSMD_dotNetTemplatesInstall -ScriptBlock {
    Get-PSFTaskEngineCache -Module PSModuleDevelopment -Name "dotNetTemplates"
}

Register-PSFTeppScriptblock -Name PSMD_dotNetTemplatesUninstall -ScriptBlock {
    if (-not (Test-Path "$env:USERPROFILE\.templateengine\dotnetcli")) { return }
    
    $folder = (Get-ChildItem "$env:USERPROFILE\.templateengine\dotnetcli" | Sort-Object Name | Select-Object -Last 1).FullName
    $items = Get-Content -Path "$folder\installUnitDescriptors.json" | ConvertFrom-Json | Select-Object -ExpandProperty InstalledItems
    $items.PSObject.Properties.Value
}

Register-PSFTeppScriptblock -Name 'PSModuleDevelopment.Repository' -ScriptBlock {
    (Get-PSRepository).Name
}
Register-PSFTeppArgumentCompleter -Command Publish-PSMDStagedModule -Parameter Repository -Name 'PSModuleDevelopment.Repository'
Register-PSFTeppArgumentCompleter -Command Set-PSMDStagingRepository -Parameter Repository -Name 'PSModuleDevelopment.Repository'

Register-PSFTeppScriptblock -Name PSMD_templatestore -ScriptBlock {
    Get-PSFConfig -FullName "PSModuleDevelopment.Template.Store.*" | ForEach-Object {
        $_.Name -replace "^.+\."
    }
}

Register-PSFTeppScriptblock -Name PSMD_templatename -ScriptBlock {
    if ($fakeBoundParameter.Store)
    {
        $storeName = $fakeBoundParameter.Store
    }
    else
    {
        $storeName = "*"
    }
    
    $storePaths = Get-PSFConfig -FullName "PSModuleDevelopment.Template.Store.$storeName" | Select-Object -ExpandProperty Value
    $names = @()
    foreach ($path in $storePaths)
    {
        Get-ChildItem $path | Where-Object { $_.Name -match '-Info.xml$' } | ForEach-Object {
            $names += $_.Name -replace '-\d+(\.\d+){0,3}-Info.xml$'
        }
    }
    
    $names | Select-Object -Unique
}

#region Templates
# New-PSMDDotNetProject
Register-PSFTeppArgumentCompleter -Name PSMD_dotNetTemplates -Command New-PSMDDotNetProject -Parameter TemplateName
Register-PSFTeppArgumentCompleter -Name PSMD_dotNetTemplatesUninstall -Command New-PSMDDotNetProject -Parameter Uninstall
Register-PSFTeppArgumentCompleter -Name PSMD_dotNetTemplatesInstall -Command New-PSMDDotNetProject -Parameter Install

# New-PSMDTemplate
Register-PSFTeppArgumentCompleter -Name PSMD_templatestore -Command New-PSMDTemplate -Parameter OutStore

# Get-PSMDTemplate
Register-PSFTeppArgumentCompleter -Name PSMD_templatestore -Command Get-PSMDTemplate -Parameter Store
Register-PSFTeppArgumentCompleter -Name PSMD_templatename -Command Get-PSMDTemplate -Parameter TemplateName

# Invoke-PSMDTemplate
Register-PSFTeppArgumentCompleter -Name PSMD_templatestore -Command Invoke-PSMDTemplate -Parameter Store
Register-PSFTeppArgumentCompleter -Name PSMD_templatename -Command Invoke-PSMDTemplate -Parameter TemplateName
Register-PSFTeppArgumentCompleter -Name psframework-encoding -Command Invoke-PSMDTemplate -Parameter Encoding

# Remove-PSMDTemplate
Register-PSFTeppArgumentCompleter -Name PSMD_templatestore -Command Remove-PSMDTemplate -Parameter Store
Register-PSFTeppArgumentCompleter -Name PSMD_templatename -Command Remove-PSMDTemplate -Parameter TemplateName

#endregion Templates

#region Refactor
Register-PSFTeppArgumentCompleter -Name psframework-encoding -Command Set-PSMDEncoding -Parameter Encoding
#endregion Refactor

$scriptBlock = {
    $webclient = New-Object System.Net.WebClient
    $string = $webclient.DownloadString("http://dotnetnew.azurewebsites.net/")
    $templates = $string -split "`n" | Select-String '<a href="/template/(.*?)/.*?">.*?</a>' | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -Unique | Sort-Object
    
    Set-PSFTaskEngineCache -Module PSModuleDevelopment -Name "dotNetTemplates" -Value $templates
}
Register-PSFTaskEngineTask -Name "psmd_dotNetTemplateCache" -ScriptBlock $scriptBlock -Priority Low -Once -Description "Builds up the cache of installable templates for dotnet"

$action = {
    param (
        $Parameters
    )
    
    $rootPath = $Parameters.RootPath
    $actualParameters = $Parameters.Parameters
    
    #region Process Parameters
    if (-not $actualParameters.Command) {
        throw "Mandatory parameter: Command not specified"
    }
    
    if ($actualParameters.Command -is [System.Management.Automation.ScriptBlock]) {
        $scriptblock = $actualParameters.Command
    }
    else {
        try { $scriptblock = [scriptblock]::Create($actualParameters.Command) }
        catch {
            throw "Error parsing command '$($actualParameters.Command)' : $_"
        }
    }
    
    $actualArguments = foreach ($argument in $actualParameters.ArgumentList) {
        if ($argument -isnot [string]) {
            $argument
            continue
        }
        if ($argument -notlike '%!*!%') {
            $argument
            continue
        }
        $artifactName = $argument -replace '^%!(.+)!%$', '$1'
        $artifactObject = Get-PSMDBuildArtifact -Name $artifactName
        if (-not $artifactObject) { throw "Artifact for arguments not found: $artifactName" }
        $artifactObject.Value
    }
    
    $inSession = $null
    if ($actualParameters.InSession) {
        $inSession = foreach ($sessionInput in $actualParameters.InSession) {
            if ($sessionInput -is [System.Management.Automation.Runspaces.PSSession]) {
                $sessionInput
                continue
            }
            $artifactObject = Get-PSMDBuildArtifact -Name $sessionInput
            if ($artifactObject.Value -is [System.Management.Automation.Runspaces.PSSession]) {
                $artifactObject.Value
                continue
            }
            if (-not $artifactObject) { throw "Artifact for parameter InSession not found: $($sessionInput)" }
            throw "Artifact for parameter InSession ($($sessionInput)) is not a pssession!"
        }
    }
    #endregion Process Parameters
    
    #region Execution
    $invokeParam = @{
        ScriptBlock     = $scriptblock
        ArgumentList = $actualArguments
    }
    if ($inSession) { $invokeParam.Session = $inSession }
    try { Invoke-Command @invokeParam -ErrorAction Stop }
    catch { throw }
    #endregion Execution
}

$params = @{
    Name        = 'command'
    Action        = $action
    Description = 'Execute a scriptblock'
    Parameters  = @{
        Command         = '(mandatory) Scriptcode to run'
        ArgumentList = 'Any number of arguments to pass to the command. To insert artifacts, specify a string with the special notation "%!ArtifactName!%"'
        InSession    = 'Execute the scriptfile in the target PSSession. Either provide a full session object or an artifact name pointing at one.'
    }
}

Register-PSMDBuildAction @params

$action = {
    param (
        $Parameters
    )
    
    $rootPath = $Parameters.RootPath
    $actualParameters = $Parameters.Parameters
    
    #region Utility Functions
    function ConvertTo-PSSession {
        [CmdletBinding()]
        param (
            [Parameter(ValueFromPipeline = $true)]
            $InputObject
        )
        process {
            if ($InputObject -is [System.Management.Automation.Runspaces.PSSession]) {
                return $InputObject
            }
            $artifactValue = (Get-PSMDBuildArtifact -Name $InputObject).Value
            if ($artifactValue -is [System.Management.Automation.Runspaces.PSSession]) {
                return $artifactValue
            }
        }
    }
    #endregion Utility Functions
    
    if (-not ($actualParameters.Path -and $actualParameters.Destination)) {
        throw "Invalid parameters! Specify both Path and Destination."
    }
    
    $paths = $actualParameters.Path -replace '%ProjectRoot%', $rootPath
    $copyParam = @{
        Destination = $actualParameters.Destination -replace '%ProjectRoot%', $rootPath
    }
    if ($actualParameters.Recurse) { $copyParam.Recurse = $true }
    if ($actualParameters.Force) { $copyParam.Force = $true }
    if ($actualParameters.FromSession) {
        $fromSession = $actualParameters.FromSession | ConvertTo-PSSession
        if (-not $fromSession) {
            throw "FromSession $($actualParameters.FromSession) not found!"
        }
        $copyParam.FromSession = $fromSession
    }
    if ($actualParameters.ToSession) {
        $toSession = $actualParameters.ToSession | ConvertTo-PSSession
        if (-not $toSession) {
            throw "ToSession $($actualParameters.ToSession) not found!"
        }
        $copyParam.ToSession = $toSession
    }
    foreach ($path in $paths) {
        try { Copy-Item @copyParam -Path $path -ErrorAction Stop }
        catch { throw }
    }
}

$params = @{
    Name        = 'copy-item'
    Action        = $action
    Description = 'Copies files & folders from A to B'
    Parameters  = @{
        Path        = '(mandatory) Path(s) to copy. Use "%ProjectRoot%" to reference to the root path containing the build file.'
        Destination = '(mandatory) Path to copy to. Use "%ProjectRoot%" to reference to the root path containing the build file.'
        FromSession = 'Artifact Name of the PSSession to copy from.'
        ToSession   = 'Artifact Name of the PSSession to copy to.'
        Recurse        = 'Whether to copy child items'
        Force        = 'Whether to use force (Remove destination items)'
    }
}

Register-PSMDBuildAction @params

$action = {
    param (
        $Parameters
    )

    trap {
        if ($workingDirectory) {
            Remove-Item -Path $workingDirectory -Recurse -Force -ErrorAction SilentlyContinue
        }
        throw $_
    }
    
    $rootPath = $Parameters.RootPath
    $actualParameters = $Parameters.Parameters
    
    #region Validate Input
    if (-not $actualParameters.Session) {
        throw "No Sessions specified!"
    }
    
    if ($actualParameters.Session | Where-Object State -NE Opened) {
        throw "Sessions not open!"
    }
    if ($actualParameters.Repository -and (-not (Get-PSRepository -Name $actualParameters.Repository -ErrorAction Ignore))) {
        throw "Repository $($actualParameters.Repository) not found!"
    }
    
    foreach ($module in $actualParameters.Module) {
        if ($module -notmatch '\\|/') { continue }
        
        try { $null = Resolve-PSFPath -Path $module -Provider FileSystem }
        catch { throw "Unable to resolve path: $module"}
    }
    #endregion Validate Input
    
    #region Prepare modules to transfer
    $workingDirectory = Join-Path -Path (Get-PSFPath -Name temp) -ChildPath "psmd_action_$(Get-Random)"
    $null = New-Item -Path $workingDirectory -ItemType Directory -Force -ErrorAction Stop
    
    $saveModuleParam = @{
        Path = $workingDirectory
        Repository = $actualParameters.Repository
    }
    
    foreach ($module in $actualParameters.Module) {
        if ($module -notmatch '\\|/') {
            if ($actualParameters.Repository) {
                Save-Module $module @saveModuleParam
                continue
            }
            $moduleObject = Get-Module -Name $module -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1
            if (-not $moduleObject) {
                throw "Cannot find module $module!"
            }
            Copy-Item -Path $moduleObject.ModuleBase -Destination "$workingDirectory\$($moduleObject.Name)" -Recurse -Force
            continue
        }
        
        foreach ($path in Resolve-PSFPath -Path $module -Provider FileSystem) {
            if (Test-Path -LiteralPath $path -PathType Leaf) { $path = Split-Path -LiteralPath $path }
            Copy-Item -LiteralPath $path -Destination $workingDirectory -Recurse -Force
        }
    }
    #endregion Prepare modules to transfer
    
    foreach ($moduleFolder in Get-ChildItem -Path $workingDirectory) {
        if (-not $actualParameters.NoDelete) {
            Invoke-Command -Session $actualParameters.Session -ScriptBlock {
                param ($Name)
                if (-not (Test-Path -Path "$env:ProgramFiles\WindowsPowerShell\Modules\$Name")) { return }
                Remove-Item -Path "$env:ProgramFiles\WindowsPowerShell\Modules\$Name" -Recurse -Force
            } -ArgumentList $moduleFolder.Name
        }
        foreach ($session in $actualParameters.Session) {
            Copy-Item -LiteralPath $moduleFolder.FullName -Destination "$env:ProgramFiles\WindowsPowerShell\Modules" -Recurse -Force -ToSession $session -ErrorAction Stop
        }
    }
}

$params = @{
    Name        = 'deployModule'
    Action        = $action
    Description = 'Deploys a module to the target computer(s)'
    Parameters  = @{
        Session    = '(mandatory) The PSRemoting sessions to deploy the module through.'
        Module       = '(mandatory) A list of names or paths of modules to deploy. Can be used in any combination, specifying by name will use the latest version found on the local computer unless also using he "Repository" parameter to specify an alternate source.'
        Repository = 'The repository from which to download the module(s) (and any dependencies). Modules will be sourced locally if empty.'
        NoDelete   = '[bool] Whether to keep other versions of the target module on the remote machine. By default, all other versions will be deleted.'
    }
}

Register-PSMDBuildAction @params

$action = {
    param (
        $Parameters
    )
    
    $rootPath = $Parameters.RootPath
    $actualParameters = $Parameters.Parameters
    
    if (-not $actualParameters.ArtifactName) { throw "No ArtifactName specified! Unable to publish remoting session for build." }
    if (-not ($actualParameters.VMName -or $actualParameters.ComputerName)) { throw "Neither ComputerName nor VMName specified, unable to connect to nothing!" }
    if ($actualParameters.VMName -and $actualParameters.ComputerName) { throw "Both ComputerName and VMName specified, unable to connect to both at once!" }
    
    $credential = $null
    if ($actualParameters.CredentialPath) {
        $path = $actualParameters.CredentialPath -replace '%ProjectRoot%', $rootPath
        try { $credential = Import-PSFClixml -Path $path -ErrorAction Stop }
        catch { throw "Error accessing credentials from $path : $_" }
    }
    if ($actualParameters.Credential) {
        if ($actualParameters.Credential -isnot [pscredential]) {
            throw "Not a credential object: $($actualParameters.Credential)"
        }
        $credential = $actualParameters.Credential
    }
    
    $paramNewPSSession = @{ }
    if ($actualParameters.VMName) { $paramNewPSSession.VMName = $actualParameters.VMName }
    if ($actualParameters.ComputerName) { $paramNewPSSession.ComputerName = $actualParameters.ComputerName }
    if ($actualParameters.Port) { $paramNewPSSession.Port = $actualParameters.Port }
    if ($credential) { $paramNewPSSession.Credential = $credential }
    
    try { $session = New-PSSession @paramNewPSSession -ErrorAction Stop }
    catch { throw "Error establishing PS Remoting session: $_" }
    
    Publish-PSMDBuildArtifact -Name $actualParameters.ArtifactName -Value $session -Tag pssession
}

$params = @{
    Name        = 'new-pssession'
    Action      = $action
    Description = 'Establish a PSSession to a target computer and provide it as an artifact'
    Parameters  = @{
        ComputerName   = 'The Computer to connect to'
        Port           = 'Port you want to connect to'
        VMName         = 'The virtual machine to which to connect to via the HyperV VM Bus'
        CredentialPath = 'The path to the credentials to use for the connection. Use %ProjectRoot% to insert the folder path to where the buildfile is located'
        Credential     = 'PSCredential object to use for authenticatioon'
        ArtifactName   = '(mandatory) The name under which to publish the session as an artifact'
    }
}

Register-PSMDBuildAction @params

$action = {
    param (
        $Parameters
    )
    
    $rootPath = $Parameters.RootPath
    $actualParameters = $Parameters.Parameters
    
    if (-not $actualParameters.Path) {
        throw "Invalid parameters! Specify a Path to delete."
    }
    
    $paths = $actualParameters.Path -replace '%ProjectRoot%', $rootPath
    $deleteParam = @{ }
    if ($actualParameters.Recurse) { $deleteParam.Recurse = $true }
    if ($actualParameters.Force) { $deleteParam.Force = $true }
    
    $inSession = $null
    if ($actualParameters.InSession) {
        if ($actualParameters.InSession -is [System.Management.Automation.Runspaces.PSSession]) {
            $inSession = $actualParameters.InSession
        }
        $artifactObject = Get-PSMDBuildArtifact -Name $actualParameters.InSession
        if (-not $artifactObject) { throw "Artifact for parameter InSession not found: $($actualParameters.InSession)" }
        if ($artifactObject.Value -isnot [System.Management.Automation.Runspaces.PSSession]) { throw "Artifact for parameter InSession ($($actualParameters.InSession)) is not a pssession!" }
        $inSession = $artifactObject.Value
    }
    
    if ($inSession) {
        $failed = Invoke-Command -Session $inSession -ScriptBlock {
            param ($DeleteParam, $Paths)
            
            foreach ($path in $Paths) {
                if (-not (Get-Item -Path $path -Force -ErrorAction Ignore)) { continue }
                try { Remove-Item @DeleteParam -Path $path -ErrorAction Stop }
                catch { return $_ }
            }
        } -ArgumentList $deleteParam, $paths
        if ($failed) {
            throw $failed
        }
    }
    
    foreach ($path in $paths) {
        if (-not (Get-Item -Path $path -Force -ErrorAction Ignore)) { continue }
        try { Remove-Item @DeleteParam -Path $path -ErrorAction Stop }
        catch { throw }
    }
}

$params = @{
    Name        = 'remove-item'
    Action        = $action
    Description = 'Removes files or folders'
    Parameters  = @{
        Path        = '(mandatory) Path(s) to the item(s) to delete. Use "%ProjectRoot%" to reference to the root path containing the build file.'
        InSession   = 'Artifact Name of the PSSession within which to execute the deletion'
        Recurse        = 'Whether to delete child items'
        Force        = 'Whether to use force'
    }
}

Register-PSMDBuildAction @params

$action = {
    param (
        $Parameters
    )
    
    $rootPath = $Parameters.RootPath
    $actualParameters = $Parameters.Parameters
    
    if ($actualParameters.All) {
        foreach ($artifact in Get-PSMDBuildArtifact -Tag pssession) {
            try {
                $artifact.Value | Remove-PSSession -ErrorAction Stop
                Remove-PSMDBuildArtifact -Name $artifact.Name
            }
            catch {
                throw "Failed to remove PSSession artifact $($artifact.Name) to $($artifact.Value) | $_"
            }
        }
    }
    elseif ($actualParameters.ArtifactName) {
        $artifact = Get-PSMDBuildArtifact -Name $actualParameters.ArtifactName
        if ($artifact) {
            try {
                $artifact.Value | Remove-PSSession -ErrorAction Stop
                Remove-PSMDBuildArtifact -Name $artifact.Name
            }
            catch {
                throw "Failed to remove PSSession artifact $($artifact.Name) to $($artifact.Value) | $_"
            }
        }
    }
    else {
        throw "Invalid parameters! Specify either 'All' or 'ArtifactName' in step definition."
    }
}

$params = @{
    Name        = 'remove-pssession'
    Action        = $action
    Description = 'Removes a PSSession that was previously established with the new-pssession action'
    Parameters  = @{
        ArtifactName = 'The name under which to publish the session as an artifact'
        All             = 'Whether all PSSession artifacts should be removed'
    }
}

Register-PSMDBuildAction @params

$action = {
    param (
        $Parameters
    )
    
    $rootPath = $Parameters.RootPath
    $actualParameters = $Parameters.Parameters
    
    #region Process Parameters
    if (-not $actualParameters.Path) {
        throw "Mandatory parameter: Path not specified"
    }
    
    $scriptPath = $actualParameters.Path -replace '%ProjectRoot%', $rootPath
    
    if (-not (Test-Path $scriptPath)) {
        throw "Cannot find resolved script path: $scriptPath"
    }
    
    $actualArguments = foreach ($argument in $actualParameters.ArgumentList) {
        if ($argument -isnot [string]) {
            $argument
            continue
        }
        if ($argument -notlike '%!*!%') {
            $argument
            continue
        }
        $artifactName = $argument -replace '^%!(.+)!%$', '$1'
        $artifactObject = Get-PSMDBuildArtifact -Name $artifactName
        if (-not $artifactObject) { throw "Artifact for arguments not found: $artifactName" }
        $artifactObject.Value
    }
    
    $inSession = $null
    if ($actualParameters.InSession) {
        if ($actualParameters.InSession -is [System.Management.Automation.Runspaces.PSSession]) {
            $inSession = $actualParameters.InSession
        }
        $artifactObject = Get-PSMDBuildArtifact -Name $actualParameters.InSession
        if (-not $artifactObject) { throw "Artifact for parameter InSession not found: $($actualParameters.InSession)" }
        if ($artifactObject.Value -isnot [System.Management.Automation.Runspaces.PSSession]) { throw "Artifact for parameter InSession ($($actualParameters.InSession)) is not a pssession!" }
        $inSession = $artifactObject.Value
    }
    #endregion Process Parameters
    
    #region Execution
    $invokeParam = @{
        FilePath     = $scriptPath
        ArgumentList = $actualArguments
    }
    if ($inSession) { $invokeParam.Session = $inSession }
    try { Invoke-Command @invokeParam -ErrorAction Stop }
    catch { throw }
    #endregion Execution
}

$params = @{
    Name        = 'script'
    Action        = $action
    Description = 'Execute a scriptfile'
    Parameters  = @{
        Path         = '(mandatory) Path to the scriptfile to run. Use %ProjectRoot% to reference the same folder the build action file is stored in.'
        ArgumentList = 'Any number of arguments to pass to the scripts. To insert artifacts, specify a string with the special notation "%!ArtifactName!%"'
        InSession    = 'Execute the scriptfile in the target PSSession. Either provide a full session object or an artifact name pointing at one.'
    }
}

Register-PSMDBuildAction @params

New-PSFLicense -Product 'PSModuleDevelopment' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2017-04-27") -Text @"
Copyright (c) 2017 Friedrich Weinmann
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@


$__modules = Get-PSMDModuleDebug | Sort-Object Priority

foreach ($__module in $__modules)
{
    if ($__module.AutoImport)
    {
        try { . Import-PSMDModuleDebug -Name $__module.Name -ErrorAction Stop }
        catch { Write-PSFMessage -Level Warning -Message "Failed to import Module: $($__module.Name)" -Tag import -ErrorRecord $_ -Target $__module.Name }
    }
}
#endregion Load compiled code