PwSh.Fw.Core.psm1

<#
 
    .SYNOPSIS
    PwSh.Fw main module : PwSh.Fw.Core
 
    .DESCRIPTION
    PwSh.Fw.Core contains generic purpose functions.
 
    .NOTES
        Author: Charles-Antoine Degennes <cadegenn@gmail.com>
 
#>


# handle $IsWindows prior to Powershell 6
if ($PSVersionTable.PSVersion.Major -lt 6) {
    # Powershell 1-5 is only on windows
    $Global:IsWindows = $true
}

<#
    .SYNOPSIS
    Template function
 
    .DESCRIPTION
    Skeleton of a typical function to use un PwSh.Fw.Core
 
    .PARAMETER string
    a string
 
    .EXAMPLE
    New-TemplateFunction -string "a string"
 
    .NOTES
    General notes
 
    .LINK
    https://gitlab.com/pwsh.fw/pwsh.fw.core
 
#>

function New-TemplateFunction {
    [CmdletBinding()]
    [OutputType([System.String])]
    Param (
        [Parameter(Mandatory,ValueFromPipeLine = $true)][string]$string
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        return $string
    }

    End {
        Write-LeaveFunction
    }
}

function Set-PwShFwConfiguration {
    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Alias('Devel')]
        [Parameter(Mandatory = $false, ValueFromPipeLine = $true)][switch]$dev,
        [Alias('Debug_')]
        [Parameter(Mandatory = $false, ValueFromPipeLine = $true)][switch]$d,
        [Alias('Verbose_')]
        [Parameter(Mandatory = $false, ValueFromPipeLine = $true)][switch]$v,
        [Alias('Info_')]
        [Parameter(Mandatory = $false, ValueFromPipeLine = $true)][switch]$i,
        [Parameter(Mandatory = $false, ValueFromPipeLine = $true)][switch]$Trace,
        [Parameter(Mandatory = $false, ValueFromPipeLine = $true)][switch]$Ask,
        [Parameter(Mandatory = $false, ValueFromPipeLine = $true)][switch]$Quiet,
        [Parameter(Mandatory = $false, ValueFromPipeLine = $true)][string]$Log,
        [Parameter(Mandatory = $false, ValueFromPipeLine = $true)][switch]$OverridePSPreferences
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        $Global:INFO = $i
        $Global:VERBOSE = $v
        $Global:DEBUG = $d
        $Global:DEVEL = $dev
        $Global:TRACE = $trace
        $Global:ASK = $ask
        $Global:QUIET = $quiet
        $Global:LOG = $log
        # keep the order as-is please !
        $Script:oldDebugPreference = $DebugPreference
        $Script:oldVerbosePreference = $VerbosePreference
        $Script:oldInformationPreference = $InformationPreference
        if ($ASK)   { Set-PSDebug -Step }
        if ($TRACE) {
            # Set-PSDebug -Trace 1
            $Global:DEVEL = $true
        }
        if ($DEVEL) {
            $Global:DEBUG = $true;
        }
        if ($DEBUG) {
            # $DebugPreference= ( $OverridePSPreferences ? "Continue" : "SilentlyContinue")
            if ($OverridePSPreferences) { $DebugPreference = "Continue" } else { $DebugPreference = "SilentlyContinue" }
            $Global:VERBOSE = $true
        }
        if ($VERBOSE) {
            # $VerbosePreference= ( $OverridePSPreferences ? "Continue" : "SilentlyContinue")
            if ($OverridePSPreferences) { $VerbosePreference = "Continue" } else { $VerbosePreference = "SilentlyContinue" }
            $Global:INFO = $true
        }
        if ($INFO) {
            # $InformationPreference= ( $OverridePSPreferences ? "Continue" : "SilentlyContinue")
            if ($OverridePSPreferences) { $InformationPreference = "Continue" } else { $InformationPreference = "SilentlyContinue" }
        }
        if ($QUIET) {
            $Global:DEVEL = $false
            $Global:DEBUG = $false;
            $Global:VERBOSE = $false
            $Global:INFO = $false
            # $DebugPreference= ( $OverridePSPreferences ? "SilentlyContinue" : "SilentlyContinue")
            # $VerbosePreference= ( $OverridePSPreferences ? "SilentlyContinue" : "SilentlyContinue")
            # $InformationPreference= ( $OverridePSPreferences ? "SilentlyContinue" : "SilentlyContinue")
            if ($OverridePSPreferences) { $DebugPreference = "Continue" } else { $DebugPreference = "SilentlyContinue" }
            if ($OverridePSPreferences) { $VerbosePreference = "Continue" } else { $VerbosePreference = "SilentlyContinue" }
            if ($OverridePSPreferences) { $InformationPreference = "Continue" } else { $InformationPreference = "SilentlyContinue" }
        }

        if ($log) {
            if ($TRACE) {
                Start-Transcript -Path $log
            # } else {
            # # add -Append:$false to overwrite logfile
            # # Write-ToLogFile -Message "Initialize log" -Append:$false
            # Write-ToLogFile -Message "Initialize log"
            }
            $null = Load-Module -Name PwSh.Fw.Log -Quiet -Policy Required
             Write-ToLogFile -Message "Initialize log"
        }
    }

    End {
        Write-LeaveFunction
    }
}

