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