Support short only options and switches

This commit is contained in:
topjohnwu
2025-10-19 03:20:58 -07:00
committed by John Wu
parent 52d8910bdd
commit 57d9fc6099
5 changed files with 61 additions and 57 deletions

View File

@@ -175,23 +175,29 @@ impl<'a> StructField<'a> {
}
// Determine the "long" name of options and switches.
// Defaults to the kebab-case'd field name if `#[argh(long = "...")]` is omitted.
// Defaults to the kebab-cased field name if `#[argh(long = "...")]` is omitted.
// If `#[argh(long = none)]` is explicitly set, no long name will be set.
let long_name = match kind {
FieldKind::Switch | FieldKind::Option => {
let long_name = attrs
.long
.as_ref()
.map(syn::LitStr::value)
.unwrap_or_else(|| {
let long_name = match &attrs.long {
None => {
let kebab_name = to_kebab_case(&name.unraw().to_string());
check_long_name(errors, name, &kebab_name);
kebab_name
});
if long_name == "help" {
errors.err(field, "Custom `--help` flags are not supported.");
Some(kebab_name)
}
Some(None) => None,
Some(Some(long)) => Some(long.value()),
}
let long_name = format!("--{}", long_name);
Some(long_name)
.map(|long_name| {
if long_name == "help" {
errors.err(field, "Custom `--help` flags are not supported.");
}
format!("--{}", long_name)
});
if let (None, None) = (&attrs.short, &long_name) {
errors.err(field, "At least one of `short` or `long` has to be set.")
};
long_name
}
FieldKind::SubCommand | FieldKind::Positional => None,
};
@@ -214,6 +220,15 @@ impl<'a> StructField<'a> {
.map(LitStr::value)
.unwrap_or_else(|| self.name.to_string().trim_matches('_').to_owned())
}
fn option_arg_name(&self) -> String {
match (&self.attrs.short, &self.long_name) {
(None, None) => unreachable!("short and long cannot both be None"),
(Some(short), None) => format!("-{}", short.value()),
(None, Some(long)) => long.clone(),
(Some(short), Some(long)) => format!("-{},{long}", short.value()),
}
}
}
fn to_kebab_case(s: &str) -> String {
@@ -228,22 +243,6 @@ fn to_kebab_case(s: &str) -> String {
res
}
#[test]
fn test_kebabs() {
#[track_caller]
fn check(s: &str, want: &str) {
let got = to_kebab_case(s);
assert_eq!(got.as_str(), want)
}
check("", "");
check("_", "");
check("foo", "foo");
check("__foo_", "foo");
check("foo_bar", "foo-bar");
check("foo__Bar", "foo-Bar");
check("foo_bar__baz_", "foo-bar-baz");
}
/// Implements `FromArgs` and `TopLevelCommand` or `SubCommand` for a `#[derive(FromArgs)]` struct.
fn impl_from_args_struct(
errors: &Errors,
@@ -620,17 +619,15 @@ fn unwrap_from_args_fields<'a>(
/// to an index in the output table.
fn flag_str_to_output_table_map_entries<'a>(fields: &'a [StructField<'a>]) -> Vec<TokenStream> {
let mut flag_str_to_output_table_map = vec![];
for (i, (field, long_name)) in fields
.iter()
.filter_map(|field| field.long_name.as_ref().map(|long_name| (field, long_name)))
.enumerate()
{
for (i, field) in fields.iter().enumerate() {
if let Some(short) = &field.attrs.short {
let short = format!("-{}", short.value());
flag_str_to_output_table_map.push(quote! { (#short, #i) });
}
flag_str_to_output_table_map.push(quote! { (#long_name, #i) });
if let Some(long) = &field.long_name {
flag_str_to_output_table_map.push(quote! { (#long, #i) });
}
}
flag_str_to_output_table_map
}
@@ -658,10 +655,7 @@ fn append_missing_requirements<'a>(
}
}
FieldKind::Option => {
let name = field
.long_name
.as_ref()
.expect("options always have a long name");
let name = field.option_arg_name();
quote! {
if #field_name.slot.is_none() {
#mri.missing_option(#name)

View File

@@ -16,7 +16,7 @@ pub struct FieldAttrs {
pub description: Option<Description>,
pub from_str_fn: Option<syn::ExprPath>,
pub field_type: Option<FieldType>,
pub long: Option<syn::LitStr>,
pub long: Option<Option<syn::LitStr>>,
pub short: Option<syn::LitChar>,
pub arg_name: Option<syn::LitStr>,
pub greedy: Option<syn::Path>,
@@ -179,10 +179,20 @@ impl FieldAttrs {
}
fn parse_attr_long(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
parse_attr_single_string(errors, m, "long", &mut self.long);
let long = self.long.as_ref().unwrap();
let value = long.value();
check_long_name(errors, long, &value);
if let Some(first) = &self.long {
errors.duplicate_attrs("long", first, m);
} else if let syn::Expr::Path(syn::ExprPath { path, .. }) = &m.value
&& let Some(ident) = path.get_ident()
&& ident.to_string().eq_ignore_ascii_case("none")
{
self.long = Some(None);
} else if let Some(lit_str) = errors.expect_lit_str(&m.value) {
self.long = Some(Some(lit_str.clone()));
}
if let Some(Some(long)) = &self.long {
let value = long.value();
check_long_name(errors, long, &value);
}
}
fn parse_attr_short(&mut self, errors: &Errors, m: &syn::MetaNameValue) {

View File

@@ -43,9 +43,9 @@ enum Action {
#[derive(FromArgs)]
#[argh(subcommand, name = "unpack")]
struct Unpack {
#[argh(switch, short = 'n')]
#[argh(switch, short = 'n', long = none)]
no_decompress: bool,
#[argh(switch, short = 'h')]
#[argh(switch, short = 'h', long = none)]
dump_header: bool,
#[argh(positional)]
img: Utf8CString,
@@ -54,7 +54,7 @@ struct Unpack {
#[derive(FromArgs)]
#[argh(subcommand, name = "repack")]
struct Repack {
#[argh(switch, short = 'n')]
#[argh(switch, short = 'n', long = none)]
no_compress: bool,
#[argh(positional)]
img: Utf8CString,
@@ -136,7 +136,7 @@ struct Dtb {
#[derive(FromArgs)]
#[argh(subcommand, name = "split")]
struct Split {
#[argh(switch, short = 'n')]
#[argh(switch, short = 'n', long = none)]
no_decompress: bool,
#[argh(positional)]
file: Utf8CString,

View File

@@ -18,7 +18,7 @@ pub(crate) enum DtbAction {
#[derive(FromArgs)]
#[argh(subcommand, name = "print")]
pub(crate) struct Print {
#[argh(switch, short = 'f')]
#[argh(switch, short = 'f', long = none)]
fstab: bool,
}

View File

@@ -17,21 +17,21 @@ use std::io::BufReader;
struct ResetProp {
#[argh(switch, short = 'v')]
verbose: bool,
#[argh(switch, short = 'w')]
#[argh(switch, short = 'w', long = none)]
wait_mode: bool,
#[argh(switch, short = 'p')]
#[argh(switch, short = 'p', long = none)]
persist: bool,
#[argh(switch, short = 'P')]
#[argh(switch, short = 'P', long = none)]
persist_only: bool,
#[argh(switch, short = 'Z')]
#[argh(switch, short = 'Z', long = none)]
context: bool,
#[argh(switch, short = 'n')]
#[argh(switch, short = 'n', long = none)]
skip_svc: bool,
#[argh(option, short = 'f')]
file: Option<Utf8CString>,
#[argh(option, long = "delete", short = 'd')]
#[argh(option, short = 'd', long = "delete")]
delete_key: Option<Utf8CString>,
#[argh(positional)]
#[argh(positional, greedy = true)]
args: Vec<Utf8CString>,
}
@@ -57,7 +57,7 @@ Wait mode arguments (toggled with -w):
General flags:
-h,--help show this message
-v print verbose output to stderr
-v,--verbose print verbose output to stderr
-w switch to wait mode
Read mode flags: