Reuse.psm1

Function Get-Defaults {
    Param(
        [Parameter(ValueFromPipeline = $true)]
        $Defaults
    )

    If ($Defaults.GetType() -ne [HashTable]) {
        $customObject = Get-Content -Path $Defaults | ConvertFrom-Json
        $Defaults = @{}
        $customObject | Get-Member -MemberType NoteProperty | %{ $Defaults[$_.Name] = $customObject."$($_.Name)" }
    }

    $Defaults
}

Function Update-WithDefaults {
    <#
        .SYNOPSIS
        Replaces placeholders in string with values from Hashtable or JSON file
         
        .DESCRIPTION
        Replaces placeholders in text with values from Defaults.
 
        Placeholder styles:
         - AutoDetect[DEFAULT]
         - PercentWrapped %placeholder_name%
         - FigureWrapped {placeholder_name}
         - MSBuild $(placeholder_name)
 
         Placeholder names:
          - Can't contain spaces
          - Can contain any other character except closing one
         
        .OUTPUTS
        String or String[]
        Text with replaced placeholders or list of variables which will be replaced
         
        .PARAMETER Text
        String with placeholders to be replaced with values from Defaults
 
        .PARAMETER Defaults
        Hashtable with parameters or path to a file which contains JSON dictionary
 
        .PARAMETER PlaceholderStyle
        Style of placeholders. Check main description to see available styles
 
        .PARAMETER IgnoreMissing
        Use this switch, if you wan't to ignore missing
 
        .PARAMETER ListOnly
        Returns only list of variables which will be replaced
         
        .EXAMPLE
        Update-WithDefaults 'Service IP address: %IP%' @{ 'IP'='127.0.0.1' }
 
        .EXAMPLE
        Update-WithDefaults -Text 'Building $(assembly)' -Defaults @{ 'assembly'='test.dll' } -PlaceholderStyle MSBuild
 
        .EXAMPLE
        Update-WithDefaults -Text 'Deploying %service% to slot %slot%' -Defaults my-config.json
         
        my-config.json contents:
        {
            'service': 'myservice',
            'slot': 'Production'
        }
    #>
  
    [CmdletBinding(SupportsShouldProcess=$true)]  
    Param(
        [Parameter(Mandatory = $true, HelpMessage = 'Text with placeholders')]
        [String]
        $Text,

        [Parameter(Mandatory = $true, HelpMessage = 'Hashtable of defaults or path to JSON file with them')]
        [ValidateScript({ ($_ -is [HashTable]) -or (Test-Path -Path $_ -PathType Leaf) })]
        $Defaults, 
              
        [ValidateSet('AutoDetect', 'PercentWrapped', 'FigureWrapped', 'MSBuild')]
        $PlaceholderStyle = 'AutoDetect',

        [switch]
        $IgnoreMissing,

        [switch]
        $ListOnly
    )

    $ErrorActionPreference = 'Stop'

    $Defaults = Get-Defaults -Defaults $Defaults
    
    $patterns = @{
        'PercentWrapped' = '%([^ %]+)%';
        'FigureWrapped'  = '\{([^ %]+)\}';
        'MSBuild'        = '\$\(([^ )]+)\)';
    }
    $pattern = $null

    If ($patterns.ContainsKey($PlaceholderStyle)) {
        $pattern = $patterns[$PlaceholderStyle]
    } Else {
        ForEach($key in $patterns.Keys) {
            $variables = [Regex]::Matches($Text, $patterns[$key]) | %{ $_.Groups[1].Value } | Select-Object -Unique
            If ($variables | ?{ $Defaults.ContainsKey($_) }) {
                $pattern = $patterns[$key]
                Write-Verbose "Auto detected $key pattern"
                Break
            }
        }
    }

    $replacements = @()
    If ($pattern) {
        $matches = [Regex]::Matches($Text, $pattern)
        If ($matches) {
            $matches | %{
                $name = $_.Groups[1].Value
                $capture = [Regex]::Escape($_.Groups[0].Value)
                If ($Defaults.Keys -icontains $name) {
                    $replacements += $name
                    $Text = $Text -replace $capture, $Defaults[$name]
                } ElseIf (-not $IgnoreMissing.IsPresent -and -not $ListOnly.IsPresent) {
                    throw "Value for '$name' placeholder is not available. "
                }
            }
        }
    }
    If (-not $ListOnly.IsPresent) {
        $Text
    } Else {
        $replacements
    }
}

