First time here? Check out the site's "greatest hits" or read a post from the archives. Feel free to leave a comment or ask a question, and consider subscribing to the latest posts via RSS or e-mail. Thanks for visiting!
Do you Tweet? Follow me on Twitter @shanselman or learn how to use Twitter!
« Querying Virtual Server 2005 via VM with... | Main | Serializing Objects as JavaScript using ... »


I like my XML pretty. There's no format-xml cmdlet or tidy-xml in PowerShell, so here's my first try:

#Name me tidy-xml.ps1
# - this crap written by Scott Hanselman
[System.Reflection.Assembly]::LoadWithPartialName("System.Xml") > $null
$PRIVATE:tempString = ""
if ($args[0].GetType().Name -eq "XmlDocument")
{
 $PRIVATE:tempString = $args[0].get_outerXml()
}
if ($args[0].GetType().Name -eq "String")
{
 $PRIVATE:tempString = $args[0]
}
$r = new-object System.Xml.XmlTextReader(new-object System.IO.StringReader($PRIVATE:tempString))
$sw = new-object System.IO.StringWriter
$w = new-object System.Xml.XmlTextWriter($sw)
$w.Formatting = [System.Xml.Formatting]::Indented
do { $w.WriteNode($r, $false) } while ($r.Read())
$w.Close()
$r.Close()
$sw.ToString()

