See also: The Introduction | Part 1: The Path | Part 2: The Loop.
We'll start a little Execution detour, before continuing with the Shell.
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.
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 ********
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:
0
for program
success, and a positive number when an error occurred.
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».
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.
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
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
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
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.