Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
lib:repl [2018/12/31 23:31] – [Simplest Use] reladmlib:repl [2020/11/15 23:14] (current) – Stopped at printing; more needs to be done. sprowell
Line 9: Line 9:
   * Coloring of error and other message types.   * Coloring of error and other message types.
  
-The REPL support a concept of //commands// These are indicated by a leading colon, such as '':help''.+The REPL supports a concept of //commands// These are indicated by a leading colon, such as '':help''.
  
 ===== Simplest Use ===== ===== 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 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.
  
 <code Rust [enable_line_numbers="true"]> <code Rust [enable_line_numbers="true"]>
 use relision::repl; use relision::repl;
 +use relision::configure_logging;
 fn main() { fn main() {
-    let mut repl = repl::Repl::new(); +    configure_logging(); 
-    repl.run(());+    let mut repl = repl::Repl::new(()); 
 +    repl.run();
 } }
 +</code>
 +
 +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 ''<nowiki>cargo run --example repl_example1</nowiki>''.
 +
 +<code>
 +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
 </code> </code>
  
 ===== Adding Line Evaluation ===== ===== Adding Line Evaluation =====
  
-Let's add custom evaluation of lines.  Our "evaluation" will consist of just echoing the line, converted to all upper case andif the line is "QUIT" then we will also exit the REPL.+Let's add 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 ''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.  If the returned ''Option<String>'' is //not// ''None'' then it is output.  If the Boolean is ''true'' then the REPL continues, and if it is ''false'' the REPL terminates.+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. The function we want looks like this.
  
 <code Rust [enable_line_numbers="true"]> <code Rust [enable_line_numbers="true"]>
-fn echo(line: &String, _context: ReplContext) -> (Option<String>, bool) {+use relision::repl::ReplContext; 
 + 
 +fn echo<U>(line: &String, _context: &mut ReplContext<U>) -> (Option<String>, bool) {
     let answer = line.to_uppercase();     let answer = line.to_uppercase();
     let cont = answer != "QUIT";     let cont = answer != "QUIT";
Line 39: Line 68:
 </code> </code>
  
-We install it as the evaluation function after creating the instance and before invoking ''run''.+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.
  
 <code Rust [enable_line_numbers="true"]> <code Rust [enable_line_numbers="true"]>
 +use relision::repl;
 +use relision::repl::ReplContext;
 +
 +fn echo<U>(line: &String, _context: &mut ReplContext<U>) -> (Option<String>, bool) {
 +    let answer = line.to_uppercase();
 +    let cont = answer != "QUIT";
 +    (Some(answer), cont)
 +}
 +
 fn main() { fn main() {
-    let mut repl = repl::Repl::new();+    let mut repl = repl::Repl::new(());
     repl.set_eval(echo);     repl.set_eval(echo);
     repl.run();     repl.run();
 } }
 </code> </code>
 +
 +Let's try this.  You can run this from the source distribution with ''<nowiki>cargo run --example repl_example2</nowiki>''.
 +
 +<code>
 +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
 +</code>
 +
 +===== 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''.
 +
 +<code Rust [enable_line_numbers="true"]>
 +use relision::repl;
 +use relision::repl::ReplContext;
 +
 +fn noop<U>(_line: &String, _context: &mut ReplContext<U>) -> (Option<String>, bool) {
 +    (None, true)
 +}
 +
 +fn echo<U>(line: &String, _context: &mut ReplContext<U>) -> (Option<String>, bool) {
 +    let answer = line.to_uppercase();
 +    let cont = answer != "QUIT";
 +    (Some(answer), cont)
 +}
 +
 +fn main() {
 +    let mut repl = repl::Repl::new(&());
 +    repl.set_eval(echo);
 + 
 +    // Install a custom command (using a closure).
 +    repl.add_command(
 +        "echo".to_string(),
 +        "Echo the rest of the line in uppercase.".to_string(),
 +        "Usage: :echo [line]\n\
 +         Echo the provided line, after converting it to all upper case."
 +            .to_string(),
 +        |line, context| (echo(line, context).0, true),
 +    );
 + 
 +    // Install a custom colon command (using a function pointer).
 +    repl.add_command(
 +        "noop".to_string(),
 +        "Perform no action.".to_string(),
 +        "Usage: :noop\n\
 +         Ignore any arguments and perform no action.  Could be a comment."
 +            .to_string(),
 +        noop,
 +    );
 + 
 +    // Set the prompt and start the REPL.
 +    repl.set_prompt("repl> ".to_string());
 +    repl.run();
 +}
 +</code>
 +
 +Again, let's try this.  Note that this is ''repl_example3'' in the distribution.
 +
 +<code>
 +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
 +</code>
 +
 +===== 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 [[https://crates.io/crates/rustyline|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 [[https://crates.io/crates/console|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.
 +
 +<code Rust [enable_line_numbers="true"]>
 +pub struct Counts {
 +    pub win: u32,
 +    pub lose: u32,
 +}
 +</code>
 +
 +Now we will specialize ''ReplContext'' on this struct in the callbacks.  Let's write the guessing game code.
 +
 +<code Rust [enable_line_numbers="true"]>
 +fn guess(_line: &String, context: &mut ReplContext<Counts>) -> (Option<String>, bool) {
 +    let limit = 10;
 +    let high = 100;
 +    // Tell the user the rules.
 +    sayln!(context, "I'm choosing a number in the interval [1,{}].", high);
 +    sayln!(context, "You have played {} games and won {}.",
 +        context.get().win + context.get().lose, context.get().win);
 +    sayln!(context, "Try to guess it!  You have {} guesses.", limit);
 +    // Pick a random number.
 +    let secret = rand::thread_rng().gen_range(1, high+1);
 +    // Collect guesses.
 +    let mut attempt = 1;
 +    while attempt < limit {
 +        // Get the next guess.
 +        let line = context.editor.readline(
 +            format!("Enter your guess (attempt {}): ", attempt).as_str());
 +        match line {
 +            Ok(text) => {
 +                sayln!(context, "[ {} ]", text);
 +                match text.trim().parse::<u32>() {
 +                    Ok(number) => {
 +                        if number < secret {
 +                            sayln!(context, "Too low.");
 +                        } else if number > secret {
 +                            sayln!(context, "Too high.");
 +                        } else {
 +                            sayln!(context, "That's it!");
 +                            context.get_mut().win += 1;
 +                            return (None, true);
 +                        }
 +                    },
 +                    Err(_) => {
 +                        sayln!(context, "Please enter a number.");
 +                    }
 +                }
 +            },
 +            Err(ReadlineError::Interrupted) => {
 +                // This is a CTRL+C.  Stop the game.
 +                sayln!(context, "Game stopped by CTRL+C.");
 +                return (None, true);
 +            },
 +            Err(ReadlineError::Eof) => {
 +                // This is end of file (CTRL+D).  Stop the game.
 +                sayln!(context, "Game stopped by CTRL+D.");
 +                return (None, true);
 +            },
 +            Err(err) => {
 +                // This is just an error.  Continue.
 +                error!("{:?}", err);
 +            }
 +        }
 +        attempt += 1;
 +    } // Collect guesses.
 +    context.get_mut().lose += 1;
 +    sayln!(context, "You ran out of attempts!");
 +    (None, true)
 +}
 +</code>
 +
 +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.
 +
 +<code Rust [enable_line_numbers=true]>
 +context.editor.add_history_entry(text.clone());
 +</code>
 +