Configuration Files for PowerShell Scripts and Modules – Part 1

One of the common sets of questions we get in the Virtual User Group about PowerShell (particularly from authors who are just graduating from scripts to writing their first modules) are about how to store configuration data. There are actually several options in PowerShell, but most of them aren’t optimal for various reasons.

The most obvious solution is the PrivateData block in the module manifest. Added to PowerShell v2, it’s designed for use in modules to store configuration, settings, default values, etc. In your module manifest you can put pretty much anything (as long as it’s valid “RestrictedMode” PowerShell, see about_Language_Modes in the PowerShell help, although you can also use $PSScriptRoot and Join-Path).

The nice thing about this is that the syntax is native PowerShell. The PSD1 file extension is “PowerShell Data” and it is to PowerShell what JSON is to Javascript: a restricted subset of the language written in the normal syntax of the language, with simple restrictions. What makes PrivateData obvious and easy to use is that it’s already parsed for you in your module and available so settings in your PrivateData are immediately available. For example, a function like this will output it’s values:

function Read-Config {
   [CmdletBinding()]param()
   $MyInvocation.MyCommand.Module.PrivateData
}

The only downside of this is that changing the settings (permanently) requires either editing the manifest file by hand (in a text editor), regenerating the manifest (using New-ModuleManifest from inside your module — as I did in the Authenticode module), or reading in the manifest content and parsing it via regex or parser to edit and rewrite it. Any of those will work, but the real problem is that you have to manually generate a string representation of your PrivateData hashtable.

In case you wondered, even for code-signed modules there are no problems with editing or regenerating the module manifest, as long as you don’t change the rest of the manifest. PowerShell does not check the manifest signature, so as long as you keep your PrivateData valid, and don’t change the rest of it, you’re ok.

Some other options:

The most powerful round-trip solution are the Clixml cmdlets. With Import-CliXml and Export-CliXml we have almost total fidelity in serializing objects to disk, and we can easily store any hashtable of settings, and re-import it without the Restricted Mode limitations. Unfortunately, the CliXML format is nearly impossible for a human to read and edit, so using them for configuration is not ideal: you have to give up on letting people edit your settings in a text editor in favor of having them work with the hashtable or custom object inside PowerShell.

The JSON cmdlets which shipped in PowerShell 3 are an easy choice also. While they don’t write to disk, making them do so is simply a matter of using ConvertTo-Json and ConvertFrom-Json together with Get-Content and Set-Content. However, the JSON serializer falls down on simple things like serializing $Pwd (a System.Management.Automation.PathInfo object), and depending on what you store you may be very surprised at how convoluted the JSON gets. You’ll have no problem if you keep yourself to a single top-level hashtable containing other hashtables, arrays, strings and numbers that would be allowed in restricted language mode, however (and it will output human-readable formatted JSON). The only real problem with the JSON serializer is that it is a new syntax. If you ask your users to edit it in a text file, many PowerShell users won’t know the slightly different rules for JavaScript object notation (vs PowerShell Data), and will struggle with the use of colons instead of equals for assignment, square braces for arrays instead of and parentheses, and curly braces without the sign for dictionaries.

What would it take to “fix” PowerShell Data?

As I said before, the only real problem with PSD is that there’s no built-in mechanism round-tripping for it: you can’t easily modify the values in the file from code, because we can’t safely get the modified value back into our module’s manifest. We could fix that, of course, by writing a simple serializer. For the simplest case we want to be able to serialize a hashtable as our settings dictionary, and support round-trip for numbers, strings, arrays and hashtables. Something like this should work:

function ConvertTo-PSData {
   [CmdletBinding()]
   param(
      [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0)]
      $InputObject
   )
   # $t is our indent
   begin { $t = "  " }
   process {
      if($InputObject -is [Int16] -or
         $InputObject -is [Int32] -or
         $InputObject -is [Byte]) {
         "{0:G}" -f $InputObject
      }
      elseif($InputObject -is [Double])  {
         "{0:G}" -f $InputObject -replace '^(\d+)$','.0'
      }
      elseif($InputObject -is [Int64])  {
         "{0:G}l" -f $InputObject
      }
      elseif($InputObject -is [Decimal])  {
         "{0:G}d" -f $InputObject
      }
      elseif($InputObject -is [bool])  {
         if($InputObject) { '$True' } else { '$False' }
      }      
      elseif($InputObject -is [String]) {
         "'{0}'" -f $InputObject
      }      
      elseif($InputObject -is [System.Collections.IDictionary]) {
         "@{{`n$t{0}`n}}" -f ($(
         ForEach($key in @($InputObject.Keys)) {
            if("$key" -match '^(\w+|-?\d+\.?\d*)$') {
               "$key = " + (ConvertTo-PSData $InputObject.($key))
            }
            else {
               "'$key' = " + (ConvertTo-PSData $InputObject.($key))
            }
         }) -split "`n" -join "`n$t")
      }
      elseif($InputObject -is [System.Collections.IEnumerable]) {
         "@($($(ForEach($item in @($InputObject)) { ConvertTo-PSData $item }) -join ','))"
      }
      else {
         "'{0}'" -f $InputObject
      }
   }
}

What I would recommend is that if you need to change your settings in your module, you create two functions: the Read-Config from earlier, and a new Write-Config which would take the hashtable as the parameter, and would have the values you want in your manifest hard-coded in a call to New-ModuleManifest. You can build the path for your manifest, and don’t forget to read the name, version, and GUID from the current values so you don’t make it hard to rename your module. Something like this:

function Write-Config {
   param([Hashtable]$Config)

   $Name = $ExecutionContext.SessionState.Module.Name
   $Guid = $ExecutionContext.SessionState.Module.Guid
   $Version = $ExecutionContext.SessionState.Module.Version
   $Path = Join-Path $PSSCriptRoot "${Name}.psd1"

   $Properties = @{
      ModuleVersion =  $Version
      Guid = $Guid
      ModuleToProcess = "${Name}.psm1"
      Author = 'Joel Bennett'
      Company = 'HuddledMasses.org'
      PowerShellVersion = '2.0'
      Copyright = '(c) 2014, Joel Bennett'
      Description = 'A special module for something'
      FileList = "${Name}.psm1","${Name}.psd1"
      FunctionsToExport = "*"
      CmdletsToExport = @()
      VariablesToExport = @()
      AliasesToExport = @()
      PrivateData = $Config | ConvertTo-PSData
   }
   New-ModuleManifest $Path @Properties
}

Further thoughts

I’ve got some code to take this further, which I’ll post in Part 2 of series, but in the meantime, I’m interested in hearing from any PowerShell authors who’ve worked on modules, if you’ve done any configuration, whether in PrivateData, or some other way, whether you think your way is better than this or not, and whether you think this method will work for you or not. Really, any feedback at all ;)

Similar Posts:

    None Found

4 thoughts on “Configuration Files for PowerShell Scripts and Modules – Part 1”

  1. So, any reason you can’t just use a simple hashtable in a .ps1 file? I’ve never needed anything else for configuration management.

    1. The simple answer is that what you’re describing is a PSD1. The only upside of putting a hashtable in a .ps1 versus a .psd1 file is that you can dot-source the .ps1 file. On the upside .psd1 files have a restricted (safe) subset of the language, and their code-signatures are not checked, so in a locked-down “AllSigned” environment, you can edit a .psd1 without breaking it, but not a .ps1 …

      The more complicated answer is that the main reason we have settings is that we don’t want to ask “random users” to edit the module. I want to make it hard to break the module accidentally (which is why I don’t like having the settings in the manifest, incidentally), and I want to be able to change settings from cmdlets in the module.

      Even in an all-signed environment where we’re keeping the code in C:\Program Files\WindowsPowerShell\Modules, I can put a settings file in a writable location (I’ll explain more about that in a future post), and as long as I can read the settings without crashing if they’re not perfect, I don’t have to worry about the users breaking things, and we don’t have to teach them to sign code just so they can change settings. But the .ps1 file would break that because it would have to be signed (unless I was going to get-content and invoke-expression … but let’s not go there for now).

  2. One issue that arise with this concept is if the module is installed in a global location, like c:\program files\WindowsPowerShell\Modules.

    That is not a good location to write to.
    Wouldn’t ProgramData be a better place?

Comments are closed.