PowerShell Saturday: The Iron Scripter

PowerShell Saturday: The Iron Scripter

  • Comments 3
  • Likes

Summary: Microsoft Scripting Guy, Ed Wilson, shares information about the Windows PowerShell Saturday #002 Iron Scripter Event.

Microsoft Scripting Guy, Ed Wilson, is here. Today, I will share the Iron Scripter event and winning script from Windows PowerShell Saturday #002 in Charlotte, NC. Jim Christopher, Windows PowerShell MVP and leader of the Charlotte PowerShell Users Group, provided the scenario and source files. Jonathon Tyler won the event, and we will hear his thoughts later in this post. This is timely because Jim will host another Iron Scripter event at Windows PowerShell Saturday #003 in Atlanta (Alpharetta, Georgia) this Saturday, October 27, 2012. Tickets are still available—but going fast. The trophy for the first Iron Scripter is shown here (and, yes, it is as hefty as it looks. Solid steel—it checks in at nearly 25 pounds).

 Iron Scripter Trophy

The scenario, according to Jim:

 Correct data field formatting in XML files

Your script must process a collection of XML files. Each file contains information about a single user. The phone field has been manually entered and is not always in the correct format. Your script must validate the format of the phone field in each file, fix misformatted phone fields, and update the invalid XML files on disk.

 The required format of the phone field is:

(###) ###-####

The XML files contain phone fields in a wide variety of formats. Some contain parenthesis, hyphens, spaces, periods, etc, and others do not.

 In all cases, the digits of the phone number are in the correct order.

 Example of an invalid phone field:

<user>

    <id>User98</id>

    <name>Dacia Charbonneau</name>

    <phone>2389101449</phone>

    <email>DChrb@contoso.com</email>

</user>

 Example of a corrected phone field:

<user>

    <id>User98</id>

    <name>Dacia Charbonneau</name>

    <phone>(238) 910-1449</phone>

    <email>DChrb@contoso.com</email>

</user>

The sample data provided to the users during the event can be found at the Script Gallery.

Judges are instructed to rate your scripts on the following criteria:

1) Functionality: Does your script solve the problem?

2) Usability: Does your script apply to new instances of the problem?

3) Readability: Is your script easily understandable?

Judges ratings are final. No complaining allowed.

For posterity (and comparison), my 2-minute solution (typed at the console, not in a script file) is below:

dir *.xml | foreach {

    $x = [xml](get-content $_);

    if( $x.user.phone -notmatch '\(\d{3}\) \d{3}-\d{4}' )

    {

        $p = $x.user.phone -replace '\D+','';

        $p = $p -replace '(\d{3})(\d{3})(\d{4})','($1) $2-$3';

        $x.user.phone = $p

        $x.save( $_.fullname );

    }

}

In the photo shown here, Jonathan Tyler receives the Iron Scripter trophy from the Scripting Wife at the packed house awards ceremony at the end of Windows PowerShell Saturday in Charlotte, North Carolina. The competition was hard fought and a fraction of a point separated Jonathan from multiple Scripting Games winner Glen Sizemore.

Jonathan Tyler receiving award

 The solution as supplied by Jonathan

Originally, I had not planned to go to the Windows PowerShell Saturday event, although I really wanted to go. My wife was scheduled to be out of town, and I don’t think the Microsoft Campus would be a good place for three small children while trying to learn some new techniques—much less at a scripting competition. However, as time progressed, schedules changed, and I was able to attend.

When I saw the presentation schedule, I saw the Iron Scripter! competition and thought it might be fun. The more I thought about it, the more I realized I wanted to try it. I had competed in the Official Scripting Games two years ago and had fun and learned a lot. (I do plan on entering the 2013 Games, by the way.) 

When Jim Christopher (@beefarino) presented the scenario, I was a little relieved. I had recently done some work with Windows PowerShell and XML, so it was somewhat fresh in my mind. I have even written a couple of blog posts about XML with SharePoint by using Powershell. The scenario was to read in a series of XML files, validate the phone number field for a specific format, and then write the file back with the corrected format. This had to be completed within one hour.