<#
.SYNOPSIS
Reset PwSh Framework to default configuration
 
.DESCRIPTION
Reset all configurations previously set by Set-PwShFwConfiguration
 
.EXAMPLE
Reset-PwShFwConfiguration
 
#>

function Reset-PwShFwConfiguration {
    [CmdletBinding()]
    [OutputType([void])]
    Param (
        # [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][string]$string
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        if ($Global:TRACE -and $Global:LOG) {
            Stop-Transcript
        }

        $Global:INFO = $false
        $Global:VERBOSE = $false
        $Global:DEBUG = $false
        $Global:DEVEL = $false
        $Global:TRACE = $false
        $Global:ASK = $false
        $Global:QUIET = $false
        $Global:LOG = $null

        $DebugPreference = $Script:oldDebugPreference
        $VerbosePreference = $Script:oldVerbosePreference
        $InformationPreference = $Script:oldInformationPreference
    }

    End {
        Write-LeaveFunction
    }
}

function Get-PwShFwConfiguration {
    [CmdletBinding()]
    [OutputType([Void])]
    Param (
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        Write-Message "INFO = $($Global:INFO)"
        Write-Message "VERBOSE = $($Global:VERBOSE)"
        Write-Message "DEBUG = $($Global:DEBUG)"
        Write-Message "DEVEL = $($Global:DEVEL)"
        Write-Message "TRACE = $($Global:TRACE)"
        Write-Message "ASK = $($Global:ASK)"
        Write-Message "QUIET = $($Global:QUIET)"
        Write-Message "LOG = $($Global:LOG)"
    }

    End {
        Write-LeaveFunction
    }
}

# function Get-PwShFwModuleInfos {
# [CmdletBinding()][OutputType([String])]Param (
# # [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][string]$string
# )
# Begin {
# # eenter($Script:NS + '\' + $MyInvocation.MyCommand)
# }

# Process {
# Write-Output "PSCommandPath = $PSCommandPath"
# Write-Output "PSScriptRoot = $PSScriptRoot"
# }

# End {
# # eleave($Script:NS + '\' + $MyInvocation.MyCommand)
# }
# }

