3

Today I saw this piece of code:

if (    not defined($reply_address) 
     or not defined($from_name) 
     or not defined($subject) 
     or not defined($date) )
{ 
  die "couldn’t glean the required information!"; 
}

(Jeffrey Friedl, "Mastering Regular Expressions", p. 59, 3rd ed.)

and I thought "How can I know which variable misfired?"

Of course, if there are only 4 variables to test, as in the example above, one could come up with:

if ( not defined $reply_address ) 
{ 
  die "\$reply_address is not defined" 
}
elsif ( not defined $from_name )
{
  die "\$from_name is not defined"
}
elsif ...

But what if there are 14 variables? Or 40...? One still needs to go through all of them, manually testing each and every one?

Isn't there a shorter, more "magical" way of telling which variable was left undefined?

Aditya
  • 354
  • 2
  • 6
  • 10
  • I don't have a magic enough answer, but `die '$reply_address is not defined' unless defined $reply_address;` would be a cleaner example of your code. – UncleCarl Jun 04 '21 at 16:57

3 Answers3

6

You could create a table to simplify a little bit:

use strict;
use warnings;

my $reply_address = "xyz";
my $from_name;
my $subject = "test";
my $date;

my @checks = (
    [\$reply_address, '$reply_adress'],
    [\$from_name, '$from_name'],
    [\$subject, '$subject'],
    [\$date, '$date'],
);

for my $check (@checks) {
    if (not defined ${$check->[0]}) {
        die $check->[1] . " is not defined";
    }
}
Håkon Hægland
  • 39,012
  • 21
  • 81
  • 174
3

You can do what you want with symbolic references, though using them is generally not a great idea, and it can only be done with package variables, not lexically scoped variables (and lexically scoped variables are preferred to package variables -- see this answer for a brief comparison of the two).

#!/usr/bin/env perl

use strict;
use warnings;

use 5.014;

our($foo1) = 1;
our($bar1) = undef;
our($baz1) = 3;

foreach my $name (qw(foo1 bar1 baz1)) {
    {
        no strict 'refs';
        my($value) = $$name;
        warn "$name: is not defined" unless defined $value;
        say "$name: <$value>";
    }
}

Using warn instead of die for illustrative purposes.

</tmp> $ ./test.pl
foo1: <1>
bar1: is not defined at ./test.pl line 16.
Use of uninitialized value $value in concatenation (.) or string at ./test.pl line 17.
bar1: <>
baz1: <3>

You can also just loop through all of the variables using common code to check them:

#!/usr/bin/env perl

use strict;
use warnings;

use 5.014;

my($foo2) = 1;
my($bar2) = undef;
my($baz2) = 3;

foreach my $vardef (["foo2", $foo2], ["bar2", $bar2], ["baz2", $baz2]) {
    my($name) = $vardef->[0];
    my($value)  = $vardef->[1];

    warn "$name: is not defined" unless defined $value;
    say "$name: <$value>";
}

which gives similar output:

foo2: <1>
bar2: is not defined at ./test.pl line 29.
Use of uninitialized value $value in concatenation (.) or string at ./test.pl line 30.
bar2: <>
baz2: <3>

Finally, if you can manage to get the variables into a hash, you can loop through the keys of the hash and test them that way:

#!/usr/bin/env perl

use strict;
use warnings;

use 5.014;

my($vars) = {
    foo3 => 1,
    bar3 => undef,
    baz3 => 3,
};

foreach my $name (sort keys %$vars) {
    my($value)  = $vars->{$name};

    warn "$name: is not defined" unless defined $value;
    say "$name: <$value>";
}

I threw the sort in there because I like deterministic behavior...

bar3: is not defined at ./test.pl line 42.
Use of uninitialized value $value in concatenation (.) or string at ./test.pl line 43.
bar3: <>
baz3: <3>
foo3: <1>

If the test really was as simple as die if ! defined then I would probably just list them out:

#!/usr/bin/env perl

use strict;
use warnings;

use 5.014;

my($foo4) = 1;
my($bar4) = undef;
my($baz4) = 3;

die qq([ERROR] \$foo4 not defined\n) unless defined $foo4;
die qq([ERROR] \$bar4 not defined\n) unless defined $bar4;
die qq([ERROR] \$baz4 not defined\n) unless defined $baz4;

which just gives us:

[ERROR] $bar4 not defined

The last approach is just very straightforward and unambiguous. If the test is not as dead simple as this, then I'd go with the second approach. If you're worried about a list of 40 (or even 14) checks of this nature, then I'd look at the design.

See also this PadWalker code example for a very complicated version of the first option, but allowing lexically scoped variables.

Joe Casadonte
  • 15,888
  • 11
  • 45
  • 57
1

Could be done with a string-eval:

use strict;
use warnings;

my ($reply_address, $from_name, $subject, $date) = ('', '', undef, '');

for my $var (qw(reply_address from_name subject date)) {
    my $defined;
    eval "\$defined = defined \$$var";
    die "eval failed: $@" if $@;
    die "\$$var is not defined" unless $defined;
}
jo-37
  • 91
  • 1
  • 6
  • 1
    Seriously, this is a very bad idea. You could literally do the exact same idea with a hash and skip the `eval`. Few good ideas involve `eval`. This should not have 2 upvotes. – TLP Jun 04 '21 at 22:00
  • It would help me if you could explain why this idea is "very bad". Not in general, but in this concrete usage. – jo-37 Jun 05 '21 at 07:53
  • Because using `eval` on strings allows users to execute arbitrary code on your system. While your specific case might *seem* safe enough, you never know what security holes other users will introduce when they use your code. And as security holes go, this is a big one. And also because this is exactly how hashes work, you take a string which you use as a key to get a value. E.g. `die "$var is not defined" unless defined $data{$var}`. – TLP Jun 05 '21 at 10:50
  • 1
    Here's are two excellent write-ups on the (mis-)use of string eval: https://wiki.sei.cmu.edu/confluence/display/perl/IDS35-PL.+Do+not+invoke+the+eval+form+with+a+string+argument and https://perlmaven.com/string-eval – Joe Casadonte Jun 05 '21 at 12:16