Unfortunately, when I saw that, my mind went completely blank on how to work with XML—we’ll call it stage fright. To begin, I opened the Windows PowerShell ISE and loaded one of the sample data files. I began to play with the XML file in the console to see that the properties lined up from the elements in the XML file. When starting a new script, I normally will run through a series of piece-meal steps in the console window to make sure I am thinking correctly as I am writing snippets into the code editing window. It took a couple of minutes to figure out a direction, and the coding began.

I began scripting furiously and quickly developed a solid base from which to work. I felt pretty pleased with my initial work, and my script worked. However, it was not exactly what the scenario required. I had run through several tests, restored the test files from the zip file, and run the script again to make sure I was getting valid results. I was. I took some time to accept pipeline input as well as a string filename input. I had even taken some time to write in comment-based help for some extra brownie points. Then it happened. I looked up and re-read the requirements.  “Validate” and “correct” were the keywords (they were in bold type) that jumped off the screen. My initial solution simply forced all of the phone number fields to the correct format, even if they were already in the correct format. It worked, but the requirements were not yet met.

I looked at my watch and saw that I had about 20 minutes left. I opened up my Regular Expression editor and began plugging in a quick format for the phone number. Once I got the format test ready, I modified my code to check the Phone field for the proper format. If it failed, I sent the Phone string up to a secondary function that did the conversion.  It stripped the Phone string of any non-numeric characters, converted that resulting numeric string to Int64, and then formatted it by using the ToString() method with the required format. That newly formatted string was assigned back to the Phone element, and the file was saved. After a few more tests against the data (and some debugging strings output to the console for verification that were later removed), I submitted the script to the server.

During the first half of the competition, I wasn’t as worried about the time. My first thoughts were to get something saved to the server. Once I got rolling with a workable solution, I spent all my time trying to get it formatted properly. I realized at one point that I was doing the same “work” in two different places. This prompted me to write the secondary function that I put in the BEGIN block of the script.  Once I did that, it made the code look a lot cleaner, which is always helpful when you need to figure out what is going on in your scripts. I was able to submit my final version with about five to ten minutes to spare.

Time was a factor in the solution. If I had more time to work on the script, I would have done a few more things:

  • Passed the XML data and the file name to another function that would have done nothing but validate, update, and save the information back. 
  • Set up file testing (Test-Path) to verify that the file actually exists.
  • Might also have included a parameter set to handle in-memory XML as well.

I had a blast with this competition. A friend and colleague of mine, David Mitchell (@surgeterrix), was able to go with me. I had mentioned to him about the competition before the event. He told me that he didn’t feel like he was knowledgeable enough about Windows PowerShell  to be able to enter the competition. I finally talked him into entering. After the submissions were closed, he told me that he had fun working on it as well…and that he learned something new about Windows PowerShell. I believe this is the biggest key to these types of competitions. The things you learn from working out the scenarios well outweigh the prizes at the end of the competition. Don’t get me wrong—I am enjoying the bragging rights (Glenn Sizemore – @glnsize), but it is just plain fun to compete and learn at the same time. I dig in a little to Glenn, but he was formidable competition to say the least. The difference in our scores was only 0.1 point.

================ SOURCE CODE =====================

Function Update-PhoneNumber

