Skip to main content

usage/go/
mod.rs

1//! Emitting Go parse tables from a spec.
2//!
3//! The Go side of usage has no derive macro to emit its tables, because Go has no
4//! macros: what a Rust CLI gets from `#[derive(Cli)]` at compile time, a Go CLI
5//! gets from this, at build time, through `go:generate`. The output is a plain Go
6//! file an author checks in and a reviewer can read.
7//!
8//! # What it emits, and what it does not
9//!
10//! Two tables, kept apart on purpose. The hot one is what binding reads: which
11//! token becomes which flag or argument, and nothing else. The cold one — `Meta` —
12//! carries what the rules decided after the last token need: `required`,
13//! `choices`, `default`, `env`, the var bounds, and the four that compare one
14//! entry against another. A parse never touches the second.
15//!
16//! Help text is in neither. mise's runs to several hundred kilobytes, and a table
17//! carrying it would put all of that in front of the parser; rendering help is its
18//! own cold table and its own piece of work.
19//!
20//! # Why package-level `var` and not `const`
21//!
22//! Go has no `const` for composite data. What it does have is a linker that
23//! statically initializes package-level variables holding plain data, which is
24//! the property the whole design rests on: `go tool nm` reports these symbols as
25//! type `D`, and the generated package has no `init` function. So a 211-command
26//! table costs bytes in the binary and no instructions at startup — the thing
27//! cobra and kong each pay a million or more for.
28//!
29//! Commands are emitted as separate variables rather than one nested literal
30//! because `default_subcommand` has to point at a node inside the tree, and a
31//! composite literal cannot refer to its own interior.
32
33mod structs;
34
35use std::collections::{BTreeMap, HashMap};
36use std::fmt::Write as _;
37
38use heck::AsPascalCase;
39
40use crate::spec::unknown_flags::UnknownFlags;
41use crate::{
42    Spec, SpecArg, SpecChoices, SpecCommand, SpecDoubleDashChoices, SpecFlag, SpecFlagAction,
43};
44
45/// How to emit.
46#[derive(Debug, Clone, Default)]
47pub struct GoOptions {
48    /// The Go package clause. Defaults to the spec's `bin`, made into an
49    /// identifier.
50    ///
51    /// Must satisfy [`is_valid_package`]. A caller taking this from a user should
52    /// check it and say so; one that does not gets it sanitized, because emitting a
53    /// file that cannot compile helps nobody.
54    pub package: Option<String>,
55}
56
57/// Turn a spec into a Go source file declaring its parse tables.
58pub fn generate(spec: &Spec, opts: &GoOptions) -> String {
59    Emitter::new(spec, opts).run()
60}
61
62/// One entry's identifiers: the exported key constant, and for a command the
63/// variable holding it.
64struct Named {
65    key: String,
66    var: String,
67    number: u64,
68}
69
70struct Emitter<'a> {
71    spec: &'a Spec,
72    package: String,
73    /// Every identifier handed out, so a second entry wanting the same spelling
74    /// gets a suffix instead of silently colliding.
75    taken: HashMap<String, u32>,
76    /// Assigned in emission order, so a key is stable as long as the spec is.
77    next_key: u64,
78    out: String,
79}
80
81impl<'a> Emitter<'a> {
82    fn new(spec: &'a Spec, opts: &GoOptions) -> Self {
83        // An explicit package that is not an identifier is sanitized rather than
84        // emitted: a caller that wants to reject it should ask `is_valid_package`
85        // first, which the CLI does.
86        let package = match opts.package.as_deref() {
87            Some(name) if is_valid_package(name) => name.to_string(),
88            Some(name) => package_ident(name),
89            None => package_ident(&spec.bin),
90        };
91        Emitter {
92            spec,
93            package,
94            taken: HashMap::new(),
95            next_key: 0,
96            out: String::new(),
97        }
98    }
99
100    /// Reserve an identifier, adding a numeric suffix if the spelling is taken.
101    ///
102    /// Collisions are ordinary rather than exotic: mise has both a `macos-defaults`
103    /// command and a `macos defaults` path, and both want to be spelled
104    /// `CmdMacosDefaults`.
105    ///
106    /// The suffixed spelling is reserved too, and the loop is what makes that
107    /// safe. Counting alone was not enough: `macos-defaults` and `macos defaults`
108    /// produce `CmdMacosDefaults` and `CmdMacosDefaults2`, and a third command
109    /// named `macos-defaults2` asks for `CmdMacosDefaults2` directly — which was
110    /// unclaimed, so the file declared it twice and did not compile.
111    fn unique(&mut self, base: &str) -> String {
112        let mut n = self.taken.get(base).copied().unwrap_or(0);
113        loop {
114            n += 1;
115            let candidate = if n == 1 {
116                base.to_string()
117            } else {
118                format!("{base}{n}")
119            };
120            if !self.taken.contains_key(&candidate) {
121                self.taken.insert(base.to_string(), n);
122                // The spelling itself, so a later entry that asks for it by name is
123                // suffixed rather than handed a duplicate.
124                self.taken.entry(candidate.clone()).or_insert(0);
125                return candidate;
126            }
127        }
128    }
129
130    fn name(&mut self, prefix: &str, path: &[&str], own: &str) -> Named {
131        let mut base = String::from(prefix);
132        for segment in path {
133            let _ = write!(base, "{}", AsPascalCase(segment));
134        }
135        let _ = write!(base, "{}", AsPascalCase(own));
136        let key = self.unique(&base);
137        self.next_key += 1;
138        Named {
139            var: format!("cmd{}", &key[prefix.len()..]),
140            key,
141            number: self.next_key,
142        }
143    }
144
145    fn run(mut self) -> String {
146        // Collected first so the constants can be emitted in one block before any
147        // table refers to them, which is also the order a reader wants: the names
148        // they will switch on, then the data.
149        let mut commands = Vec::new();
150        self.collect(&self.spec.cmd.clone(), &[], true, &mut commands);
151
152        self.header();
153        self.constants(&commands);
154        self.tables(&commands);
155        self.metadata(&commands);
156        self.help_table(&commands);
157        structs::emit(&mut self.out, &commands);
158
159        // Each command is followed by a blank line, which leaves one at the end of
160        // the file. gofmt strips it, and a generated file that is not gofmt-clean
161        // is one every adopter has to run a formatter over before committing.
162        let trimmed = self.out.trim_end().len();
163        self.out.truncate(trimmed);
164        self.out.push('\n');
165        self.out
166    }
167
168    /// Walk the tree, naming everything, so that emission is a second pass with no
169    /// lookaheads.
170    fn collect(&mut self, cmd: &SpecCommand, path: &[&str], root: bool, out: &mut Vec<Emitted>) {
171        let named = if root {
172            self.next_key += 1;
173            Named {
174                // Claimed through the same counter as everything else, not just
175                // spelled: a subcommand named `root` would otherwise be handed
176                // `CmdRoot` too, and the file would declare the constant twice and
177                // fail to compile.
178                key: self.unique("CmdRoot"),
179                var: "Root".to_string(),
180                number: self.next_key,
181            }
182        } else {
183            self.name("Cmd", &path[..path.len() - 1], path[path.len() - 1])
184        };
185
186        let flags = cmd
187            .flags
188            .iter()
189            .map(|f| (f.clone(), self.name("Flag", path, &f.name)))
190            .collect::<Vec<_>>();
191        let args = cmd
192            .args
193            .iter()
194            .map(|a| (a.clone(), self.name("Arg", path, &a.name)))
195            .collect::<Vec<_>>();
196
197        let index = out.len();
198        out.push(Emitted {
199            named,
200            cmd: cmd.clone(),
201            flags,
202            args,
203            subcommands: Vec::new(),
204            root,
205        });
206
207        // Declaration order, not sorted: a recent change made the spec hold the
208        // order a CLI declares its commands in, and a generated file that reordered
209        // them would lose it for no gain — lookup is by name either way.
210        let mut children = Vec::new();
211        for (name, sub) in &cmd.subcommands {
212            // An alias appears in `subcommands` under its own key as well as the
213            // canonical name; emitting it twice would declare two commands where the
214            // spec has one.
215            if name != &sub.name {
216                continue;
217            }
218            let mut child_path = path.to_vec();
219            child_path.push(name);
220            let at = out.len();
221            self.collect(sub, &child_path, false, out);
222            children.push(at);
223        }
224        out[index].subcommands = children;
225    }
226
227    fn header(&mut self) {
228        let _ = writeln!(
229            self.out,
230            "// Code generated by `usage generate go`. DO NOT EDIT.\n\
231             //\n\
232             // Binding tables for `{}`, read by\n\
233             // [github.com/jdx/usage/go/argv]. Regenerate rather than editing: the spec is\n\
234             // the definition, and a hand-edit here is a difference no reviewer can see.\n\
235             //\n\
236             // These are package-level variables holding plain data, so the linker lays them\n\
237             // out and nothing runs before main.\n\
238             \n\
239             package {}\n\
240             \n\
241             import \"github.com/jdx/usage/go/argv\"\n",
242            self.spec.bin, self.package
243        );
244
245        if let Some(version) = self
246            .spec
247            .version
248            .as_ref()
249            .or(self.spec.long_version.as_ref())
250        {
251            let _ = writeln!(
252                self.out,
253                "// Version is what the spec declares, so a caller answering `--version` has it\n\
254                 // without the parse tables carrying a string binding never reads.\n\
255                 const Version = {}\n",
256                go_string(version)
257            );
258        }
259        if let Some(version) = &self.spec.long_version {
260            let _ = writeln!(
261                self.out,
262                "// LongVersion is the extended text printed for `--version`; `-V` uses Version.\n\
263                 const LongVersion = {}\n",
264                go_string(version)
265            );
266        }
267    }
268
269    fn constants(&mut self, commands: &[Emitted]) {
270        let _ = writeln!(
271            self.out,
272            "// Keys identify a table entry without a string comparison: switch on the Key an\n\
273             // event carries rather than on its Name, which is there for diagnostics.\n\
274             const ("
275        );
276        let mut entries: Vec<(&str, u64)> = Vec::new();
277        for e in commands {
278            entries.push((&e.named.key, e.named.number));
279            entries.extend(e.flags.iter().map(|(_, n)| (n.key.as_str(), n.number)));
280            entries.extend(e.args.iter().map(|(_, n)| (n.key.as_str(), n.number)));
281        }
282        // One run, so every name pads to the longest — which is what gofmt does to
283        // a const block with no blank line in it.
284        let width = entries.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
285        for (key, number) in entries {
286            let _ = writeln!(
287                self.out,
288                "\t{key}{:pad$} uint64 = {number}",
289                "",
290                pad = width - key.len()
291            );
292        }
293        let _ = writeln!(self.out, ")\n");
294    }
295
296    fn tables(&mut self, commands: &[Emitted]) {
297        // Resolved once, against the root's *direct* subcommands, because the spec
298        // declares it once at the top and it names one of them. Searching the whole
299        // tree instead is what wired mise's `default_subcommand run` to `oci run`,
300        // which comes first in a depth-first walk — the parser would then have
301        // descended into a command that is not the root's child at all. A name
302        // nothing answers to is left unset rather than guessed at.
303        let default_subcommand = self.spec.default_subcommand.as_ref().and_then(|name| {
304            let direct = || commands[0].subcommands.iter().map(|at| &commands[*at]);
305            // Names before aliases, as the grammar says: a command's own name outranks
306            // another command's alias, so which one this resolves to does not depend on
307            // the order the spec declares them in.
308            direct()
309                .find(|e| &e.cmd.name == name)
310                .or_else(|| {
311                    direct().find(|e| {
312                        e.cmd.aliases.contains(name) || e.cmd.hidden_aliases.contains(name)
313                    })
314                })
315                .map(|e| e.named.var.clone())
316        });
317
318        for (i, e) in commands.iter().enumerate() {
319            let doc = if e.root {
320                format!(
321                    "// Root is the command tree for `{}`. Pass it to argv.New.",
322                    self.spec.bin
323                )
324            } else {
325                format!("// {}", e.cmd.full_cmd.join(" "))
326            };
327            let mut lines = vec![
328                Line::Field("Name".into(), go_string(&e.cmd.name)),
329                Line::Field("Key".into(), e.named.key.clone()),
330            ];
331
332            let aliases: Vec<&String> = e
333                .cmd
334                .aliases
335                .iter()
336                .chain(e.cmd.hidden_aliases.iter())
337                .collect();
338            if !aliases.is_empty() {
339                // A hidden alias selects a command exactly as a visible one does:
340                // hiding is about help output, which binding never reads.
341                let list = aliases
342                    .iter()
343                    .map(|a| go_string(a))
344                    .collect::<Vec<_>>()
345                    .join(", ");
346                lines.push(Line::Field("Aliases".into(), format!("[]string{{{list}}}")));
347            }
348
349            if !e.flags.is_empty() {
350                let mut block = vec!["Flags: []*argv.Flag{".to_string()];
351                for (flag, named) in &e.flags {
352                    block.push(format!("\t{},", flag_literal(flag, named)));
353                }
354                block.push("},".to_string());
355                lines.push(Line::Block(block));
356            }
357
358            if !e.args.is_empty() {
359                let mut block = vec!["Args: []*argv.Arg{".to_string()];
360                for (arg, named) in &e.args {
361                    block.push(format!("\t{},", arg_literal(arg, named)));
362                }
363                block.push("},".to_string());
364                lines.push(Line::Block(block));
365            }
366
367            if !e.subcommands.is_empty() {
368                let list = e
369                    .subcommands
370                    .iter()
371                    .map(|at| commands[*at].named.var.clone())
372                    .collect::<Vec<_>>()
373                    .join(", ");
374                lines.push(Line::Field(
375                    "Subcommands".into(),
376                    format!("[]*argv.Command{{{list}}}"),
377                ));
378            }
379
380            // Already resolved: inheritance is the generator's job, so that the
381            // parser reads one field rather than walking ancestors per token.
382            if effective_unknown_flags(self.spec, commands, i) == UnknownFlags::Error {
383                lines.push(Line::Field(
384                    "UnknownFlags".into(),
385                    "argv.UnknownFlagsError".into(),
386                ));
387            }
388
389            if e.cmd.external_subcommand {
390                lines.push(Line::Field("ExternalSubcommand".into(), "true".into()));
391            }
392            if e.cmd.arg_required_else_help {
393                lines.push(Line::Field("ArgRequiredElseHelp".into(), "true".into()));
394            }
395            if e.cmd.disable_help_flag {
396                lines.push(Line::Field("DisableHelpFlag".into(), "true".into()));
397            }
398            if e.cmd.disable_help_subcommand {
399                lines.push(Line::Field("DisableHelpSubcommand".into(), "true".into()));
400            }
401            if e.cmd.disable_version_flag {
402                lines.push(Line::Field("DisableVersionFlag".into(), "true".into()));
403            }
404            if e.cmd.subcommand_negates_reqs {
405                lines.push(Line::Field("SubcommandNegatesReqs".into(), "true".into()));
406            }
407            if e.cmd.args_conflicts_with_subcommands {
408                lines.push(Line::Field(
409                    "ArgsConflictWithSubcommands".into(),
410                    "true".into(),
411                ));
412            }
413            if e.cmd.subcommand_precedence_over_arg {
414                lines.push(Line::Field(
415                    "SubcommandPrecedenceOverArg".into(),
416                    "true".into(),
417                ));
418            }
419            if e.cmd.allow_missing_positional {
420                lines.push(Line::Field("AllowMissingPositional".into(), "true".into()));
421            }
422            if e.cmd.dont_delimit_trailing_values {
423                lines.push(Line::Field(
424                    "DontDelimitTrailingValues".into(),
425                    "true".into(),
426                ));
427            }
428            if e.root {
429                if let Some(var) = &default_subcommand {
430                    lines.push(Line::Field("DefaultSubcommand".into(), var.clone()));
431                }
432                if self.spec.version.is_some() || self.spec.long_version.is_some() {
433                    // Only where the CLI declares a version: a `--version` that answers
434                    // with nothing is worse than one that is not there.
435                    lines.push(Line::Field("Version".into(), "true".into()));
436                }
437            }
438
439            let _ = writeln!(self.out, "{doc}");
440            let _ = writeln!(self.out, "var {} = &argv.Command{{", e.named.var);
441            render(&mut self.out, "\t", &lines);
442            let _ = writeln!(self.out, "}}\n");
443        }
444    }
445}
446
447impl Emitter<'_> {
448    /// Emit the cold table: everything binding deliberately does not know.
449    ///
450    /// Indexed by key, which is what makes a lookup an index rather than a map —
451    /// and a Go map would have to be built at init, which is the one thing these
452    /// tables are for avoiding. Keys are handed out to commands as well as to
453    /// flags and arguments, and a command has no cold half, so its slot is an
454    /// empty entry rather than a gap: `Metadata.Lookup` checks the key it finds
455    /// and reports nothing when it does not match, so an empty slot answers
456    /// correctly and the index stays dense.
457    fn metadata(&mut self, commands: &[Emitted]) {
458        // By key, so the slice can be written in one pass in index order.
459        let mut by_key: BTreeMap<u64, String> = BTreeMap::new();
460        for e in commands {
461            for (flag, named) in &e.flags {
462                by_key.insert(named.number, self.flag_meta(flag, named, e, commands));
463            }
464            for (arg, named) in &e.args {
465                by_key.insert(named.number, arg_meta(self.spec, arg, named, e, commands));
466            }
467        }
468
469        let total = commands
470            .iter()
471            .map(|e| 1 + e.flags.len() + e.args.len())
472            .sum::<usize>() as u64;
473
474        let _ = writeln!(
475            self.out,
476            "// Meta is the cold table, read only by the rules that are decided once the\n\
477             // last token has been read: required, choices, the env-then-default fallback,\n\
478             // the var bounds, and the four that compare one entry against another. A parse\n\
479             // never touches it.\n\
480             //\n\
481             // Indexed by key, so entry Key sits at Meta[Key-1]. A command's slot is empty:\n\
482             // commands take keys too, and have no cold half.\n\
483             var Meta = argv.Metadata{{"
484        );
485        for key in 1..=total {
486            match by_key.get(&key) {
487                Some(entry) => {
488                    let _ = writeln!(self.out, "\t{entry},");
489                }
490                None => {
491                    let _ = writeln!(self.out, "\t{{}},");
492                }
493            }
494        }
495        let _ = writeln!(self.out, "}}\n");
496    }
497
498    /// The cold half of a flag.
499    fn flag_meta(
500        &self,
501        flag: &SpecFlag,
502        named: &Named,
503        owner: &Emitted,
504        commands: &[Emitted],
505    ) -> String {
506        let mut fields = vec![
507            format!("Key: {}", named.key),
508            format!("Name: {}", go_string(&flag.name)),
509            "Flag: true".to_string(),
510        ];
511        if !owner.cmd.args_override_self
512            && !flag.var
513            && !flag.count
514            && !flag.arg.as_ref().is_some_and(|arg| arg.var)
515        {
516            fields.push("RejectDuplicate: true".to_string());
517        }
518        if flag.required {
519            fields.push("Required: true".to_string());
520        }
521        if flag.arg.is_none() {
522            fields.push("RequiresIfBoolean: true".to_string());
523        }
524        // How a user types it, worked out where the forms are visible: the rules
525        // that judge an entry never see a flag, and guessing from the name gets a
526        // one-letter long form and a short the wrong way round.
527        if let Some(long) = flag.long.first() {
528            fields.push(format!("Spelling: {}", go_string(&format!("--{long}"))));
529        } else if let Some(short) = flag.short.first() {
530            fields.push(format!("Spelling: {}", go_string(&format!("-{short}"))));
531        }
532        // What the value is called, which is what says whether a path belongs
533        // there — `--into <DIR>` completes directories because of the name.
534        if let Some(value) = flag.arg.as_ref() {
535            fields.push(format!("ValueName: {}", go_string(&value.name)));
536        }
537        let named_value = flag
538            .arg
539            .as_ref()
540            .map(|a| a.name.as_str())
541            .unwrap_or(flag.name.as_str());
542        if let Some(kind) = complete_type(self.spec, named_value) {
543            fields.push(format!("CompleteType: {}", go_string(kind)));
544        }
545        // Written on the value a flag takes, never on the flag.
546        if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) {
547            fields.push(format!(
548                "Choices: {}",
549                string_slice(&visible_choices(choices))
550            ));
551            fields.push(format!(
552                "AcceptedChoices: {}",
553                string_slice(&accepted_choices(choices))
554            ));
555            if choices.ignore_case {
556                fields.push("IgnoreCase: true".to_string());
557            }
558            if !choices.strict {
559                fields.push("AllowUnknownChoices: true".to_string());
560            }
561        }
562        // A default can be written in either place, and usage-lib falls back to
563        // the one on the value. `env` deliberately does not follow the same
564        // nesting, because usage-lib does not read it there either.
565        let default = if !flag.default.is_empty() {
566            &flag.default
567        } else {
568            flag.arg
569                .as_ref()
570                .map(|a| &a.default)
571                .unwrap_or(&flag.default)
572        };
573        if !default.is_empty() {
574            fields.push(format!("Default: {}", string_slice(default)));
575        }
576        if let Some(env) = &flag.env {
577            fields.push(format!("Env: {}", go_string(env)));
578        }
579        if !flag.env_fallback.is_empty() {
580            fields.push(format!("EnvFallback: {}", string_slice(&flag.env_fallback)));
581        }
582        if !flag.deprecated_env.is_empty() {
583            fields.push(format!(
584                "DeprecatedEnv: {}",
585                string_slice(&flag.deprecated_env)
586            ));
587        }
588        let minimum = flag
589            .arg
590            .as_ref()
591            .filter(|arg| arg.var)
592            .and_then(|arg| arg.var_min)
593            .or(flag.var_min);
594        if let Some(min) = minimum {
595            fields.push(format!("VarMin: {}", clamp_var_max(min)));
596        }
597        // Occurrences. The per-occurrence value bound is a limit binding applies
598        // and lives on the parse table.
599        if let Some(max) = flag.var_max {
600            fields.push(format!("VarMax: {}", clamp_var_max(max)));
601        }
602
603        for (label, names) in [
604            ("Conflicts", &flag.conflicts),
605            ("Overrides", &flag.overrides),
606            ("RequiredUnless", &flag.required_unless),
607            ("RequiredUnlessAll", &flag.required_unless_all),
608            ("RequiredIf", &flag.required_if),
609            ("Requires", &flag.requires),
610        ] {
611            let keys = resolve_relationship(names, owner, commands);
612            if !keys.is_empty() {
613                fields.push(format!("{label}: {}", key_slice(&keys)));
614            }
615        }
616        for (label, conditions) in [
617            ("RequiredIfEq", &flag.required_if_eq),
618            ("RequiredIfEqAll", &flag.required_if_eq_all),
619        ] {
620            let values = conditions
621                .iter()
622                .filter_map(|condition| {
623                    resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
624                        .into_iter()
625                        .next()
626                        .map(|key| {
627                            format!("{{Key: {key}, Value: {}}}", go_string(&condition.value))
628                        })
629                })
630                .collect::<Vec<_>>();
631            if !values.is_empty() {
632                fields.push(format!(
633                    "{label}: []argv.ValueCondition{{{}}}",
634                    values.join(", ")
635                ));
636            }
637        }
638        let requires_if = flag
639            .requires_if
640            .iter()
641            .filter_map(|condition| {
642                resolve_relationship(std::slice::from_ref(&condition.requires), owner, commands)
643                    .into_iter()
644                    .next()
645                    .map(|key| format!("{{Value: {}, Key: {key}}}", go_string(&condition.value)))
646            })
647            .collect::<Vec<_>>();
648        if !requires_if.is_empty() {
649            fields.push(format!(
650                "RequiresIf: []argv.ValueRequirement{{{}}}",
651                requires_if.join(", ")
652            ));
653        }
654        let default_if = flag
655            .default_if
656            .iter()
657            .filter_map(|condition| {
658                resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
659                    .into_iter()
660                    .next()
661                    .map(|key| match &condition.when {
662                        None => format!("{{Key: {key}, Value: {}}}", go_string(&condition.value)),
663                        Some(when) => format!(
664                            "{{Key: {key}, When: {}, Value: {}}}",
665                            go_string(when),
666                            go_string(&condition.value)
667                        ),
668                    })
669            })
670            .collect::<Vec<_>>();
671        if !default_if.is_empty() {
672            fields.push(format!(
673                "DefaultIf: []argv.DefaultIf{{{}}}",
674                default_if.join(", ")
675            ));
676        }
677
678        format!("{{{}}}", fields.join(", "))
679    }
680}
681
682/// The cold half of a positional argument.
683/// The type a spec's `complete` block names for an entry, if it names one.
684///
685/// By lowercased name, which is how usage-lib files them: `complete "FILE"` and
686/// an argument written `<file>` are the same position as far as the reference is
687/// concerned.
688fn complete_type<'a>(spec: &'a Spec, name: &str) -> Option<&'a str> {
689    spec.complete
690        .get(&name.to_lowercase())
691        .and_then(|c| c.type_.as_deref())
692}
693
694fn arg_meta(
695    spec: &Spec,
696    arg: &SpecArg,
697    named: &Named,
698    owner: &Emitted,
699    commands: &[Emitted],
700) -> String {
701    let mut fields = vec![
702        format!("Key: {}", named.key),
703        format!("Name: {}", go_string(&arg.name)),
704    ];
705    if arg.required {
706        fields.push("Required: true".to_string());
707    }
708    // What the position takes, where the spec said so. Read by completion rather
709    // than by any post-binding rule: an author who wrote `complete "input"
710    // type="file"` named what belongs there, and the alternative is inferring it
711    // from a name they did not choose.
712    if let Some(kind) = complete_type(spec, &arg.name) {
713        fields.push(format!("CompleteType: {}", go_string(kind)));
714    }
715    if let Some(choices) = &arg.choices {
716        fields.push(format!(
717            "Choices: {}",
718            string_slice(&visible_choices(choices))
719        ));
720        fields.push(format!(
721            "AcceptedChoices: {}",
722            string_slice(&accepted_choices(choices))
723        ));
724        if choices.ignore_case {
725            fields.push("IgnoreCase: true".to_string());
726        }
727        if !choices.strict {
728            fields.push("AllowUnknownChoices: true".to_string());
729        }
730    }
731    if !arg.default.is_empty() {
732        fields.push(format!("Default: {}", string_slice(&arg.default)));
733    }
734    if let Some(env) = &arg.env {
735        fields.push(format!("Env: {}", go_string(env)));
736    }
737    if !arg.env_fallback.is_empty() {
738        fields.push(format!("EnvFallback: {}", string_slice(&arg.env_fallback)));
739    }
740    if !arg.deprecated_env.is_empty() {
741        fields.push(format!(
742            "DeprecatedEnv: {}",
743            string_slice(&arg.deprecated_env)
744        ));
745    }
746    if let Some(min) = arg.var_min {
747        fields.push(format!("VarMin: {}", clamp_var_max(min)));
748    }
749    let conflicts = resolve_relationship(&arg.conflicts, owner, commands);
750    if !conflicts.is_empty() {
751        fields.push(format!("Conflicts: {}", key_slice(&conflicts)));
752    }
753    for (label, names) in [
754        ("Requires", &arg.requires),
755        ("RequiredIf", &arg.required_if),
756        ("RequiredUnless", &arg.required_unless),
757        ("RequiredUnlessAll", &arg.required_unless_all),
758    ] {
759        let keys = resolve_relationship(names, owner, commands);
760        if !keys.is_empty() {
761            fields.push(format!("{label}: {}", key_slice(&keys)));
762        }
763    }
764    for (label, conditions) in [
765        ("RequiredIfEq", &arg.required_if_eq),
766        ("RequiredIfEqAll", &arg.required_if_eq_all),
767    ] {
768        let values = conditions
769            .iter()
770            .filter_map(|condition| {
771                resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
772                    .into_iter()
773                    .next()
774                    .map(|key| format!("{{Key: {key}, Value: {}}}", go_string(&condition.value)))
775            })
776            .collect::<Vec<_>>();
777        if !values.is_empty() {
778            fields.push(format!(
779                "{label}: []argv.ValueCondition{{{}}}",
780                values.join(", ")
781            ));
782        }
783    }
784    // No VarMax: for an argument the bound is a limit binding applies, which is
785    // what makes `[a]… [b]` fillable at all, so judging it again would fail an
786    // invocation that never broke it.
787    format!("{{{}}}", fields.join(", "))
788}
789
790/// Turn the names in a relationship into the keys they refer to.
791///
792/// Resolved here, where the whole command is visible, so that nothing downstream
793/// searches by name on a path it would repeat per parse. The names arrive as
794/// written — `--stdin`, dashes and all — so they are matched against a flag's long
795/// forms, its shorts, and the name the spec gives it.
796///
797/// A name nothing answers to is dropped. That is a spec bug worth reporting, but
798/// this function has no way to; the check belongs beside the duplicate-form and
799/// duplicate-key checks that already run where the whole tree is visible.
800fn resolve_relationship(names: &[String], owner: &Emitted, commands: &[Emitted]) -> Vec<String> {
801    let mut out = Vec::new();
802    for name in names {
803        // The declaring command's own flags first, then any ancestor's globals —
804        // the scope a token has, in the order a token gets it, so a subcommand
805        // redeclaring an inherited name shadows it here as it does at parse time.
806        let mut found = match_flag(owner, name, false);
807        if found.is_none() && !name.starts_with('-') {
808            found = owner
809                .args
810                .iter()
811                .find(|(arg, _)| arg.name == *name)
812                .map(|(_, named)| named.key.clone());
813        }
814        if found.is_none() {
815            let path = &owner.cmd.full_cmd;
816            for depth in (0..path.len()).rev() {
817                let ancestor = commands
818                    .iter()
819                    .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]);
820                if let Some(key) = ancestor.and_then(|a| match_flag(a, name, true)) {
821                    found = Some(key);
822                    break;
823                }
824            }
825        }
826        if let Some(key) = found {
827            out.push(key);
828        }
829    }
830    out
831}
832
833/// Find a flag by any spelling a declaration may use for it.
834///
835/// The negation counts, and resolves to the same entry: usage-lib treats
836/// `conflicts = "--no-color"` as naming the `color` flag and reports the conflict
837/// whichever of the two spellings was typed. The relationship is between entries
838/// rather than between tokens, which is what the key model already assumes.
839fn match_flag(cmd: &Emitted, name: &str, globals_only: bool) -> Option<String> {
840    // Two passes, in the order the parser itself looks: every ordinary form
841    // first, then negations.
842    //
843    // That order is not a nicety. The parser tries every long form before it
844    // tries any negation, so with `--a` declaring `negate = "--zap"` and a
845    // separate `--zap`, typing `--zap` binds *zap*. A per-candidate search hands
846    // the relationship to `a`, and the table then enforces a rule against a flag
847    // the command line never binds. The table has to agree with the binder it
848    // feeds.
849    let eligible = |flag: &SpecFlag| !globals_only || flag.global;
850
851    // The form is part of the name: `--q` does not reach the short `-q`, and
852    // `-color` does not reach the long `--color`. usage-lib resolves neither.
853    let (long, short, bare) = if let Some(rest) = name.strip_prefix("--") {
854        (Some(rest), None, None)
855    } else if let Some(rest) = name.strip_prefix('-') {
856        let mut chars = rest.chars();
857        match (chars.next(), chars.next()) {
858            (Some(c), None) => (None, Some(c), None),
859            _ => (None, None, None),
860        }
861    } else {
862        (None, None, Some(name))
863    };
864
865    let ordinary = cmd.flags.iter().find(|(flag, _)| {
866        if !eligible(flag) {
867            return false;
868        }
869        if let Some(bare) = bare {
870            return flag.name == bare;
871        }
872        if let Some(long) = long {
873            return flag.long.iter().any(|l| l == long);
874        }
875        short.is_some_and(|c| flag.short.contains(&c))
876    });
877    if let Some((_, named)) = ordinary {
878        return Some(named.key.clone());
879    }
880
881    // Negations, compared exactly as both sides were written — dashes included.
882    // `negate = "-no-tint"` is named by `-no-tint` and not by `--no-tint`, and
883    // usage-lib resolves it that way round too.
884    cmd.flags
885        .iter()
886        .find(|(flag, _)| eligible(flag) && flag.negate.as_deref() == Some(name))
887        .map(|(_, named)| named.key.clone())
888}
889fn string_slice(values: &[String]) -> String {
890    let list = values
891        .iter()
892        .map(|v| go_string(v))
893        .collect::<Vec<_>>()
894        .join(", ");
895    format!("[]string{{{list}}}")
896}
897
898fn accepted_choices(choices: &SpecChoices) -> Vec<String> {
899    choices
900        .choices
901        .iter()
902        .chain(
903            choices
904                .details
905                .iter()
906                .flat_map(|choice| choice.aliases.iter().map(|alias| &alias.value)),
907        )
908        .cloned()
909        .collect()
910}
911
912fn visible_choices(choices: &SpecChoices) -> Vec<String> {
913    choices
914        .choices
915        .iter()
916        .filter(|value| {
917            !choices
918                .details
919                .iter()
920                .any(|choice| choice.value == value.as_str() && choice.hide)
921        })
922        .chain(choices.details.iter().flat_map(|choice| {
923            choice
924                .aliases
925                .iter()
926                .filter(|alias| !alias.hide)
927                .map(|alias| &alias.value)
928        }))
929        .cloned()
930        .collect()
931}
932
933fn key_slice(keys: &[String]) -> String {
934    format!("[]uint64{{{}}}", keys.join(", "))
935}
936
937impl Emitter<'_> {
938    /// Emit the help table: what a page prints.
939    ///
940    /// A third table rather than more fields on `Meta`, because Go's linker drops
941    /// an unreferenced package-level symbol whole — folding help text into the
942    /// post-binding table would make every CLI that applies a rule carry mise's
943    /// several hundred kilobytes of help strings too.
944    fn help_table(&mut self, commands: &[Emitted]) {
945        let mut by_key: BTreeMap<u64, String> = BTreeMap::new();
946        for e in commands {
947            by_key.insert(e.named.number, command_help(e));
948            for (flag, named) in &e.flags {
949                by_key.insert(named.number, flag_help(flag, named));
950            }
951            for (arg, named) in &e.args {
952                by_key.insert(named.number, arg_help(arg, named));
953            }
954        }
955
956        let total = commands
957            .iter()
958            .map(|e| 1 + e.flags.len() + e.args.len())
959            .sum::<usize>() as u64;
960
961        let _ = writeln!(
962            self.out,
963            "// HelpText is the third table, read only when a page is rendered. Neither the\n\
964             // parser nor the post-binding rules touch it, and a CLI that never prints help\n\
965             // does not carry it: Go's linker drops an unreferenced table whole.\n\
966             //\n\
967             // Indexed by key, like the others.\n\
968             var HelpText = argv.HelpTable{{"
969        );
970        for key in 1..=total {
971            match by_key.get(&key) {
972                Some(entry) => {
973                    let _ = writeln!(self.out, "\t{entry},");
974                }
975                None => {
976                    let _ = writeln!(self.out, "\t{{}},");
977                }
978            }
979        }
980        let _ = writeln!(self.out, "}}\n");
981
982        let mut fields = vec![
983            format!("Name: {}", go_string(&self.spec.name)),
984            format!("Bin: {}", go_string(&self.spec.bin)),
985        ];
986        if let Some(version) = self
987            .spec
988            .version
989            .as_ref()
990            .or(self.spec.long_version.as_ref())
991        {
992            fields.push(format!("Version: {}", go_string(version)));
993        }
994        if let Some(version) = &self.spec.long_version {
995            fields.push(format!("LongVersion: {}", go_string(version)));
996        }
997        // `about` alone, with no fall back to the long one: usage-lib's short page
998        // prints nothing where a spec wrote only `about_long`, and the long page
999        // is what reads LongAbout.
1000        if let Some(about) = &self.spec.about {
1001            fields.push(format!("About: {}", go_string(about)));
1002        }
1003        if let Some(long) = &self.spec.about_long {
1004            fields.push(format!("LongAbout: {}", go_string(long)));
1005        }
1006        if let Some(author) = &self.spec.author {
1007            fields.push(format!("Author: {}", go_string(author)));
1008        }
1009        if let Some(license) = &self.spec.license {
1010            fields.push(format!("License: {}", go_string(license)));
1011        }
1012        if let Some(before) = &self.spec.before_help {
1013            fields.push(format!("BeforeHelp: {}", go_string(before)));
1014        }
1015        if let Some(after) = &self.spec.after_help {
1016            fields.push(format!("AfterHelp: {}", go_string(after)));
1017        }
1018        if let Some(before) = &self.spec.before_help_long {
1019            fields.push(format!("BeforeLongHelp: {}", go_string(before)));
1020        }
1021        if let Some(after) = &self.spec.after_help_long {
1022            fields.push(format!("AfterLongHelp: {}", go_string(after)));
1023        }
1024        // One template for the whole tree, naming the sections a page is assembled from.
1025        if let Some(template) = &self.spec.help_template {
1026            fields.push(format!("HelpTemplate: {}", go_string(template)));
1027        }
1028        let _ = writeln!(
1029            self.out,
1030            "// HelpMeta is what a page needs from the spec's root rather than from any one\n\
1031             // command: the header, and the text that brackets every page.\n\
1032             var HelpMeta = argv.HelpSpec{{{}}}\n",
1033            fields.join(", ")
1034        );
1035    }
1036}
1037
1038/// The help entry for a command: its about text.
1039fn command_help(e: &Emitted) -> String {
1040    let mut fields = vec![format!("Key: {}", e.named.key)];
1041    if e.cmd.hide {
1042        fields.push("Hide: true".to_string());
1043    }
1044    if let Some(heading) = &e.cmd.help_heading {
1045        fields.push(format!("Heading: {}", go_string(heading)));
1046    }
1047    if let Some(order) = e.cmd.display_order {
1048        fields.push(format!("DisplayOrder: {order}"));
1049        fields.push("DisplayOrderSet: true".to_string());
1050    }
1051    if let Some(help) = e.cmd.help.as_deref().or(e.cmd.help_long.as_deref()) {
1052        fields.push(format!("Short: {}", go_string(help)));
1053    }
1054    if let Some(long) = &e.cmd.help_long {
1055        fields.push(format!("Long: {}", go_string(long)));
1056    }
1057    if let Some(message) = &e.cmd.deprecated {
1058        fields.push(format!("Deprecated: {}", go_string(message)));
1059    }
1060    if let Some(at) = &e.cmd.deprecated_warn_at {
1061        fields.push(format!("DeprecatedWarnAt: {}", go_string(at)));
1062    }
1063    if let Some(at) = &e.cmd.deprecated_remove_at {
1064        fields.push(format!("DeprecatedRemoveAt: {}", go_string(at)));
1065    }
1066    if let Some(heading) = &e.cmd.subcommand_help_heading {
1067        fields.push(format!("SubcommandHelpHeading: {}", go_string(heading)));
1068    }
1069    if let Some(name) = &e.cmd.subcommand_value_name {
1070        fields.push(format!("SubcommandValueName: {}", go_string(name)));
1071    }
1072    if e.cmd.next_line_help {
1073        fields.push("NextLineHelp: true".to_string());
1074    }
1075    if e.cmd.flatten_help {
1076        fields.push("FlattenHelp: true".to_string());
1077    }
1078    if e.cmd.subcommand_required {
1079        fields.push("SubcommandRequired: true".to_string());
1080    }
1081    // Visible only: the parse table merges hidden aliases in beside these,
1082    // because binding does not care which is which. A page does.
1083    let visible: Vec<String> = e
1084        .cmd
1085        .aliases
1086        .iter()
1087        .filter(|a| !e.cmd.hidden_aliases.contains(a))
1088        .cloned()
1089        .collect();
1090    if !visible.is_empty() {
1091        fields.push(format!("VisibleAliases: {}", string_slice(&visible)));
1092    }
1093    if let Some(before) = &e.cmd.before_help {
1094        fields.push(format!("BeforeHelp: {}", go_string(before)));
1095    }
1096    if let Some(after) = &e.cmd.after_help {
1097        fields.push(format!("AfterHelp: {}", go_string(after)));
1098    }
1099    // The long page's own brackets, which most of mise's commands use: their
1100    // examples are written as `after_long_help`, and a generated CLI that dropped
1101    // them printed a page with the examples missing.
1102    if let Some(before) = &e.cmd.before_help_long {
1103        fields.push(format!("BeforeLongHelp: {}", go_string(before)));
1104    }
1105    if let Some(after) = &e.cmd.after_help_long {
1106        fields.push(format!("AfterLongHelp: {}", go_string(after)));
1107    }
1108    if !e.cmd.examples.is_empty() {
1109        let items = e
1110            .cmd
1111            .examples
1112            .iter()
1113            .map(|x| {
1114                let mut parts = Vec::new();
1115                if let Some(header) = &x.header {
1116                    parts.push(format!("Header: {}", go_string(header)));
1117                }
1118                parts.push(format!("Code: {}", go_string(&x.code)));
1119                // The line the long page prints above the command. It introduces
1120                // the invocation rather than commenting on it, and a generated CLI
1121                // that dropped it printed the command with nothing to say why.
1122                if let Some(help) = &x.help {
1123                    parts.push(format!("Help: {}", go_string(help)));
1124                }
1125                format!("{{{}}}", parts.join(", "))
1126            })
1127            .collect::<Vec<_>>()
1128            .join(", ");
1129        fields.push(format!("Examples: []argv.Example{{{items}}}"));
1130    }
1131    format!("{{{}}}", fields.join(", "))
1132}
1133
1134fn flag_help(flag: &SpecFlag, named: &Named) -> String {
1135    let mut fields = vec![format!("Key: {}", named.key)];
1136    if let Some(message) = &flag.deprecated {
1137        fields.push(format!("Deprecated: {}", go_string(message)));
1138    }
1139    if let Some(at) = &flag.deprecated_warn_at {
1140        fields.push(format!("DeprecatedWarnAt: {}", go_string(at)));
1141    }
1142    if let Some(at) = &flag.deprecated_remove_at {
1143        fields.push(format!("DeprecatedRemoveAt: {}", go_string(at)));
1144    }
1145    if flag.hide {
1146        fields.push("Hide: true".to_string());
1147    }
1148    if let Some(order) = flag.display_order {
1149        fields.push(format!("DisplayOrder: {order}"));
1150        fields.push("DisplayOrderSet: true".to_string());
1151    }
1152    for (name, hidden) in [
1153        ("HideDefaultValue", flag.hide_default_value),
1154        ("HideEnv", flag.hide_env),
1155        ("HideEnvValues", flag.hide_env_values),
1156        ("HidePossibleValues", flag.hide_possible_values),
1157        ("HideShortHelp", flag.hide_short_help),
1158        ("HideLongHelp", flag.hide_long_help),
1159    ] {
1160        if hidden {
1161            fields.push(format!("{name}: true"));
1162        }
1163    }
1164    // Required *and* undefaulted, which is what decides the brackets: a required
1165    // flag with a default is one the user never has to type.
1166    if flag.required && flag.default.is_empty() {
1167        fields.push("Demanded: true".to_string());
1168    }
1169    if flag.var {
1170        fields.push("Repeatable: true".to_string());
1171    }
1172    if let Some(arg) = &flag.arg {
1173        if arg.name != flag.name {
1174            fields.push(format!("ValueName: {}", go_string(&arg.name)));
1175        }
1176        // The value's own requiredness, which is independent of the flag's:
1177        // `<--v <n>>` is a required flag whose value must be given, and
1178        // `<--jobs [n]>` a required flag whose value has a default.
1179        if arg.required && arg.default.is_empty() {
1180            fields.push("ValueDemanded: true".to_string());
1181        }
1182        if !arg.value_names.is_empty() {
1183            fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1184        }
1185        if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1186            fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1187        }
1188    }
1189    // The whole `help`, not its first line: usage-lib's short page prints the
1190    // text as declared, and mise has flags whose help is two lines.
1191    if let Some(help) = flag.help.as_deref().or(flag.help_first_line.as_deref()) {
1192        fields.push(format!("Short: {}", go_string(help)));
1193    }
1194    if let Some(long) = flag.help_long.as_deref().or(flag.help.as_deref()) {
1195        fields.push(format!("Long: {}", go_string(long)));
1196    }
1197    if let Some(heading) = &flag.help_heading {
1198        fields.push(format!("Heading: {}", go_string(heading)));
1199    }
1200    // Annotations. A flag's choices are declared on the value it takes.
1201    if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) {
1202        fields.push(format!(
1203            "Choices: {}",
1204            string_slice(&visible_choices(choices))
1205        ));
1206    }
1207    if let Some(env) = &flag.env {
1208        fields.push(format!("Env: {}", go_string(env)));
1209    }
1210    if !flag.env_fallback.is_empty() {
1211        fields.push(format!("EnvFallback: {}", string_slice(&flag.env_fallback)));
1212    }
1213    if !flag.deprecated_env.is_empty() {
1214        fields.push(format!(
1215            "DeprecatedEnv: {}",
1216            string_slice(&flag.deprecated_env)
1217        ));
1218    }
1219    let default = if !flag.default.is_empty() {
1220        &flag.default
1221    } else {
1222        flag.arg
1223            .as_ref()
1224            .map(|a| &a.default)
1225            .unwrap_or(&flag.default)
1226    };
1227    if !default.is_empty() {
1228        fields.push(format!("Default: {}", string_slice(default)));
1229    }
1230    format!("{{{}}}", fields.join(", "))
1231}
1232
1233fn arg_help(arg: &SpecArg, named: &Named) -> String {
1234    let mut fields = vec![format!("Key: {}", named.key)];
1235    if let Some(order) = arg.display_order {
1236        fields.push(format!("DisplayOrder: {order}"));
1237        fields.push("DisplayOrderSet: true".to_string());
1238    }
1239    if arg.hide {
1240        fields.push("Hide: true".to_string());
1241    }
1242    for (name, hidden) in [
1243        ("HideDefaultValue", arg.hide_default_value),
1244        ("HideEnv", arg.hide_env),
1245        ("HideEnvValues", arg.hide_env_values),
1246        ("HidePossibleValues", arg.hide_possible_values),
1247        ("HideShortHelp", arg.hide_short_help),
1248        ("HideLongHelp", arg.hide_long_help),
1249    ] {
1250        if hidden {
1251            fields.push(format!("{name}: true"));
1252        }
1253    }
1254    if arg.required && arg.default.is_empty() {
1255        fields.push("Demanded: true".to_string());
1256    }
1257    if !arg.value_names.is_empty() {
1258        fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1259    }
1260    if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1261        fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1262    }
1263    if let Some(help) = arg.help.as_deref().or(arg.help_first_line.as_deref()) {
1264        fields.push(format!("Short: {}", go_string(help)));
1265    }
1266    if let Some(long) = arg.help_long.as_deref().or(arg.help.as_deref()) {
1267        fields.push(format!("Long: {}", go_string(long)));
1268    }
1269    if let Some(heading) = &arg.help_heading {
1270        fields.push(format!("Heading: {}", go_string(heading)));
1271    }
1272    if let Some(choices) = &arg.choices {
1273        fields.push(format!(
1274            "Choices: {}",
1275            string_slice(&visible_choices(choices))
1276        ));
1277    }
1278    if let Some(env) = &arg.env {
1279        fields.push(format!("Env: {}", go_string(env)));
1280    }
1281    if !arg.env_fallback.is_empty() {
1282        fields.push(format!("EnvFallback: {}", string_slice(&arg.env_fallback)));
1283    }
1284    if !arg.deprecated_env.is_empty() {
1285        fields.push(format!(
1286            "DeprecatedEnv: {}",
1287            string_slice(&arg.deprecated_env)
1288        ));
1289    }
1290    if !arg.default.is_empty() {
1291        fields.push(format!("Default: {}", string_slice(&arg.default)));
1292    }
1293    format!("{{{}}}", fields.join(", "))
1294}
1295
1296/// A line inside a `const` block or a composite literal.
1297///
1298/// The distinction exists only to reproduce gofmt's alignment, which pads within
1299/// *runs* of consecutive single-line entries and starts a new run after anything
1300/// that spans lines. Emitting gofmt-clean output rather than close-enough output
1301/// is what lets a generated file be committed as it comes out: the alternative is
1302/// every adopter needing a formatting step, and this repo's own CI failing
1303/// `gofmt -l` on the table it checks in.
1304enum Line {
1305    /// `Key: value,` — aligned against its neighbours.
1306    Field(String, String),
1307    /// Verbatim, and it breaks the run either side of it.
1308    Block(Vec<String>),
1309}
1310
1311/// Render lines with gofmt's column alignment.
1312fn render(out: &mut String, indent: &str, lines: &[Line]) {
1313    let mut run: Vec<(&String, &String)> = Vec::new();
1314
1315    fn flush(out: &mut String, indent: &str, run: &mut Vec<(&String, &String)>) {
1316        let width = run.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
1317        for (key, value) in run.iter() {
1318            let _ = writeln!(
1319                out,
1320                "{indent}{key}:{:width$} {value},",
1321                "",
1322                width = width - key.len()
1323            );
1324        }
1325        run.clear();
1326    }
1327
1328    for line in lines {
1329        match line {
1330            Line::Field(key, value) => run.push((key, value)),
1331            Line::Block(block) => {
1332                flush(out, indent, &mut run);
1333                for l in block {
1334                    let _ = writeln!(out, "{indent}{l}");
1335                }
1336            }
1337        }
1338    }
1339    flush(out, indent, &mut run);
1340}
1341
1342/// One command, named and ready to emit.
1343struct Emitted {
1344    named: Named,
1345    cmd: SpecCommand,
1346    flags: Vec<(SpecFlag, Named)>,
1347    args: Vec<(SpecArg, Named)>,
1348    /// Indices into the flat list, in declaration order.
1349    subcommands: Vec<usize>,
1350    root: bool,
1351}
1352
1353/// What an unrecognized flag-like token means at a command, with inheritance
1354/// applied.
1355///
1356/// The nearest enclosing command that states a preference wins, then the spec,
1357/// then `value`. Walked over `full_cmd` rather than threaded through the collect
1358/// pass, so that emission does not depend on the order commands happen to sit in.
1359fn effective_unknown_flags(spec: &Spec, commands: &[Emitted], at: usize) -> UnknownFlags {
1360    let path = &commands[at].cmd.full_cmd;
1361    for depth in (0..=path.len()).rev() {
1362        let ancestor = commands
1363            .iter()
1364            .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]);
1365        if let Some(mode) = ancestor.and_then(|e| e.cmd.unknown_flags) {
1366            return mode;
1367        }
1368    }
1369    spec.unknown_flags.unwrap_or_default()
1370}
1371
1372fn flag_literal(flag: &SpecFlag, named: &Named) -> String {
1373    let mut fields = vec![
1374        format!("Key: {}", named.key),
1375        format!("Name: {}", go_string(&flag.name)),
1376    ];
1377    if !flag.long.is_empty() {
1378        let longs = flag
1379            .long
1380            .iter()
1381            .map(|l| go_string(l))
1382            .collect::<Vec<_>>()
1383            .join(", ");
1384        fields.push(format!("Longs: []string{{{longs}}}"));
1385    }
1386    if !flag.hidden_aliases.is_empty() {
1387        fields.push(format!(
1388            "HiddenLongs: {}",
1389            string_slice(&flag.hidden_aliases)
1390        ));
1391    }
1392    if !flag.short.is_empty() {
1393        let shorts = flag
1394            .short
1395            .iter()
1396            .map(|c| go_byte(*c))
1397            .collect::<Vec<_>>()
1398            .join(", ");
1399        fields.push(format!("Shorts: []byte{{{shorts}}}"));
1400    }
1401    if !flag.hidden_short_aliases.is_empty() {
1402        let shorts = flag
1403            .hidden_short_aliases
1404            .iter()
1405            .map(|c| go_byte(*c))
1406            .collect::<Vec<_>>()
1407            .join(", ");
1408        fields.push(format!("HiddenShorts: []byte{{{shorts}}}"));
1409    }
1410    if let Some(negate) = &flag.negate {
1411        // The spec stores the negation with its dashes; the table wants the bare
1412        // name, since that is what the parser has after stripping the `--`.
1413        fields.push(format!(
1414            "Negate: {}",
1415            go_string(negate.trim_start_matches('-'))
1416        ));
1417    }
1418    if flag.arg.is_some() {
1419        fields.push("TakesValue: true".to_string());
1420    }
1421    if flag.value_optional {
1422        fields.push("ValueOptional: true".to_string());
1423    }
1424    if flag.bool_value {
1425        fields.push("BoolValue: true".to_string());
1426    }
1427    let action = match flag.action {
1428        SpecFlagAction::Set => None,
1429        SpecFlagAction::Help => Some("argv.ActionHelp"),
1430        SpecFlagAction::HelpShort => Some("argv.ActionHelpShort"),
1431        SpecFlagAction::HelpLong => Some("argv.ActionHelpLong"),
1432        SpecFlagAction::HelpAll => Some("argv.ActionHelpAll"),
1433        SpecFlagAction::Version => Some("argv.ActionVersion"),
1434    };
1435    if let Some(action) = action {
1436        fields.push(format!("Action: {action}"));
1437    }
1438    // Only a variadic *argument* is greedy. The spec's flag-level `var` means the
1439    // flag may be repeated and takes one value each time, which needs nothing from
1440    // the parser: it reports every occurrence separately either way. Conflating the
1441    // two makes a merely repeatable flag greedy enough to eat a positional.
1442    if let Some(arg) = flag.arg.as_ref().filter(|a| a.var) {
1443        fields.push("Variadic: true".to_string());
1444        if let Some(max) = arg.var_max {
1445            fields.push(format!("VarMax: {}", clamp_var_max(max)));
1446        }
1447    }
1448    if flag.allow_hyphen_values() {
1449        fields.push("AllowHyphenValues: true".to_string());
1450    }
1451    if let Some(arg) = &flag.arg {
1452        if arg.allow_negative_numbers {
1453            fields.push("AllowNegativeNumbers: true".to_string());
1454        }
1455        if let Some(terminator) = &arg.value_terminator {
1456            fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1457        }
1458        if let Some(delimiter) = arg.delimiter {
1459            fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1460        }
1461    }
1462    if flag.require_equals {
1463        fields.push("RequireEquals: true".to_string());
1464    }
1465    if let Some(missing) = &flag.default_missing {
1466        fields.push(format!("DefaultMissing: {}", go_string(missing)));
1467    }
1468    if flag.global {
1469        fields.push("Global: true".to_string());
1470    }
1471    format!("{{{}}}", fields.join(", "))
1472}
1473
1474fn arg_literal(arg: &SpecArg, named: &Named) -> String {
1475    let mut fields = vec![
1476        format!("Key: {}", named.key),
1477        format!("Name: {}", go_string(&arg.name)),
1478    ];
1479    if arg.required {
1480        fields.push("Required: true".to_string());
1481    }
1482    if arg.var {
1483        fields.push("Var: true".to_string());
1484        if let Some(max) = arg.var_max {
1485            fields.push(format!("VarMax: {}", clamp_var_max(max)));
1486        }
1487    }
1488    if arg.allow_negative_numbers {
1489        fields.push("AllowNegativeNumbers: true".to_string());
1490    }
1491    if let Some(terminator) = &arg.value_terminator {
1492        fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1493    }
1494    if let Some(delimiter) = arg.delimiter {
1495        fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1496    }
1497    let double_dash = match arg.double_dash {
1498        SpecDoubleDashChoices::Required => Some("argv.DoubleDashRequired"),
1499        SpecDoubleDashChoices::Preserve => Some("argv.DoubleDashPreserve"),
1500        SpecDoubleDashChoices::Automatic => Some("argv.DoubleDashAutomatic"),
1501        _ => None,
1502    };
1503    if let Some(dd) = double_dash {
1504        fields.push(format!("DoubleDash: {dd}"));
1505    }
1506    format!("{{{}}}", fields.join(", "))
1507}
1508
1509/// Zero means unbounded in the table, which is also what an absent `var_max`
1510/// lowers to, so the two agree. A bound past a `uint32` saturates rather than
1511/// wrapping: truncating four billion and one to one would read as "stop at once"
1512/// rather than "no real limit".
1513fn clamp_var_max(max: usize) -> u32 {
1514    u32::try_from(max).unwrap_or(u32::MAX)
1515}
1516
1517/// Go's reserved words, which cannot be a package name.
1518///
1519/// Not hypothetical: `go`, `range`, `select`, `import` and `package` are all
1520/// plausible names for a CLI, and `package go` does not compile.
1521const GO_KEYWORDS: &[&str] = &[
1522    "break",
1523    "case",
1524    "chan",
1525    "const",
1526    "continue",
1527    "default",
1528    "defer",
1529    "else",
1530    "fallthrough",
1531    "for",
1532    "func",
1533    "go",
1534    "goto",
1535    "if",
1536    "import",
1537    "interface",
1538    "map",
1539    "package",
1540    "range",
1541    "return",
1542    "select",
1543    "struct",
1544    "switch",
1545    "type",
1546    "var",
1547];
1548
1549/// Two more names a table package cannot have, for two different reasons.
1550///
1551/// `_` is refused where it is written: `invalid package name _`. `init` declares
1552/// perfectly well and cannot be *imported* — an import binds the package name as
1553/// an identifier in file scope, and `init` may only be a func, so an importer gets
1554/// `cannot import package as init - init must be a func`. A table package exists
1555/// to be imported, so it is out either way.
1556///
1557/// Both checked against the compiler rather than taken from a citation. The issue
1558/// usually cited for `init` is about the import, and `package init` on its own
1559/// does build — so a validator written from the citation would have rejected it
1560/// for a reason that is not true.
1561const UNUSABLE_PACKAGE_NAMES: &[&str] = &["_", "init"];
1562
1563/// Whether a string can be written after `package` and then imported.
1564///
1565/// Deliberately ASCII-only. Go itself allows a Unicode letter, but a package name
1566/// that needs one is a worse problem for an adopter than the restriction is.
1567pub fn is_valid_package(name: &str) -> bool {
1568    !name.is_empty()
1569        && !GO_KEYWORDS.contains(&name)
1570        && !UNUSABLE_PACKAGE_NAMES.contains(&name)
1571        && !name.starts_with(|c: char| c.is_ascii_digit())
1572        && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1573}
1574
1575/// A Go field name from a spec name: exported, and an identifier.
1576fn field_name(name: &str) -> String {
1577    let ident = format!("{}", AsPascalCase(name));
1578    if ident.is_empty() || ident.starts_with(|c: char| c.is_ascii_digit()) {
1579        format!("X{ident}")
1580    } else {
1581        ident
1582    }
1583}
1584
1585/// A Go package identifier from a binary name: `my-cli` is not one, `mycli` is.
1586///
1587/// Only ever applied to a name derived from the spec, which the author did not
1588/// choose for this purpose and cannot be asked to fix. A `--package` given
1589/// explicitly is checked rather than mangled — see [`is_valid_package`] — because
1590/// silently emitting `mypkg` for someone who asked for `my-pkg` is a surprise
1591/// waiting in a build script.
1592fn package_ident(bin: &str) -> String {
1593    let lowered: String = bin
1594        .chars()
1595        .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
1596        .collect::<String>()
1597        .to_ascii_lowercase();
1598    if is_valid_package(&lowered) {
1599        lowered
1600    } else {
1601        // One rule rather than a second copy of the conditions, so the sanitizer
1602        // cannot come to disagree with the validator about what is acceptable.
1603        // `cli` in front keeps it recognizable: `cligo`, `cli7zip`, `cliinit`.
1604        format!("cli{lowered}")
1605    }
1606}
1607
1608/// A Go string literal.
1609///
1610/// Written out rather than borrowed from Rust's `{:?}`, which escapes to Rust's
1611/// rules: it spells a delete character `\u{7f}`, which Go does not accept.
1612fn go_string(s: &str) -> String {
1613    let mut out = String::with_capacity(s.len() + 2);
1614    out.push('"');
1615    for c in s.chars() {
1616        match c {
1617            '"' => out.push_str("\\\""),
1618            '\\' => out.push_str("\\\\"),
1619            '\n' => out.push_str("\\n"),
1620            '\r' => out.push_str("\\r"),
1621            '\t' => out.push_str("\\t"),
1622            c if (c as u32) < 0x20 || c as u32 == 0x7f => {
1623                let _ = write!(out, "\\x{:02x}", c as u32);
1624            }
1625            c => out.push(c),
1626        }
1627    }
1628    out.push('"');
1629    out
1630}
1631
1632/// A Go byte literal for a short flag.
1633///
1634/// Non-ASCII shorts are emitted as their low byte, which can never match: a
1635/// cluster is walked one byte at a time. The spec is what should refuse them, and
1636/// silently dropping one here would be a flag that vanished.
1637fn go_byte(c: char) -> String {
1638    match c {
1639        '\'' => "'\\''".to_string(),
1640        '\\' => "'\\\\'".to_string(),
1641        c if c.is_ascii_graphic() => format!("'{c}'"),
1642        c => format!("0x{:02x}", (c as u32) & 0xff),
1643    }
1644}
1645
1646#[cfg(test)]
1647mod tests {
1648    use super::*;
1649
1650    /// The emitted `Meta` line for an entry, so a test can assert about the part
1651    /// it cares about rather than the whole rendered row — which grows a field
1652    /// every time the cold table learns something.
1653    ///
1654    /// Used for what a row *does* say as well as for what it does not. Two
1655    /// substring checks over the whole file — one for the name, one for the
1656    /// relationship — pass when the relationship is attached to a different flag
1657    /// entirely, which is the regression these tests exist to catch.
1658    fn entry_of(out: &str, name: &str) -> String {
1659        out.lines()
1660            .find(|l| l.contains(&format!("Name: \"{name}\", Flag: true")))
1661            .unwrap_or_default()
1662            .to_string()
1663    }
1664
1665    fn go(kdl: &str) -> String {
1666        let spec: Spec = kdl.parse().expect("the fixture spec should parse");
1667        generate(&spec, &GoOptions::default())
1668    }
1669
1670    #[test]
1671    fn a_whole_cli() {
1672        let out = go(r#"
1673name "ex"
1674bin "ex"
1675version "1.2.3"
1676long_version "1.2.3\ncommit abc123"
1677flag "-v --verbose" global=#true help="be loud"
1678flag "--color" negate="--no-color"
1679flag "-j --jobs <n>"
1680flag "--include <pattern>..." var_max=3
1681arg "<file>"
1682arg "[rest]..." var=#true
1683cmd "install" {
1684    alias "i"
1685    flag "-f --force"
1686    arg "<pkg>"
1687}
1688cmd "config" {
1689    cmd "ls" {
1690        flag "--no-header"
1691    }
1692}
1693"#);
1694        insta::assert_snapshot!(out);
1695    }
1696
1697    #[test]
1698    fn rich_choices_keep_acceptance_visibility_and_strictness_separate() {
1699        let out = go(r#"
1700name "ex"
1701bin "ex"
1702flag "--color <when>" {
1703    choices ignore_case=#true strict=#false {
1704        choice "always" {
1705            alias "yes"
1706            alias "on" hide=#true
1707        }
1708        choice "never" hide=#true
1709    }
1710}
1711"#);
1712        let entry = entry_of(&out, "color");
1713        assert!(
1714            entry.contains(r#"Choices: []string{"always", "yes"}"#),
1715            "{entry}"
1716        );
1717        assert!(
1718            entry.contains(r#"AcceptedChoices: []string{"always", "never", "yes", "on"}"#),
1719            "{entry}"
1720        );
1721        assert!(entry.contains("IgnoreCase: true"), "{entry}");
1722        assert!(entry.contains("AllowUnknownChoices: true"), "{entry}");
1723    }
1724
1725    /// Inheritance is resolved here so the parser reads one field per command.
1726    #[test]
1727    fn unknown_flags_are_inherited_and_overridable() {
1728        let out = go(r#"
1729name "ex"
1730bin "ex"
1731unknown_flags "error"
1732cmd "strict" {
1733    cmd "deep" {}
1734}
1735cmd "exec" unknown_flags="value" {
1736    cmd "nested" {}
1737}
1738"#);
1739        insta::assert_snapshot!(out);
1740    }
1741
1742    /// mise declares both a `macos-defaults` command and a `macos defaults` path,
1743    /// and both want the same Go identifier.
1744    #[test]
1745    fn colliding_names_get_distinct_identifiers() {
1746        let out = go(r#"
1747name "ex"
1748bin "ex"
1749cmd "macos-defaults" {
1750    flag "--apply"
1751}
1752cmd "macos" {
1753    cmd "defaults" {
1754        flag "--apply"
1755    }
1756}
1757"#);
1758        insta::assert_snapshot!(out);
1759    }
1760
1761    #[test]
1762    fn a_default_subcommand_points_into_the_tree() {
1763        let out = go(r#"
1764name "ex"
1765bin "ex"
1766default_subcommand "run"
1767arg "[task]"
1768cmd "run" {
1769    arg "[args]..." var=#true
1770}
1771"#);
1772        insta::assert_snapshot!(out);
1773    }
1774
1775    #[test]
1776    fn a_bin_name_that_is_not_an_identifier_still_gives_a_package() {
1777        assert_eq!(package_ident("my-cli"), "mycli");
1778        assert_eq!(package_ident("7zip"), "cli7zip");
1779        assert_eq!(package_ident(""), "cli");
1780        // `package go` does not compile, and `go` is a plausible name for a CLI.
1781        assert_eq!(package_ident("go"), "cligo");
1782        assert_eq!(package_ident("type"), "clitype");
1783        // `package _` is refused outright; `package init` declares fine and cannot
1784        // be imported, which for a table package is the same thing.
1785        assert_eq!(package_ident("_"), "cli_");
1786        assert_eq!(package_ident("init"), "cliinit");
1787        // Two underscores is fine, and only the exact name is reserved.
1788        assert_eq!(package_ident("__"), "__");
1789        assert_eq!(package_ident("initialize"), "initialize");
1790
1791        // Whatever it produces must be something the validator accepts, for every
1792        // one of these — the sanitizer disagreeing with the check is how a file
1793        // that does not compile gets emitted.
1794        for bin in [
1795            "my-cli", "7zip", "", "go", "type", "_", "init", "__", "MiSe",
1796        ] {
1797            let out = package_ident(bin);
1798            assert!(is_valid_package(&out), "{bin:?} sanitized to {out:?}");
1799        }
1800    }
1801
1802    /// Counting alone let a third command collide with a generated suffix.
1803    #[test]
1804    fn a_name_matching_a_generated_suffix_still_gets_its_own() {
1805        let out = go(r#"
1806name "ex"
1807bin "ex"
1808cmd "macos-defaults" {}
1809cmd "macos" {
1810    cmd "defaults" {}
1811}
1812cmd "macos-defaults2" {}
1813"#);
1814        // The invariant, not a guess at the spelling. The third command lands on
1815        // `CmdMacosDefaults22` rather than `...3`, which is unlovely and correct;
1816        // asserting the exact name would pin the suffix scheme instead of the
1817        // property that matters, which is that nothing is declared twice.
1818        assert_declares_each_constant_once(&out);
1819    }
1820
1821    /// Every constant in the emitted `const` block, in declaration order.
1822    fn constant_names(out: &str) -> Vec<&str> {
1823        out.lines()
1824            .skip_while(|l| !l.starts_with("const ("))
1825            .skip(1)
1826            .take_while(|l| !l.starts_with(')'))
1827            .filter_map(|l| l.split_whitespace().next())
1828            .collect()
1829    }
1830
1831    /// Two entries sharing a constant is a file that does not compile.
1832    fn assert_declares_each_constant_once(out: &str) {
1833        let names = constant_names(out);
1834        assert!(!names.is_empty(), "no constants at all:\n{out}");
1835        let mut seen = std::collections::HashSet::new();
1836        for name in &names {
1837            assert!(seen.insert(*name), "{name} is declared twice:\n{out}");
1838        }
1839    }
1840
1841    #[test]
1842    fn a_package_that_would_not_compile_is_refused_rather_than_emitted() {
1843        assert!(is_valid_package("mycli"));
1844        assert!(is_valid_package("mise_tables"));
1845        assert!(!is_valid_package("my-pkg"));
1846        assert!(!is_valid_package("7zip"));
1847        assert!(!is_valid_package(""));
1848        assert!(!is_valid_package("range"));
1849        assert!(!is_valid_package("_"));
1850        assert!(!is_valid_package("init"));
1851        assert!(is_valid_package("__"));
1852
1853        // A library caller that skips the check still gets a file that compiles.
1854        let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
1855        let out = generate(
1856            &spec,
1857            &GoOptions {
1858                package: Some("my-pkg".into()),
1859            },
1860        );
1861        assert!(out.contains("package mypkg"), "{out}");
1862    }
1863
1864    /// The bug the checked-in mise tables caught: `default_subcommand run` was
1865    /// wired to `oci run`, which a depth-first walk reaches first.
1866    ///
1867    /// It names a subcommand *of the root*, so nothing deeper is a candidate — and
1868    /// the parser would otherwise descend into a command that is not the root's
1869    /// child at all.
1870    /// The long page's text is a table entry too.
1871    ///
1872    /// mise writes its examples as `after_long_help`, on 115 of its commands, so a
1873    /// generator that dropped them emitted a `--help` with every example missing —
1874    /// while the page tests, which build their tables by lowering rather than by
1875    /// generating, saw nothing wrong. The two producers are compared against each
1876    /// other now; this is the same rule from the emitter's side.
1877    #[test]
1878    fn the_long_pages_text_reaches_the_tables() {
1879        let out = go(r#"
1880name "ex"
1881bin "ex"
1882about "Short."
1883about_long "Long."
1884before_long_help "ROOT-BEFORE"
1885after_long_help "ROOT-AFTER"
1886cmd "run" help="Run it" {
1887    before_long_help "RUN-BEFORE"
1888    after_long_help "RUN-AFTER"
1889}
1890"#);
1891        let meta = out
1892            .lines()
1893            .find(|l| l.contains("var HelpMeta"))
1894            .expect("a root header is emitted");
1895        assert!(
1896            meta.contains(r#"About: "Short.""#) && meta.contains(r#"LongAbout: "Long.""#),
1897            "the two abouts are separate fields: {meta}"
1898        );
1899        assert!(
1900            meta.contains(r#"BeforeLongHelp: "ROOT-BEFORE""#)
1901                && meta.contains(r#"AfterLongHelp: "ROOT-AFTER""#),
1902            "the root's long brackets are emitted: {meta}"
1903        );
1904
1905        let run = out
1906            .lines()
1907            .find(|l| l.contains("Short: \"Run it\""))
1908            .expect("the command has a help entry");
1909        assert!(
1910            run.contains(r#"BeforeLongHelp: "RUN-BEFORE""#)
1911                && run.contains(r#"AfterLongHelp: "RUN-AFTER""#),
1912            "a command's long brackets are emitted: {run}"
1913        );
1914    }
1915
1916    /// An example's help line reaches the tables.
1917    ///
1918    /// The long page prints it above the command, where it introduces the
1919    /// invocation; a generated CLI that dropped it printed the command with
1920    /// nothing to say why. mise cannot show this — it writes its examples as
1921    /// `after_long_help` text rather than as `example` nodes — so the producer
1922    /// comparison over mise's spec cannot see it either.
1923    #[test]
1924    fn an_examples_help_line_reaches_the_tables() {
1925        let out = go(r#"
1926name "ex"
1927bin "ex"
1928cmd "run" help="Run it" {
1929    example "ex run --fast" header="Speed" help="When you are in a hurry"
1930    example "ex run"
1931}
1932"#);
1933        let run = out
1934            .lines()
1935            .find(|l| l.contains("Examples: []argv.Example"))
1936            .expect("the command's examples are emitted");
1937        assert!(
1938            run.contains(
1939                r#"{Header: "Speed", Code: "ex run --fast", Help: "When you are in a hurry"}"#
1940            ),
1941            "all three fields are emitted: {run}"
1942        );
1943        // And a bare example says only what it has, rather than an empty header.
1944        assert!(
1945            run.contains(r#"{Code: "ex run"}"#),
1946            "an example with no header emits no header: {run}"
1947        );
1948    }
1949
1950    /// `about_long` alone leaves the short page's About unset, because usage-lib
1951    /// prints nothing there: the long text belongs to the long page.
1952    #[test]
1953    fn a_long_about_alone_does_not_become_the_short_one() {
1954        let out = go(r#"
1955name "ex"
1956bin "ex"
1957about_long "Long only."
1958"#);
1959        let meta = out
1960            .lines()
1961            .find(|l| l.contains("var HelpMeta"))
1962            .expect("a root header is emitted");
1963        assert!(
1964            !meta.contains(", About: ") && meta.contains(r#"LongAbout: "Long only.""#),
1965            "only the long one is set: {meta}"
1966        );
1967    }
1968
1969    #[test]
1970    fn a_default_subcommand_ignores_a_deeper_command_of_the_same_name() {
1971        let out = go(r#"
1972name "ex"
1973bin "ex"
1974default_subcommand "run"
1975cmd "oci" {
1976    cmd "run" {}
1977}
1978cmd "run" {
1979    arg "[args]..." var=#true
1980}
1981"#);
1982        assert!(
1983            out.contains("DefaultSubcommand: cmdRun,"),
1984            "should point at the root's own `run`, got:\n{out}"
1985        );
1986    }
1987
1988    #[test]
1989    fn command_builtin_controls_reach_generated_go_tables() {
1990        let out = go(r#"
1991name "ex"
1992bin "ex"
1993disable_help_flag #true
1994disable_help_subcommand #true
1995disable_version_flag #true
1996"#);
1997        let root = out
1998            .split("var Root = &argv.Command{")
1999            .nth(1)
2000            .expect("the root command should be emitted")
2001            .split("}\n")
2002            .next()
2003            .unwrap();
2004        assert!(root.contains("DisableHelpFlag:"), "{root}");
2005        assert!(root.contains("DisableHelpSubcommand:"), "{root}");
2006        assert!(root.contains("DisableVersionFlag:"), "{root}");
2007    }
2008
2009    /// A command's own name outranks another command's alias, so which command the
2010    /// emitted `DefaultSubcommand` points at does not depend on declaration order.
2011    #[test]
2012    fn a_default_subcommand_prefers_a_name_to_another_commands_alias() {
2013        let ordered = |first: &str, second: &str| {
2014            go(&format!(
2015                r#"
2016name "ex"
2017bin "ex"
2018default_subcommand "run"
2019{first}
2020{second}
2021"#
2022            ))
2023        };
2024        let alpha = "cmd \"alpha\" {\n    alias \"run\"\n}";
2025        let run = "cmd \"run\" {\n    arg \"[args]...\" var=#true\n}";
2026        for out in [ordered(alpha, run), ordered(run, alpha)] {
2027            assert!(
2028                out.contains("DefaultSubcommand: cmdRun,"),
2029                "should point at the command named `run`, got:\n{out}"
2030            );
2031        }
2032    }
2033
2034    #[test]
2035    fn an_external_subcommand_is_emitted_on_the_command_that_declares_it() {
2036        let out = go(r#"
2037name "ex"
2038bin "ex"
2039external_subcommand #true
2040cmd "install"
2041cmd "exec" external_subcommand=#true
2042"#);
2043        let block = |var: &str| {
2044            let start = out
2045                .find(&format!("var {var} ="))
2046                .unwrap_or_else(|| panic!("{var} should be emitted, got:\n{out}"));
2047            let rest = &out[start..];
2048            let end = rest[1..]
2049                .find("\nvar ")
2050                .map(|i| i + 1)
2051                .unwrap_or(rest.len());
2052            &rest[..end]
2053        };
2054        assert!(
2055            block("Root").contains("ExternalSubcommand: true"),
2056            "the root should forward unmatched words:\n{}",
2057            block("Root")
2058        );
2059        assert!(
2060            block("cmdExec").contains("ExternalSubcommand: true"),
2061            "a nested command can forward too:\n{}",
2062            block("cmdExec")
2063        );
2064        assert!(
2065            !block("cmdInstall").contains("ExternalSubcommand"),
2066            "a command that does not declare it should not carry it:\n{}",
2067            block("cmdInstall")
2068        );
2069    }
2070
2071    #[test]
2072    fn arg_required_else_help_reaches_the_table_and_typed_front_door() {
2073        let out = go(r#"
2074name "ex"
2075bin "ex"
2076cmd "run" arg_required_else_help=#true {
2077    flag "--all"
2078}
2079"#);
2080        assert!(
2081            out.contains("ArgRequiredElseHelp: true"),
2082            "the command table should carry the policy:\n{out}"
2083        );
2084        assert!(
2085            out.contains("p.Command().ArgRequiredElseHelp && p.CommandStart() == len(args)"),
2086            "the typed parser should enforce it before fallbacks:\n{out}"
2087        );
2088    }
2089
2090    #[test]
2091    fn subcommand_negates_requirements_reaches_generated_go() {
2092        let out = go(
2093            "name \"ex\"\nbin \"ex\"\nsubcommand_negates_reqs #true\nflag \"--config\" required=#true\ncmd \"run\"\n",
2094        );
2095        assert!(out.contains("SubcommandNegatesReqs: true"), "{out}");
2096        assert!(
2097            out.contains("checkRequirements := i == len(chain)-1 || !cmd.SubcommandNegatesReqs"),
2098            "{out}"
2099        );
2100        assert!(
2101            out.contains("CheckRelationshipsWithValuesAndRequirements"),
2102            "{out}"
2103        );
2104    }
2105
2106    #[test]
2107    fn argument_subcommand_conflicts_reach_generated_go() {
2108        let out = go(
2109            "name \"ex\"\nbin \"ex\"\nargs_conflicts_with_subcommands #true\nflag \"--verbose\"\ncmd \"run\"\n",
2110        );
2111        assert!(out.contains("ArgsConflictWithSubcommands: true"), "{out}");
2112    }
2113
2114    #[test]
2115    fn allow_missing_positional_reaches_generated_go() {
2116        let out = go(
2117            "name \"ex\"\nbin \"ex\"\nallow_missing_positional #true\narg \"[optional]\"\narg \"<required>\"\n",
2118        );
2119        assert!(out.contains("AllowMissingPositional: true"), "{out}");
2120        assert!(out.contains("Name: \"optional\""), "{out}");
2121        assert!(out.contains("Name: \"required\", Required: true"), "{out}");
2122    }
2123
2124    #[test]
2125    fn optional_flag_values_reach_generated_go() {
2126        let out = go("name \"ex\"\nbin \"ex\"\nflag \"--color [WHEN]\" value_optional=#true\n");
2127        assert!(
2128            out.contains("TakesValue: true, ValueOptional: true"),
2129            "{out}"
2130        );
2131    }
2132
2133    #[test]
2134    fn explicit_boolean_values_reach_generated_go() {
2135        let out = go(
2136            "name \"ex\"\nbin \"ex\"\nflag \"--color\" negate=\"--no-color\" bool_value=#true\n",
2137        );
2138        assert!(out.contains("BoolValue: true"), "{out}");
2139        assert!(out.contains("if ev.Flag.BoolValue"), "{out}");
2140        assert!(
2141            out.contains("given[ev.Flag.Key] = []string{ev.Value}"),
2142            "{out}"
2143        );
2144        assert!(
2145            out.contains("(ev.Value == \"true\") != ev.Negated"),
2146            "{out}"
2147        );
2148    }
2149
2150    #[test]
2151    fn flag_actions_reach_generated_go() {
2152        let out = go(
2153            "name \"ex\"\nbin \"ex\"\nflag \"--help-all\" action=\"help_all\"\nflag \"--version\" action=\"version\"\n",
2154        );
2155        assert!(out.contains("Action: argv.ActionHelpAll"), "{out}");
2156        assert!(out.contains("Action: argv.ActionVersion"), "{out}");
2157    }
2158
2159    #[test]
2160    fn granular_help_hides_reach_generated_go() {
2161        let out = go(
2162            "name \"ex\"\nbin \"ex\"\nflag \"--mode <mode>\" hide_default_value=#true hide_env=#true hide_env_values=#true hide_possible_values=#true hide_short_help=#true hide_long_help=#true\n",
2163        );
2164        for field in [
2165            "HideDefaultValue: true",
2166            "HideEnv: true",
2167            "HideEnvValues: true",
2168            "HidePossibleValues: true",
2169            "HideShortHelp: true",
2170            "HideLongHelp: true",
2171        ] {
2172            assert!(out.contains(field), "missing {field}:\n{out}");
2173        }
2174    }
2175
2176    #[test]
2177    fn strict_duplicate_policy_reaches_metadata() {
2178        let permissive = go("name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\n");
2179        assert!(!permissive.contains("RejectDuplicate"), "{permissive}");
2180
2181        let strict =
2182            go("name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\n");
2183        assert!(strict.contains("RejectDuplicate: true"), "{strict}");
2184    }
2185
2186    #[test]
2187    fn strict_negated_flags_track_each_spelling_separately() {
2188        let out = go(
2189            "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n",
2190        );
2191        assert!(out.contains("polaritySeen := map[uint64]uint8{}"), "{out}");
2192        assert!(
2193            out.contains("polaritySeen[ev.Flag.Key]&polarity != 0"),
2194            "{out}"
2195        );
2196        assert!(out.contains("if duplicateSeen[key]"), "{out}");
2197    }
2198
2199    #[test]
2200    fn strict_global_duplicate_tracking_resets_at_subcommands() {
2201        let out = go(
2202            "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\" global=#true\ncmd \"run\" {\n  args_override_self #false\n}\n",
2203        );
2204        assert!(
2205            out.contains("levelSeen = map[uint64]int{}"),
2206            "a subcommand should start a new duplicate scope:\n{out}"
2207        );
2208        assert!(out.contains("strictSeen[ev.Flag.Key] = true"), "{out}");
2209        assert!(out.contains("if strictSeen[key]"), "{out}");
2210    }
2211
2212    /// A subcommand actually named `root` wants the constant the root has.
2213    #[test]
2214    fn a_subcommand_named_root_does_not_collide_with_the_root() {
2215        let out = go(r#"
2216name "ex"
2217bin "ex"
2218cmd "root" {
2219    flag "--wat"
2220}
2221"#);
2222        // By first token, because the const block is column-aligned: matching
2223        // "CmdRoot uint64" would find nothing and pass for the wrong reason.
2224        let declared = |name: &str| {
2225            out.lines()
2226                .filter(|l| l.split_whitespace().next() == Some(name))
2227                .count()
2228        };
2229        assert_eq!(declared("CmdRoot"), 1, "CmdRoot declared twice:\n{out}");
2230        assert_eq!(declared("CmdRoot2"), 1, "no distinct key for it:\n{out}");
2231        assert_declares_each_constant_once(&out);
2232    }
2233
2234    /// The two `var_max` are different questions, and the corpus pins them apart:
2235    /// on a flag's *argument* it bounds one occurrence's values and belongs in the
2236    /// binding table, while on the flag it counts occurrences and is checked after
2237    /// the parse.
2238    #[test]
2239    fn only_the_per_occurrence_bound_reaches_the_table() {
2240        let out = go(r#"
2241name "ex"
2242bin "ex"
2243flag "--include <pattern>..." {
2244    arg "<pattern>..." var=#true var_min=2 var_max=2
2245}
2246flag "--tag <t>" var=#true var_max=1
2247"#);
2248        assert!(
2249            out.contains("Name: \"include\", Longs: []string{\"include\"}, TakesValue: true, Variadic: true, VarMax: 2"),
2250            "{out}"
2251        );
2252        assert!(
2253            out.contains("Name: \"include\", Flag: true") && out.contains("VarMin: 2"),
2254            "the nested value minimum must reach post-binding metadata:\n{out}"
2255        );
2256        let tag = out.lines().find(|l| l.contains("\"tag\"")).unwrap();
2257        assert!(!tag.contains("VarMax"), "occurrence bound leaked: {tag}");
2258    }
2259
2260    #[test]
2261    fn exact_arity_with_one_label_reaches_go_help() {
2262        let out = go(r#"
2263name "ex"
2264bin "ex"
2265flag "--pair <ITEM>..." {
2266    arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2267        value_names "ITEM"
2268    }
2269}
2270arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2271    value_names "ITEM"
2272}
2273"#);
2274        assert_eq!(out.matches("ValueArity: 2").count(), 2, "{out}");
2275        assert_eq!(
2276            out.matches("ValueNames: []string{\"ITEM\"}").count(),
2277            2,
2278            "{out}"
2279        );
2280    }
2281
2282    #[test]
2283    fn allow_hyphen_values_reaches_the_table() {
2284        let out = go(r#"
2285name "ex"
2286bin "ex"
2287flag "--args <ARGS>" allow_hyphen_values=#true
2288"#);
2289        assert!(
2290            out.contains("Name: \"args\", Longs: []string{\"args\"}, TakesValue: true, AllowHyphenValues: true"),
2291            "{out}"
2292        );
2293    }
2294
2295    #[test]
2296    fn require_equals_reaches_the_table() {
2297        let out = go(r#"
2298name "ex"
2299bin "ex"
2300flag "--inspect <PORT>" require_equals=#true
2301"#);
2302        assert!(
2303            out.contains("Name: \"inspect\", Longs: []string{\"inspect\"}, TakesValue: true, RequireEquals: true"),
2304            "{out}"
2305        );
2306    }
2307
2308    #[test]
2309    fn default_missing_reaches_the_table() {
2310        let out = go(r#"
2311name "ex"
2312bin "ex"
2313flag "--color <WHEN>" default_missing="always"
2314"#);
2315        assert!(
2316            out.contains(
2317                "Name: \"color\", Longs: []string{\"color\"}, TakesValue: true, DefaultMissing: \"always\""
2318            ),
2319            "{out}"
2320        );
2321    }
2322
2323    /// A relationship names a flag by any spelling that reaches it, and from
2324    /// anywhere the flag is in scope.
2325    ///
2326    /// Both halves were silently resolving to nothing, which is worse than an
2327    /// error: the rule simply never fired, while usage-lib enforced it.
2328    #[test]
2329    fn a_relationship_resolves_through_scope_and_negation() {
2330        let out = go(r#"
2331name "ex"
2332bin "ex"
2333flag "--quiet" global=#true
2334flag "--color" negate="--no-color"
2335flag "--plain" conflicts="--no-color"
2336cmd "run" {
2337    flag "--loud" conflicts="--quiet"
2338    flag "--solo" conflicts="--plain"
2339}
2340"#);
2341        // A negation names the flag it belongs to.
2342        assert!(out.contains("Conflicts: []uint64{FlagColor}"), "{out}");
2343        // An inherited global is in scope from below.
2344        assert!(out.contains("Conflicts: []uint64{FlagQuiet}"), "{out}");
2345        // `--plain` is not global, so from a subcommand it names nothing — the
2346        // other half, and the one a looser search would get wrong.
2347        assert!(
2348            !entry_of(&out, "solo").contains("Conflicts"),
2349            "a non-global should not resolve from below:\n{out}"
2350        );
2351    }
2352
2353    #[test]
2354    fn positional_conflicts_reach_go_metadata_in_both_directions() {
2355        let out = go(r#"
2356name "ex"
2357bin "ex"
2358flag "--from-file <file>" conflicts="value"
2359arg "[value]" conflicts="--from-file"
2360"#);
2361
2362        assert!(
2363            entry_of(&out, "from-file").contains("Conflicts: []uint64{ArgValue}"),
2364            "{out}"
2365        );
2366        assert!(
2367            out.lines().any(|line| {
2368                line.contains("{Key: ArgValue, Name: \"value\"")
2369                    && line.contains("Conflicts: []uint64{FlagFromFile}")
2370            }),
2371            "{out}"
2372        );
2373    }
2374
2375    #[test]
2376    fn a_value_conditional_requirement_reaches_go_metadata() {
2377        let out = go(r#"
2378name "ex"
2379bin "ex"
2380flag "--format <format>" {
2381    requires_if "json" "--schema"
2382}
2383flag "--schema <file>"
2384"#);
2385        assert!(
2386            entry_of(&out, "format").contains(
2387                "RequiresIf: []argv.ValueRequirement{{Value: \"json\", Key: FlagSchema}}"
2388            ),
2389            "{out}"
2390        );
2391        assert!(
2392            out.contains("argv.CheckRelationshipsWithValues"),
2393            "the emitted parser must enforce the metadata:\n{out}"
2394        );
2395    }
2396
2397    #[test]
2398    fn required_if_eq_makes_generated_go_supply_values() {
2399        let out = go(r#"
2400name "ex"
2401bin "ex"
2402flag "--token <token>" {
2403    required_if_eq "--mode" "remote"
2404}
2405flag "--mode <mode>"
2406"#);
2407        assert!(
2408            entry_of(&out, "token").contains(
2409                "RequiredIfEq: []argv.ValueCondition{{Key: FlagMode, Value: \"remote\"}}"
2410            ),
2411            "{out}"
2412        );
2413        assert!(out.contains("resolved := map[uint64][]string{}"), "{out}");
2414        assert!(out.contains("argv.CheckRelationshipsWithValues"), "{out}");
2415    }
2416
2417    #[test]
2418    fn boolean_sources_are_normalized_for_value_relationships() {
2419        let out = go(r#"
2420name "ex"
2421bin "ex"
2422flag "--token <token>" {
2423    required_if_eq "--mode" "true"
2424}
2425flag "--mode" negate="--no-mode" bool_value=#true
2426"#);
2427        assert!(
2428            entry_of(&out, "mode").contains("RequiresIfBoolean: true"),
2429            "{out}"
2430        );
2431    }
2432
2433    #[test]
2434    fn a_conditional_default_reaches_go_metadata() {
2435        let out = go(r#"
2436name "ex"
2437bin "ex"
2438flag "--bin-names" {
2439    default_if "--json" "true"
2440    default_if "--output" "json" "pretty"
2441}
2442flag "--json"
2443flag "--output <fmt>"
2444"#);
2445        assert!(
2446            entry_of(&out, "bin-names")
2447                .contains("DefaultIf: []argv.DefaultIf{{Key: FlagJson, Value: \"true\"}"),
2448            "{out}"
2449        );
2450        assert!(
2451            entry_of(&out, "bin-names").contains("When: \"json\""),
2452            "{out}"
2453        );
2454        assert!(
2455            out.contains("argv.ApplyDefaultIf"),
2456            "the emitted parser must apply the metadata:\n{out}"
2457        );
2458        assert!(
2459            out.contains("negated[ev.Flag.Key] = ev.Negated"),
2460            "Equals default_if needs the negate form:\n{out}"
2461        );
2462    }
2463
2464    /// The form is part of the name, and usage-lib resolves neither of the
2465    /// mismatched ones — so resolving them would have a generated CLI enforcing a
2466    /// rule the reference does not.
2467    #[test]
2468    fn a_relationship_needs_the_right_form() {
2469        let out = go(r#"
2470name "ex"
2471bin "ex"
2472flag "-q --quiet"
2473flag "--color"
2474flag "--a" conflicts="--q"
2475flag "--b" conflicts="-color"
2476flag "--c" conflicts="-q"
2477flag "--d" conflicts="--color"
2478"#);
2479        // `--q` is not a long form of anything, and `-color` is not a short.
2480        assert!(!entry_of(&out, "a").contains("Conflicts"), "{out}");
2481        assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
2482        // The forms the flags actually have.
2483        assert!(
2484            entry_of(&out, "c").contains("Conflicts: []uint64{FlagQuiet}"),
2485            "{out}"
2486        );
2487        assert!(
2488            entry_of(&out, "d").contains("Conflicts: []uint64{FlagColor}"),
2489            "{out}"
2490        );
2491    }
2492
2493    /// The table has to agree with the binder it feeds.
2494    ///
2495    /// The parser tries every long form before any negation, so with `--a`
2496    /// declaring `negate="--zap"` and a separate `--zap`, typing `--zap` binds
2497    /// *zap*. A per-candidate search handed the relationship to `a`, which would
2498    /// have enforced the rule against a flag the command line never binds.
2499    #[test]
2500    fn an_ordinary_form_beats_another_flags_negation() {
2501        let out = go(r#"
2502name "ex"
2503bin "ex"
2504flag "--a" negate="--zap"
2505flag "--zap"
2506flag "--p" conflicts="--zap"
2507"#);
2508        assert!(
2509            entry_of(&out, "p").contains("Conflicts: []uint64{FlagZap}"),
2510            "should name the flag `--zap` binds, not the one negating to it:\n{out}"
2511        );
2512    }
2513
2514    /// A negation is named by the form it was written as, whatever the dashes.
2515    #[test]
2516    fn a_single_dash_negation_is_named_by_its_own_form() {
2517        let out = go(r#"
2518name "ex"
2519bin "ex"
2520flag "--tint" negate="-no-tint"
2521flag "--plain" conflicts="-no-tint"
2522flag "--other" conflicts="--no-tint"
2523"#);
2524        assert!(
2525            entry_of(&out, "plain").contains("Conflicts: []uint64{FlagTint}"),
2526            "the exact form should resolve:\n{out}"
2527        );
2528        // And the form it was not written as does not.
2529        assert!(
2530            !entry_of(&out, "other").contains("Conflicts"),
2531            "`--no-tint` is not how it was declared:\n{out}"
2532        );
2533    }
2534
2535    /// A negation is matched as the spec wrote it, dashes and all.
2536    #[test]
2537    fn a_negation_is_matched_as_written() {
2538        let out = go(r#"
2539name "ex"
2540bin "ex"
2541flag "--color" negate="--no-color"
2542flag "--tint" negate="-no-tint"
2543flag "--a" conflicts="--no-color"
2544flag "--b" conflicts="--no-tint"
2545"#);
2546        assert!(
2547            entry_of(&out, "a").contains("Conflicts: []uint64{FlagColor}"),
2548            "{out}"
2549        );
2550        // `--no-tint` is not the form `-no-tint`, so it names nothing — as in
2551        // usage-lib, which does not resolve it either.
2552        assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
2553    }
2554
2555    #[test]
2556    fn strings_are_escaped_to_go_rules() {
2557        assert_eq!(go_string(r#"a"b\c"#), r#""a\"b\\c""#);
2558        assert_eq!(go_string("tab\there"), r#""tab\there""#);
2559        // Rust would spell this `\u{7f}`, which Go rejects.
2560        assert_eq!(go_string("\u{7f}"), r#""\x7f""#);
2561    }
2562}