cajwine
cajwine

Reputation: 3170

Select the 1st element only - with condition using XML::Twig

Having this code:

#!/usr/bin/env perl
use 5.014;
use warnings;
use XML::Twig;

my $twig = XML::Twig->parse( \*DATA );
$twig->set_pretty_print('indented_a');

# 1st search
# this prints OK the all <files> nodes where the <type> == 'release'
$_->print for ( $twig->findnodes( '//type[string()="release"]/..' ) );

# 2nd search    
# try to get first matched only
my $latest = $twig->findnodes( '(//type[string()="release"])[1]/..' );
$latest->print;

__DATA__
<root>
    <files>
        <type>beta</type>
        <ver>3.0</ver>
    </files>
    <files>
        <type>alpha</type>
        <ver>3.0</ver>
    </files>
    <files>
        <type>release</type>
        <ver>2.0</ver>
    </files>
    <files>
        <type>release</type>
        <ver>1.0</ver>
    </files>
</root>

The above prints

  <files>
    <type>release</type>
    <ver>2.0</ver>
  </files>
  <files>
    <type>release</type>
    <ver>1.0</ver>
  </files>
error in xpath expression (//type[string()="release"])[1]/.. around (//type[string()="release"])[1]/.. at /opt/anyenv/envs/plenv/versions/5.24.0/lib/perl5/site_perl/5.24.0/XML/Twig.pm line 3648.

The wanted output from the 2nd search

    <files>
        <type>release</type>
        <ver>2.0</ver>
    </files>

e.g. the first <files> node where the <type> eq 'release'.

According to this answer the used XPath expression (//type[string()="release"])[1]/..' should work, but seems I again missed something important.

Could anyone help, please?

Upvotes: 3

Views: 881

Answers (3)

Borodin
Borodin

Reputation: 126762

XML::Twig doesn't support the full XPath syntax. The documentation for the get_xpath method (the same as findnodes) says this

A subset of the XPATH abbreviated syntax is covered:

tag
tag[1] (or any other positive number)
tag[last()]
tag[@att] (the attribute exists for the element)
tag[@att="val"]
tag[@att=~ /regexp/]
tag[att1="val1" and att2="val2"]
tag[att1="val1" or att2="val2"]
tag[string()="toto"] (returns tag elements which text (as per the text method) 
                     is toto)
tag[string()=~/regexp/] (returns tag elements which text (as per the text 
                        method) matches regexp)
expressions can start with / (search starts at the document root)
expressions can start with . (search starts at the current element)
// can be used to get all descendants instead of just direct children
* matches any tag

So subexpressions within parentheses aren't supported, and you may specify only a single predicate

It's also important that, in scalar context, findnodes will only ever return a count of the number of nodes found. You must use it in list context to retrieve the nodes themselves, which means that a simpler way to find just the first matching element is to write

my ($latest) = $twig->findnodes( '//type[string()="release"]/..' );

which works fine

If you really need the full power of XPath, then you can use XML::Twig::XPath instead. This module uses either XML::XPath or the excellent XML::XPathEngine to provide the full XPath syntax by overloading findnodes. (The other methods get_xpath and find_nodes continue to use the reduced XML::Twig variation.)

findnodes in scalar context now returns an XML::XPathEngine::NodeSet object which has array indexing overloaded. So you can write

my $latest = $twig->findnodes( '//type[string()="release"]/..' );
$latest->[0]->print;

or just

my ($latest) = $twig->findnodes( '//type[string()="release"]/..' );

as above.

Finally, I would prefer to see /root/files[type[string()="release"]] in preference to the trailing parent::node(), but that is purely personal

Upvotes: 4

mirod
mirod

Reputation: 16171

XML::Twig doesn't support all of XPath, but XML::Twig::XPath does.

So use XML::Twig::XPath;, then my $twig = XML::Twig::XPath->parse(... and voilà... you can now get to fixing the $latest=... line, which should be:

my $latest = ($twig->findnodes( '(//type[string()="release"])[1]/..' ))[0];

(the way you have it $latest is an XML::XPathEngine::NodeSet, you need to take the first element of that set).

Upvotes: 3

choroba
choroba

Reputation: 242323

XML::Twig doesn't support the whole XPath. The expression works correctly in XML::LibXML.

You can walk the structure yourself in Perl:

my $latest = ($twig->findnodes('//type[string()="release"]'))[0]->parent;

Upvotes: 2

Related Questions