XML Part 2: Write, Add And Change XML Data

by Feb 2, 2009

In the previous post, I demonstrated how PowerShell handles XML data and how easy it is to load XML from a file or the Internet and then analyze its content. Today, I’d like to share how you can change and append XML data.

Creating Sample XML

I’d like to start this by creating an XML template. I don’t bother using special XML functionality. Instead, I simply write the XML template using plain text like this:

PS> $template = “<employee version=’1.0′>
>> <person>
>> <firstname>Tobias</firstname>
>> <lastname>Weltner</lastname>
>> </person>
>> </employee>
>> “
>>
PS> $template
<employee version=’1.0′>
<person>
<firstname>Tobias</firstname>
<lastname>Weltner</lastname>
</person>
</employee>

PS> $template | Out-File $home\template.xml

Loading Sample XML

Next, I load my XML template into an XML object. This gives me the ability to browse and analyze the content of my XML file. If you have questions regarding browsing the XML content, please take a look at Part1 of this blog.

PS> $xml = New-Object XML
PS> $xml.Load(“$home\template.xml”)
PS> $xml

employee
——–
employee
PS> $xml.employee

version                                                       person
——-                                                       ——
1.0                                                           person
PS> $xml.employee.person

firstname                                                     lastname
———                                                     ——–
Tobias                                                        Weltner
PS> $xml.employee.person.firstname
Tobias

Manipulating XML Content

Once you have loaded the XML into an XML object, it is amazingly simple to change and update its content. Let’s say I’d like to change my lastname to a different name. First, I select the data I want to change. Next, I change it. That is all:

PS> $xml.employee.person | Where-Object { $_.lastname -eq ‘Weltner’ }

firstname                                                     lastname
———                                                     ——–
Tobias                                                        Weltner
PS> $xml.employee.person | 
>> Where-Object { $_.lastname -eq ‘Weltner’ } | 
>> ForEach-Object { $_.lastname = ‘NewName’ }
>>
PS> $xml.employee.person

firstname                                                     lastname
———                                                     ——–
Tobias                                                        NewName

Saving Changes As New XML File

All changes occur in memory inside your loaded XML object. To persist your changes to an updated XML file, you use the Save() method found as part of your XML object. Before I save my changes to a new file, I also update the version attribute I originally added to the <employee> node:

PS> $xml.employee.version = “2.0”
PS> $xml.Save(“$home\updated.xml”)
PS> Get-Content $home\updated.xml
<employee version=”2.0″>
  <person>
    <firstname>Tobias</firstname>
    <lastname>NewName</lastname>
  </person>
</employee>

As you can see, all changes have been written to proper XML. I do not need to fiddle with XML object model methods to make all of this happen.

Adding New Data To XML

Changing existing data inside an XML file is easy. But how do I add new data? Let’s say I want to add a new person to my employee list. Here is how you do it:

PS> $oldperson = @($xml.employee.person)[0]
PS> $oldperson

firstname                                                       lastname
———                                                       ——–
Tobias                                                          NewName
PS> $newperson = $oldperson.Clone()
PS> $newperson.firstname = “Cofi”
PS> $newperson.lastname = “Weltner”
PS> $newperson

firstname                                                       lastname
———                                                       ——–
Cofi                                                            Weltner

To add a new entry to my XML, I first grab an existing entry. Since my XML only contains one person at this time, I make sure I wrap the call in @(). This way, I always receive an array even though there is only one entry. Without this statement, PowerShell would have returned the person item right away. Next, I read the first person from that array using index #0 and store it as $oldperson.

$oldperson is still connected to my XML file and still represents the old person. So if I made changes to this object, I would actually manipulate the existing person. To add a new person, I call Clone() to create an independent clone of that object and store it in $newperson. This object now is a new data entry, and I can go ahead and change its properties to anything I want.

Inserting New Data Into XML

Right now, $newperson is not connected to my XML. Instead, it just hangs around in memory. To make it part of my XML file, I need to insert it into my XML at the position I want. I want $newperson to be part of my employees so I add it like this:

PS> $xml.employee.AppendChild($newperson)

firstname                             lastname
———                             ——–
Cofi                                  Weltner
PS> $xml.employee.person

firstname                             lastname
———                             ——–
Tobias                                NewName
Cofi                                  Weltner

It worked: The new person now is part of my XML, and all I needed to do now is to save the appended XML to file to persist my changes. It really is that easy!

Creating A List Of Local Users

To illustrate how all of this works, I’d like to show you a quick example. It queries WMI for all local user accounts and creates a nice XML file.

First of all, I need to figure out how to get the user data I want to wrap as XML. As it turns out, Get-WMIObject does the job for me. The information is contained in the Win32_UserAccount class. Since I am only interested in local accounts, I make sure I query only for instances with LocalAccount set to true. I filter this on the server side because querying all accounts in a large enterprise could otherwise take a long time:

PS> Get-WmiObject win32_useraccount -filter ‘LocalAccount=true’

AccountType : 512
Caption     : PCNEU01\Administrator
Domain      : PCNEU01
SID         : S-1-5-21-2613171836-1965730769-3820153312-500
FullName    :
Name        : Administrator

AccountType : 512
Caption     : PCNEU01\ASPNET
Domain      : PCNEU01
SID         : S-1-5-21-2613171836-1965730769-3820153312-1001
FullName    : ASP.NET Machine Account
Name        : ASPNET

AccountType : 512
Caption     : PCNEU01\Fred
Domain      : PCNEU01
SID         : S-1-5-21-2613171836-1965730769-3820153312-1016
FullName    : Fred
Name        : Fred

(…)

Next, I create my XML template designed to hold the data I am after. I want to store these properties: Name, Fullname, Description, SID, PasswordRequired, Disabled. J

