Home > Software engineering >  Perl ... create horizontal children of a %hash using @array items
Perl ... create horizontal children of a %hash using @array items

Time:04-28

I've been banging my head on this awhile and searched many ways. I'm sure this is going to boil down to being really basic.

I have data in an @array that I want to move to a tree in a %hash.

This might be something more appropriate to JSON? But I haven't delved into it before and I don't need to save out/restore this information.

Desire:

Create a dependent tree of USB devices that can nest under each other that can track the end point (deviceC) through a hub (deviceB) and finally the root (deviceA).

Example:

Simplified (I hope ... this isn't from the actual longer script):

I want to convert an array in this format:

my @array = ['deviceA','deviceB','deviceC'];

to multidimensional hashes equal to:

my %hash = ('deviceA' => { 'deviceB' => { 'deviceC' => '' } } )

that would dump like:

$VAR1 = {
          'deviceA' => {
                         'deviceB' => {
                                        'deviceC' => ''
                                      }
                       }
        };

For just looking at a single device this isn't necessary, but I'm building out an IOMMU -> PCI Device -> USB map that contains many devices.

NOTES:

  • I'm trying to avoid installing CPAN modules so the script is to similar systems (Proxmox VE)
  • The last device (deviceC above) has no children
    • value '' is fine
    • undef would probably work
    • mixing the types would work but I need to know how to set that
  • I will never need to modify or manipulate the hash once created
  • I don't know the right way to recurse the @array to populate the %hash children. * I want the data horizontal for each USB device
  • I'd switch to an Object/package but each device can have a different set of children (or none) making it infeasible to know Object names
    • Some USB devices have no children (root hubs) ... similar to %hash = ('deviceA' => '')
    • Some have 1 child that is the final device ... similar to %hash = ('deviceA' => { 'deviceB' =>'' } )
    • Some have multiple steps between the root via additional hub(s) ... similar to %hash = ('deviceA' => { 'deviceB' => { 'deviceC' => '' } } ) or more

Starting point :

This is basic and incomplete but will run:

#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper qw(Dumper);

# data in from parsing usb device path:
my @array = ['deviceA','deviceB','deviceC'];
# needs to be converted to:
my %hash = ('deviceA' => { 'deviceB' => { 'deviceC' => '' } } );

print "\n\%hash:\n" . Dumper \%hash;

Pseudo-code

This section is NOT working code in any form. I'm just trying to make a note of what I'm thinking. I know the format is wrong, I've tried multiple ways to create this and I'd look even dumber showing all of my attempts :)

I'm very new to refs and I'm not going to try and get that right here. The idea below is:

  • For each item in @array:
    • Create a way (either a ref or a copy of the current hash) that can be used next iteration to place the next child
    • Attach item as a child of the previous iteration with an empty value (that can be appended if there is further iteration)
my @array = ['deviceA','deviceB','deviceC'];
my %hash = {};
my %trackref;
for (@array) {
  %trackref = %hash; # a copy of the existing that won't change when %hash updates
  $hash{last_child} ::append_child:: $_;
}

CodePudding user response:

You're actually pretty close, but it seems that you need to understand references a bit better. perldoc perlref is probably a good starting point to understand references.

A few mistakes in your code, before looking at the solution:

  • my @array = [ ... ];: [] creates an arrayref, not an array, which means that @array actually stores a single scalar item: a reference to another array. Use () to initialize an array: my @array = ( ... );.

  • my %hash = {};: similarly, {} creates a hashref, not a hash. Which means that this lines stores a single hashref in %hash, which will cause this warning: Reference found where even-sized list expected at hash.pl line (because a hash contains keys-values and you only provided a key). Use () for a simple (ie, not a hashref) hash. In this case however, you don't need to initialize %hash: my %hash; and my %hash = () do the same thing (that is, create an empty hash).

  • %trackref = %hash; copies the content of %hash in %trackref. Which means that, contrary to what the name "trackref" implies, %trackref doesn't contain a reference to anything, but a copy of %hash. Use \%hash to create a reference to %hash.
    Note that if you already have a hashref, then assigning it to another variables copies the reference. For instance, if you do my $hash1 = {}; my $hash2 = $hash1, then both $hash1 and $hash2 reference the same hash.

So, fixing those issues in your attempt, we get:

my @array = ('deviceA','deviceB','deviceC');
my %hash;
my $trackref = \%hash;
for my $usb (@array) {
    $trackref->{$usb} = {};
    $trackref = $trackref->{$usb};
}

print Dumper \%hash;

Which outputs:

$VAR1 = {
          'deviceA' => {
                         'deviceB' => {
                                        'deviceC' => {}
                                      }
                       }
        };

The main change that I did was to replace your $hash{last_child} ::append_child:: $_; by $trackref->{$_} = {};. But the idea remains the same: Attach item as a child of the previous iteration with an empty value to reuse your words.

To help you understand the code a bit better, let's see what happens in the loop step by step:

  • Before the first iteration, %hash is empty and $trackref references %hash.

  • In the first iteration, we put deviceA => {} in $trackref (or, more pedantically, we associate {} with the key deviceA in $trackref). Since $trackref references %hash, this puts deviceA => {} in %hash. Then, we store in $trackref this new {} that we just created, which means that $trackref now references $hash{deviceA}.

  • In the second iteration, we put deviceB => {} in $trackref. $trackeref references $hash{deviceA} (which we created in the previous iteration), which means that %hash is now (deviceA => { deviceB => {} }). We then store in $trackref the new {}.

  • And so on...

You'll note that in the innermost hash, {} is associated to the key deviceC. When iterating of the hash, you can thus know if you are at the end by doing something like if (%$hash) (instead of just if ($hash) if this last {} would have been undef or ''). Let me know if that's an issue: we can add a bit of code to convert this {} into undef (alternatively, you can do it yourself, it will be a good exercise to get used to references)


Minor remark: @array and %hash are poor array and hash names, because the @ already indicates an array, and % already indicates a hash. It's possible that you used those names just for this small example for your question, in which case, no problem. However, if you use those names in your actual code, consider changing them for something more explicit... @usb_devices and %usb_devices_tree maybe?

  • Related