Hash in array in a hash - arrays

I'm trying to identify the output of Data::Dumper, it produces the output below when used on a hash in some code I'm trying to modify:
print Dumper(\%unholy_horror);
$VAR1 = {
'stream_details' => [
{
'file_path' => '../../../../tools/test_data/',
'test_file' => 'test_file_name'
}
]
};
Is this a hash inside an array inside a hash? If not what is it? and what is the syntax to access the "file path" and "test_file" keys, and their values.
I want to iterate over that inner hash like below, how would I do that?
while ( ($key, $value) = each %hash )
{
print "key: $key, value: $hash{$key}\n";
}

You're correct. It's a hash in an array in a hash.
my %top;
$top{'stream_details'}[0]{'file_path'} = '../../../../tools/test_data/';
$top{'stream_details'}[0]{'test_file'} = 'test_file_name';
print Dumper \%top;
You can access the elements as above, or iterate with 3 levels of for loop - assuming you want to iterate the whole thing.
foreach my $topkey ( keys %top ) {
print "$topkey\n";
foreach my $element ( #{$top{$topkey}} ) {
foreach my $subkey ( keys %$element ) {
print "$subkey = ",$element->{$subkey},"\n";
}
}
}
I would add - sometimes you get some quite odd seeming hash topologies as a result of parsing XML or JSON. It may be worth looking to see if that's what's happening, because 'working' with the parsed object might be easier.
The above might be the result of:
#JSON
{"stream_details":[{"file_path":"../../../../tools/test_data/","test_file":"test_file_name"}]}
Or something similar from an API. (I think it's unlikely to be XML, since XML doesn't implicitly have 'arrays' in the way JSON does).

Related

Merge Perl hashes into one array and loop through it

