Expand description
repl-rs - REPL library for Rust
Example
use std::collections::HashMap;
use repl_rs::{Command, Parameter, Result, Value};
use repl_rs::{Convert, Repl};
// Write "Hello"
fn hello<T>(args: HashMap<String, Value>, _context: &mut T) -> Result<Option<String>> {
Ok(Some(format!("Hello, {}", args["who"])))
}
fn main() -> Result<()> {
let mut repl = Repl::new(())
.with_name("MyApp")
.with_version("v0.1.0")
.with_description("My very cool app")
.add_command(
Command::new("hello", hello)
.with_parameter(Parameter::new("who").set_required(true)?)?
.with_help("Greetings!"),
);
repl.run()
}
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 HashMap of named arguments, contained in a
Value struct, 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
Conversions
The Value type has conversions defined for all the primitive types. Here’s how that works in practice:
use repl_rs::{Command, Parameter, Result, Value};
use repl_rs::{Convert, Repl};
use std::collections::HashMap;
// Add two numbers.
fn add<T>(args: HashMap<String, Value>, _context: &mut T) -> Result<Option<String>> {
let first: i32 = args["first"].convert()?;
let second: i32 = args["second"].convert()?;
Ok(Some((first + second).to_string()))
}
fn main() -> Result<()> {
let mut repl = Repl::new(())
.with_name("MyApp")
.with_version("v0.1.0")
.with_description("My very cool app")
.add_command(
Command::new("add", add)
.with_parameter(Parameter::new("first").set_required(true)?)?
.with_parameter(Parameter::new("second").set_required(true)?)?
.with_help("Add two numbers together"),
);
repl.run()
}
This example adds two numbers. The convert()
function manages the conversion for you.
Context
The Context
type is used to keep state between REPL calls. Here’s an example:
use repl_rs::{Command, Parameter, Result, Value};
use repl_rs::{Convert, Repl};
use std::collections::{HashMap, VecDeque};
#[derive(Default)]
struct Context {
list: VecDeque<String>,
}
// Append name to list
fn append(args: HashMap<String, Value>, context: &mut Context) -> Result<Option<String>> {
let name: String = args["name"].convert()?;
context.list.push_back(name);
let list: Vec<String> = context.list.clone().into();
Ok(Some(list.join(", ")))
}
// Prepend name to list
fn prepend(args: HashMap<String, Value>, context: &mut Context) -> Result<Option<String>> {
let name: String = args["name"].convert()?;
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())
.add_command(
Command::new("append", append)
.with_parameter(Parameter::new("name").set_required(true)?)?
.with_help("Append name to end of list"),
)
.add_command(
Command::new("prepend", prepend)
.with_parameter(Parameter::new("name").set_required(true)?)?
.with_help("Prepend name to front of list"),
);
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 “initialize_repl” macro
Instead of hardcoding your package name, version and description in your code, you can instead
use those values from your Cargo.toml
file, using the initialize_repl
macro:
#[macro_use]
extern crate clap;
use repl_rs::{initialize_repl, Convert, Repl};
use repl_rs::{Command, Parameter, Result, Value};
use std::collections::{HashMap, VecDeque};
/// Example using initialize_repl
#[derive(Default)]
struct Context {
list: VecDeque<String>,
}
// Append name to list
fn append(args: HashMap<String, Value>, context: &mut Context) -> Result<Option<String>> {
let name: String = args["name"].convert()?;
context.list.push_back(name);
let list: Vec<String> = context.list.clone().into();
Ok(Some(list.join(", ")))
}
// Prepend name to list
fn prepend(args: HashMap<String, Value>, context: &mut Context) -> Result<Option<String>> {
let name: String = args["name"].convert()?;
context.list.push_front(name);
let list: Vec<String> = context.list.clone().into();
Ok(Some(list.join(", ")))
}
fn main() -> Result<()> {
let mut repl = initialize_repl!(Context::default())
.use_completion(true)
.add_command(
Command::new("append", append)
.with_parameter(Parameter::new("name").set_required(true)?)?
.with_help("Append name to end of list"),
)
.add_command(
Command::new("prepend", prepend)
.with_parameter(Parameter::new("name").set_required(true)?)?
.with_help("Prepend name to front of list"),
);
repl.run()
}
Note the #[macro_use] extern crate clap
at the top. You’ll need that in order to avoid
getting messages like error: cannot find macro 'crate_name' in this scope
.
Help
repl-rs has support for supplying help commands for your REPL. This is accomplished through the HelpViewer, which is a trait that has a default implementation which should give you pretty much what you expect.
% myapp
Welcome to MyApp v0.1.0
MyApp> help
MyApp v0.1.0: My very cool app
------------------------------
append - Append name to end of list
prepend - Prepend name to front of list
MyApp> help append
append: Append name to end of list
Usage:
append name
MyApp>
If you want to roll your own help, just implement HelpViewer and add it to your REPL using the .with_help_viewer() method.
Errors
Your command functions don’t need to return 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 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.
use repl_rs::{Command, Parameter, Value};
use repl_rs::{Convert, Repl};
use std::collections::HashMap;
use std::fmt;
use std::result::Result;
// My custom error type
#[derive(Debug)]
enum Error {
DivideByZeroError,
ReplError(repl_rs::Error),
}
// Implement conversion from repl_rs::Error to my error type
impl From<repl_rs::Error> for Error {
fn from(error: repl_rs::Error) -> Self {
Error::ReplError(error)
}
}
// My error has to implement Display as well
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), fmt::Error> {
match self {
Error::DivideByZeroError => write!(f, "Whoops, divided by zero!"),
Error::ReplError(error) => write!(f, "{}", error),
}
}
}
// Divide two numbers.
fn divide<T>(args: HashMap<String, Value>, _context: &mut T) -> Result<Option<String>, Error> {
let numerator: f32 = args["numerator"].convert()?;
let denominator: f32 = args["denominator"].convert()?;
if denominator == 0.0 {
return Err(Error::DivideByZeroError);
}
Ok(Some((numerator / denominator).to_string()))
}
fn main() -> Result<(), Error> {
let mut repl = Repl::new(())
.with_name("MyApp")
.with_version("v0.1.0")
.with_description("My very cool app")
.add_command(
Command::new("divide", divide)
.with_parameter(Parameter::new("numerator").set_required(true)?)?
.with_parameter(Parameter::new("denominator").set_required(true)?)?
.with_help("Divide two numbers"),
);
Ok(repl.run()?)
}
Modules
Command
line argument parser
Macros
- Create an
Arg
from a usage string. - Allows you to build the
Command
instance from your Cargo.toml at compile time. - Allows you to pull the authors for the command from your Cargo.toml at compile time in the form:
"author1 lastname <author1@example.com>:author2 lastname <author2@example.com>"
- Allows you to pull the description from your Cargo.toml at compile time.
- Allows you to pull the name from your Cargo.toml at compile time.
- Allows you to pull the version from your Cargo.toml at compile time as
MAJOR.MINOR.PATCH_PKGVERSION_PRE
- Initialize the name, version and description of the Repl from your crate name, version and description
- Select a
ValueParser
implementation from the intended type
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.
- Family of related arguments.
- Container for parse results.
- Struct to define a command in the REPL
- Struct which gets sent to HelpViewer when
help
command is called - Help entry which gets sent to HelpViewer when help for a particular command is requested
- Command parameter
- Main REPL struct
- Value type. Has conversions to every primitive type.
Enums
- Behavior of arguments when they are encountered while parsing
- Represents the color preferences for program output
- Error type
- Provide shell with hint on how to complete an argument.
Traits
- Parse a set of arguments into a user-defined container.
- Create a
Command
relevant for a user-defined container. - Trait to convert from a Value to some other type.
- Converts an instance of
ArgMatches
to a user-defined container. - Trait to be used if you want your own custom Help output
- Parse command-line arguments into
Self
. - Parse a sub-command into a user-defined enum.
- Parse arguments into enums.
Type Aliases
- Command callback function signature
- Result type