Using the REPL

The Relision library provides a generic read, evaluate, print loop (a REPL) that is available through the `relision::repl::Repl` struct. This provides the following services.

  • Loading and saving of command history
  • Reading lines from the prompt with history
  • Detecting and executing commands, including providing help
  • Word wrapping of the output
  • Coloring of error and other message types.

The REPL supports a concept of commands. These are indicated by a leading colon, such as :help.

Simplest Use

The minimum that must be done is to make a new instance of the Repl struct and then invoke its run method. This will not load or save any history, and it will provide the default set of commands and will, by default, just echo other lines.

The one wrinkle is that you must provide some data to the constructor. Why this is so will become clear later. For now, let's just provide () to the constructor.

  1. use relision::repl;
  2. use relision::configure_logging;
  3. fn main() {
  4. configure_logging();
  5. let mut repl = repl::Repl::new(());
  6. repl.run();
  7. }

Note the configure_logging call. The Relision library uses the log crate to send log messages. If you are using your own logging framework, you should set it up however you want, but you can use this library call to configure it using the RELISION_LOG environment variable, if you wish.

We can try this. This example is repl_example1.rs in the examples folder. You should be able to build and run it with the command cargo run --example repl_example1.

Commands start with a colon (:).  Enter :help for help.
> :help
Commands can be abbreviated to the first few characters that uniquely identify them.

:help       Access the help system.
:history    Provide help on commands.
:info       Provide system information.
:quit       Quit the REPL.

> :fred
Error: Command fred not found.
> fred
> :quit

Adding Line Evaluation

Let's add a custom evaluator for lines. Our “evaluation” will consist of just echoing the line, converted to all uppercase and if the line is “QUIT” then we will also exit the REPL.

To do this we need to create a function that takes a &String and a &mut relision::repl::ReplContext. The &String is the line to evaluate, and the ReplContext packages information from the REPL that we might need. The return is an Option<String> and a Boolean. The return value is interpreted by the REPL as follows.

  • If the returned Option<String> is not None then it is output regardless of the quiet setting.
  • If the Boolean is true then the REPL continues, and if it is false the REPL terminates.

The function we want looks like this.

  1. use relision::repl::ReplContext;
  2.  
  3. fn echo<U>(line: &String, _context: &mut ReplContext<U>) -> (Option<String>, bool) {
  4. let answer = line.to_uppercase();
  5. let cont = answer != "QUIT";
  6. (Some(answer), cont)
  7. }

The <U> will be used later when we introduce user data, but for now you can ignore it. Also note that the mut on context will be required later when we read lines from the editor, which is in the context and must be mutable.

We install echo as the evaluation function after creating the instance and before invoking run. The full example looks as follows.

  1. use relision::repl;
  2. use relision::repl::ReplContext;
  3.  
  4. fn echo<U>(line: &String, _context: &mut ReplContext<U>) -> (Option<String>, bool) {
  5. let answer = line.to_uppercase();
  6. let cont = answer != "QUIT";
  7. (Some(answer), cont)
  8. }
  9.  
  10. fn main() {
  11. let mut repl = repl::Repl::new(());
  12. repl.set_eval(echo);
  13. repl.run();
  14. }

Let's try this. You can run this from the source distribution with cargo run --example repl_example2.

Commands start with a colon (:).  Enter :help for help.
> :help
Commands can be abbreviated to the first few characters that uniquely identify them.

:help       Access the help system.
:history    Provide help on commands.
:info       Provide system information.
:quit       Quit the REPL.

> :fred
Error: Command fred not found.
> fred
FRED
> quit
QUIT

Adding a New Command

We can add a new command to the REPL using the add_command method. We need to provide the command name, the help text, and a callback for the command. The callback has the same form as for the evaluation method.

We will use our echo function for this, but we don't want to quit when the user enters quit, so we just modify the returned Boolean to always be true. Again, we need to install this command after creating the REPL instance and before invoking run. While we are at it, let's also install a “no-op” command that does nothing. We could do this with a closure, as before: |_,_| (None, true), but let's use a function pointer, instead.

We will also change the prompt, using set_prompt.

  1. use relision::repl;
  2. use relision::repl::ReplContext;
  3.  
  4. fn noop<U>(_line: &String, _context: &mut ReplContext<U>) -> (Option<String>, bool) {
  5. (None, true)
  6. }
  7.  
  8. fn echo<U>(line: &String, _context: &mut ReplContext<U>) -> (Option<String>, bool) {
  9. let answer = line.to_uppercase();
  10. let cont = answer != "QUIT";
  11. (Some(answer), cont)
  12. }
  13.  
  14. fn main() {
  15. let mut repl = repl::Repl::new(&());
  16. repl.set_eval(echo);
  17.  
  18. // Install a custom command (using a closure).
  19. repl.add_command(
  20. "echo".to_string(),
  21. "Echo the rest of the line in uppercase.".to_string(),
  22. "Usage: :echo [line]\n\
  23. Echo the provided line, after converting it to all upper case."
  24. .to_string(),
  25. |line, context| (echo(line, context).0, true),
  26. );
  27.  
  28. // Install a custom colon command (using a function pointer).
  29. repl.add_command(
  30. "noop".to_string(),
  31. "Perform no action.".to_string(),
  32. "Usage: :noop\n\
  33. Ignore any arguments and perform no action. Could be a comment."
  34. .to_string(),
  35. noop,
  36. );
  37.  
  38. // Set the prompt and start the REPL.
  39. repl.set_prompt("repl> ".to_string());
  40. repl.run();
  41. }

