Skip to content

Commit a47278b

Browse files
committed
handle commands
1 parent d10a3cf commit a47278b

File tree

3 files changed

+50
-109
lines changed

3 files changed

+50
-109
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "clap-config-file"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2021"
55
description = "A proc macro for adding config file support to clap"
66
license = "MIT"

examples/advanced/src/main.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use clap_config_file::ClapConfigFile;
88
/// - `#[cli_only]`: Field only comes from CLI.
99
/// - `#[multi_value_behavior]`: Controls how `Vec` fields merge between config and CLI.
1010
#[derive(ClapConfigFile, Parser, Debug)]
11-
#[clap(trailing_var_arg = true)]
11+
#[command(trailing_var_arg = true, allow_external_subcommands = true)]
1212
struct AdvancedConfig {
1313
/// Example of a field that can come from both CLI and config, with CLI taking precedence.
1414
///
@@ -51,9 +51,10 @@ struct AdvancedConfig {
5151
#[multi_value_behavior(Overwrite)]
5252
pub overwrite_list: Vec<String>,
5353

54-
/// Any additional commands or arguments
55-
#[clap(last = true)]
56-
pub commands: Vec<String>,
54+
/// A captrue all for additional commands or arguments
55+
#[cli_only]
56+
#[positional]
57+
commands: Vec<String>,
5758
}
5859

5960
fn main() {
@@ -64,11 +65,14 @@ fn main() {
6465
println!("Final merged config:\n{:#?}", config);
6566

6667
// Example usage:
67-
// $ cargo run --example advanced -- --port 3001 --overwrite-list CLI_item additional_command
68+
// $ cargo run --example advanced -- --port 3001 --overwrite-list CLI_item command1 command2
6869
//
6970
// That overrides the port and the overwrite_list. If advanced-config.yaml
7071
// has `database_url` etc., that remains unless overridden or suppressed.
72+
// additional_command is passed to the program as an external subcommand.
7173
if !config.commands.is_empty() {
72-
println!("\nReceived commands: {:?}", config.commands);
74+
for command in config.commands {
75+
println!("\nReceived commands: {:?}", command);
76+
}
7377
}
7478
}

src/lib.rs

Lines changed: 39 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -7,53 +7,16 @@ use syn::{
77
};
88

99
/// A derive macro that enables structs to handle configuration from both CLI arguments and config files.
10-
///
11-
/// This macro generates the necessary code to:
12-
/// 1. Parse command-line arguments using clap
13-
/// 2. Load and parse config files (YAML/JSON/TOML)
14-
/// 3. Merge the configurations with CLI taking precedence
15-
///
16-
/// # Attributes
17-
/// - `#[cli_only]`: Field only appears as a CLI argument
18-
/// - `#[config_only]`: Field only appears in config file
19-
/// - `#[cli_and_config]`: Field appears in both (CLI overrides config)
20-
/// - `#[config_arg(name = "x", short = 'x', long = "xxx", default_value = "y")]`: Customize CLI argument
21-
/// - `#[multi_value_behavior(extend/overwrite)]`: For Vec fields, control merge behavior
22-
///
23-
/// # Example
24-
/// ```rust
25-
/// use clap_config_file::ClapConfigFile;
26-
///
27-
/// #[derive(ClapConfigFile)]
28-
/// struct Config {
29-
/// // CLI-only flag with custom name
30-
/// #[cli_only]
31-
/// #[config_arg(name = "verbose", short = 'v')]
32-
/// verbose: bool,
33-
///
34-
/// // Config file only setting
35-
/// #[config_only]
36-
/// database_url: String,
37-
///
38-
/// // Available in both with CLI override
39-
/// #[cli_and_config]
40-
/// #[config_arg(name = "port", default_value = "8080")]
41-
/// port: u16,
42-
///
43-
/// // Vec field that extends values from both sources
44-
/// #[cli_and_config]
45-
/// #[multi_value_behavior(extend)]
46-
/// tags: Vec<String>,
47-
/// }
48-
/// ```
10+
/// ...
4911
#[proc_macro_derive(
5012
ClapConfigFile,
5113
attributes(
5214
cli_only,
5315
config_only,
5416
cli_and_config,
5517
config_arg,
56-
multi_value_behavior
18+
multi_value_behavior,
19+
positional
5720
)
5821
)]
5922
pub fn derive_clap_config_file(input: TokenStream) -> TokenStream {
@@ -64,35 +27,29 @@ pub fn derive_clap_config_file(input: TokenStream) -> TokenStream {
6427
}
6528
}
6629

