1

I am writing a subroutine that uses the XML::Simple module to pull out information from a config file.

The XML file has the following structure

<main>
  <server hostname="blahblah" ... more_attributes="more"/>
  <server etc./>
  <server etc./>
</main>

The code works fine. It puts the XML data into a nested hash as expected. But when I want to isolate one server from the rest using the current system's hostname I run into issues.

I think this line

my %systemHash = %{$xmlObject->{SERVER}->{`hostname`}};

should insert the current machine's hostname into the last set of brackets. However when execute the code I get

Can't use an undefined value as a HASH reference
Borodin
  • 126,100
  • 9
  • 70
  • 144
Andrew
  • 397
  • 1
  • 3
  • 13
  • Can you share the code that reads the XML? There are several options that change XML::Simple's behavior. – Jim Davis Jun 16 '14 at 22:58
  • To clarify: With the defaults, `XMLin` gives me a hash of an array of hashes, and not nested hashes. – Jim Davis Jun 16 '14 at 23:03
  • 1
    @JimDavis: Most of all it is important to abide by the wishes of the author, Grant McLean, of the `XML::Simple` module, whoe says *"The use of this module in new code is discouraged"* – Borodin Jun 16 '14 at 23:58
  • BTW, Perl core module `Sys::Hostname` exports native function `hostname`, which works without need to use backticks or external commands – mvp Jun 17 '14 at 00:00

2 Answers2

5

First of all, please don't use XML::Simple. Its own documentation says this

The use of this module in new code is discouraged. Other modules are available which provide more straightforward and consistent interfaces. In particular, XML::LibXML is highly recommended.

The major problems with this module are the large number of options and the arbitrary ways in which these options interact - often with unexpected results.

You should also check the result of any subroutine call or shell command using Data::Dump. It would look like this

perl -MData::Dump -E'dd `hostname`'

which on my system shows

"Samurai-U\n"

Hopefully you now see the problem? The string returned by the backticks has a trailing newline character, and there is element in your $xmlObject hash that has a key like that. You can fix it with

chomp(my $host = `hostname`)

after which you could write

my %systemHash = %{ $xmlObject->{SERVER}{$host} }

Finally, it is wasteful to copy all of the first level of a hash as you do here

my %systemHash = %{$xmlObject->{SERVER}->{`hostname`}}

You don't go on to show what you want to use this information for, but in general it is far better to extract a hash reference, using

chomp( my $hostname = `hostname` );
my $systemHash = $xmlObject->{SERVER}{$hostname};

Update

It would be very much better to use a proper XML-parsing module.

Here is an example solution using XML::LibXML

use strict;
use warnings;
use 5.010;    # For 'say'

use XML::LibXML;

my ($xml_file) = @ARGV;

my $xml = XML::LibXML->load_xml(location => $xml_file);

my @servers      = $xml->findnodes('/main/server');
my @server_names = map $_->findvalue('@hostname'), @servers;

say "- $_" for @server_names;

input file

<main>
  <server hostname="server1" more_attributes="more"/>
  <server hostname="server2" more_attributes="more"/>
  <server hostname="server3" more_attributes="more"/>
</main>

output

- server1
- server2
- server3
Community
  • 1
  • 1
Borodin
  • 126,100
  • 9
  • 70
  • 144
  • thanks for the perl -MData::Dump -E'dd `hostname`' example. I have never ran across that, very helpful. – Andrew Jun 17 '14 at 15:18
3

You should use Data::Dumper to dump the output from XML::Simple like this:

use Data::Dumper;
# Retrieve your data structure via XML::Simple. Then...

print Dumper $xmlObject;

You will see that it's not creating the structure that you expect, which is why you're getting the Can't use an undefined value as a HASH reference message. That message means that either $xmlObject is undefined, or that $xmlObject->{SERVER} is undefined. perldiag describes the error as:

A value used as either a hard reference or a symbolic reference must be a defined value. This helps to delurk some insidious errors.

You are treating an undefined value as a hash reference. And as mentioned, cause is probably that the structure XML::Simple produced differs from your expectations.

XML::Simple isn't as simple as its name implies, nor as one would hope. And currently its own documentation discourages its use. There are configuration options which help to normalize the structure it produces, but without seeing more of your XML, and the code you're using to read it, that's about as detailed an explanation as I can give. The best advice with respect to how you're parsing your XML is to use a more reliable module. Two very common and well-regarded alternatives are XML::Twig and XML::LibXML.

There's an additional bug in your code, that you will encounter next: The hostname returned by your system call to 'hostname' has a newline at the end, so you're asking for a hash key named:

"somehost\n"

...when you really want just "somehost". I suspect you're not counting on that. Minimally, you'll need something like this:

chomp( my $h = `hostname` );
my %systemHash = %{$xmlObject->{SERVER}->{$h}};

Or this:

use Sys::Hostname;
my %systemHash = %{$xmlObject->{SERVER}->{hostname()};

I would prefer the latter, as it's more portable, and the module Sys::Hostname comes with Perl, so you already have it on your system. Its output will not require chomping.

DavidO
  • 13,812
  • 3
  • 38
  • 66
  • 1
    Thanks @DavidO for this answer. While others may have provided similar information you answered the actual question I was asking and suggested another option. In this case using `Sys::Hostname` and `hostname()` Solved my problem. The suggestion concerning `XML::LibXML` was good. After looking into the module further I think it is a little overkill for the very simple parsing I need here. Thanks again! – Andrew Jun 17 '14 at 15:16