From 52c3dce589ccd25f748dfcf8fd3f4251f620aac4 Mon Sep 17 00:00:00 2001 From: Mark S Date: Tue, 1 Sep 2026 10:46:53 -0400 Subject: [PATCH 1/3] feat: add `skip` field-level attribute to `argh::FromArghs` --- argh_derive/src/args_info.rs | 10 +++-- argh_derive/src/lib.rs | 67 +++++++++++++++++++++++++++++----- argh_derive/src/parse_attrs.rs | 17 +++++---- 3 files changed, 74 insertions(+), 20 deletions(-) diff --git a/argh_derive/src/args_info.rs b/argh_derive/src/args_info.rs index 7e5a0dd..67cb560 100644 --- a/argh_derive/src/args_info.rs +++ b/argh_derive/src/args_info.rs @@ -2,6 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +use proc_macro2::{Span, TokenStream}; +use quote::{quote, quote_spanned, ToTokens}; +use syn::LitStr; + use crate::{ enum_only_single_field_unnamed_variants, errors::Errors, @@ -9,9 +13,6 @@ use crate::{ parse_attrs::{check_enum_type_attrs, FieldAttrs, FieldKind, TypeAttrs, VariantAttrs}, Optionality, StructField, }; -use proc_macro2::{Span, TokenStream}; -use quote::{quote, quote_spanned, ToTokens}; -use syn::LitStr; /// Implement the derive macro for ArgsInfo. pub(crate) fn impl_args_info(input: &syn::DeriveInput) -> TokenStream { @@ -67,6 +68,9 @@ fn impl_arg_info_struct( .iter() .filter_map(|field| { let attrs = FieldAttrs::parse(errors, field); + if attrs.skip { + return None; + } StructField::new(errors, field, attrs) }) .collect(); diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index ff439d0..08927f8 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -11,15 +11,15 @@ use syn::ext::IdentExt as _; /// For more thorough documentation, see the `argh` crate itself. extern crate proc_macro; -use { - crate::{ - errors::Errors, - parse_attrs::{check_long_name, FieldAttrs, FieldKind, TypeAttrs}, - }, - proc_macro2::{Span, TokenStream}, - quote::{quote, quote_spanned, ToTokens}, - std::{collections::HashMap, str::FromStr}, - syn::{spanned::Spanned, GenericArgument, LitStr, PathArguments, Type}, +use std::{collections::HashMap, str::FromStr}; + +use proc_macro2::{Span, TokenStream}; +use quote::{quote, quote_spanned, ToTokens}; +use syn::{spanned::Spanned, GenericArgument, LitStr, PathArguments, Type}; + +use crate::{ + errors::Errors, + parse_attrs::{check_long_name, FieldAttrs, FieldKind, TypeAttrs}, }; mod args_info; @@ -306,16 +306,21 @@ fn impl_from_args_struct( .iter() .filter_map(|field| { let attrs = FieldAttrs::parse(errors, field); + if attrs.skip { + return None; + } StructField::new(errors, field, attrs) }) .collect(); + let skipped = skipped_field_initializers(errors, ds); + ensure_unique_names(errors, &fields); ensure_only_last_positional_is_optional(errors, &fields); let impl_span = Span::call_site(); - let from_args_method = impl_from_args_struct_from_args(errors, type_attrs, &fields); + let from_args_method = impl_from_args_struct_from_args(errors, type_attrs, &fields, &skipped); let redact_arg_values_method = impl_from_args_struct_redact_arg_values(errors, type_attrs, &fields); @@ -341,6 +346,7 @@ fn impl_from_args_struct_from_args<'a>( errors: &Errors, type_attrs: &TypeAttrs, fields: &'a [StructField<'a>], + skipped: &[TokenStream], ) -> TokenStream { let init_fields = declare_local_storage_for_from_args_fields(fields); let unwrap_fields = unwrap_from_args_fields(fields); @@ -450,6 +456,7 @@ fn impl_from_args_struct_from_args<'a>( ::core::result::Result::Ok(Self { #( #unwrap_fields, )* + #( #skipped, )* }) } }; @@ -611,6 +618,46 @@ fn impl_from_args_struct_redact_arg_values<'a>( method_impl } +/// Generate `field_name: ` initializers for fields marked `#[argh(skip)]`. +/// +/// Skipped fields are omitted entirely from parsing and help output; they are populated +/// from the `default` expression if one is supplied, or `Default::default()` otherwise. +fn skipped_field_initializers(errors: &Errors, data_struct: &syn::DataStruct) -> Vec { + let mut initializers = vec![]; + + for field in data_struct.fields.iter() { + let FieldAttrs { skip, default, .. } = FieldAttrs::parse(errors, field); + + if !skip { + continue; + } + + let name = field.ident.as_ref().expect("missing ident for named field"); + + let value = if let Some(default) = default { + match TokenStream::from_str(&default.value()) { + Err(_) => { + errors.err(&default, "Invalid tokens: unable to lex `default` value"); + quote! { std::default::Default::default() } + } + Ok(tokens) => tokens + .into_iter() + .map(|mut tree| { + tree.set_span(default.span()); + tree + }) + .collect::(), + } + } else { + quote! { std::default::Default::default() } + }; + + initializers.push(quote! { #name: #value }); + } + + initializers +} + /// Ensures that only the last positional arg is non-required. fn ensure_only_last_positional_is_optional(errors: &Errors, fields: &[StructField<'_>]) { let mut first_non_required_span = None; diff --git a/argh_derive/src/parse_attrs.rs b/argh_derive/src/parse_attrs.rs index ca7c1c0..7551b0d 100644 --- a/argh_derive/src/parse_attrs.rs +++ b/argh_derive/src/parse_attrs.rs @@ -2,13 +2,12 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +use std::collections::hash_map::{Entry, HashMap}; + +use proc_macro2::Span; use syn::{parse::Parser, punctuated::Punctuated}; -use { - crate::errors::Errors, - proc_macro2::Span, - std::collections::hash_map::{Entry, HashMap}, -}; +use crate::errors::Errors; /// Attributes applied to a field of a `#![derive(FromArgs)]` struct. #[derive(Default)] @@ -23,6 +22,7 @@ pub struct FieldAttrs { pub greedy: Option, pub hidden_help: bool, pub usage: bool, + pub skip: bool, } /// The purpose of a particular field on a `#![derive(FromArgs)]` struct. @@ -129,13 +129,15 @@ impl FieldAttrs { this.hidden_help = true; } else if name.is_ident("usage") { this.usage = true; + } else if name.is_ident("skip") { + this.skip = true; } else { errors.err( &meta, concat!( "Invalid field-level `argh` attribute\n", "Expected one of: `arg_name`, `default`, `description`, `from_str_fn`, `greedy`, ", - "`long`, `option`, `short`, `subcommand`, `switch`, `hidden_help`, `usage`", + "`long`, `option`, `short`, `skip`, `subcommand`, `switch`, `hidden_help`, `usage`", ), ); } @@ -573,9 +575,10 @@ fn check_option_description(errors: &Errors, desc: &str, span: Span) { #[test] fn test_initialisms() { + use std::panic::Location; + use proc_macro2::TokenStream; use quote::ToTokens; - use std::panic::Location; #[track_caller] fn check(s: &str, should_succeed: bool) { From 9cc37a82bb8b1c2748dbd46e8aa7a36af2131c1f Mon Sep 17 00:00:00 2001 From: Mark S Date: Tue, 1 Sep 2026 10:47:15 -0400 Subject: [PATCH 2/3] chore(tests): add rests for `skip` attribute --- argh/tests/lib.rs | 86 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs index 9ef1774..fad1b3b 100644 --- a/argh/tests/lib.rs +++ b/argh/tests/lib.rs @@ -11,10 +11,9 @@ clippy::unwrap_in_result )] -use { - argh::{FromArgValue, FromArgs}, - std::fmt::Debug, -}; +use std::fmt::Debug; + +use argh::{FromArgValue, FromArgs}; #[test] fn basic_example() { @@ -40,8 +39,7 @@ fn basic_example() { #[test] fn generic_example() { - use std::fmt::Display; - use std::str::FromStr; + use std::{fmt::Display, str::FromStr}; #[derive(FromArgs, PartialEq, Debug)] /// Reach new heights. @@ -468,6 +466,82 @@ fn assert_error(args: &[&str], err_msg: &str) { e.status.expect_err("error had a positive status"); } +mod skip { + use super::*; + + #[derive(Debug, Default, PartialEq)] + struct DefaultableValue { + inner: bool, + } + + #[derive(Debug, PartialEq, argh::FromArgs)] + /// SkipTest + struct WithSkipDefaultImpl { + #[argh(switch)] + /// foo bar baz + flag: bool, + /// skipped, falls back to `Default::default()` + #[argh(skip)] + skipped: DefaultableValue, + } + + #[derive(Debug, PartialEq, argh::FromArgs)] + /// SkipTest + struct WithSkipExplicitDefault { + #[argh(option)] + /// foo bar baz + option: usize, + /// skipped, uses the provided `default` + #[argh(skip, default = "DefaultableValue { inner: true }")] + skipped: DefaultableValue, + } + + #[test] + fn skip_uses_default_impl() { + assert_output( + &["--flag"], + WithSkipDefaultImpl { flag: true, skipped: DefaultableValue { inner: false } }, + ); + } + + #[test] + fn skip_honors_explicit_default() { + assert_output( + &["--option", "5"], + WithSkipExplicitDefault { option: 5, skipped: DefaultableValue { inner: true } }, + ); + } + + #[test] + fn skip_is_not_parsed_as_an_option() { + #[cfg(not(feature = "fuzzy_search"))] + let expected = "Unrecognized argument: --skipped\n"; + + #[cfg(feature = "fuzzy_search")] + let expected = "Unrecognized argument: \"--skipped\". Did you mean \"--option\"?\n"; + + assert_error::( + &["--option", "5", "--skipped", "whatever"], + expected, + ); + } + + #[test] + #[cfg(feature = "help")] + fn skip_is_omitted_from_help() { + assert_help_string::( + r#"Usage: test_arg_0 --option