Function FromFileOrMask {
    Param(
        $FileOrMaskOrObject,
        $DefaultMask = '*',
        $Folder
    )
    If (-not $Folder) { $Folder = Get-Location }

    Push-Location -Path $Folder

    If ($FileOrMaskOrObject -eq $null) {
        $result = Get-ChildItem -Name $DefaultMask | Resolve-Path 
    } ElseIf ($FileOrMaskOrObject -is [string]) {
        If (Test-Path -Path $FileOrMaskOrObject -PathType Leaf -IsValid) {
            $result = $FileOrMaskOrObject | Resolve-Path 
        } Else {
            $result = Get-ChildItem -Name $FileOrMaskOrObject | Resolve-Path 
        }
    } Else {
        $result = $FileOrMaskOrObject
    }

    Pop-Location

    If (-not $result) {
        $search = $FileOrMaskOrObject
        If (-not $search) { $search = $DefaultMask }

        throw "File/s '$search' not found in '$Folder'" 
    }

    $result
}

Function Invoke-CmdletWithDefaults {
    <#
        .SYNOPSIS
        Invokes any cmdlet with arguments from Hashtable or JSON file
 
        .DESCRIPTION
        You can invoke any cmdlet via this one using some default values.
        Those default values can be provided in the form of Hashtable or as path to a file containing JSON dictionary.
 
        You can provide as many arguments as you want:
            Invoke-CmdletWithDefaults -CmdletName Do-Something -Defaults @{ 'Arg1': 'value1'; 'Arg2': 'value2'; 'Switch1': 'True' }
            will invoke
            Do-Something -Arg1 value1 -Arg2 value2 -Switch1
 
        You can have switches in defaults too:
            Invoke-CmdletWithDefaults -CmdletName Do-Something -Defaults @{ 'Switch1': '' }
            will invoke
            Do-Something -Switch1
 
        You can provide more arguments to invoked cmdlet simply adding them as you would do with invoked cmdlet
            Invoke-CmdletWithDefaults -CmdletName Do-Something -Defaults @{ 'Arg1': 'value' } -Arg2 value -Switch1
            will invoke
            Do-Something -Defaults -Arg1 value -Arg2 value -Switch1
 
        You can override some of defaults:
            Invoke-CmdletWithDefaults -CmdletName Do-Something -Defaults @{ 'Arg1': 'value1'; 'Arg2': 'value2' } -Arg2 'value1 B'
            will invoke
            Do-Something -Arg1 value1 -Arg2 'value1 B'
 
        Even switches:
            Invoke-CmdletWithDefaults -CmdletName Do-Something -Defaults @{ 'Switch1': 'True' } -Switch1:$false
            will invoke
            Do-Something
 
        .OUTPUTS
        Value from invoked cmdlet
 
        .PARAMETER CmdletName
        Name of cmdlet to invoke
 
        .PARAMETER Defaults
        Hashtable with parameters or path to a file which contains JSON dictionary
 
        .EXAMPLE
        Invoke-CmdletWithDefaults -Cmdlet Test-Path -Defaults .\defaults.json
 
        defaults.json contents:
        {
            'Path': 'c:\Windows'
        }
 
        .EXAMPLE
        Invoke-CmdletWithDefaults -Cmdlet Test-Path -Defaults @{ Path='c:\asdsad' } '-Verbose'
        Passing default argument to invoked function
    #>


    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, HelpMessage = 'Name of Cmdlet to execute')]
        [ValidateScript({ Get-Command -Name $_ -ErrorAction Stop })]
        $CmdletName,

        [Parameter(Mandatory = $true, HelpMessage = 'Hashtable of defaults or path to JSON file with them')]
        [ValidateScript({ ($_ -is [HashTable]) -or (Test-Path -Path $_ -PathType Leaf) })]
        $Defaults,

        [Parameter(ValueFromRemainingArguments = $true)]
        $ArgumentList = @()
    )

    $Defaults = Get-Defaults $Defaults

    $cmdlet = Get-Command -Name $CmdletName
    $parameters = $cmdlet | Select-Object -ExpandProperty Parameters
    $flagNames = $parameters.Keys | ?{  $parameters[$_].SwitchParameter }

    # Override values from defaults by OverridedArguments

    $argumentName = $null
    $ArgumentList | %{
        $isName = $_ -is [String] -and $_[0] -eq '-'

        If ($isName) {
            $argumentName = $_.Split(':')[0].TrimStart('-')
            $flagValue = $_.Split(':')[1]

            If ($flagValue) {                
                $Defaults[$argumentName] = $flagValue
                $argumentName = $null
            } Else {
                $Defaults[$argumentName] = $null
            }            
        } Else {
            If (-not $argumentName) { throw "Value '$_' belongs to no argument" }

            $Defaults[$argumentName] = $_
            $argumentName = $null
        }
    }

    # Check what arguments cmdlet has
    $expression = "$CmdletName "
    $Defaults.Keys | ?{ $parameters.Keys -icontains $_ } | %{
        $value = $Defaults[$_]

        If ($flagNames -icontains $_) {
            If ($value -eq $null -or $value -eq $true -or $value -eq $true.ToString() -or $value -eq '$true') {
                $expression += "-$_ "
            }
        } Else {
            $expression += "-$_ `$Defaults['$_']"
        }
    }
    $Defaults.Keys | ?{ $parameters.Keys -inotcontains $_ } | %{ Write-Verbose "Parameter '-$_' skipped as it does't belong to $Cmdlet cmdlet" }

    Write-Verbose "Executing following expression: $expression"

    &( [scriptblock]::Create($expression) )
}

