#![deny(missing_docs)]
use std::str::FromStr;
pub use argh_derive::FromArgs;
pub type CommandInfo = argh_shared::CommandInfo<'static>;
pub trait FromArgs: Sized {
fn from_args(command_name: &[&str], args: &[&str]) -> Result<Self, EarlyExit>;
fn redact_arg_values(_command_name: &[&str], _args: &[&str]) -> Result<Vec<String>, EarlyExit> {
Ok(vec!["<<REDACTED>>".into()])
}
}
pub trait TopLevelCommand: FromArgs {}
pub trait SubCommands: FromArgs {
const COMMANDS: &'static [&'static CommandInfo];
fn dynamic_commands() -> &'static [&'static CommandInfo] {
&[]
}
}
pub trait SubCommand: FromArgs {
const COMMAND: &'static CommandInfo;
}
impl<T: SubCommand> SubCommands for T {
const COMMANDS: &'static [&'static CommandInfo] = &[T::COMMAND];
}
pub trait DynamicSubCommand: Sized {
fn commands() -> &'static [&'static CommandInfo];
fn try_redact_arg_values(
command_name: &[&str],
args: &[&str],
) -> Option<Result<Vec<String>, EarlyExit>>;
fn try_from_args(command_name: &[&str], args: &[&str]) -> Option<Result<Self, EarlyExit>>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EarlyExit {
pub output: String,
pub status: Result<(), ()>,
}
impl From<String> for EarlyExit {
fn from(err_msg: String) -> Self {
Self { output: err_msg, status: Err(()) }
}
}
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)
}
pub fn from_env<T: TopLevelCommand>() -> T {
let strings: Vec<String> = std::env::args_os()
.map(|s| s.into_string())
.collect::<Result<Vec<_>, _>>()
.unwrap_or_else(|arg| {
eprintln!("Invalid utf8: {}", arg.to_string_lossy());
std::process::exit(1)
});
if strings.is_empty() {
eprintln!("No program name, argv is empty");
std::process::exit(1)
}
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| {
std::process::exit(match early_exit.status {
Ok(()) => {
println!("{}", early_exit.output);
0
}
Err(()) => {
eprintln!("{}\nRun {} --help for more information.", early_exit.output, cmd);
1
}
})
})
}
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| {
std::process::exit(match early_exit.status {
Ok(()) => {
println!("{}", early_exit.output);
0
}
Err(()) => {
eprintln!("{}\nRun --help for more information.", early_exit.output);
1
}
})
})
}
pub trait FromArgValue: Sized {
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())
}
}
#[doc(hidden)]
pub trait ParseFlag {
fn set_flag(&mut self, arg: &str);
}
impl<T: Flag> ParseFlag for T {
fn set_flag(&mut self, _arg: &str) {
<T as Flag>::set_flag(self);
}
}
#[doc(hidden)]
pub struct RedactFlag {
pub slot: Option<String>,
}
impl ParseFlag for RedactFlag {
fn set_flag(&mut self, arg: &str) {
self.slot = Some(arg.to_string());
}
}
#[doc(hidden)]
pub trait ParseValueSlot {
fn fill_slot(&mut self, arg: &str, value: &str) -> Result<(), String>;
}
#[doc(hidden)]
pub struct ParseValueSlotTy<Slot, T> {
pub slot: Slot,
pub parse_func: fn(&str, &str) -> Result<T, String>,
}
impl<T> ParseValueSlot for ParseValueSlotTy<Option<T>, T> {
fn fill_slot(&mut self, arg: &str, value: &str) -> Result<(), String> {
if self.slot.is_some() {
return Err("duplicate values provided".to_string());
}
self.slot = Some((self.parse_func)(arg, value)?);
Ok(())
}
}
impl<T> ParseValueSlot for ParseValueSlotTy<Vec<T>, T> {
fn fill_slot(&mut self, arg: &str, value: &str) -> Result<(), String> {
self.slot.push((self.parse_func)(arg, value)?);
Ok(())
}
}
pub trait Flag {
fn default() -> Self
where
Self: Sized;
fn set_flag(&mut self);
}
impl Flag for bool {
fn default() -> Self {
false
}
fn set_flag(&mut self) {
*self = true;
}
}
impl Flag for Option<bool> {
fn default() -> Self {
None
}
fn set_flag(&mut self) {
*self = Some(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,];
#[doc(hidden)]
pub fn parse_struct_args(
cmd_name: &[&str],
args: &[&str],
mut parse_options: ParseStructOptions<'_>,
mut parse_positionals: ParseStructPositionals<'_>,
mut parse_subcommand: Option<ParseStructSubCommand<'_>>,
help_func: &dyn Fn() -> String,
) -> Result<(), EarlyExit> {
let mut help = false;
let mut remaining_args = args;
let mut positional_index = 0;
let mut options_ended = false;
'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 == "--" {
options_ended = true;
continue;
}
if help {
return Err("Trailing arguments are not allowed after `help`.".to_string().into());
}
parse_options.parse(next_arg, &mut remaining_args)?;
continue;
}
if let Some(ref mut parse_subcommand) = parse_subcommand {
if parse_subcommand.parse(help, cmd_name, next_arg, remaining_args)? {
help = false;
break 'parse_args;
}
}
options_ended |= parse_positionals.parse(&mut positional_index, next_arg)?;
}
if help {
Err(EarlyExit { output: help_func(), status: Ok(()) })
} else {
Ok(())
}
}
#[doc(hidden)]
pub struct ParseStructOptions<'a> {
pub arg_to_slot: &'static [(&'static str, usize)],
pub slots: &'a mut [ParseStructOption<'a>],
}
impl<'a> ParseStructOptions<'a> {
fn parse(&mut self, arg: &str, remaining_args: &mut &[&str]) -> Result<(), String> {
let pos = self
.arg_to_slot
.iter()
.find_map(|&(name, pos)| if name == arg { Some(pos) } else { None })
.ok_or_else(|| unrecognized_argument(arg))?;
match self.slots[pos] {
ParseStructOption::Flag(ref mut b) => b.set_flag(arg),
ParseStructOption::Value(ref mut pvs) => {
let value = remaining_args
.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| {
["Error parsing option '", arg, "' with value '", value, "': ", &s, "\n"]
.concat()
})?;
}
}
Ok(())
}
}
fn unrecognized_argument(x: &str) -> String {
["Unrecognized argument: ", x, "\n"].concat()
}
#[doc(hidden)]
pub enum ParseStructOption<'a> {
Flag(&'a mut dyn ParseFlag),
Value(&'a mut dyn ParseValueSlot),
}
#[doc(hidden)]
pub struct ParseStructPositionals<'a> {
pub positionals: &'a mut [ParseStructPositional<'a>],
pub last_is_repeating: bool,
pub last_is_greedy: bool,
}
impl<'a> ParseStructPositionals<'a> {
fn parse(&mut self, index: &mut usize, arg: &str) -> Result<bool, EarlyExit> {
if *index < self.positionals.len() {
self.positionals[*index].parse(arg)?;
if self.last_is_repeating && *index == self.positionals.len() - 1 {
Ok(self.last_is_greedy)
} else {
*index += 1;
Ok(false)
}
} else {
Err(EarlyExit { output: unrecognized_arg(arg), status: Err(()) })
}
}
}
#[doc(hidden)]
pub struct ParseStructPositional<'a> {
pub name: &'static str,
pub slot: &'a mut dyn ParseValueSlot,
}
impl<'a> ParseStructPositional<'a> {
fn parse(&mut self, arg: &str) -> Result<(), EarlyExit> {
self.slot.fill_slot("", arg).map_err(|s| {
[
"Error parsing positional argument '",
self.name,
"' with value '",
arg,
"': ",
&s,
"\n",
]
.concat()
.into()
})
}
}
#[doc(hidden)]
pub struct ParseStructSubCommand<'a> {
pub subcommands: &'static [&'static CommandInfo],
pub dynamic_subcommands: &'a [&'static CommandInfo],
#[allow(clippy::type_complexity)]
pub parse_func: &'a mut dyn FnMut(&[&str], &[&str]) -> Result<(), EarlyExit>,
}
impl<'a> ParseStructSubCommand<'a> {
fn parse(
&mut self,
help: bool,
cmd_name: &[&str],
arg: &str,
remaining_args: &[&str],
) -> Result<bool, EarlyExit> {
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);
let prepended_help;
let remaining_args = if help {
prepended_help = prepend_help(remaining_args);
&prepended_help
} else {
remaining_args
};
(self.parse_func)(&command, remaining_args)?;
return Ok(true);
}
}
Ok(false)
}
}
fn prepend_help<'a>(args: &[&'a str]) -> Vec<&'a str> {
[&["help"], args].concat()
}
#[doc(hidden)]
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);
}
out
}
fn unrecognized_arg(arg: &str) -> String {
["Unrecognized argument: ", arg, "\n"].concat()
}
#[doc(hidden)]
#[derive(Default)]
pub struct MissingRequirements {
options: Vec<&'static str>,
subcommands: Option<Vec<&'static CommandInfo>>,
positional_args: Vec<&'static str>,
}
const NEWLINE_INDENT: &str = "\n ";
impl MissingRequirements {
#[doc(hidden)]
pub fn missing_option(&mut self, name: &'static str) {
self.options.push(name)
}
#[doc(hidden)]
pub fn missing_subcommands(&mut self, commands: impl Iterator<Item = &'static CommandInfo>) {
self.subcommands = Some(commands.collect());
}
#[doc(hidden)]
pub fn missing_positional_arg(&mut self, name: &'static str) {
self.positional_args.push(name)
}
#[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('\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('\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);
}
}