Perl Part I · Module 15 · Command-Line Perl and Script Execution
%ENV: Reading the Environment
Arguments are what this run was told. The environment is what the session already knew before your script started — where the project lives, which scratch area to use, which tool version is on the path. Perl exposes all of it as an ordinary hash called %ENV, which you can read, change and pass on to programs you launch. The mechanics take two minutes. The parts worth measuring are the two that cause real bugs: a variable that exists but is empty behaves differently from one that was never set, and the environment flows strictly downhill, so a child process can never hand anything back to its parent no matter how it is written.
Foundation14 min readPerlPart ICommand Line
Part I · Module 15 · Chapter 15.5 · %ENV: Reading the Environment
1. Reading, With a Default
%ENV is a plain hash. Everything from Module 6 applies: exists, delete, keys, iteration.
use strict;
use warnings;
# a name no session will have set, so the demonstration is reproducible
my $name = "VLSI_PROJECT_ROOT";
printf "exists? %s\n", (exists $ENV{$name} ? "yes" : "no");
printf "defined? %s\n", (defined $ENV{$name} ? "yes" : "no");
my $root = $ENV{$name} // "/proj/default";
printf "value with a default: %s\n\n", $root;
# now set it, and read it back the same way
$ENV{$name} = "/proj/chipA";
printf "after setting it : %s\n", $ENV{$name} // "/proj/default";
# %ENV is a hash, so every hash operation works on it
printf "is it a hash? keys %%ENV is a list: %s\n",
(scalar(keys %ENV) > 0 ? "yes, and non-empty" : "empty");exists? no
defined? no
value with a default: /proj/default
after setting it : /proj/chipA
is it a hash? keys %ENV is a list: yes, and non-emptyThe pattern to use is // with an explicit default on the same line:
my $root = $ENV{PROJECT_ROOT} // "/proj/default";
my $scratch = $ENV{SCRATCH} // "/tmp";
my $jobs = $ENV{JOBS} // 4;Reading $ENV{SOMETHING} that is not set gives undef, and under use warnings that produces the uninitialized-value warning from Chapter 14.7 the moment you use it. The // prevents it and documents the fallback in the same breath.
2. Set, Empty, and Unset Are Three Different Things
This is where the real bug lives.
use strict;
use warnings;
delete $ENV{VLSI_A}; # never set
$ENV{VLSI_B} = ""; # set, but empty
$ENV{VLSI_C} = "0"; # set to a string that is false
$ENV{VLSI_D} = "/proj/chipA"; # set to a real value
printf "%-8s %-8s %-8s %-6s %-14s %s\n",
"NAME", "exists", "defined", "true", "// 'dflt'", "|| 'dflt'";
for my $n (qw(VLSI_A VLSI_B VLSI_C VLSI_D)) {
no warnings 'uninitialized';
printf "%-8s %-8s %-8s %-6s %-14s %s\n",
$n,
(exists $ENV{$n} ? "yes" : "no"),
(defined $ENV{$n} ? "yes" : "no"),
($ENV{$n} ? "yes" : "NO"),
"[" . ($ENV{$n} // "dflt") . "]",
"[" . ($ENV{$n} || "dflt") . "]";
}NAME exists defined true // 'dflt' || 'dflt'
VLSI_A no no NO [dflt] [dflt]
VLSI_B yes yes NO [] [dflt]
VLSI_C yes yes NO [0] [dflt]
VLSI_D yes yes yes [/proj/chipA] [/proj/chipA]Read the last two columns across the VLSI_B and VLSI_C rows.
VLSI_B is set to the empty string. It exists, it is defined, and it is false. // keeps the empty string, because // asks "is it defined?". || replaces it, because || asks "is it true?".
VLSI_C is set to "0" — a perfectly good value that happens to be false. Again // keeps it and || throws it away.
Which is correct depends on what the variable means, and you have to decide:
//when empty or zero is a real value.JOBS=0,VERBOSE=0, a deliberately blank prefix. This is the usual case and the safe default.||when empty should mean "not configured". Shell users often clear a variable by setting it to""rather than unsetting it, soPROJECT_ROOT=""probably does mean "fall back".
The bug this prevents is specific: my $jobs = $ENV{JOBS} || 4; with JOBS=0 set deliberately to mean "do not parallelise" silently runs four jobs. That is Chapter 3.8's distinction, in the place it bites hardest.
3. The Environment Flows Downhill Only
use strict;
use warnings;
use File::Temp qw(tempdir);
use File::Spec;
my $dir = tempdir(CLEANUP => 1);
# a child that reports what it inherited
my $reader = File::Spec->catfile($dir, "reader.pl");
open my $w, ">", $reader or die "cannot write: $!\n";
print $w <<'PROG';
printf " child sees: ROOT=[%s] GONE=[%s] NEW=[%s]\n",
($ENV{VLSI_ROOT} // "(unset)"),
($ENV{VLSI_GONE} // "(unset)"),
($ENV{VLSI_NEW} // "(unset)");
PROG
close $w;
# a child that tries to set something for its parent
my $setter = File::Spec->catfile($dir, "setter.pl");
open my $w2, ">", $setter or die "cannot write: $!\n";
print $w2 '$ENV{VLSI_NEW} = "set-by-child";' . "\n";
close $w2;
$ENV{VLSI_ROOT} = "/proj/chipA";
$ENV{VLSI_GONE} = "temporary";
delete $ENV{VLSI_GONE};
delete $ENV{VLSI_NEW};
print "1. parent sets ROOT and deletes GONE, then runs the child:\n";
system($^X, $reader);
print "2. the child sets VLSI_NEW, then the parent checks:\n";
system($^X, $setter);
printf " parent sees VLSI_NEW=[%s]\n", $ENV{VLSI_NEW} // "(unset)";
print "3. and a second child, to confirm nothing was exported back:\n";
system($^X, $reader);1. parent sets ROOT and deletes GONE, then runs the child:
child sees: ROOT=[/proj/chipA] GONE=[(unset)] NEW=[(unset)]
2. the child sets VLSI_NEW, then the parent checks:
parent sees VLSI_NEW=[(unset)]
3. and a second child, to confirm nothing was exported back:
child sees: ROOT=[/proj/chipA] GONE=[(unset)] NEW=[(unset)]Three measured results:
A child inherits a copy of the parent's environment. VLSI_ROOT crossed over. Changes you make to %ENV before launching affect every program you launch afterwards, which is how you configure a tool you are about to run.
delete $ENV{X} really removes it. The child saw GONE as unset. Setting it to "" would not have been the same thing — see section 2.
A child cannot change its parent's environment. The setter ran, assigned the variable, and exited. The parent still sees nothing, and so does the next child. The copy the child modified died with it.
That third point is worth stating plainly because people lose time to it. There is no way for a child process to export a variable back. Not from Perl, not from a shell script, not with any flag. If a child needs to hand something back, it has to write it somewhere the parent reads — standard output, a file, or an exit status. Chapter 15.10's backticks are the usual answer:
my $value = `compute-it.pl`; # capture the output
chomp $value;This is also why cd and source are shell builtins rather than programs. A program could not change your shell's directory any more than a Perl script can change its parent's environment.
4. local, For One Child Only
Setting %ENV changes it for the rest of the script. Usually you want a change that applies to one call and then goes away.
use strict;
use warnings;
use File::Temp qw(tempdir);
use File::Spec;
my $dir = tempdir(CLEANUP => 1);
my $reader = File::Spec->catfile($dir, "reader.pl");
open my $w, ">", $reader or die "cannot write: $!\n";
print $w 'printf " child sees ROOT=[%s]\n", ($ENV{VLSI_ROOT} // "(unset)");' . "\n";
close $w;
$ENV{VLSI_ROOT} = "/proj/chipA";
print "before the block:\n";
system($^X, $reader);
{
local $ENV{VLSI_ROOT} = "/proj/chipB"; # restored at the closing brace
print "inside the block:\n";
system($^X, $reader);
}
print "after the block:\n";
system($^X, $reader);before the block:
child sees ROOT=[/proj/chipA]
inside the block:
child sees ROOT=[/proj/chipB]
after the block:
child sees ROOT=[/proj/chipA]local saved the old value, installed the new one, and put the old one back when the block ended. The child inside saw chipB; the child after saw chipA again.
This is exactly what Chapter 12.8 described local for, and %ENV is the clearest case for it in the whole language. Use it whenever you need a tool to run with a particular setting:
{
local $ENV{LC_ALL} = "C"; # force predictable sort order
local $ENV{TZ} = "UTC"; # and predictable timestamps
system("sort", $file) == 0 or die "sort failed\n";
}
# both are back to whatever they wereThat block is worth keeping. LC_ALL=C makes sort order bytes rather than following locale rules, which is the difference between a reproducible comparison and one that changes on a colleague's machine.
5. PATH, and Two Things Not To Do
PATH is the environment variable you are most likely to want to change, and the one most worth being careful with.
# add a tool directory for the children this script launches
local $ENV{PATH} = "/proj/tools/bin:$ENV{PATH}";Prepending puts your directory first; appending leaves the system's version winning. Both are reasonable, and the choice should be deliberate.
Three practical notes:
The separator is : on Unix. Use File::Spec->path to read the entries portably rather than splitting on a hard-coded colon.
Changing PATH does not affect the already-running script, only programs it starts afterwards. Perl resolved its own interpreter long before your line ran.
system("tool") with a relative name searches PATH at the moment of the call, so the local block above does work for the commands inside it.
And two things to avoid:
Do not put secrets in the environment and then launch untrusted programs. Every child inherits the lot. If a token must be passed, pass it on standard input or in a file with restricted permissions.
Do not read %ENV deep inside a library. A function whose behaviour changes based on a variable set three thousand lines away is very hard to reason about and nearly impossible to test. Read the environment once at the top, in main, and pass the values down as arguments — the same argument Chapter 14.6 made about a module that should not impose a pragma on its caller.
6. What To Remember
%ENVis an ordinary hash —exists,delete,keysand iteration all work.- Read with
//and an explicit default on the same line, or an unset name givesundefand warns when used. - Collect the environment once at the top into a
%confighash, so the dependencies are visible and the code is testable. - Set, empty and unset are three different states. Measured: an empty variable exists, is defined, and is false.
//keeps an empty value;||replaces it. Measured on both""and"0".- Use
//when empty or zero is real —JOBS=0with||silently runs four jobs. existsanddefinedagree for%ENV, since the OS only stores strings; useexiststo tell "blanked" from "never set".- A child inherits a copy of whatever
%ENVholds at launch. delete $ENV{X}really removes it — not the same as setting it to"".- A child can never change its parent's environment. Measured both ways. It must return data instead, via output, a file or an exit status.
local $ENV{X} = ...scopes a change to a block and restores it afterwards — the clearest use oflocalin the language.local $ENV{LC_ALL} = "C"before runningsortfor reproducible ordering.- Prepend to
PATHto win, append to defer, and remember it affects only programs started afterwards. - Do not put secrets in the environment when you launch programs you do not control.
- Do not read
%ENVinside a library — read it inmainand pass values down. - The environment is input. Validate it like an argument.
Chapter 15.6 changes the subject from writing scripts to not writing them. For a job you will do once, perl -e runs a whole program typed straight onto the command line, with no file to create and nothing to clean up afterwards.
Where this fits
Part of the Perl curriculum.