Function Invoke-ApplicationWithDefaults {
    <#
        .SYNOPSIS
        Invokes application replacing arguments with values from Hashtable or JSON file
 
        .DESCRIPTION
        You can start any application providing values from Hashtable or JSON file in arguments.
 
        Here is basic example how invocation can look like:
        Invoke-ApplicationWithDefaults -Defaults @{ "IP"='127.0.0.1' } ping %IP% -n 1
 
        It can also be used to invoke cmdlets.
         
        .OUTPUTS
        String
        STDOUT of started application or command line if -WhatIf is present
 
        .PARAMETER Application
        Name, absolute or relative path of application to start
 
        .PARAMETER Defaults
        Hashtable with parameters or path to a file which contains JSON dictionary
 
        .PARAMETER Defaults
        Ignores placeholders with no values in Defaults
 
        .EXAMPLE
        Invoke-ApplicationWithDefaults -Defaults defaults.json ping %IP%
         
        Will invoke:
        ping 127.0.0.1
 
        defaults.json contents:
        {
            'IP': '127.0.0.1'
        }
 
        .EXAMPLE
        Invoke-ApplicationWithDefaults -Defaults @{ 'domain'='bing.com'; 'port'=80 } Invoke-WebRequest 'http://%domain%:%port%'
         
        Will invoke:
        Invoke-WebRequest http://bing.com:80
         
        .EXAMPLE
        Invoke-ApplicationWithDefaults -Defaults defaults.json ping %IP% -n '%number of attempts%'
 
        Will invoke:
        ping 127.0.0.1 -n 5
 
        defaults.json contents:
        {
            'IP': '127.0.0.1'
            'number of attempts': 5
        }
    #>

    [CmdletBinding(SupportsShouldProcess=$true, PositionalBinding=$False)]
    Param(
        [Parameter(Mandatory = $true, HelpMessage = 'Hashtable of defaults or path to JSON file with them')]
        [ValidateScript({ ($_ -is [HashTable]) -or (Test-Path -Path $_ -PathType Leaf) })]
        $Defaults,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        $Application,  
        
        [ValidateSet('AutoDetect', 'PercentWrapped', 'FigureWrapped', 'MSBuild')]
        $PlaceholderStyle = 'AutoDetect',      

        [switch]
        $IgnoreMissing,

        [Parameter(ValueFromRemainingArguments = $true)]
        $ArgumentList = @()
    )

    $Defaults = Get-Defaults $Defaults   
    
    $ArgumentList = $ArgumentList | %{
        $argument = $_
        
        $argument = Update-WithDefaults -Text $argument -Defaults $Defaults -PlaceholderStyle $PlaceholderStyle -IgnoreMissing:$IgnoreMissing        
        
        # escaping internal quotes
        $argument = $argument -replace '"', '""'

        # adding wrapping quotes if needed
        If ($argument -match ' ') {
            $argument = """$argument"""
        }

        $argument
    }

    $commandLine = ". $Application " + [string]::Join(' ', $ArgumentList)
    If ($WhatIfPreference) {
        $commandLine
    } Else {
        Write-Verbose $commandLine
        Invoke-Expression -Command $commandLine
    }
}

