1

I'm attempting to remove the defining of variables from a script and read them in from an XML configuration file similar to the below:

XML File

<?xml version="1.0" encoding="utf-8" ?>
<settings>
    <process>FALSE</process>
    <xmlDir>\\serv1\dev</xmlDir>
    <scanDir>\\serv1\dev</scanDir>
    <processedDir>\\serv1\dev\done</processedDir>
    <errorDir>\\serv1\dev\err</errorDir>
    <log>\\serv1\dev\log\dev-Log##DATE##.log</log>
    <retryDelay>5</retryDelay>
    <retryLimit>3</retryLimit>
</settings>

Then parse the XML in the script with the below:

[xml]$configFile = Get-Content $PSScriptRoot\$confFile
$settings = $configFile.settings.ChildNodes
foreach ($setting in $settings) {  
    New-Variable -Name $setting.LocalName -Value ($setting.InnerText -replace '##DATE##',(get-date -f yyyy-MM-dd)) -Force
}

This works great but the problem is that they are all read as a string but some I require as an integer. To get around this issue I'm having to change them to integer after the variables have been created as below:

$retryDelay = ([int]$retryDelay)
$retryLimit = ([int]$retryLimit)

Although this works, I'd like to have other variables in the XML such as boolean $true / $false (and read in as a boolean) and would rather have the foreach be able to handle their types rather than additional lines in the script. Any clues appreciated.

Rob Berry
  • 185
  • 1
  • 4
  • 14
  • 2
    XML has no type information unless you add it. Knowing nothing else, every element and attribute value is a string. Maybe you want to use JSON as your config file format? – Tomalak Oct 25 '17 at 12:54

2 Answers2

4

Firstly, never read XML files like this. This breaks the encoding detection that is built into XML parsers and will result in mangling your data sooner or later.

# BAD, DO NOT USE
[xml]$configFile = Get-Content $PSScriptRoot\$confFile

Reading XML files properly works like this - create a new XML object and let it handle the file loading:

$configFile = New-Object xml
$configFile.Load("$PSScriptRoot\$confFile")

Secondly, I strongly advise against creating global variables from a file. This is bad style as it can easily break your program by blindly overriding existing variables. Use a hash to store the values from the file, or simply use the XML file directly as your config.

$config = @{}

foreach ($setting in $configFile.SelectNodes("/settings/*") ) {
    $config[$setting.Name] = $setting.InnerText
}

Thirdly, XML has no inherent data type information. Everything is a string until you add more info about it. One way could be a type attribute (type="string" can be seen as default):

<settings>
    <process type="boolean">FALSE</process>
    <xmlDir type="string">\\serv1\dev</xmlDir>
    <scanDir type="string">\\serv1\dev</scanDir>
    <processedDir type="string">\\serv1\dev\done</processedDir>
    <errorDir type="string">\\serv1\dev\err</errorDir>
    <log type="string">\\serv1\dev\log\dev-Log##DATE##.log</log>
    <retryDelay type="int">5</retryDelay>
    <retryLimit type="int">3</retryLimit>
</settings>

Of course the type attribute means nothing in and of itself. You need to write the code that pays attention to these attributes and does the necessary type conversions (if ($setting.type -eq "boolean") { ... } etc).

Fourthly, I believe you will be much better-off with simply using JSON as your config file format. It's easier to edit and it has inherent data type information.

{
    "settings": {
        "process": false,
        "xmlDir": "\\\\serv1\\dev",
        "scanDir": "\\\\serv1\\dev",
        "processedDir": "\\\\serv1\\dev\\done",
        "errorDir": "\\\\serv1\\dev\\err",
        "log": "\\\\serv1\dev\\log\\dev-Log##DATE##.log",
        "retryDelay": 5,
        "retryLimit": 3
    }
}

Use the ConvertFrom-JSON cmdlet to parse the data. Use Get-Content -Encoding UTF8 to read it.

