Romko
Romko

Reputation: 1798

Perl hashes. iterate through foreach

Have some problem with Perl hashes. I have an XML:

<?xml version="1.0" encoding="utf-8"?>
<SvnRequestUsers>
  <AccessCode>$TR0ngP@ssvv0rd!</AccessCode>
  <SvnUsers>
    <SvnUser>
      <Username>svn.testname</Username>
      <Password>jA=B*+q%</Password>
      <Create>true</Create>
    </SvnUser>
    <SvnUser>
      <Username>svn.testname2</Username>
      <Password>jA=B*+q%</Password>
      <Create>true</Create>
    </SvnUser>
  </SvnUsers>
</SvnRequestUsers>

I want to loop through SvnUser nodes. When I use

my $usersList = $ref->{'SvnUsers'};

foreach my $key ( @{$usersList->{'SvnUser'}} )
{ ..... }

That works when there is more than one node but doesn't work for One node.

When use

my @usersList = $ref->{'SvnUsers'}->{'SvnUser'};

foreach my $key ( @usersList )
{ ..... }

that works only when exactly one node is there. Where is the trick ???

Upvotes: 1

Views: 690

Answers (5)

Joel Berger
Joel Berger

Reputation: 20280

As usual I will recommend Mojo::DOM which is part of the Mojolicious suite. Using selectors and transform/iterator methods the problem gets really simple:

#!/usr/bin/env perl

use strict;
use warnings;

use Mojo::DOM;

my $dom = Mojo::DOM->new(<<'END');
<?xml version="1.0" encoding="utf-8"?>
<SvnRequestUsers>
  <AccessCode>$TR0ngP@ssvv0rd!</AccessCode>
  <SvnUsers>
    <SvnUser>
      <Username>svn.testname</Username>
      <Password>jA=B*+q%</Password>
      <Create>true</Create>
    </SvnUser>
    <SvnUser>
      <Username>svn.testname2</Username>
      <Password>jA=B*+q%</Password>
      <Create>true</Create>
    </SvnUser>
  </SvnUsers>
</SvnRequestUsers>
END

# get an array

my @users = $dom->find('SvnUser')
                ->pluck('Username')
                ->pluck('text')
                ->each;

# or do something with each element

$dom->find('SvnUser')
    ->pluck('Username')
    ->pluck('text')
    ->each(sub{ print "$_\n" });

Of course if you were actually just wanting the username you might just use the more tailored selector find('SvnUser Username'), my above example was to show a method.

Upvotes: 0

Borodin
Borodin

Reputation: 126722

This looks like the work of the far-from-simple XML::Simple. I am sure it has won its popularity through being the first to claim its namespace, and IMO it has a lot of shortcomings.

If my guess is correct then you can overcome some of the awkwardness by enabling the ForceArray option. The POD for the module says

check out ForceArray because you'll almost certainly want to turn it on

In your case, this will make sure that $ref->{SvnUsers}{SvnUser} is always an array reference, even if there is only a single <SvnUser> element at that point in the XML. So $ref->{SvnUsers}{SvnUser}[0] will consistently access the first or only user element.

My advice would be to to upgrade to a more consistent XML module. XML::Smart allows the data to be accessed in a very similar way to the XML::Simple structure, but it makes extensive use of tie to provide a much better DWIM interface. For instance under XML::Smart the first SvnUser of the first SvnUsers element can be accessed using either $ref->{SvnUsers}{SvnUser} or $ref->{SvnUsers}[0]{SvnUser}[0].

Just switching to XML::Smart will fix your immediate problem. For example the code below will list the usernames of all users, whether there is one or many of them (or indeed none, as the object will return an empty list in that case).

My own preference would be a "proper" XML module like XML::LibXML or XML::Twig, but of course the choice is yours.

use strict;
use warnings;

use XML::Smart;

my $ref = XML::Smart->new(\*DATA);

my $users_list= $ref->{SvnRequestUsers}{SvnUsers};

foreach my $key ( @{ $users_list->{SvnUser} } ) {
  print $key->{Username}, "\n";
}

__DATA__
<?xml version="1.0" encoding="utf-8"?>
<SvnRequestUsers>
  <AccessCode>$TR0ngP@ssvv0rd!</AccessCode>
  <SvnUsers>
    <SvnUser>
      <Username>svn.testname</Username>
      <Password>jA=B*+q%</Password>
      <Create>true</Create>
    </SvnUser>
    <SvnUser>
      <Username>svn.testname2</Username>
      <Password>jA=B*+q%</Password>
      <Create>true</Create>
    </SvnUser>
  </SvnUsers>
</SvnRequestUsers>

output

svn.testname
svn.testname2

Upvotes: 1

ysth
ysth

Reputation: 98398

If you are using XML::Simple, you probably want to turn on ForceArray, either globally (which will probably require you to add more array dereferences in your code) or for the specific element that may have one or many nodes; see https://metacpan.org/module/XML::Simple#ForceArray-names-in---important.

Upvotes: 2

Gilles Qu&#233;not
Gilles Qu&#233;not

Reputation: 185161

Try doing this :

#!/usr/bin/perl
use strict;
use warnings;

use XML::XPath;
use XML::XPath::XMLParser;

my $xp = XML::XPath->new(filename => 'file.xml');

my $nodeset = $xp->find('/SvnRequestUsers/SvnUsers/SvnUser'); # find svn users

foreach my $node ($nodeset->get_nodelist) {
    print "FOUND\n\n",
        XML::XPath::XMLParser::as_string($node),
        "\n\n";
}

Result :

FOUND

<SvnUser>
      <Username>svn.testname</Username>
      <Password>jA=B*+q%</Password>
      <Create>true</Create>
    </SvnUser>

FOUND

<SvnUser>
      <Username>svn.testname2</Username>
      <Password>jA=B*+q%</Password>
      <Create>true</Create>
    </SvnUser>

Upvotes: 0

Demnogonis
Demnogonis

Reputation: 3222

Which module do you use to read the XML in the ahref? I'd guess that you'll need to access your data like this if there are more users:

my @usersList = $ref->{'SvnUsers'}->[0]->{'SvnUser'};

This would give you the first user, because there will be an arrayref as value under the 'SvnUsers'-key

An iteration could look like this:

my $maxusers = scalar(@{$ref->{'SvnUsers'}}); # get the number of elements in your ref
for my $i(0 .. $maxusers){
    my $svnuser = $ref->{'SvnUsers'}->[$i]->{'SvnUser'}; # get current
    ....... # do your stuff
}

Upvotes: 0

Related Questions