Crate reedline_repl_rs

source ·
Expand description

reedline-repl-rs - REPL library for Rust

§Example

//! Minimal example
use reedline_repl_rs::clap::{Arg, ArgMatches, Command};
use reedline_repl_rs::{Repl, Result};

/// Write "Hello" with given name
fn hello<T>(args: ArgMatches, _context: &mut T) -> Result<Option<String>> {
    Ok(Some(format!(
        "Hello, {}",
        args.get_one::<String>("who").unwrap()
    )))
}

fn main() -> Result<()> {
    let mut repl = Repl::new(())
        .with_name("MyApp")
        .with_version("v0.1.0")
        .with_description("My very cool app")
        .with_banner("Welcome to MyApp")
        .with_command(
            Command::new("hello")
                .arg(Arg::new("who").required(true))
                .about("Greetings!"),
            hello,
        );
    repl.run()
}

reedline-repl-rs uses the builder pattern extensively. What these lines are doing is:

  • creating a repl with an empty Context (see below)
  • with a name of “MyApp”, the given version, and the given description
  • and adding a “hello” command which calls out to the hello callback function defined above
  • the hello command has a single parameter, “who”, which is required, and has the given help message

The hello function takes a reference to ArgMatches, and an (unused) Context, which is used to hold state if you need to - the initial context is passed in to the call to Repl::new, in our case, (). Because we’re not using a Context, we need to include a generic type in our hello function, because there’s no way to pass an argument of type () otherwise.

All command function callbacks return a Result<Option<String>>. This has the following effect:

  • If the return is Ok(Some(String)), it prints the string to stdout
  • If the return is Ok(None), it prints nothing
  • If the return is an error, it prints the error message to stderr

§Context

The Context type is used to keep state between REPL calls. Here’s an example:

//! Example using Repl with Context
use reedline_repl_rs::clap::{Arg, ArgMatches, Command};
use reedline_repl_rs::{Repl, Result};
use std::collections::VecDeque;

#[derive(Default)]
struct Context {
    list: VecDeque<String>,
}

/// Append name to list
fn append(args: ArgMatches, context: &mut Context) -> Result<Option<String>> {
    let name: String = args.get_one::<String>("name").unwrap().to_string();
    context.list.push_back(name);
    let list: Vec<String> = context.list.clone().into();

    Ok(Some(list.join(", ")))
}

/// Prepend name to list
fn prepend(args: ArgMatches, context: &mut Context) -> Result<Option<String>> {
    let name: String = args.get_one::<String>("name").unwrap().to_string();
    context.list.push_front(name);
    let list: Vec<String> = context.list.clone().into();

    Ok(Some(list.join(", ")))
}

fn main() -> Result<()> {
    let mut repl = Repl::new(Context::default())
        .with_name("MyList")
        .with_version("v0.1.0")
        .with_description("My very cool List")
        .with_command(
            Command::new("append")
                .arg(Arg::new("name").required(true))
                .about("Append name to end of list"),
            append,
        )
        .with_command(
            Command::new("prepend")
                .arg(Arg::new("name").required(true))
                .about("Prepend name to front of list"),
            prepend,
        )
        .with_on_after_command(|context| Ok(Some(format!("MyList [{}]", context.list.len()))));
    repl.run()
}

A few things to note:

  • you pass in the initial value for your Context struct to the call to Repl::new()
  • the context is passed to your command callback functions as a mutable reference
  • the prompt can be changed after each executed commmand using with_on_after_command as shown

§Async Support

The async feature allows you to write async REPL code:

//! Example using Repl with a custom error type.
use reedline_repl_rs::clap::{Arg, ArgMatches, Command};
use reedline_repl_rs::{Repl, Result};

/// Write "Hello" with given name
async fn hello<T>(args: ArgMatches, _context: &mut T) -> Result<Option<String>> {
    Ok(Some(format!(
        "Hello, {}",
        args.get_one::<String>("who").unwrap()
    )))
}

/// Called after successful command execution, updates prompt with returned Option
async fn update_prompt<T>(_context: &mut T) -> Result<Option<String>> {
    Ok(Some("updated".to_string()))
}

#[tokio::main]
async fn main() -> Result<()> {
    let mut repl = Repl::new(())
        .with_name("MyApp")
        .with_version("v0.1.0")
        .with_command_async(
            Command::new("hello")
                .arg(Arg::new("who").required(true))
                .about("Greetings!"),
            |args, context| Box::pin(hello(args, context)),
        )
        .with_on_after_command_async(|context| Box::pin(update_prompt(context)));
    repl.run_async().await
}

A few things to note:

  • The ugly Pin::Box workaround is required because of unstable rust async Fn’s

§Keybindings

