Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions argh/examples/simple_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
use {argh::FromArgs, std::fmt::Debug};

#[derive(FromArgs, PartialEq, Debug)]
#[argh(global)]
/// Top-level command.
struct TopLevel {
#[argh(switch, short = 'v')]
/// verbose mode
verbose: bool,

#[argh(subcommand)]
nested: MySubCommandEnum,
}
Expand Down
37 changes: 33 additions & 4 deletions argh/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,22 @@ pub trait FromArgs: Sized {
/// ```
fn from_args(command_name: &[&str], args: &[&str]) -> Result<Self, EarlyExit>;

/// Implementation of `from_args` that accepts global arguments.
/// This allows subcommands to recognize parent options when `#[argh(global)]` is used.
///
/// * `command_name` and `args` are the same as in `from_args`.
/// * `parent_options` ("global") is usually passed in by a top-level command.
#[doc(hidden)]
fn from_args_global(
command_name: &[&str],
args: &[&str],
parent_options: Option<&mut ParseStructOptions<'_>>,
) -> Result<Self, EarlyExit> {
// Default implementation for types that don't support global arguments
let _ = parent_options;
Self::from_args(command_name, args)
}

/// Get a String with just the argument names, e.g., options, flags, subcommands, etc, but
/// without the values of the options and arguments. This can be useful as a means to capture
/// anonymous usage statistics without revealing the content entered by the end user.
Expand Down Expand Up @@ -938,6 +954,7 @@ impl_flag_for_integers![u8, u16, u32, u64, u128, i8, i16, i32, i64, i128,];
/// `parse_options`: Helper to parse optional arguments.
/// `parse_positionals`: Helper to parse positional arguments.
/// `parse_subcommand`: Helper to parse a subcommand.
/// `parent_options`: Optional parent command's options ("global" arguments).
/// `help_func`: Generate a help message.
#[doc(hidden)]
pub fn parse_struct_args(
Expand All @@ -946,6 +963,7 @@ pub fn parse_struct_args(
mut parse_options: ParseStructOptions<'_>,
mut parse_positionals: ParseStructPositionals<'_>,
mut parse_subcommand: Option<ParseStructSubCommand<'_>>,
mut parent_options: Option<&mut ParseStructOptions<'_>>,
help_func: &dyn Fn() -> String,
) -> Result<(), EarlyExit> {
let mut help = false;
Expand All @@ -970,12 +988,18 @@ pub fn parse_struct_args(
return Err("Trailing arguments are not allowed after `help`.".to_string().into());
}

parse_options.parse(next_arg, &mut remaining_args)?;
parse_options.parse(next_arg, &mut remaining_args).or_else(
|err| match parent_options.as_mut() {
Some(parent) => parent.parse(next_arg, &mut remaining_args),
None => Err(err),
},
)?;
continue;
}

if let Some(ref mut parse_subcommand) = parse_subcommand {
if parse_subcommand.parse(help, cmd_name, next_arg, remaining_args)? {
let parent_options = Some(&mut parse_options);
if parse_subcommand.parse(help, cmd_name, next_arg, remaining_args, parent_options)? {
// Unset `help`, since we handled it in the subcommand
help = false;
break 'parse_args;
Expand Down Expand Up @@ -1155,7 +1179,11 @@ pub struct ParseStructSubCommand<'a> {

// The function to parse the subcommand arguments.
#[allow(clippy::type_complexity)]
pub parse_func: &'a mut dyn FnMut(&[&str], &[&str]) -> Result<(), EarlyExit>,
pub parse_func: &'a mut dyn FnMut(
&[&str],
&[&str],
Option<&mut ParseStructOptions<'_>>,
) -> Result<(), EarlyExit>,
}

impl ParseStructSubCommand<'_> {
Expand All @@ -1165,6 +1193,7 @@ impl ParseStructSubCommand<'_> {
cmd_name: &[&str],
arg: &str,
remaining_args: &[&str],
parent_options: Option<&mut ParseStructOptions<'_>>,
) -> Result<bool, EarlyExit> {
for subcommand in self.subcommands.iter().chain(self.dynamic_subcommands.iter()) {
if subcommand.name == arg
Expand All @@ -1180,7 +1209,7 @@ impl ParseStructSubCommand<'_> {
remaining_args
};

(self.parse_func)(&command, remaining_args)?;
(self.parse_func)(&command, remaining_args, parent_options)?;

return Ok(true);
}
Expand Down
138 changes: 138 additions & 0 deletions argh/tests/global_options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright (c) 2026 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.

// Test that #[argh(global)] allows parent command options after subcommands

use argh::FromArgs;

#[derive(FromArgs, PartialEq, Debug)]
#[argh(global)]
/// Top-level command with global option placement
struct TopCommand {
#[argh(switch, short = 'v')]
/// verbose mode
verbose: bool,

#[argh(option, short = 'c')]
/// config file
config: Option<String>,

#[argh(subcommand)]
sub: SubCmd,
}

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand)]
enum SubCmd {
Run(RunCmd),
Build(BuildCmd),
}

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "run")]
/// Run the program
struct RunCmd {
#[argh(option, short = 'n')]
/// number of iterations
iterations: usize,
}

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "build")]
/// Build the program
struct BuildCmd {
#[argh(switch, short = 'r')]
/// release mode
release: bool,
}