I'm creating a Perl plugin for cPanel which has to get all domains in the account of a user and display it in a HTML select field. Originally, I'm a PHP developer, so I'm having a hard time understanding some of the logic of Perl. I do know that cPanel plugins can also be written in PHP, but for this plugin I'm limited to Perl.
This is how I get the data from cPanel:
my #user_domains = $cpliveapi->uapi('DomainInfo', 'list_domains');
#user_domains = $user_domains[0]{cpanelresult}{result}{data};
This is what it looks like using print Dumper #user_domains:
$VAR1 = {
'addon_domains' => ['domain1.com', 'domain2.com', 'domain3.com'],
'parked_domains' => ['parked1.com', 'parked2.com', 'parked3.com'],
'main_domain' => 'main-domain.com',
'sub_domains' => ['sub1.main-domain.com', 'sub2.main-domain.com']
};
I want the data to look like this (thanks #simbabque):
#domains = qw(domain1.com domain2.com domain3.com main-domain.com parked1.com parked2.com parked3.com);
So, I want to exclude sub_domains and merge the others in 1 single-dimensional array so I can loop through them with a single loop. I've struggled the past few days with what sounds like an extremely simple task, but I just can't wrap my head around it.
You need something like this
If you find you have a copy of List::Util that doesn't include uniq then you can either upgrade the module or use this definition
sub uniq {
my %seen;
grep { not $seen{$_}++ } #_;
}
From your dump, the uapi call is returning a reference to a hash. That goes into $cp_response and then drilling down into the structure fetches the data hash reference into $data
delete removes the subdomain information from the hash.
The lists you want are the values of the hash to which $data refers, so I extract those. Those values are references to arrays of strings if there is more than one domain in the list, or simple strings if there is only one
The map converts all the domain names to a single list by dereferencing array references, or passing strings straight through. That is what the ref() ? #$_ : $_ is doing. FInally uniq removes multiple occurrences of the same name
use List::Util 'uniq';
my $cp_response = $cpliveapi->uapi('DomainInfo', 'list_domains');
my $data = $cp_response->{cpanelresult}{result}{data};
delete $data->{sub_domains};
my #domains = uniq map { ref() ? #$_ : $_ } values %$data;
output
parked1.com
parked2.com
parked3.com
domain1.com
domain2.com
domain3.com
main-domain.com
That isn't doing what you think it' doing. {} is the anonymous hash constructor, so you're making a 1 element array, with a hash in it.
You probably want:
use Data::Dumper;
my %user_domains = (
'addon_domains' => ['domain1.com', 'domain2.com', 'domain3.com'],
'parked_domains' => ['parked1.com', 'parked2.com', 'parked3.com'],
'main_domain' => 'main-domain.com',
'sub_domains' => ['sub1.main-domain.com', 'sub2.main-domain.com'],
);
print Dumper \%user_domains;
And at which point the 'other' array elements you can iterate through either a double loop:
foreach my $key ( keys %user_domains ) {
if ( not ref $user_domains{$key} ) {
print $user_domains{$key},"\n";
next;
}
foreach my $domain ( #{$user_domains{$key}} ) {
print $domain,"\n";
}
}
Or if you really want to 'flatten' your hash:
my #flatten = map { ref $_ : #$_ ? $_ } values %user_domains;
print Dumper \#flatten;
(You need the ref test, because without it, the non-array main-domain won't work properly)
So for the sake of consistency, you might be better off with:
my %user_domains = (
'addon_domains' => ['domain1.com', 'domain2.com', 'domain3.com'],
'parked_domains' => ['parked1.com', 'parked2.com', 'parked3.com'],
'main_domain' => ['main-domain.com'],
'sub_domains' => ['sub1.main-domain.com', 'sub2.main-domain.com'],
);

How to get first n values from perl Hash of arrays

Experts,
I have a hash of array in perl which I want to print the first 2 values.
my %dramatis_personae = (
humans => [ 'hamnet', 'shakespeare', 'robyn', ],
faeries => [ 'oberon', 'titania', 'puck', ],
other => [ 'morpheus, lord of dreams' ],
);
foreach my $group (keys %dramatis_personae) {
foreach (#{$dramatis_personae{$group}}[0..1]) { print "\t$_\n";}
}
The output I get is
"hamnet
shakespeare
oberon
titania
morpheus
lord of dreams"
which is basically first two array values for each key. But I am looking to have the output as:
hamnet
shakespeare
Please advise how I can get this result. Thanks!
Keys of hashes are not ordered, so you should specify keys ordering by yourself. Then you can concatenate arrays from each key specified and take first two values from resulting array, is it what you want ?
print "\t$_\n" foreach (map {(#{$dramatis_personae{$_}})} qw/humans faeries other/)[0..1];
Hashes are unordered, so what you requested to achieve is impossible. Unless you have some knowledge about the keys and the order they should be in, the closest you can get is something that can produce any of the following:
'hamnet', 'shakespeare'
'oberon', 'titania'
'morpheus, lord of dreams', 'hamnet'
'morpheus, lord of dreams', 'oberon'
The following is an implementation that does just that:
my $to_fetch = 2;
my #fetched = ( map #$_, values %dramatis_personae )[0..$to_fetch-1];
The following is a more efficient version for larger structures. It also handles insufficient data better:
my $to_fetch = 2;
my #fetched;
for my $group (values(%dramatis_personae)) {
if (#$group > $to_fetch) {
push #fetched, #$group[0..$to_fetch-1];
$to_fetch = 0;
last;
} else {
push #fetched, #$group;
$to_fetch -= #$group;
}
}
die("Insufficient data\n") if $to_fetch;

Accessing returned values as an array

I have simple XML that I want to read in Perl and make hash containing all read keys.
Consider this code:
my $content = $xml->XMLin("filesmap.xml")->{Item};
my %files = map { $_->{Path} => 1 } #$content;
This snippet works great when XML file contains many Item tags. Then $content is a reference to array. But when there is only one Item I get an error when dereferencing as array. My assumption is that $content is a reference to the scalar, not array.
What is the practice to make sure I get array of values read from XML?
What you need is to not use XML::Simple and then it's really trivial. My favourite for fairly straightforward XML samples is XML::Twig
use XML::Twig;
my $twig = XML::Twig -> new -> parsefile ( 'filesmap.xml' );
my #files = map { $_ -> trimmed_text } $twig -> get_xpath ( '//Path' );
With a more detailed XML sample (and desired result) I'll be able to give you a better answer.
Part of the problem with XML::Simple is it tries to turn an XML data structure into perl data structures, and - because hashes are key-values and unordered but arrays are ordered, it has to guess. And sometimes it does it wrong, and other times it's inconsistent.
If you want it to be consistent, you can set:
my $xml = XMLin( "filesmap.xml", ForceArray => 1, KeyAttr => [], ForceContent => 1 );
But really - XML::Simple is just a route to pain. Don't use it. If you don't like XML::Twig, try XML::LibXML instead.
What I would say you need is a flatten-ing step.
my %files
= map { $_->{Path} => 1 }
# flatten...
map { ref() eq 'ARRAY' ? #$_ : $_ }
$xml->XMLin("filesmap.xml")->{Item}
;
You can do a check and force the return into an array reference if necessary:
my $content = $xml->XMLin("filesmap.xml")->{Item};
$content = ref $content eq 'ARRAY'
? $content
: [$content];

Referenced array dropped in size to one element

Dear fellow perl programmers,
I wanted to access to this array
my #vsrvAttribs = qw(
Code
Description
vsrv_id
vsrv_name
vsrv_vcpu_no
vsrv_vmem_size
vsrv_vdspace_alloc
vsrv_mgmt_ip
vsrv_os
vsrv_virt_platf
vsrv_owner
vsrv_contact
vsrv_state
);
through a variable composed of a variable and a string suffix, which of course led to the error message like this
Can't use string ("#vsrvAttribs") as an ARRAY ref while "strict refs" in use at cmdbuild.pl line 262.`
Therefore I decided to get the reference to the array through a hash
my %attribs = ( vsrv => #vsrvAttribs );
And this is the code where I need to get the content of aforementioned array
foreach my $classTypeKey (keys %classTypes) {
my #attribs = $attribs{$classTypeKey};
print Dumper(\#attribs);
}
It seems I can get the reference to the array #vsrvAttribs, but when I checked the content of the array with Dumper , the array have got only one element
$VAR1 = [
'Code'
];
Do you have any idea where could be the problem?
How do you store the array in a hash and access it later?
You need to store your array by reference like this:
my %attribs = ( vsrv => \#vsrvAttribs );
Note the backslash before the # sigil. This tells perl that you want a reference to the array.
Then when access the array stored in $attribs{vsrv} you need to treat it as a reference instead of as an array. You'll do something like this:
foreach my $classTypeKey (keys %classTypes) {
# make a copy of the array by dereferencing
my #attribs = #{ $attribs{$classTypeKey} };
# OR just use the array reference if profiling shows performance issues:
my $attribs = $attribs{$classTypeKey}
# these will show the same thing if you haven't done anything to #attribs
# in the interim
print Dumper(\#attribs);
print Dumper($attribs);
}
Why did you only get one value and where did the rest of the array go?
Your missing values from #vsrvAttribs weren't lost they were assigned as keys and values to %attribs itself. Try adding the following just after you made your assignment and you'll see it for yourself:
my %attribs = ( vsrv => #vsrvAttribs );
print Dumper(\%attribs);
You'll see output like this:
$VAR1 = {
'vsrv_contact' => 'vsrv_state',
'vsrv_virt_platf' => 'vsrv_owner',
'vsrv' => 'Code',
'vsrv_name' => 'vsrv_vcpu_no',
'vsrv_mgmt_ip' => 'vsrv_os',
'Description' => 'vsrv_id',
'vsrv_vmem_size' => 'vsrv_vdspace_alloc'
};
This is because perl interpreted your assignment by expanding the contents #vsrvAttribs as multiple arguments to the list literal ():
my %attribs = (
# your key => first value from array
vsrv => 'Code',
# subsequent values of the array
Description => 'vsrv_id',
vsrv_name => 'vsrv_vcpu_no',
vsrv_vmem_size => 'vsrv_vdspace_alloc',
vsrv_mgmt_ip => 'vsrv_os',
vsrv_virt_platf => 'vsrv_owner',
vsrv_contact => 'vsrv_state',
);
This is legal in perl and there are reasons where you might want to do this but in your case it wasn't what you wanted.
Incidentally, you would have been warned that perl was doing something that you might not want if you had an even number of elements in your array. Your 13 elements plush the hash key "vsrv" makes 14 which is even. Perl will take any list with an even number of elements and happily make it into a hash. If your array had another element for 15 elements total with the hash key you would get a warning: Odd number of elements in hash assignment at foo.pl line 28.
See "Making References" and "Using References" in perldoc perlreftut for more information.
If you use a bare array in a hash definition like
my %attribs = ( vsrv => #vsrv_attribs )
the array is expanded and used as key/value pairs, so you will get
my %attribs = (
vsrv => 'Code',
Description => 'vsrv_id',
vsrv_name => 'vsrv_vcpu_no',
vsrv_vmem_size => 'vsrv_vdspace_alloc',
...
)
The value of a Perl hash element can only be a scalar value, so if you want an array of values there you have to take a reference, as shown below
It is also a bad idea to use capitals in Perl identifiers for anything except globals, such as package names. Local names are conventional lower-case alphanumeric plus underscore, so $class_type_key instead of $classTypeKey
use strict;
use warnings;
use Data::Dumper;
my #vsrv_attribs = qw(
Code
Description
vsrv_id
vsrv_name
vsrv_vcpu_no
vsrv_vmem_size
vsrv_vdspace_alloc
vsrv_mgmt_ip
vsrv_os
vsrv_virt_platf
vsrv_owner
vsrv_contact
vsrv_state
);
my %attribs = (
vsrc => \#vsrv_attribs,
);
for my $class_type_key (keys %attribs) {
my $attribs = $attribs{$class_type_key};
print Dumper $attribs;
}
output
$VAR1 = [
'Code',
'Description',
'vsrv_id',
'vsrv_name',
'vsrv_vcpu_no',
'vsrv_vmem_size',
'vsrv_vdspace_alloc',
'vsrv_mgmt_ip',
'vsrv_os',
'vsrv_virt_platf',
'vsrv_owner',
'vsrv_contact',
'vsrv_state'
];

Looping through known elements in a hash of hashes of arrays

I have a question I am hoping someone could help with (simplified for the purposes of explaining my question).
I have the following hash of hashes of arrays (I think that is what it is anyway?)
Data structure
{
Cat => {
Height => ["Tiny"],
},
Dog => {
Colour => ["Black"],
Height => ["Tall"],
Weight => ["Fat", "Huge"],
},
Elephant => {
Colour => ["Grey"],
Height => ["Really Big"],
Weight => ["Fat", "Medium", "Thin"],
},
}
What I am trying to do
The program below will print the whole data structure. I want to use this kind of way to do it
my %h;
for my $animal (keys %h) {
print "$animal\n";
for my $attribute ( keys %{$h{$animal}} ) {
print "\t $attribute\n";
for my $i (0 .. $#{$h{$animal}{$attribute}} ) {
print "\t\t$h{$animal}{$attribute}[$i]\n";
}
}
}
The problem I am having
I am trying to access a particular part of the data structure. For example, I want to only print out the Height arrays for each animal as I do not care about the other Colour, Weight attributes in this example.
I'm sure there is a simple answer to this, and I know I need to specify the Height part, but what is the correct way of doing it? I have tried multiple ways that I thought would work without success.
In your code, instead of looping over all the attributes with
for my $attribute ( keys %{ $h{$animal} } ) { ... }
just use the one you are interested in. Like this
for my $animal (keys %h) {
print "$animal\n";
for my $attribute ( 'Height' ) {
print "\t $attribute\n";
for my $i (0 .. $#{$h{$animal}{$attribute}} ) {
print "\t$h{$animal}{$attribute}[$i]\n";
}
}
}
I would choose to loop over the contents of the heights array rather than the indices, making the code look like this:
for my $animal (keys %h) {
print "$animal\n";
print "\t\t$_\n" for #{ $h{$animal}{Height} };
}
Taking a quick look at your data structure: It's a hash of hashes of arrays! Wow. Mind officially blown.
Here's a quick way of printing out all of the data:
use feature qw(say);
# Working with a Hash of Hash of Arrays
for my $animal (keys %h) {
say "Animal: $animal";
# Dereference: Now I am talking about a hash of arrays
my %animal_attributes = %{ $h{$animal} };
for my $attribute (keys %animal_attributes) {
# Dereference: Now I am talking about just an array
my #attribute_value_list = #{ $animal_attributes{$attribute} };
say "\tAttribute: $attribute - " . join ", ", #attribute_value_list;
}
}
Note I use dereferencing. I don't have to do the dereference, but it makes the code a bit easier to work with. I don't have to think of my various levels. I know my animal is a hash of attributes, and those attributes are an array of attribute values. By using dereferencing, it allows me to keep things straight.
Now, let's say you want to print out only a list of desirable attributes. You can use the exists function to see if that attribute exists before trying to print it out.
use feature qw(say);
use constant DESIRED_ATTRIBUTES => qw(weight height sex_appeal);
# Working with a Hash of Hash of Arrays
for my $animal (keys %h) {
say "Animal: $animal";
# Dereference: Now I am talking about a hash of arrays
my %animal_attributes = %{ $h{$animal} };
for my $attribute ( DESIRED_ATTRIBUTES ) {
if ( exists $animal_attributes{$attribute} ) {
# Dereference: Now I am talking about just an array
my #attribute_value_list = #{ $animal_attributes{$attribute} };
say "\tAttribute: $attribute - " . join ", ", #attribute_value_list;
}
}
}
Same code, I just added an if clause.
When you get into these complex data structures, you might be better off using Object Oriented design. Perl has an excellent tutorial on OOP Perl. If you used that, you could have defined a class of animals and have various methods to pull out the data you want. It makes maintenance much easier and allows you to bravely create even more complex data structures without worrying about tracking where you are.
I think sometimes it's easier to use the value directly, if it is a reference to another structure. You could do something like:
my $height = "Height";
while (my ($animal, $attr) = each %h) {
print "$animal\n";
print "\t$height\n";
print "\t\t$_\n" for #{ $attr->{$height} };
}
Using the value of the main keys, you can skip over one step of references and go straight at the Height attribute. The output below is after the format you had in your original code.
Output:
Elephant
Height
Really Big
Cat
Height
Tiny
Dog
Height
Tall
Assuming your variable is called %h:
foreach my $animal (keys %h) {
my $heights = $h{$animal}->{Height}; #gets the Height array
print $animal, "\n";
foreach my $height( #$heights ) {
print " ", $height, "\n";
}
}
I think I have worked it out and found what I was doing wrong?
This is how I think it should be:
my %h;
for my $animal (keys %h) {
print "$animal\n";
for my $i (0 .. $#{$h{$animal}{Height}} ) {
print "\t\t$h{$animal}{Height}[$i]\n";
}
}

Resources