Search code examples
arrayspowershellhashtable

Powershell - Write Hash table to file with an Array


I would like to write out a hash table to a file with an array as one of the hash table items. My array item is written out, but it contains files=System.Object[]

Note - Once this works, I will want to reverse the process and read the hash table back in again.

clear-host
$resumeFile="c:\users\paul\resume.log"
$files = Get-ChildItem *.txt
$files.GetType()
write-host
$types="txt"
$in="c:\users\paul"

Remove-Item $resumeFile -ErrorAction SilentlyContinue
$resumeParms=@{}
$resumeParms['types']=$types
$resumeParms['in']=($in)
$resumeParms['files']=($files)
$resumeParms.GetEnumerator() | ForEach-Object {"{0}={1}" -f $_.Name,$_.Value} | Set-Content $resumeFile
write-host "Contents of $resumefile"
get-content $resumeFile

Results

IsPublic IsSerial Name                                     BaseType                                                      
-------- -------- ----                                     --------                                                      
True     True     Object[]                                 System.Array                                                  

Contents of c:\users\paul\resume.log
files=System.Object[]
types=txt
in=c:\users\paul

Solution

  • The immediate fix is to create your own array representation, by enumerating the elements and separating them with ,, enclosing string values in '...':

    # Sample input hashtable. [ordered] preserves the entry order.
    $resumeParms = [ordered] @{ foo = 42; bar = 'baz'; arr = (Get-ChildItem *.txt) }
    
    $resumeParms.GetEnumerator() |
      ForEach-Object { 
        "{0}={1}" -f $_.Name, (
          $_.Value.ForEach({ 
           (("'{0}'" -f ($_ -replace "'", "''")), $_)[$_.GetType().IsPrimitive] 
          }) -join ','
        )
      }
    

    Not that this represents all non-primitive .NET types as strings, by their .ToString() representation, which may or may not be good enough.

    The above outputs something like:

    foo=42
    bar='baz'
    arr='C:\Users\jdoe\file1.txt','C:\Users\jdoe\file2.txt','C:\Users\jdoe\file3.txt'
    

    See the bottom section for a variation that creates a *.psd1 file that can later be read back into a hashtable instance with Import-PowerShellDataFile.


    Alternatives for saving settings / configuration data in text files:

    • If you don't mind taking on a dependency on a third-party module:

      • Consider using the PSIni module, which uses the Windows initialization file (*.ini) file format; see this answer for a usage example.

        • Adding support for initialization files to PowerShell itself (not present as of 7.0) is being proposed in GitHub issue #9035.
      • Consider using YAML as the file format; e.g., via the FXPSYaml module.

        • Adding support for YAML files to PowerShell itself (not present as of 7.0) is being proposed in GitHub issue #3607.
    • The Configuration module provides commands to write to and read from *.psd1 files, based on persisted PowerShell hashtable literals, as you would declare them in source code.

      • Alternatively, you could modify the output format in the code at the top to produce such files yourself, which allows you to read them back in via
        Import-PowerShellDataFile, as shown in the bottom section.

      • As of PowerShell 7.0 there's no built-in support for writing such as representation; that is, there is no complementary Export-PowerShellDataFile cmdlet. However, adding this ability is being proposed in GitHub issue #11300.

    • If creating a (mostly) plain-text file is not a must:

    • The solution that provides the most flexibility with respect to the data types it supports is the XML-based CLIXML format that Export-Clixml creates, as Lee Dailey suggests, whose output can later be read with Import-Clixml.
      However, this format too has limitations with respect to type fidelity, as explained in this answer.

    • Saving a JSON representation of the data, as Lee also suggests, via ConvertTo-Json / ConvertFrom-Json, is another option, which makes for human-friendlier output than XML, but is still not as friendly as a plain-text representation; notably, all \ chars. in file paths must be escaped as \\ in JSON.


    Writing a *.psd1 file that can be read with Import-PowerShellDataFile

    Within the stated constraints regarding data types - in essence, anything that isn't a number or a string becomes a string - it is fairly easy to modify the code at the top to write a PowerShell hashtable-literal representation to a *.psd1 file so that it can be read back in as a [hashtable] instance via Import-PowerShellDataFile:

    As noted, if you don't mind installing a module, consider the Configuration module, which has this functionality built int.

    # Sample input hashtable.
    $resumeParms = [ordered] @{ foo = 42; bar = 'baz'; arr = (Get-ChildItem *.txt) }
    
    # Create a hashtable-literal representation and save it to file settings.psd1
    @"
    @{
    $(
      ($resumeParms.GetEnumerator() |
        ForEach-Object { 
          "  {0}={1}" -f $_.Name, (
            $_.Value.ForEach({ 
              (("'{0}'" -f ($_ -replace "'", "''")), $_)[$_.GetType().IsPrimitive] 
             }) -join ','
          )
        }
      ) -join "`n"
    )
    }
    "@ > settings.psd1
    

    If you read settings.psd1 with Import-PowerShellDataFile settings.psd1 later, you'll get a [hashtable] instance whose entries you an access as usual and which produces the following display output:

    Name                           Value
    ----                           -----
    bar                            baz
    arr                            {C:\Users\jdoe\file1.txt, C:\Users\jdoe\file1.txt, C:\Users\jdoe\file1.txt}
    foo                            42
    

    Note how the order of entries (keys) was not preserved, because hashtable entries are inherently unordered.

    On writing the *.psd1 file you can preserve the key(-creation) order by declaring the input hashtable (System.Collections.Hashtable) as [ordered], as shown above (which creates a System.Collections.Specialized.OrderedDictionary instance), but the order is, unfortunately, lost on reading the *.psd1 file.

    As of PowerShell 7.0, even if you place [ordered] before the opening @{ in the *.psd1 file, Import-PowerShellDataFile quietly ignores it and creates an unordered hashtable nonetheless.