ust in case you are wondering why only part of these properties appeared in the last screenshot, always remember: PowerShell only shows you the most important properties of an object. To see all properties, you need to add to your pipeline either | Format-List *  or | Get-Member.

Here is my XML template:

PS> $template = “
>> <localusers machine=’pc1′ version=’1.0′>
>> <users>
>> <Name></Name>
>> <Fullname></Fullname>
>> <Description></Description>
>> <SID></SID>
>> <PasswordRequired></PasswordRequired>
>> <Disabled></Disabled>
>> </users>
>> </localusers>
>> “
>>
PS> $template

<localusers machine=’pc1′ version=’1.0′>
<users>
<Name></Name>
<Fullname></Fullname>
<Description></Description>
<SID></SID>
<PasswordRequired></PasswordRequired>
<Disabled></Disabled>
</users>
</localusers>

PS> $template | Out-File $home\users.xml

Next, I load the template into my XML object:

PS> $xml = New-Object xml
PS> $xml.Load(“$home\users.xml”)
PS> $xml

localusers
———-
localusers
PS> $xml.localusers

machine                  version                  users
——-                  ——-                  —–
pc1                      1.0                      users
PS> $xml.localusers.users

Name             :
Fullname         :
Description      :
SID              :
PasswordRequired :
Disabled         :

Now, I grab an existing entry and use this as template for my new entries:

PS> $newuser = (@($xml.localusers.users)[0]).Clone()
PS> $newuser

Name             :
Fullname         :
Description      :
SID              :
PasswordRequired :
Disabled         :

Finally, I ask WMI for the actual user information and write it to my XML file. Just be aware that XML can only store string data. This is why I convert boolean values to string using toString():

PS> Get-WmiObject Win32_UserAccount -filter ‘LocalAccount=true’ |
>> ForEach-Object {
>> $newuser = $newuser.clone()
>> $newuser.Name = $_.Name
>> $newuser.Fullname = $_.FullName
>> $newuser.Description = $_.Description
>> $newuser.SID = $_.SID
>> $newuser.PasswordRequired = $_.PasswordRequired.toString()
>> $newuser.Disabled = $_.Disabled.toString()
>> $xml.localusers.AppendChild($newuser) > $null
>> }
>>
PS> $xml.localusers.users.Count
17
PS> $xml.localusers.users | Format-Table Name, SID, Description

Name                     SID                      Description
—-                     —                      ———–

Administrator            S-1-5-21-2613171836-1… Predefined Account …
ASPNET                   S-1-5-21-2613171836-1… Account used for runn…
Fred                     S-1-5-21-2613171836-1… Pension
(…)

It worked! One last clean up thing to do is to remove the empty template. To remove all entries with an empty Name property, I do this:

PS> $xml.localusers.users |
>> Where-Object { $_.Name -eq ” } |
>> ForEach-Object { $xml.localusers.RemoveChild($_) }
>>

Name             :
Fullname         :
Description      :
SID              :
PasswordRequired :
Disabled         :

PS> $xml.localusers.users.Count
16

Now I can save the XML to file and take a look at the XML I just created:

PS> $xml.Save(“$home\userlist.xml”)
PS> Get-Content $home\userlist.xml
<localusers machine=”pc1″ version=”1.0″>
  <users>
    <Name>Administrator</Name>
    <Fullname>
    </Fullname>
    <Description>Predefined Account</Description>
    <SID>S-1-5-21-2613171836-1965730769-3820153312-500</SID>
    <PasswordRequired>True</PasswordRequired>
    <Disabled>True</Disabled>
  </users>
  <users>
    <Name>ASPNET</Name>
    <Fullname>ASP.NET Machine Account</Fullname>
    <Description>Account used for running the ASP.NET worker process (aspn
et_wp.exe)</Description>
    <SID>S-1-5-21-2613171836-1965730769-3820153312-1001</SID>
    <PasswordRequired>False</PasswordRequired>
    <Disabled>False</Disabled>
  </users>
  <users>
    <Name>Fred</Name>
    <Fullname>Fred</Fullname>
    <Description>Pension</Description>
    <SID>S-1-5-21-2613171836-1965730769-3820153312-1016</SID>
    <PasswordRequired>True</PasswordRequired>
    <Disabled>False</Disabled>
  </users>
(…)

Wrap Up

Here is a script that creates the XML local user list for you and demonstrates the individual steps in one file:

# create a template XML to hold data
$template = @'
<localusers machine='pc1' version='1.0'>
<users>
<Name></Name>
<Fullname></Fullname>
<Description></Description>
<SID></SID>
<PasswordRequired></PasswordRequired>
<Disabled></Disabled>
</users>
</localusers>
'@

$template | Out-File $home\users.xml -encoding UTF8

# load template into XML object
$xml = New-Object xml
$xml.Load("$home\users.xml")

# grab template user
$newuser = (@($xml.localusers.users)[0]).Clone()

# use template to add local user accounts to xml
Get-WmiObject Win32_UserAccount -filter 'LocalAccount=true' |
ForEach-Object {
$newuser = $newuser.clone()
$newuser.Name = $_.Name
$newuser.Fullname = $_.FullName
$newuser.Description = $_.Description
$newuser.SID = $_.SID
$newuser.PasswordRequired = $_.PasswordRequired.toString()
$newuser.Disabled = $_.Disabled.toString()
$xml.localusers.AppendChild($newuser) > $null
}

# remove users with undefined name (remove template)
$xml.localusers.users |
Where-Object { $_.Name -eq "" } |
ForEach-Object { [void]$xml.localusers.RemoveChild($_) }

# save xml to file
$xml.Save("$home\userlist.xml")

# play with results

$xml.localusers.users | Sort-Object Name | Format-Table name, description, SID

& "$home\userlist.xml"

Cheerio

-Tobias