Using the Encoding parameter is important when dealing with text files, also when you write a file with Set-Content or Out-File. There is no hidden magic that does the right thing here, you must be explicit about the encoding.

Here is some more in-depth information about the behavior of Out-File and Set-Content. Powershell set-content and out-file what is the difference?

Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • Thanks for the info @Tomalak, much appreciated! I think it's probably easier to use JSON as you suggest. I presume I can iterate through the JSON using foreach, similar to how I parse the XML? – Rob Berry Oct 26 '17 at 08:09
  • Well. If you structure the JSON like shown, iteration would have to be based on `$config.settings | Get-Member -Type NoteProperty | ...`. Also see [this earlier answer of mine](https://stackoverflow.com/a/33521853/18771) for some more explanation on this. – Tomalak Oct 26 '17 at 09:05
  • **BUT**, as I said, I am not a fan of the whole idea of using a loop to fill variables from a config file. Your config file contains your settings. Your program knows what settings to expect, what names they have and what default values they have. You can use the `$config` variable as-is in your program, there is no need to transfer its contents anyplace else. Just use `$retryLimit = $config.settings.retryLimit` or juse `$config.settings.retryLimit` directly when you need it. – Tomalak Oct 26 '17 at 09:05
  • Now if you want to handle the case where `$config.settings.retryLimit` might be unset, there is a way to do that, too. In JavaScript your would say `config.settings.retryLimit || 3` to make it default to 3 in case it is undefined. Just lile in Javascript, it's not an error to access a property that is unset in Powershell. You would just get `$null`. The construct to set it the default to 3 in Powershell would be `($config.settings.retryLimit, 3 -ne $null)[0]` and the explanation why that is so is here: https://stackoverflow.com/a/17647824/18771 – Tomalak Oct 26 '17 at 09:08
  • 1
    In the hope that someone finds this - if you're trying to inspect windows packet filtering platform firewall configuration exports - generated by `netsh wfp show filters` - you *need* to use this technique and not `get-content` – olamotte Dec 15 '20 at 16:35
1

I agree with Tomalak's answer, JSON is probably better for your use case. Here's a practical example to show you how you might use it. This is using a Custom Object created from a hashtable from which to generate the JSON and save it to a file:

$Config = [pscustomobject]@{
    Process = $false
    xmldir = '\\serv1\dev'
    scanDir = '\\serv1\dev'
    processedDir = '\\serv1\dev\done'
    errorDir = '\\serv1\dev\err'
    log = '\\serv1\dev\log\dev-Log##DATE##.log'
    retryDelay = 5
    retryLimit = 3
}

$Config | ConvertTo-Json | Out-File .\config.txt -Encoding UTF8

This creates JSON that looks like this:

{
    "Process":  false,
    "xmldir":  "\\\\serv1\\dev",
    "scanDir":  "\\\\serv1\\dev",
    "processedDir":  "\\\\serv1\\dev\\done",
    "errorDir":  "\\\\serv1\\dev\\err",
    "log":  "\\\\serv1\\dev\\log\\dev-Log##DATE##.log",
    "retryDelay":  5,
    "retryLimit":  3
}

And can be read like this:

$Settings = Get-Content .\config.txt -Encoding UTF8 | ConvertFrom-Json

Because of the way you can see that JSON is storing the variables, PowerShell does a better job of correctly typing them when they are read back in.

Mark Wragg
  • 22,105
  • 7
  • 39
  • 68
  • 1
    `Get-Content` does not neither default to UTF-8 not has it any file encoding detection magic, but that's the underlying assumption for many people. Always use an explicit `Encoding` setting when reading/writing text files. – Tomalak Oct 25 '17 at 13:34
  • See here: https://stackoverflow.com/questions/10655788/powershell-set-content-and-out-file-what-is-the-difference for details on how `Out-File` and `Set-Content` behave and handle file encoding. – Tomalak Oct 25 '17 at 13:48
  • 1
    @Mark Wragg thanks very much. Really appreciated the assistance. – Rob Berry Oct 26 '17 at 08:10