Rash - A Raku Shell

Part 3: The Execution

by Arne Sommer

Rash - Part 3: The Execution

[67.3] Published 13. April 2020.

See also: The Introduction | Part 1: The Path | Part 2: The Loop.

We'll start a little Execution detour, before continuing with the Shell.

Executing Programs

We can modify the «which6» program (from Part 1) so that it displays the file. If given with a path, it is looked up there. If given without, we assume a program and look in the user's path.

The easiest way of executing programs is with the run command.

The run function searches the path (available in %*ENV<PATH>, as described in Part 1: The Path) automatically, so that we don't have to do this ourselves.

File: show-program
my $pager = %*ENV<PAGER> // "more";                   # [1]

unit sub MAIN ($program); 

show-program($program) if $program.contains("/");     # [2]

for %*ENV<PATH>.split(":") -> $dir
{
  next unless $dir.IO.d; # Is this a directory?

  for indir($dir, &dir).sort -> $file
  {
    next if $file.d;
    next unless $file.x;
    show-program("$dir/$file") if $program eq $file;  # [3]
  }
}

sub show-program ($file)
{
  say "@@ $file @@";
  run $pager, $file;                                  # [4]
  exit;                                               # [4b]
}

[1] It is normal to specify an external pager program (used to show some content) with this environment variable. It it isn't set, we default ot the «more» program.

[2] If the program has a (one or more) «/» in it, we assume a full path (or relative to the current directory), and show it. contains checks if the specified string is a substring of the first one.

[3] We have found it, show it.

[4] We use run to execute the pager program.

Note that any arguments to run must be passed as an array!

See docs.raku.org/routine/contains for more information about contains.

See docs.raku.org/routine/run for more information about run.

Running it:

$ raku show-program 2to3-2.7
@@ /usr/bin/2to3-2.7 @@
#! /usr/bin/python2.7
import sys
from lib2to3.main import main

sys.exit(main("lib2to3.fixes"))

Running it (actually «more») on a binary file gives a warning:

$ raku show-program passwd
@@ /usr/bin/passwd @@

******** /usr/bin/passwd: Not a text file ********

shell

If you want more power than provided with run out of the box, use shell.

shell will run the command through the system shell, so that the shell handles pipes, redirects and environment variables.

This command use the system shell, which is «/bin/sh c» on non-Windows machines. On Windows it is «%*ENV<ComSpec> /c».

Note that shell escapes are a severe security concern, and can cause confusion with unusual file names. Use run if you want to be safe.

$ raku
> shell 'ls -lR | gzip -9 > ls-lR.gz';
Proc.new(in => IO::Pipe, out => IO::Pipe, err => IO::Pipe, exitcode => 0, \
    signal => 0, pid => 16910, command => ("ls -lR | gzip -9 > ls-lR.gz",))

The possibilities (and pitfalls) are endless.

The return value of both run and shell is an object of type Proc, as shown when we print the returned value from the calls (as done above implicitly in REPL mode). This makes it e.g. possible to redirect output, and we'll look into that in Part 6.

We can add support for executing external programs, as it is a shell after all. See «Running Programs» section above.

We can use the normal shell approach of just running anything specified that is not a built-in function, or we can add an explicit command. We'll do the latter, and go for «run».

We start with adding «run» to the list of known commands:

my @commands = <exit help run version>;

Then we add the command in the given block:

when /^run\s+(.*)/ { do-run $0; }

We use a regex, sending everything after the initial "run" and one or more spaces to «do-run».

«do-run» is quite simple:

sub do-run ($line)
{
  my ($cmd, @args) = $line.words;

  @args
    ?? run $cmd, @args
    !! run $cmd;
}

Note that any arguments to the program we give to run, must be specified as an array.

We can try with a single argument with spaces between what we think are the arguments. But that is just one argument, a string with spaces in it.

Try to run «emacs "12 34 56 78"» in your shell to confirm it.

The full program is available as «rash-run».

Let us try it with a known program as «pwd» (usually located as «/bin/pwd»):

