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" }
}

Set-PSFConfig -Module PSModuleDevelopment -Name 'Debug.ConfigPath' -Value "$($path_FileUserShared)\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') -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 "$path_FileUserShared\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 "$env: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 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 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
#>

    [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 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
     
        .NOTES
            Version 1.0.0.0
            Author: Friedrich Weinmann
            Created on: August 15th, 2016
    #>

    [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 }
    }
}
New-Alias -Name hex -Value Get-PSMDHelp -Scope Global -Option AllScope

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.
    #>

    [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()
        }
    }
}
New-Alias -Name ipmod -Value Import-ModuleDebug -Option AllScope -Scope Global

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)
    #>

    [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
    }
}
Set-Alias -Name smd -Value Set-PSMDModuleDebug -Option AllScope -Scope Global

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$') { 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$') { return $false }
                    if (-not ($args[0].CommandElements.ParameterName -match '^String$|^ActionString$')) { return $false }
                    $true
                }, $true)
            
            foreach ($commandAst in $commandAsts)
            {
                $stringParam = $commandAst.CommandElements | Where-Object ParameterName -match '^String$|^ActionString$'
                $stringParamValue = $commandAst.CommandElements[($commandAst.CommandElements.IndexOf($stringParam) + 1)].Value
                
                $stringValueParam = $commandAst.CommandElements | Where-Object ParameterName -match '^StringValues$|^ActionStringValues$'
                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$') { 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
#>

    [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
            }
        }
    }
}
Set-Alias -Name parse -Value Read-PSMDScript

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.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", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [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 -eq 'C:\Windows\System32\WindowsPowerShell\v1.0') { continue }
            if ($command.Module.ModuleBase -eq '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
    {
        Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param'
        
        $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-Clixml $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-Clixml $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 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 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', '')]
    [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)]
        [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]
        $Force,
        
        [switch]
        $Silent,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        #region Validate output path
        try
        {
            $resolvedPath = Resolve-Path $OutPath -ErrorAction Stop
            if (($resolvedPath | Measure-Object).Count -ne 1)
            {
                throw "Cannot resolve $OutPath to a single folder"
            }
            if ($resolvedPath.Provider -notlike "*FileSystem")
            {
                throw "Path $OutPath was not recognized as a filesystem path"
            }
        }
        catch
        {
            Stop-PSFFunction -Message "Could not resolve output path to a valid folder: $OutPath" -EnableException $EnableException -ErrorRecord $_ -Tag 'fail', 'path', 'validate'
            return
        }
        #endregion Validate output path
        
        $templates = @()
        switch ($PSCmdlet.ParameterSetName)
        {
            'NameStore' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Store $Store }
            'NamePath' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Path $Path }
        }
        
        #region Parameter Processing
        if (-not $Parameters) { $Parameters = @{ } }
        if ($Name) { $Parameters["Name"] = $Name }
        
        foreach ($config in (Get-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.ParameterDefault.*'))
        {
            $cfgName = $config.Name -replace '^.+\.([^\.]+)$', '$1'
            if (-not $Parameters.ContainsKey($cfgName))
            {
                $Parameters[$cfgName] = $config.Value
            }
        }
        #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,
                
                [bool]
                $Silent
            )
            Write-PSFMessage -Level Verbose -Message "Processing template $($item)" -Tag 'template', 'invoke' -FunctionName Invoke-PSMDTemplate
            
            $templateData = Import-Clixml -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
            
            switch ($templateData.Type.ToString())
            {
                #region File
                "File"
                {
                    foreach ($child in $templateData.Children)
                    {
                        Write-TemplateItem -Item $child -Path $OutPath -Encoding $Encoding -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)
                    {
                        Write-TemplateItem -Item $child -Path $newFolder.FullName -Encoding $Encoding -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)þþþ",""
                        }
                        
                        $configFile = Join-Path $newFolder.FullName "PSMDTemplate.ps1"
                        Set-Content -Path $configFile -Value $optionsTemplate -Encoding ([PSFEncoding]'utf-8').Encoding
                    }
                    #endregion Write Config File (Raw)
                }
                #endregion Project
            }
        }
        
        function Write-TemplateItem
        {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [CmdletBinding()]
            param (
                [PSModuleDevelopment.Template.TemplateItemBase]
                $Item,
                
                [string]
                $Path,
                
                [PSFEncoding]
                $Encoding,
                
                [hashtable]
                $ParameterFlat,
                
                [hashtable]
                $ParameterScript,
                
                [bool]
                $Raw
            )
            
            Write-PSFMessage -Level Verbose -Message "Creating file: $($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 = $fileName -replace "$($identifier)$([regex]::Escape($param))$($identifier)",$ParameterFlat[$param]
                    }
                    foreach ($param in $Item.FileSystemParameterScript)
                    {
                        $fileName = $fileName -replace "$($identifier)!$([regex]::Escape($param))!$($identifier)", $ParameterScript[$param]
                    }
                }
                $destPath = Join-Path $Path $fileName
                
                if ($Item.PlainText)
                {
                    $text = $Item.Value
                    if (-not $Raw)
                    {
                        foreach ($param in $Item.ContentParameterFlat)
                        {
                            $text = $text -replace "$($identifier)$([regex]::Escape($param))$($identifier)", $ParameterFlat[$param]
                        }
                        foreach ($param in $Item.ContentParameterScript)
                        {
                            $text = $text -replace "$($identifier)!$([regex]::Escape($param))!$($identifier)", $ParameterScript[$param]
                        }
                    }
                    [System.IO.File]::WriteAllText($destPath, $text, $Encoding)
                }
                else
                {
                    $bytes = [System.Convert]::FromBase64String($Item.Value)
                    [System.IO.File]::WriteAllBytes($destPath, $bytes)
                }
            }
            #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 = New-Item -Path $Path -Name $folderName -ItemType Directory
                
                foreach ($child in $Item.Children)
                {
                    Write-TemplateItem -Item $child -Path $folder.FullName -Encoding $Encoding -ParameterFlat $ParameterFlat -ParameterScript $ParameterScript -Raw $Raw
                }
            }
            #endregion Folder
        }
        #endregion Helper function
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        foreach ($item in $Template)
        {
            if ($PSCmdlet.ShouldProcess($item, "Invoking template"))
            {
                try { Invoke-Template -Template $item -OutPath $resolvedPath.ProviderPath -NoFolder $NoFolder -Encoding $Encoding -Parameters $Parameters.Clone() -Raw $Raw -Silent $Silent }
                catch { Stop-PSFFunction -Message "Failed to invoke template $($item)" -EnableException $EnableException -ErrorRecord $_ -Target $item -Tag 'fail', 'template', 'invoke' -Continue }
            }
        }
        foreach ($item in $templates)
        {
            if ($PSCmdlet.ShouldProcess($item, "Invoking template"))
            {
                try { Invoke-Template -Template $item -OutPath $resolvedPath.ProviderPath -NoFolder $NoFolder -Encoding $Encoding -Parameters $Parameters.Clone() -Raw $Raw -Silent $Silent }
                catch { Stop-PSFFunction -Message "Failed to invoke template $($item)" -EnableException $EnableException -ErrorRecord $_ -Target $item -Tag 'fail', 'template', 'invoke' -Continue }
            }
        }
    }
}