67-
/// Defines how a field's value can be provided, controlling whether it's available
68-
/// via CLI arguments, config file, or both.
6930
#[derive(Debug, Clone, Copy)]
7031
enum FieldAvailability {
7132
CliOnly,
7233
ConfigOnly,
7334
CliAndConfig,
7435
}
7536

76-
/// Controls how Vec fields merge values when present in both CLI and config.
77-
/// - Extend: Append CLI values to config values
78-
/// - Overwrite: Replace config values with CLI values if present
7937
#[derive(Debug, Clone, Default)]
8038
enum MultiValueBehavior {
8139
#[default]
8240
Extend,
8341
Overwrite,
8442
}
8543

86-
/// Holds CLI argument customization options specified via #[config_arg(...)]
8744
#[derive(Debug, Default, Clone)]
8845
struct ArgAttributes {
8946
name: Option<String>,
9047
short: Option<char>,
9148
long: Option<String>,
9249
default_value: Option<String>,
50+
is_positional: bool, // <--- new flag
9351
}
9452

95-
/// Aggregates all metadata about a struct field needed for code generation
9653
#[derive(Debug, Clone)]
9754
struct FieldInfo {
9855
availability: FieldAvailability,
@@ -102,12 +59,6 @@ struct FieldInfo {
10259
ty: syn::Type,
10360
}
10461

105-
/// Main implementation function that generates the expanded code for the derive macro.
106-
/// This includes:
107-
/// - Hidden CLI struct with clap attributes
108-
/// - Hidden config struct with serde attributes
109-
/// - Implementation of parsing and merging logic
110-
/// - Helper functions for config file discovery and loading
11162
fn build_impl(ast: &DeriveInput) -> syn::Result<TokenStream2> {
11263
let struct_name = &ast.ident;
11364
let generics = &ast.generics;
@@ -131,13 +82,12 @@ fn build_impl(ast: &DeriveInput) -> syn::Result<TokenStream2> {
13182
}
13283
};
13384

134-
// Parse user's field attributes
13585
let parsed_fields: Vec<FieldInfo> = fields
13686
.iter()
137-
.map(|field| parse_one_field(field))
87+
.map(|f| parse_one_field(f))
13888
.collect::<Result<_, _>>()?;
13989

140-
// For each field, generate the CLI struct token, the config struct token, and the final merge expression
90+
// Build up the hidden CLI struct fields, config struct fields, and merge expressions
14191
let mut cli_struct_fields = Vec::new();
14292
let mut cfg_struct_fields = Vec::new();
14393
let mut merge_stmts = Vec::new();
@@ -148,22 +98,20 @@ fn build_impl(ast: &DeriveInput) -> syn::Result<TokenStream2> {
14898
let cfg_ts_opt = generate_config_field_tokens(pf);
14999
let merge_expr = generate_merge_expr(pf);
150100

151-
// If we got Some(...) token stream, push it into the vectors
152101
if let Some(ts) = cli_ts_opt {
153102
cli_struct_fields.push(ts);
154103
}
155104
if let Some(ts) = cfg_ts_opt {
156105
cfg_struct_fields.push(ts);
157106
}
158107

159-
// Always push the final merge line (the user's struct field)
160108
let field_name = &pf.ident;
161109
merge_stmts.push(quote! {
162110
#field_name: #merge_expr
163111
});
164112
}
165113

166-
// Add our special meta fields (for --no-config, etc.) to the CLI struct
114+
// Add special config fields to the CLI struct
167115
cli_struct_fields.push(quote! {
168116
/// If true, skip reading any config file
169117
#[arg(long = "no-config", default_value_t = false)]
@@ -178,22 +126,19 @@ fn build_impl(ast: &DeriveInput) -> syn::Result<TokenStream2> {
178126
pub __raw_config: Option<String>,
179127
});
180128

181-
// Construct hidden struct names
182129
let cli_struct_ident = syn::Ident::new(&format!("__{}_Cli", struct_name), Span::call_site());
183130
let cfg_struct_ident = syn::Ident::new(&format!("__{}_Cfg", struct_name), Span::call_site());
184-
185131
let num_fields = parsed_fields.len();
186132
let field_names = parsed_fields.iter().map(|pf| &pf.ident);
187133

188-
// Build final output
134+
// We add #[command(name="advanced")] below. Adjust or remove as needed.
189135
let expanded = quote! {
190-
// Hidden CLI struct
191136
#[derive(::clap::Parser, Debug, Default)]
137+
#[command(name = "advanced")]
192138
struct #cli_struct_ident {
193139
#(#cli_struct_fields),*
194140
}
195141

196-
// Hidden config struct
197142
#[derive(serde::Serialize, serde::Deserialize, Debug, Default)]
198143
struct #cfg_struct_ident {
199144
#(#cfg_struct_fields),*
@@ -389,13 +334,11 @@ fn build_impl(ast: &DeriveInput) -> syn::Result<TokenStream2> {
389334
Ok(expanded)
390335
}
391336

392-
/// Parses all attributes on a field to determine its configuration behavior
393337
fn parse_one_field(field: &Field) -> syn::Result<FieldInfo> {
394338
let mut availability = None;
395339
let mut mv_behavior = MultiValueBehavior::default();
396340
let mut arg_attrs = ArgAttributes::default();
397341

398-
// Check each attribute for relevant markers
399342
for attr in &field.attrs {
400343
let path_ident = match attr.path().get_ident() {
401344
Some(i) => i.to_string(),
@@ -415,6 +358,10 @@ fn parse_one_field(field: &Field) -> syn::Result<FieldInfo> {
415358
ensure_avail_none(&availability, attr)?;
416359
availability = Some(FieldAvailability::CliAndConfig);
417360
}
361+
"positional" => {
362+
// We'll mark this field as positional, so we skip generating `long/short`
363+
arg_attrs.is_positional = true;
364+
}
418365
"config_arg" => {
419366
let meta = attr.meta.require_list()?;
420367
for nested in meta.parse_args_with(
@@ -524,25 +471,17 @@ fn generate_cli_field_tokens(pf: &FieldInfo) -> Option<TokenStream2> {
524471
short,
525472
long,
526473
default_value,
474+
is_positional,
527475
} = &pf.arg_attrs;
528476

529-
// Create owned String here instead of referencing temporary
477+
// name fallback for 'long' unless it's positional
530478
let name_str = name.clone().unwrap_or_else(|| field_name.to_string());
479+
let default_attr = default_value
480+
.as_ref()
481+
.map(|dv| quote!(default_value = #dv, ))
482+
.unwrap_or_default();
531483

532-
let short_attr = short.map(|c| quote!(short = #c,));
533-
let long_attr = if let Some(ref l) = long {
534-
quote!(long = #l,)
535-
} else {
536-
quote!(long = #name_str,)
537-
};
538-
539-
let default_attr = if let Some(dv) = default_value {
540-
quote!(default_value = #dv,)
541-
} else {
542-
quote!()
543-
};
544-
545-
// If it's bool, use Option<bool> + ArgAction::SetTrue
484+
// If bool, we parse it via Option<bool> + ArgAction::SetTrue
546485
let (final_ty, action) = if is_bool_type(ty) {
547486
(
548487
quote!(Option<bool>),
@@ -552,26 +491,34 @@ fn generate_cli_field_tokens(pf: &FieldInfo) -> Option<TokenStream2> {
552491
(quote!(Option<#ty>), quote!())
553492
};
554493

555-
Some(quote! {
556-
#[arg(#short_attr #long_attr #default_attr #action)]
557-
pub #field_name: #final_ty
558-
})
494+
// If it's marked #[positional], skip long/short and let Clap treat it as a positional.
495+
if *is_positional {
496+
Some(quote! {
497+
#[arg(#action #default_attr)]
498+
pub #field_name: #final_ty
499+
})
500+
} else {
501+
let short_attr = short.map(|c| quote!(short = #c,));
502+
let long_attr = if let Some(ref l) = long {
503+
quote!(long = #l,)
504+
} else {
505+
quote!(long = #name_str,)
506+
};
507+
Some(quote! {
508+
#[arg(#short_attr #long_attr #default_attr #action)]
509+
pub #field_name: #final_ty
510+
})
511+
}
559512
}
560513
}
561514
}
562515

563-
/// Generates the field definition for the hidden config struct.
564-
/// All fields are marked with #[serde(default)] to handle missing values.
565516
fn generate_config_field_tokens(pf: &FieldInfo) -> Option<TokenStream2> {
566517
match pf.availability {
567-
FieldAvailability::CliOnly => {
568-
// No config field for CLI-only
569-
None
570-
}
518+
FieldAvailability::CliOnly => None,
571519
FieldAvailability::ConfigOnly | FieldAvailability::CliAndConfig => {
572520
let field_name = &pf.ident;
573521
let ty = &pf.ty;
574-
// We just do `#[serde(default)] pub field_name: T`
575522
Some(quote! {
576523
#[serde(default)]
577524
pub #field_name: #ty
@@ -580,22 +527,15 @@ fn generate_config_field_tokens(pf: &FieldInfo) -> Option<TokenStream2> {
580527
}
581528
}
582529

583-
/// Generates the expression that combines CLI and config values for a field.
584-
/// The merge strategy depends on:
585-
/// - Field availability (cli_only, config_only, cli_and_config)
586-
/// - Whether the field is a Vec (uses multi_value_behavior)
587-
/// - For non-Vec fields, CLI takes precedence if present
588530
fn generate_merge_expr(pf: &FieldInfo) -> TokenStream2 {
589531
let field_name = &pf.ident;
590532
match pf.availability {
591533
FieldAvailability::CliOnly => {
592-
// final = cli.field.unwrap_or_default()
593534
quote! {
594535
cli.#field_name.unwrap_or_default()
595536
}
596537
}
597538
FieldAvailability::ConfigOnly => {
598-
// final = cfg.field
599539
quote! {
600540
cfg.#field_name
601541
}
@@ -622,7 +562,6 @@ fn generate_merge_expr(pf: &FieldInfo) -> TokenStream2 {
622562
},
623563
}
624564
} else {
625-
// normal scalar
626565
quote! {
627566
cli.#field_name.unwrap_or(cfg.#field_name)
628567
}
@@ -631,7 +570,6 @@ fn generate_merge_expr(pf: &FieldInfo) -> TokenStream2 {
631570
}
632571
}
633572

634-
/// Determines if a type is a bool by checking its path segments
635573
fn is_bool_type(ty: &syn::Type) -> bool {
636574
if let syn::Type::Path(tp) = ty {
637575
if let Some(seg) = tp.path.segments.last() {
@@ -641,7 +579,6 @@ fn is_bool_type(ty: &syn::Type) -> bool {
641579
false
642580
}
643581

644-
/// Determines if a type is a Vec by checking its path segments
645582
fn is_vec_type(ty: &syn::Type) -> bool {
646583
if let syn::Type::Path(tp) = ty {
647584
if let Some(seg) = tp.path.segments.last() {

0 commit comments

Comments
 (0)