XML Manipulation with VBScript

XML manipulation using the WSH is achieved through the Microsoft.XMLDOM object, which comes as standard with everything since NT5 (i.e. XP, 2000, etc).

In case you're unfamiliar with DOM parsing of XML, just think of it as taking an XML document and converting into a tree representation. To use a DOM parser effectively, you need to understand how XML is structured and how nodes relate to one another. However, I'm not going to get into this here. This section assumes you already know enough about XML to get on with the job at hand.

The most important objects are the DOMDocument (the root node) and the IXMLDOMNode (any other node).

The DOMDocument

Properties

Methods for writing XML

Methods for reading XML

The IXMLDOMNode

Properties

Methods for reading

Methods for writing

Sample Scripts to Parse XML

Let's say we want to parse this XML:

<?xml version="1.0" encoding="UTF-8"?>
<MaintenanceConfig>
    <Functions>
        <Function>
            <Name>Database Reindex</Name>
            <Sequence>1</Sequence>
            <RunDays>Monday</RunDays>
            <DependenciesBefore>System_Closedown</DependenciesBefore>
            <DependenciesAfter>System_Startup</DependenciesAfter>
            <Suspended/>
        </Function>
        <Function>
            <Name>System Backup</Name>
            <Sequence>2</Sequence>
            <RunDays>Monday Tuesday Wednesday Thursday Friday Saturday Sunday</RunDays>
            <DependenciesBefore>X1_Closedown</DependenciesBefore>
            <DependenciesAfter>X1_Startup</DependenciesAfter>
            <ForceSuspendToday/>
        </Function>
        <Function>
            <Name>Server Restart</Name>
            <Sequence>3</Sequence>
            <RunDays>Tuesday</RunDays>
            <Parameters>Server1 Server2 Server3</Parameters>
            <ForceRunToday/>
        </Function>
    </Functions>
    <Dependencies>
        <Dependency dependencyID="System_Closedown">
            <Name>System Closedown</Name>
            <Sequence>5</Sequence>
        </Dependency>
        <Dependency dependencyID="X1_Closedown">
            <Name>X1 Closedown</Name>
            <Sequence>10</Sequence>
        </Dependency>
        <Dependency dependencyID="X1_Startup">
            <Name>X1 Startup</Name>
            <Sequence>90</Sequence>
        </Dependency>
        <Dependency dependencyID="System_Startup">
            <Name>System Startup</Name>
            <Sequence>95</Sequence>
        </Dependency>
    </Dependencies>
    <GlobalSuspend>False</GlobalSuspend>
</MaintenanceConfig>

We could parse it with a script that looks like this:

Set args = WScript.Arguments
if (args.length <> 1) then
   Wscript.echo "Bad arguments. Must supply location of XML file."
   Wscript.quit (1)
end if
inputFile = args.item(0)

set xmlDoc = CreateObject("Microsoft.XMLDOM")
xmlDoc.async = false
if xmlDoc.load(inputFile) then
    Wscript.echo "File " & inputFile & " loaded"
else
    Wscript.echo "Could not load file " & inputFile & ". Aborting."
    Wscript.quit (1)
end if
set root = xmlDoc.documentElement
Wscript.echo "Root: " + root.nodeName
Wscript.echo

' Demonstrate use of XPath
xmlDoc.setProperty "SelectionLanguage", "XPath"
set globalSuspendNode = xmlDoc.selectSingleNode("//GlobalSuspend")
WScript.echo "GlobalSuspend: " & globalSuspendNode.text

const ELEMENT_TEXT = 3

' Get all the nodes called 'Function'
WScript.echo "Displaying Functions"
WScript.echo "--------------------"
WScript.echo
set objFunctionList = root.getElementsByTagName("Function")
for each func in objFunctionList
  set functionNodeList = func.childNodes
  for each child in functionNodeList
    if child.hasChildNodes then
      Wscript.echo child.nodeName & ": " & child.firstChild.nodeValue
    else
      Wscript.echo child.nodeName
    end if
  next
  Wscript.echo  ' blank line
next

' Get all the nodes called 'Function'
WScript.echo "Displaying Dependencies"
WScript.echo "-----------------------"
WScript.echo
set objDependencyList = root.getElementsByTagName("Dependency")
for each dependency in objDependencyList
  set attribs = dependency.attributes
  dependencyID = attribs.getNamedItem("dependencyID").nodeValue
  WScript.echo "DependencyID: " & dependencyID

  set dependencyNodeList = dependency.childNodes
  for each child in dependencyNodeList
    if child.hasChildNodes then
      Wscript.echo child.nodeName & ": " & child.firstChild.nodeValue
    else
      Wscript.echo child.nodeName
    end if
  next
  WScript.echo
next

And the output would look like this:

Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

File c:\temp\maintenanceConfig.xml loaded
Root: MaintenanceConfig

GlobalSuspend: False

Displaying Functions
--------------------

Name: Database Reindex
Sequence: 1
RunDays: Monday
DependenciesBefore: System_Closedown
DependenciesAfter: System_Startup
Suspended

Name: System Backup
Sequence: 2
RunDays: Monday Tuesday Wednesday Thursday Friday Saturday Sunday
DependenciesBefore: X1_Closedown
DependenciesAfter: X1_Startup
ForceSuspendToday

Name: Server Restart
Sequence: 3
RunDays: Tuesday
Parameters: System1 System2 System3
ForceRunToday