Again, let's try this. Note that this is repl_example3 in the distribution.

Commands start with a colon (:).  Enter :help for help.
repl> :help
Commands can be abbreviated to the first few characters that uniquely identify them.

:echo       Echo the rest of the line in uppercase.
:help       Access the help system.
:history    Provide help on commands.
:info       Provide system information.
:noop       Perform no action.
:quit       Quit the REPL.

repl> :echo I'm not shouting.
I'M NOT SHOUTING.
repl> :noop This is ignored.
repl> :echo quit
QUIT
repl> quit
QUIT

User Data

Our functions will probably want to do something a little more sophisticated, so let's take a look at ReplContext. We get some important things packaged in this struct.

  • The line editor (provided by the rustyline crate) being used by the REPL, which gives us access to line history as well as the ability to read lines ourselves and (potentially) have those lines be part of the same history.
  • The terminal (provided by the console crate) that lets us provide a consistent output, though this is not essential.
  • The current value of the “quiet” flag, that lets us decide whether we should provide output. Note that if our callbacks return some string, that string is always printed.
  • Some user data, which can hold anything we want.

Reading a Line

You can read a line from the console using context.editor.readline(prompt). Here prompt is a string prompt to print. The result will be one of the following.

  • Ok(text) if text is successfully read
  • Err(ReadlineError::Interrupted) if CTRL+C is pressed
  • Err(ReadliineError::Eof) if CTRL+D is pressed or the input stream ends
  • Err(_) if some other error occurs

Lines read in this way are not automatically added to the history; you can use context.editor.add_history_entry(text.clone()) to add text to the history.

Printing

The context provides context.term

Let's add a guessing game to the program. We will use the user data to hold the number of times the user has won and lost.

  1. pub struct Counts {
  2. pub win: u32,
  3. pub lose: u32,
  4. }

Now we will specialize ReplContext on this struct in the callbacks. Let's write the guessing game code.

  1. fn guess(_line: &String, context: &mut ReplContext<Counts>) -> (Option<String>, bool) {
  2. let limit = 10;
  3. let high = 100;
  4. // Tell the user the rules.
  5. sayln!(context, "I'm choosing a number in the interval [1,{}].", high);
  6. sayln!(context, "You have played {} games and won {}.",
  7. context.get().win + context.get().lose, context.get().win);
  8. sayln!(context, "Try to guess it! You have {} guesses.", limit);
  9. // Pick a random number.
  10. let secret = rand::thread_rng().gen_range(1, high+1);
  11. // Collect guesses.
  12. let mut attempt = 1;
  13. while attempt < limit {
  14. // Get the next guess.
  15. let line = context.editor.readline(
  16. format!("Enter your guess (attempt {}): ", attempt).as_str());
  17. match line {
  18. Ok(text) => {
  19. sayln!(context, "[ {} ]", text);
  20. match text.trim().parse::<u32>() {
  21. Ok(number) => {
  22. if number < secret {
  23. sayln!(context, "Too low.");
  24. } else if number > secret {
  25. sayln!(context, "Too high.");
  26. } else {
  27. sayln!(context, "That's it!");
  28. context.get_mut().win += 1;
  29. return (None, true);
  30. }
  31. },
  32. Err(_) => {
  33. sayln!(context, "Please enter a number.");
  34. }
  35. }
  36. },
  37. Err(ReadlineError::Interrupted) => {
  38. // This is a CTRL+C. Stop the game.
  39. sayln!(context, "Game stopped by CTRL+C.");
  40. return (None, true);
  41. },
  42. Err(ReadlineError::Eof) => {
  43. // This is end of file (CTRL+D). Stop the game.
  44. sayln!(context, "Game stopped by CTRL+D.");
  45. return (None, true);
  46. },
  47. Err(err) => {
  48. // This is just an error. Continue.
  49. error!("{:?}", err);
  50. }
  51. }
  52. attempt += 1;
  53. } // Collect guesses.
  54. context.get_mut().lose += 1;
  55. sayln!(context, "You ran out of attempts!");
  56. (None, true)
  57. }

There is a lot going on in this function. Note the context.editor.readline. This function take a prompt and requests a line from the user through the configured editor. You can see how the different kinds of errors are handled. Note that lines read in this manner do not automatically go into the history.

If you wanted to add the line to the history, you would do the following.

  1. context.editor.add_history_entry(text.clone());