Sometimes XML is thought of as strings and sometimes as [xml] in PowerShell. This script will take either a string or [xml] but will always return a string. (e.g. It's on you to do the final [xml] cast because if you did, the tidying is moot). For example:

PS> $a = "<foo><bar>asdasd</bar></foo>"
PS> ./tidy-xml $a
<foo>
  <bar>asdasd</bar>
</foo>
PS> $b = [xml]"<foo><bar>asdasd</bar></foo>"
PS> ./tidy-xml $b
<foo>
  <bar>asdasd</bar>
</foo>

I wanted to make it so I could do these scenarios. Thoughts? Remember that I need to normalize to a string for the StringReader constructor.

#couldn't because it returned an Object[] of strings and it got sloppy fast
get-content foo.xml | tidy-xml

#couldn't because it (oddly) returned an ArrayList of strings and it got sloppy fast
get-content foo.xml -ov c
tidy-xml $c

Enjoy (or improve!)

UPDATE: Here's a better version that includes a number of best-practices changes as well as the support for taking IN objects from the pipeline (like I wanted originally):

#The following cases work
#
#PS>$a
#<foo><bar>this is A</bar></foo
#PS>$b.get_OuterXml()
#<foo><bar>this is B</bar></foo
#PS>Get-Content foo.xml
#<foo>
#   <bar>this is C</bar>
#</foo>
#
#Now try the following.
#PS>sal ti tidy-xml
#PS>$a | ti
#PS>$b | ti
#PS>$c | ti
#PS>ti $a
#PS>ti $b
#PS>ti $c
#PS>$a, $b | ti
#PS>$a, $c | ti
#PS>$c, $b | ti
#PS>$a, $b, $c | ti
#
#What doesn't work here is when you pass a multiple parameter input as follows:
#tidy-xml $a, $b # doesn't work
#
#Uhm, i think i would have to change my logic "completely" to actually get that to work...
#(after refactoring "process" block...)
#
#Name me tidy-xml.ps1
# - some of this crap written by Scott Hanselman
function Tidy-Xml {
    begin {
        $private:str = ""
       
        # recursively concatenate strings from passed-in arrays of schmutz
        # not sure how to improve this...
        function ConcatString ([object[]] $szArray) {
            # return string
            $private:rStr = ""

            # Recursively call itself, if a string is also of array or a collection type
            foreach ($private:sz in $szArray) {
                if (($private:sz.GetType().IsArray) -or `
                    ($private:sz -is [System.Collections.IList])) {
                    $private:rStr += ConcatString($private:sz)
                }
                elseif ($private:sz -is [xml]) {
                    $private:rStr += $private:sz.Get_OuterXml()
                }
                else {
                    $private:rStr += $private:sz
                }
            }
            return $private:rStr;
        }
       
        # Original "Tidy-Xml" portion
        function FormatXmlString ($arg) {
            # ignore parse errors
            trap { continue; }
           
            # out-null hides output of the assembly load
            [System.Reflection.Assembly]::LoadWithPartialName("System.Xml") | out-null

            $PRIVATE:tempString = ""
            if ($arg -is [xml]){
                $PRIVATE:tempString = $arg.get_outerXml()
            }
            if ($arg -is [string]){
                $PRIVATE:tempString = $arg
            }

            # the ` tick mark is a line-continuation char
            $r = new-object System.Xml.XmlTextReader(`
                new-object System.IO.StringReader($PRIVATE:tempString))
            $sw = new-object System.IO.StringWriter
            $w = new-object System.Xml.XmlTextWriter($sw)
            $w.Formatting = [System.Xml.Formatting]::Indented

            do { $w.WriteNode($r, $false) } while ($r.Read())

            $w.Close()
            $r.Close()
            $sw.ToString()
        }
    }
   
    process {
        # For non-xml strings or types, they will be buffered and will be
        # taken care of in "end" block
        
        # this checks for objects that have been "pipe'd" in.
        if ($_) {
            # check if whatever we have appended is a valid XML or not
            $private:xmlStr = ($private:str + $_) -as [xml]
           
            if ($private:xmlStr -ne $null) {
                FormatXmlString([xml]$private:xmlStr)
                # clear the string not to be handled in "end" block
                $private:str = $null
            } else {
                if ($_ -is [string]) {
                    $private:str += $_
                } elseif ($_ -is [xml]) {
                    FormatXmlString($_)
                }
                # for an array or a collection type,
                elseif ($_.Count) {
                    # iterate each item in the collection and append
                    foreach ($i in $_) {
                        $private:line += $i
                    }
                    $private:str += $private:line
                }
            }
        }
    }

    end {
        if ([string]::IsNullOrEmpty($private:str)) {
            $private:szXml = $(ConcatString($args)) -as [xml]
            if (! [string]::IsNullOrEmpty($private:szXml)) {
                FormatXmlString([xml]$private:szXml)
            }
        } else {
            FormatXmlString([xml]$private:str)
        }
    }
}

Thanks to MonadBlog for the Updates! There's definitely some room for refactoring of the begin/end/process, but it's more funcitonal this way.

Tracked by:
"Power to the PowerShell" (Impersonation Failure) [Trackback]
"Interesting Finds: July 4, 2006" (Jason Haley) [Trackback]


Monday, July 03, 2006 8:29:51 PM (Pacific Standard Time, UTC-08:00)
Hello there, Scott,
I have modified your source a bit and pasted it on http://pastehere.com/?dcuqpi as well as some comments

The code now looks like complete crap thanks to my modification... almost unreadable... sorry about botching your source.

Monday, July 03, 2006 9:03:56 PM (Pacific Standard Time, UTC-08:00)
Nonsense, I love it. Thanks for the modifications, I'll add them to the post and annotate the changes for everyone's (an my) ongoing education.
Scott Hanselman
Tuesday, July 04, 2006 1:48:49 PM (Pacific Standard Time, UTC-08:00)
Ah, you actually did post it.. ;) thanks

Btw, I have also posted the "COMPLETE" tabExpansion function on "http://pastehere.com/?armhfr" but i have broken one of the functionalities($host.ui.rawUi.[tab] doesn't work, meaning nested properties aren't expanded properly...)

Tuesday, July 04, 2006 2:57:58 PM (Pacific Standard Time, UTC-08:00)
I just have fixed the multi-level tab expansion and "about_[tab]"... problems.. on http://pastehere.com/?gbrsgj
Comments are closed.

Contact

Sponsors

Hosting By

Hot Topics

Tags

Calendar

<November 2009>
SunMonTueWedThuFriSat
25262728293031
1234567
891011121314
15161718192021
22232425262728
293012345

Archives

November, 2009 (5)
October, 2009 (19)
September, 2009 (11)
August, 2009 (12)
July, 2009 (21)
June, 2009 (26)
May, 2009 (16)
April, 2009 (13)
March, 2009 (17)
February, 2009 (17)
January, 2009 (18)
December, 2008 (32)
November, 2008 (17)
October, 2008 (22)
September, 2008 (16)
August, 2008 (14)
July, 2008 (25)
June, 2008 (19)
May, 2008 (17)
April, 2008 (17)
March, 2008 (26)
February, 2008 (21)
January, 2008 (28)
December, 2007 (19)
November, 2007 (17)
October, 2007 (31)
September, 2007 (39)
August, 2007 (37)
July, 2007 (43)
June, 2007 (37)
May, 2007 (32)
April, 2007 (38)
March, 2007 (29)
February, 2007 (46)
January, 2007 (31)
December, 2006 (27)
November, 2006 (31)
October, 2006 (32)
September, 2006 (39)
August, 2006 (34)
July, 2006 (40)
June, 2006 (18)
May, 2006 (31)
April, 2006 (34)
March, 2006 (30)
February, 2006 (38)
January, 2006 (44)
December, 2005 (19)
November, 2005 (34)
October, 2005 (24)
September, 2005 (37)
August, 2005 (20)
July, 2005 (24)
June, 2005 (33)
May, 2005 (16)
April, 2005 (22)
March, 2005 (34)
February, 2005 (15)
January, 2005 (37)
December, 2004 (28)
November, 2004 (30)
October, 2004 (34)
September, 2004 (22)
August, 2004 (34)
July, 2004 (18)
June, 2004 (64)
May, 2004 (49)
April, 2004 (21)
March, 2004 (29)
February, 2004 (29)
January, 2004 (36)
December, 2003 (25)
November, 2003 (24)
October, 2003 (59)
September, 2003 (42)
August, 2003 (24)
July, 2003 (44)
June, 2003 (29)
May, 2003 (21)
April, 2003 (30)
March, 2003 (27)
February, 2003 (47)
January, 2003 (50)
December, 2002 (31)
November, 2002 (38)
October, 2002 (44)
September, 2002 (15)
May, 2002 (2)
April, 2002 (4)

Google Ads