{

<#

.SYNOPSIS

Updates the phone number format to a pre-defined format in an XML document.

.DESCRIPTION

This function will read a single or multiple XML files of user objects. The phone number field will be read and forced to the format: (###) ###-####. The XML data will be saved back to the original file when complete.

.PARAMETER File

Accepts System.IO.FileInfo objects from the pipeline to process multiple files.

.PARAMETER FileName

Loads the specified file and processes the phone number field.

.INPUTS

System.IO.FileInfo - Using the Get-ChildItem cmdlet, you can specify a group of XML files to process.

String - Using a single fully qualified file name to process a single file.

.OUTPUTS

XML document saved back to the original file name.

.EXAMPLE

c:\PS> Update-PhoneNumber -FileName c:\temp\users\user0.xml

This example will read in the user0.xml file from the specified directory and force the format for the phone number.

.EXAMPLE

c:\PS> Get-ChildItem -Path C:\Temp\Users -filter *.xml | Update-PhoneNumber

This example will read all XML files in the C:\Temp\Users directory and update the phone number format and save the files back to the same location, overwriting the original data.

#>

    [CmdletBinding()]

    Param(

        [Parameter(Mandatory=$true,Position=0, ParameterSetName="FileInfo", ValueFromPipeline=$true)]

        [System.IO.FileInfo]$File,

        [Parameter(Mandatory=$true,Position=0, ParameterSetName="FileName")]

        [string]$FileName

    )

    BEGIN

    {

        Function Convert-PhoneString

        {

            Param([string]$phoneString)

            $phoneNumber = ""

            foreach ($char in $xmlFile.user.phone.ToCharArray())

            {

               if ($char -match "\d")

               {

                   $phoneNumber += $char

               }

            }

            return ([int64]$phoneNumber).ToString("(###) ###-####")

        }

    }

    Process

    {

        switch ($PSCmdlet.ParameterSetName)

        {

            "FileInfo"

            {

                $xmlFile = [xml](get-content $file.FullName)

                if (-not ($xmlFile.user.phone -match "^\(\d{3}\)\s\d{3}-\d{4}$"))

                {

                    $xmlFile.user.phone = Convert-PhoneString $xmlFile.user.phone

                    $xmlFile.Save($file.FullName);

                }

            }

            "FileName"

            {

                $xmlFile = [xml](get-content $FileName)

                if (-not ($xmlFile.user.phone -match "^\(\d{3}\)\s\d{3}-\d{4}$"))

                {

                    $xmlFile.user.phone = Convert-PhoneString $xmlFile.user.phone

                    $xmlFile.Save($FileName);

                }

            }

        }

    }

}

Jonathan’s complete script is available through the Scripting Guys Script Repository

Well, that is it. The first ever Iron Scripter event was a success, and, as you can see, the competition was tremendous. Jim will be hosting the second Iron Scripter event at Windows PowerShell Saturday #003 in Atlanta (Alpharetta, Georgia) this Saturday, October 27, 2012. There are still tickets available. Come check it out—it will be a blast.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Perhaps im wrong, but function Convert-PhoneString takes a variable called $PhoneString but never uses it and instead reads  the phonenumber from $xmlFile.user.phone. Or am i missing something?

  • @JohanW: Right!

    The line:  foreach ($char in $xmlFile.user.phone.ToCharArray())

    should read:  foreach ($char in $PhoneString.ToCharArray())

    I suppose ...

    Iron Scripter seems to be a great competition ( I like this Superman figure with the powershell prompt on its breast ... I always loved Superman) ( ... and Batchman, of course ... Sean! Can you here me! ? :-)

    Handling XML files with  powershell is really great! I just tried to change an XML file via Cmd.exe which was a pain!

    Native XML support is a requirement today!

    One little statement to add here putting the best of two worlds ( Ed's and Jonathan's in this case )

    I would change the function that does the hard work ... correcting the phone number to:

    Function Convert-PhoneString ([string]$phoneString)

    {

       "{0:(###) ###-####}" -f [int]($phoneString -replace "\D")

    }

    I think that replacing the non digits with a regular expression operator first, before any other processing of the phone number, is absolutely the best solution to get right to the point!

    Klaus

  • You guys are both right.  The $PhoneString variable is not used as it was intended to be.  As I recall, when I was writing the script in the contest, I was doing some ad-hoc testing in the command line with the data, and I think I just inadvertently copied the wrong code portion (the part with the $xmlFile variable) into where I was going to use the $PhoneString variable.

    Hopefully, even though there was this small error, it will help others learn some XML techniques in Powershell!