fn parse_from(args: &[&str]) -> Result<TopCommand, argh::EarlyExit> {
TopCommand::from_args(&["test"], args)
}

#[test]
fn global_option_after_subcommand() {
let result = parse_from(&["run", "-n", "5", "--verbose"]).unwrap();
assert!(result.verbose);
assert_eq!(result.sub, SubCmd::Run(RunCmd { iterations: 5 }));
}

#[test]
fn global_option_before_subcommand() {
let result = parse_from(&["--verbose", "run", "-n", "5"]).unwrap();
assert!(result.verbose);
assert_eq!(result.sub, SubCmd::Run(RunCmd { iterations: 5 }));
}

#[test]
fn global_multiple_options_after() {
let result = parse_from(&["run", "-n", "5", "--verbose", "-c", "config.toml"]).unwrap();
assert!(result.verbose);
assert_eq!(result.config, Some("config.toml".to_string()));
assert_eq!(result.sub, SubCmd::Run(RunCmd { iterations: 5 }));
}

#[test]
fn global_mixed_order() {
let result = parse_from(&["--verbose", "run", "-n", "5", "-c", "config.toml"]).unwrap();
assert!(result.verbose);
assert_eq!(result.config, Some("config.toml".to_string()));
assert_eq!(result.sub, SubCmd::Run(RunCmd { iterations: 5 }));
}

#[test]
fn global_short_option_after() {
let result = parse_from(&["run", "-n", "5", "-v"]).unwrap();
assert!(result.verbose);
assert_eq!(result.sub, SubCmd::Run(RunCmd { iterations: 5 }));
}

#[test]
fn global_with_build_subcommand() {
let result = parse_from(&["build", "--release", "--verbose"]).unwrap();
assert!(result.verbose);
assert_eq!(result.sub, SubCmd::Build(BuildCmd { release: true }));
}

// Test that non-global commands still reject options after subcommands
#[derive(FromArgs, PartialEq, Debug)]
/// Non-global command
struct NonGlobalTop {
#[argh(switch)]
/// verbose mode
verbose: bool,

#[argh(subcommand)]
sub: NonGlobalSub,
}

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand)]
enum NonGlobalSub {
Run(NonGlobalRun),
}

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "run")]
/// Run command
struct NonGlobalRun {
#[argh(option)]
/// iterations
iterations: usize,
}

#[test]
fn non_global_rejects_option_after_subcommand() {
let result = NonGlobalTop::from_args(&["test"], &["run", "--iterations", "5", "--verbose"]);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.output.contains("Unrecognized"));
}

#[test]
fn non_global_accepts_option_before_subcommand() {
let result =
NonGlobalTop::from_args(&["test"], &["--verbose", "run", "--iterations", "5"]).unwrap();
assert_eq!(result.verbose, true);
}
40 changes: 34 additions & 6 deletions argh_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,15 +383,21 @@ fn impl_from_args_struct_from_args<'a>(
let append_missing_requirements =
append_missing_requirements(&missing_requirements_ident, fields);