Per default Emacs-style keybindings are used

//! Example with custom Keybinding
use reedline::{EditCommand, ReedlineEvent};
use reedline::{KeyCode, KeyModifiers};
use reedline_repl_rs::clap::{Arg, ArgMatches, Command};
use reedline_repl_rs::{Repl, Result};

/// Write "Hello" with given name
fn hello<T>(args: ArgMatches, _context: &mut T) -> Result<Option<String>> {
    Ok(Some(format!(
        "Hello, {}",
        args.get_one::<String>("who").unwrap()
    )))
}

fn main() -> Result<()> {
    let mut repl = Repl::new(())
        .with_name("MyApp")
        .with_version("v0.1.0")
        .with_description("My very cool app")
        .with_banner("Welcome to MyApp")
        .with_command(
            Command::new("hello")
                .arg(Arg::new("who").required(true))
                .about("Greetings!"),
            hello,
        )
        // greet friend with CTRG+g
        .with_keybinding(
            KeyModifiers::CONTROL,
            KeyCode::Char('g'),
            ReedlineEvent::ExecuteHostCommand("hello Friend".to_string()),
        )
        // show help with CTRL+h
        .with_keybinding(
            KeyModifiers::CONTROL,
            KeyCode::Char('h'),
            ReedlineEvent::ExecuteHostCommand("help".to_string()),
        )
        // uppercase current word with CTRL+u
        .with_keybinding(
            KeyModifiers::CONTROL,
            KeyCode::Char('u'),
            ReedlineEvent::Edit(vec![EditCommand::UppercaseWord]),
        )
        // uppercase current word with CTRL+l
        .with_keybinding(
            KeyModifiers::CONTROL,
            KeyCode::Char('l'),
            ReedlineEvent::Edit(vec![EditCommand::LowercaseWord]),
        );

    println!("Keybindings:");
    let keybindings = repl.get_keybindings();
    for search_modifier in [
        KeyModifiers::NONE,
        KeyModifiers::CONTROL,
        KeyModifiers::SHIFT,
        KeyModifiers::ALT,
    ] {
        for ((modifier, key_code), reedline_event) in &keybindings {
            if *modifier == search_modifier {
                println!("{:?} + {:?} => {:?}", modifier, key_code, reedline_event);
            }
        }
    }
    repl.run()
}

A few things to note:

  • The ugly Pin::Box workaround is required because of unstable rust async Fn’s

§Help

reedline-repl-rs automatically builds help commands for your REPL using clap print_help:

% myapp
MyApp> 〉help
MyApp v0.1.0: My very cool app

COMMANDS:
    append     Append name to end of list
    help       Print this message or the help of the given subcommand(s)
    prepend    Prepend name to front of list

MyApp> 〉help append
append
Append name to end of list

USAGE:
    append <name>

ARGS:
    <name>

OPTIONS:
    -h, --help    Print help information
MyApp> 〉

§Errors

Your command functions don’t need to return reedline_repl_rs::Error; you can return any error from them. Your error will need to implement std::fmt::Display, so the Repl can print the error, and you’ll need to implement std::convert::From for reedline_repl_rs::Error to your error type. This makes error handling in your command functions easier, since you can just allow whatever errors your functions emit bubble up.

//! Example using Repl with a custom error type.

use reedline_repl_rs::clap::{ArgMatches, Command};
use reedline_repl_rs::Repl;
use std::fmt;

#[derive(Debug)]
enum CustomError {
    ReplError(reedline_repl_rs::Error),
    StringError(String),
}

impl From<reedline_repl_rs::Error> for CustomError {
    fn from(e: reedline_repl_rs::Error) -> Self {
        CustomError::ReplError(e)
    }
}

impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            CustomError::ReplError(e) => write!(f, "REPL Error: {}", e),
            CustomError::StringError(s) => write!(f, "String Error: {}", s),
        }
    }
}

impl std::error::Error for CustomError {}

/// Do nothing, unsuccesfully
fn hello<T>(_args: ArgMatches, _context: &mut T) -> Result<Option<String>, CustomError> {
    Err(CustomError::StringError("Returning an error".to_string()))
}

fn main() -> Result<(), reedline_repl_rs::Error> {
    let mut repl = Repl::new(())
        .with_name("MyApp")
        .with_version("v0.1.0")
        .with_description("My very cool app")
        .with_command(
            Command::new("hello").about("Do nothing, unsuccessfully"),
            hello,
        );
    repl.run()
}

Re-exports§

Structs§

  • Main REPL struct

Enums§

Functions§

  • Utility to format prompt strings as green and bold. Use yansi directly instead for custom colors.
  • Utility to format prompt strings as yellow and bold. Use yansi directly instead for custom colors.

Type Aliases§