Function Set-DefaultsToTemplateInternal {
    [CmdletBinding(SupportsShouldProcess=$true)]
    Param (
        [ValidateScript({ Test-Path $_ -PathType Leaf })]
        $TemplatePath,

        [ValidateScript({ ($_ -is [HashTable]) -or (Test-Path -Path $_ -PathType Leaf) })]
        $Defaults,

        [Switch]
        $IgnoreMissing,

        [Switch]
        $Force,

        [ValidateSet('AutoDetect', 'PercentWrapped', 'FigureWrapped', 'MSBuild')]
        $PlaceholderStyle = 'AutoDetect'
    ) 

    $DefaultsFile = $Defaults

    $Defaults = Get-Defaults $Defaults

    If ($DefaultsFile -isnot [HashTable] -and $Defaults -notcontains 'defaults') {            
        $Defaults['defaults'] = [System.IO.Path]::GetFileNameWithoutExtension($DefaultsFile)
        Write-Verbose "Adding 'defaults'='$($Defaults['defaults'])' to `$Defaults"
    }
    $ResultPath = $TemplatePath -ireplace '\.template$', ''   

    $ResultPath = Update-WithDefaults -Text $ResultPath -Defaults $Defaults -PlaceholderStyle $PlaceholderStyle -IgnoreMissing:$IgnoreMissing.IsPresent
    Write-Verbose "ResultPath will be '$ResultPath'"    
    
    $text = [System.IO.File]::ReadAllText($TemplatePath)

    $replaced = Update-WithDefaults -Text $text -Defaults $Defaults -PlaceholderStyle $PlaceholderStyle -IgnoreMissing:$IgnoreMissing.IsPresent
    
    If ($ResultPath -ieq $TemplatePath) {
        throw "ResultPath can't be same as TemplatePath, even with -Force flag '$ResultPath'"
    }
    If (-not $Force.IsPresent -and ( Test-Path -Path $ResultPath )) {
        throw "`$ResultPath already exists, use -Force flag to overwrite it: '$ResultPath'"        
    }

    If (-not $WhatIfPreference) {
        Write-Verbose "`$ResultPath already exists, overwriting: '$ResultPath'"
        Set-Content -Path $ResultPath -Value $replaced -Force
    } Else {
        New-Object PSCustomObject -Property @{ 'ResultPath' = $ResultPath; 'Content' = $replaced }
    }
}

Function Set-DefaultsToTemplate {
    <#
        .SYNOPSIS
        Creates file replacing placeholders with values from Hashtable or JSON file
         
        .DESCRIPTION
        Creates file from template file and defaults.
 
        Placeholders in resulting file content and name will by values in Defaults.
 
        If $ResultPath is absent, then template file name will be used without '.template' extension.
 
        If $ResultPath contains placeholders they will be also replced.
 
        If Defaults is a path to a JSON file, then it's name without extension will be also available by name 'defaultsName'.
        For example:
            Set-DefaultsToTemplate -TemplatePath %defaultsName%-service.xml.template -Defaults myservice-prod.json
        Will save result to: 'myservice-prod-service.xml'.
 
        To get help about placeholders and their styles run:
        Get-Help Update-WithDefaults
         
        .OUTPUTS
        String
        $ResultPath eventually used
         
        If -WhatIf switch is presents returns hashtable with target path and content
 
        .PARAMETER TemplatePath
        Relative or absolute path to template file
         
        .PARAMETER Defaults
        Hashtable with parameters or path to a file which contains JSON dictionary
 
        .PARAMETER PlaceholderStyle
        Style of placeholders
 
        .PARAMETER IgnoreMissing
        Use this switch, if you wan't to ignore missing
    #>


    [CmdletBinding(SupportsShouldProcess=$true)]
    Param (
        $TemplatePath,

        $Defaults,

        [Switch]
        $IgnoreMissing,

        [Switch]
        $Force,

        [ValidateSet('AutoDetect', 'PercentWrapped', 'FigureWrapped', 'MSBuild')]
        $PlaceholderStyle = 'AutoDetect'
    )

    #return Set-DefaultsToTemplateInternal -TemplatePath $TemplatePath -Defaults $Defaults -ResultPath $ResultPath -IgnoreMissing:$IgnoreMissing -Force:$Force -PlaceholderStyle $PlaceholderStyle

    $templates = FromFileOrMask -FileOrMaskOrObject $TemplatePath -DefaultMask *.template
    
    $defaultCollections = FromFileOrMask -FileOrMaskOrObject $Defaults -DefaultMask *.json
    
    $templates | %{
        $template = $_

        $defaultCollections | %{
            $anotherDefaults = $_

            Set-DefaultsToTemplateInternal -TemplatePath $template -Defaults $anotherDefaults -IgnoreMissing:$IgnoreMissing -Force:$Force -PlaceholderStyle $PlaceholderStyle
        }
    }
}

Export-ModuleMember -Function Update-WithDefaults, Invoke-CmdletWithDefaults, Invoke-ApplicationWithDefaults, Set-DefaultsToTemplate