# Parsing poorly written XML

**URL:** <https://www.ruby-forum.com/t/parsing-poorly-written-xml/126989>\
**Category:** Ruby\
**Created:** [January 23, 2008, 11:35pm UTC](https://www.ruby-forum.com/t/parsing-poorly-written-xml/126989 "2008-01-23T23:35:10Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![cghost](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/c/898d66/32.png) [@cghost](https://www.ruby-forum.com/u/cghost)\
**Post date:** [January 23, 2008, 11:35pm UTC](https://www.ruby-forum.com/t/parsing-poorly-written-xml/126989/1 "2008-01-23T23:35:10Z")

</div>

I’ve found an application which dumps all the registry settings for  
installed applications into an .xml file for me (you can find it as  
“myuninstall” via google).

I can’t figure out the ruby code I need from REXML to do the following:  
-find a particular “product\_name” in the xml file  
-if found, save off the “uninstall\_string” so I can execute it on the  
commandline and uninstall the app.

I’m a newbie to Ruby, and I’m even newer to parsing XML with Ruby, so  
I’m struggling a bit. It seems there should be a way to navigate within  
that “item” after I match on the product name, but I can’t figure out  
how. Any guidance would be very VERY appreciated!

Here’s a snippet of the problem xml:

\<?xml version="1.0" encoding="ISO-8859-1" ?\>

\<installed\_app version=“1.0”\>  
  
\<entry\_name\>Devicescape\</entry\_name\>  
\<product\_name\>DevicescapeDesktop\</product\_name\>  
2.0.5  
Devicescape  
DevicescapeDesktop  
No  
Yes  
\<installation\_folder\>C:\Program  
Files\Devicescape\Devicescape\</installation\_folder\>  
\<web\_site\>[http://www.devicescape.com/](http://www.devicescape.com/)\</web\_site\>  
\<installation\_date\>1/22/2008 4:42:14 PM\</installation\_date\>  
\<uninstall\_string\>MsiExec.exe  
/I{C3BFE4FC-C9F6-494F-B3AB-ABD75556DDC5}\</uninstall\_string\>  
\<quiet\_uninstall\>No\</quiet\_uninstall\>  
\<registry\_key\>{C3BFE4FC-C9F6-494F-B3AB-ABD75556DDC5}\</registry\_key\>  
Windows Installer  
\<root\_key\>HKEY\_LOCAL\_MACHINE\</root\_key\>

---

<div class="post-metadata">

**Author:** ![cghost](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/c/898d66/32.png) [@cghost](https://www.ruby-forum.com/u/cghost)\
**Post date:** [January 24, 2008, 12:20am UTC](https://www.ruby-forum.com/t/parsing-poorly-written-xml/126989/2 "2008-01-24T00:20:52Z")

</div>

On Jan 23, 4:07 pm, Phrogz [removed\_email\_address@domain.invalid](mailto:removed_email_address@domain.invalid) wrote:

> #=\> 42  
> 17  
>   
>   
> foo  
> 17

Or, using XPath more liberally (with the same DATA section as quoted  
above):

require ‘rexml/document’  
doc = REXML::Document.new( DATA.read )  
p REXML::XPath.match( doc, “//item[name=‘foo’]/value/text()” )  
#=\> [“42”, “17”]

---

<div class="post-metadata">

**Author:** ![cghost](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/c/898d66/32.png) [@cghost](https://www.ruby-forum.com/u/cghost)\
**Post date:** [January 24, 2008, 12:10am UTC](https://www.ruby-forum.com/t/parsing-poorly-written-xml/126989/3 "2008-01-24T00:10:21Z")

</div>

On Jan 23, 3:35 pm, Patrick C. [removed\_email\_address@domain.invalid](mailto:removed_email_address@domain.invalid) wrote:

> I’ve found an application which dumps all the registry settings for  
> installed applications into an .xml file for me (you can find it as  
> “myuninstall” via google).
> 
> I can’t figure out the ruby code I need from REXML to do the following:  
> -find a particular “product\_name” in the xml file  
> -if found, save off the “uninstall\_string” so I can execute it on the  
> commandline and uninstall the app.

require ‘rexml/document’  
doc = REXML::Document.new( DATA.read )

# Find ‘item’ elements at any level

# that have a child ‘name’ element whose text value is ‘foo’

doc.each\_element( “//item[name=‘foo’]” ){ |item|

# Find every child element of the item element

# whose name is ‘value’, get the first one, and get its text

puts item.get\_elements( “value” ).first.text  
}  
#=\> 42  
#=\> 17

**END**  
  
  
foo  
42  
  
  
bar  
17  
  
  
foo  
17

---

<div class="post-metadata">

**Author:** ![cghost](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/c/898d66/32.png) [@cghost](https://www.ruby-forum.com/u/cghost)\
**Post date:** [January 24, 2008, 1:08am UTC](https://www.ruby-forum.com/t/parsing-poorly-written-xml/126989/4 "2008-01-24T01:08:07Z")

</div>

Gavin K. wrote:

> On Jan 23, 3:35 pm, Patrick C. [removed\_email\_address@domain.invalid](mailto:removed_email_address@domain.invalid) wrote:
> 
> > I’ve found an application which dumps all the registry settings for  
> > installed applications into an .xml file for me (you can find it as  
> > “myuninstall” via google).
> > 
> > I can’t figure out the ruby code I need from REXML to do the following:  
> > -find a particular “product\_name” in the xml file  
> > -if found, save off the “uninstall\_string” so I can execute it on the  
> > commandline and uninstall the app.
> 
> require ‘rexml/document’  
> doc = REXML::Document.new( DATA.read )
> 
> # Find ‘item’ elements at any level
> 
> # that have a child ‘name’ element whose text value is ‘foo’
> 
> doc.each\_element( “//item[name=‘foo’]” ){ |item|
> 
> # Find every child element of the item element
> 
> # whose name is ‘value’, get the first one, and get its text
> 
> puts item.get\_elements( “value” ).first.text  
> }  
> #=\> 42  
> #=\> 17
> 
> **END**  
>   
>   
> foo  
> 42  
>   
>   
> bar  
> 17

Wow! That worked on the first try! I knew there had to be a way, but  
I’m trying to whip this script out and I didn’t want to spend a week  
studying XML parsing options in Ruby to do it. You’ve saved me a bunch  
of effort. Now I just need to figure out how to gracefully bail out  
and continue on with the rest of the script if “foo” isn’t there at all.  
Uhm… I seem to be having trouble with that piece too. I’m sure it’s  
easier than the first problem, but maybe it’s just 'cause my brain is  
cooked right now…

Thanks a bunch!

---

<div class="post-metadata">

**Author:** ![cghost](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/c/898d66/32.png) [@cghost](https://www.ruby-forum.com/u/cghost)\
**Post date:** [January 24, 2008, 5:00am UTC](https://www.ruby-forum.com/t/parsing-poorly-written-xml/126989/5 "2008-01-24T05:00:59Z")

</div>

On Jan 23, 5:08 pm, Patrick C. [removed\_email\_address@domain.invalid](mailto:removed_email_address@domain.invalid) wrote:

> Gavin K. wrote:  
> Wow! That worked on the first try! I knew there had to be a way, but  
> I’m trying to whip this script out and I didn’t want to spend a week  
> studying XML parsing options in Ruby to do it. You’ve saved me a bunch  
> of effort. Now I just need to figure out how to gracefully bail out  
> and continue on with the rest of the script if “foo” isn’t there at all.  
> Uhm… I seem to be having trouble with that piece too. I’m sure it’s  
> easier than the first problem, but maybe it’s just 'cause my brain is  
> cooked right now…

The first piece of code I wrote has a potential to be cranky.

doc.each\_element( “//item[name=‘foo’]” ){ |item|  
puts item.get\_elements( “value” ).first.text  
}

If no …foo elements can be found, the block will  
never be called. However, if it finds such an element, but the   
doesn’t have a child, then item.get\_elements( “value” ) will  
return an empty array. Calling .first on an empty array will return  
nil, and then you’ll have an error when you try to run the ‘text’  
method of nil.

You could modify the above code to guard for this:  
doc.each\_element( “//item[name=‘foo’]” ){ |item|  
first\_value = item.get\_elements( “value” ).first  
if first\_value  
puts first\_value.text  
end  
}

However, the second (single XPath query) solution I wrote is both  
shorter and also more fault tolerant. If you assume that there’s only  
one that will match the foo criterion, then you  
can change from REXML::XPath.match (which returns an array, possibly  
empty) to REXML::XPath.first (which returns either a single value, or  
nil).

Something like:  
value = REXML::XPath.first( doc, “//item[name=‘foo’]/value/text()” )  
if value  
# do my stuff here  
end

Or, to use your exact case:

# Returns the uninstall command run (if found),

# or nil if the necessary info couldn’t be found.

def uninstall\_product( xml\_string, prod\_name )  
require ‘rexml’  
include REXML  
uninstall\_command = XPath.first(  
Document.new( xml\_string ),  
“//item[product\_name=‘#{prod\_name}’]/uninstall\_string/text()”  
)  
if uninstall\_command  
# Do the uninstall; maybe as simple as:  
`#{uninstall_command}`  
else  
warn "Either product ‘#{prod\_name}’ doesn’t exist, " +  
“or it doesn’t have an uninstall string.”  
end  
uninstall\_command  
end
