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