Displaying Dependencies
-----------------------

DependencyID: System_Closedown
Name: System Closedown
Sequence: 5

DependencyID: X1_Closedown
Name: X1 Closedown
Sequence: 10

DependencyID: X1_Startup
Name: X1 Startup
Sequence: 90

DependencyID: System_Startup
Name: System Startup
Sequence: 95

And here's some alternative code, using XPath and populating a Dictionary object:

Set dictDependencies = CreateObject("Scripting.Dictionary")
dictDependencies.CompareMode = TEXT_MODE

' Get all the nodes called 'Function'
WScript.echo "Displaying Dependencies"
WScript.echo "-----------------------"
WScript.echo

set objDependencyList = root.getElementsByTagName("Dependency")
for each dependency in objDependencyList
  dictDependencies.add _
  		dependency.selectSingleNode("@dependencyID").text, _
  		dependency.selectSingleNode("Sequence").text
next

for each strKey in dictDependencies.keys
  WScript.echo strKey  & ": " & dictDependencies.item(strKey)
next

Sample Scripts for Writing XML

This script generates an XML document from scratch:

Sub GenerateStatusReportingEvent(eventTypeCode, system, msg, completionStatus)
    Dim eventDestPath, filename, destination
    Dim logTime
    Dim statusReportingEventNode
    Dim eventTypeCodeNode, branchNumNode, logTimeNode, _
    	originatingSystemNode, detailsNode, completionStatusNode

    eventDestPath = objShell.ExpandEnvironmentStrings("%instance_data%") _
            & "\projects\POS_Branch_StatusReporting\incomingEvents"

    if not objFso.FolderExists(eventDestPath) then
        LogInfo "ERR  :  " & eventDestPath & " does not exist. Creating...", 1, 1
        CreateFolderHierarchy(eventDestPath)
    end if

    logTime = getDateStr(now())
    filename = logTime & "_" & statusReportingEventCount & ".evt"
    destination = eventDestPath & "\" & filename
    destination = objFSO.GetAbsolutePathName(destination)

    LogInfo "INFO :  Generating event with status " & completionStatus & ": " _
    		& destination, 1, 0

    Dim strXML
    strXML = "<!DOCTYPE StatusReportingEvent SYSTEM 'wrbrMaintenanceEvent.dtd'>"_
                & vbCRLF &_
                "<StatusReportingEvent />"

    set xmlDoc = CreateObject("Microsoft.XMLDOM")
    xmlDoc.validateOnParse = False
    xmlDoc.async = false
    if not (xmlDoc.loadXML(strXML)) then
      LogInfo "ERR  :  Unable to load XML: " & xmlDoc.parseError.reason
      set statusReportingEventNode = _
      		xmlDoc.appendChild(xmlDoc.createElement("StatusReportingEvent"))
      xmlDoc.appendChild(xmlDoc.createProcessingInstruction("xml", "version = ""1.0"""))
    else
      xmlDoc.insertBefore xmlDoc.createProcessingInstruction("xml", "version = ""1.0"""), _
      		xmlDoc.childNodes(0)
      set statusReportingEventNode = xmlDoc.childNodes(2)
    end if

    with statusReportingEventNode
        .appendChild xmlDoc.createTextNode(vbCrLf)
        set eventTypeCodeNode = .appendChild(xmlDoc.createElement("EventTypeCode"))
        .appendChild xmlDoc.createTextNode(vbCrLf)
        set branchNumNode = .appendChild(xmlDoc.createElement("BranchNum"))
        .appendChild xmlDoc.createTextNode(vbCrLf)
        set logTimeNode = .appendChild(xmlDoc.createElement("LogTime"))
        .appendChild xmlDoc.createTextNode(vbCrLf)
        set originatingSystemNode = .appendChild(xmlDoc.createElement("OriginatingSystem"))
        .appendChild xmlDoc.createTextNode(vbCrLf)
        set detailsNode = .appendChild(xmlDoc.createElement("Details"))
        .appendChild xmlDoc.createTextNode(vbCrLf)
        set completionStatusNode = .appendChild(xmlDoc.createElement("CompletionStatus"))
        .appendChild xmlDoc.createTextNode(vbCrLf)
    end with

    eventTypeCodeNode.appendChild(xmlDoc.createTextNode(eventTypeCode))
    branchNumNode.appendChild(xmlDoc.createTextNode(getBranchNum()))
    logTimeNode.appendChild(xmlDoc.createTextNode(logTime))
    originatingSystemNode.appendChild(xmlDoc.createTextNode(system))
    detailsNode.appendChild(xmlDoc.createTextNode(msg))
    completionStatusNode.appendChild(xmlDoc.createTextNode(completionStatus))

    xmlDoc.save destination
    statusReportingEventCount = CInt(statusReportingEventCount) + 1
End Sub

Actually, that's a small lie. It actually seeds the XML using a string which includes the doctype definition. That's because the DOM doesn't directly support adding this line programatically.

Here's some code that demonstrates how to replace the text in an element:

if func.selectSingleNode("Name").text = "System Backup" then
  set parameterNode = func.selectSingleNode("Parameters")
  LogInfo "Amending System Backup parameter to workstation 2", 0, 1
  parameterNode.replaceChild xmlDoc.createTextNode("2"), parameterNode.firstChild
end if