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, 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        while self.args.len() > self.index {
425            let arg = self.args[self.index];
426            let arg_span = Span::new(self.arg_indices[self.index], arg.len());
427            let at = if self.positional_only {
428                ArgType::Positional
429            } else {
430                ArgType::parse(arg)
431            };
432            tracing::trace!("Parsing variant field, arg: {at:?}");
433
434            match at {
435                ArgType::DoubleDash => {
436                    self.positional_only = true;
437                    self.index += 1;
438                }
439                ArgType::LongFlag(flag) => {
440                    // Reject flags that start with `-` (e.g., `---verbose`)
441                    if flag.starts_with('-') {
442                        return Err(ArgsErrorKind::UnknownLongFlag {
443                            flag: flag.to_string(),
444                            fields,
445                        });
446                    }
447
448                    let flag_span = Span::new(arg_span.start + 2, arg_span.len - 2);
449                    match split(flag, flag_span) {
450                        Some(tokens) => {
451                            let mut tokens = tokens.into_iter();
452                            let key = tokens.next().unwrap();
453                            let value = tokens.next().unwrap();
454
455                            let snek = key.s.to_snake_case();
456                            tracing::trace!(
457                                "Looking up long flag {flag} in variant (field name: {snek})"
458                            );
459                            let Some(field_index) = fields.iter().position(|f| f.name == snek)
460                            else {
461                                return Err(ArgsErrorKind::UnknownLongFlag {
462                                    flag: flag.to_string(),
463                                    fields,
464                                });
465                            };
466                            p = self.handle_field(p, field_index, Some(value))?;
467                        }
468                        None => {
469                            let snek = flag.to_snake_case();
470                            tracing::trace!(
471                                "Looking up long flag {flag} in variant (field name: {snek})"
472                            );
473                            let Some(field_index) = fields.iter().position(|f| f.name == snek)
474                            else {
475                                return Err(ArgsErrorKind::UnknownLongFlag {
476                                    flag: flag.to_string(),
477                                    fields,
478                                });
479                            };
480                            p = self.handle_field(p, field_index, None)?;
481                        }
482                    }
483                }
484                ArgType::ShortFlag(flag) => {
485                    let flag_span = Span::new(arg_span.start + 1, arg_span.len - 1);
486                    match split(flag, flag_span) {
487                        Some(tokens) => {
488                            let mut tokens = tokens.into_iter();
489                            let key = tokens.next().unwrap();
490                            let value = tokens.next().unwrap();
491
492                            let short_char = key.s;
493                            tracing::trace!("Looking up short flag {short_char} in variant");
494                            let Some(field_index) =
495                                find_field_index_with_short_char(fields, short_char)
496                            else {
497                                return Err(ArgsErrorKind::UnknownShortFlag {
498                                    flag: short_char.to_string(),
499                                    fields,
500                                    precise_span: None, // Not chained, use default arg span
501                                });
502                            };
503                            p = self.handle_field(p, field_index, Some(value))?;
504                        }
505                        None => {
506                            // No `=` in the flag. Use helper to handle chaining.
507                            p = self.process_short_flag(p, flag, flag_span, fields)?;
508                        }
509                    }
510                }
511                ArgType::Positional => {
512                    // Check for subcommand field first (for nested subcommands)
513                    if let Some((field_index, field)) = find_subcommand_field(fields)
514                        && !p.is_field_set(field_index)?
515                    {
516                        p = self.handle_subcommand_field(p, field_index, field)?;
517                        continue;
518                    }
519
520                    // Look for positional field
521                    let mut chosen_field_index: Option<usize> = None;
522
523                    for (field_index, field) in fields.iter().enumerate() {
524                        let is_positional = field.has_attr(Some("args"), "positional");
525                        if !is_positional {
526                            continue;
527                        }
528
529                        if matches!(field.shape().def, Def::List(_)) {
530                            // list field, keep going
531                        } else if p.is_field_set(field_index)? {
532                            continue;
533                        }
534
535                        chosen_field_index = Some(field_index);
536                        break;
537                    }
538
539                    let Some(chosen_field_index) = chosen_field_index else {
540                        return Err(ArgsErrorKind::UnexpectedPositionalArgument { fields });
541                    };
542
543                    p = p.begin_nth_field(chosen_field_index)?;
544                    let value = self.args[self.index];
545
546                    // Check if this is an enum field without the subcommand attribute
547                    if let Type::User(UserType::Enum(_)) = fields[chosen_field_index].shape().ty
548                        && !fields[chosen_field_index].has_attr(Some("args"), "subcommand")
549                    {
550                        return Err(ArgsErrorKind::EnumWithoutSubcommandAttribute {
551                            field: &fields[chosen_field_index],
552                        });
553                    }
554
555                    p = self.handle_value(p, value)?;
556                    p = p.end()?;
557                    self.index += 1;
558                }
559                ArgType::None => todo!(),
560            }
561        }
562
563        // Finalize variant fields
564        p = self.finalize_variant_fields(p, fields)?;
565
566        Ok(p)
567    }
568
569    /// Handle a field marked with args::subcommand
570    fn handle_subcommand_field(
571        &mut self,
572        p: Partial<'static>,
573        field_index: usize,
574        field: &'static Field,
575    ) -> Result<Partial<'static>, ArgsErrorKind> {
576        let field_shape = field.shape();
577        tracing::trace!(
578            "Handling subcommand field: {} with shape {}",
579            field.name,
580            field_shape
581        );
582
583        let mut p = p.begin_nth_field(field_index)?;
584
585        // Check if the field is an Option<Enum> or just an Enum
586        // IMPORTANT: Check Def::Option FIRST because Option is represented as an enum internally
587        let (is_optional, _enum_shape, enum_type) = if let Def::Option(option_def) = field_shape.def
588        {
589            // It's Option<T>, get the inner type
590            let inner_shape = option_def.t;
591            if let Type::User(UserType::Enum(enum_type)) = inner_shape.ty {
592                (true, inner_shape, enum_type)
593            } else {
594                return Err(ArgsErrorKind::NoFields { shape: field_shape });
595            }
596        } else if let Type::User(UserType::Enum(enum_type)) = field_shape.ty {
597            // It's a direct enum
598            (false, field_shape, enum_type)
599        } else {
600            return Err(ArgsErrorKind::NoFields { shape: field_shape });
601        };
602
603        // Get the subcommand name from current argument
604        let subcommand_name = self.args[self.index];
605        tracing::trace!("Looking for subcommand variant: {subcommand_name}");
606
607        // Find matching variant
608        let variant = match find_variant_by_name(enum_type, subcommand_name) {
609            Ok(v) => v,
610            Err(e) => {
611                if is_optional {
612                    // For optional subcommand, if we can't find a variant, leave it as None
613                    // But first we need to "undo" begin_nth_field... we can't easily do that
614                    // So instead we should check if it's a valid subcommand BEFORE calling begin_nth_field
615                    // For now, return the error
616                    return Err(e);
617                } else {
618                    return Err(e);
619                }
620            }
621        };
622
623        self.index += 1;
624
625        if is_optional {
626            // Set Option to Some(variant)
627            p = p.begin_some()?;
628        }
629
630        // Select the variant
631        p = p.select_variant_named(variant.name)?;
632
633        // Parse the variant's fields
634        p = self.parse_variant_fields(p, variant)?;
635
636        if is_optional {
637            p = p.end()?; // end Some
638        }
639
640        p = p.end()?; // end field
641
642        Ok(p)
643    }
644
645    /// Finalize struct fields (set defaults, check required)
646    fn finalize_struct(&self, mut p: Partial<'static>) -> Result<Partial<'static>, ArgsErrorKind> {
647        let fields = self.fields(&p)?;
648        for (field_index, field) in fields.iter().enumerate() {
649            if p.is_field_set(field_index)? {
650                continue;
651            }
652
653            // Check if it's an optional subcommand field
654            if field.has_attr(Some("args"), "subcommand") {
655                let field_shape = field.shape();
656                if let Def::Option(_) = field_shape.def {
657                    // Optional subcommand, set to None using default
658                    // Option<T> has a default_in_place that sets it to None
659                    p = p.set_nth_field_to_default(field_index)?;
660                    continue;
661                } else {
662                    // Required subcommand missing
663                    return Err(ArgsErrorKind::MissingSubcommand {
664                        variants: get_variants_from_shape(field_shape),
665                    });
666                }
667            }
668
669            if field.has_default() {
670                tracing::trace!("Setting #{field_index} field to default: {field:?}");
671                p = p.set_nth_field_to_default(field_index)?;
672            } else if field.shape().is_shape(bool::SHAPE) {
673                // bools are just set to false
674                p = p.set_nth_field(field_index, false)?;
675            } else {
676                return Err(ArgsErrorKind::MissingArgument { field });
677            }
678        }
679        Ok(p)
680    }
681
682    /// Finalize variant fields (set defaults, check required)
683    fn finalize_variant_fields(
684        &self,
685        mut p: Partial<'static>,
686        fields: &'static [Field],
687    ) -> Result<Partial<'static>, ArgsErrorKind> {
688        for (field_index, field) in fields.iter().enumerate() {
689            if p.is_field_set(field_index)? {
690                continue;
691            }
692
693            // Check if it's a subcommand field
694            if field.has_attr(Some("args"), "subcommand") {
695                let field_shape = field.shape();
696                if let Def::Option(_) = field_shape.def {
697                    // Optional subcommand, set to None using default
698                    p = p.set_nth_field_to_default(field_index)?;
699                    continue;
700                } else {
701                    // Required subcommand missing
702                    return Err(ArgsErrorKind::MissingSubcommand {
703                        variants: get_variants_from_shape(field_shape),
704                    });
705                }
706            }
707
708            if field.has_default() {
709                tracing::trace!("Setting variant field #{field_index} to default: {field:?}");
710                p = p.set_nth_field_to_default(field_index)?;
711            } else if field.shape().is_shape(bool::SHAPE) {
712                p = p.set_nth_field(field_index, false)?;
713            } else {
714                return Err(ArgsErrorKind::MissingArgument { field });
715            }
716        }
717        Ok(p)
718    }
719}
720
721/// Find a variant by its CLI name (kebab-case) or its actual name
722fn find_variant_by_name(
723    enum_type: EnumType,
724    name: &str,
725) -> Result<&'static Variant, ArgsErrorKind> {
726    tracing::trace!(
727        "find_variant_by_name: looking for '{}' among variants: {:?}",
728        name,
729        enum_type
730            .variants
731            .iter()
732            .map(|v| v.name)
733            .collect::<Vec<_>>()
734    );
735
736    // First check for rename attribute
737    for variant in enum_type.variants {
738        if let Some(attr) = variant.get_builtin_attr("rename")
739            && let Some(rename) = attr.get_as::<&str>()
740            && *rename == name
741        {
742            return Ok(variant);
743        }
744    }
745
746    // Then check kebab-case conversion of variant name
747    for variant in enum_type.variants {
748        let kebab_name = variant.name.to_kebab_case();
749        tracing::trace!(
750            "  checking variant '{}' -> kebab '{}' against '{}'",
751            variant.name,
752            kebab_name,
753            name
754        );
755        if kebab_name == name {
756            return Ok(variant);
757        }
758    }
759
760    // Finally check exact name match
761    for variant in enum_type.variants {
762        if variant.name == name {
763            return Ok(variant);
764        }
765    }
766
767    Err(ArgsErrorKind::UnknownSubcommand {
768        provided: name.to_string(),
769        variants: enum_type.variants,
770    })
771}
772
773/// Find a field marked with args::subcommand
774fn find_subcommand_field(fields: &'static [Field]) -> Option<(usize, &'static Field)> {
775    fields
776        .iter()
777        .enumerate()
778        .find(|(_, f)| f.has_attr(Some("args"), "subcommand"))
779}
780
781/// Result of `split`
782#[derive(Debug, PartialEq)]
783struct SplitToken<'input> {
784    s: &'input str,
785    span: Span,
786}
787
788/// Split on `=`, e.g. `a=b` returns (`a`, `b`).
789/// Span-aware. If `=` is not contained in the input string,
790/// returns None
791fn split<'input>(input: &'input str, span: Span) -> Option<Vec<SplitToken<'input>>> {
792    let equals_index = input.find('=')?;
793
794    let l = &input[0..equals_index];
795    let l_span = Span::new(span.start, l.len());
796
797    let r = &input[equals_index + 1..];
798    let r_span = Span::new(equals_index + 1, r.len());
799
800    Some(vec![
801        SplitToken { s: l, span: l_span },
802        SplitToken { s: r, span: r_span },
803    ])
804}
805
806#[test]
807fn test_split() {
808    assert_eq!(split("ababa", Span::new(5, 5)), None);
809    assert_eq!(
810        split("foo=bar", Span::new(0, 7)),
811        Some(vec![
812            SplitToken {
813                s: "foo",
814                span: Span::new(0, 3)
815            },
816            SplitToken {
817                s: "bar",
818                span: Span::new(4, 3)
819            },
820        ])
821    );
822    assert_eq!(
823        split("foo=", Span::new(0, 4)),
824        Some(vec![
825            SplitToken {
826                s: "foo",
827                span: Span::new(0, 3)
828            },
829            SplitToken {
830                s: "",
831                span: Span::new(4, 0)
832            },
833        ])
834    );
835    assert_eq!(
836        split("=bar", Span::new(0, 4)),
837        Some(vec![
838            SplitToken {
839                s: "",
840                span: Span::new(0, 0)
841            },
842            SplitToken {
843                s: "bar",
844                span: Span::new(1, 3)
845            },
846        ])
847    );
848}
849
850impl<'input> Context<'input> {
851    /// Process a short flag that may contain chained flags or an attached value.
852    ///
853    /// This function handles three cases:
854    /// 1. Single flag: `-v` → process as bool or look for value in next arg
855    /// 2. Chained bool flags: `-abc` → recursively process `-a`, then `-bc`, then `-c`
856    /// 3. Attached value: `-j4` → process `-j` with value `4`
857    ///
858    /// The function is recursive for chained flags, maintaining proper span tracking
859    /// and index management. Only increments `self.index` at the leaf of recursion.
860    fn process_short_flag(
861        &mut self,
862        mut p: Partial<'static>,
863        flag: &'input str,
864        flag_span: Span,
865        fields: &'static [Field],
866    ) -> Result<Partial<'static>, ArgsErrorKind> {
867        // Get the first character as the flag
868        let first_char = flag.chars().next().unwrap();
869        let first_char_str = &flag[..first_char.len_utf8()];
870        let rest = &flag[first_char.len_utf8()..];
871
872        tracing::trace!("Looking up short flag '{first_char}' (rest: '{rest}')");
873
874        // Look up the field for this character
875        let Some(field_index) = find_field_index_with_short_char(fields, first_char_str) else {
876            // Error: unknown flag, report just the first character with precise span
877            let char_span = Span::new(flag_span.start, first_char.len_utf8());
878            return Err(ArgsErrorKind::UnknownShortFlag {
879                flag: first_char_str.to_string(),
880                fields,
881                precise_span: Some(char_span),
882            });
883        };
884
885        let field = &fields[field_index];
886        let field_shape = field.shape();
887
888        // Check if the field is bool or Vec<bool>
889        let is_bool = field_shape.is_shape(bool::SHAPE);
890        let is_bool_list = if let facet_core::Def::List(list_def) = field_shape.def {
891            list_def.t.is_shape(bool::SHAPE)
892        } else {
893            false
894        };
895
896        if rest.is_empty() {
897            // Leaf case: last character in the chain
898            if is_bool || is_bool_list {
899                // Bool or Vec<bool> at the end of chain
900                p = p.begin_nth_field(field_index)?;
901
902                if is_bool_list {
903                    // For Vec<bool> fields, initialize list and push an item
904                    p = p.begin_list()?;
905                    p = p.begin_list_item()?;
906                    p = p.set(true)?;
907                    p = p.end()?; // end list item
908                } else {
909                    // For simple bool fields, just set to true
910                    p = p.set(true)?;
911                }
912
913                p = p.end()?; // end field
914                self.index += 1; // Move to next arg
915            } else {
916                // Non-bool field: use handle_field which looks for value in next arg
917                p = self.handle_field(p, field_index, None)?;
918            }
919        } else if is_bool || is_bool_list {
920            // Bool flag with trailing chars: could be chaining like `-abc` or `-vvv`
921            // Process current bool flag without going through handle_field
922            // (which would increment index and consume next arg)
923            p = p.begin_nth_field(field_index)?;
924
925            if is_bool_list {
926                // For Vec<bool> fields, we need to initialize the list and push an item
927                p = p.begin_list()?;
928                p = p.begin_list_item()?;
929                p = p.set(true)?;
930                p = p.end()?; // end list item
931            } else {
932                // For simple bool fields, just set to true
933                p = p.set(true)?;
934            }
935
936            p = p.end()?; // end field
937
938            // Recursively process remaining characters as a new short flag chain
939            let rest_span = Span::new(flag_span.start + first_char.len_utf8(), rest.len());
940            p = self.process_short_flag(p, rest, rest_span, fields)?;
941            // Note: index increment happens in the leaf recursion
942        } else {
943            // Non-bool flag with attached value: `-j4`
944            let value_span = Span::new(flag_span.start + first_char.len_utf8(), rest.len());
945            p = self.handle_field(
946                p,
947                field_index,
948                Some(SplitToken {
949                    s: rest,
950                    span: value_span,
951                }),
952            )?;
953        }
954
955        Ok(p)
956    }
957}
958
959/// Given an array of fields, find the field with the given `args::short = 'a'`
960/// annotation. Uses extension attribute syntax: #[facet(args::short = "j")]
961/// The `short` parameter should be a single character (as a string slice).
962fn find_field_index_with_short_char(fields: &'static [Field], short: &str) -> Option<usize> {
963    let short_char = short.chars().next()?;
964    fields.iter().position(|f| {
965        if let Some(ext) = f.get_attr(Some("args"), "short") {
966            // The attribute stores the full Attr enum
967            if let Some(crate::Attr::Short(opt_char)) = ext.get_as::<crate::Attr>() {
968                match opt_char {
969                    Some(c) => *c == short_char,
970                    None => {
971                        // No explicit short specified, use first char of field name
972                        f.name.starts_with(short_char)
973                    }
974                }
975            } else {
976                false
977            }
978        } else {
979            false
980        }
981    })
982}