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