I worked on a project recently and saw this ugly side firsthand, through thousands of lines of PowerShell script. Scripting spread out over dozens of files. I fought to learn the pattern of execution, and discovered that making even simple changes had side effects in processes that I didn’t even know were related. To overcome this, my team began using an XML file to maintain the list and order of commands to execute, and then had a simpler, generic PowerShell file to execute everything.
Today, our configuration has grown complex, supporting the installation and configuration of several enterprise software components that need to exist in concert with each other. Even with the complexities in the XML, it is easy to trace an error to the problem step, make corrections, and continue.
In this post we will lay down a foundation for anyone to build upon and organize their own PowerShell execution process cleanly inside of XML.
For the example below, each desired PowerShell command to execute is referred to as a ‘step’. A process can be a few steps, or many.
This PowerShell script contains three helper functions along with the executing body. The first line of the script file is used to establish a parameter for allowing the XML file name/path to be passed in externally, such as via the command-line. Both absolute and relatives paths could be used.
Let’s take a look at the example above. This code can be broken down into four main parts, with further explanation of each piece to come later in this post.
- First, we read in the XML file, defaulting to the “steps.xml” located in the current folder.
- Second, build a list of steps that are going to be executed. This uses the Get-StepCollection function to produce an array of XmlElements that correspond to PowerShell functions.
- Third, evaluate each of those steps using the Expand-StepXml function. The Expand-StepXml function will expand any recognizable PowerShell variables names as well as evaluate any code within the input XML to allow your steps to be dynamic and react to the execution environment.
- And finally, build a command that is then executed using Invoke-Expression.
Before we dig too deeply into our code, let’s pause and look at an example configuration file. This should help put things in perspective. A simple example is shown below:
The root element, Steps, plays a vital role. The Get-StepsCollection function will be looking for all child elements of the “Steps” node. And let’s not forget that XML is case sensitive.
Each child element represents a command that can be executed within PowerShell. Our example begins with the setting of some variables (both pre-existing and creating new). Next, some of those variable names are embedded into the XML, and the result is a string that is displayed to the host (This shows a possible although not particularly practical use of the Expand-String functionality). We follow that by importing another script file containing additional functions for execution, then execute one of those functions. Our final step is launching notepad and pausing script execution until it is closed.
The Get-StepCollection function takes a file name (relative or absolute) of an XML configuration file. It then iterates over each XmlNode in the file (skipping comments) and makes an array of elements that will be executed. Each element represents one step.
The Expand-StepXml function converts an XmlElement into a string, invokes the ExpandString function against it, converts the modified string back to an XmlElement, and returns it to the caller. Using our example configuration from above the element “<Write-Host>$Hello $World!</Write-Host>” will be translated to “Hello World!” or whatever value the variable Hello and World represent.
Note: In PowerShell versions prior to major version 3.0, there was a defect with the ExpandString function. To understand more on working around this issue, read Expanding Variable Strings in PowerShell.
The Build-Command function takes the XmlElement and builds a string representation of the step to execute.
The PowerShell command name is derived from the name of the XmlElement node. Any child content of the XmlElement is flattened into a single string (the InnerXml property) and appended immediately after the command name. Attributes of the XmlElement represent parameters of the command. The attribute name is synonymous with the parameter name, as is the attribute value the parameter value.
Upon executing our simple example, we see the following in a PowerShell console:
In practice, my project team has built upon this foundation. We have added registry tracking for current steps and support rebooting and continuation of an execution process. We have moved many of the custom functions into a custom PowerShell SnapIn that is loaded at the start of the script. We added logging in order to leave the script unattended and return at a later time to review the results.
Thank you to Andrew Bassett for assisting with the authoring of this post.