<#
    .SYNOPSIS
    Execute a DOS/Shell command.
 
    .DESCRIPTION
    Wrapper for executing a DOS/Shell command. It handle (not yet) logging, (not yet) simulating and (not yet) asking.
    Please use following syntax :
    $rc = Execute-Commande "commande.exe" "arguments"
    to catch return code. Otherwise it will be printed to stdout and its quite ugly.
 
    .PARAMETER exe
    full path to executable
 
    .PARAMETER args
    all arguments enclosed in double-quotes. You may have to escape inner quotes to handle args with special characters.
 
    .PARAMETER AsInt
    Return code will be an int instead of a boolean.
 
    .EXAMPLE
    $rc = Execute-Command "net" "use w: \\srv\share"
 
    .EXAMPLE
    $rc = Execute-Command -exe "net" -args "use w: \\srv\share"
 
    .LINK
    https://gitlab.com/pwsh.fw/pwsh.fw.core
 
#>

function Execute-Command() {
    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Execute-Command is a more intuitive verb for this function and does not conflict with default Invoke-Command cmdlet.")]
    param(
        [parameter(mandatory=$true, position=0)][string]$exe,
        [parameter(mandatory=$false, position=1, ValueFromRemainingArguments=$true)][string]$args,
        [switch]$AsInt
    )

    $cmd = Get-Command -Name "$exe"
    switch ($cmd.CommandType) {
        'Application' {
            $exe = "& '" + $exe + "'"
            break
        }
        'ExternalScript' {
            $exe = "& '" + $exe + "'"
            break
        }
        'Cmdlet' {

        }
        default {

        }
    }

    Write-Debug "$exe $args`n"
    # $rcFile = $([System.IO.Path]::GetTempPath() + [IO.Path]::DirectorySeparatorChar + "rc")
    $rcFile = $([System.IO.Path]::GetTempPath() + "rc")
    # edevel("rcFile = $rcFile")
    # try {
        # if ($Global:DEVEL) {
        # if ($AsInt) {
        # Invoke-Expression ("$exe $args; `$LastExitCode | Out-File '$rcFile'") | Foreach-Object { Write-Devel $_ }
        # } else {
        # Invoke-Expression ("$exe $args; `$? | Out-File '$rcFile'") | Foreach-Object { Write-Devel $_ }
        # }
        # #return $?
        # } elseif ($Global:DEBUG) {
        # if ($AsInt) {
        # Invoke-Expression ("$exe $args; `$LastExitCode | Out-File '$rcFile'") | Out-Null
        # } else {
        # Invoke-Expression ("$exe $args; `$? | Out-File '$rcFile'") | Out-Null
        # }
        # #return $?
        # } else {
        # if ($AsInt) {
        # Invoke-Expression ("$exe $args; `$LastExitCode | Out-File '$rcFile'") | Out-Null
        # } else {
        # Invoke-Expression ("$exe $args; `$? | Out-File '$rcFile'") | Out-Null
        # }
        # #return $?
        # }
        if ($AsInt) {
            $out = Invoke-Expression ("$exe $args; `$LastExitCode | Out-File '$rcFile'")
        } else {
            $out = Invoke-Expression ("$exe $args; `$? | Out-File '$rcFile'")
        }
        if ($Global:DEVEL) {
            $out | Foreach-Object { Write-Devel $_ }
        # } elseif ($Global:DEBUG) {
        # $out | Foreach-Object { Write-MyDebug $_ }
        }
        # $rc = Get-Content "$rcFile" -Raw
        # # edevel("rc = $rc")
        # Remove-Item "$rcFile"
        # # edevel("return $rc")
        # # if ($null -ne $rc) {
            # return $rc
        # } else {
        # return $false
        # }
    # return $true
    # } catch {
    # return $false
    # }
    if ($AsInt) {
        return Get-Content "$rcFile" -Raw
    } else {
        return Get-Content "$rcFile" | Resolve-Boolean
    }
}

<#
    .SYNOPSIS
    Wrapper for Import-Module
 
    .DESCRIPTION
    It handle everything to not worry about error messages.
    * check if module exist in module path
    * if an absolute filename is given, check if module exist as well as manifest
    * load-it with an optional $Force parameter
    * write a warning if module cannot be found
 
    .PARAMETER Name
    Name of the module to load
 
    .PARAMETER FullyQualifiedName
    Absolute path and name of the module to load. It can be either the manifest file (.psd1) or the module file (.psm1)
 
    .PARAMETER Force
    Force a reload if module is already loaded
 
    .LINK
    https://gitlab.com/pwsh.fw/pwsh.fw.core
 
