Rash - A Raku Shell

Exercise 4: Removing Aliases

by Arne Sommer

Rash - Exercise 4: Removing Aliases

[67.5.2] Published 13. April 2020.

You have read the Exercise text first?

Add the command to the list:

File: rash-unalias (changes only)
%commands{$_} = True for <alias cd exit help pwd rehash unalias version which6>;

Add this line to the given block:

when /^unalias\s+(.*)/ { unalias $0; }

Removing an alias is just a matter of removing it from the alias hash, done by this procedure:

File: rash-unalias (changes only)
sub unalias ($command)
{
  my ($alias) = $command.split(/\s/, 1); 

  if %aliases{$alias}:exists
  {
    %aliases{$alias}:delete;
    do-rehash;
  }
}

The call to «do-rehash» should set up the Linenoise command completion list again. We do that by moving the linenoiseSetCompletionCallback block into the procedure, so that it looks like this:

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

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

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

[1] This line has been moved from the when "rehash" ... line, as we need it every time we run the procedure.

The complete program:

File: rash-unalias
use Linenoise;

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

linenoiseHistoryLoad(HIST_FILE);
linenoiseHistorySetMaxLen(HIST_LEN);

my %commands; %commands{$_} = True for <alias cd exit help pwd rehash unalias version which6>;
my %programs;
my %aliases;

read-conf;
do-rehash;

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

signal(SIGINT).tap();

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

  my $cmd = $line.words[0];
  $line.= subst(/^$cmd/, %aliases{$cmd}) if %aliases{$cmd};

  given $line
  {
    when "alias"           { do-alias; }
    when /^alias\s+(.*)/   { do-alias $0; }
    when /^cd\s+(\S+)/     { do-chdir $0; }
    when "exit"            { last; }
    when "help"            { say "Built-in commands: { %commands.keys.sort }" }
    when "pwd"             { say $*CWD.Str; }
    when "rehash"          { do-rehash; }
    when /^unalias\s+(.*)/ { unalias $0; }
    when "version"         { say "Version 0.19"; }
    when /^which6\s+(\S+)/ { do-which $0; }
    default
    {
      my ($cmd, @args) = $line.words;
      %programs{$cmd}
        ?? do-run %programs{$cmd}, @args
        !! say "Unknown command: \"$_\" (use \"help\" for a list of 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
{
  %programs = ();
  
  for %*ENV<PATH>.split(":") -> $dir
  {
    next unless $dir.IO.d;
    %programs{.Str} = "$dir/"  ~ .Str for indir($dir, { dir(:x) }).sort
  }

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

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

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

multi sub do-alias ($command)
{
  my ($alias, $full) = $command.split(/\s/, 2); 

  if %commands{$alias}
  {
    say "Unable to redefine internal command: $alias.";
  }
  else
  {
    %aliases{$alias} = $full;
    do-rehash;
  }
}

multi sub do-alias ()
{
  say "$_ -> %aliases{$_}" for %aliases.keys.sort; 
}

sub unalias ($command)
{
  my ($alias) = $command.split(/\s/, 1); 

  if %aliases{$alias}:exists
  {
    %aliases{$alias}:delete;
    do-rehash;
  }
}

sub read-conf
{
  return unless CONF_FILE.IO.r;
  
  for CONF_FILE.IO.lines
  {
    when /^alias\s+(.*)/ { do-alias $0.Str }
  }
}

Note that the Linenoise command completion list now will include aliases added manually by the user in the shell, and not just those coming from the configuration file.

It may be overkill to traverse the path (registering the programs) again and again each time we add or remove an alias. It is sufficient to do it at startup and when the user types «rehash»:

File: rash-unalias2 (changes only)
    do-rehash(:full); # [1]
    when "rehash"          { do-rehash(:full); }
sub do-rehash (:$full)
{
  if ($full)
  {
    %programs = ();
    for %*ENV<PATH>.split(":") -> $dir
    {
      next unless $dir.IO.d;
      %programs{.Str} = "$dir/"  ~ .Str for indir($dir, { dir(:x) }).sort
    }
  }
  
  linenoiseSetCompletionCallback(-> $line, $c
  {
    my @all = (|%commands.keys, |%programs.keys, |%aliases.keys).sort;

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

[1] This is a named argument with «True» as the value. See my Raku Colonoscopy article for more information about the colon in Raku.

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