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§

pub use clap;
pub use crossterm;
pub use nu_ansi_term;
pub use reedline;
pub use yansi;

Structs§

Repl
Main REPL struct

Enums§

Error
Error type

Functions§

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

Type Aliases§

AfterCommandCallback
AfterCommand callback function signature
Callback
Command callback function signature
Result
Result type