#>

function Load-Module {
    [CmdletBinding(
        DefaultParameterSetName="NAME"
    )]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Load-Module is a more intuitive verb for this function and does not conflict with default Get-Module cmdlet.")]
    Param (
        [Parameter(ParameterSetName="NAME",Mandatory,ValueFromPipeLine = $true)]
        [string]$Name,
        [Parameter(ParameterSetName="FILENAME",Mandatory,ValueFromPipeLine = $true)]
        [string]$FullyQualifiedName,
        [switch]$Force,
        [ValidateSet('Required', 'Optional')]
        [string]$Policy = "Required",
        [switch]$Quiet
    )
    Begin {
        # eenter($MyInvocation.MyCommand)
    }

    Process {
        if ($Quiet) {
            $oldQuiet = $Global:QUIET
            $Global:QUIET = $true
        }
        switch ($PSCmdlet.ParameterSetName) {
            "NAME" {
                $module = Get-Module -ListAvailable $Name | Sort-Object -Property Version | Select-Object -Last 1
                if ($null -eq $module) {
                    # fake module to display correct informations
                    # PowerShell < 5 does not return anything
                    $module = @{ name = $Name; path = $null}
                }
                break
            }
            "FILENAME" {
                $module = Get-Module -ListAvailable $FullyQualifiedName -ErrorAction Ignore | Sort-Object -Property Version | Select-Object -Last 1
                if ($null -eq $module) {
                    # fake module to display correct informations
                    # PowerShell < 5 does not return anything
                    $module = @{ name = (Split-Path -Leaf $FullyQualifiedName); path = $FullyQualifiedName}
                }
                break
            }
        }

        # exit if module is already loaded
        $rc = Get-Module -Name $module.Name
        if ($null -ne $rc) {
            if ($Force -eq $false) {
                Write-Debug "Module $($module.name) already loaded..."
                if ($Quiet) { $Global:QUIET = $oldQuiet    }
                return $true
            }
        }

        if ($Global:VERBOSE) {
            if ($Global:DEBUG) {
                Write-Debug "Importing module $($module.name) from '$($module.Path)'"
            } else {
                Write-Verbose "Importing module $($module.name)"
            }
        }
        switch ($Policy) {
            'Required' {
                $ErrorAction = 'Ignore'
                # $ErrorAction = 'Continue'
            }
            'Optional' {
                $ErrorAction = 'Ignore'
            }
        }

        switch ($PSCmdlet.ParameterSetName) {
            "NAME" {
                Import-Module -Name $module.name -Global -Force:$Force -DisableNameChecking -ErrorAction $ErrorAction
                $rc = $?
                break
            }
            "FILENAME" {
                # -FullyQualifiedName is not supported in PS < 5
                if ($PSVersionTable.PSVersion.Major -lt 5) {
                    Import-Module $module.Path -Global -Force:$Force -DisableNameChecking -ErrorAction $ErrorAction
                } else {
                    Import-Module -FullyQualifiedName $module.Path -Global -Force:$Force -DisableNameChecking -ErrorAction $ErrorAction
                }
                $rc = $?
                break
            }
        }

        switch ($Policy) {
            'Required' {
                if ($rc -eq $false) {
                    efatal("Module $($module.name) was not found and policy is '$Policy'.")
                }
            }
            'Optional' {
            }
        }

        if ($Global:VERBOSE) {
            eend $rc
        }

        if ($Quiet) { $Global:QUIET = $oldQuiet    }

        return $rc
    }

    End {
        # eleave($MyInvocation.MyCommand)
    }
}