if (-not (Test-Path Alias:\imt)) { Set-Alias -Name imt -Value Invoke-PSMDTemplate }

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', '')]
    [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
        }
    }
}

New-Alias -Name dotnetnew -Value New-PSMDDotNetProject -Option AllScope -Scope Global -ErrorAction Ignore

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'.
#>

    [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
    }
}
New-Alias -Name find -Value Find-PSMDFileContent -Scope Global -Option AllScope

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.
         
        .NOTES
            Version 1.0.0.0
            Author: Friedrich Weinmann
            Created on: August 6th, 2016
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    Param (
        [Switch]
        $NoExit,
        
        [Switch]
        $Admin,
        
        [switch]
        $NoProfile
    )
    
    begin
    {
        $powershellPath = (Get-Process -id $pid).Path
    }
    process
    {
        if ($PSCmdlet.ShouldProcess("Current shell", "Restart"))
        {
            if ($NoProfile)
            {
                if ($Admin) { Start-Process $powershellPath -Verb RunAs -ArgumentList '-NoProfile' }
                else { Start-Process $powershellPath -ArgumentList '-NoProfile' }
            }
            else
            {
                if ($Admin) { Start-Process $powershellPath -Verb RunAs }
                else { Start-Process $powershellPath }
            }
            if (-not $NoExit) { exit }
        }
    }
}
New-Alias -Name Restart-Shell -Value Restart-PSMDShell -Option AllScope -Scope Global
New-Alias -Name rss -Value Restart-PSMDShell -Option AllScope -Scope Global

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())"
    }
}

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

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"

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