2

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.

1
  • 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? Commented Oct 25, 2017 at 12:54

2 Answers 2

5

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?

Sign up to request clarification or add additional context in comments.

5 Comments

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?
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 for some more explanation on this.
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.
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: stackoverflow.com/a/17647824/18771
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
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.

3 Comments

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.
See here: stackoverflow.com/questions/10655788/… for details on how Out-File and Set-Content behave and handle file encoding.
@Mark Wragg thanks very much. Really appreciated the assistance.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.