Skip to main content

facet_args/
format.rs

1use crate::{
2    arg::ArgType,
3    error::{ArgsError, ArgsErrorKind, ArgsErrorWithInput, get_variants_from_shape},
4    help::{HelpConfig, generate_help_for_shape},
5    is_counted_field, is_supported_counted_type,
6    span::Span,
7};
8use alloc::collections::BTreeMap;
9use facet_core::{Def, EnumType, Facet, Field, Shape, StructKind, Type, UserType, Variant};
10use facet_reflect::{HeapValue, Partial};
11use heck::{ToKebabCase, ToSnakeCase};
12
13/// Check if the given argument is a help flag
14fn is_help_flag(arg: &str) -> bool {
15    matches!(arg, "-h" | "--help" | "-help" | "/?")
16}
17
18/// Parse command line arguments provided by std::env::args() into a Facet-compatible type
19pub fn from_std_args<T: Facet<'static>>() -> Result<T, ArgsErrorWithInput> {
20    let args = std::env::args().skip(1).collect::<Vec<String>>();
21    let args_str: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
22    from_slice(&args_str[..])
23}
24
25/// Parse command line arguments into a Facet-compatible type
26pub fn from_slice<'input, T: Facet<'static>>(
27    args: &'input [&'input str],
28) -> Result<T, ArgsErrorWithInput> {
29    from_slice_with_config(args, &HelpConfig::default())
30}
31
32/// Parse command line arguments with custom help configuration
33pub fn from_slice_with_config<'input, T: Facet<'static>>(
34    args: &'input [&'input str],
35    help_config: &HelpConfig,
36) -> Result<T, ArgsErrorWithInput> {
37    // Check for help flag as the only argument (or first argument for simplicity)
38    if let Some(first_arg) = args.first()
39        && is_help_flag(first_arg)
40    {
41        let help_text = generate_help_for_shape(T::SHAPE, help_config);
42        let span = Span::new(0, first_arg.len());
43        return Err(ArgsErrorWithInput {
44            inner: ArgsError::new(ArgsErrorKind::HelpRequested { help_text }, span),
45            flattened_args: args.join(" "),
46        });
47    }
48
49    let mut cx = Context::new(args, T::SHAPE);
50    let hv = cx.work_add_input()?;
51
52    // TODO: proper error handling
53    Ok(hv.materialize::<T>().unwrap())
54}
55
56struct Context<'input> {
57    /// The shape we're building
58    shape: &'static Shape,
59
60    /// Input arguments (already tokenized)
61    args: &'input [&'input str],
62
63    /// Argument we're currently parsing
64    index: usize,
65
66    /// Flips to true after `--`, which makes us only look for positional args
67    positional_only: bool,
68
69    /// Index of every argument in `flattened_args`
70    arg_indices: Vec<usize>,
71
72    /// Essentially `input.join(" ")`
73    flattened_args: String,
74
75    /// Stack of counted field maps (one per struct/variant nesting level).
76    /// Maps field_index -> count for fields with `args::counted`.
77    counted_stack: Vec<BTreeMap<usize, u64>>,
78}
79
80impl<'input> Context<'input> {
81    fn new(args: &'input [&'input str], shape: &'static Shape) -> Self {
82        let mut arg_indices = vec![];
83        let mut flattened_args = String::new();
84
85        for arg in args {
86            arg_indices.push(flattened_args.len());
87            flattened_args.push_str(arg);
88            flattened_args.push(' ');
89        }
90        tracing::trace!("flattened args: {flattened_args:?}");
91        tracing::trace!("arg_indices: {arg_indices:?}");
92
93        Self {
94            shape,
95            args,
96            index: 0,
97            positional_only: false,
98            arg_indices,
99            flattened_args,
100            counted_stack: Vec::new(),
101        }
102    }
103
104    fn push_counted_scope(&mut self) {
105        self.counted_stack.push(BTreeMap::new());
106    }
107
108    fn pop_and_apply_counted_fields(
109        &mut self,
110        mut p: Partial<'static>,
111    ) -> Result<Partial<'static>, ArgsErrorKind> {
112        let counts = self.counted_stack.pop().unwrap_or_default();
113        for (field_index, count) in counts {
114            p = p.begin_nth_field(field_index)?;
115            p = p.parse_from_str(&count.to_string())?;
116            p = p.end()?;
117        }
118        Ok(p)
119    }
120
121    fn increment_counted(&mut self, field_index: usize) {
122        if let Some(counts) = self.counted_stack.last_mut() {
123            let count = counts.entry(field_index).or_insert(0);
124            *count = count.saturating_add(1);
125        }
126    }
127
128    fn try_handle_counted_long_flag(&mut self, field: &'static Field, field_index: usize) -> bool {
129        if is_counted_field(field) && is_supported_counted_type(field.shape()) {
130            self.increment_counted(field_index);
131            self.index += 1;
132            true
133        } else {
134            false
135        }
136    }
137
138    /// Returns fields for the current shape, errors out if it's not a struct
139    fn fields(&self, p: &Partial<'static>) -> Result<&'static [Field], ArgsErrorKind> {
140        let shape = p.shape();
141        match &shape.ty {
142            Type::User(UserType::Struct(struct_type)) => Ok(struct_type.fields),
143            _ => Err(ArgsErrorKind::NoFields { shape }),
144        }
145    }
146
147    /// Once we have found the struct field that corresponds to a `--long` or `-s` short flag,
148    /// this is where we toggle something on, look for a value, etc.
149    fn handle_field(
150        &mut self,
151        p: Partial<'static>,
152        field_index: usize,
153        value: Option<SplitToken<'input>>,
154    ) -> Result<Partial<'static>, ArgsErrorKind> {
155        tracing::trace!("Handling field at index {field_index}");
156
157        let mut p = p.begin_nth_field(field_index)?;
158
159        tracing::trace!("After begin_field, shape is {}", p.shape());
160        if p.shape().is_shape(bool::SHAPE) {
161            // For bool flags, check if a value was provided via `=`
162            let bool_value = if let Some(value) = value {
163                // Parse the value as a boolean
164                match value.s.to_lowercase().as_str() {
165                    "true" | "yes" | "1" | "on" => true,
166                    "false" | "no" | "0" | "off" => false,
167                    "" => true, // `--flag=` means true
168                    other => {
169                        tracing::warn!("Unknown boolean value '{other}', treating as true");
170                        true
171                    }
172                }
173            } else {
174                // No value provided, presence of flag means true
175                true
176            };
177            tracing::trace!("Flag is boolean, setting it to {bool_value}");
178            p = p.set(bool_value)?;
179
180            self.index += 1;
181        } else {
182            tracing::trace!("Flag isn't boolean, expecting a {} value", p.shape());
183
184            if let Some(value) = value {
185                p = self.handle_value(p, value.s)?;
186            } else {
187                if self.index + 1 >= self.args.len() {
188                    return Err(ArgsErrorKind::ExpectedValueGotEof { shape: p.shape() });
189                }
190                let value = self.args[self.index + 1];
191
192                self.index += 1;
193                p = self.handle_value(p, value)?;
194            }
195
196            self.index += 1;
197        }
198
199        p = p.end()?;
200
201        Ok(p)
202    }
203
204    fn handle_value(
205        &mut self,
206        p: Partial<'static>,
207        value: &'input str,
208    ) -> Result<Partial<'static>, ArgsErrorKind> {
209        // Check if this is a subcommand field by looking at the current shape
210        // If it's an enum and we're trying to parse it from a string, it's likely a subcommand
211        if let Type::User(UserType::Enum(_)) = p.shape().ty {
212            // This is an enum field being set via a flag value, which is likely a mistake
213            // for subcommand fields. Provide a helpful error.
214            return Err(ArgsErrorKind::ReflectError(
215                facet_reflect::ReflectError::OperationFailed {
216                    shape: p.shape(),
217                    operation: "Subcommands must be provided as positional arguments, not as flag values. Use the subcommand name directly instead of --flag <value>.",
218                },
219            ));
220        }
221
222        let p = match p.shape().def {
223            Def::List(_) => {
224                // if it's a list, then we'll want to initialize the list first and push to it
225                let mut p = p.init_list()?;
226                p = p.begin_list_item()?;
227                p = p.parse_from_str(value)?;
228                p.end()?
229            }
230            Def::Option(_) => {
231                // if it's an Option<T>, wrap the value in Some
232                let mut p = p.begin_some()?;
233                p = p.parse_from_str(value)?;
234                p.end()?
235            }
236            _ => {
237                // TODO: this surely won't be enough eventually
238                p.parse_from_str(value)?
239            }
240        };
241
242        Ok(p)
243    }
244
245    fn work_add_input(&mut self) -> Result<HeapValue<'static>, ArgsErrorWithInput> {
246        self.work().map_err(|e| ArgsErrorWithInput {
247            inner: e,
248            flattened_args: self.flattened_args.clone(),
249        })
250    }
251
252    /// Forward to `work_inner`, converts `ArgsErrorKind` to `ArgsError` (with span)
253    fn work(&mut self) -> Result<HeapValue<'static>, ArgsError> {
254        self.work_inner().map_err(|kind| {
255            // Use precise span if the error kind has one, otherwise use the whole arg span
256            let span = kind.precise_span().unwrap_or_else(|| {
257                if self.index >= self.args.len() {
258                    Span::new(self.flattened_args.len(), 0)
259                } else {
260                    let arg = self.args[self.index];
261                    let index = self.arg_indices[self.index];
262                    Span::new(index, arg.len())
263                }
264            });
265            ArgsError::new(kind, span)
266        })
267    }
268
269    #[allow(unsafe_code)]
270    fn work_inner(&mut self) -> Result<HeapValue<'static>, ArgsErrorKind> {
271        // SAFETY: self.shape comes from T::SHAPE where T: Facet<'static>,
272        // which guarantees the shape accurately describes the type.
273        let p = unsafe { Partial::alloc_shape(self.shape) }?;
274
275        // Only parse structs at the top level
276        // Enums should only be parsed as subcommands when explicitly marked with args::subcommand attribute
277        match self.shape.ty {
278            Type::User(UserType::Struct(_)) => self.parse_struct(p),
279            Type::User(UserType::Enum(_)) => {
280                // Enum at top level without explicit subcommand attribute is not supported
281                Err(ArgsErrorKind::ReflectError(
282                    facet_reflect::ReflectError::OperationFailed {
283                        shape: self.shape,
284                        operation: "Top-level enums must be wrapped in a struct with #[facet(args::subcommand)] attribute to be used as subcommands.",
285                    },
286                ))
287            }
288            _ => Err(ArgsErrorKind::NoFields { shape: self.shape }),
289        }
290    }
291
292    /// Parse a struct type
293    fn parse_struct(
294        &mut self,
295        mut p: Partial<'static>,
296    ) -> Result<HeapValue<'static>, ArgsErrorKind> {
297        self.push_counted_scope();
298
299        while self.args.len() > self.index {
300            let arg = self.args[self.index];
301            let arg_span = Span::new(self.arg_indices[self.index], arg.len());
302            let at = if self.positional_only {
303                ArgType::Positional
304            } else {
305                ArgType::parse(arg)
306            };
307            tracing::trace!("Parsed {at:?}");
308
309            match at {
310                ArgType::DoubleDash => {
311                    self.positional_only = true;
312                    self.index += 1;
313                }
314                ArgType::LongFlag(flag) => {
315                    // Reject flags that start with `-` (e.g., `---verbose`)
316                    if flag.starts_with('-') {
317                        let fields = self.fields(&p)?;
318                        return Err(ArgsErrorKind::UnknownLongFlag {
319                            flag: flag.to_string(),
320                            fields,
321                        });
322                    }
323
324                    let flag_span = Span::new(arg_span.start + 2, arg_span.len - 2);
325                    match split(flag, flag_span) {
326                        Some(tokens) => {
327                            // We have something like `--key=value`
328                            let mut tokens = tokens.into_iter();
329                            let Some(key) = tokens.next() else {
330                                unreachable!()
331                            };
332                            let Some(value) = tokens.next() else {
333                                unreachable!()
334                            };
335
336                            let flag = key.s;
337                            tracing::trace!("Looking up long flag {flag}");
338                            let fields = self.fields(&p)?;
339                            let Some(field_index) =
340                                find_field_index_by_effective_name(fields, flag)
341                            else {
342                                return Err(ArgsErrorKind::UnknownLongFlag {
343                                    flag: flag.to_string(),
344                                    fields,
345                                });
346                            };
347                            p = self.handle_field(p, field_index, Some(value))?;
348                        }
349                        None => {
350                            tracing::trace!("Looking up long flag {flag}");
351                            let fields = self.fields(&p)?;
352                            let Some(field_index) =
353                                find_field_index_by_effective_name(fields, flag)
354                            else {
355                                return Err(ArgsErrorKind::UnknownLongFlag {
356                                    flag: flag.to_string(),
357                                    fields,
358                                });
359                            };
360                            if !self.try_handle_counted_long_flag(&fields[field_index], field_index)
361                            {
362                                p = self.handle_field(p, field_index, None)?;
363                            }
364                        }
365                    }
366                }
367                ArgType::ShortFlag(flag) => {
368                    let flag_span = Span::new(arg_span.start + 1, arg_span.len - 1);
369                    match split(flag, flag_span) {
370                        Some(tokens) => {
371                            // We have something like `-k=value`
372                            let mut tokens = tokens.into_iter();
373                            let Some(key) = tokens.next() else {
374                                unreachable!()
375                            };
376                            let Some(value) = tokens.next() else {
377                                unreachable!()
378                            };
379
380                            let short_char = key.s;
381                            tracing::trace!("Looking up short flag {short_char}");
382                            let fields = self.fields(&p)?;
383                            let Some(field_index) =
384                                find_field_index_with_short_char(fields, short_char)
385                            else {
386                                return Err(ArgsErrorKind::UnknownShortFlag {
387                                    flag: short_char.to_string(),
388                                    fields,
389                                    precise_span: None, // Not chained, use default arg span
390                                });
391                            };
392                            p = self.handle_field(p, field_index, Some(value))?;
393                        }
394                        None => {
395                            // No `=` in the flag. Use helper to handle chaining.
396                            let fields = self.fields(&p)?;
397                            p = self.process_short_flag(p, flag, flag_span, fields)?;
398                        }
399                    }
400                }
401                ArgType::Positional => {
402                    let fields = self.fields(&p)?;
403
404                    // First, check if there's a subcommand field that hasn't been set yet
405                    if let Some((field_index, field)) = find_subcommand_field(fields)
406                        && !p.is_field_set(field_index)?
407                    {
408                        p = self.handle_subcommand_field(p, field_index, field)?;
409                        continue;
410                    }
411
412                    // Otherwise, look for a positional field
413                    let mut chosen_field_index: Option<usize> = None;
414
415                    for (field_index, field) in fields.iter().enumerate() {
416                        let is_positional = field.has_attr(Some("args"), "positional");
417                        if !is_positional {
418                            continue;
419                        }
420
421                        // we've found a positional field. if it's a list, then we're done: every
422                        // positional argument will just be pushed to it.
423                        if matches!(field.shape().def, Def::List(_list_def)) {
424                            // cool, keep going
425                        } else if p.is_field_set(field_index)? {
426                            // field is already set, continue
427                            continue;
428                        }
429
430                        tracing::trace!("found field, it's not a list {field:?}");
431                        chosen_field_index = Some(field_index);
432                        break;
433                    }
434
435                    let Some(chosen_field_index) = chosen_field_index else {
436                        return Err(ArgsErrorKind::UnexpectedPositionalArgument { fields });
437                    };
438
439                    p = p.begin_nth_field(chosen_field_index)?;
440
441                    let value = self.args[self.index];
442
443                    // Check if this is an enum field without the subcommand attribute
444                    if let Type::User(UserType::Enum(_)) = fields[chosen_field_index].shape().ty
445                        && !fields[chosen_field_index].has_attr(Some("args"), "subcommand")
446                    {
447                        return Err(ArgsErrorKind::EnumWithoutSubcommandAttribute {
448                            field: &fields[chosen_field_index],
449                        });
450                    }
451
452                    p = self.handle_value(p, value)?;
453
454                    p = p.end()?;
455                    self.index += 1;
456                }
457                ArgType::None => todo!(),
458            }
459        }
460
461        p = self.pop_and_apply_counted_fields(p)?;
462        p = self.finalize_struct(p)?;
463
464        Ok(p.build()?)
465    }
466
467    /// Parse fields of an enum variant (similar to struct parsing)
468    fn parse_variant_fields(
469        &mut self,
470        mut p: Partial<'static>,
471        variant: &'static Variant,
472    ) -> Result<Partial<'static>, ArgsErrorKind> {
473        let fields = variant.data.fields;
474
475        // Flatten newtype tuple variants: `Build(BuildArgs)` -> parse BuildArgs fields directly
476        if variant.data.kind == StructKind::TupleStruct && fields.len() == 1 {
477            let inner_shape = fields[0].shape();
478            if let Type::User(UserType::Struct(struct_type)) = inner_shape.ty {
479                p = p.begin_nth_field(0)?;
480                p = self.parse_fields_loop(p, struct_type.fields)?;
481                p = self.pop_and_apply_counted_fields(p)?;
482                p = self.finalize_variant_fields(p, struct_type.fields)?;
483                p = p.end()?;
484                return Ok(p);
485            }
486        }
487
488        self.push_counted_scope();
489
490        while self.args.len() > self.index {
491            let arg = self.args[self.index];
492            let arg_span = Span::new(self.arg_indices[self.index], arg.len());
493            let at = if self.positional_only {
494                ArgType::Positional
495            } else {
496                ArgType::parse(arg)
497            };
498            tracing::trace!("Parsing variant field, arg: {at:?}");
499
500            match at {
501                ArgType::DoubleDash => {
502                    self.positional_only = true;
503                    self.index += 1;
504                }
505                ArgType::LongFlag(flag) => {
506                    // Reject flags that start with `-` (e.g., `---verbose`)
507                    if flag.starts_with('-') {
508                        return Err(ArgsErrorKind::UnknownLongFlag {
509                            flag: flag.to_string(),
510                            fields,
511                        });
512                    }
513
514                    let flag_span = Span::new(arg_span.start + 2, arg_span.len - 2);
515                    match split(flag, flag_span) {
516                        Some(tokens) => {
517                            let mut tokens = tokens.into_iter();
518                            let key = tokens.next().unwrap();
519                            let value = tokens.next().unwrap();
520
521                            let flag = key.s;
522                            tracing::trace!("Looking up long flag {flag} in variant");
523                            let Some(field_index) =
524                                find_field_index_by_effective_name(fields, flag)
525                            else {
526                                return Err(ArgsErrorKind::UnknownLongFlag {
527                                    flag: flag.to_string(),
528                                    fields,
529                                });
530                            };
531                            p = self.handle_field(p, field_index, Some(value))?;
532                        }
533                        None => {
534                            tracing::trace!("Looking up long flag {flag} in variant");
535                            let Some(field_index) =
536                                find_field_index_by_effective_name(fields, flag)
537                            else {
538                                return Err(ArgsErrorKind::UnknownLongFlag {
539                                    flag: flag.to_string(),
540                                    fields,
541                                });
542                            };
543                            if !self.try_handle_counted_long_flag(&fields[field_index], field_index)
544                            {
545                                p = self.handle_field(p, field_index, None)?;
546                            }
547                        }
548                    }
549                }
550                ArgType::ShortFlag(flag) => {
551                    let flag_span = Span::new(arg_span.start + 1, arg_span.len - 1);
552                    match split(flag, flag_span) {
553                        Some(tokens) => {
554                            let mut tokens = tokens.into_iter();
555                            let key = tokens.next().unwrap();
556                            let value = tokens.next().unwrap();
557
558                            let short_char = key.s;
559                            tracing::trace!("Looking up short flag {short_char} in variant");
560                            let Some(field_index) =
561                                find_field_index_with_short_char(fields, short_char)
562                            else {
563                                return Err(ArgsErrorKind::UnknownShortFlag {
564                                    flag: short_char.to_string(),
565                                    fields,
566                                    precise_span: None, // Not chained, use default arg span
567                                });
568                            };
569                            p = self.handle_field(p, field_index, Some(value))?;
570                        }
571                        None => {
572                            // No `=` in the flag. Use helper to handle chaining.
573                            p = self.process_short_flag(p, flag, flag_span, fields)?;
574                        }
575                    }
576                }
577                ArgType::Positional => {
578                    // Check for subcommand field first (for nested subcommands)
579                    if let Some((field_index, field)) = find_subcommand_field(fields)
580                        && !p.is_field_set(field_index)?
581                    {
582                        p = self.handle_subcommand_field(p, field_index, field)?;
583                        continue;
584                    }
585
586                    // Look for positional field
587                    let mut chosen_field_index: Option<usize> = None;
588
589                    for (field_index, field) in fields.iter().enumerate() {
590                        let is_positional = field.has_attr(Some("args"), "positional");
591                        if !is_positional {
592                            continue;
593                        }
594
595                        if matches!(field.shape().def, Def::List(_)) {
596                            // list field, keep going
597                        } else if p.is_field_set(field_index)? {
598                            continue;
599                        }
600
601                        chosen_field_index = Some(field_index);
602                        break;
603                    }
604
605                    let Some(chosen_field_index) = chosen_field_index else {
606                        return Err(ArgsErrorKind::UnexpectedPositionalArgument { fields });
607                    };
608
609                    p = p.begin_nth_field(chosen_field_index)?;
610                    let value = self.args[self.index];
611
612                    // Check if this is an enum field without the subcommand attribute
613                    if let Type::User(UserType::Enum(_)) = fields[chosen_field_index].shape().ty
614                        && !fields[chosen_field_index].has_attr(Some("args"), "subcommand")
615                    {
616                        return Err(ArgsErrorKind::EnumWithoutSubcommandAttribute {
617                            field: &fields[chosen_field_index],
618                        });
619                    }
620
621                    p = self.handle_value(p, value)?;
622                    p = p.end()?;
623                    self.index += 1;
624                }
625                ArgType::None => todo!(),
626            }
627        }
628
629        // Finalize variant fields
630        p = self.pop_and_apply_counted_fields(p)?;
631        p = self.finalize_variant_fields(p, fields)?;
632
633        Ok(p)
634    }
635
636    /// Handle a field marked with args::subcommand
637    fn handle_subcommand_field(
638        &mut self,
639        p: Partial<'static>,
640        field_index: usize,
641        field: &'static Field,
642    ) -> Result<Partial<'static>, ArgsErrorKind> {
643        let field_shape = field.shape();
644        tracing::trace!(
645            "Handling subcommand field: {} with shape {}",
646            field.name,
647            field_shape
648        );
649
650        let mut p = p.begin_nth_field(field_index)?;
651
652        // Check if the field is an Option<Enum> or just an Enum
653        // IMPORTANT: Check Def::Option FIRST because Option is represented as an enum internally
654        let (is_optional, _enum_shape, enum_type) = if let Def::Option(option_def) = field_shape.def
655        {
656            // It's Option<T>, get the inner type
657            let inner_shape = option_def.t;
658            if let Type::User(UserType::Enum(enum_type)) = inner_shape.ty {
659                (true, inner_shape, enum_type)
660            } else {
661                return Err(ArgsErrorKind::NoFields { shape: field_shape });
662            }
663        } else if let Type::User(UserType::Enum(enum_type)) = field_shape.ty {
664            // It's a direct enum
665            (false, field_shape, enum_type)
666        } else {
667            return Err(ArgsErrorKind::NoFields { shape: field_shape });
668        };
669
670        // Get the subcommand name from current argument
671        let subcommand_name = self.args[self.index];
672        tracing::trace!("Looking for subcommand variant: {subcommand_name}");
673
674        // Find matching variant
675        let variant = match find_variant_by_name(enum_type, subcommand_name) {
676            Ok(v) => v,
677            Err(e) => {
678                if is_optional {
679                    // For optional subcommand, if we can't find a variant, leave it as None
680                    // But first we need to "undo" begin_nth_field... we can't easily do that
681                    // So instead we should check if it's a valid subcommand BEFORE calling begin_nth_field
682                    // For now, return the error
683                    return Err(e);
684                } else {
685                    return Err(e);
686                }
687            }
688        };
689
690        self.index += 1;
691
692        // Check if the next argument (if it exists) is a help flag for this subcommand
693        if self.index < self.args.len() && is_help_flag(self.args[self.index]) {
694            // Generate help for this specific subcommand variant
695            let help_text = crate::help::generate_subcommand_help(
696                variant,
697                "command", // This would ideally be the program name, but we don't have it in Context
698                &HelpConfig::default(),
699            );
700            return Err(ArgsErrorKind::HelpRequested { help_text });
701        }
702
703        if is_optional {
704            // Set Option to Some(variant)
705            p = p.begin_some()?;
706        }
707
708        // Select the variant
709        p = p.select_variant_named(variant.effective_name())?;
710
711        // Parse the variant's fields
712        p = self.parse_variant_fields(p, variant)?;
713
714        if is_optional {
715            p = p.end()?; // end Some
716        }
717
718        p = p.end()?; // end field
719
720        Ok(p)
721    }
722
723    /// Parse fields from an explicit slice (used for flattened tuple variant structs)
724    fn parse_fields_loop(
725        &mut self,
726        mut p: Partial<'static>,
727        fields: &'static [Field],
728    ) -> Result<Partial<'static>, ArgsErrorKind> {
729        self.push_counted_scope();
730
731        while self.args.len() > self.index {
732            let arg = self.args[self.index];
733            let arg_span = Span::new(self.arg_indices[self.index], arg.len());
734            let at = if self.positional_only {
735                ArgType::Positional
736            } else {
737                ArgType::parse(arg)
738            };
739            tracing::trace!("Parsing flattened struct field, arg: {at:?}");
740
741            match at {
742                ArgType::DoubleDash => {
743                    self.positional_only = true;
744                    self.index += 1;
745                }
746                ArgType::LongFlag(flag) => {
747                    if flag.starts_with('-') {
748                        return Err(ArgsErrorKind::UnknownLongFlag {
749                            flag: flag.to_string(),
750                            fields,
751                        });
752                    }
753
754                    let flag_span = Span::new(arg_span.start + 2, arg_span.len - 2);
755                    match split(flag, flag_span) {
756                        Some(tokens) => {
757                            let mut tokens = tokens.into_iter();
758                            let key = tokens.next().unwrap();
759                            let value = tokens.next().unwrap();
760
761                            let flag = key.s;
762                            let Some(field_index) =
763                                find_field_index_by_effective_name(fields, flag)
764                            else {
765                                return Err(ArgsErrorKind::UnknownLongFlag {
766                                    flag: flag.to_string(),
767                                    fields,
768                                });
769                            };
770                            p = self.handle_field(p, field_index, Some(value))?;
771                        }
772                        None => {
773                            let Some(field_index) =
774                                find_field_index_by_effective_name(fields, flag)
775                            else {
776                                return Err(ArgsErrorKind::UnknownLongFlag {
777                                    flag: flag.to_string(),
778                                    fields,
779                                });
780                            };
781                            if !self.try_handle_counted_long_flag(&fields[field_index], field_index)
782                            {
783                                p = self.handle_field(p, field_index, None)?;
784                            }
785                        }
786                    }
787                }
788                ArgType::ShortFlag(flag) => {
789                    let flag_span = Span::new(arg_span.start + 1, arg_span.len - 1);
790                    match split(flag, flag_span) {
791                        Some(tokens) => {
792                            let mut tokens = tokens.into_iter();
793                            let key = tokens.next().unwrap();
794                            let value = tokens.next().unwrap();
795
796                            let short_char = key.s;
797                            let Some(field_index) =
798                                find_field_index_with_short_char(fields, short_char)
799                            else {
800                                return Err(ArgsErrorKind::UnknownShortFlag {
801                                    flag: short_char.to_string(),
802                                    fields,
803                                    precise_span: None,
804                                });
805                            };
806                            p = self.handle_field(p, field_index, Some(value))?;
807                        }
808                        None => {
809                            p = self.process_short_flag(p, flag, flag_span, fields)?;
810                        }
811                    }
812                }
813                ArgType::Positional => {
814                    // Look for a positional field
815                    let mut chosen_field_index: Option<usize> = None;
816                    for (field_index, field) in fields.iter().enumerate() {
817                        let is_positional = field.has_attr(Some("args"), "positional");
818                        if !is_positional {
819                            continue;
820                        }
821
822                        // If it's a list, we can keep appending to it even if already set.
823                        // Otherwise, skip fields that are already set.
824                        if matches!(field.shape().def, Def::List(_)) {
825                            // List field - can accept multiple positional arguments
826                        } else if p.is_field_set(field_index)? {
827                            continue;
828                        }
829
830                        chosen_field_index = Some(field_index);
831                        break;
832                    }
833
834                    if let Some(field_index) = chosen_field_index {
835                        let value = SplitToken {
836                            s: arg,
837                            span: arg_span,
838                        };
839                        p = self.handle_field(p, field_index, Some(value))?;
840                    } else {
841                        return Err(ArgsErrorKind::UnexpectedPositionalArgument { fields });
842                    }
843                }
844                ArgType::None => todo!(),
845            }
846        }
847        Ok(p)
848    }
849
850    /// Finalize struct fields (set defaults, check required)
851    fn finalize_struct(&self, mut p: Partial<'static>) -> Result<Partial<'static>, ArgsErrorKind> {
852        let fields = self.fields(&p)?;
853        for (field_index, field) in fields.iter().enumerate() {
854            if p.is_field_set(field_index)? {
855                continue;
856            }
857
858            // Check if it's an optional subcommand field
859            if field.has_attr(Some("args"), "subcommand") {
860                let field_shape = field.shape();
861                if let Def::Option(_) = field_shape.def {
862                    // Optional subcommand, set to None using default
863                    // Option<T> has a default_in_place that sets it to None
864                    p = p.set_nth_field_to_default(field_index)?;
865                    continue;
866                } else {
867                    // Required subcommand missing
868                    return Err(ArgsErrorKind::MissingSubcommand {
869                        variants: get_variants_from_shape(field_shape),
870                    });
871                }
872            }
873
874            if is_counted_field(field) && is_supported_counted_type(field.shape()) {
875                // Counted fields default to 0 if not incremented
876                p = p.begin_nth_field(field_index)?;
877                p = p.parse_from_str("0")?;
878                p = p.end()?;
879            } else if field.has_default() {
880                tracing::trace!("Setting #{field_index} field to default: {field:?}");
881                p = p.set_nth_field_to_default(field_index)?;
882            } else if field.shape().is_shape(bool::SHAPE) {
883                // bools are just set to false
884                p = p.set_nth_field(field_index, false)?;
885            } else if let Def::Option(_) = field.shape().def {
886                // Option<T> fields default to None
887                p = p.set_nth_field_to_default(field_index)?;
888            } else {
889                return Err(ArgsErrorKind::MissingArgument { field });
890            }
891        }
892        Ok(p)
893    }
894
895    /// Finalize variant fields (set defaults, check required)
896    fn finalize_variant_fields(
897        &self,
898        mut p: Partial<'static>,
899        fields: &'static [Field],
900    ) -> Result<Partial<'static>, ArgsErrorKind> {
901        for (field_index, field) in fields.iter().enumerate() {
902            if p.is_field_set(field_index)? {
903                continue;
904            }
905
906            // Check if it's a subcommand field
907            if field.has_attr(Some("args"), "subcommand") {
908                let field_shape = field.shape();
909                if let Def::Option(_) = field_shape.def {
910                    // Optional subcommand, set to None using default
911                    p = p.set_nth_field_to_default(field_index)?;
912                    continue;
913                } else {
914                    // Required subcommand missing
915                    return Err(ArgsErrorKind::MissingSubcommand {
916                        variants: get_variants_from_shape(field_shape),
917                    });
918                }
919            }
920
921            if is_counted_field(field) && is_supported_counted_type(field.shape()) {
922                // Counted fields default to 0 if not incremented
923                p = p.begin_nth_field(field_index)?;
924                p = p.parse_from_str("0")?;
925                p = p.end()?;
926            } else if field.has_default() {
927                tracing::trace!("Setting variant field #{field_index} to default: {field:?}");
928                p = p.set_nth_field_to_default(field_index)?;
929            } else if field.shape().is_shape(bool::SHAPE) {
930                p = p.set_nth_field(field_index, false)?;
931            } else if let Def::Option(_) = field.shape().def {
932                // Option<T> fields default to None
933                p = p.set_nth_field_to_default(field_index)?;
934            } else {
935                return Err(ArgsErrorKind::MissingArgument { field });
936            }
937        }
938        Ok(p)
939    }
940}
941
942/// Find a variant by its CLI name (kebab-case) or its actual name
943fn find_variant_by_name(
944    enum_type: EnumType,
945    name: &str,
946) -> Result<&'static Variant, ArgsErrorKind> {
947    tracing::trace!(
948        "find_variant_by_name: looking for '{}' among variants: {:?}",
949        name,
950        enum_type
951            .variants
952            .iter()
953            .map(|v| v.name)
954            .collect::<Vec<_>>()
955    );
956
957    // First check for rename attribute
958    for variant in enum_type.variants {
959        if let Some(attr) = variant.get_builtin_attr("rename")
960            && let Some(rename) = attr.get_as::<&str>()
961            && *rename == name
962        {
963            return Ok(variant);
964        }
965    }
966
967    // Then check kebab-case conversion of variant name
968    for variant in enum_type.variants {
969        let kebab_name = variant.name.to_kebab_case();
970        tracing::trace!(
971            "  checking variant '{}' -> kebab '{}' against '{}'",
972            variant.name,
973            kebab_name,
974            name
975        );
976        if kebab_name == name {
977            return Ok(variant);
978        }
979    }
980
981    // Finally check exact name match
982    for variant in enum_type.variants {
983        if variant.name == name {
984            return Ok(variant);
985        }
986    }
987
988    Err(ArgsErrorKind::UnknownSubcommand {
989        provided: name.to_string(),
990        variants: enum_type.variants,
991    })
992}
993
994/// Find a field marked with args::subcommand
995fn find_subcommand_field(fields: &'static [Field]) -> Option<(usize, &'static Field)> {
996    fields
997        .iter()
998        .enumerate()
999        .find(|(_, f)| f.has_attr(Some("args"), "subcommand"))
1000}
1001
1002/// Result of `split`
1003#[derive(Debug, PartialEq)]
1004struct SplitToken<'input> {
1005    s: &'input str,
1006    span: Span,
1007}
1008
1009/// Split on `=`, e.g. `a=b` returns (`a`, `b`).
1010/// Span-aware. If `=` is not contained in the input string,
1011/// returns None
1012fn split<'input>(input: &'input str, span: Span) -> Option<Vec<SplitToken<'input>>> {
1013    let equals_index = input.find('=')?;
1014
1015    let l = &input[0..equals_index];
1016    let l_span = Span::new(span.start, l.len());
1017
1018    let r = &input[equals_index + 1..];
1019    let r_span = Span::new(equals_index + 1, r.len());
1020
1021    Some(vec![
1022        SplitToken { s: l, span: l_span },
1023        SplitToken { s: r, span: r_span },
1024    ])
1025}
1026
1027#[test]
1028fn test_split() {
1029    assert_eq!(split("ababa", Span::new(5, 5)), None);
1030    assert_eq!(
1031        split("foo=bar", Span::new(0, 7)),
1032        Some(vec![
1033            SplitToken {
1034                s: "foo",
1035                span: Span::new(0, 3)
1036            },
1037            SplitToken {
1038                s: "bar",
1039                span: Span::new(4, 3)
1040            },
1041        ])
1042    );
1043    assert_eq!(
1044        split("foo=", Span::new(0, 4)),
1045        Some(vec![
1046            SplitToken {
1047                s: "foo",
1048                span: Span::new(0, 3)
1049            },
1050            SplitToken {
1051                s: "",
1052                span: Span::new(4, 0)
1053            },
1054        ])
1055    );
1056    assert_eq!(
1057        split("=bar", Span::new(0, 4)),
1058        Some(vec![
1059            SplitToken {
1060                s: "",
1061                span: Span::new(0, 0)
1062            },
1063            SplitToken {
1064                s: "bar",
1065                span: Span::new(1, 3)
1066            },
1067        ])
1068    );
1069}
1070
1071impl<'input> Context<'input> {
1072    /// Process a short flag that may contain chained flags or an attached value.
1073    ///
1074    /// This function handles three cases:
1075    /// 1. Single flag: `-v` → process as bool or look for value in next arg
1076    /// 2. Chained bool flags: `-abc` → recursively process `-a`, then `-bc`, then `-c`
1077    /// 3. Attached value: `-j4` → process `-j` with value `4`
1078    ///
1079    /// The function is recursive for chained flags, maintaining proper span tracking
1080    /// and index management. Only increments `self.index` at the leaf of recursion.
1081    fn process_short_flag(
1082        &mut self,
1083        mut p: Partial<'static>,
1084        flag: &'input str,
1085        flag_span: Span,
1086        fields: &'static [Field],
1087    ) -> Result<Partial<'static>, ArgsErrorKind> {
1088        // Get the first character as the flag
1089        let first_char = flag.chars().next().unwrap();
1090        let first_char_str = &flag[..first_char.len_utf8()];
1091        let rest = &flag[first_char.len_utf8()..];
1092
1093        tracing::trace!("Looking up short flag '{first_char}' (rest: '{rest}')");
1094
1095        // Look up the field for this character
1096        let Some(field_index) = find_field_index_with_short_char(fields, first_char_str) else {
1097            // Error: unknown flag, report just the first character with precise span
1098            let char_span = Span::new(flag_span.start, first_char.len_utf8());
1099            return Err(ArgsErrorKind::UnknownShortFlag {
1100                flag: first_char_str.to_string(),
1101                fields,
1102                precise_span: Some(char_span),
1103            });
1104        };
1105
1106        let field = &fields[field_index];
1107        let field_shape = field.shape();
1108
1109        // Check if the field is bool or Vec<bool>
1110        let is_bool = field_shape.is_shape(bool::SHAPE);
1111        let is_bool_list = if let facet_core::Def::List(list_def) = field_shape.def {
1112            list_def.t.is_shape(bool::SHAPE)
1113        } else {
1114            false
1115        };
1116        let is_counted = is_counted_field(field) && is_supported_counted_type(field_shape);
1117
1118        if rest.is_empty() {
1119            // Leaf case: last character in the chain
1120            if is_counted {
1121                self.increment_counted(field_index);
1122                self.index += 1;
1123            } else if is_bool || is_bool_list {
1124                // Bool or Vec<bool> at the end of chain
1125                p = p.begin_nth_field(field_index)?;
1126
1127                if is_bool_list {
1128                    // For Vec<bool> fields, initialize list and push an item
1129                    p = p.init_list()?;
1130                    p = p.begin_list_item()?;
1131                    p = p.set(true)?;
1132                    p = p.end()?; // end list item
1133                } else {
1134                    // For simple bool fields, just set to true
1135                    p = p.set(true)?;
1136                }
1137
1138                p = p.end()?; // end field
1139                self.index += 1; // Move to next arg
1140            } else {
1141                // Non-bool field: use handle_field which looks for value in next arg
1142                p = self.handle_field(p, field_index, None)?;
1143            }
1144        } else if is_counted {
1145            // Counted flag with trailing chars: `-vvv` increments for each `v`
1146            self.increment_counted(field_index);
1147            let rest_span = Span::new(flag_span.start + first_char.len_utf8(), rest.len());
1148            p = self.process_short_flag(p, rest, rest_span, fields)?;
1149        } else if is_bool || is_bool_list {
1150            // Bool flag with trailing chars: could be chaining like `-abc` or `-vvv`
1151            // Process current bool flag without going through handle_field
1152            // (which would increment index and consume next arg)
1153            p = p.begin_nth_field(field_index)?;
1154
1155            if is_bool_list {
1156                // For Vec<bool> fields, we need to initialize the list and push an item
1157                p = p.init_list()?;
1158                p = p.begin_list_item()?;
1159                p = p.set(true)?;
1160                p = p.end()?; // end list item
1161            } else {
1162                // For simple bool fields, just set to true
1163                p = p.set(true)?;
1164            }
1165
1166            p = p.end()?; // end field
1167
1168            // Recursively process remaining characters as a new short flag chain
1169            let rest_span = Span::new(flag_span.start + first_char.len_utf8(), rest.len());
1170            p = self.process_short_flag(p, rest, rest_span, fields)?;
1171            // Note: index increment happens in the leaf recursion
1172        } else {
1173            // Non-bool flag with attached value: `-j4`
1174            let value_span = Span::new(flag_span.start + first_char.len_utf8(), rest.len());
1175            p = self.handle_field(
1176                p,
1177                field_index,
1178                Some(SplitToken {
1179                    s: rest,
1180                    span: value_span,
1181                }),
1182            )?;
1183        }
1184
1185        Ok(p)
1186    }
1187}
1188
1189/// Given an array of fields, find the field with the given `args::short = 'a'`
1190/// annotation. Uses extension attribute syntax: #[facet(args::short = "j")]
1191/// The `short` parameter should be a single character (as a string slice).
1192fn find_field_index_with_short_char(fields: &'static [Field], short: &str) -> Option<usize> {
1193    let short_char = short.chars().next()?;
1194    fields.iter().position(|f| {
1195        if let Some(ext) = f.get_attr(Some("args"), "short") {
1196            // The attribute stores the full Attr enum
1197            if let Some(crate::Attr::Short(opt_char)) = ext.get_as::<crate::Attr>() {
1198                match opt_char {
1199                    Some(c) => *c == short_char,
1200                    None => {
1201                        // No explicit short specified, use first char of effective name
1202                        // (effective_name returns rename if set, otherwise the field name)
1203                        f.effective_name().starts_with(short_char)
1204                    }
1205                }
1206            } else {
1207                false
1208            }
1209        } else {
1210            false
1211        }
1212    })
1213}
1214
1215/// Find the field index by matching against the effective name (respects rename attribute).
1216/// The `flag` parameter should be a kebab-case CLI flag name.
1217fn find_field_index_by_effective_name(fields: &'static [Field], flag: &str) -> Option<usize> {
1218    let snek = flag.to_snake_case();
1219    fields
1220        .iter()
1221        .position(|f| f.effective_name().to_snake_case() == snek)
1222}