简体   繁体   中英

Perl JSON arrays within a parent key

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, ie:

{
  "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 hashrefs 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 server entries 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 ] }  );

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 };

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM