Crate clapcmd

source ·
Expand description

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

Re-exports

Modules

Structs

  • The abstract representation of a command line argument. Used to set all the options and relationships that define a valid argument for the program.
  • Container for parse results.
  • An interactive CLI interface, holding state and responsible for acquiring user input and assigning tasks to callback functions.
  • The ClapCmdBuilder is mostly a thin wrapper around the rustyline builder that allows you to specify how to interact with the readline interface. The main difference is that the default options for the ClapCmdBuilder includes auto_add_history
  • Build a command-line interface.

Enums

  • Provide shell with hint on how to complete an argument.

Traits

Type Definitions