See also: The Introduction | Part 1: The Path | Part 2: The Loop | Part 3: The Execution.
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».
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».
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:
%commands
given
loop
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};
}
}
See the next part; Part 5: The Dynamic.