LeetABit.Build.Arguments.psm1

#requires -version 6
using namespace System.Collections
using namespace System.Collections.Generic
using namespace System.Management.Automation
using namespace System.Diagnostics.CodeAnalysis
using module LeetABit.Build.Common

Set-StrictMode -Version 2
Import-LocalizedData -BindingVariable LocalizedData -FileName LeetABit.Build.Arguments.Resources.psd1

$ConfigurationJson   = $Null
$NamedArguments      = @{}
$PositionalArguments = @()
[ArrayList]$UnknownArguments = @()

$AllParameterSets = '__AllParameterSets'

##################################################################################################################
# Public Commands
##################################################################################################################


function Find-CommandArgument {
    <#
    .SYNOPSIS
        Locates an argument for a specified named parameter.
    .DESCRIPTION
        Find-CommandArgument cmdlet tries to find argument for the specified parameter. The cmdlet is looking for a variable which name matches one of the following case-insensitive patterns: `LeetABitBuild_$ExtensionName_$ParameterName`, `$ExtensionName_$ParameterName`, `LeetABitBuild_$ParameterName`, `{ParameterName}`. Any dots in the name are ignored. There are four different argument sources, listed below in a precedence order:
 
        1. Dictionary of arguments specified as value for AdditionalArguments parameter.
        2. Arguments provided via Set-CommandArgumentSet and Add-CommandArgument cmdlets.
        3. Values stored in 'LeetABit.Build.json' file located in the repository root directory provided via Set-CommandArgumentSet cmdlet or on of its subdirectories.
        4. Environment variables.
    .EXAMPLE
        PS> Find-CommandArgument "TaskName" "LeetABit.Build" "help" -AdditionalArguments $arguments
 
        Tries to find a value for a parameter "TaskName" or "LeetABitBuild_TaskName". At the beginning specified arguments dictionary is being checked. If the value is not found the cmdlet checks all the arguments previously specified via `Add-CommandArgument` and `Set-CommandArgumentSet` cmdlets. If there was no value provided for any of the parameters a default value "help" is returned.
    .EXAMPLE
        PS> Find-CommandArgument "ProducePackages" -IsSwitch
 
        Tries to find a value for a parameter "ProducePackages" and gives a hint that the parameter is a switch which may be specified without providing a value for it via argument list.
    .NOTES
        This cmdlet is using module's internal state that could be modified by `Reset-CommandArgumentSet`, `Add-CommandArgument`, `Set-CommandArgumentSet` cmdlets.
        When an argument is found in the unknown arguments specified by `Set-CommandArgumentSet` cmdlet it is being moved from unknown arguments list to a named arguments collection.
    .LINK
        Reset-CommandArgumentSet
    .LINK
        Add-CommandArgument
    .LINK
        Set-CommandArgumentSet
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([Object])]

    param (
        # Name of the parameter.
        [Parameter(HelpMessage = 'Provide parameter name.',
                   Position=0,
                   Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [String]
        $ParameterName,

        # Name of the build extension in which the command is defined.
        [Parameter(Position=1,
                   Mandatory=$False,
                   ValueFromPipeline=$False,
                   ValueFromPipelineByPropertyName=$True)]
        [String]
        $ExtensionName,

        # Default value that shall be used when no argument with the specified name is found.
        [Parameter(Position=2,
                   Mandatory=$False,
                   ValueFromPipeline=$False,
                   ValueFromPipelineByPropertyName=$True)]
        [Object]
        $DefaultValue,

        # Indicates whether the argument shall be threated as a value for [Switch] parameter.
        [Parameter(Mandatory=$False,
                   ValueFromPipeline=$False,
                   ValueFromPipelineByPropertyName=$True)]
        [Switch]
        $IsSwitch,

        # A dictionary that holds an additional arguments to be used as a parameter's value source.
        [Parameter(Mandatory=$False,
                   ValueFromPipeline=$False,
                   ValueFromPipelineByPropertyName=$False)]
        [IDictionary]
        $AdditionalArguments
    )

    begin {
        LeetABit.Build.Common\Import-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process {
        $parameterNames = @()
        if ($ExtensionName) {
            $sanitizedExtensionName = "$($ExtensionName.Replace('.', [String]::Empty))"
            $parameterNames += "LeetABitBuild_$sanitizedExtensionName`_$ParameterName"

            if ($sanitizedExtensionName.StartsWith("LeetABitBuild")) {
                $trimmedExtensionName = $sanitizedExtensionName.Substring("LeetABitBuild".Length)
                if ($trimmedExtensionName) {
                    $parameterNames += "LeetABitBuild_$trimmedExtensionName`_$ParameterName"
                }
            }

            $parameterNames += "$sanitizedExtensionName`_$ParameterName"
        }

        $parameterNames += "LeetABitBuild_$ParameterName"
        $parameterNames += $ParameterName

        foreach ($parameterNameToFind in $parameterNames) {
            $result = Find-CommandArgumentInDictionary $parameterNameToFind -Dictionary $AdditionalArguments
            if ($result) {
                Convert-ArgumentValue $result -IsSwitch:$IsSwitch
                return
            }

            $result = Find-CommandArgumentInCommandLine $parameterNameToFind -IsSwitch:$IsSwitch
            if ($result) {
                Convert-ArgumentValue $result -IsSwitch:$IsSwitch
                return
            }

            $result = Find-CommandArgumentInConfiguration $parameterNameToFind
            if ($result) {
                Convert-ArgumentValue $result -IsSwitch:$IsSwitch
                return
            }

            $result = Find-CommandArgumentInEnvironment $parameterNameToFind
            if ($result) {
                Convert-ArgumentString $result -IsSwitch:$IsSwitch
                return
            }
        }

        if ($DefaultValue) {
            $DefaultValue
        }
    }
}


function Reset-CommandArgumentSet {
    <#
    .SYNOPSIS
        Removes all command arguments set in the module command.
    .DESCRIPTION
        Reset-CommandArgumentSet cmdlet clears all the module internal state that has been set via any of the previous calls to `Add-CommandArgument` and `Set-CommandArgumentSet` cmdlets.
    .EXAMPLE
        PS> Reset-CommandArgumentSet
 
        Removes all arguments stored in the `LeetABit.Build.Arguments` module.
    .LINK
        Add-CommandArgument
    .LINK
        Set-CommandArgumentSet
    #>

    [CmdletBinding(PositionalBinding = $False,
                   SupportsShouldProcess = $True,
                   ConfirmImpact = "Low")]

    param ()

    begin {
        LeetABit.Build.Common\Import-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process {
        if ($PSCmdlet.ShouldProcess($LocalizedData.Resource_CurrentCommandArgumentSet,
                                    $LocalizedData.Operation_Clear)) {
            $script:ConfigurationJson = $Null
            $script:NamedArguments = @{}
            $script:PositionalArguments = @()
            $script:UnknownArguments.Clear()
        }
    }
}


function Select-CommandArgumentSet {
    <#
    .SYNOPSIS
        Selects a collection of arguments that match specified command parameters.
    .DESCRIPTION
        Select-CommandArgumentSet cmdlet tries to find parameters for the specified command, script block or parameter collection. The cmdlet is looking for a variables which name matches one of the following case-insensitive patterns: `LeetABitBuild_$ExtensionName_$ParameterName`, `$ExtensionName_$ParameterName`, `LeetABitBuild_$ParameterName`, `{ParameterName}`. Any dots in the name are ignored. There are four different argument sources, listed below in a precedence order:
        1. Dictionary of arguments specified as value for AdditionalArguments parameter.
        2. Arguments provided via Set-CommandArgumentSet and Add-CommandArgument cmdlets.
        3. Values stored in 'LeetABit.Build.json' file located in the repository root directory provided via Set-CommandArgumentSet cmdlet or on of its subdirectories.
        4. Environment variables.
    .EXAMPLE
        PS> Select-CommandArgumentSet -Command (Get-Command LeetABit.Build.PowerShell\Deploy-Project)
 
        Tries to selects arguments for a Get-Command LeetABit.Build.PowerShell\Deploy-Project command.
    .EXAMPLE
        PS> Select-CommandArgumentSet -ScriptBlock $script -ExtensionName "LeetABit.Build.PowerShell" -AdditionalArguments $arguments
 
        Tries to selects arguments for a script block defined in "LeetABit.Build.PowerShell" module with an additional arguments specified as a parameter.
    .EXAMPLE
        PS> Select-CommandArgumentSet -ParameterSets (Get-Command LeetABit.Build.PowerShell\Deploy-Project).ParameterSets
 
        Tries to selects arguments for a Get-Command LeetABit.Build.PowerShell\Deploy-Project command via its parameter sets.
    .NOTES
        Select-CommandArgumentSet cmdlet tries to match each of the command's parameter set till it finds the first satisfied completely.
        If no parameter set is satisfied with the current arguments provided to the module this cmdlet emits an error message.
    .LINK
        Add-CommandArgument
    .LINK
        Set-CommandArgumentSet
    #>

    [CmdletBinding(PositionalBinding = $False,
                   DefaultParameterSetName = "Command")]
    [OutputType([Object[]])]

    param (
        # Command for which a matching arguments shall be selected.
        [Parameter(HelpMessage = 'Provide command info object for which arguments shall be selected.',
                   Position = 0,
                   Mandatory = $True,
                   ValueFromPipeline = $True,
                   ValueFromPipelineByPropertyName = $True,
                   ParameterSetName = "Command")]
        [System.Management.Automation.CommandInfo]
        $Command,

        # Script block for which a matching arguments shall be selected.
        [Parameter(HelpMessage = 'Provide script block object for which arguments shall be selected.',
                   Position = 0,
                   Mandatory = $True,
                   ValueFromPipeline = $True,
                   ValueFromPipelineByPropertyName = $True,
                   ParameterSetName = "ScriptBlock")]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock,

        # Collection of the parameter sets for which a matching arguments shall be selected.
        [Parameter(Position = 0,
                   Mandatory = $False,
                   ValueFromPipeline = $True,
                   ValueFromPipelineByPropertyName = $True,
                   ParameterSetName = "Manual")]
        [CommandParameterSetInfo[]]
        $ParameterSets,

        # Name of the build extension for which the arguments shall be selected.
        [Parameter(Position = 1,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True,
                   ParameterSetName = "Command")]
        [Parameter(Position = 1,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True,
                   ParameterSetName = "ScriptBlock")]
        [Parameter(Position = 1,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True,
                   ParameterSetName = "Manual")]
        [String]
        $ExtensionName,

        # Dictionary of additional arguments that shall be used as a source of parameter's values.
        [Parameter(Position = 2,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $False,
                   ParameterSetName = "Command")]
        [Parameter(Position = 2,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $False,
                   ParameterSetName = "ScriptBlock")]
        [Parameter(Position = 2,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $False,
                   ParameterSetName = "Manual")]
        [IDictionary]
        $AdditionalArguments
    )

    begin {
        LeetABit.Build.Common\Import-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process {
        if ($PSCmdlet.ParameterSetName -eq 'Command') {
            $ParameterSets = $Command.ParameterSets

            if (-not $ExtensionName -and $Command.ModuleName) {
                $ExtensionName = $Command.ModuleName
            }
        } elseif ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
            if ($ScriptBlock.Ast.ParamBlock) {
                $function:private:ScriptBlockCommand = $ScriptBlock
                $commandFunction = Get-Command -Name ScriptBlockCommand -Type Function
                $ParameterSets = $commandFunction.ParameterSets
            }

            if (-not $ExtensionName -and $ScriptBlock.Module) {
                $ExtensionName = $ScriptBlock.Module.Name
            }
        }

        $errors = @()
        $namedArguments = @{}
        $positionalArguments = @()
        $found = $false
        $mostWideParameterSet = $null
        $mostWideParameterSetName = $Null

        foreach ($parameterSet in $parameterSets) {
            try {
                $currentNamedArguments, $currentPositionalArguments = Select-CommandArgumentSetCore -Parameters $parameterSet.Parameters -ExtensionName $ExtensionName -AdditionalArguments $AdditionalArguments
            }
            catch {
                if ($parameterSet.Name -eq $AllParameterSets) {
                    $errors += $_.Exception.Message
                }
                else {
                    $errors += "{0}: {1}" -f $parameterSet.Name, $_.Exception.Message
                }

                continue
            }

            $found = $true
            $currentParameterNames = $parameterSet.Parameters | Foreach-Object { $_.Name }
            $currentParameterSetName = $parameterSet.Name
            if (-not $mostWideParameterSet) {
                $mostWideParameterSet = $currentParameterNames
                $mostWideParameterSetName = $currentParameterSetName
                $namedArguments = $currentNamedArguments
                $positionalArguments = $currentPositionalArguments
            }
            else {
                $union = $mostWideParameterSet + $currentParameterNames | Sort-Object | Get-Unique
                if ($union.Length -gt [Math]::Max($mostWideParameterSet.Length, $currentParameterNames.Length)) {
                    throw $LocalizedData.Error_SelectCommandArgumentSet_Reason -f
                        ($LocalizedData.Reason_MultipleParameterSetsMatch_FirstParameterSet_SecondParameterSet -f $mostWideParameterSetName, $currentParameterSetName)
                }
                elseif ($currentParameterNames.Length -gt $mostWideParameterSet.Length) {
                    $mostWideParameterSet = $currentParameterNames
                    $mostWideParameterSetName = $currentParameterSetName
                    $namedArguments = $currentNamedArguments
                    $positionalArguments = $currentPositionalArguments
                }
            }
        }

        if (-not $found) {
            throw $LocalizedData.Error_SelectCommandArgumentSet_Reason -f
                ($LocalizedData.Reason_NoMatchingParameterSetFound_NewLine_Errors -f [Environment]::NewLine, ($errors -join [Environment]::NewLine))
        }

        $namedArguments
        Write-Output -InputObject $positionalArguments -NoEnumerate
    }
}


function Add-CommandArgument {
    <#
    .SYNOPSIS
        Adds a value for the specified parameter.
    .DESCRIPTION
        Add-CommandArgument cmdlet stores a specified value for the parameter in internal module state for later usage. This value may be further selected by Find-CommandArgument or Select-CommandArgumentSet cmdlets.
    .EXAMPLE
        PS> Add-CommandArgument -ParameterName "TaskName" -ParameterValue "help"
 
        Checks whether an argument for parameter "TaskName" has been already set. If not the cmdlet assigns a "help" value for it.
    .EXAMPLE
        PS> Add-CommandArgument -ParameterName "TaskName" -ParameterValue "help" -Force
 
        Sets "help" value for parameter "TaskName" regardless it was already set or not.
    .LINK
        Find-CommandArgument
    .LINK
        Select-CommandArgumentSet
    #>

    [CmdletBinding(PositionalBinding = $False)]

    param (
        # Name of the parameter which value shall be updated.
        [Parameter(HelpMessage = 'Provide name of the parameter which value shall be updated',
                   Position = 0,
                   Mandatory = $True,
                   ValueFromPipeline = $True,
                   ValueFromPipelineByPropertyName = $True)]
        [ValidateIdentifierAttribute()]
        [String]
        $ParameterName,

        # A new value for the parameter.
        [Parameter(HelpMessage = 'Provide a new value for the parameter.',
                   Position=1,
                   Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [AllowNull()]
        [Object]
        $ParameterValue,

        # Indicates that this cmdlet overwrites value already set to the parameter.
        [Parameter(Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $False)]
        [Switch]
        $Force)

    begin {
        LeetABit.Build.Common\Import-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process {
        if ($script:NamedArguments.ContainsKey($ParameterName)) {
            if (-not $Force) {
                throw $LocalizedData.Error_SetCommandArgument_Reason -f
                    ($LocalizedData.Reason_ArgumentAlreadySet_ParameterName -f $ParameterName)
            }
        }

        $script:NamedArguments[$ParameterName] = $ParameterValue
    }
}


function Set-CommandArgumentSet {
    <#
    .SYNOPSIS
        Sets a collection of arguments that shall be used for command execution.
    .DESCRIPTION
        Set-CommandArgumentSet cmdlet clears all arguments previously set and stores a new values for the parameters in internal module state for later usage. These values may be further selected by Find-CommandArgument or Select-CommandArgumentSet cmdlets.
    .EXAMPLE
        PS> Set-CommandArgumentSet -RepositoryRoot "." -NamedArguments @{ "TaskName" = "help" } -UnknownArguments $args
 
        Clears all arguments previously set in the module and initializes internal module data with values from the specified parameters.
    .NOTES
        This cmdlet searches for repository configuration file called 'LeetABit.Build.json' inside repository root directory. Values from this file are used as one of the arguments source.
        This file shall contain one JSON object with properties which names match parameter name and which values shall be used as arguments for these parameters.
        A schema for this file is located at https://raw.githubusercontent.com/LeetABit/Build/master/schema/LeetABit.Build.schema.json
    #>

    [CmdletBinding(PositionalBinding = $False,
                   SupportsShouldProcess = $True,
                   ConfirmImpact = 'Low')]

    param (
        # Location of the repository on which te command will be executed.
        [Parameter(HelpMessage = 'Provide path to the root folder of the repository for which the command will be executed.',
                   Position=0,
                   Mandatory=$True,
                   ValueFromPipeline=$False,
                   ValueFromPipelineByPropertyName=$True)]
        [ValidateContainerPathAttribute()]
        [String]
        $RepositoryRoot,

        # Dictionary of buildstrapper parameters (including dynamic ones) that have been successfully bound.
        [Parameter(Position=1,
                   Mandatory=$False,
                   ValueFromPipeline=$False,
                   ValueFromPipelineByPropertyName=$True)]
        [IDictionary]
        $NamedArguments,

        # Collection of other arguments passed.
        [Parameter(Position=2,
                   Mandatory=$False,
                   ValueFromPipeline=$False,
                   ValueFromPipelineByPropertyName=$True)]
        [String[]]
        $UnknownArguments)

    begin {
        LeetABit.Build.Common\Import-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process {
        if ($PSCmdlet.ShouldProcess($LocalizedData.Resource_CurrentCommandArgumentSet,
                                    $LocalizedData.Operation_Overwrite)) {
            $script:ConfigurationJson = Read-ConfigurationFromFile $RepositoryRoot
            $script:NamedArguments = @{}
            $script:PositionalArguments = @()
            $script:UnknownArguments.Clear()

            $NamedArguments.Keys | ForEach-Object {
                $script:NamedArguments.Add($_, $NamedArguments[$_])
            }

            if ($UnknownArguments) {
                $script:UnknownArguments.AddRange($UnknownArguments)
                Pop-PositionalArguments
            }
        }
    }
}


##################################################################################################################
# Private Commands
##################################################################################################################


function Convert-ArgumentString {
    <#
    .SYNOPSIS
        Conditionally converts a specified string argument to a [Switch] type.
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([Object[]])]

    [SuppressMessage(
        'PSReviewUnusedParameter',
        'IsSwitch',
        Justification = 'False positive as rule does not scan child scopes: https://github.com/PowerShell/PSScriptAnalyzer/issues/1472')]
    param (
        # Argument string to convert.
        [Parameter(HelpMessage = 'Provide argument string value.',
                   Position=0,
                   Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [String[]]
        $Value,

        # Indicates whether the argument shall be threated as a value for [Switch] parameter.
        [Parameter(Position=1,
                   Mandatory=$False,
                   ValueFromPipeline=$False,
                   ValueFromPipelineByPropertyName=$True)]
        [Switch]
        $IsSwitch)

    process {
        $Value | ForEach-Object {
            if ($IsSwitch) {
                if ($_ -eq '0' -or
                $_ -eq 'False') {
                    [Switch]$False
                } else {
                    [Switch]$True
                }
            }
            else {
                $_
            }
        }
    }
}


function Convert-ArgumentValue {
    <#
    .SYNOPSIS
        Conditionally converts a specified argument to a [Switch] type.
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([Object[]])]

    [SuppressMessage(
        'PSReviewUnusedParameter',
        'IsSwitch',
        Justification = 'False positive as rule does not scan child scopes: https://github.com/PowerShell/PSScriptAnalyzer/issues/1472')]
    param (
        # Argument to convert.
        [Parameter(HelpMessage = 'Provide argument value.',
                   Position=0,
                   Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [Object[]]
        $Value,

        # Indicates whether the argument shall be threated as a value for [Switch] parameter.
        [Parameter(Position=1,
                   Mandatory=$False,
                   ValueFromPipeline=$False,
                   ValueFromPipelineByPropertyName=$True)]
        [Switch]
        $IsSwitch)

    process {
        $Value | ForEach-Object {
            if ($IsSwitch) {
                if ($_) {
                    [Switch][Boolean]$_
                } else {
                    [Switch]$True
                }
            }
            else {
                $_
            }
        }
    }
}


function Find-CommandArgumentInCommandLine {
    <#
    .SYNOPSIS
        Examines command line arguments for presence of a specified named parameter's value.
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([Object])]

    param (
        # Name of the parameter.
        [Parameter(HelpMessage = 'Provide parameter name.',
                   Position=0,
                   Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [String]
        $ParameterName,

        # Indicates whether the argument shall be threated as a value for [Switch] parameter.
        [Parameter(Position=1,
                   Mandatory=$False,
                   ValueFromPipeline=$False,
                   ValueFromPipelineByPropertyName=$True)]
        [Switch]
        $IsSwitch)

    process {
        foreach ($dictionaryParameterName in $script:NamedArguments.Keys) {
            if ($dictionaryParameterName -ne $ParameterName) {
                continue
            }

            $script:NamedArguments[$ParameterName]
            return
        }

        Find-CommandArgumentInUnknownArguments $ParameterName $IsSwitch
    }
}


function Find-CommandArgumentInConfiguration {
    <#
    .SYNOPSIS
        Examines JSON configuration file for presence of a specified named parameter's value.
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([Object])]

    param (
        # Name of the parameter.
        [Parameter(HelpMessage = 'Provide parameter name.',
                   Position=0,
                   Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [String]
        $ParameterName)

    process {
        if ($script:ConfigurationJson -and $script:ConfigurationJson.ContainsKey($ParameterName)) {
            $script:ConfigurationJson[$ParameterName]
            return
        }
    }
}


function Find-CommandArgumentInDictionary {
    <#
    .SYNOPSIS
        Examines specified arguments dictionary for presence of a specified named parameter's value.
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([Object])]

    param (
        # Name of the parameter.
        [Parameter(HelpMessage = 'Provide parameter name.',
                   Position=0,
                   Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [String]
        $ParameterName,

        # A dictionary that holds an arguments to be used as a parameter's value source.
        [Parameter(Position=2,
                   Mandatory=$False,
                   ValueFromPipeline=$False,
                   ValueFromPipelineByPropertyName=$False)]
        [IDictionary]
        $Dictionary)

    process {
        if ($Dictionary) {
            foreach ($dictionaryParameterName in $Dictionary.Keys) {
                if ($dictionaryParameterName -ne $ParameterName) {
                    continue
                }

                $Dictionary[$dictionaryParameterName]
                break
            }
        }
    }
}


function Find-CommandArgumentInEnvironment {
    <#
    .SYNOPSIS
        Examines environmental variables for presence of a specified named parameter's value.
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([Object])]

    param (
        # Name of the parameter.
        [Parameter(HelpMessage = 'Provide parameter name.',
                   Position=0,
                   Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [String]
        $ParameterName
    )

    process {
        if (Test-Path "env:\$ParameterName") {
            Get-Content "env:\$ParameterName"
        }
    }
}


function Find-CommandArgumentInUnknownArguments {
    <#
    .SYNOPSIS
        Examines a collection of arguments which kind has not yet been determined for presence of a specified named parameter's value.
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([Object])]

    param (
        # Name of the parameter.
        [Parameter(HelpMessage = 'Provide parameter name.',
                   Position=0,
                   Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [String]
        $ParameterName,

        # Indicates whether the argument shall be threated as a value for [Switch] parameter.
        [Parameter(Position=1,
                   Mandatory=$False,
                   ValueFromPipeline=$False,
                   ValueFromPipelineByPropertyName=$True)]
        [Switch]
        $IsSwitch
    )

    process {
        for ($i = 0; $i -lt $script:UnknownArguments.Count; ++$i) {
            $unknownCandidate = $script:UnknownArguments[$i]
            $hasNextArgument = ($i + 1) -lt $script:UnknownArguments.Count
            if ($hasNextArgument) {
                $nextCandidate = $script:UnknownArguments[$i + 1]
            } elseif (-Not ($IsSwitch)) {
                break
            }

            $candidateParameterName = Select-ParameterName $unknownCandidate
            if ($candidateParameterName -eq $ParameterName) {
                $simpleSwitch = $False

                if ($IsSwitch) {
                    if ($unknownCandidate.EndsWith(':')) {
                        if ($hasNextArgument) {
                            if (($nextCandidate -eq 'True') -or ($nextCandidate -eq 'False')) {
                                $result = [Switch][System.Boolean]$nextCandidate
                            }
                        }
                    } else {
                        $result = [Switch]$True
                        $simpleSwitch = $True
                    }
                } else {
                    $result = $nextCandidate
                }

                $script:NamedArguments[$ParameterName] = $result
                $script:UnknownArguments.RemoveAt($i)
                if (-not $simpleSwitch) { $script:UnknownArguments.RemoveAt($i) }
                if ($i -eq 0)           { Pop-PositionalArguments              }
                $script:NamedArguments[$ParameterName]
                return
            }
        }
    }
}


function Pop-PositionalArguments {
    <#
    .SYNOPSIS
        Locates a positional argument in a head of the collection of arguments which kind has not yet been determined.
    #>

    [CmdletBinding(PositionalBinding = $False)]

    param ()

    process {
        while ($script:UnknownArguments.Count -gt 0) {
            if (-Not (Test-ParameterName $script:UnknownArguments[0])) {
                $script:PositionalArguments += $script:UnknownArguments[0]
                $script:UnknownArguments.RemoveAt(0)
            }
            else {
                break
            }
        }
    }
}


function Read-ConfigurationFromFile {
    <#
    .SYNOPSIS
        Initializes a script configuration values from LeetABit.Build.json configuration file.
    #>

    [CmdletBinding(PositionalBinding = $False)]

    param (
        # Location of the repository for which te configuration file shall be located.
        [Parameter(HelpMessage = "Provide path to the repository root directory.",
                   Position=0,
                   Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [ValidateContainerPathAttribute()]
        [String]
        $RepositoryRoot
    )

    process {
        $result = @{}
        Get-ChildItem -Path $RepositoryRoot -Filter 'LeetABit.Build.json' -Recurse | Foreach-Object {
            $configFilePath = $_.FullName
            Write-Verbose ($LocalizedData.Message_InitializingConfigurationFromFile_FilePath -f $configFilePath)

            if (Test-Path $configFilePath -PathType Leaf) {
                try {
                    $configFileContent = Get-Content -Raw -Encoding UTF8 -Path $configFilePath
                    ConvertFrom-Json $configFileContent | ConvertTo-Hashtable | ForEach-Object {
                        foreach ($key in $_.Keys) {
                            $result[$key] = $_[$key]
                        }
                    }
                }
                catch {
                    throw [System.IO.FileFormatException]::new([Uri]::new($configFilePath), $LocalizedData.Exception_IncorrectJsonFileFormat, $_)
                }
            }
        }

        $result
    }
}


function ConvertTo-Hashtable {
    <#
    .SYNOPSIS
        Converts an input object to a HAshtable.
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([Hashtable])]
    param (
        # Object to convert.
        [Parameter(Position = 0,
                   Mandatory = $False,
                   ValueFromPipeline = $True,
                   ValueFromPipelineByPropertyName = $True)]
        [Object]
        [AllowNull()]
        $InputObject
    )

    process {
        if ($Null -eq $InputObject -or $InputObject -is [IDictionary]) {
            $InputObject
        }
        elseif ($InputObject -is [IEnumerable] -and $InputObject -isnot [string]) {
            $InputObject | ForEach-Object {
                ConvertTo-Hashtable -InputObject $_
            }
        } elseif ($InputObject -is [PSObject]) {
            $hash = @{}
            foreach ($property in $InputObject.PSObject.Properties) {
                $hash[$property.Name] = ConvertTo-Hashtable -InputObject $property.Value
            }

            $hash
        } else {
            $InputObject
        }
    }
}


function Select-CommandArgumentSetCore {
    <#
    .SYNOPSIS
        Selects a collection of arguments that match specified command parameter set.
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([IDictionary],[Object[]])]

    param (
        # Collection of the parameters for which a matching arguments shall be selected.
        [Parameter(HelpMessage = 'Provide collection of the parameters for which a matching arguments shall be selected.',
                   Position = 0,
                   Mandatory = $True,
                   ValueFromPipeline = $True,
                   ValueFromPipelineByPropertyName = $True)]
        [CommandParameterInfo[]]
        $Parameters,

        # Prefix for the parameters name that shall be used.
        [Parameter(HelpMessage = 'Provide prefix for the parameters name that shall be used.',
                   Position = 1,
                   Mandatory = $True,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True)]
        [String]
        $ExtensionName,

        # Dictionary of additional arguments that shall be used as a source of parameter's values.
        [Parameter(HelpMessage = "Provide dictionary of additional arguments that shall be used as a source of parameter's values.",
                   Position = 2,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $False)]
        [IDictionary]
        $AdditionalArguments
    )

    begin {
        LeetABit.Build.Common\Import-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process {
        $namedArguments = @{}
        $positionalArguments = @()
        $requiredMandatoryPositionalArguments = 0
        $requiredOptionalPositionalArguments = 0

        if ($Parameters) {
            foreach ($parameter in $Parameters) {
                $argument = $Null

                foreach ($suffix in @($parameter.Name) + $parameter.Aliases) {
                    $argument = Find-CommandArgument $parameter.Name $ExtensionName -IsSwitch:($parameter.ParameterType -eq [SwitchParameter]) -AdditionalArguments $AdditionalArguments
                    if ($null -ne $argument) {
                        break
                    }
                }

                if ($null -ne $argument) {
                    $namedArguments[$parameter.Name] = $argument
                    continue
                }

                if ($parameter.IsMandatory) {
                    if ($parameter.Position -ge 0) {
                        $requiredMandatoryPositionalArguments = [Math]::Max($requiredMandatoryPositionalArguments, ($parameter.Position + 1))
                    } else {
                        throw $LocalizedData.Exception_NamedParameterValueMissing_ParameterName -f $parameter.Name
                    }
                } else {
                    if ($parameter.Position -ge 0) {
                        $requiredOptionalPositionalArguments = [Math]::Max($requiredOptionalPositionalArguments, ($parameter.Position + 1))
                    }
                }
            }
        }

        $missingArguments = $requiredMandatoryPositionalArguments - $script:PositionalArguments.Length
        if ($missingArguments -gt 0) {
            throw $LocalizedData.Exception_PositionalParameterValueMissing_ParameterCount -f $missingArguments
        }

        $missingArguments = $requiredOptionalPositionalArguments - $script:PositionalArguments.Length

        if ($missingArguments -gt 0) {
            $positionalLimit = [Math]::Min($requiredMandatoryPositionalArguments, $script:PositionalArguments.Length)
            for ($i = 0; $i -lt $positionalLimit; ++$i) {
                $positionalArguments += $script:PositionalArguments[$i]
            }
        } else {
            $positionalLimit = [Math]::Min($requiredOptionalPositionalArguments, $script:PositionalArguments.Length)
            for ($i = 0; $i -lt $positionalLimit; ++$i) {
                $positionalArguments += $script:PositionalArguments[$i]
            }
        }

        $namedArguments
        Write-Output $positionalArguments -NoEnumerate
    }
}


function Select-ParameterName {
    <#
    .SYNOPSIS
        Obtains a parameter name from the specified argument if it matches an parameter name pattern.
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([String])]

    param (
        # Argument to examine.
        [Parameter(HelpMessage = "Provide value of the command's argument.",
                   Position=0,
                   Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [String]
        $Argument)

    process {
        $firstParameterChar = '\p{Lu}|\p{Ll}|\p{Lt}|\p{Lm}|\p{Lo}|_|\?'
        $parameterChar = '[^\{\}\(\)\;\,\|\&\.\[\:\s\n]'

        if ($Argument -match "^-(($firstParameterChar)($parameterChar)*)\:?$") {
            $matches[1]
        }
    }
}


function Test-ParameterName {
    <#
    .SYNOPSIS
        Checks whether the specified argument represents a name of the parameter specifier.
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([System.Boolean])]

    param (
        # Argument which shall be checked.
        [Parameter(HelpMessage = "Provide value of the command's argument.",
                   Position=0,
                   Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [String]
        $Argument)

    process {
        $firstParameterChar = '\p{Lu}|\p{Ll}|\p{Lt}|\p{Lm}|\p{Lo}|_|\?'
        $parameterChar = '[^\{\}\(\)\;\,\|\&\.\[\:\s\n]'

        $Argument -match "^-(($firstParameterChar)($parameterChar)*)\:?$"
    }
}


Export-ModuleMember -Function '*' -Variable '*' -Alias '*' -Cmdlet '*'

# SIG # Begin signature block
# MIIM3wYJKoZIhvcNAQcCoIIM0DCCDMwCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBQFmYSnXj0UAVd
# R0EzBBjmiK4Y1rFvadvdCr8EcKVgZqCCCe0wggTeMIIDxqADAgECAhBrMmoPAyjT
# eh1TC/0jvUjiMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK
# ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy
# dGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5l
# dHdvcmsgQ0EwHhcNMTUxMDI5MTEzMDI5WhcNMjcwNjA5MTEzMDI5WjCBgDELMAkG
# A1UEBhMCUEwxIjAgBgNVBAoMGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAl
# BgNVBAsMHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAwwb
# Q2VydHVtIENvZGUgU2lnbmluZyBDQSBTSEEyMIIBIjANBgkqhkiG9w0BAQEFAAOC
# AQ8AMIIBCgKCAQEAt9uo2MjjvNrag7q5v9bVV1NBt0C6FwxEldTpZjt/tL6Qo5QJ
# pa0hIBeARrRDJj6OSxpk7A5AMkP8gp//Si3qlN1aETaLYe/sFtRJA9jnXcNlW/JO
# CyvDwVP6QC3CqzMkBYFwfsiHTJ/RgMIYew4UvU4DQ8soSLAt5jbfGz2Lw4ydN57h
# BtclUN95Pdq3X+tGvnYoNrgCAEYD0DQbeLQox1HHyJU/bo2JGNxJ8cIPGvSBgcdt
# 1AR3xSGjLlP5d8/cqZvDweXVZy8xvMDCaJxKluUf8fNINQ725LHF74eAOuKADDSd
# +hRkceQcoaqyzwCn4zdy+UCtniiVAg3OkONbxQIDAQABo4IBUzCCAU8wDwYDVR0T
# AQH/BAUwAwEB/zAdBgNVHQ4EFgQUwHu0yLduVqcJSJr4ck/X1yQsNj4wHwYDVR0j
# BBgwFoAUCHbNywf/JPbFze27kLzihDdGdfcwDgYDVR0PAQH/BAQDAgEGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMDMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9jcmwuY2Vy
# dHVtLnBsL2N0bmNhLmNybDBrBggrBgEFBQcBAQRfMF0wKAYIKwYBBQUHMAGGHGh0
# dHA6Ly9zdWJjYS5vY3NwLWNlcnR1bS5jb20wMQYIKwYBBQUHMAKGJWh0dHA6Ly9y
# ZXBvc2l0b3J5LmNlcnR1bS5wbC9jdG5jYS5jZXIwOQYDVR0gBDIwMDAuBgRVHSAA
# MCYwJAYIKwYBBQUHAgEWGGh0dHA6Ly93d3cuY2VydHVtLnBsL0NQUzANBgkqhkiG
# 9w0BAQsFAAOCAQEAquU/dlQCTHAOKak5lgYPMbcL8aaLUvsQj09CW4y9MSMBZp3o
# KaFNw1D69/hFDh2C1/z+pjIEc/1x7MyID6OSCMWBWAL9C2k7zbg/ST3QjRwTFGgu
# mw2arbAZ4p7SfDl3iG8j/XuE/ERttbprcJJVbJSx2Df9qVkdtGOy3BPNeI4lNcGa
# jzeELtRFzOP1zI1zqOM6beeVlHBXkVC2be9zck8vAodg4uoioe0+/dGLZo0ucm1P
# xl017pOomNJnaunaGc0Cg/l0/F96GAQoHt0iMzt2bEcFXdVS/g66dvODEMduMF+n
# YMf6dCcxmyiD7SGKG/EjUoTtlbytOqWjQgGdvDCCBQcwggPvoAMCAQICECxWDYHo
# gPTFxULdYYZu+b0wDQYJKoZIhvcNAQELBQAwgYAxCzAJBgNVBAYTAlBMMSIwIAYD
# VQQKDBlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLDB5DZXJ0dW0g
# Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkxJDAiBgNVBAMMG0NlcnR1bSBDb2RlIFNp
# Z25pbmcgQ0EgU0hBMjAeFw0yMDAzMTgwNjMzMDdaFw0yMTAzMTgwNjMzMDdaMHAx
# CzAJBgNVBAYTAlBMMRAwDgYDVQQHDAdLcmFrw7N3MR4wHAYDVQQKDBVPcGVuIFNv
# dXJjZSBEZXZlbG9wZXIxLzAtBgNVBAMMJk9wZW4gU291cmNlIERldmVsb3Blciwg
# SHViZXJ0IEJ1a293c2tpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
# 3B0pw0zPe4KG0FR7q4ZrHDWpELuc3KyfaaaDkF6EXpbX5bCByq97QrQ4YJjru9UW
# JK+u45hyUpiUXWPfeKHKKz3bLMCmIMaPq+FpfSh2xzB3lFnj/1LlO7htUpfKZ6Ks
# FpCkrKk6ZiPs8PxXpfmoiOzxryySbcqVZr7ZEQnRKfl6Op4IKiZQ54lZOy4ORRMu
# ghxlxJfK49XQ5gUrV1dRL3blFSIrfOl1K6wB0/5QxxWeO4WxHzD9WtfpSTs2/gML
# fj4xe80QjaGGShpN+qtPRaa+2qEdm2Dm+Btto8Gy9eVcxUXAQNZFTZPMd2Sf71yl
# whZIsqEChoTawyrnZTMwgwIDAQABo4IBijCCAYYwDAYDVR0TAQH/BAIwADAyBgNV
# HR8EKzApMCegJaAjhiFodHRwOi8vY3JsLmNlcnR1bS5wbC9jc2Nhc2hhMi5jcmww
# cQYIKwYBBQUHAQEEZTBjMCsGCCsGAQUFBzABhh9odHRwOi8vY3NjYXNoYTIub2Nz
# cC1jZXJ0dW0uY29tMDQGCCsGAQUFBzAChihodHRwOi8vcmVwb3NpdG9yeS5jZXJ0
# dW0ucGwvY3NjYXNoYTIuY2VyMB8GA1UdIwQYMBaAFMB7tMi3blanCUia+HJP19ck
# LDY+MB0GA1UdDgQWBBSqIB8Yxug7KjAkRKhvS62xLJPR1zAdBgNVHRIEFjAUgRJj
# c2Nhc2hhMkBjZXJ0dW0ucGwwDgYDVR0PAQH/BAQDAgeAMEsGA1UdIAREMEIwCAYG
# Z4EMAQQBMDYGCyqEaAGG9ncCBQEEMCcwJQYIKwYBBQUHAgEWGWh0dHBzOi8vd3d3
# LmNlcnR1bS5wbC9DUFMwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQEL
# BQADggEBAKkWEKsxeIDC+mcLz+zJgNkK+eXZR1sEueM5LcK7iDzWPG8pPOfrKJMH
# v67m3XG1PYy54Qn3AHGIZzXPF+HgIatkEFE931TUTjUFhuTuiEtKft+gsZgEyCXG
# Km2e5fYiaBRUAtvQKPpDrocSazIP92x+blTaIKM1Z+Ysx/2YTwkpyMclviK7OisV
# JHzbKmxgLxhatMwCPtLbFuAUffDxG8igXstCbQ3Qoa4qj2HldQy4HVCYDfdA3PcV
# 9LGXPpCKGeSFCGekSdZW2f61xATc0mCpfECVTUQBJL4taNCeR219IfX20ETo+zH6
# epfSds5WtOnY/9uzFR6jLNtAx3IVrQwxggJIMIICRAIBATCBlTCBgDELMAkGA1UE
# BhMCUEwxIjAgBgNVBAoMGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNV
# BAsMHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAwwbQ2Vy
# dHVtIENvZGUgU2lnbmluZyBDQSBTSEEyAhAsVg2B6ID0xcVC3WGGbvm9MA0GCWCG
# SAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcN
# AQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUw
# LwYJKoZIhvcNAQkEMSIEIGxRG53tjJygQK9tfNQoH1PXt+kGfvaQIA25cObVhRZx
# MA0GCSqGSIb3DQEBAQUABIIBAL9gOaG360qdHYI9cUfGO+B7bn/pcHCXeLF1o+bL
# 59L1Cbj63HHlC8ZPQNmsdfEtoSvlNYOJ8gzE1yUcF3bbBHqYx1IRRoF4KtCtgo9X
# dSfB0/og7MgV5d8MiZEy5JDoKfiFo3X7J4OqfxHUQAwn9hpcU7fyT/dRPm8wGsTZ
# 9cRCEf2uNvt92A5/jAS8CkJTSKpemGRYS2MmQQCOgytiuusNMb7hdmCyzpg9q+OA
# 8l+R5+1sGGtEgJGH10HDV3556mXpE5dvTYzZro+0pU4oWrU4rvBhfesIiOxIxvIj
# vGPgY2L05V4u5CXegdLYbGhCjvr/p+lUmKSJ7bCUGfTyh9Q=
# SIG # End signature block