Perl JSON arrays within a parent key - arrays

I have created a script to get some information from various external sources, the results should then be in json format. There is a lot of data and I push everything to an array in a loop, then print the json array after everything has been completed, an extract of that loop part of the script:
#!/usr/bin/perl
use JSON -convert_blessed_universally;
use strict;
use warnings;
my #json_arr;
my #servers = ("SERVER1", "SERVER2");
my #details = ("SERVER1,10.1.2.3,Suse Linux",
"SERVER2,10.1.2.4,Windows 10",
"SERVER3,10.1.2.5,Windows XP");
my $json = JSON->new->convert_blessed;
foreach my $server(#servers) {
foreach (#details) {
my #detail = split(',',$_);
if ($server eq $detail[0]) {
push #json_arr, {name => "$server", ip => "$detail[1]", os => "$detail[2]"};
}
}
}
my $result = $json->encode(\#json_arr);
print $result;
This gives an output of:
[
{
"name" : "SERVER1",
"ip" : "10.1.2.3",
"os" : "Suse Linux",
},
{
"name" : "SERVER2",
"ip" : "10.1.2.4",
"os" : "Widows 10"
}
]
and a screenshot:
I am however trying to do it by setting a key element instead and having the additional data as children of the device name, i.e:
{
"instance" : [
{
"SERVER1" : {
"ip" : "10.1.2.3",
"os" : "Suse Linux"
},
"SERVER2" : {
"ip" : "10.1.2.4",
"os" : "Windows 10"
}
}
]
}
So I have tried a few things, including something like the below, then pushing to array, but I am getting funny results and just not getting the desired results.
my $json = '{
"instance" : [
$server => {
ip => "$detail[0]",
os => "$detail[1]"
}
]
}';
push #json_arr, $json;