let forward_options = if type_attrs.global.is_some() {
quote! { __parent_options }
} else {
quote! { None }
};

let parse_subcommands = if let Some(subcommand) = subcommand {
let name = subcommand.name;
let ty = subcommand.ty_without_wrapper;
quote_spanned! { impl_span =>
Some(argh::ParseStructSubCommand {
subcommands: <#ty as argh::SubCommands>::COMMANDS,
dynamic_subcommands: &<#ty as argh::SubCommands>::dynamic_commands(),
parse_func: &mut |__command, __remaining_args| {
#name = Some(<#ty as argh::FromArgs>::from_args(__command, __remaining_args)?);
parse_func: &mut |__command, __remaining_args, __parent_options| {
#name = Some(<#ty as argh::FromArgs>::from_args_global(__command, __remaining_args, #forward_options)?);
::core::result::Result::Ok(())
},
})
Expand All @@ -413,6 +419,16 @@ fn impl_from_args_struct_from_args<'a>(
let method_impl = quote_spanned! { impl_span =>
fn from_args(__cmd_name: &[&str], __args: &[&str])
-> ::core::result::Result<Self, argh::EarlyExit>
{
Self::from_args_global(__cmd_name, __args, None)
}

#[doc(hidden)]
fn from_args_global(
__cmd_name: &[&str],
__args: &[&str],
mut __parent_options: Option<&mut argh::ParseStructOptions<'_>>,
) -> ::core::result::Result<Self, argh::EarlyExit>
{
#![allow(clippy::unwrap_in_result)]

Expand All @@ -439,7 +455,8 @@ fn impl_from_args_struct_from_args<'a>(
last_is_greedy: #last_positional_is_greedy,
},
#parse_subcommands,
&|| #help,
__parent_options.as_deref_mut(),
&|| #help
)?;

let mut #missing_requirements_ident = argh::MissingRequirements::default();
Expand Down Expand Up @@ -534,7 +551,7 @@ fn impl_from_args_struct_redact_arg_values<'a>(
Some(argh::ParseStructSubCommand {
subcommands: <#ty as argh::SubCommands>::COMMANDS,
dynamic_subcommands: &<#ty as argh::SubCommands>::dynamic_commands(),
parse_func: &mut |__command, __remaining_args| {
parse_func: &mut |__command, __remaining_args, _parent_options| {
#name = Some(<#ty as argh::FromArgs>::redact_arg_values(__command, __remaining_args)?);
::core::result::Result::Ok(())
},
Expand Down Expand Up @@ -585,7 +602,8 @@ fn impl_from_args_struct_redact_arg_values<'a>(
last_is_greedy: #last_positional_is_greedy,
},
#redact_subcommands,
&|| #help,
None,
&|| #help
)?;

let mut #missing_requirements_ident = argh::MissingRequirements::default();
Expand Down Expand Up @@ -1105,6 +1123,16 @@ fn impl_from_args_enum(
impl #impl_generics argh::FromArgs for #name #ty_generics #where_clause {
fn from_args(command_name: &[&str], args: &[&str])
-> std::result::Result<Self, argh::EarlyExit>
{
Self::from_args_global(command_name, args, None)
}

#[doc(hidden)]
fn from_args_global(
command_name: &[&str],
args: &[&str],
parent_options: Option<&mut argh::ParseStructOptions<'_>>,
) -> std::result::Result<Self, argh::EarlyExit>
{
let subcommand_name = if let Some(subcommand_name) = command_name.last() {
*subcommand_name
Expand All @@ -1119,7 +1147,7 @@ fn impl_from_args_enum(
&& subcommand_name.starts_with(*<#variant_ty as argh::SubCommand>::COMMAND.short))
{
return ::core::result::Result::Ok(#name_repeating::#variant_names(
<#variant_ty as argh::FromArgs>::from_args(command_name, args)?
<#variant_ty as argh::FromArgs>::from_args_global(command_name, args, parent_options)?
));
}
)*
Expand Down
Loading