1

I am going to keep my question high level, because that is me, that is my style, and that is what I need answer for.

I have a multi-level hash to work with (hash of hashes). I also have algorithms that give me a sequence of keys in @key (to various values to look up).

My technique to access individual values is:

Simply build an expression looking like

 $h-> {$key[0] }=> {$key[1]} => ... e.t.c.

and then "eval" that expression.

Are there better techniques to deal with variable key sequences, without eval?

( The hash is a mirror of a directory structure. Values are individual files, and my program needs to read the content of those files.)

I tried and it works with the eval option.

Gilles Quénot
  • 173,512
  • 41
  • 224
  • 223
  • 1
    Why do you need to `eval` those "individual files" (I presume filenames?) Can just access the value in the data structure, `$h{k1}{k2}...{filename}`, and read the file ...? i guess I am not getting something. There's also modules for drilling down data structures but I guess tht `eval` is the question. – zdim May 03 '23 at 23:32
  • 1
    I'm pretty sure you meant to write `$h-> {$key[0] } -> {$key[1]} -> ...` please correct your question, because that doesn't make much sense else. – LanX May 04 '23 at 20:22

5 Answers5

7

Data::Diver makes this easy.

use Data::Diver qw( DiveVal Dive );

my $h;

DiveVal( $h, map \$_, @keys ) = 123;

say Dive( $h, map \$_, @keys );   # 123

You can use ${ DiveRef( ... ) } instead of DiveVal.


You could also use the following:

sub dive {
   my $x = shift;
   $x &&= $x->{ $_ } for @_;
   $x
}

sub dive_ref {
   my $p = \shift;
   $p = \( ($$p)->{ $_ } ) for @_;
   $p
}

sub dive_val :lvalue {
   my $p = \shift;
   $p = \( ($$p)->{ $_ } ) for @_;
   $$p
}

my $h;

dive_val( $h, @keys ) = 123;

say dive( $h, @keys );   # 123

You can use ${ dive_ref( ... ) } instead of dive_val.


dive_ref can be adapted to create a data structure from a file tree.

sub load_data {
   my $p       = \shift;
   my $dir_qfn =  shift;

   my ( @d_fns, @f_fns );

   {
      opendir( my $dh, $dir_qfn )
         or die( "Can't open `$dir_qfn`: $!\n" );

      while ( defined( my $fn = readdir( $dh ) ) ) {
         next if $fn =~ /^\./;

         my $qfn = "$dir_qfn/$fn";
         stat( $qfn )
            or die( "Can't stat `$qfn`: $!\n" );

         push @{ -d _ ? \@d_fns : \@f_fns }, $fn;
      }
 
      closedir( $dh )
         or die( "Error reading `$dir_qfn`: $!\n" );
   }

   ($$p)->{ ".files" } = \@f_fns;  # Or whatever.

   load_data( ($$p)->{ $_ }, "$dir_qfn/$_" ) for @d_fns;
}

load_data( my $data, "." );
ikegami
  • 367,544
  • 15
  • 269
  • 518
4

If you're evaling a string, there is almost certainly a better way. If you already have a list of keys, you can walk the hash until you hit something that is not another hash.

my @keys = qw(one two three);
my $h = {
  one => {
    two => {three => 42, bar => 23}
  }
};
while( ref $h eq 'HASH' ) {
  $h = $h->{shift @keys};
}
print $h;  # 42

The hash is a mirror of a directory structure.

For your particular case, use Path::Tiny, File::Find, File::Find::Rule, or similar module to walk the directory structure. This will avoid having to load the whole directory structure into memory, and it handles the walk for you.

In the more general case, there are modules such as Data::Walk to traverse a data structure.

If you want to do it by hand, walk the hash recursively.

sub handle_files {
  my $dir = shift;

  for my $entry (values %$dir) {
    if( ref $entry eq 'HASH' ) {
      # It's a directory, recurse.
      handle_files($entry);
    }
    elsif( ref $entry eq '' ) {
      print "It's a file named $entry\n";
    }
    else {
      die "Unexpected reference found: $entry";
    }
  }
}
Schwern
  • 153,029
  • 25
  • 195
  • 336
  • Data::Walk helps a lot. Usually it should be a toss-up between Dirwalk and Data::walk. The trees we deal with are small -less than 500 files. The eccentric fellow who designed the system believed that everything in UNIX should be a file. So his file names actually contain math formulas to be calculated (You get it? File names with formulas in them! So, I am going with hashes. – pkrauss6171 May 04 '23 at 03:51
1

use reduce from List::Util

something like

use List::Util qw/reduce/;
my @keys = qw/a b c/;
my $h = +{ a => { b => { c => 1 } } };
my $final = reduce { $a->{$b} || {} }  $h, @keys;
roger
  • 41
  • 1
0

For completeness, Perl supports Multi-dimensional hashes which make your task quite easy

Usage:

$h{ join( $; , @keys ) }  = 1;

# or when the number is known
$h{ $keys[0], $keys[1], $keys[2], ... } = 1
  • Pro: they can be more performant and memory efficient than Hashes of Hashes
  • Con: you have to make sure that $; (or another special character) can't be part of your keys.

It really depends on your use case.

LanX
  • 478
  • 3
  • 10
0

A recursive solution would seem intuitive to me, and I imagine it to be easier-to-understand than the string-building-then-eval solution.

use v5.36;
use Ref::Util qw(is_hashref);

sub get ($h, $ks) {
    return $h if @$ks == 0 || !is_hashref($h);

    my $k = shift @$ks;
    return get($h->{$k}, $ks);
}

my $h = +{ a => { b => { c => 1 } } };

my $v;
$v = get($h, [qw( a b c )]); #=> 1
$v = get($h, [qw( x y z )]); #=> undef

OTOH if the data in $h mirrors a file system, I don't see why it cannot be a simple, non-nested hash, while keys being the full path such as "/foo/bar/baz.txt".... but that's obviously a different discussion.

gugod
  • 830
  • 4
  • 10