<#
    .SYNOPSIS
    Test if a registry property exist
 
    .DESCRIPTION
    There is not yet a builtin function to test existence of registry value. Thanks to Jonathan Medd, here it is.
 
    .PARAMETER RegPath
    Registry path to the key
 
    .EXAMPLE
    Test-RegKeyExist -RegPath HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion
 
    .NOTES
    General notes
 
    .LINK
    https://www.jonathanmedd.net/2014/02/testing-for-the-presence-of-a-registry-key-and-value.html
#>


function Test-RegKeyExist {
    param (
        [Alias('Path')]
        [parameter(Mandatory=$true)][ValidateNotNullOrEmpty()]$RegPath
    )

    Test-Path -Path $RegPath -PathType Container
}

<#
    .SYNOPSIS
    Test if a registry property exist
 
    .DESCRIPTION
    There is not yet a builtin function to test existence of registry value. Thanks to Jonathan Medd, here it is.
 
    .PARAMETER RegPath
    Registry path to the key
 
    .PARAMETER Name
    Name of the value to test
 
    .EXAMPLE
    Test-RegValueExist -RegPath HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion -Value ProgramFilesDir
 
    .NOTES
    General notes
 
    .LINK
    https://www.jonathanmedd.net/2014/02/testing-for-the-presence-of-a-registry-key-and-value.html
#>


function Test-RegValueExist {
    param (
        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]$RegPath,
        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]$Name
    )

    try {
        $null = Get-ItemProperty -Path $RegPath | Select-Object -ExpandProperty $Name -ErrorAction Stop
        return $true
    } catch {
        return $false
    }
}

<#
    .SYNOPSIS
    Test if a variable exist
 
    .DESCRIPTION
    This function is silent and only return $true if the variable exist or $false otherwise.
 
    .PARAMETER Name
    a variable name to test
 
    .EXAMPLE
    Test-Variable -Name "myvar"
 
    .LINK
    https://gitlab.com/pwsh.fw/pwsh.fw.core
 
#>

function Test-Variable {
    [CmdletBinding()]Param (
        [Parameter(Mandatory,ValueFromPipeLine = $true)][string]$Name
    )
    Begin {
        # eenter($MyInvocation.MyCommand)
    }

    Process {
        Get-Variable -Name $Name -ErrorAction SilentlyContinue | Out-Null
        return $?
    }

    End {
        # eleave($MyInvocation.MyCommand)
    }
}

<#
.SYNOPSIS
Get a property value from a file
 
.DESCRIPTION
Get a property value from a file. The content of the file must be in the StringData format
 
.PARAMETER Filename
Path and name of the file to fetch data from
 
.PARAMETER Propertyname
Property to fetch
 
.EXAMPLE
Get-PropertyValueFromFile -Filename ./project.conf -PropertyName Version
 
.NOTES
General notes
 
.LINK
https://gitlab.com/pwsh.fw/pwsh.fw.core
 
#>

function Get-PropertyValueFromFile {
    [CmdletBinding()]Param (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [string]$Filename,
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [string]$Propertyname
    )
    Begin {
        # eenter($MyInvocation.MyCommand)
    }

    Process {
        $value = (Get-Content $Filename -Raw | ConvertFrom-StringData).$Propertyname
        # trim quotes
        $value = $value -replace "'", "" -replace '"', ''
        return $value
    }

    End {
        # eleave($MyInvocation.MyCommand)
    }
}

<#
.SYNOPSIS
Add a custom path to the PSModulePath environment variable.
 
.DESCRIPTION
Add a custom path to the PSModulePath environment variable.
 
.PARAMETER Path
Path to add
 
.PARAMETER First
If specified, add the Path at the beginning of PSModulePath
 
.EXAMPLE
Add-PSModulePath -Path c:\MyProject\MyModules
 
.EXAMPLE
"C:\MyProject\MyModules" | Add-PSModulePath -First
 
.NOTES
General notes
 
.LINK
https://gitlab.com/pwsh.fw/pwsh.fw.core
 
