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
767                        // If it's a list, we can keep appending to it even if already set.
768                        // Otherwise, skip fields that are already set.
769                        if matches!(field.shape().def, Def::List(_)) {
770                            // List field - can accept multiple positional arguments
771                        } else if p.is_field_set(field_index)? {
772                            continue;
773                        }
774
775                        chosen_field_index = Some(field_index);
776                        break;
777                    }
778
779                    if let Some(field_index) = chosen_field_index {
780                        let value = SplitToken {
781                            s: arg,
782                            span: arg_span,
783                        };
784                        p = self.handle_field(p, field_index, Some(value))?;
785                    } else {
786                        return Err(ArgsErrorKind::UnexpectedPositionalArgument { fields });
787                    }
788                }
789                ArgType::None => todo!(),
790            }
791        }
792        Ok(p)
793    }
794
795    /// Finalize struct fields (set defaults, check required)
796    fn finalize_struct(&self, mut p: Partial<'static>) -> Result<Partial<'static>, ArgsErrorKind> {
797        let fields = self.fields(&p)?;
798        for (field_index, field) in fields.iter().enumerate() {
799            if p.is_field_set(field_index)? {
800                continue;
801            }
802
803            // Check if it's an optional subcommand field
804            if field.has_attr(Some("args"), "subcommand") {
805                let field_shape = field.shape();
806                if let Def::Option(_) = field_shape.def {
807                    // Optional subcommand, set to None using default
808                    // Option<T> has a default_in_place that sets it to None
809                    p = p.set_nth_field_to_default(field_index)?;
810                    continue;
811                } else {
812                    // Required subcommand missing
813                    return Err(ArgsErrorKind::MissingSubcommand {
814                        variants: get_variants_from_shape(field_shape),
815                    });
816                }
817            }
818
819            if field.has_default() {
820                tracing::trace!("Setting #{field_index} field to default: {field:?}");
821                p = p.set_nth_field_to_default(field_index)?;
822            } else if field.shape().is_shape(bool::SHAPE) {
823                // bools are just set to false
824                p = p.set_nth_field(field_index, false)?;
825            } else if let Def::Option(_) = field.shape().def {
826                // Option<T> fields default to None
827                p = p.set_nth_field_to_default(field_index)?;
828            } else {
829                return Err(ArgsErrorKind::MissingArgument { field });
830            }
831        }
832        Ok(p)
833    }
834
835    /// Finalize variant fields (set defaults, check required)
836    fn finalize_variant_fields(
837        &self,
838        mut p: Partial<'static>,
839        fields: &'static [Field],
840    ) -> Result<Partial<'static>, ArgsErrorKind> {
841        for (field_index, field) in fields.iter().enumerate() {
842            if p.is_field_set(field_index)? {
843                continue;
844            }
845
846            // Check if it's a subcommand field
847            if field.has_attr(Some("args"), "subcommand") {
848                let field_shape = field.shape();
849                if let Def::Option(_) = field_shape.def {
850                    // Optional subcommand, set to None using default
851                    p = p.set_nth_field_to_default(field_index)?;
852                    continue;
853                } else {
854                    // Required subcommand missing
855                    return Err(ArgsErrorKind::MissingSubcommand {
856                        variants: get_variants_from_shape(field_shape),
857                    });
858                }
859            }
860
861            if field.has_default() {
862                tracing::trace!("Setting variant field #{field_index} to default: {field:?}");
863                p = p.set_nth_field_to_default(field_index)?;
864            } else if field.shape().is_shape(bool::SHAPE) {
865                p = p.set_nth_field(field_index, false)?;
866            } else if let Def::Option(_) = field.shape().def {
867                // Option<T> fields default to None
868                p = p.set_nth_field_to_default(field_index)?;
869            } else {
870                return Err(ArgsErrorKind::MissingArgument { field });
871            }
872        }
873        Ok(p)
874    }
875}
876
877/// Find a variant by its CLI name (kebab-case) or its actual name
878fn find_variant_by_name(
879    enum_type: EnumType,
880    name: &str,
881) -> Result<&'static Variant, ArgsErrorKind> {
882    tracing::trace!(
883        "find_variant_by_name: looking for '{}' among variants: {:?}",
884        name,
885        enum_type
886            .variants
887            .iter()
888            .map(|v| v.name)
889            .collect::<Vec<_>>()
890    );
891
892    // First check for rename attribute
893    for variant in enum_type.variants {
894        if let Some(attr) = variant.get_builtin_attr("rename")
895            && let Some(rename) = attr.get_as::<&str>()
896            && *rename == name
897        {
898            return Ok(variant);
899        }
900    }
901
902    // Then check kebab-case conversion of variant name
903    for variant in enum_type.variants {
904        let kebab_name = variant.name.to_kebab_case();
905        tracing::trace!(
906            "  checking variant '{}' -> kebab '{}' against '{}'",
907            variant.name,
908            kebab_name,
909            name
910        );
911        if kebab_name == name {
912            return Ok(variant);
913        }
914    }
915
916    // Finally check exact name match
917    for variant in enum_type.variants {
918        if variant.name == name {
919            return Ok(variant);
920        }
921    }
922
923    Err(ArgsErrorKind::UnknownSubcommand {
924        provided: name.to_string(),
925        variants: enum_type.variants,
926    })
927}
928
929/// Find a field marked with args::subcommand
930fn find_subcommand_field(fields: &'static [Field]) -> Option<(usize, &'static Field)> {
931    fields
932        .iter()
933        .enumerate()
934        .find(|(_, f)| f.has_attr(Some("args"), "subcommand"))
935}
936
937/// Result of `split`
938#[derive(Debug, PartialEq)]
939struct SplitToken<'input> {
940    s: &'input str,
941    span: Span,
942}
943
944/// Split on `=`, e.g. `a=b` returns (`a`, `b`).
945/// Span-aware. If `=` is not contained in the input string,
946/// returns None
947fn split<'input>(input: &'input str, span: Span) -> Option<Vec<SplitToken<'input>>> {
948    let equals_index = input.find('=')?;
949
950    let l = &input[0..equals_index];
951    let l_span = Span::new(span.start, l.len());
952
953    let r = &input[equals_index + 1..];
954    let r_span = Span::new(equals_index + 1, r.len());
955
956    Some(vec![
957        SplitToken { s: l, span: l_span },
958        SplitToken { s: r, span: r_span },
959    ])
960}
961
962#[test]
963fn test_split() {
964    assert_eq!(split("ababa", Span::new(5, 5)), None);
965    assert_eq!(
966        split("foo=bar", Span::new(0, 7)),
967        Some(vec![
968            SplitToken {
969                s: "foo",
970                span: Span::new(0, 3)
971            },
972            SplitToken {
973                s: "bar",
974                span: Span::new(4, 3)
975            },
976        ])
977    );
978    assert_eq!(
979        split("foo=", Span::new(0, 4)),
980        Some(vec![
981            SplitToken {
982                s: "foo",
983                span: Span::new(0, 3)
984            },
985            SplitToken {
986                s: "",
987                span: Span::new(4, 0)
988            },
989        ])
990    );
991    assert_eq!(
992        split("=bar", Span::new(0, 4)),
993        Some(vec![
994            SplitToken {
995                s: "",
996                span: Span::new(0, 0)
997            },
998            SplitToken {
999                s: "bar",
1000                span: Span::new(1, 3)
1001            },
1002        ])
1003    );
1004}
1005
1006impl<'input> Context<'input> {
1007    /// Process a short flag that may contain chained flags or an attached value.
1008    ///
1009    /// This function handles three cases:
1010    /// 1. Single flag: `-v` → process as bool or look for value in next arg
1011    /// 2. Chained bool flags: `-abc` → recursively process `-a`, then `-bc`, then `-c`
1012    /// 3. Attached value: `-j4` → process `-j` with value `4`
1013    ///
1014    /// The function is recursive for chained flags, maintaining proper span tracking
1015    /// and index management. Only increments `self.index` at the leaf of recursion.
1016    fn process_short_flag(
1017        &mut self,
1018        mut p: Partial<'static>,
1019        flag: &'input str,
1020        flag_span: Span,
1021        fields: &'static [Field],
1022    ) -> Result<Partial<'static>, ArgsErrorKind> {
1023        // Get the first character as the flag
1024        let first_char = flag.chars().next().unwrap();
1025        let first_char_str = &flag[..first_char.len_utf8()];
1026        let rest = &flag[first_char.len_utf8()..];
1027
1028        tracing::trace!("Looking up short flag '{first_char}' (rest: '{rest}')");
1029
1030        // Look up the field for this character
1031        let Some(field_index) = find_field_index_with_short_char(fields, first_char_str) else {
1032            // Error: unknown flag, report just the first character with precise span
1033            let char_span = Span::new(flag_span.start, first_char.len_utf8());
1034            return Err(ArgsErrorKind::UnknownShortFlag {
1035                flag: first_char_str.to_string(),
1036                fields,
1037                precise_span: Some(char_span),
1038            });
1039        };
1040
1041        let field = &fields[field_index];
1042        let field_shape = field.shape();
1043
1044        // Check if the field is bool or Vec<bool>
1045        let is_bool = field_shape.is_shape(bool::SHAPE);
1046        let is_bool_list = if let facet_core::Def::List(list_def) = field_shape.def {
1047            list_def.t.is_shape(bool::SHAPE)
1048        } else {
1049            false
1050        };
1051
1052        if rest.is_empty() {
1053            // Leaf case: last character in the chain
1054            if is_bool || is_bool_list {
1055                // Bool or Vec<bool> at the end of chain
1056                p = p.begin_nth_field(field_index)?;
1057
1058                if is_bool_list {
1059                    // For Vec<bool> fields, initialize list and push an item
1060                    p = p.begin_list()?;
1061                    p = p.begin_list_item()?;
1062                    p = p.set(true)?;
1063                    p = p.end()?; // end list item
1064                } else {
1065                    // For simple bool fields, just set to true
1066                    p = p.set(true)?;
1067                }
1068
1069                p = p.end()?; // end field
1070                self.index += 1; // Move to next arg
1071            } else {
1072                // Non-bool field: use handle_field which looks for value in next arg
1073                p = self.handle_field(p, field_index, None)?;
1074            }
1075        } else if is_bool || is_bool_list {
1076            // Bool flag with trailing chars: could be chaining like `-abc` or `-vvv`
1077            // Process current bool flag without going through handle_field
1078            // (which would increment index and consume next arg)
1079            p = p.begin_nth_field(field_index)?;
1080
1081            if is_bool_list {
1082                // For Vec<bool> fields, we need to initialize the list and push an item
1083                p = p.begin_list()?;
1084                p = p.begin_list_item()?;
1085                p = p.set(true)?;
1086                p = p.end()?; // end list item
1087            } else {
1088                // For simple bool fields, just set to true
1089                p = p.set(true)?;
1090            }
1091
1092            p = p.end()?; // end field
1093
1094            // Recursively process remaining characters as a new short flag chain
1095            let rest_span = Span::new(flag_span.start + first_char.len_utf8(), rest.len());
1096            p = self.process_short_flag(p, rest, rest_span, fields)?;
1097            // Note: index increment happens in the leaf recursion
1098        } else {
1099            // Non-bool flag with attached value: `-j4`
1100            let value_span = Span::new(flag_span.start + first_char.len_utf8(), rest.len());
1101            p = self.handle_field(
1102                p,
1103                field_index,
1104                Some(SplitToken {
1105                    s: rest,
1106                    span: value_span,
1107                }),
1108            )?;
1109        }
1110
1111        Ok(p)
1112    }
1113}
1114
1115/// Given an array of fields, find the field with the given `args::short = 'a'`
1116/// annotation. Uses extension attribute syntax: #[facet(args::short = "j")]
1117/// The `short` parameter should be a single character (as a string slice).
1118fn find_field_index_with_short_char(fields: &'static [Field], short: &str) -> Option<usize> {
1119    let short_char = short.chars().next()?;
1120    fields.iter().position(|f| {
1121        if let Some(ext) = f.get_attr(Some("args"), "short") {
1122            // The attribute stores the full Attr enum
1123            if let Some(crate::Attr::Short(opt_char)) = ext.get_as::<crate::Attr>() {
1124                match opt_char {
1125                    Some(c) => *c == short_char,
1126                    None => {
1127                        // No explicit short specified, use first char of field name
1128                        f.name.starts_with(short_char)
1129                    }
1130                }
1131            } else {
1132                false
1133            }
1134        } else {
1135            false
1136        }
1137    })
1138}