aboutsummaryrefslogtreecommitdiff
path: root/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs220
1 files changed, 192 insertions, 28 deletions
diff --git a/src/lib.rs b/src/lib.rs
index 984d927..c507be0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -31,9 +31,7 @@
//! pilot_nickname: Option<String>,
//! }
//!
-//! fn main() {
-//! let up: GoUp = argh::from_env();
-//! }
+//! let up: GoUp = argh::from_env();
//! ```
//!
//! `./some_bin --help` will then output the following:
@@ -129,6 +127,34 @@
//! The last positional argument may include a default, or be wrapped in
//! `Option` or `Vec` to indicate an optional or repeating positional argument.
//!
+//! If your final positional argument has the `greedy` option on it, it will consume
+//! any arguments after it as if a `--` were placed before the first argument to
+//! match the greedy positional:
+//!
+//! ```rust
+//! use argh::FromArgs;
+//! #[derive(FromArgs, PartialEq, Debug)]
+//! /// A command with a greedy positional argument at the end.
+//! struct WithGreedyPositional {
+//! /// some stuff
+//! #[argh(option)]
+//! stuff: Option<String>,
+//! #[argh(positional, greedy)]
+//! all_the_rest: Vec<String>,
+//! }
+//! ```
+//!
+//! Now if you pass `--stuff Something` after a positional argument, it will
+//! be consumed by `all_the_rest` instead of setting the `stuff` field.
+//!
+//! Note that `all_the_rest` won't be listed as a positional argument in the
+//! long text part of help output (and it will be listed at the end of the usage
+//! line as `[all_the_rest...]`), and it's up to the caller to append any
+//! extra help output for the meaning of the captured arguments. This is to
+//! enable situations where some amount of argument processing needs to happen
+//! before the rest of the arguments can be interpreted, and shouldn't be used
+//! for regular use as it might be confusing.
+//!
//! Subcommands are also supported. To use a subcommand, declare a separate
//! `FromArgs` type for each subcommand as well as an enum that cases
//! over each command:
@@ -168,6 +194,102 @@
//! fooey: bool,
//! }
//! ```
+//!
+//! You can also discover subcommands dynamically at runtime. To do this,
+//! declare subcommands as usual and add a variant to the enum with the
+//! `dynamic` attribute. Instead of deriving `FromArgs`, the value inside the
+//! dynamic variant should implement `DynamicSubCommand`.
+//!
+//! ```rust
+//! # use argh::CommandInfo;
+//! # use argh::DynamicSubCommand;
+//! # use argh::EarlyExit;
+//! # use argh::FromArgs;
+//! # use once_cell::sync::OnceCell;
+//!
+//! #[derive(FromArgs, PartialEq, Debug)]
+//! /// Top-level command.
+//! struct TopLevel {
+//! #[argh(subcommand)]
+//! nested: MySubCommandEnum,
+//! }
+//!
+//! #[derive(FromArgs, PartialEq, Debug)]
+//! #[argh(subcommand)]
+//! enum MySubCommandEnum {
+//! Normal(NormalSubCommand),
+//! #[argh(dynamic)]
+//! Dynamic(Dynamic),
+//! }
+//!
+//! #[derive(FromArgs, PartialEq, Debug)]
+//! /// Normal subcommand.
+//! #[argh(subcommand, name = "normal")]
+//! struct NormalSubCommand {
+//! #[argh(option)]
+//! /// how many x
+//! x: usize,
+//! }
+//!
+//! /// Dynamic subcommand.
+//! #[derive(PartialEq, Debug)]
+//! struct Dynamic {
+//! name: String
+//! }
+//!
+//! impl DynamicSubCommand for Dynamic {
+//! fn commands() -> &'static [&'static CommandInfo] {
+//! static RET: OnceCell<Vec<&'static CommandInfo>> = OnceCell::new();
+//! RET.get_or_init(|| {
+//! let mut commands = Vec::new();
+//!
+//! // argh needs the `CommandInfo` structs we generate to be valid
+//! // for the static lifetime. We can allocate the structures on
+//! // the heap with `Box::new` and use `Box::leak` to get a static
+//! // reference to them. We could also just use a constant
+//! // reference, but only because this is a synthetic example; the
+//! // point of using dynamic commands is to have commands you
+//! // don't know about until runtime!
+//! commands.push(&*Box::leak(Box::new(CommandInfo {
+//! name: "dynamic_command",
+//! description: "A dynamic command",
+//! })));
+//!
+//! commands
+//! })
+//! }
+//!
+//! fn try_redact_arg_values(
+//! command_name: &[&str],
+//! args: &[&str],
+//! ) -> Option<Result<Vec<String>, EarlyExit>> {
+//! for command in Self::commands() {
+//! if command_name.last() == Some(&command.name) {
+//! // Process arguments and redact values here.
+//! if !args.is_empty() {
+//! return Some(Err("Our example dynamic command never takes arguments!"
+//! .to_string().into()));
+//! }
+//! return Some(Ok(Vec::new()))
+//! }
+//! }
+//! None
+//! }
+//!
+//! fn try_from_args(command_name: &[&str], args: &[&str]) -> Option<Result<Self, EarlyExit>> {
+//! for command in Self::commands() {
+//! if command_name.last() == Some(&command.name) {
+//! if !args.is_empty() {
+//! return Some(Err("Our example dynamic command never takes arguments!"
+//! .to_string().into()));
+//! }
+//! return Some(Ok(Dynamic { name: command.name.to_string() }))
+//! }
+//! }
+//! None
+//! }
+//! }
+//! ```
#![deny(missing_docs)]
@@ -445,6 +567,11 @@ pub trait TopLevelCommand: FromArgs {}
pub trait SubCommands: FromArgs {
/// Info for the commands.
const COMMANDS: &'static [&'static CommandInfo];
+
+ /// Get a list of commands that are discovered at runtime.
+ fn dynamic_commands() -> &'static [&'static CommandInfo] {
+ &[]
+ }
}
/// A `FromArgs` implementation that represents a single subcommand.
@@ -457,6 +584,34 @@ impl<T: SubCommand> SubCommands for T {
const COMMANDS: &'static [&'static CommandInfo] = &[T::COMMAND];
}
+/// Trait implemented by values returned from a dynamic subcommand handler.
+pub trait DynamicSubCommand: Sized {
+ /// Info about supported subcommands.
+ fn commands() -> &'static [&'static CommandInfo];
+
+ /// Perform the function of `FromArgs::redact_arg_values` for this dynamic
+ /// command.
+ ///
+ /// The full list of subcommands, ending with the subcommand that should be
+ /// dynamically recognized, is passed in `command_name`. If the command
+ /// passed is not recognized, this function should return `None`. Otherwise
+ /// it should return `Some`, and the value within the `Some` has the same
+ /// semantics as the return of `FromArgs::redact_arg_values`.
+ fn try_redact_arg_values(
+ command_name: &[&str],
+ args: &[&str],
+ ) -> Option<Result<Vec<String>, EarlyExit>>;
+
+ /// Perform the function of `FromArgs::from_args` for this dynamic command.
+ ///
+ /// The full list of subcommands, ending with the subcommand that should be
+ /// dynamically recognized, is passed in `command_name`. If the command
+ /// passed is not recognized, this function should return `None`. Otherwise
+ /// it should return `Some`, and the value within the `Some` has the same
+ /// semantics as the return of `FromArgs::from_args`.
+ fn try_from_args(command_name: &[&str], args: &[&str]) -> Option<Result<Self, EarlyExit>>;
+}
+
/// Information to display to the user about why a `FromArgs` construction exited early.
///
/// This can occur due to either failed parsing or a flag like `--help`.
@@ -481,8 +636,8 @@ impl From<String> for EarlyExit {
}
/// Extract the base cmd from a path
-fn cmd<'a>(default: &'a String, path: &'a String) -> &'a str {
- std::path::Path::new(path).file_name().map(|s| s.to_str()).flatten().unwrap_or(default.as_str())
+fn cmd<'a>(default: &'a str, path: &'a str) -> &'a str {
+ std::path::Path::new(path).file_name().and_then(|s| s.to_str()).unwrap_or(default)
}
/// Create a `FromArgs` type from the current process's `env::args`.
@@ -501,7 +656,7 @@ pub fn from_env<T: TopLevelCommand>() -> T {
0
}
Err(()) => {
- eprintln!("{}", early_exit.output);
+ eprintln!("{}\nRun {} --help for more information.", early_exit.output, cmd);
1
}
})
@@ -527,7 +682,7 @@ pub fn cargo_from_env<T: TopLevelCommand>() -> T {
0
}
Err(()) => {
- eprintln!("{}", early_exit.output);
+ eprintln!("{}\nRun --help for more information.", early_exit.output);
1
}
})
@@ -683,14 +838,14 @@ pub fn parse_struct_args(
let mut positional_index = 0;
let mut options_ended = false;
- 'parse_args: while let Some(&next_arg) = remaining_args.get(0) {
+ 'parse_args: while let Some(&next_arg) = remaining_args.first() {
remaining_args = &remaining_args[1..];
if (next_arg == "--help" || next_arg == "help") && !options_ended {
help = true;
continue;
}
- if next_arg.starts_with("-") && !options_ended {
+ if next_arg.starts_with('-') && !options_ended {
if next_arg == "--" {
options_ended = true;
continue;
@@ -712,7 +867,7 @@ pub fn parse_struct_args(
}
}
- parse_positionals.parse(&mut positional_index, next_arg)?;
+ options_ended |= parse_positionals.parse(&mut positional_index, next_arg)?;
}
if help {
@@ -751,7 +906,7 @@ impl<'a> ParseStructOptions<'a> {
ParseStructOption::Flag(ref mut b) => b.set_flag(arg),
ParseStructOption::Value(ref mut pvs) => {
let value = remaining_args
- .get(0)
+ .first()
.ok_or_else(|| ["No value provided for option '", arg, "'.\n"].concat())?;
*remaining_args = &remaining_args[1..];
pvs.fill_slot(arg, value).map_err(|s| {
@@ -783,25 +938,31 @@ pub enum ParseStructOption<'a> {
pub struct ParseStructPositionals<'a> {
pub positionals: &'a mut [ParseStructPositional<'a>],
pub last_is_repeating: bool,
+ pub last_is_greedy: bool,
}
impl<'a> ParseStructPositionals<'a> {
/// Parse the next positional argument.
///
/// `arg`: the argument supplied by the user.
- fn parse(&mut self, index: &mut usize, arg: &str) -> Result<(), EarlyExit> {
+ ///
+ /// Returns true if non-positional argument parsing should stop
+ /// after this one.
+ fn parse(&mut self, index: &mut usize, arg: &str) -> Result<bool, EarlyExit> {
if *index < self.positionals.len() {
self.positionals[*index].parse(arg)?;
- // Don't increment position if we're at the last arg
- // *and* the last arg is repeating.
- let skip_increment = self.last_is_repeating && *index == self.positionals.len() - 1;
-
- if !skip_increment {
+ if self.last_is_repeating && *index == self.positionals.len() - 1 {
+ // Don't increment position if we're at the last arg
+ // *and* the last arg is repeating. If it's also remainder,
+ // halt non-option processing after this.
+ Ok(self.last_is_greedy)
+ } else {
+ // If it is repeating, though, increment the index and continue
+ // processing options.
*index += 1;
+ Ok(false)
}
-
- Ok(())
} else {
Err(EarlyExit { output: unrecognized_arg(arg), status: Err(()) })
}
@@ -847,7 +1008,10 @@ pub struct ParseStructSubCommand<'a> {
// The subcommand commands
pub subcommands: &'static [&'static CommandInfo],
+ pub dynamic_subcommands: &'a [&'static CommandInfo],
+
// The function to parse the subcommand arguments.
+ #[allow(clippy::type_complexity)]
pub parse_func: &'a mut dyn FnMut(&[&str], &[&str]) -> Result<(), EarlyExit>,
}
@@ -859,7 +1023,7 @@ impl<'a> ParseStructSubCommand<'a> {
arg: &str,
remaining_args: &[&str],
) -> Result<bool, EarlyExit> {
- for subcommand in self.subcommands {
+ for subcommand in self.subcommands.iter().chain(self.dynamic_subcommands.iter()) {
if subcommand.name == arg {
let mut command = cmd_name.to_owned();
command.push(subcommand.name);
@@ -877,7 +1041,7 @@ impl<'a> ParseStructSubCommand<'a> {
}
}
- return Ok(false);
+ Ok(false)
}
}
@@ -888,7 +1052,7 @@ fn prepend_help<'a>(args: &[&'a str]) -> Vec<&'a str> {
}
#[doc(hidden)]
-pub fn print_subcommands(commands: &[&CommandInfo]) -> String {
+pub fn print_subcommands<'a>(commands: impl Iterator<Item = &'a CommandInfo>) -> String {
let mut out = String::new();
for cmd in commands {
argh_shared::write_description(&mut out, cmd);
@@ -905,7 +1069,7 @@ fn unrecognized_arg(arg: &str) -> String {
#[derive(Default)]
pub struct MissingRequirements {
options: Vec<&'static str>,
- subcommands: Option<&'static [&'static CommandInfo]>,
+ subcommands: Option<Vec<&'static CommandInfo>>,
positional_args: Vec<&'static str>,
}
@@ -920,8 +1084,8 @@ impl MissingRequirements {
// Add a missing required subcommand.
#[doc(hidden)]
- pub fn missing_subcommands(&mut self, commands: &'static [&'static CommandInfo]) {
- self.subcommands = Some(commands);
+ pub fn missing_subcommands(&mut self, commands: impl Iterator<Item = &'static CommandInfo>) {
+ self.subcommands = Some(commands.collect());
}
// Add a missing positional argument.
@@ -951,7 +1115,7 @@ impl MissingRequirements {
if !self.options.is_empty() {
if !self.positional_args.is_empty() {
- output.push_str("\n");
+ output.push('\n');
}
output.push_str("Required options not provided:");
for option in &self.options {
@@ -960,9 +1124,9 @@ impl MissingRequirements {
}
}
- if let Some(missing_subcommands) = self.subcommands {
+ if let Some(missing_subcommands) = &self.subcommands {
if !self.options.is_empty() {
- output.push_str("\n");
+ output.push('\n');
}
output.push_str("One of the following subcommands must be present:");
output.push_str(NEWLINE_INDENT);