#>

function Add-PSModulePath {
    [CmdletBinding()]Param (
        [Parameter(Mandatory,ValueFromPipeLine = $true)][string]$Path,
        [switch]$First
    )
    Begin {
        # eenter($Script:NS + '\' + $MyInvocation.MyCommand)
    }

    Process {
        try {
            $current = [Environment]::GetEnvironmentVariable('PSModulePath')
            if ($First) {
                $newpath = $PATH + [IO.Path]::PathSeparator + $env:PSModulePath
            } else {
                $newpath = $env:PSModulePath + [IO.Path]::PathSeparator + $PATH
            }
            [Environment]::SetEnvironmentVariable('PSModulePath', $newpath)
            Write-Devel "PSModulePath = $([Environment]::GetEnvironmentVariable('PSModulePath'))"
        } catch {
            ewarn($_.Exception.ItemName + ": " + $_.Exception.Message)
        }
        # edevel("env:PSModulePath = " + $env:PSModulePath)
    }

    End {
        # eleave($Script:NS + '\' + $MyInvocation.MyCommand)
    }
}

<#
    .SYNOPSIS
    Convert a config file in a Hashtable of "key = value" pair
 
    .DESCRIPTION
    At this time, supported configuration files are
    .yml containing a list of "key: value" in the YAML language
    .txt containing a list of "key = value" pair (can be .conf or .whatever)
 
    .PARAMETER File
    Complete path and filename of a file containing key=value pairs
 
    .EXAMPLE
    $conf = ConvertFrom-ConfigFile "./config.conf"
    This example will load all "key = value" pair into the $conf object
 
#>

function ConvertFrom-ConfigFile {
    [CmdletBinding()][OutputType([Hashtable])]Param (
        [Parameter(Mandatory = $true,ValueFromPipeLine = $true)][string]$File
    )
    Begin {
    }

    Process {
        if (-not(fileExist $File)) {
            eerror("Config file '" + $File + "' not found.")
            return @{}
        }
        $item = Get-Item $File
        switch ($item.extension) {
            '.yaml' {
                $conf = Get-Content $File -Raw | ConvertFrom-Yaml
            }
            '.yml' {
                $conf = Get-Content $File -Raw | ConvertFrom-Yaml
            }
            default {
                $conf = Get-Content $File | ConvertFrom-StringData
                if ($conf.Keys.Count -gt 0) {
                    $conf = $conf.Keys | ForEach-Object { $c = @{} } { $c[$_] = $conf.$_.Trim('"') } { $c }
                } else {
                    $conf = @{}
                }
            }
        }

        return $conf
    }

    End {
    }
}

<#
.SYNOPSIS
Helper function for argument completer
 
.DESCRIPTION
Get valid values from a path to use with ArgumentCompleter object
 
.PARAMETER Path
Path to search in
 
.PARAMETER Filter
Optional filter to pass to Get-ChildItem
 
.EXAMPLE
[ArgumentCompleter(
    {
        param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams)
        Get-ValidValuesFromPath -Path "/my/path" | Where-Object { $_ -like "$wordToComplete*" }
    }
)]
[ValidateScript(
    {
        $_ -in (Get-ValidValuesFromPath -Path "/my/path")
    }
)]
 
.NOTES
General notes
#>

function Get-ValidValuesFromPath {
    [CmdletBinding()]
    param($Path, $Filter)

    (Get-ChildItem -Path $Path -File -Filter $Filter).Name
}

Set-Alias -Force -Name eexec        -Value Execute-Command
Set-Alias -Force -Name fileExist    -Value Test-FileExist
Set-Alias -Force -Name dirExist        -Value Test-DirExist
Set-Alias -Force -Name regKeyExist    -Value Test-RegKeyExist
Set-Alias -Force -Name regValueExist    -Value Test-RegValueExist
Set-Alias -Force -Name varExist        -Value Test-Variable

# Export-ModuleMember -Function * -Alias *