$ raku rash-run
rash: Enter «exit» to exit
> run pwd
/home/raku/course

And it works!

Let us try with several arguments:

$ raku rash-run
rash: Enter «exit» to exit
> run mkdir 1 2 3 4

The directories are created, if we have write access to the current directory, and there are no files or directories there already. (Remember to remove them afterwards.)

Remember the warning about run requiring an array? If we had forgotten about it and passed the string «1 2 3 4» we would have gotten one directory with the name «1 2 3 4».

We can try with a non-existing program:

$ raku rash-run
> run KyRRWQQQQQ
The spawned command 'KyRRWQQQQQ' exited unsuccessfully (exit code: 1)

And it failed. That is as expected, but the error message isn't very nice - and our shell was terminated.

We can avoid the error message and termination by catching the return value from run, which - as you may remember (from the «shell» section) - is a Proc object.

The documentation (at docs.raku.org/language/ipc) tells us that we can use:

  • «exitcode» - which gives us the status code 0 for program success, and a positive number when an error occurred.
  • «pid» - which gives us the process id. An illegal value indicates that the process has not run (typically as the program doesn't exist).

A non existing program will give an exit code of 1. As will a program that fails. But we can use the «pid» (the process id of the child):

$ raku
> my $proc = run( '/bin/false' );
Proc.new(in => IO::Pipe, out => IO::Pipe, err => IO::Pipe, exitcode => 1, \
         signal => 0, pid => 23301, command => ("/bin/false",))

> my $proc = run( '/bin/falseXX' );
Proc.new(in => IO::Pipe, out => IO::Pipe, err => IO::Pipe, exitcode => 1, \
         signal => 0, pid => Nil, command => ("/bin/falseXX",))

Applied to the shell:

File: rash-run2 (changes only)
sub do-run ($line)
{
  my ($cmd, @args) = $line.words;

  my $res = @args
    ?? run $cmd, @args
    !! run $cmd;

  if ! $res.pid
  {
    say "$cmd: command not found";
  }
  elsif $res.exitcode 
  {
    say "$cmd: exit with code { $res.exitcode }";
  }
}

Running it:

$ raku rash-run2
> run /bin/false
/bin/false: exit with code 1

> run /bin/falseXXXX
/bin/falseXXXX: command not found

The complete program is available as «rash-run2».

Changing Directories

We should be able to navigate the file system. The «cd» command (or «chdir») do not exist as program (as it is built-in to a normal shell), and they would not make sense, as the directory change would bw lost after the program has finished. But we can use the Raku function chdir.

First we add «cd» to the list of known commands:

my @commands = <cd exit help run version>;

Then we add the command in the given block:

when /^cd\s+(\S+)/ { do-chdir $0; }

We use a regex, sending the first word after the initial "cd" and one or more spaces to «do-chdir».

«do-chdir» is quite simple:

sub do-chdir ($dir)
{
  say "cd: $dir: No such file or directory" unless chdir $dir;
}

The error message is the same as «bash» uses.

The complete program is available as «rash-cd».

Running it:

$ raku rash-cd
rash: Enter «exit» to exit

> run pwd
/home/raku/course
> cd ..

> run pwd
/home/raku
> 
> cd skssksksks
cd: skssksksks: No such file or directory

The current directory applies to the shell. When we exit it, we'll be back in our normal shell just where we left it.

Without «run»

Typing «run pwd» is tiresome. We can add «pwd» as a shortcut. But we'll do it so that we can add other shortcuts (and set up «more» at the same time).

We add the following lines just after the definition of @commands:

my %allow;

do-allow "pwd";
do-allow "more";

«do-allow» looks like this:

sub do-allow ($cmd)
{
  @commands.push($cmd);
  %allow{$cmd} = True;
}

And the final change is in the default block:

default
{
  my ($cmd, $arg) = $line.split(/\s/, 2);
  %allow{$cmd}
    ?? do-run $line
    !! say "Unknown command: \"$_\" (use \"help\" for a list of commands)";
}

The complete program is available as «rash-allow».

You may have guessed that it is easy to let the user add «allowe»s in the shell. All we have to do is add this line:

when /^allow\s+(.*)/ { do-allow $0.Str; }

The trailing .Str is extremely important here, as $0 is a match object and not a string. If we display the value, it will be stringified, but inside the callback we'd get «MoarVM panic: Internal error: Unwound entire stack and missed handler» and a terminated program if we tried to apply a shortcut added without .Str.

The complete program:

File: rash-allow2
use Linenoise;

constant HIST_FILE = ( $*HOME.add: '.rash-hist' ).Str;
constant HIST_LEN  = 25;

linenoiseHistoryLoad(HIST_FILE);
linenoiseHistorySetMaxLen(HIST_LEN);

my @commands = <allow cd exit help run version>;
my %allow;

do-allow "pwd";
do-allow "more";

linenoiseSetCompletionCallback(-> $line, $c
{
  for @commands.grep(/^ $line /).sort -> $cmd
  {
    linenoiseAddCompletion($c, $cmd);
  }
});

say 'rash: Enter «exit» to exit';

while (my $line = linenoise '> ').defined
{
  linenoiseHistoryAdd($line);

  given $line
  {
    when /^allow\s+(.*)/ { do-allow $0; }
    when /^cd\s+(\S+)/   { do-chdir $0; }
    when "exit"          { last; }
    when "help"          { say "Legal commands: { @commands }" }
    when /^run\s+(.*)/   { do-run $0; }
    when "version"       { say "Version 0.09"; }
    default
    {
      my ($cmd, $arg) = $line.split(/\s/, 2);
      %allow{$cmd}
        ?? do-run $line
        !! say "Unknown command: \"$_\" (use \"help\" for a list of commands)";
    }
  }
}

linenoiseHistorySave(HIST_FILE);

sub do-run ($line)
{
  my ($cmd, @args) = $line.words;

  @args
    ?? run $cmd, @args
    !! run $cmd;

  if ! $res.pid
  {
    say "$cmd: command not found";
  }
  elsif $res.exitcode 
  {
    say "$cmd: exit with code { $res.exitcode }";
  }
}

sub do-chdir ($dir)
{
  say "cd: $dir: No such file or directory" unless chdir $dir;
}

sub do-allow ($cmd)
{
  @commands.push($cmd);
  %allow{$cmd} = True;
}

Running it:

$ raku rash-allow2
rash: Enter «exit» to exit

> ping vg.no
Unknown command: "ping vg.no" (use "help" for a list of commands)

> allow ping

> ping vg
ping: vg: Name or service not known
ping: exit with code 2

> ping vg.no
PING vg.no (195.88.55.16) 56(84) bytes of data.
64 bytes from www.vg.no (195.88.55.16): icmp_seq=1 ttl=247 time=1.98 ms
64 bytes from www.vg.no (195.88.55.16): icmp_seq=2 ttl=247 time=1.89 ms
^C

Exercise 1: PWD

Add a «pwd» (Print Working Directory) command to our shell, using the dynamic variable $*CWD (Current Working Directory).

See docs.raku.org/language/variables#index-entry-$*CWD for more information about $*CWD. (Note that it is possible to change it manually, even to illegal values. Use chdir instead, as that ensures legal values.)

Start with «rash-allow2».

See The solution

Exercise 2: Configuration

A legal command as «top» after allowing it with «allow top» will not be legal the next time we run «rash», even though we can find it in the history.

Add support for a user configuration file «.rashrc» in the user's home directory. It is read when we start the shell, and we will ignore everything besides «allow» lines.

Start with «rash-allow2» or the answer Excercise 1 («rash-pwd»).

Remove the «do-allow "more";» line in the program.

«.rashrc» should look something like this:

allow more
allow top

Hint: Start without the configuration file, and handle the absence silently.

See The solution

Part 4: The Interrupt

See the next part; Part 4: Interrupt.

Note that the changes from exercise 1 and 2 have been applied to the code shown in part 4 and beyond.