=begin pod

=head1 NAME

Commands - handle interactive user commands

=head1 SYNOPSIS

=begin code :lang<raku>
use Commands;

# Set up commands
my $commands = Commands.new:
  default  => { say .EVAL },
  commands => (
    quit  => { last },  # also allows "q", "qu", "qui"
    exit  => "quit",    # handle same as "quit"
    sleep => { sleep .[1] // 1 }
    ""    => { say "assume same" },
    "release all" => { say "all released" },
    &shout,
  ),
;

sub shout($_) { say .skip.uc ~ "!" }

# Run a very simple REPL
loop {
    last without my $line = prompt("> ");
    $commands.process($line);
}
=end code

=head1 DESCRIPTION

The C<Commands> role provides a declarative way to handle (user) inputs
in REPL-like environments.  It allows this by describing the possible
command interactions once (possibly at compile time) and the associated
actions, and takes away the burden of figuring out all of the possible
combinations of commands with possible shortcuts.

The main declarations consist of the named argument C<"commands"> which
is expected to contain a C<List> of values.  Each value can either be
aC<Pair> where the key is the command, and the value is either a
C<Callable>, or a string referring to an other command (allowing a
simple way to define aliases).  Or can be a subroutine: in which case
the name of the subroutine will be taken as the command name.

The action to be performed if the user entered something that did not
match any of the commands, can also be specified with C<"default">.

=head2 Specifying a command

Each command consists of zero or more words.  The user will be able to
shortcut the first word of any command until it is no longer unique.  So
if we take the command set in the SYNOPSIS, the user can enter "sc" to
execute the "scream" command, but cannot enter "s", because that would be
ambiguous with "sleep".  On the other hand, the user would be able to
enter "r all", because only one command starts with "r", but would not
be able to shorten "all" to "a", as only the first word of any command
can be shortened.

=head1 DECLARATIONS

The declarations of possible actions can be done at object instantion
(with the C<:commands> named argument) or be added later at run-time
(possibly interactively) with the C<add-command> method.

=head1 METHODS

=head2 new

=begin code :lang<raku>
# Set up commands
my $commands = Commands.new:
  catch    => True,
  default  => { say .EVAL },
  commands => (
    quit  => { last },  # also allows "q", "qu", "qui"
    exit  => "quit",    # handle same as "quit"
    shout => { say .skip.uc ~ "!" },
    ...
  ),
  out => $*OUT,  # default: $*OUT at moment of .process(...)
  err => $*ERR,  # default: $*ERR at moment of .process(...)
  sys => $.err,  # default: self.err at moment of .process(...)
  tokenizer    => *.words,
  commandifier => *.split(";").map(*.trim),  # default: False
;
=end code

=head3 :commands

Required.  The C<:commands> named argument expects a list.  Its values
should either be a C<Pair> of which the key determines the command name,
and the value determines the associated action to be performed.  The
action to be performed can be an actual C<Callable>, or a string
indicating the command the given key is an alias to.  Or it can be a
subroutine: in which case the name of the subroutine will be taken as
the command name.

If a C<Callable> is specified, then it should expect a single positional
argument: a C<List> of the "tokens" as entered by the user (which defaults
to executing the C<.words> method on the input string).  Not specifying
parameters on a C<Block> is generally enough, especially if one is B<not>
interested in any additional arguments.

=head3 :default

Required.  The C<:default> named argument expects a single C<Callable> to
be executed if an input could not be interpreted to be any of the known
commands.

=head3 :catch

Optional.  The C<:catch> named argument indicates whether any exceptions
should be caught, and have just their message shown.  It defaults to
C<True>.  If C<False> was specified, any exception will terminate the
program, and produce a stack trace.

=head3 :out

Optional.  The C<:out> named argument specifies the value of C<$*OUT>
whenever a command is executed.  If not specified, or specified with
an undefined value, will assume the value of $*OUT at command execution
time.

=head3 :err

Optional.  The C<:err> named argument specifies the value of C<$*ERR>
whenever a command is executed.  If not specified, or specified with
an undefined value, will assume the value of $*ERR at command execution
time.

=head3 :sys

Optional.  The C<:sys> named argument specifies the output handle to be
used for system messages, such as when a shortened command entered by
the user turned out to be ambiguous.  It defaults to the (implicit) value
specified with C<:err>.

=head3 :tokenizer

Optional.  The C<:tokenizer> named argument specifies how a user input
line should be parsed into tokens.  It should be specified with a C<Callable>.
It defaults to C<*.words>.

=head3 :commandifier

Optional.  The C<:commandifier> named argument specifies how a user input
line should be separated into multiple commands.  It should be specified
with a C<Callable>.  It defaults to C<False>, indicating that each line
passed to "process" should always be seen as a single command.

=head2 add-command

=begin code :lang<raku>
$commands.add-command( "sleep" => { sleep .[1] // 1 } );

sub frobnicate($_) { say "frobnicating: $_" }
$commands.add-command(&frobnicate);  # add "frobnicate" command

$commands.add-command( "bt" => { say "backtrace" }, :no-shortcuts );
=end code

The C<add-command> method allows one to add a command to the existing
command structure during the lifetime of the C<Commands> object.  It
expects an argument, just as in the C<List> specified with the
C<:commands> named argument at object instantion.

Optionally takes a C<:no-shortcuts> boolean named argument: if specified
with a true value, will B<not> create any shortcuts for the given command
name.  By default, shortcuts for the command name will be created as long
as they don't interfere with other commands.

=head2 resolve-command

=begin code :lang<raku>
say $commands.resolve-command("foo");
=end code

The C<resolve-command> method is a helper method that will either
expand the given command, or return C<Nil> if it couldn't for whatever
reason.

=head2 process

=begin code :lang<raku>
loop {
    last without my $line = prompt("> ");
    $commands.process($line);
}
=end code

The C<process> method takes a single string argument, attempts to process
it as one or more command statements.  Each command is then tokenized and
an associated action C<Callable> is selected.

If no direct action could be identified, the shortened versions of the
first token will tried.  If that also fails, the default action will be
assumed.

It then sets the these dynamic variables:
=item $*INPUT - the non-tokenized original command statement
=item $*COMMANDS - the Commands object
=item $*OUT - the value (implicitely) specified with :out
=item $*ERR - the value (implicitely) specified with :err

And then executes the associated action C<Callable> with the C<List> of
tokens passed as the single argument.  If the execution failed for any
reason, the associated error message will be shown using the value
(implicitely) specified with C<:err> at instantiation.

=head2 primaries

=begin code :lang<raku>
my $commands = Commands.new:
  default => { say .EVAL },
  commands => (
    ...
    help => { .say for $*COMMANDS.primaries },
    ...
  ),
;
=end code

The C<primaries> method returns a sorted list of primary commands (that
is: commands that specified).  It is intended to provide basic "help"
support.

=head2 aliases

=begin code :lang<raku>
my $commands = Commands.new:
  default => { say .EVAL },
  commands => (
    quit => { last },
    exit => "quit",
    ...
  ),
;

say "$_.key(): $_.value()" for $commands.aliases;  # quit: exit
=end code

The C<aliases> method returns a (potentionally empty) C<Map> with as
keys the commands that have aliases, and as values a C<List> of commands
that are aliases to the command of the key.

=head2 commands

The C<commands> method returns a C<Map> with the internal lookup structure.
It is intended for debugging issues only.

=head2 default

The C<default> method returns the C<Callable> that was specified with the
C<:default> named argument at object instantiation.  It is intended for
debugging issues only.

=head2 catch

=begin code :lang<raku>
say $commands.catch;  # True

$commands.catch = False;
=end code

The C<catch> method returns a C<Bool> to indicate whether processing of
a command is secured by a C<CATCH> mechanism.  It can also be called
as a left-value to allow changing of the catch state.

Note that disabling the catch mechanism is usually done only in a
debugging environment.

=head2 out

Returns the object that was (implicitely) specified with the C<:out>
named argument at object instantiation.

=head2 err

Returns the object that was (implicitely) specified with the C<:err>
named argument at object instantiation.

=head2 sys

Returns the object that was (implicitely) specified with the C<:sys>
named argument at object instantiation.

=head2 tokenizer

Returns the C<Callable> that was (implicitely) specified with the
C<:tokenizer> named argument at object instantiation.

=head2 commandifier

Returns what was (implicitely) specified with the C<:commandifier>
named argument at object instantiation.

=head1 CREATING EXTENDED HELP

Creating a "help" command is easy.  Handling more in-depth help
requests can be more complicated, like "help frobnicate" to get
more help about the "frobnicate" command.  But since commands
can be shortened, e.g. to "frob", users might be inclined to
enter "help frob" to get help about the "frobnicate" command.

The C<extended-help-from-hash> instance method returns a new
C<Commands> object that can help with the "help frob" case,
as well as the "help frobnicate" case.

All one needs to do is set up a hash where a key should match
one of the commands of a C<Commands> instance, and the value
is a text to be shown when in-depth help is requested.

A simple case:

=begin code :lang<raku>
my constant %help =
  quit  => "Quit the editor and save the history",
  shout => "Say input in capitals with exclamation mark",
;

my $commands = Commands.new:
  default => { note "default: '$*INPUT'" },
  commands => (
    quit  => { last },
    exit  => "quit",
    shout => { say .skip.uc ~ "!" },
    help  => {
        state $help = $*COMMANDS.extended-help-from-hash(%help);
        if .skip.join(" ") -> $deeper {
            $help.process($deeper)
        }
        else {
            .say for $*COMMANDS.primaries;
        }
    },
  )
);

loop {
    last without my $line = prompt("> ");
    $commands.process($line);
}
=end code

=begin output
> help
exit
help
quit
shout
> help quit
More information about: quit
Quit the editor and save the history
> help sh
More information about: shout
Say input in capitals with exclamation mark
> h e
More information about: exit
Quit the editor and save the history
=end output

To customize the handling further, it is possible to specify the
following named arguments:

=head3 :default

Specifies the C<Callable> to be called if the in-depth help request
could not be processed, with the same semantics as the <:default>
named argument on C<.new>.

=head3 :handler

Specifies a C<Callable> that will be called for an in-depth help
request for which there is a help text.  It is expected to receive
two positional arguments: the full command name (e.g. "quit") and
the associated text (e.g. "Quit the editor and save the history").

=head1 AUTHOR

Elizabeth Mattijsen <liz@raku.rocks>

Source can be located at: https://codeberg.org/lizmat/Commands . Comments and
Pull Requests are welcome.

If you like this module, or what I'm doing more generally, committing to a
L<small sponsorship|https://github.com/sponsors/lizmat/>  would mean a great
deal to me!

=head1 COPYRIGHT AND LICENSE

Copyright 2024, 2025, 2026 Elizabeth Mattijsen

This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.

=end pod

# vim: expandtab shiftwidth=4