It only takes a small re-arrangement
use warnings;
use strict;
use feature 'say';
use Data::Dumper;
use JSON::XS;
...
my #json_ary;
foreach my $server (#servers) {
foreach (#details) {
my #detail = split /,/;
if ($server eq $detail[0]) {
#push #json_ary, {name => "$server", ip => "$detail[1]" ...
push #json_ary,
{ $server => { ip => $detail[1], os => $detail[2] } }
}
}
}
print Dumper \#json_ary;
# Encode `{ instance => \#json_ary }` for the desired output
my $json = JSON->new->convert_blessed;
my $result = $json->pretty->encode( { instance => \#json_ary } );
say $result;
A few notes
No need for quotes around variables, since they get evaluated anyway ("$detail[0]" --> $detail[0] etc)
No need for quotes around hash keys, as a syntax convenience: key => 'value' is OK (and if the value is a variable it's just key => $var). That is, as long as there are no spaces in the key name
One way to pretty-print an existing JSON string:
print JSON::XS->new->ascii->pretty->encode(decode_json $json_str);
There may be a question of whether a hashref around each server entry (JSON objects) is needed or not; the above was confirmed in a comment after a discussion so I settled with it.
But if the output only needs a hashref with entries for servers in the array (and not an array of hashrefs for each server) then the code in the question can be modified to
my %server_details;
foreach my $server (#servers) {
foreach (#details) {
my #detail = split /,/;
if ($server eq $detail[0]) {
$server_details{$server} = { ip => $detail[1], os => $detail[2] }
}
}
}
Now this hash has details for all servers, and can be encoded with the key instance. Then it is not clear to me what the overall structure should be; one option is
my $result = JSON->pretty->encode( { instance => [ \%server_details ] } );
This would be suitable if there is more data in reality. If not then there may not be any need for an arrayref, but encode the usual
{ instance => \%server_details }

The problem is that you are adding hashes to an array (push #json_arr, ...) when you mean to add to a hash ($instance{ $server_name } = ...).
my %servers = map { $_ => 1 } #servers;
my %instance;
for ( #details ) {
my ( $name, $ip, $os ) = split /,/;
next if !$servers{ $name };
$instance{ $name } = {
ip => $ip,
os => $os,
};
}
my #instances = \%instance;
my $data = { instance => \#instances };
Or using map:
my %servers = map { $_ => 1 } #servers;
my %instance = (
map { $_->[0] => { ip => $_->[1], os => $_->[2] } }
grep $servers{ $_->[0] },
map [ split /,/ ],
#details
);
my #instances = \%instance;
my $data = { instance => \#instances };
This produces the following, as requested:
{
"instance" : [
{
"SERVER1" : {
"ip" : "10.1.2.3",
"os" : "Suse Linux"
},
"SERVER2" : {
"ip" : "10.1.2.4",
"os" : "Windows 10"
}
}
]
}
(This is different than zdim's first solution.)
Note that I got rid of the nested loops because they are nasty. For a few items, it's not a problem. But performance would suffer is #servers or #details would become large. So it's a bad idea, and a bad habit to get into.
Having an array which only even has a single element is weird. Did you perhaps want
{
"instance" : {
"SERVER1" : {
"ip" : "10.1.2.3",
"os" : "Suse Linux"
},
"SERVER2" : {
"ip" : "10.1.2.4",
"os" : "Windows 10"
}
}
}
This would be achieved by replacing
my $data = { instance => \#instances };
with
my $data = { instance => \%instance };

Related

Convert array to multidimensional hash

My task is convert array, containing hash with x keys to x-1 dimensional hash.
Example:
use Data::Dumper;
my $arr = [
{
'source' => 'source1',
'group' => 'group1',
'param' => 'prm1',
'value' => 1,
},
{
'source' => 'source1',
'group' => 'group1',
'param' => 'prm2',
'value' => 2,
},
];
my $res;
for my $i (#$arr) {
$res->{ $i->{source} } = {};
$res->{ $i->{source} }{ $i->{group} } = {};
$res->{ $i->{source} }{ $i->{group} }{ $i->{param} } = $i->{value};
}
warn Dumper $res;
my $res_expected = {
'source1' => {
'group1' => {
'prm1' => 1, # wasn't added, why ?
'prm2' => 2
}
}
};
However it doesn't work as expected, 'prm1' => 1 wasn't added. What is wrong and how to solve this task ?
The problem is that you are assigning to the source even if something was there, and you lose it. Just do a ||= instead of = and you'll be fine.
Or even easier, just use the fact that Perl autovivifies and leave that out.
my $res;
for my $i (#$arr) {
$res->{ $i->{source} }{ $i->{group} }{ $i->{param} } = $i->{value};
}
warn Dumper $res;
The first 2 lines in the for loop are what is causing your problem. They assign a new hash reference each iteration of the loop (and erase what was entered in the previous iteration). In perl, there is no need to set a reference as you did. Just eliminate the first 2 lines and your data structure will be as you wish.
The method you chose only shows 'prmt' => 2 because that was the last item entered.

Clean way to access a nested data structure

I have a segment of code that, although it works, does not look like a clean way to do things.
I build the structure using:
foreach my $n (#node_list)
{
chomp ($n);
foreach my $c (#cpes)
{
my #returned; #Interfaces to CPEs with MED settings
my #creturned; #General Customer Interfaces
my ($cust) = $c =~ /([a-zA-Z]+)[_-][a-zA-Z0-9]+/s;
print "\n\t\tCustomer is $cust\n";
chomp($c);
$c = uc $c;
my ($search) = $c;
(#returned) = `cat /curr/$n | grep "$search"`;
if (#returned)
{
my $cust_match = 'interface \"' . $cust;
(#creturned) = `cat /curr/$n | egrep -i "$cust_match" | grep -v "$search"`;
}
if (#creturned) #Have we found other CPEs on the same router
{
my ($nf) = $n =~ /([a-zA-Z0-9-]+).cfg/s;
my (#interfaces) = map { /([A-Z0-9_]+)/s } #creturned;
#interfaces = uniq(#interfaces);
unshift (#interfaces, $c);
push (#new_out, {$nf => {$cust => [#interfaces]}});
}
}
This will return:
$VAR1 = [
{
'router-xx-xx' => {
'50000' => [
[
'THXXXXVF_NLXXXX40_1121_2',
'10x.xx.x.50'
],
[
'THXXXPVF_NLXXXX66_1121_1',
'10x.xx.x.70'
],
[
'THXXXXVF_NLXXXX67_1121_2',
'10x.xx.x.78'
],
}
},
Each router can have a number of VPRNs and each VPRN can contain multiple interfaces. In the example above I've shown one router with one VPRN.
However, when it comes to accessing elements in the above, I've written the following convoluted (but working) code:
foreach my $candidate (#nodes)
{
my %node = %{ $candidate };
foreach my $n (keys %node)
{
print "\nRouter is $n\n";
foreach my $cust (keys %{ $node{$n} })
{
print "Customer on $n is \n" . Dumper $cust;
my #intlist = #{$node{$n}{$cust}};
my $med_cpe = $intlist[0]; #the CPE that was used to find node
{truncated}
}
}
}
}
You don't explain exactly what you find "convoluted" about the traversal code, but you have made it unnecessarily complex by duplicating data into #intlist and %node. The excessive and inconsistent indentation also makes it ungainly
I would write something closer to this
for my $node ( #nodes ) {
for my $n ( keys %$node ) {
print "\nRouter is $n\n";
for my $cust ( keys %{ $node->{$n} } ) {
print "Customer on $n is \n" . Dumper \$cust;
my $med_cpe = $node->{$n}{$cust}[0];
}
}
}
If you don't need the values of $node and $n except to access $med_cpe then you don't need a nested data structure at all: a simple array is fine. On the face of it, an array like this will do what you need
[
[
'router-xx-xx',
'50000',
'THXXXXVF_NLXXXX40_1121_2',
'10x.xx.x.50',
],
[
'router-xx-xx',
'50000',
'THXXXPVF_NLXXXX66_1121_1',
'10x.xx.x.70',
],
...
]

Dereferencing an array reference from a nested Perl hash

I hope I've stated that subject correctly. I have a hash of hashes that I've built from reading a file. The outer hash is groups, then the inner hash is parameters within that group. Each parameter value can either be a scalar or array, and the arrays can start at either zero or one.
I've written a subroutine that returns the value of a parameter. The calling function has to figure out whether the returned value is a scalar or an array. Works fine for scalars. Returns a reference to an array for array values (looks like ARRAY(0x004f00)). Using Data::Dumper spits out data that looks like an array, but I can't figure out how to dereference it in the code. Can someone point to what I'm doing wrong?
%HoH = (
flintstones => {
husband => "fred",
possessions => [ undef, "car", "record player", "rock" ],
pal => "barney",
pets => [ "bird", "dinosaur" ],
},
);
get_value("possessions");
sub get_value {
my $what_i_want = shift;
#groups = keys(%HoH);
foreach my $group ( #groups ) {
foreach my $param ( keys( %{ HoH {group} } ) ) {
if ( $param eq $what_i_want ) {
return $HoH{$group}{$param};
}
}
}
}
The caller assigns the return value to an array, #return, so in the case of a scalar it should put the value in $return[0].
In the case of an array, it should populate the array. When I call Dumper, it prints out scalars in single quotes and arrays in square brackets, as it should. However, when I use scalar(#return) to check the size of the array, it returns 1.
I've tried dereferencing the return statement using square brackets at the end to see if I could even get a scalar returned, but no luck.
You don't show the subroutine being called in context, but a quick fix would be to put this after the call
#return = #{ $return[0] } if ref $return[0]
Update
You're missing the point of hashes. You can access the correct element of the parameter hash by using $what_i_want as a hash key
I suggest you change your subroutine code to look like this
for my $group ( keys %HoH ) {
my $ret = $HoH{$group}{$what_i_want};
return unless defined $ret;
return ref $ret ? #$ret : $ret;
}
That way it will never return a reference
Update 2
Here's your complete program modified as I suggested
use strict;
use warnings 'all';
my %HoH = (
flintstones => {
husband => "fred",
possessions => [ undef, "car", "record player", "rock" ],
pal => "barney",
pets => [ "bird", "dinosaur" ],
},
);
my #return = get_value('possessions');
use Data::Dump;
dd \#return;
sub get_value {
my ($wanted) = #_;
for my $group ( keys %HoH ) {
my $ret = $HoH{$group}{$wanted};
if ( defined $ret ) {
return ref $ret ? #$ret : $ret;
}
}
return;
}
output
[undef, "car", "record player", "rock"]

Perl: Getting difference between two arrays of hashes?

I have two array references that contain hashes:
$A = [
{
"t" => "1419054300000",
"v" => "28.1"
},
{
"t" => "1419053400000",
"v" => "28.2"
},
{
"t" => "1419052500000",
"v" => "28.4"
}
];
$B = [
{
"t" => "1419053400000",
"v" => "28.2"
},
{
"t" => "1419052500000",
"v" => "28.4"
}
];
I want to get only the hashes from $A where their value of t doesn't already exist in one of the hashes in $B (the t values are unique per arrayref, v isn't).
I assume there's some obvious method of doing this, but I've been banging my head against this all day without success.
You can use the perl5i diff method.
use perl5i::2;
...initialize $A and $B...
say $A->diff($B)->mo->as_perl;
__END__
[
{
't' => '1419054300000',
'v' => '28.1'
}
]
As always you can build hash look up where keys are elements you want to filter out,
my %seen;
#seen{ map $_->{t}, #$B } = ();
my $C = [
grep { !exists $seen{$_->{t}} } #$A
];

puppet define array loop

I am playing with define array but not understand why its not working in following example. I am trying to create following directions
/tmp/foo.0
/tmp/foo.1
/tmp/foo.2
My init.pp
class loop {
define loop ( $x ) {
exec {"$name":
command => "/bin/mkdir /tmp/$name.$x",
creates => "/tmp/$name.$x",
}
}
loop{ "foo": x => ["0", "1", "2"] }
}
Its creating directory like /tmp/tomcat7.012
Not sure this is a good idea to name the define with same name than class.
It would work if you invert the $name and $x
class loop {
define loop::loop ( $x ) {
exec {"$x":
command => "/bin/mkdir /tmp/$x.$name",
creates => "/tmp/$x.$name",
}
}
loop::loop{ ["0", "1", "2"]: x => "foo" }
}
Hope this helps

Resources