See also: The Introduction | Part 1: The Path.
The first first version of the shell just echos back what we type. Use «Ctrl-C» to exit it:
File: rash-loop
say 'rash: Enter <Control-C> to exit';
loop
{
say prompt "> "; # [1]
}
[1] prompt
prints the specified text (if any), waits
for user input, and returns it.
See
docs.raku.org/routine/prompt for more information about prompt
.
The program is short, concise and to the point. What point, you may ask, as it doesn't really do anything usefull yet.
An eternal loop without an exit strategy. Let us add an «exit» command, terminating the shell:
File: rash-exit
say 'rash: Enter «exit» to exit';
my $cmd;
loop
{
$cmd = prompt "> ";
last if $cmd eq "exit"; # [1]
say $cmd;
}
[1] I have used last
to break out of the loop. exit
is another possibility. It terminates the whole program, which in this case
amounts to the same thing.
Let us add support for a second command, e.g. «version».
Instead of if
(and a lot of elsif
as we add more commands),
we can use Raku's take on switch: given
/when
:
say 'rash: Enter «exit» to exit';
loop
{
given prompt "> "
{
when "exit" { last; }
when "version" { say "Version 0.01"; }
default { .say } # [1]
}
}
[1] default
in a given
block behaves in the same way as an else
in an if
block.
We could have written our when
clauses as statement modifiers:
say 'rash: Enter «exit» to exit';
loop
{
given prompt "> "
{
last when "exit";
say "Version 0.02" when "version";
default { .say; }
}
}
But the normal form, with the condition first, usually makes for more readable code.
given
sets $_
inside the following block, and can be used
alone (without when
) just to that effect:
> given 42 { .say; .^name.say; }
42
Int
We should give an error for unknown commands, and we add a «help» command showing legal commands:
File: rash-help
my @commands = <exit help version>; # [1]
say 'rash: Enter «exit» to exit';
loop
{
given prompt "> "
{
when "exit" { last; }
when "help" { say "Legal commands: { @commands }" }
when "version" { say "Version 0.03"; }
default { say "Unknown command: \"$_\" (use \"help\" for a list of commands)"; }
}
}
[1] The list of legal commands.
Can we combine loop
and given
, so that we can get rid of one
set of braces?
We can use when
in a for
loop:
for 42, 43, "foo", 44, "bar"
{
when Int { .say }
when /:i ^Bar/ { .say }
default { say "Not an Int or a Bar" }
}
# -> «4243Not an Int or a Bar44Bar»
Using for prompt "> "
in our code will not work as prompt
returns a single value.
We can make an iterator of prompt "> "
with
gather
/take
, and we have managed to remove one level
of braces.
my @commands = <exit help version>;
say 'rash: Enter «exit» to exit';
my $prompt := gather take prompt "> " while True; # [1]
for $prompt
{
when "exit" { last; }
when "help" { say "Legal commands: { @commands }" }
when "version" { say "Version 0.04"; }
default { say "Unknown command: \"$_\" (use \"help\" for a list of commands)"; }
}
[1] while True
gives an eternal loop.
If we enter a command with a typo, e.g «histroy» instead of «history», we'd want to edit the command and try again. As supported by the up and down arrow keys in a normal shell. But that doesn't work.
It works in «REPL», courtesy of the «Linenoise» module. We can do the same.
We start with a look at the module documentation:
$ p6doc Linenoise
Note the program name, where «p6» stands for «Perl 6». The program should really be renamed to «rakudoc».
If the formatting is less than awesome, read the documentation online instead: github.com/hoelzro/p6-linenoise.
The «Basic History» Section:
use Linenoise;
my constant HIST_FILE = '.myhist';
my constant HIST_LEN = 10;
linenoiseHistoryLoad(HIST_FILE);
linenoiseHistorySetMaxLen(HIST_LEN);
while (my $line = linenoise '> ').defined {
linenoiseHistoryAdd($line);
say "got a line: $line";
}
linenoiseHistorySave(HIST_FILE);
We can apply this to our shell like this. And note that the gather
/take
we added in the «rash-gather» version had to go away:
use Linenoise;
constant HIST_FILE = ( $*HOME.add: '.rash-hist' ).Str;
constant HIST_LEN = 25;
linenoiseHistoryLoad(HIST_FILE);
linenoiseHistorySetMaxLen(HIST_LEN);
This is mostly copy and paste, except that we store the history file («.rash-hist») in the user's home directory.
The dynamic variable $*HOME
gives us the home directory, as
an IO
object. We attach the history file name with the IO.add
method (that handles the directory separator for us, so that we can avoid that problem).
We then apply .Str
on it to get a string.
Remember to convert things that are not string
to strings, as done here with an IO
object. The «Linenoise» library is written
in C, and sending off objects to C code expecting string values can lead to interesting errors.
See
docs.raku.org/language/variables#index-entry-$*HOME for more information about
$*HOME
.
See
docs.raku.org/routine/add for more information about IO.add
.
The loop looks like this:
while (my $line = linenoise '> ').defined
{
linenoiseHistoryAdd($line);
given $line
{
...
}
}
Note that «Linenoise» catches <Control-C>, but the
value is undefined in that case. We use .defined
on the loop to drop out when
<Control-C>is used.
Also note that «linenoiseHistoryAdd» ignores empty lines, and will not add the command if it is exactly the same as the previous one.
All that remains is saving the history file:
linenoiseHistorySave(HIST_FILE);
Let us add command completion while we are at it, as it is described in the «Linenoise» documentation as well.
The «Tab Completion» Section:
use Linenoise;
my @commands = <help quit list get set>;
linenoiseSetCompletionCallback(-> $line, $c {
my ( $prefix, $last-word ) = find-last-word($line);
for @commands.grep(/^ "$last-word" /).sort -> $cmd {
linenoiseAddCompletion($c, $prefix ~ $cmd);
}
});
while (my $line = linenoise '> ').defined {
say "got a line: $line";
}
The procedure «find-last-word» isn't defined anywhere, but we can get away with a simpler version expanding the whole command.
We add this simplified callback after the line defining @commands
:
linenoiseSetCompletionCallback(-> $line, $c
{
for @commands.grep(/^ "$line" /).sort -> $cmd
{
linenoiseAddCompletion($c, $cmd);
}
});
The full program looks like this:
File: rash-tab
use Linenoise;
constant HIST_FILE = ( $*HOME.add: '.rash-hist' ).Str;
constant HIST_LEN = 25;
linenoiseHistoryLoad(HIST_FILE);
linenoiseHistorySetMaxLen(HIST_LEN);
my @commands = <exit help version>;
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 "exit" { last; }
when "help" { say "Legal commands: { @commands }" }
when "version" { say "Version 0.05"; }
default { say "Unknown command: \"$_\" (use \"help\" for a list of commands)"; }
}
}
linenoiseHistorySave(HIST_FILE);
It isn't very useful yet, but try it out.
A history command could have been nice, but «Linenoise» doesn't support it.
See the next part; Part 3: The Execution.