functions/hashtableTools.ps1


Function Convert-HashtableString {
    [cmdletbinding()]
    [OutputType([System.Collections.Hashtable])]

    Param(
        [parameter(Mandatory, HelpMessage = "Enter your hashtable string", ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Text
    )

    Begin {
        Write-Verbose "[BEGIN ] Starting: $($MyInvocation.MyCommand)"
    } #begin

    Process {

        $tokens = $null
        $err = $null
        $ast = [System.Management.Automation.Language.Parser]::ParseInput($Text, [ref]$tokens, [ref]$err)
        $data = $ast.find( { $args[0] -is [System.Management.Automation.Language.HashtableAst] }, $true)

        if ($err) {
            Throw $err
        }
        else {
            $data.SafeGetValue()
        }
    }

    End {
        Write-Verbose "[END ] Ending: $($MyInvocation.MyCommand)"
    } #end

}

Function ConvertTo-Hashtable {

    [cmdletbinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    [OutputType([System.Collections.Hashtable])]

    Param(
        [Parameter(
            Position = 0,
            Mandatory,
            HelpMessage = "Please specify an object",
            ValueFromPipeline
        )]
        [ValidateNotNullOrEmpty()]
        [object]$InputObject,
        [switch]$NoEmpty,
        [string[]]$Exclude,
        [switch]$Alphabetical,
        [Parameter(HelpMessage = "Create an ordered hashtable instead of a plain hashtable.")]
        [switch]$Ordered
    )

    Process {
        <#
            get type using the [Type] class because deserialized objects won't have
            a GetType() method which is what I would normally use.
        #>


        $TypeName = [system.type]::GetTypeArray($InputObject).name
        Write-Verbose "Converting an object of type $TypeName"

        #get property names using Get-Member
        $names = $InputObject | Get-Member -MemberType properties |
        Select-Object -ExpandProperty name

        if ($Alphabetical) {
            Write-Verbose "Sort property names alphabetically"
            $names = $names | Sort-Object
        }

        #define an empty hash table
        if ($Ordered) {
            Write-Verbose "Creating an ordered hashtable"
            $hash = [ordered]@{ }
        }
        else {
            $hash = @{ }
        }

        #go through the list of names and add each property and value to the hash table
        $names | ForEach-Object {
            #only add properties that haven't been excluded
            if ($Exclude -NotContains $_) {
                #only add if -NoEmpty is not called and property has a value
                if ($NoEmpty -AND -Not ($InputObject.$_)) {
                    Write-Verbose "Skipping $_ as empty"
                }
                else {
                    Write-Verbose "Adding property $_"
                    $hash.Add($_, $InputObject.$_)
                }
            } #if exclude notcontains
            else {
                Write-Verbose "Excluding $_"
            }
        } #foreach
        Write-Verbose "Writing the result to the pipeline"
        Write-Output $hash
    }#close process

}#end function

Function Convert-HashtableToCode {
    [cmdletbinding(DefaultParameterSetName = "psd1")]
    [alias("chc")]
    [OutputType([System.String])]

    Param(
        [Parameter(Position = 0, ValueFromPipeline, Mandatory)]
        [ValidateNotNullOrEmpty()]
        [hashtable]$Hashtable,

        [Parameter(ParameterSetName = "psd1")]
        [Alias("tab")]
        [int]$Indent = 1,

        [Parameter(ParameterSetName = "inline", HelpMessage = "Write the hashtable as an inline expression")]
        [switch]$Inline
    )

    Begin {
        Write-Verbose "Starting $($MyInvocation.MyCommand)"
        if ($Inline) {
            Write-Verbose "Creating an inline expression"
        }
    }
    Process {
        Write-Verbose "Processing a hashtable with $($hashtable.keys.count) keys"

        $hashtable.GetEnumerator() | ForEach-Object -begin {

            [string]$out = "@{"
            if ($PSCmdlet.ParameterSetName -eq 'psd1') {
                $out += "`n"
            }

        }  -Process {
            Write-Verbose "Testing value type $($_.value.gettype().name) for key $($_.key)"
            #determine if the value needs to be enclosed in quotes
            if ($_.value.gettype().name -match "Int|double") {
                Write-Verbose "..is a numeric"
                $value = $_.value
            }
            elseif ($_.value -is [array]) {
                #assuming all the members of the array are of the same type
                Write-Verbose "..is an array"
                #test if an array of numbers otherwise treat as strings
                if ($_.value[0].Gettype().name -match "int|double") {
                    $value = "@($($_.value -join ','))"
                }
                elseif ($_.value[0].GetType().name -eq "Hashtable") {
                    #10/2/2020 JDH need to process nested hashtables in an array (Issue #91)
                    if ($inline) {
                        $value =  "@($(($_.value | Convert-HashtableToCode -inline).trim() -join ","))"
                    }
                    else {
                        #format nested hashtables with @() Issue #91
                        $tables = Foreach ($t in $_.value) {
                            $in = "`t"*$($indent+1)
                            "{0}{1}" -f $in,(Convert-HashTableToCode -Indent $($indent+2) -Hashtable $t).trimend()
                        }
                        $joined = ($tables -join ",`n").TrimEnd()
                        $close = "`t"*$indent
                        $value = "@(`n$joined`n$close)".trimEnd()
                    }
                }
                else {
                    $value = "@($("'{0}'" -f ($_.value -join "','")))"
                }
            } #arrays
            elseif ($_.value -is [hashtable]) {
                Write-Verbose "Creating nested entry"
                #10/2/2020 JDH convert hashtables using current values
                if ($inline) {
                    $nested = Convert-HashtableToCode $_.value -inline
                }
                else {
                    $nested = Convert-HashTableToCode $_.value -Indent $($indent + 1)
                }
                $value = "$($nested)".trimEnd()
            }
            elseif ($_.value -is [scriptblock]) {
                Write-Verbose "Parsing scriptblock"
                $value = "{$($_.value)}"
            }
            else {
                Write-Verbose "..defaulting as a string"
                $value = "'$($_.value)'"
            }

            if ($inline) {
                $out += "$($_.key) = $value;"
            }
            else {
                $tabcount = "`t" * $Indent
                $out += "$tabcount$($_.key) = $value `n"
            }
        }  -end {
            if ($inline) {
                #strip off the last ;
                $out = $out.remove($out.Length - 1)
                $out += "}"
            }
            else {
                $tabcount = "`t" * ($Indent - 1)
                $out += "$tabcount}`n"
            }
            $out
        }

    } #process
    End {
        Write-Verbose "Ending $($MyInvocation.MyCommand)"
    }
} #end function
Function Join-Hashtable {
    [cmdletbinding()]
    [OutputType([System.Collections.Hashtable])]
    Param (
        [hashtable]$First,
        [hashtable]$Second,
        [switch]$Force
    )

    #create clones of hashtables so originals are not modified
    $Primary = $First.Clone()
    $Secondary = $Second.Clone()

    #check for any duplicate keys
    $duplicates = $Primary.keys | Where-Object { $Secondary.ContainsKey($_) }
    if ($duplicates) {
        foreach ($item in $duplicates) {
            if ($force) {
                #force primary key, so remove secondary conflict
                $Secondary.Remove($item)
            }
            else {
                Write-Host "Duplicate key $item" -ForegroundColor Yellow
                Write-Host "A $($Primary.Item($item))" -ForegroundColor Yellow
                Write-Host "B $($Secondary.Item($item))" -ForegroundColor Yellow
                $r = Read-Host "Which key do you want to KEEP [AB]?"
                if ($r -eq "A") {
                    $Secondary.Remove($item)
                }
                elseif ($r -eq "B") {
                    $Primary.Remove($item)
                }
                Else {
                    Write-Warning "Aborting operation"
                    Return
                }
            } #else prompt
        }
    }

    #join the two hash tables
    $Primary + $Secondary

} #end Join-Hashtable

Function Convert-CommandToHashtable {
    [cmdletbinding()]
    [OutputType("[System.String]")]

    Param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        #"Enter a PowerShell expression with full parameter names"
        [string]$Text
    )

    Set-StrictMode -Version latest

    New-Variable astTokens -force
    New-Variable astErr -force

    #trim spaces
    $Text = $Text.trim()
    Write-Verbose "Converting $text"

    $ast = [System.Management.Automation.Language.Parser]::ParseInput($Text, [ref]$astTokens, [ref]$astErr)

    #resolve the command name
    $cmdType = Get-Command $asttokens[0].text
    if ($cmdType.CommandType -eq 'Alias') {
        $cmd = $cmdType.ResolvedCommandName
    }
    else {
        $cmd = $cmdType.Name
    }

    #last item is end of input token
    $r = for ($i = 1; $i -lt $astTokens.count - 1 ; $i++) {
        if ($astTokens[$i].ParameterName) {
            $p = $astTokens[$i].ParameterName
            $v = ""
            #check next token
            if ($astTokens[$i + 1].Kind -match 'Parameter|EndOfInput') {
                #the parameter must be a switch
                $v = "`$True"
            }
            else {
                While ($astTokens[$i + 1].Kind -notmatch 'Parameter|EndOfInput') {
                    $i++
                    #test if value is a string and if it is quoted, if not include quotes
                    #if ($astTokens[$i].Kind -eq "Identifier" -AND $astTokens[$i].Text -notmatch """\w+.*""" -AND $astTokens[$i].Text -notmatch "'\w+.*'") {
                    if ($astTokens[$i].Text -match "\D" -AND $astTokens[$i].Text -notmatch """\w+.*""" -AND $astTokens[$i].Text -notmatch "'\w+.*'") {
                        #ignore commas and variables
                        if ($astTokens[$i].Kind -match 'Comma|Variable') {
                            $value = $astTokens[$i].Text
                        }
                        else {
                            #Assume text and quote it
                            $value = """$($astTokens[$i].Text)"""
                        }
                    }
                    else {
                        $value = $astTokens[$i].Text
                    }
                    $v += $value
                } #while
            }
            #don't add a line return if this is going to be the last item
            if ($i + 1 -ge $astTokens.count - 1) {
                " $p = $v"
            }
            else {
                " $p = $v`n"
            }
        } #if ast parameter name

    } #for

    $hashtext = @"
`$paramHash = @{
$r
}
 
$cmd @paramHash
"@


    $hashtext


}

Function Rename-Hashtable {

    [cmdletbinding(SupportsShouldProcess, DefaultParameterSetName = "Pipeline")]
    [alias("rht")]

    Param(
        [parameter(
            Position = 0,
            Mandatory,
            HelpMessage = "Enter the name of your hash table variable without the `$",
            ParameterSetName = "Name"
        )]
        [ValidateNotNullOrEmpty()]
        [string]$Name,
        [parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline,
            ParameterSetName = "Pipeline"
        )]
        [ValidateNotNullOrEmpty()]
        [object]$InputObject,
        [parameter(
            Position = 1,
            Mandatory,
            HelpMessage = "Enter the existing key name you want to rename")]
        [ValidateNotNullOrEmpty()]
        [string]$Key,
        [parameter(position = 2, Mandatory, HelpMessage = "Enter the NEW key name"
        )]
        [ValidateNotNullOrEmpty()]
        [string]$NewKey,
        [switch]$PassThru,
        [ValidateSet("Global", "Local", "Script", "Private", 0, 1, 2, 3)]
        [ValidateNotNullOrEmpty()]
        [string]$Scope = "Global"
    )

    Begin {
        Write-Verbose -Message "Starting $($MyInvocation.MyCommand)"
        Write-Verbose "using parameter set $($PSCmdlet.ParameterSetName)"
    }

    Process {
        Write-Verbose "PSBoundParameters"
        Write-Verbose $($PSBoundParameters | Out-String)
        #validate Key and NewKey are not the same
        if ($key -eq $NewKey) {
            Write-Warning "The values you specified for -Key and -NewKey appear to be the same. Names are NOT case-sensitive"
            #bail out
            Return
        }

        Try {
            #validate variable is a hash table
            if ($InputObject) {
                #create a completely random name to avoid any possible naming collisions
                $name = [system.io.path]::GetRandomFileName()
                Write-Verbose "Creating temporary hashtable ($name) from pipeline input"
                Set-Variable -Name $name -Scope $scope -value $InputObject -WhatIf:$False
                $PassThru = $True
            }
            else {
                Write-Verbose "Using hashtable variable $name"
            }

            Write-Verbose (Get-Variable -Name $name -Scope $scope | Out-String)
            Write-Verbose "Validating $name as a hashtable in $Scope scope."
            #get the variable
            $var = Get-Variable -Name $name -Scope $Scope -ErrorAction Stop
            Write-Verbose "Detected a $($var.value.GetType().fullname)"

            Write-Verbose "Testing for key $key"
            if (-Not $var.value.Contains($key)) {
                Write-Warning "Failed to find the key $key in the hashtable."
                #bail out
                Return
            }
            if ( $var.Value -is [hashtable]) {
                #create a temporary copy

                Write-Verbose "Cloning a temporary hashtable"
                <#
                Use the clone method to create a separate copy.
                If you just assign the value to $temphash, the
                two hash tables are linked in memory so changes
                to $tempHash are also applied to the original
                object.
                #>

                $tempHash = $var.Value.Clone()

                if ($PSCmdlet.ShouldProcess($NewKey, "Replace key $key")) {
                    Write-Verbose "Writing the new hashtable to variable named $hashname"
                    #create a key with the new name using the value from the old key
                    Write-Verbose "Adding new key $newKey to the temporary hashtable"
                    $tempHash.Add($NewKey, $tempHash.$Key)
                    #remove the old key
                    Write-Verbose "Removing $key"
                    $tempHash.Remove($Key)
                    #write the new value to the variable
                    Write-Verbose "Writing the new hashtable to variable named $Name"
                    Write-Verbose ($tempHash | Out-String)
                    Set-Variable -Name $Name -Value $tempHash -Scope $Scope -Force -PassThru:$PassThru |
                    Select-Object -ExpandProperty Value
                }
            }
            elseif ($var.value -is [System.Collections.Specialized.OrderedDictionary]) {
                Write-Verbose "Processing as an ordered dictionary"
                $varHash = $var.value
                #find the index number of the existing key
                $i = -1
                Do {
                    $i++

                } Until (($varHash.GetEnumerator().name)[$i] -eq $Key)

                #save the current value
                $val = $varhash.item($i)

                if ($PSCmdlet.ShouldProcess($NewKey, "Replace key $key at $i")) {
                    #remove at the index number
                    $varhash.RemoveAt($i)
                    #insert the new value at the index number
                    $varhash.Insert($i, $NewKey, $val)
                    Write-Verbose "Writing the new hashtable to variable named $name"
                    Write-Verbose ($varHash | Out-String)
                    Set-Variable -Name $name -Value $varhash -Scope $Scope -Force -PassThru:$PassThru |
                    Select-Object -ExpandProperty Value
                }
            }
            else {
                Write-Warning "The variable $name does not appear to be a hash table or ordered dictionaryBet"
            }
        } #Try

        Catch {
            Write-Warning "Failed to find a variable with a name of $Name. $($_.exception.message)."
        }

        Write-Verbose "Rename complete."
    } #Process

    End {
        #clean up any temporary variables
        if ($InputObject) {
            Write-Verbose "Removing temporary variable $name"
            Remove-Variable -name $Name -Scope $scope -WhatIf:$False
        }
        Write-Verbose -Message "Ending $($MyInvocation.MyCommand)"
    } #end

} #end Rename-Hashtable