Rash - A Raku Shell

Part 4: The Interrupt

by Arne Sommer

Rash - Part 4: The Interrupt

[67.4] Published 13. April 2020.

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

Catching Interrupts

In rash we can use <ontrol-C>to terminate a running program, e.g. «top». But it will terminate the shell as well.

<Control-C> sends the SIGINT interrupt (or signal). We can set up a handler to catch it with signal:

signal(SIGINT).tap();

We tap into (or subscribe to) Interrupts (which are Supply objects) with tap. We can specify code inside the tap, but in this case we want the program to ignore the interrupt and do nothing. The handler is invoked instead of the normal interrupt handler, which would have terminated the program.

This works extremely well. If we press <Control-C> while «top» is running, it terminates just that program. If we press it at the «rash» prompt, «rash» is terminated.

See docs.raku.org/language/routine/signal for more information about signal.

See docs.raku.org/language/routine/tap for more information about tap.

See docs.raku.org/type/Supply for more information about Supply.

I have added the line just before the while loop.

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

A Better Command List

The list of commands displayed by the «help» command is sorted, even for allow'es set up in the configuration file. But commands added manually in the shell itself are added to the end:

$ rash-interrupt
rash: Enter «exit» to exit
> help
Legal commands: allow cd exit help pwd run version more top
> allow less
> help
Legal commands: allow cd exit help pwd run version more top less
> allow less
> help
Legal commands: allow cd exit help pwd run version more top less less

We could sort the list each time we add to it, and remove any duplicates at the same time:

@commands = @commands.sort.squish;

I chose squish instead of unique as the list is sorted.

Or we could use a hash, avoiding the duplicate value problem:

my %commands(allow => True, cd  => True, exit    => True,  help => True,
             pwd   => True, run => True, version => True);

Or perhaps better (and slower):

my %commands; %commands{$_} = True for <allow cd exit help pwd run version>;

The when line becomes:

when "help" { say "Legal commands: { %commands.keys.sort }" }

In the callback:

for %commands.keys.grep(/^ $line /).sort -> $cmd

And finally «do-allow»:

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

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

An Even Better Command List

We can include all the programs in the path in the command list, making them available for <TAB> completion. At the same time, we can get rid of «run» (and «allow»).

The run and shell functions check the path for us, but this time we'll have to do it manually.

We have already shown how to do that; see Part 1: The Path.

Let us start with registering all the programs available in %programs:

my %programs;

rehash;

sub rehash
{
  for %*ENV<PATH>split(":") -> $dir
  {
    next unless $dir.IO.d;
    %programs{.Str} = "$dir/"  ~ .Str for indir($dir, { dir(:x) }).sort # [1]
  }
}

[1] Note that dir gives us IO objects back, so we have to stringify them (with .Str. The :x flag is the same as the IO.x, and is used to only select executable files.

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

See docs.raku.org/type/IO::Path#method_x for more information about IO.x.

Then we add a «which6» command (so that we don't hide the «which» program) to test that it works:

sub do-which ($command)
{
  if %commands{$command}
  {
    say "$command is a rash built-in command.";
  }
  else
  {
    say %programs{$command} if %programs{$command};
  }
}

Add this line to the given loop:

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

Update the list of commands:

my %commands; %commands{$_} = True for <cd exit help pwd version which6>;

And test it:

$ raku rash-path
rash: Enter «exit» to exit
> which6 true
/bin/true
> which6 trueX

It behaves exactly like the «which» program, except that it recognises internal commands.

The next step is executing commands without the «run» prefix. We change the default block:

default
{
  my ($cmd, @args) = $line.words;
  %programs{$cmd}
    ?? do-run %programs{$cmd}, @args
    !! say "Unknown command: \"$_\" (use \"help\" for a list of commands)";
}

And change the start of «do-run»:

sub do-run ($cmd, @args)
{
  my $res = @args

Test that it works:

$ raku rash-path
rash: Enter «exit» to exit
> emacs rash-pwd rash-run rash-tab

Then we can remove the «allow» and «run» commands:

  • Remove «allow» and «run» from %commands
  • Remove «allow» and «run» from the given loop
  • Remove the call to «read-conf» and the procedure
  • Remove the definition of «CONF_FILE»

And finally we add the programs to the command line completion list. In the linenoise callback, change this line:

for %commands.keys.grep(/^ $line /).sort -> $cmd

with these:

my @all = (|%commands.keys, |%programs.keys); # [1]

for @all.grep(/^ $line /).sort -> $cmd

[1] We flatten both lists, so that @all is a list of names.

Change the help message, to use «Built-in» instead of «Legal»:

when "help" { say "Built-in commands: { %commands.keys.sort }" }

The full program looks like this:

File: rash-path
use Linenoise;

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

linenoiseHistoryLoad(HIST_FILE);
linenoiseHistorySetMaxLen(HIST_LEN);

my %commands; %commands{$_} = True for <cd exit help pwd version which>;
my %programs;

do-rehash;

linenoiseSetCompletionCallback(-> $line, $c
{
  my @all = (|%commands.keys, |%programs.keys);

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

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

signal(SIGINT).tap();

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

  given $line
  {
    when /^cd\s+(\S+)/     { do-chdir $0; }
    when "exit"            { last; }
    when "help"            { say "Built-in commands: { %commands.keys.sort }" }
    when /^which6\s+(\S+)/ { do-which $0; }
    when "pwd"             { say $*CWD.Str; }
    when "version"         { say "Version 0.14"; }
    default
    {
      my ($cmd, @args) = $line.words;
      %programs{$cmd}
        ?? do-run %programs{$cmd}, @args
        !! say "Unknown command: \"$_\" (use \"help\" for a list of built-in commands)";
    }
  }
}

linenoiseHistorySave(HIST_FILE);

sub do-run ($cmd, @args)
{
  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 }";
  }
}

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

sub do-rehash
{
  for %*ENV.split(":") -> $dir
  {
    next unless $dir.IO.d;
    %programs{.Str} = "$dir/"  ~ .Str for indir($dir, { dir(:x) }).sort
  }
}

sub do-which ($command)
{
  if %commands{$command}
  {
    say "$command is a rash built-in command.";
  }
  else
  {
    say %programs{$command} if %programs{$command};
  }
}

Part 5: The Dynamic

See the next part; Part 5: The Dynamic.