perlevalsymbol-table

Issues with dynamically importing variables into namespace


I have an array of hashes in Perl. I would like to loop through the array and execute some code specified in a string on each of the hashes (remark: I would not like to discuss this approach or its security considerations; let's focus on how it can be done). An example implementation could look like this:

use 5.014;
use strict;
use warnings;

my $list = [
  {foo => "foo1"},
  {foo => "foo2"},
  {foo => "foo3"},
  {foo => "foo4"},
];

my $code = q{say $item->{foo}};

foreach my $item (@$list) {
  eval qq{$code};
}

As expected, this prints:

foo1
foo2
foo3
foo4

So far, so good. However, I would like the array and hash data structure to be transparent to the code snippet, i.e. the code snippet should be able to use $foo instead of $item->{foo} etc. Therefore, I am trying to import the hash entries into the main namespace. (Again, I would like to discuss how to do this, not whether I should do it.)

use 5.014;
use strict;
use warnings;

my $list = [
  {foo => "foo1"},
  {foo => "foo2"},
  {foo => "foo3"},
  {foo => "foo4"},
];

my $code = q{say $foo};

foreach my $item (@$list) {
  foreach my $key (keys %$item) {
    $main::{$key} = \$item->{$key};
  }

  {
    no strict "vars";
    eval qq{$code};
  }
}

I would expect this to work, but it does not (fully). This code prints:

Use of uninitialized value $foo in say at (eval 1) line 1.

foo2
foo3
foo4

For some reason, the first item is not processed correctly.

I have tried several ways to circumvent this or find out more about what is wrong. The following changes all work (potentially with a warning about $foo only being used once):

  1. replace eval qq{$code} with a literal say $foo
  2. replace my $code = q{say $foo} with my $code = q{say ${$main::{foo}}} (however, $main::foo does not suffice)
  3. use the variable $foo somewhere before, e.g. put a { no strict "vars"; my $bar = $foo; } before the outer foreach loop
  4. explicitly create the package variable by inserting eval sprintf('our $%s;', $key); before $main::{$key} = \$item->{$key};

Option 4 is what I would do now. It feels a bit hacky, but does the job. However, I would still like to understand why this is necessary and whether there is a better way to fix it.

UPDATE: As pointed out by TLP, a simpler way to achieve this is to use a symbolic reference instead of symbol table manipulation. See my own answer to the question. However, I would still like to understand why the symbol table approach does not work out of the box.


Solution

  • A symbolic reference is a simpler way than manipulation of the symbol table. Here is an adaptation of the code in the question that solves the problem:

    use 5.014;
    use strict;
    use warnings;
    
    my $list = [
      {foo => "foo1"},
      {foo => "foo2"},
      {foo => "foo3"},
      {foo => "foo4"},
    ];
    
    my $code = q{say $foo};
    
    foreach my $item (@$list) {
      foreach my $key (keys %$item) {
        no strict 'refs';
        ${$key} = $item->{$key};
      }
    
      {
        no strict 'vars';
        eval qq{$code};
      }
    }
    

    Importantly, the solution above makes a copy of the values in the data structure instead of creating an alias. This matters if the code in $code modifies the values. The modified code example below showcases this. It also contains (commented out) the fixed symbol table manipulation, which creates an alias instead, thereby allowing the original data structure to be modified by $code. See also the answer by clamp.

    use 5.014;
    use strict;
    use warnings;
    
    my $list = [
      {foo => "foo1"},
      {foo => "foo2"},
      {foo => "foo3"},
      {foo => "foo4"},
    ];
    
    my $code = q{$foo .= '_bar'; say $foo};
    
    foreach my $item (@$list) {
      foreach my $key (keys %$item) {
        no strict 'refs';
        ${$key} = $item->{$key};  # copy
        # *{$key} = \$item->{$key};  # alias
      }
    
      {
        no strict 'vars';
        eval qq{$code};
      }
    
      say $item->{foo};
    }