Rash - A Raku Shell

Part 1: The Path

by Arne Sommer

Rash - Part 1: The Path

[67.1] Published 13. April 2020.

See also: The Introduction.

The Environment

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

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.


Part 2: The Loop

See the next part; Part 2: The Loop.