#shell #cmd #interactive #repl #command-line #command-line-arguments #keyboard-shortcuts

clapcmd

A readline wrapper that allows for creating custom interactive shells, similar to python's cmd module

9 releases

0.3.3 Aug 20, 2023
0.3.2 Aug 1, 2023
0.3.0 Jul 31, 2023
0.2.1 Jul 31, 2023
0.1.1 Jul 28, 2023

#566 in Command-line interface

MIT/Apache

71KB
2K SLoC

ClapCmd

A library to quickly build full-featured REPLs supported by CLAP and readline (provided via rustyline)

Features

  • Full readline support that exposes all customization for rustyline
    • emacs-style keyboard shortcuts by default (customizable via rustyline)
    • command history (in memory buffer)
  • Full integration with clap builders allowing for full-featured commands
  • Tab completion for:
    • commands and (TODO) command aliases
    • arguments
    • subcommands
    • values supplied via value_parsers (i.e. a list of valid values)
    • value hints (e.g. ValueHint::FilePath)
    • TODO: callback and/or demo for how to query value_parsers at runtime
  • Callback style approach with provided state
  • Customizable prompts that can be updated at anytime during execution
  • Support for writing to stdout outside of the command loop without mangling the input line via get_async_writer()
  • Create loadable and unloadable command groups
  • Multiline input support via the '\' character at end-of-line
  • Combine multiple commands in one line via:
    • semicolon (;) for unconditional evaluation
    • double ampersand (&&) for chaining successful evaluations
    • double pipe (||) for error handling evaluations
  • Automated testing via the test-runner feature

Basic Example

A minimal example showing a basic REPL is as follows:

use clapcmd::{ArgMatches, ClapCmd, ClapCmdResult, Command};

fn do_ping(cmd: &mut ClapCmd, _: ArgMatches) -> ClapCmdResult {
    cmd.output("pong");
    Ok(())
}

fn main() {
    let mut cmd = ClapCmd::default();
    cmd.add_command(
        do_ping,
        Command::new("ping").about("do a ping")
    );
    cmd.run_loop();
}

With State

To pass state or persistent information to callbacks, provide a State class like so. The State class must implement Clone trait, and can be accessed via the get_state() and set_state() methods on the ClapCmd reference passed into the callback.

use clapcmd::{ArgMatches, ClapCmd, ClapCmdResult, Command};

#[derive(Clone)]
struct State {
    counter: u32,
}

fn do_count(cmd: &mut ClapCmd<State>, _: ArgMatches) -> ClapCmdResult {
    let state = cmd.get_state().ok_or("state missing")?;
    let new_count = state.counter + 1;
    cmd.info(format!("the count is now: {}", new_count));
    cmd.set_state(State { counter: new_count });
    Ok(())
}

fn main() {
    let mut cmd = ClapCmd::with_state(State { counter: 0 });
    cmd.add_command(do_count, Command::new("count").about("increment a counter"));
    cmd.run_loop();
}

Using Groups

Groups can be used to logically separate sets of commands in the built-in help menu. They can also be used to quickly activate and deactivate commands via the add_group and remove_group methods

use clapcmd::{ArgMatches, ClapCmd, ClapCmdResult, Command, HandlerGroup};
use once_cell::sync::Lazy;

static LOADED_GROUP: Lazy<HandlerGroup> = Lazy::new(|| {
    ClapCmd::group("Fruit")
        .description("Commands to do cool fruit things")
        .command(
            do_apple,
            Command::new("apple").about("do the cool apple thing"),
        )
        .command(
            do_banana,
            Command::new("banana").about("do the cool banana thing"),
        )
        .command(
            do_unload,
            Command::new("unload").about("unload the cool fruit group"),
        )
});

static UNLOADED_GROUP: Lazy<HandlerGroup> = Lazy::new(|| {
    ClapCmd::unnamed_group().command(
        do_load,
        Command::new("load").about("load the cool fruit group"),
    )
});

fn do_load(cmd: &mut ClapCmd, _: ArgMatches) -> ClapCmdResult {
    cmd.add_group(&LOADED_GROUP);
    cmd.remove_group(&UNLOADED_GROUP);
    cmd.info("loaded");
    Ok(())
}

fn do_unload(cmd: &mut ClapCmd, _: ArgMatches) -> ClapCmdResult {
    cmd.add_group(&UNLOADED_GROUP);
    cmd.remove_group(&LOADED_GROUP);
    cmd.info("unloaded");
    Ok(())
}

fn do_apple(cmd: &mut ClapCmd, _: ArgMatches) -> ClapCmdResult {
    cmd.output("apple");
    Ok(())
}

fn do_banana(cmd: &mut ClapCmd, _: ArgMatches) -> ClapCmdResult {
    cmd.output("banana");
    Ok(())
}

fn main() {
    let mut cmd = ClapCmd::default();
    cmd.add_group(&UNLOADED_GROUP);
    cmd.run_loop();
}

E2E Testing

By enabling the test-runner feature and using the built-in output, success, info, warn, and error functions, it is easy to automate e2e tests of your CLI. See the tests/ folder for more examples.

use clapcmd::{ArgMatches, ClapCmd, ClapCmdResult, Command};

fn do_hello(cmd: &mut ClapCmd, _: ArgMatches) -> ClapCmdResult {
    cmd.output("hello");
    Ok(())
}

let mut cmd = ClapCmd::default();
cmd.add_command(
    do_hello,
    Command::new("hello").about("simple hello world")
);
let _ = cmd.one_cmd("goodbye");
#[cfg(feature = "test-runner")]
assert!(
    cmd.error.contains("unknown command"),
    "did not detect invalid command",
);
let _ = cmd.one_cmd("hello");
#[cfg(feature = "test-runner")]
assert!(
    cmd.output.contains("hello"),
    "did not run hello world command correctly",
);

Other Examples

Refer to the examples/ folder for more demonstrations of advanced use cases

MSRV

This library is tested with Rust 1.65 along with the latest version of Rust

Dependencies

~5.5MB
~93K SLoC