├── .gitignore ├── Cargo.toml ├── README.md ├── examples ├── custom_error.rs ├── custom_prompt.rs ├── initialize_repl.rs ├── no_context.rs └── with_context.rs └── src ├── command.rs ├── error.rs ├── help.rs ├── lib.rs ├── parameter.rs ├── repl.rs └── value.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "repl-rs" 3 | version = "0.2.8" 4 | authors = ["Jack Lund "] 5 | description = "Library to generate a REPL for your application" 6 | license = "MIT" 7 | repository = "https://github.com/jacklund/repl-rs" 8 | homepage = "https://github.com/jacklund/repl-rs" 9 | readme = "README.md" 10 | keywords = ["repl", "interpreter"] 11 | categories = ["command-line-interface"] 12 | edition = "2018" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | rustyline = "8.2.0" 18 | yansi = "0.5.0" 19 | regex = "1.5.4" 20 | rustyline-derive = "0.4.0" 21 | clap = { version = "4.4.1", features = ["cargo"] } 22 | 23 | [target.'cfg(unix)'.dev-dependencies] 24 | nix = "0.21.0" 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # repl-rs 2 | 3 | Library to help you create a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) for your application. 4 | 5 | Basic example code: 6 | 7 | ```rust 8 | use std::collections::HashMap; 9 | use repl_rs::{Command, Error, Parameter, Result, Value}; 10 | use repl_rs::{Convert, Repl}; 11 | 12 | // Add two numbers. 13 | fn add(args: HashMap, _context: &mut T) -> Result> { 14 | let first: i32 = args["first"].convert()?; 15 | let second: i32 = args["second"].convert()?; 16 | 17 | Ok(Some((first + second).to_string())) 18 | } 19 | 20 | // Write "Hello" 21 | fn hello(args: HashMap, _context: &mut T) -> Result> { 22 | Ok(Some(format!("Hello, {}", args["who"]))) 23 | } 24 | 25 | fn main() -> Result<()> { 26 | let mut repl = Repl::new(()) 27 | .with_name("MyApp") 28 | .with_version("v0.1.0") 29 | .with_description("My very cool app") 30 | .add_command( 31 | Command::new("add", add) 32 | .with_parameter(Parameter::new("first").set_required(true)?)? 33 | .with_parameter(Parameter::new("second").set_required(true)?)? 34 | .with_help("Add two numbers together"), 35 | ) 36 | .add_command( 37 | Command::new("hello", hello) 38 | .with_parameter(Parameter::new("who").set_required(true)?)? 39 | .with_help("Greetings!"), 40 | ); 41 | repl.run() 42 | } 43 | ``` 44 | 45 | Running the example above: 46 | 47 | ```bash 48 | % my_app 49 | Welcome to MyApp v0.1.0 50 | MyApp> help 51 | MyApp v0.1.0: My very cool app 52 | ------------------------------ 53 | add - Add two numbers together 54 | hello - Greetings! 55 | MyApp> help add 56 | add: Add two numbers together 57 | Usage: 58 | add first second 59 | MyApp> add 1 2 60 | 3 61 | MyApp> 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /examples/custom_error.rs: -------------------------------------------------------------------------------- 1 | extern crate repl_rs; 2 | 3 | use std::fmt; 4 | 5 | use repl_rs::{Command, Repl, Value}; 6 | use std::collections::HashMap; 7 | 8 | /// Example using Repl with a custom error type. 9 | #[derive(Debug)] 10 | enum CustomError { 11 | ReplError(repl_rs::Error), 12 | StringError(String), 13 | } 14 | 15 | impl From for CustomError { 16 | fn from(e: repl_rs::Error) -> Self { 17 | CustomError::ReplError(e) 18 | } 19 | } 20 | 21 | impl fmt::Display for CustomError { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | match self { 24 | CustomError::ReplError(e) => write!(f, "REPL Error: {}", e), 25 | CustomError::StringError(s) => write!(f, "String Error: {}", s), 26 | } 27 | } 28 | } 29 | 30 | impl std::error::Error for CustomError {} 31 | 32 | // Do nothing, unsuccesfully 33 | fn hello( 34 | _args: HashMap, 35 | _context: &mut T, 36 | ) -> Result, CustomError> { 37 | Err(CustomError::StringError("Returning an error".to_string())) 38 | } 39 | 40 | fn main() -> Result<(), repl_rs::Error> { 41 | let mut repl = Repl::new(()) 42 | .with_name("MyApp") 43 | .with_version("v0.1.0") 44 | .with_description("My very cool app") 45 | .add_command(Command::new("hello", hello).with_help("Do nothing, unsuccessfully")); 46 | repl.run() 47 | } 48 | -------------------------------------------------------------------------------- /examples/custom_prompt.rs: -------------------------------------------------------------------------------- 1 | extern crate repl_rs; 2 | 3 | use repl_rs::Repl; 4 | use repl_rs::{Command, Parameter, Result, Value}; 5 | use std::collections::HashMap; 6 | use std::fmt::Display; 7 | 8 | /// Example using Repl with a custom prompt 9 | struct CustomPrompt; 10 | 11 | impl Display for CustomPrompt { 12 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 13 | write!(f, "$ ") 14 | } 15 | } 16 | 17 | fn hello(args: HashMap, _context: &mut T) -> Result> { 18 | Ok(Some(format!("Hello, {}", args["who"]))) 19 | } 20 | 21 | fn main() -> Result<()> { 22 | let mut repl = Repl::new(()) 23 | .with_name("MyApp") 24 | .with_prompt(&CustomPrompt) 25 | .with_version("v0.1.0") 26 | .with_description("My very cool app") 27 | .add_command( 28 | Command::new("hello", hello) 29 | .with_parameter(Parameter::new("who").set_required(true)?)? 30 | .with_help("Greetings!"), 31 | ); 32 | repl.run() 33 | } 34 | -------------------------------------------------------------------------------- /examples/initialize_repl.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate clap; 3 | 4 | use repl_rs::{initialize_repl, Convert, Repl}; 5 | use repl_rs::{Command, Parameter, Result, Value}; 6 | use std::collections::{HashMap, VecDeque}; 7 | 8 | /// Example using initialize_repl 9 | 10 | #[derive(Default)] 11 | struct Context { 12 | list: VecDeque, 13 | } 14 | 15 | // Append name to list 16 | fn append(args: HashMap, context: &mut Context) -> Result> { 17 | let name: String = args["name"].convert()?; 18 | context.list.push_back(name); 19 | let list: Vec = context.list.clone().into(); 20 | 21 | Ok(Some(list.join(", "))) 22 | } 23 | 24 | // Prepend name to list 25 | fn prepend(args: HashMap, context: &mut Context) -> Result> { 26 | let name: String = args["name"].convert()?; 27 | context.list.push_front(name); 28 | let list: Vec = context.list.clone().into(); 29 | 30 | Ok(Some(list.join(", "))) 31 | } 32 | 33 | fn main() -> Result<()> { 34 | let mut repl = initialize_repl!(Context::default()) 35 | .use_completion(true) 36 | .add_command( 37 | Command::new("append", append) 38 | .with_parameter(Parameter::new("name").set_required(true)?)? 39 | .with_help("Append name to end of list"), 40 | ) 41 | .add_command( 42 | Command::new("prepend", prepend) 43 | .with_parameter(Parameter::new("name").set_required(true)?)? 44 | .with_help("Prepend name to front of list"), 45 | ); 46 | repl.run() 47 | } 48 | -------------------------------------------------------------------------------- /examples/no_context.rs: -------------------------------------------------------------------------------- 1 | extern crate repl_rs; 2 | 3 | use repl_rs::{Command, Parameter, Result, Value}; 4 | use repl_rs::{Convert, Repl}; 5 | use std::collections::HashMap; 6 | 7 | /// Example using Repl without Context (or, more precisely, a Context of ()) 8 | 9 | // Add two numbers. Have to make this generic to be able to pass a Context of type () 10 | fn add(args: HashMap, _context: &mut T) -> Result> { 11 | let first: i32 = args["first"].convert()?; 12 | let second: i32 = args["second"].convert()?; 13 | 14 | Ok(Some((first + second).to_string())) 15 | } 16 | 17 | // Write "Hello" 18 | fn hello(args: HashMap, _context: &mut T) -> Result> { 19 | Ok(Some(format!("Hello, {}", args["who"]))) 20 | } 21 | 22 | fn main() -> Result<()> { 23 | let mut repl = Repl::new(()) 24 | .with_name("MyApp") 25 | .with_version("v0.1.0") 26 | .with_description("My very cool app") 27 | .add_command( 28 | Command::new("add", add) 29 | .with_parameter(Parameter::new("first").set_required(true)?)? 30 | .with_parameter(Parameter::new("second").set_required(true)?)? 31 | .with_help("Add two numbers together"), 32 | ) 33 | .add_command( 34 | Command::new("hello", hello) 35 | .with_parameter(Parameter::new("who").set_required(true)?)? 36 | .with_help("Greetings!"), 37 | ); 38 | repl.run() 39 | } 40 | -------------------------------------------------------------------------------- /examples/with_context.rs: -------------------------------------------------------------------------------- 1 | extern crate repl_rs; 2 | 3 | use repl_rs::{Command, Parameter, Result, Value}; 4 | use repl_rs::{Convert, Repl}; 5 | use std::collections::{HashMap, VecDeque}; 6 | 7 | /// Example using Repl with Context 8 | 9 | #[derive(Default)] 10 | struct Context { 11 | list: VecDeque, 12 | } 13 | 14 | // Append name to list 15 | fn append(args: HashMap, context: &mut Context) -> Result> { 16 | let name: String = args["name"].convert()?; 17 | context.list.push_back(name); 18 | let list: Vec = context.list.clone().into(); 19 | 20 | Ok(Some(list.join(", "))) 21 | } 22 | 23 | // Prepend name to list 24 | fn prepend(args: HashMap, context: &mut Context) -> Result> { 25 | let name: String = args["name"].convert()?; 26 | context.list.push_front(name); 27 | let list: Vec = context.list.clone().into(); 28 | 29 | Ok(Some(list.join(", "))) 30 | } 31 | 32 | fn main() -> Result<()> { 33 | let mut repl = Repl::new(Context::default()) 34 | .with_name("MyApp") 35 | .with_version("v0.1.0") 36 | .with_description("My very cool app") 37 | .use_completion(true) 38 | .add_command( 39 | Command::new("append", append) 40 | .with_parameter(Parameter::new("name").set_required(true)?)? 41 | .with_help("Append name to end of list"), 42 | ) 43 | .add_command( 44 | Command::new("prepend", prepend) 45 | .with_parameter(Parameter::new("name").set_required(true)?)? 46 | .with_help("Prepend name to front of list"), 47 | ); 48 | repl.run() 49 | } 50 | -------------------------------------------------------------------------------- /src/command.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crate::Callback; 3 | use crate::Parameter; 4 | use std::fmt; 5 | 6 | /// Struct to define a command in the REPL 7 | pub struct Command { 8 | pub(crate) name: String, 9 | pub(crate) parameters: Vec, 10 | pub(crate) callback: Callback, 11 | pub(crate) help_summary: Option, 12 | } 13 | 14 | impl fmt::Debug for Command { 15 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 16 | f.debug_struct("Command") 17 | .field("name", &self.name) 18 | .field("parameters", &self.parameters) 19 | .field("help_summary", &self.help_summary) 20 | .finish() 21 | } 22 | } 23 | 24 | impl std::cmp::PartialEq for Command { 25 | fn eq(&self, other: &Command) -> bool { 26 | self.name == other.name 27 | && self.parameters == other.parameters 28 | && self.help_summary == other.help_summary 29 | } 30 | } 31 | 32 | impl Command { 33 | /// Create a new command with the given name and callback function 34 | pub fn new(name: &str, callback: Callback) -> Self { 35 | Self { 36 | name: name.to_string(), 37 | parameters: vec![], 38 | callback, 39 | help_summary: None, 40 | } 41 | } 42 | 43 | /// Add a parameter to the command. The order of the parameters is the same as the order in 44 | /// which this is called for each parameter. 45 | pub fn with_parameter(mut self, parameter: Parameter) -> Result> { 46 | if parameter.required && self.parameters.iter().any(|param| !param.required) { 47 | return Err(Error::IllegalRequiredError(parameter.name)); 48 | } 49 | 50 | self.parameters.push(parameter); 51 | 52 | Ok(self) 53 | } 54 | 55 | /// Add a help summary for the command 56 | pub fn with_help(mut self, help: &str) -> Command { 57 | self.help_summary = Some(help.to_string()); 58 | 59 | self 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::convert::From; 2 | use std::fmt; 3 | use std::num; 4 | 5 | /// Result type 6 | pub type Result = std::result::Result; 7 | 8 | /// Error type 9 | #[derive(Debug, PartialEq)] 10 | pub enum Error { 11 | /// Parameter is required when it shouldn't be 12 | IllegalRequiredError(String), 13 | 14 | /// Parameter is defaulted when it's also required 15 | IllegalDefaultError(String), 16 | 17 | /// A required argument is missing 18 | MissingRequiredArgument(String, String), 19 | 20 | /// Too many arguments were provided 21 | TooManyArguments(String, usize), 22 | 23 | /// Error parsing a bool value 24 | ParseBoolError(std::str::ParseBoolError), 25 | 26 | /// Error parsing an int value 27 | ParseIntError(num::ParseIntError), 28 | 29 | /// Error parsing a float value 30 | ParseFloatError(num::ParseFloatError), 31 | 32 | /// Command not found 33 | UnknownCommand(String), 34 | } 35 | 36 | impl std::error::Error for Error {} 37 | 38 | impl fmt::Display for Error { 39 | fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), fmt::Error> { 40 | match self { 41 | Error::IllegalDefaultError(parameter) => { 42 | write!(f, "Error: Parameter '{}' cannot have a default", parameter) 43 | } 44 | Error::IllegalRequiredError(parameter) => { 45 | write!(f, "Error: Parameter '{}' cannot be required", parameter) 46 | } 47 | Error::MissingRequiredArgument(command, parameter) => write!( 48 | f, 49 | "Error: Missing required argument '{}' for command '{}'", 50 | parameter, command 51 | ), 52 | Error::TooManyArguments(command, nargs) => write!( 53 | f, 54 | "Error: Command '{}' can have no more than {} arguments", 55 | command, nargs, 56 | ), 57 | Error::ParseBoolError(error) => write!(f, "Error: {}", error,), 58 | Error::ParseFloatError(error) => write!(f, "Error: {}", error,), 59 | Error::ParseIntError(error) => write!(f, "Error: {}", error,), 60 | Error::UnknownCommand(command) => write!(f, "Error: Unknown command '{}'", command), 61 | } 62 | } 63 | } 64 | 65 | impl From for Error { 66 | fn from(error: num::ParseIntError) -> Self { 67 | Error::ParseIntError(error) 68 | } 69 | } 70 | 71 | impl From for Error { 72 | fn from(error: num::ParseFloatError) -> Self { 73 | Error::ParseFloatError(error) 74 | } 75 | } 76 | 77 | impl From for Error { 78 | fn from(error: std::str::ParseBoolError) -> Self { 79 | Error::ParseBoolError(error) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/help.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crate::Parameter; 3 | use yansi::Paint; 4 | 5 | /// Help entry which gets sent to [HelpViewer](trait.HelpViewer.html) when help for a particular 6 | /// command is requested 7 | #[derive(Debug)] 8 | pub struct HelpEntry { 9 | /// Command from `help ` 10 | pub command: String, 11 | 12 | /// Parameters defined for the command 13 | pub parameters: Vec<(String, bool)>, 14 | 15 | /// Help summary for the command 16 | pub summary: Option, 17 | } 18 | 19 | impl HelpEntry { 20 | pub(crate) fn new( 21 | command_name: &str, 22 | parameters: &[Parameter], 23 | summary: &Option, 24 | ) -> Self { 25 | Self { 26 | command: command_name.to_string(), 27 | parameters: parameters 28 | .iter() 29 | .map(|pd| (pd.name.clone(), pd.required)) 30 | .collect(), 31 | summary: summary.clone(), 32 | } 33 | } 34 | } 35 | 36 | /// Struct which gets sent to [HelpViewer](trait.HelpViewer.html) when `help` command is called 37 | pub struct HelpContext { 38 | /// Application name 39 | pub app_name: String, 40 | 41 | /// Application version 42 | pub app_version: String, 43 | 44 | /// Application purpose/description 45 | pub app_purpose: String, 46 | 47 | /// List of help entries 48 | pub help_entries: Vec, 49 | } 50 | 51 | impl HelpContext { 52 | pub(crate) fn new( 53 | app_name: &str, 54 | app_version: &str, 55 | app_purpose: &str, 56 | help_entries: Vec, 57 | ) -> Self { 58 | Self { 59 | app_name: app_name.into(), 60 | app_version: app_version.into(), 61 | app_purpose: app_purpose.into(), 62 | help_entries, 63 | } 64 | } 65 | } 66 | 67 | /// Trait to be used if you want your own custom Help output 68 | pub trait HelpViewer { 69 | /// Called when the plain `help` command is called with no arguments 70 | fn help_general(&self, context: &HelpContext) -> Result<()>; 71 | 72 | /// Called when the `help` command is called with a command argument (i.e., `help foo`). 73 | /// Note that you won't have to handle an unknown command - it'll be handled in the caller 74 | fn help_command(&self, entry: &HelpEntry) -> Result<()>; 75 | } 76 | 77 | /// Default [HelpViewer](trait.HelpViewer.html) 78 | pub struct DefaultHelpViewer; 79 | 80 | impl DefaultHelpViewer { 81 | pub fn new() -> Self { 82 | Self 83 | } 84 | } 85 | 86 | impl HelpViewer for DefaultHelpViewer { 87 | fn help_general(&self, context: &HelpContext) -> Result<()> { 88 | self.print_help_header(context); 89 | for entry in &context.help_entries { 90 | print!("{}", entry.command); 91 | if entry.summary.is_some() { 92 | print!(" - {}", entry.summary.as_ref().unwrap()); 93 | } 94 | println!(); 95 | } 96 | 97 | Ok(()) 98 | } 99 | 100 | fn help_command(&self, entry: &HelpEntry) -> Result<()> { 101 | if entry.summary.is_some() { 102 | println!("{}: {}", entry.command, entry.summary.as_ref().unwrap()); 103 | } else { 104 | println!("{}:", entry.command); 105 | } 106 | println!("Usage:"); 107 | print!("\t{}", entry.command); 108 | for param in &entry.parameters { 109 | if param.1 { 110 | print!(" {}", param.0); 111 | } else { 112 | print!(" [{}]", param.0); 113 | } 114 | } 115 | println!(); 116 | 117 | Ok(()) 118 | } 119 | } 120 | 121 | impl DefaultHelpViewer { 122 | fn print_help_header(&self, context: &HelpContext) { 123 | let header = format!( 124 | "{} {}: {}", 125 | context.app_name, context.app_version, context.app_purpose 126 | ); 127 | let underline = Paint::new(" ".repeat(header.len())).strikethrough(); 128 | println!("{}", header); 129 | println!("{}", underline); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! repl-rs - [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) library 2 | //! for Rust 3 | //! 4 | //! # Example 5 | //! 6 | //! ``` 7 | //! use std::collections::HashMap; 8 | //! use repl_rs::{Command, Parameter, Result, Value}; 9 | //! use repl_rs::{Convert, Repl}; 10 | //! 11 | //! // Write "Hello" 12 | //! fn hello(args: HashMap, _context: &mut T) -> Result> { 13 | //! Ok(Some(format!("Hello, {}", args["who"]))) 14 | //! } 15 | //! 16 | //! fn main() -> Result<()> { 17 | //! let mut repl = Repl::new(()) 18 | //! .with_name("MyApp") 19 | //! .with_version("v0.1.0") 20 | //! .with_description("My very cool app") 21 | //! .add_command( 22 | //! Command::new("hello", hello) 23 | //! .with_parameter(Parameter::new("who").set_required(true)?)? 24 | //! .with_help("Greetings!"), 25 | //! ); 26 | //! repl.run() 27 | //! } 28 | //! ``` 29 | //! repl-rs uses the [builder](https://en.wikipedia.org/wiki/Builder_pattern) pattern extensively. 30 | //! What these lines are doing is: 31 | //! - creating a repl with an empty Context (see below) 32 | //! - with a name of "MyApp", the given version, and the given description 33 | //! - and adding a "hello" command which calls out to the `hello` callback function defined above 34 | //! - the `hello` command has a single parameter, "who", which is required, and has the given help 35 | //! message 36 | //! 37 | //! The `hello` function takes a HashMap of named arguments, contained in a 38 | //! [Value](struct.Value.html) struct, and an (unused) `Context`, which is used to hold state if you 39 | //! need to - the initial context is passed in to the call to 40 | //! [Repl::new](struct.Repl.html#method.new), in our case, `()`. 41 | //! Because we're not using a Context, we need to include a generic type in our `hello` function, 42 | //! because there's no way to pass an argument of type `()` otherwise. 43 | //! 44 | //! All command function callbacks return a `Result>`. This has the following 45 | //! effect: 46 | //! - If the return is `Ok(Some(String))`, it prints the string to stdout 47 | //! - If the return is `Ok(None)`, it prints nothing 48 | //! - If the return is an error, it prints the error message to stderr 49 | //! 50 | //! # Conversions 51 | //! 52 | //! The [Value](struct.Value.html) type has conversions defined for all the primitive types. Here's 53 | //! how that works in practice: 54 | //! ``` 55 | //! use repl_rs::{Command, Parameter, Result, Value}; 56 | //! use repl_rs::{Convert, Repl}; 57 | //! use std::collections::HashMap; 58 | //! 59 | //! // Add two numbers. 60 | //! fn add(args: HashMap, _context: &mut T) -> Result> { 61 | //! let first: i32 = args["first"].convert()?; 62 | //! let second: i32 = args["second"].convert()?; 63 | //! 64 | //! Ok(Some((first + second).to_string())) 65 | //! } 66 | //! 67 | //! fn main() -> Result<()> { 68 | //! let mut repl = Repl::new(()) 69 | //! .with_name("MyApp") 70 | //! .with_version("v0.1.0") 71 | //! .with_description("My very cool app") 72 | //! .add_command( 73 | //! Command::new("add", add) 74 | //! .with_parameter(Parameter::new("first").set_required(true)?)? 75 | //! .with_parameter(Parameter::new("second").set_required(true)?)? 76 | //! .with_help("Add two numbers together"), 77 | //! ); 78 | //! repl.run() 79 | //! } 80 | //! ``` 81 | //! This example adds two numbers. The `convert()` function manages the conversion for you. 82 | //! 83 | //! # Context 84 | //! 85 | //! The `Context` type is used to keep state between REPL calls. Here's an example: 86 | //! ``` 87 | //! use repl_rs::{Command, Parameter, Result, Value}; 88 | //! use repl_rs::{Convert, Repl}; 89 | //! use std::collections::{HashMap, VecDeque}; 90 | //! 91 | //! #[derive(Default)] 92 | //! struct Context { 93 | //! list: VecDeque, 94 | //! } 95 | //! 96 | //! // Append name to list 97 | //! fn append(args: HashMap, context: &mut Context) -> Result> { 98 | //! let name: String = args["name"].convert()?; 99 | //! context.list.push_back(name); 100 | //! let list: Vec = context.list.clone().into(); 101 | //! 102 | //! Ok(Some(list.join(", "))) 103 | //! } 104 | //! 105 | //! // Prepend name to list 106 | //! fn prepend(args: HashMap, context: &mut Context) -> Result> { 107 | //! let name: String = args["name"].convert()?; 108 | //! context.list.push_front(name); 109 | //! let list: Vec = context.list.clone().into(); 110 | //! 111 | //! Ok(Some(list.join(", "))) 112 | //! } 113 | //! 114 | //! fn main() -> Result<()> { 115 | //! let mut repl = Repl::new(Context::default()) 116 | //! .add_command( 117 | //! Command::new("append", append) 118 | //! .with_parameter(Parameter::new("name").set_required(true)?)? 119 | //! .with_help("Append name to end of list"), 120 | //! ) 121 | //! .add_command( 122 | //! Command::new("prepend", prepend) 123 | //! .with_parameter(Parameter::new("name").set_required(true)?)? 124 | //! .with_help("Prepend name to front of list"), 125 | //! ); 126 | //! repl.run() 127 | //! } 128 | //! ``` 129 | //! A few things to note: 130 | //! - you pass in the initial value for your Context struct to the call to 131 | //! [Repl::new()](struct.Repl.html#method.new) 132 | //! - the context is passed to your command callback functions as a mutable reference 133 | //! 134 | //! # The "initialize_repl" macro 135 | //! Instead of hardcoding your package name, version and description in your code, you can instead 136 | //! use those values from your `Cargo.toml` file, using the `initialize_repl` macro: 137 | //! ``` 138 | //! #[macro_use] 139 | //! extern crate clap; 140 | //! 141 | //! use repl_rs::{initialize_repl, Convert, Repl}; 142 | //! use repl_rs::{Command, Parameter, Result, Value}; 143 | //! use std::collections::{HashMap, VecDeque}; 144 | //! 145 | //! /// Example using initialize_repl 146 | //! 147 | //! #[derive(Default)] 148 | //! struct Context { 149 | //! list: VecDeque, 150 | //! } 151 | //! 152 | //! // Append name to list 153 | //! fn append(args: HashMap, context: &mut Context) -> Result> { 154 | //! let name: String = args["name"].convert()?; 155 | //! context.list.push_back(name); 156 | //! let list: Vec = context.list.clone().into(); 157 | //! 158 | //! Ok(Some(list.join(", "))) 159 | //! } 160 | //! 161 | //! // Prepend name to list 162 | //! fn prepend(args: HashMap, context: &mut Context) -> Result> { 163 | //! let name: String = args["name"].convert()?; 164 | //! context.list.push_front(name); 165 | //! let list: Vec = context.list.clone().into(); 166 | //! 167 | //! Ok(Some(list.join(", "))) 168 | //! } 169 | //! 170 | //! fn main() -> Result<()> { 171 | //! let mut repl = initialize_repl!(Context::default()) 172 | //! .use_completion(true) 173 | //! .add_command( 174 | //! Command::new("append", append) 175 | //! .with_parameter(Parameter::new("name").set_required(true)?)? 176 | //! .with_help("Append name to end of list"), 177 | //! ) 178 | //! .add_command( 179 | //! Command::new("prepend", prepend) 180 | //! .with_parameter(Parameter::new("name").set_required(true)?)? 181 | //! .with_help("Prepend name to front of list"), 182 | //! ); 183 | //! repl.run() 184 | //! } 185 | //! ``` 186 | //! Note the `#[macro_use] extern crate clap` at the top. You'll need that in order to avoid 187 | //! getting messages like `error: cannot find macro 'crate_name' in this scope`. 188 | //! 189 | //! # Help 190 | //! repl-rs has support for supplying help commands for your REPL. This is accomplished through the 191 | //! [HelpViewer](trait.HelpViewer.html), which is a trait that has a default implementation which should give you pretty 192 | //! much what you expect. 193 | //! ```bash 194 | //! % myapp 195 | //! Welcome to MyApp v0.1.0 196 | //! MyApp> help 197 | //! MyApp v0.1.0: My very cool app 198 | //! ------------------------------ 199 | //! append - Append name to end of list 200 | //! prepend - Prepend name to front of list 201 | //! MyApp> help append 202 | //! append: Append name to end of list 203 | //! Usage: 204 | //! append name 205 | //! MyApp> 206 | //! ``` 207 | //! If you want to roll your own help, just implement [HelpViewer](trait.HelpViewer.html) and add it to your REPL using the 208 | //! [.with_help_viewer()](struct.Repl.html#method.with_help_viewer) method. 209 | //! 210 | //! # Errors 211 | //! 212 | //! Your command functions don't need to return `repl_rs::Error`; you can return any error from 213 | //! them. Your error will need to implement `std::fmt::Display`, so the Repl can print the error, 214 | //! and you'll need to implement `std::convert::From` for `repl_rs::Error` to your error type. 215 | //! This makes error handling in your command functions easier, since you can just allow whatever 216 | //! errors your functions emit bubble up. 217 | //! 218 | //! ``` 219 | //! use repl_rs::{Command, Parameter, Value}; 220 | //! use repl_rs::{Convert, Repl}; 221 | //! use std::collections::HashMap; 222 | //! use std::fmt; 223 | //! use std::result::Result; 224 | //! 225 | //! // My custom error type 226 | //! #[derive(Debug)] 227 | //! enum Error { 228 | //! DivideByZeroError, 229 | //! ReplError(repl_rs::Error), 230 | //! } 231 | //! 232 | //! // Implement conversion from repl_rs::Error to my error type 233 | //! impl From for Error { 234 | //! fn from(error: repl_rs::Error) -> Self { 235 | //! Error::ReplError(error) 236 | //! } 237 | //! } 238 | //! 239 | //! // My error has to implement Display as well 240 | //! impl fmt::Display for Error { 241 | //! fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), fmt::Error> { 242 | //! match self { 243 | //! Error::DivideByZeroError => write!(f, "Whoops, divided by zero!"), 244 | //! Error::ReplError(error) => write!(f, "{}", error), 245 | //! } 246 | //! } 247 | //! } 248 | //! 249 | //! // Divide two numbers. 250 | //! fn divide(args: HashMap, _context: &mut T) -> Result, Error> { 251 | //! let numerator: f32 = args["numerator"].convert()?; 252 | //! let denominator: f32 = args["denominator"].convert()?; 253 | //! 254 | //! if denominator == 0.0 { 255 | //! return Err(Error::DivideByZeroError); 256 | //! } 257 | //! 258 | //! Ok(Some((numerator / denominator).to_string())) 259 | //! } 260 | //! 261 | //! fn main() -> Result<(), Error> { 262 | //! let mut repl = Repl::new(()) 263 | //! .with_name("MyApp") 264 | //! .with_version("v0.1.0") 265 | //! .with_description("My very cool app") 266 | //! .add_command( 267 | //! Command::new("divide", divide) 268 | //! .with_parameter(Parameter::new("numerator").set_required(true)?)? 269 | //! .with_parameter(Parameter::new("denominator").set_required(true)?)? 270 | //! .with_help("Divide two numbers"), 271 | //! ); 272 | //! Ok(repl.run()?) 273 | //! } 274 | //! ``` 275 | //! 276 | mod command; 277 | mod error; 278 | mod help; 279 | mod parameter; 280 | mod repl; 281 | mod value; 282 | 283 | pub use clap::*; 284 | pub use command::Command; 285 | pub use error::{Error, Result}; 286 | #[doc(inline)] 287 | pub use help::{HelpContext, HelpEntry, HelpViewer}; 288 | pub use parameter::Parameter; 289 | #[doc(inline)] 290 | pub use repl::Repl; 291 | #[doc(inline)] 292 | pub use value::{Convert, Value}; 293 | 294 | use std::collections::HashMap; 295 | 296 | /// Command callback function signature 297 | pub type Callback = 298 | fn(HashMap, &mut Context) -> std::result::Result, Error>; 299 | 300 | /// Initialize the name, version and description of the Repl from your crate name, version and 301 | /// description 302 | #[macro_export] 303 | macro_rules! initialize_repl { 304 | ($context: expr) => {{ 305 | let repl = Repl::new($context) 306 | .with_name(crate_name!()) 307 | .with_version(crate_version!()) 308 | .with_description(crate_description!()); 309 | 310 | repl 311 | }}; 312 | } 313 | -------------------------------------------------------------------------------- /src/parameter.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | 3 | /// Command parameter 4 | #[derive(Debug, PartialEq)] 5 | pub struct Parameter { 6 | pub(crate) name: String, 7 | pub(crate) required: bool, 8 | pub(crate) default: Option, 9 | } 10 | 11 | impl Parameter { 12 | /// Create a new command parameter with the given name 13 | pub fn new(name: &str) -> Self { 14 | Self { 15 | name: name.into(), 16 | required: false, 17 | default: None, 18 | } 19 | } 20 | 21 | /// Set whether the parameter is required, default is not required. 22 | /// Note that you cannot have a required parameter after a non-required one 23 | pub fn set_required(mut self, required: bool) -> Result { 24 | if self.default.is_some() { 25 | return Err(Error::IllegalRequiredError(self.name)); 26 | } 27 | self.required = required; 28 | 29 | Ok(self) 30 | } 31 | 32 | /// Set a default for an optional parameter. 33 | /// Note that you can't have a default for a required parameter 34 | pub fn set_default(mut self, default: &str) -> Result { 35 | if self.required { 36 | return Err(Error::IllegalDefaultError(self.name)); 37 | } 38 | self.default = Some(default.to_string()); 39 | 40 | Ok(self) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/repl.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crate::help::{DefaultHelpViewer, HelpContext, HelpEntry, HelpViewer}; 3 | use crate::Value; 4 | use crate::{Command, Parameter}; 5 | use rustyline::completion; 6 | use rustyline_derive::{Helper, Highlighter, Hinter, Validator}; 7 | use std::boxed::Box; 8 | use std::collections::HashMap; 9 | use std::fmt::Display; 10 | use yansi::Paint; 11 | 12 | type ErrorHandler = fn(error: E, repl: &Repl) -> Result<()>; 13 | 14 | fn default_error_handler( 15 | error: E, 16 | _repl: &Repl, 17 | ) -> Result<()> { 18 | eprintln!("{}", error); 19 | Ok(()) 20 | } 21 | 22 | /// Main REPL struct 23 | pub struct Repl { 24 | name: String, 25 | version: String, 26 | description: String, 27 | prompt: Box, 28 | custom_prompt: bool, 29 | commands: HashMap>, 30 | context: Context, 31 | help_context: Option, 32 | help_viewer: Box, 33 | error_handler: ErrorHandler, 34 | use_completion: bool, 35 | } 36 | 37 | impl Repl 38 | where 39 | E: Display + From, 40 | { 41 | /// Create a new Repl with the given context's initial value. 42 | pub fn new(context: Context) -> Self { 43 | let name = String::new(); 44 | 45 | Self { 46 | name: name.clone(), 47 | version: String::new(), 48 | description: String::new(), 49 | prompt: Box::new(Paint::green(format!("{}> ", name)).bold()), 50 | custom_prompt: false, 51 | commands: HashMap::new(), 52 | context, 53 | help_context: None, 54 | help_viewer: Box::new(DefaultHelpViewer::new()), 55 | error_handler: default_error_handler, 56 | use_completion: false, 57 | } 58 | } 59 | 60 | /// Give your Repl a name. This is used in the help summary for the Repl. 61 | pub fn with_name(mut self, name: &str) -> Self { 62 | self.name = name.to_string(); 63 | if !self.custom_prompt { 64 | self.prompt = Box::new(Paint::green(format!("{}> ", name)).bold()); 65 | } 66 | 67 | self 68 | } 69 | 70 | /// Give your Repl a version. This is used in the help summary for the Repl. 71 | pub fn with_version(mut self, version: &str) -> Self { 72 | self.version = version.to_string(); 73 | 74 | self 75 | } 76 | 77 | /// Give your Repl a description. This is used in the help summary for the Repl. 78 | pub fn with_description(mut self, description: &str) -> Self { 79 | self.description = description.to_string(); 80 | 81 | self 82 | } 83 | 84 | /// Give your Repl a custom prompt. The default prompt is the Repl name, followed by 85 | /// a `>`, all in green, followed by a space. 86 | pub fn with_prompt(mut self, prompt: &'static dyn Display) -> Self { 87 | self.prompt = Box::new(prompt); 88 | self.custom_prompt = true; 89 | 90 | self 91 | } 92 | 93 | /// Pass in a custom help viewer 94 | pub fn with_help_viewer(mut self, help_viewer: V) -> Self { 95 | self.help_viewer = Box::new(help_viewer); 96 | 97 | self 98 | } 99 | 100 | /// Pass in a custom error handler. This is really only for testing - the default 101 | /// error handler simply prints the error to stderr and then returns 102 | pub fn with_error_handler(mut self, handler: ErrorHandler) -> Self { 103 | self.error_handler = handler; 104 | 105 | self 106 | } 107 | 108 | /// Set whether to use command completion when tab is hit. Defaults to false. 109 | pub fn use_completion(mut self, value: bool) -> Self { 110 | self.use_completion = value; 111 | 112 | self 113 | } 114 | 115 | /// Add a command to your REPL 116 | pub fn add_command(mut self, command: Command) -> Self { 117 | self.commands.insert(command.name.clone(), command); 118 | 119 | self 120 | } 121 | 122 | fn validate_arguments( 123 | &self, 124 | command: &str, 125 | parameters: &[Parameter], 126 | args: &[&str], 127 | ) -> Result> { 128 | if args.len() > parameters.len() { 129 | return Err(Error::TooManyArguments(command.into(), parameters.len())); 130 | } 131 | 132 | let mut validated = HashMap::new(); 133 | for (index, parameter) in parameters.iter().enumerate() { 134 | if index < args.len() { 135 | validated.insert(parameter.name.clone(), Value::new(args[index])); 136 | } else if parameter.required { 137 | return Err(Error::MissingRequiredArgument( 138 | command.into(), 139 | parameter.name.clone(), 140 | )); 141 | } else if parameter.default.is_some() { 142 | validated.insert( 143 | parameter.name.clone(), 144 | Value::new(¶meter.default.clone().unwrap()), 145 | ); 146 | } 147 | } 148 | Ok(validated) 149 | } 150 | 151 | fn handle_command(&mut self, command: &str, args: &[&str]) -> core::result::Result<(), E> { 152 | match self.commands.get(command) { 153 | Some(definition) => { 154 | let validated = self.validate_arguments(command, &definition.parameters, args)?; 155 | match (definition.callback)(validated, &mut self.context) { 156 | Ok(Some(value)) => println!("{}", value), 157 | Ok(None) => (), 158 | Err(error) => return Err(error), 159 | }; 160 | } 161 | None => { 162 | if command == "help" { 163 | self.show_help(args)?; 164 | } else { 165 | return Err(Error::UnknownCommand(command.to_string()).into()); 166 | } 167 | } 168 | } 169 | 170 | Ok(()) 171 | } 172 | 173 | fn show_help(&self, args: &[&str]) -> Result<()> { 174 | if args.is_empty() { 175 | self.help_viewer 176 | .help_general(self.help_context.as_ref().unwrap())?; 177 | } else { 178 | let entry_opt = self 179 | .help_context 180 | .as_ref() 181 | .unwrap() 182 | .help_entries 183 | .iter() 184 | .find(|entry| entry.command == args[0]); 185 | match entry_opt { 186 | Some(entry) => { 187 | self.help_viewer.help_command(entry)?; 188 | } 189 | None => eprintln!("Help not found for command '{}'", args[0]), 190 | }; 191 | } 192 | Ok(()) 193 | } 194 | 195 | fn process_line(&mut self, line: String) -> core::result::Result<(), E> { 196 | let trimmed = line.trim(); 197 | if !trimmed.is_empty() { 198 | let r = regex::Regex::new(r#"("[^"\n]+"|[\S]+)"#).unwrap(); 199 | let args = r 200 | .captures_iter(trimmed) 201 | .map(|a| a[0].to_string().replace('\"', "")) 202 | .collect::>(); 203 | let mut args = args.iter().fold(vec![], |mut state, a| { 204 | state.push(a.as_str()); 205 | state 206 | }); 207 | let command: String = args.drain(..1).collect(); 208 | self.handle_command(&command, &args)?; 209 | } 210 | Ok(()) 211 | } 212 | 213 | fn construct_help_context(&mut self) { 214 | let mut help_entries = self 215 | .commands 216 | .values() 217 | .map(|definition| { 218 | HelpEntry::new( 219 | &definition.name, 220 | &definition.parameters, 221 | &definition.help_summary, 222 | ) 223 | }) 224 | .collect::>(); 225 | help_entries.sort_by_key(|d| d.command.clone()); 226 | self.help_context = Some(HelpContext::new( 227 | &self.name, 228 | &self.version, 229 | &self.description, 230 | help_entries, 231 | )); 232 | } 233 | 234 | fn create_helper(&mut self) -> Helper { 235 | let mut helper = Helper::new(); 236 | if self.use_completion { 237 | for name in self.commands.keys() { 238 | helper.add_command(name.to_string()); 239 | } 240 | } 241 | 242 | helper 243 | } 244 | 245 | pub fn run(&mut self) -> Result<()> { 246 | self.construct_help_context(); 247 | let mut editor: rustyline::Editor = rustyline::Editor::new(); 248 | let helper = Some(self.create_helper()); 249 | editor.set_helper(helper); 250 | println!("Welcome to {} {}", self.name, self.version); 251 | let mut eof = false; 252 | while !eof { 253 | self.handle_line(&mut editor, &mut eof)?; 254 | } 255 | 256 | Ok(()) 257 | } 258 | 259 | fn handle_line( 260 | &mut self, 261 | editor: &mut rustyline::Editor, 262 | eof: &mut bool, 263 | ) -> Result<()> { 264 | match editor.readline(&format!("{}", self.prompt)) { 265 | Ok(line) => { 266 | editor.add_history_entry(line.clone()); 267 | if let Err(error) = self.process_line(line) { 268 | (self.error_handler)(error, self)?; 269 | } 270 | *eof = false; 271 | Ok(()) 272 | } 273 | Err(rustyline::error::ReadlineError::Eof) => { 274 | *eof = true; 275 | Ok(()) 276 | } 277 | Err(error) => { 278 | eprintln!("Error reading line: {}", error); 279 | *eof = false; 280 | Ok(()) 281 | } 282 | } 283 | } 284 | } 285 | 286 | // rustyline Helper struct 287 | // Currently just does command completion with , if 288 | // use_completion() is set on the REPL 289 | #[derive(Clone, Helper, Hinter, Highlighter, Validator)] 290 | struct Helper { 291 | commands: Vec, 292 | } 293 | 294 | impl Helper { 295 | fn new() -> Self { 296 | Self { commands: vec![] } 297 | } 298 | 299 | fn add_command(&mut self, command: String) { 300 | self.commands.push(command); 301 | } 302 | } 303 | 304 | impl completion::Completer for Helper { 305 | type Candidate = String; 306 | 307 | fn complete( 308 | &self, 309 | line: &str, 310 | _pos: usize, 311 | _ctx: &rustyline::Context<'_>, 312 | ) -> rustyline::Result<(usize, Vec)> { 313 | // Complete based on whether the current line is a substring 314 | // of one of the set commands 315 | let ret: Vec = self 316 | .commands 317 | .iter() 318 | .filter(|cmd| cmd.contains(line)) 319 | .map(|s| s.to_string()) 320 | .collect(); 321 | Ok((0, ret)) 322 | } 323 | } 324 | 325 | #[cfg(all(test, unix))] 326 | mod tests { 327 | use crate::error::*; 328 | use crate::repl::{Helper, Repl}; 329 | use crate::{initialize_repl, Value}; 330 | use crate::{Command, Parameter}; 331 | use clap::{crate_description, crate_name, crate_version}; 332 | use nix::sys::wait::{waitpid, WaitStatus}; 333 | use nix::unistd::{close, dup2, fork, pipe, ForkResult}; 334 | use std::collections::HashMap; 335 | use std::fs::File; 336 | use std::io::Write; 337 | use std::os::unix::io::FromRawFd; 338 | 339 | fn test_error_handler(error: Error, _repl: &Repl) -> Result<()> { 340 | Err(error) 341 | } 342 | 343 | fn foo(args: HashMap, _context: &mut T) -> Result> { 344 | Ok(Some(format!("foo {:?}", args))) 345 | } 346 | 347 | fn run_repl(mut repl: Repl, input: &str, expected: Result<()>) { 348 | let (rdr, wrtr) = pipe().unwrap(); 349 | unsafe { 350 | match fork() { 351 | Ok(ForkResult::Parent { child, .. }) => { 352 | // Parent 353 | let mut f = File::from_raw_fd(wrtr); 354 | write!(f, "{}", input).unwrap(); 355 | if let WaitStatus::Exited(_, exit_code) = waitpid(child, None).unwrap() { 356 | assert!(exit_code == 0); 357 | }; 358 | } 359 | Ok(ForkResult::Child) => { 360 | std::panic::set_hook(Box::new(|panic_info| { 361 | println!("Caught panic: {:?}", panic_info); 362 | if let Some(location) = panic_info.location() { 363 | println!( 364 | "panic occurred in file '{}' at line {}", 365 | location.file(), 366 | location.line(), 367 | ); 368 | } else { 369 | println!("panic occurred but can't get location information..."); 370 | } 371 | })); 372 | 373 | dup2(rdr, 0).unwrap(); 374 | close(rdr).unwrap(); 375 | let mut editor: rustyline::Editor = rustyline::Editor::new(); 376 | let mut eof = false; 377 | let result = repl.handle_line(&mut editor, &mut eof); 378 | let _ = std::panic::take_hook(); 379 | if expected == result { 380 | std::process::exit(0); 381 | } else { 382 | eprintln!("Expected {:?}, got {:?}", expected, result); 383 | std::process::exit(1); 384 | } 385 | } 386 | Err(_) => println!("Fork failed"), 387 | } 388 | } 389 | } 390 | 391 | #[test] 392 | fn test_initialize_sets_crate_values() -> Result<()> { 393 | let repl: Repl<(), Error> = initialize_repl!(()); 394 | 395 | assert_eq!(crate_name!(), repl.name); 396 | assert_eq!(crate_version!(), repl.version); 397 | assert_eq!(crate_description!(), repl.description); 398 | 399 | Ok(()) 400 | } 401 | 402 | #[test] 403 | fn test_empty_line_does_nothing() -> Result<()> { 404 | let repl = Repl::new(()) 405 | .with_name("test") 406 | .with_version("v0.1.0") 407 | .with_description("Testing 1, 2, 3...") 408 | .with_error_handler(test_error_handler) 409 | .add_command( 410 | Command::new("foo", foo) 411 | .with_parameter(Parameter::new("bar").set_required(true)?)? 412 | .with_parameter(Parameter::new("baz").set_required(true)?)? 413 | .with_help("Do foo when you can"), 414 | ); 415 | run_repl(repl, "\n", Ok(())); 416 | 417 | Ok(()) 418 | } 419 | 420 | #[test] 421 | fn test_missing_required_arg_fails() -> Result<()> { 422 | let repl = Repl::new(()) 423 | .with_name("test") 424 | .with_version("v0.1.0") 425 | .with_description("Testing 1, 2, 3...") 426 | .with_error_handler(test_error_handler) 427 | .add_command( 428 | Command::new("foo", foo) 429 | .with_parameter(Parameter::new("bar").set_required(true)?)? 430 | .with_parameter(Parameter::new("baz").set_required(true)?)? 431 | .with_help("Do foo when you can"), 432 | ); 433 | run_repl( 434 | repl, 435 | "foo bar\n", 436 | Err(Error::MissingRequiredArgument("foo".into(), "baz".into())), 437 | ); 438 | 439 | Ok(()) 440 | } 441 | 442 | #[test] 443 | fn test_unknown_command_fails() -> Result<()> { 444 | let repl = Repl::new(()) 445 | .with_name("test") 446 | .with_version("v0.1.0") 447 | .with_description("Testing 1, 2, 3...") 448 | .with_error_handler(test_error_handler) 449 | .add_command( 450 | Command::new("foo", foo) 451 | .with_parameter(Parameter::new("bar").set_required(true)?)? 452 | .with_parameter(Parameter::new("baz").set_required(true)?)? 453 | .with_help("Do foo when you can"), 454 | ); 455 | run_repl( 456 | repl, 457 | "bar baz\n", 458 | Err(Error::UnknownCommand("bar".to_string())), 459 | ); 460 | 461 | Ok(()) 462 | } 463 | 464 | #[test] 465 | fn test_no_required_after_optional() -> Result<()> { 466 | assert_eq!( 467 | Err(Error::IllegalRequiredError("bar".into())), 468 | Command::<(), Error>::new("foo", foo) 469 | .with_parameter(Parameter::new("baz").set_default("20")?)? 470 | .with_parameter(Parameter::new("bar").set_required(true)?) 471 | ); 472 | 473 | Ok(()) 474 | } 475 | 476 | #[test] 477 | fn test_required_cannot_be_defaulted() -> Result<()> { 478 | assert_eq!( 479 | Err(Error::IllegalDefaultError("bar".into())), 480 | Parameter::new("bar").set_required(true)?.set_default("foo") 481 | ); 482 | 483 | Ok(()) 484 | } 485 | 486 | #[test] 487 | fn test_string_with_spaces_for_argument() -> Result<()> { 488 | let repl = Repl::new(()) 489 | .with_name("test") 490 | .with_version("v0.1.0") 491 | .with_description("Testing 1, 2, 3...") 492 | .with_error_handler(test_error_handler) 493 | .add_command( 494 | Command::new("foo", foo) 495 | .with_parameter(Parameter::new("bar").set_required(true)?)? 496 | .with_parameter(Parameter::new("baz").set_required(true)?)? 497 | .with_help("Do foo when you can"), 498 | ); 499 | run_repl(repl, "foo \"baz test 123\" foo\n", Ok(())); 500 | 501 | Ok(()) 502 | } 503 | 504 | #[test] 505 | fn test_string_with_spaces_for_argument_last() -> Result<()> { 506 | let repl = Repl::new(()) 507 | .with_name("test") 508 | .with_version("v0.1.0") 509 | .with_description("Testing 1, 2, 3...") 510 | .with_error_handler(test_error_handler) 511 | .add_command( 512 | Command::new("foo", foo) 513 | .with_parameter(Parameter::new("bar").set_required(true)?)? 514 | .with_parameter(Parameter::new("baz").set_required(true)?)? 515 | .with_help("Do foo when you can"), 516 | ); 517 | run_repl(repl, "foo foo \"baz test 123\"\n", Ok(())); 518 | 519 | Ok(()) 520 | } 521 | } 522 | -------------------------------------------------------------------------------- /src/value.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use std::fmt; 3 | 4 | /// Value type. Has conversions to every primitive type. 5 | #[derive(Clone, Debug)] 6 | pub struct Value { 7 | value: String, 8 | } 9 | 10 | impl fmt::Display for Value { 11 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 12 | write!(f, "{}", self.value) 13 | } 14 | } 15 | 16 | /// Trait to convert from a [Value](struct.Value.html) to some other type. 17 | pub trait Convert { 18 | fn convert(&self) -> Result; 19 | } 20 | 21 | impl Value { 22 | pub(crate) fn new(value: &str) -> Self { 23 | Self { 24 | value: value.to_string(), 25 | } 26 | } 27 | } 28 | 29 | impl Convert for Value { 30 | fn convert(&self) -> Result { 31 | Ok(self.value.to_string()) 32 | } 33 | } 34 | 35 | macro_rules! add_num_converter { 36 | ($type: ident) => { 37 | impl Convert<$type> for Value { 38 | fn convert(&self) -> Result<$type> { 39 | Ok(self.value.parse::<$type>()?) 40 | } 41 | } 42 | }; 43 | } 44 | 45 | add_num_converter!(i8); 46 | add_num_converter!(i16); 47 | add_num_converter!(i32); 48 | add_num_converter!(i64); 49 | add_num_converter!(i128); 50 | add_num_converter!(isize); 51 | add_num_converter!(u8); 52 | add_num_converter!(u16); 53 | add_num_converter!(u32); 54 | add_num_converter!(u64); 55 | add_num_converter!(u128); 56 | add_num_converter!(usize); 57 | add_num_converter!(f32); 58 | add_num_converter!(f64); 59 | add_num_converter!(bool); 60 | --------------------------------------------------------------------------------