See also: The Introduction.
The system environment variables are available in Raku in the dynamic variable
%*ENV
.
Let us start by writing a copy of the «printenv» program. It lists all the environment variables if run without an argument, and the specified one if given an argument. The output is close to the one given by «printenv», but I have chosen to sort the variables.
File: printenv6
multi MAIN () # [1]
{
say "$_ -> %*ENV{$_}" for %*ENV.keys.sort; # [1a]
}
multi MAIN (Str $name) # [2]
{
say %*ENV{$name} // ""; # [2b]
}
[1] The first multi MAIN
is used when the program
is executed without arguments. It prints the environment variables (%*ENV.keys
)
and the values, on separate lines [1a].
[2] The second multi MAIN
is used when the program is
executed with exactly one argument. Print the value, if it is defined [2b]. If not,
the «defined or« operator //
prints nothing (and the initial
say
gives a newline.
I have called the program «printenv6» as a nod to the former language name «Perl 6».
See
docs.raku.org/routine/$SOLIDUS$SOLIDUS
for more information about//
See
docs.raku.org/language/variables#%*ENV
for more information about%*ENV
Running it:
$ raku printenv7 PATH
/home/arne/.perl6/bin:/opt/rakudo-pkg/bin:/opt/rakudo-pkg/share/perl6/site/bi\
n:/home/arne/bin:/home/arne/.local/bin:/home/arne/.perl6/bin:/opt/rakudo-pkg/\
bin:/opt/rakudo-pkg/share/perl6/site/bin:/usr/local/sbin:/usr/local/bin:/usr/\
sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/opt/rakudo-pk\
g/bin:/opt/rakudo-pkg/share/perl6/bin
It isn't easy to see if we have any duplicates, but we can fix that with a command line option:
File: printenv7
multi MAIN ()
{
say "$_ -> %*ENV{$_}" for %*ENV.keys.sort;
}
multi MAIN (Str $name, :$split) # [1]
{
my $val = %*ENV{$name};
return unless $val; # [2]
$split
?? ( .say for $val.split(":") ) # [3]
!! say $val; # [4]
}
[1] Specify «--spilt» on the command line to activate it.
[2] This time, we print nothing (no newline) if the specified environment variable doesn't exist.
[3] If we have requested split, print the individual parts in a loop. Note that
.say
is the same as $_.say
.
[4] If not, print it as a single line.
The path is a list of directories were the shell looks for programs to execute, when we specify them whithout an explicit location.
Running «printenv7» shows that I have some duplicates in my path, which I have marked:
$ raku printenv7 --split PATH
/home/arne/.perl6/bin #1
/opt/rakudo-pkg/bin #2
/opt/rakudo-pkg/share/perl6/site/bin #3
/home/arne/bin
/home/arne/.local/bin
/home/arne/.perl6/bin #1
/opt/rakudo-pkg/bin #2
/opt/rakudo-pkg/share/perl6/site/bin #3
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
/usr/games
/usr/local/games
/snap/bin
/opt/rakudo-pkg/bin #2
/opt/rakudo-pkg/share/perl6/bin
I should do something about it...
It is possible to look up the location of a program (without executing it) with the Unix «which» command. Here it is, in Raku:
File: which6
unit sub MAIN ($program); # [1]
for %*ENV<PATH>.split(":") -> $dir # [2]
{
next unless $dir.IO.d; # [3]
for indir($dir, &dir).sort -> $file # [4]
{
next if $file.d; # [5]
next unless $file.x; # [6]
if $program eq $file # [7]
{
say "$dir/$file"; # [7a]
exit; # [7b]
}
}
}
[1] Specify the program to look for.
[2] Loop through the entries in the path.
[3] Skip it if it isn't a directory.
[4] We use indir
to execute a piece of code (the second
argument) in a specified directory (the first argument). It takes care of changing
the current directory for the scope of the call (and sets it back again afterwards).
The dir
command gives us a list of files (as IO
objects)
in the directory (but without the special «.» and «..»). Note that we specify it as
&dir
, so that indir
gets a pointer to the command, and not
the result of running it.
[5]Skip the path entry of it isn't a directory. (That would indicate a problem, but all this program cares about is avoiding a run time error.)
[6] Skip it if it isn't executable.
[7] If the name matches the opne we are looking for, print the full path [7a] and exit [7b].
See
docs.raku.org/routine/indir for more information about indir
.
See
docs.raku.org/routine/dir for more information about dir
.
Running it:
$ raku which6 which
/usr/bin/which
$raku which6 jabber
We can extend the program so that it continues looking for the program (after the first hit), thus showing if we have several ones installed (which surely indicates a problem).
File: which7
unit sub MAIN ($program, :$all = False);
for %*ENV<PATH>.split(":") -> $dir
{
next unless $dir.IO.d;
for indir($dir, &dir).sort -> $file
{
next if $file.d;
next unless $file.x;
if $program eq $file
{
say "$dir/$file";
exit unless $all;
}
}
}
Running it:
$ raku which6 --all which
/usr/bin/which
/bin/which
$ raku which7 --all raku
/opt/rakudo-pkg/bin/raku
/opt/rakudo-pkg/bin/raku
/opt/rakudo-pkg/bin/raku
The second example shows what happens if we have duplicates in the path. We have three of this one («/opt/rakudo-pkg/»), marked with #2 above. So we get three identical lines. It is possible to remove the duplicates, but it may be better to leave them - so that the user sees the problem with the path.
Looking for duplicates can be a good idea. Let us automate that:
File: duplicates6
unit sub MAIN (:$verbose);
my %all; # [1]
for %*ENV<PATH>.split(":") -> $dir # [2]
{
next unless $dir.IO.d;
for indir($dir, &dir).sort -> $file
{
next if $file.d;
next unless $file.x;
say ": $dir/$file" if $verbose;
%all{$file}.push: "$dir/$file"; # [3]
}
}
for %all.keys.sort -> $program # [4]
{
my @programs = @(%all{$program}).sort.squish; # [5]
next if @programs.elems < 2; # [6]
say "$program:"; # [7]
say " $_" for @programs; # [7a]
}
[1] We collect the programs here, with the program names as keys and the program with full path as the value. The value is an array.
[2] See «which6» (above) for the decription of this loop.
[3] Push the file and location to the array in the hash (see [1]).
[4] Get the keys (the program names) in sorted order.
[5] Get the locations, also sorted and without duplicates.
[6] Skip programs with only one entry (as there are no duplicates).
[7] Print the program, and the locations [7a].
I have used squish
(in [5]) to
get rid of duplicates. It depends on the array beeing sorted, as it is here.
Getting rid of duplicates in a non-sorted array is done with unique
.
I have used squish
as it is faster.
See
docs.raku.org/routine/squish
for more information about squish
.
See
docs.raku.org/routine/unique
for more information about unique
.
Running it gives 151 lines of output on my pc. That is not good. Here are some of them:
$ raku duplicates6
atom:
/usr/bin/atom
/usr/local/bin/atom
bailador:
/opt/rakudo-pkg/share/perl6/site/bin/bailador
/usr/local/bin/bailador
brltty:
/bin/brltty
/sbin/brltty
...
The first program (atom) is two different scripts. The first one works, but the second one fails. I removed it.
The second program (bailador) works out for the first one. The second one is a symbolic link, to a third location (and the program does exist)-
We can add a check for symbolic links:
File: duplicates7 (changes only)
for %all.keys.sort -> $program
{
my @programs = @(%all{$program}).sort.squish;
next if @programs.elems < 2;
say "$program:";
for @programs -> $current
{
if $current.IO.l # [1]
{
my $target = $current.IO.resolve; # [2]
say " $current -> $target { $target.e ?? "(OK)" !! "(ERROR)" }"; # [3]
}
else
{
say " $current";
}
}
}
[1] We use IO.l
(link) to tell us if the file is a symbolic
link.
[2] We use IO.resolve
to get the actual file (resolving
symbolic links and «..».)
[3] Print the original file, and the location it resolves to. Add a label
telling us if the file exist (OK) or not (ERROR), using IO.e
(exist)
See
docs.raku.org/routine/e
for more information about IO.e
.
See
docs.raku.org/routine/l
for more information about IO.l
.
See
docs.raku.org/routine/resolve
for more information about IO.resolve
.
Note that IO.resolve
require
a POSIX compliant system to work. This means that it will not work on Windows.
Running it (and again only showing the top):
$ raku duplicates7
bailador:
/opt/rakudo-pkg/share/perl6/site/bin/bailador
/usr/local/bin/bailador -> /usr/local/share/perl6/site/bin/bailador (OK)
brltty:
/bin/brltty
/sbin/brltty -> /bin/brltty (OK)
...
Note that the program doesn't check if the target of a symbolic link is in the path. That is as it should be.
See the next part; Part 2: The Loop.