bangor has asked for the wisdom of the Perl Monks concerning the following question:

I have some variables in a package like this:
package Person; our $address_1; our $address_2; our $address_3;
In my script I want to do something like
my @lines; foreach my $num (1..3) { push(@lines, $Person::address_$num); }
Obviously that doesn't work. I can't change the Person package right now.
Thanks for any help.

Replies are listed 'Best First'.
Re: How can I access package variables in a dynamic way
by haukex (Archbishop) on Feb 12, 2019 at 15:26 UTC
    In my script I want to do something like ... $Person::address_$num

    No, you don't :-) Full explanation at Why it's stupid to 'use a variable as a variable name'.

    You almost always want a hash, or in this case an array, instead.

    package Person; our @addresses; package main; my @lines; for my $i (0..$#Person::addresses) { push @lines, $Person::addresses[$i]; }
      Thanks, but as I said I can't change the Person package right now. I know it's wrong but I'm stuck with it.
        I can't change the Person package right now. I know it's wrong but I'm stuck with it.

        If that really, really, really is the case, then here's enough rope to shoot yourself in the foot. This uses a trick to make $Person::addresses an arrayref whose elements are aliases to the original variables, so that you can modify elements of the array to modify the original variables. Consider this monkey-patching the Person package.

        $Person::addresses = sub{\@_}->( map { no strict 'refs'; ${"Person::address_$_"} } 1..3 );

        The best approach would be to create the appropriate hash yourself then:

        my %foreign_variables = ( address_1 => \$Person::address_1, address_2 => \$Person::address_2, address_3 => \$Person::address_3, # ... );
Re: How can I access package variables in a dynamic way
by davido (Cardinal) on Feb 12, 2019 at 17:48 UTC

    If you absolutely positively must do it, then do it in the narrowest scope possible, and in a layer that falls outside your primary script, so that the script itself doesn't have to deal with the nastiness of messing around in the symbol table. A better abstraction provides more opportunity to test your work, too. This is particularly useful when your work is involved in high-risk behavior.

    package Person; use warnings; use strict; our $address_1 = "foo"; our $address_2 = "bar"; our $address_3 = "baz"; 1; package MyPerson; use strict; use warnings; use Data::Alias; my %address; foreach my $var (keys %Person::) { next unless $var =~ m/^address_(\d+)$/; alias $address{$1} = do { no strict 'refs'; ${"Person::${var}"} }; } sub get_address { return $address{shift()} } sub exists_address { return exists $address{shift()} } sub set_address { my ($num, $value) = @_; return $address{$num} = $value; } 1; package main; use strict; use warnings; print "address_1: ", MyPerson::get_address(1), "\n"; print "Setting address_1 to foofoo\n"; MyPerson::set_address(1, 'foofoo'); print "address_1 now contains: ", MyPerson::get_address(1), "\n"; print "Because we created an alias, \$Person::address_1 also changed t +o ", $Person::address_1, "\n\n"; print "Iterate through all Person address_Ns\n"; for (1..3) { print MyPerson::get_address($_), "\n"; }

    The output:

    address_1: foo Setting address_1 to foofoo address_1 now contains: foofoo Because we created an alias, $Person::address_1 also changed to foofoo Iterate through all Person address_Ns foofoo bar baz

    We probably could have done away with the intermediate hash in MyPerson, but using it allows us to ONLY deal with the symbol table of Person in one place in the code. The rest of MyPerson just deals with a run-of-the-mill hash. And package main only deals with some getter and setter functions that accept numbers. We could also have used an array instead of a hash as our alias point, which makes sense if the _number values are contiguous.

    Using a variable as a variable name often reeks of the wet paint that has painted you into a corner. But sometimes we're not the ones who did the painting, and aren't able to alter the package we're using (Person, in this case). When that happens, about the best we can do is find a way to encapsulate the ugliness into the narrowest place as is practical, and then to expose a saner interface for our calling code to consume. This code snippet above attempts to do that; to build a bridge out of the painted-in corner by exposing a simple interface that hides away the ugliness.


    Dave

      This is a very nice, elegant way to handle an unfortunately bad situation.

      Great advice, thank you Dave! I will give this a go.
Re: How can I access package variables in a dynamic way
by hippo (Archbishop) on Feb 12, 2019 at 15:30 UTC
Re: How can I access package variables in a dynamic way
by bliako (Abbot) on Feb 12, 2019 at 23:38 UTC

    And here is a little leacher to get/set methods for all of guest's "our" variables matching a keyword.

    # Leacher.pm package Leacher; # author: bliako # for: https://perlmonks.org/?node_id=1229804 # 13/02/2019 # Makes get/set subs for each 'our' method of guest module # which matches build in regex, or all if none provided. # Usage: # use Leacher 'Person.pm'; # my $mp = Leacher->new('^address_'); # print "var[1] = ".$mp->get_var(1)."\n"; # $mp->set_var(1, 101010); # print "now is var[1] = ".$mp->get_var(1)."\n"; use strict; use warnings; use lib '.'; my $par = undef; sub import { die __PACKAGE__."::import() : you need to specify a class to leach + like use Leacher 'XYZ.pm';".__PACKAGE__." 'class-to-leach-name'.\n" unless defined ($par=$_[1]); $par =~ s/\.pm$//i; print __PACKAGE__."::import() : leaching on ".$_[1]."\n"; require $_[1]; } # takes 1 param: a string-regex to match the variables you are looking + for in # the other module. It uses the remaining match till the end of string + as the key # for you to access these vars. # for example your regex is "^address_" # then it will match every "our" var starting with "address_" and use +the # remaining var name (e.g. if "address_xyz", the remaining var name wi +ll be "xyz") # as the key to get/set that variable. # TODO: the case when we have just "our address_;" sub new { die __PACKAGE__."::new() : you need to specify a class to leach li +ke <c>use ".__PACKAGE__." 'class-to-leach-name'.\n" unless defined $par; my $class = $_[0]; # input param (optional, if empty matches all 'our' params): # e.g. "^address_". In this case the key will be anything followin +g till the end my $regex_to_match_parent_vars = $_[1]; $regex_to_match_parent_vars = '' unless defined $regex_to_match_pa +rent_vars; my $self = { 'vars' => {} }; bless($self, $class); print __PACKAGE__."::new() : looking for vars in '$par' using /$re +gex_to_match_parent_vars/ ...\n"; my @k = (); eval('@k = keys %'.${par}.'::'); foreach my $entry ( @k ){ if( $entry =~ /^${regex_to_match_parent_vars}/ ){ (my $index) = $entry =~ /^${regex_to_match_parent_vars}(.* +?)$/; if( ! defined $index or $index eq "" ){ print STDERR __PAC +KAGE__."::new() : nothing left for key after subtracting regex '$rege +x_to_match_parent_vars' from varname ".$par."::$entry, skipping that. +..\n"; next } eval('$self->{"vars"}->{$index} = \$Person::'.$entry); print __PACKAGE__."::new() : found variable '$entry' and a +dding it with the key '$index'.\n"; } }; return $self } sub get_var { return ${$_[0]->{'vars'}->{$_[1]}} } sub set_var { return ${$_[0]->{'vars'}->{$_[1]}} = $_[2] } 1;

    For testing, here is a "Person" module:

    # Person.pm package Person; use strict; use warnings; our $VERSION = 0; our $address_1 = 11; our $address_2 = 12; our $address_3 = 13; our $address_ = 14; 1;

    And test:

    #!/usr/bin/env perl # harness.pl use strict; use warnings; use lib './'; use Leacher 'Person.pm'; my $mp = Leacher->new('^address_'); die unless defined $mp; print "vars[1]=".$mp->get_var(1)."\n"; $mp->set_var(1,1222); print "vars[1]=".$mp->get_var(1)."\n"; my $mp2 = Leacher->new(); die unless defined $mp2; print "vars[1]=".$mp2->get_var('address_1')."\n"; $mp2->set_var('address_1',666); print "vars[1]=".$mp2->get_var('address_1')."\n";
    $ perl -I. harness.pl Leacher::import() : leaching on Person.pm Leacher::new() : looking for vars in 'Person' using /^address_/ ... Leacher::new() : nothing left for key after subtracting regex '^addres +s_' from varname Person::address_, skipping that... Leacher::new() : found variable 'address_3' and adding it with the key + '3'. Leacher::new() : found variable 'address_2' and adding it with the key + '2'. Leacher::new() : found variable 'address_1' and adding it with the key + '1'. vars[1]=11 vars[1]=1222 Leacher::new() : looking for vars in 'Person' using // ... Leacher::new() : found variable 'VERSION' and adding it with the key ' +VERSION'. Leacher::new() : found variable 'BEGIN' and adding it with the key 'BE +GIN'. Leacher::new() : found variable 'address_' and adding it with the key +'address_'. Leacher::new() : found variable 'address_3' and adding it with the key + 'address_3'. Leacher::new() : found variable 'address_2' and adding it with the key + 'address_2'. Leacher::new() : found variable 'address_1' and adding it with the key + 'address_1'. vars[1]=1222 vars[1]=666

    bw, bliako