blob: 94e2f5ed99a20c5ac434af1247eb048eac40576d [file] [log] [blame]
// Copyright (c) 2020 Google LLC All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//! Derive-based argument parsing optimized for code size and conformance
//! to the Fuchsia commandline tools specification
//!
//! The public API of this library consists primarily of the `FromArgs`
//! derive and the `from_env` function, which can be used to produce
//! a top-level `FromArgs` type from the current program's commandline
//! arguments.
//!
//! ## Basic Example
//!
//! ```rust,no_run
//! use argh::FromArgs;
//!
//! #[derive(FromArgs)]
//! /// Reach new heights.
//! struct GoUp {
//! /// whether or not to jump
//! #[argh(switch, short = 'j')]
//! jump: bool,
//!
//! /// how high to go
//! #[argh(option)]
//! height: usize,
//!
//! /// an optional nickname for the pilot
//! #[argh(option)]
//! pilot_nickname: Option<String>,
//! }
//!
//! fn main() {
//! let up: GoUp = argh::from_env();
//! }
//! ```
//!
//! `./some_bin --help` will then output the following:
//!
//! ```bash
//! Usage: cmdname [-j] --height <height> [--pilot-nickname <pilot-nickname>]
//!
//! Reach new heights.
//!
//! Options:
//! -j, --jump whether or not to jump
//! --height how high to go
//! --pilot-nickname an optional nickname for the pilot
//! --help display usage information
//! ```
//!
//! The resulting program can then be used in any of these ways:
//! - `./some_bin --height 5`
//! - `./some_bin -j --height 5`
//! - `./some_bin --jump --height 5 --pilot-nickname Wes`
//!
//! Switches, like `jump`, are optional and will be set to true if provided.
//!
//! Options, like `height` and `pilot_nickname`, can be either required,
//! optional, or repeating, depending on whether they are contained in an
//! `Option` or a `Vec`. Default values can be provided using the
//! `#[argh(default = "<your_code_here>")]` attribute, and in this case an
//! option is treated as optional.
//!
//! ```rust
//! use argh::FromArgs;
//!
//! fn default_height() -> usize {
//! 5
//! }
//!
//! #[derive(FromArgs)]
//! /// Reach new heights.
//! struct GoUp {
//! /// an optional nickname for the pilot
//! #[argh(option)]
//! pilot_nickname: Option<String>,
//!
//! /// an optional height
//! #[argh(option, default = "default_height()")]
//! height: usize,
//!
//! /// an optional direction which is "up" by default
//! #[argh(option, default = "String::from(\"only up\")")]
//! direction: String,
//! }
//!
//! fn main() {
//! let up: GoUp = argh::from_env();
//! }
//! ```
//!
//! Custom option types can be deserialized so long as they implement the
//! `FromArgValue` trait (automatically implemented for all `FromStr` types).
//! If more customized parsing is required, you can supply a custom
//! `fn(&str) -> Result<T, String>` using the `from_str_fn` attribute:
//!
//! ```
//! # use argh::FromArgs;
//!
//! #[derive(FromArgs)]
//! /// Goofy thing.
//! struct FiveStruct {
//! /// always five
//! #[argh(option, from_str_fn(always_five))]
//! five: usize,
//! }
//!
//! fn always_five(_value: &str) -> Result<usize, String> {
//! Ok(5)
//! }
//! ```
//!
//! Positional arguments can be declared using `#[argh(positional)]`.
//! These arguments will be parsed in order of their declaration in
//! the structure:
//!
//! ```rust
//! use argh::FromArgs;
//! #[derive(FromArgs, PartialEq, Debug)]
//! /// A command with positional arguments.
//! struct WithPositional {
//! #[argh(positional)]
//! first: String,
//! }
//! ```
//!
//! The last positional argument may include a default, or be wrapped in
//! `Option` or `Vec` to indicate an optional or repeating positional argument.
//!
//! 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:
//!
//! ```rust
//! # use argh::FromArgs;
//!
//! #[derive(FromArgs, PartialEq, Debug)]
//! /// Top-level command.
//! struct TopLevel {
//! #[argh(subcommand)]
//! nested: MySubCommandEnum,
//! }
//!
//! #[derive(FromArgs, PartialEq, Debug)]
//! #[argh(subcommand)]
//! enum MySubCommandEnum {
//! One(SubCommandOne),
//! Two(SubCommandTwo),
//! }
//!
//! #[derive(FromArgs, PartialEq, Debug)]
//! /// First subcommand.
//! #[argh(subcommand, name = "one")]
//! struct SubCommandOne {
//! #[argh(option)]
//! /// how many x
//! x: usize,
//! }
//!
//! #[derive(FromArgs, PartialEq, Debug)]
//! /// Second subcommand.
//! #[argh(subcommand, name = "two")]
//! struct SubCommandTwo {
//! #[argh(switch)]
//! /// whether to fooey
//! fooey: bool,
//! }
//! ```
#![deny(missing_docs)]
use std::str::FromStr;
pub use argh_derive::FromArgs;
/// Information about a particular command used for output.
pub type CommandInfo = argh_shared::CommandInfo<'static>;
/// Types which can be constructed from a set of commandline arguments.
pub trait FromArgs: Sized {
/// Construct the type from an input set of arguments.
///
/// The first argument `command_name` is the identifier for the current
/// command, treating each segment as space-separated. This is to be
/// used in the output of `--help`, `--version`, and similar flags.
fn from_args(command_name: &[&str], args: &[&str]) -> Result<Self, EarlyExit>;
}
/// A top-level `FromArgs` implementation that is not a subcommand.
pub trait TopLevelCommand: FromArgs {}
/// A `FromArgs` implementation that can parse into one or more subcommands.
pub trait SubCommands: FromArgs {
/// Info for the commands.
const COMMANDS: &'static [&'static CommandInfo];
}
/// A `FromArgs` implementation that represents a single subcommand.
pub trait SubCommand: FromArgs {
/// Information about the subcommand.
const COMMAND: &'static CommandInfo;
}
impl<T: SubCommand> SubCommands for T {
const COMMANDS: &'static [&'static CommandInfo] = &[T::COMMAND];
}
/// 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`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EarlyExit {
/// The output to display to the user of the commandline tool.
pub output: String,
/// Status of argument parsing.
///
/// `Ok` if the command was parsed successfully and the early exit is due
/// to a flag like `--help` causing early exit with output.
///
/// `Err` if the arguments were not successfully parsed.
// TODO replace with std::process::ExitCode when stable.
pub status: Result<(), ()>,
}
impl From<String> for EarlyExit {
fn from(err_msg: String) -> Self {
Self { output: err_msg, status: Err(()) }
}
}
/// 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())
}
/// Create a `FromArgs` type from the current process's `env::args`.
///
/// This function will exit early from the current process if argument parsing
/// was unsuccessful or if information like `--help` was requested.
pub fn from_env<T: TopLevelCommand>() -> T {
let strings: Vec<String> = std::env::args().collect();
let cmd = cmd(&strings[0], &strings[0]);
let strs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect();
T::from_args(&[cmd], &strs[1..]).unwrap_or_else(|early_exit| {
println!("{}", early_exit.output);
std::process::exit(match early_exit.status {
Ok(()) => 0,
Err(()) => 1,
})
})
}
/// Create a `FromArgs` type from the current process's `env::args`.
///
/// This special cases usages where argh is being used in an environment where cargo is
/// driving the build. We skip the second env variable.
///
/// This function will exit early from the current process if argument parsing
/// was unsuccessful or if information like `--help` was requested.
pub fn cargo_from_env<T: TopLevelCommand>() -> T {
let strings: Vec<String> = std::env::args().collect();
let cmd = cmd(&strings[1], &strings[1]);
let strs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect();
T::from_args(&[cmd], &strs[2..]).unwrap_or_else(|early_exit| {
println!("{}", early_exit.output);
std::process::exit(match early_exit.status {
Ok(()) => 0,
Err(()) => 1,
})
})
}
/// Types which can be constructed from a single commandline value.
///
/// Any field type declared in a struct that derives `FromArgs` must implement
/// this trait. A blanket implementation exists for types implementing
/// `FromStr<Error: Display>`. Custom types can implement this trait
/// directly.
pub trait FromArgValue: Sized {
/// Construct the type from a commandline value, returning an error string
/// on failure.
fn from_arg_value(value: &str) -> Result<Self, String>;
}
impl<T> FromArgValue for T
where
T: FromStr,
T::Err: std::fmt::Display,
{
fn from_arg_value(value: &str) -> Result<Self, String> {
T::from_str(value).map_err(|x| x.to_string())
}
}
// The following items are all used by the generated code, and should not be considered part
// of this library's public API surface.
// A trait for for slots that reserve space for a value and know how to parse that value
// from a command-line `&str` argument.
//
// This trait is only implemented for the type `ParseValueSlotTy`. This indirection is
// necessary to allow abstracting over `ParseValueSlotTy` instances with different
// generic parameters.
#[doc(hidden)]
pub trait ParseValueSlot {
fn fill_slot(&mut self, value: &str) -> Result<(), String>;
}
// The concrete type implementing the `ParseValueSlot` trait.
//
// `T` is the type to be parsed from a single string.
// `Slot` is the type of the container that can hold a value or values of type `T`.
#[doc(hidden)]
pub struct ParseValueSlotTy<Slot, T> {
// The slot for a parsed value.
pub slot: Slot,
// The function to parse the value from a string
pub parse_func: fn(&str) -> Result<T, String>,
}
// `ParseValueSlotTy<Option<T>, T>` is used as the slot for all non-repeating
// arguments, both optional and required.
impl<T> ParseValueSlot for ParseValueSlotTy<Option<T>, T> {
fn fill_slot(&mut self, value: &str) -> Result<(), String> {
if self.slot.is_some() {
return Err("duplicate values provided".to_string());
}
self.slot = Some((self.parse_func)(value)?);
Ok(())
}
}
// `ParseValueSlotTy<Vec<T>, T>` is used as the slot for repeating arguments.
impl<T> ParseValueSlot for ParseValueSlotTy<Vec<T>, T> {
fn fill_slot(&mut self, value: &str) -> Result<(), String> {
self.slot.push((self.parse_func)(value)?);
Ok(())
}
}
/// A type which can be the receiver of a `Flag`.
pub trait Flag {
/// Creates a default instance of the flag value;
fn default() -> Self
where
Self: Sized;
/// Sets the flag. This function is called when the flag is provided.
fn set_flag(&mut self);
}
impl Flag for bool {
fn default() -> Self {
false
}
fn set_flag(&mut self) {
*self = true;
}
}
macro_rules! impl_flag_for_integers {
($($ty:ty,)*) => {
$(
impl Flag for $ty {
fn default() -> Self {
0
}
fn set_flag(&mut self) {
*self = self.saturating_add(1);
}
}
)*
}
}
impl_flag_for_integers![u8, u16, u32, u64, u128, i8, i16, i32, i64, i128,];
// `--` or `-` options, including a mutable reference to their value.
#[doc(hidden)]
pub enum CmdOption<'a> {
// A flag which is set to `true` when provided.
Flag(&'a mut dyn Flag),
// A value which is parsed from the string following the `--` argument,
// e.g. `--foo bar`.
Value(&'a mut dyn ParseValueSlot),
}
#[doc(hidden)]
pub fn unrecognized_argument(x: &str) -> String {
["Unrecognized argument: ", x, "\n"].concat()
}
// A sentinel value that indicates that there is no
// output table mapping for the given flag.
// This is used for arguments like `--verbose` and `--quiet`
// that must be silently accepted if the `argh` user hasn't
// specified their behavior explicitly.
#[doc(hidden)]
pub const OUTPUT_TABLE_NONE: usize = std::usize::MAX;
/// Parse a commandline option.
///
/// `arg`: the current option argument being parsed (e.g. `--foo`).
/// `remaining_args`: the remaining command line arguments. This slice
/// will be advanced forwards if the option takes a value argument.
/// `output_table`: the storage for output data.
/// `arg_to_input`: a mapping from option string literals to the entry
/// in the output table. This may contain multiple entries mapping to
/// the same location in the table if both a short and long version
/// of the option exist (`-z` and `--zoo`).
#[doc(hidden)]
pub fn parse_option(
arg: &str,
remaining_args: &mut &[&str],
output_table: &mut [CmdOption<'_>],
arg_to_output: &[(&str, usize)],
) -> Result<(), String> {
let pos = arg_to_output
.iter()
.find_map(|&(name, pos)| if name == arg { Some(pos) } else { None })
.ok_or_else(|| unrecognized_argument(arg))?;
if pos == OUTPUT_TABLE_NONE {
return Ok(());
}
match &mut output_table[pos] {
CmdOption::Flag(b) => b.set_flag(),
CmdOption::Value(pvs) => {
let value = remaining_args
.get(0)
.ok_or_else(|| ["No value provided for option '", arg, "'.\n"].concat())?;
*remaining_args = &remaining_args[1..];
pvs.fill_slot(value).map_err(|s| {
["Error parsing option '", arg, "' with value '", value, "': ", &s, "\n"].concat()
})?;
}
}
Ok(())
}
/// Parse a positional argument.
///
/// arg: the argument supplied by the user
/// positional: a tuple containing slot to parse into and the name of the argument
#[doc(hidden)]
pub fn parse_positional(
arg: &str,
positional: &mut (&mut dyn ParseValueSlot, &'static str),
) -> Result<(), String> {
let (slot, name) = positional;
slot.fill_slot(arg).map_err(|s| {
["Error parsing positional argument '", name, "' with value '", arg, "': ", &s, "\n"]
.concat()
})
}
// Prepend `help` to a list of arguments.
// This is used to pass the `help` argument on to subcommands.
#[doc(hidden)]
pub fn prepend_help<'a>(args: &[&'a str]) -> Vec<&'a str> {
[&["help"], args].concat()
}
#[doc(hidden)]
pub fn print_subcommands(commands: &[&CommandInfo]) -> String {
let mut out = String::new();
for cmd in commands {
argh_shared::write_description(&mut out, cmd);
}
out
}
#[doc(hidden)]
pub fn expected_subcommand(commands: &[&str]) -> String {
["Expected one of the following subcommands: ", &commands.join(", "), "\n"].concat()
}
#[doc(hidden)]
pub fn unrecognized_arg(arg: &str) -> String {
["Unrecognized argument: ", arg, "\n"].concat()
}
// An error string builder to report missing required options and subcommands.
#[doc(hidden)]
#[derive(Default)]
pub struct MissingRequirements {
options: Vec<&'static str>,
subcommands: Option<&'static [&'static CommandInfo]>,
positional_args: Vec<&'static str>,
}
const NEWLINE_INDENT: &str = "\n ";
impl MissingRequirements {
// Add a missing required option.
#[doc(hidden)]
pub fn missing_option(&mut self, name: &'static str) {
self.options.push(name)
}
// Add a missing required subcommand.
#[doc(hidden)]
pub fn missing_subcommands(&mut self, commands: &'static [&'static CommandInfo]) {
self.subcommands = Some(commands);
}
// Add a missing positional argument.
#[doc(hidden)]
pub fn missing_positional_arg(&mut self, name: &'static str) {
self.positional_args.push(name)
}
// If any missing options or subcommands were provided, returns an error string
// describing the missing args.
#[doc(hidden)]
pub fn err_on_any(&self) -> Result<(), String> {
if self.options.is_empty() && self.subcommands.is_none() && self.positional_args.is_empty()
{
return Ok(());
}
let mut output = String::new();
if !self.positional_args.is_empty() {
output.push_str("Required positional arguments not provided:");
for arg in &self.positional_args {
output.push_str(NEWLINE_INDENT);
output.push_str(arg);
}
}
if !self.options.is_empty() {
if !self.positional_args.is_empty() {
output.push_str("\n");
}
output.push_str("Required options not provided:");
for option in &self.options {
output.push_str(NEWLINE_INDENT);
output.push_str(option);
}
}
if let Some(missing_subcommands) = self.subcommands {
if !self.options.is_empty() {
output.push_str("\n");
}
output.push_str("One of the following subcommands must be present:");
output.push_str(NEWLINE_INDENT);
output.push_str("help");
for subcommand in missing_subcommands {
output.push_str(NEWLINE_INDENT);
output.push_str(subcommand.name);
}
}
output.push('\n');
Err(output)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_cmd_extraction() {
let expected = "test_cmd";
let path = format!("/tmp/{}", expected);
let cmd = cmd(&path, &path);
assert_eq!(expected, cmd);
}
}