1mod structs;
34mod types;
35
36pub use types::ValueType;
37
38use std::collections::{BTreeMap, HashMap};
39use std::fmt::Write as _;
40
41use crate::case::AsPascalCase;
42
43use crate::spec::unknown_flags::UnknownFlags;
44use crate::{
45 Spec, SpecArg, SpecChoices, SpecCommand, SpecDoubleDashChoices, SpecFlag, SpecFlagAction,
46};
47
48#[derive(Debug, Clone, Default)]
50pub struct GoOptions {
51 pub package: Option<String>,
58}
59
60pub fn generate(spec: &Spec, opts: &GoOptions) -> String {
62 Emitter::new(spec, opts).run()
63}
64
65pub fn generate_with_types(
70 spec: &Spec,
71 opts: &GoOptions,
72 field_types: &BTreeMap<String, ValueType>,
73) -> Result<String, String> {
74 if field_types.is_empty() {
75 return Ok(generate(spec, opts));
76 }
77 let mut probe = Emitter::new(spec, opts);
78 let mut commands = Vec::new();
79 probe.collect(&spec.cmd, &[], true, &mut commands);
80 types::validate(&commands, field_types)?;
81 let mut emitter = Emitter::new(spec, opts);
82 emitter.field_types = field_types.clone();
83 Ok(emitter.run())
84}
85
86struct Named {
89 key: String,
90 var: String,
91 number: u64,
92}
93
94struct Emitter<'a> {
95 spec: &'a Spec,
96 field_types: BTreeMap<String, ValueType>,
97 package: String,
98 taken: HashMap<String, u32>,
101 next_key: u64,
103 out: String,
104}
105
106impl<'a> Emitter<'a> {
107 fn new(spec: &'a Spec, opts: &GoOptions) -> Self {
109 let package = match opts.package.as_deref() {
113 Some(name) if is_valid_package(name) => name.to_string(),
114 Some(name) => package_ident(name),
115 None => package_ident(&spec.bin),
116 };
117 Emitter {
118 spec,
119 field_types: BTreeMap::new(),
120 package,
121 taken: HashMap::new(),
122 next_key: 0,
123 out: String::new(),
124 }
125 }
126
127 fn unique(&mut self, base: &str) -> String {
139 let mut n = self.taken.get(base).copied().unwrap_or(0);
140 loop {
141 n += 1;
142 let candidate = if n == 1 {
143 base.to_string()
144 } else {
145 format!("{base}{n}")
146 };
147 if !self.taken.contains_key(&candidate) {
148 self.taken.insert(base.to_string(), n);
149 self.taken.entry(candidate.clone()).or_insert(0);
152 return candidate;
153 }
154 }
155 }
156
157 fn name(&mut self, prefix: &str, path: &[&str], own: &str) -> Named {
158 let mut base = String::from(prefix);
159 for segment in path {
160 let _ = write!(base, "{}", AsPascalCase(segment));
161 }
162 let _ = write!(base, "{}", AsPascalCase(own));
163 let key = self.unique(&base);
164 self.next_key += 1;
165 Named {
166 var: format!("cmd{}", &key[prefix.len()..]),
167 key,
168 number: self.next_key,
169 }
170 }
171
172 fn run(mut self) -> String {
174 let mut commands = Vec::new();
178 self.collect(&self.spec.cmd.clone(), &[], true, &mut commands);
179
180 self.header();
181 self.constants(&commands);
182 self.tables(&commands);
183 self.metadata(&commands);
184 self.help_table(&commands);
185 structs::emit(&mut self.out, &commands, &self.field_types);
186
187 let trimmed = self.out.trim_end().len();
191 self.out.truncate(trimmed);
192 self.out.push('\n');
193 self.out
194 }
195
196 fn collect(&mut self, cmd: &SpecCommand, path: &[&str], root: bool, out: &mut Vec<Emitted>) {
199 let named = if root {
200 self.next_key += 1;
201 Named {
202 key: self.unique("CmdRoot"),
207 var: "Root".to_string(),
208 number: self.next_key,
209 }
210 } else {
211 self.name("Cmd", &path[..path.len() - 1], path[path.len() - 1])
212 };
213
214 let flags = cmd
215 .flags
216 .iter()
217 .map(|f| (f.clone(), self.name("Flag", path, &f.name)))
218 .collect::<Vec<_>>();
219 let args = cmd
220 .args
221 .iter()
222 .map(|a| (a.clone(), self.name("Arg", path, &a.name)))
223 .collect::<Vec<_>>();
224 let clause_args = cmd
225 .clause
226 .as_ref()
227 .map(|clause| {
228 clause
229 .args
230 .iter()
231 .map(|a| {
232 (
233 a.clone(),
234 self.name("Arg", path, &format!("{}-{}", clause.name, a.name)),
235 )
236 })
237 .collect::<Vec<_>>()
238 })
239 .unwrap_or_default();
240 let clause_flags = cmd
241 .clause
242 .as_ref()
243 .map(|clause| {
244 clause
245 .flags
246 .iter()
247 .map(|flag| {
248 (
249 flag.clone(),
250 self.name("Flag", path, &format!("{}-{}", clause.name, flag.name)),
251 )
252 })
253 .collect::<Vec<_>>()
254 })
255 .unwrap_or_default();
256
257 let index = out.len();
258 out.push(Emitted {
259 named,
260 cmd: cmd.clone(),
261 flags,
262 args,
263 clause_flags,
264 clause_args,
265 subcommands: Vec::new(),
266 root,
267 });
268
269 let mut children = Vec::new();
273 for (name, sub) in &cmd.subcommands {
274 if name != &sub.name {
278 continue;
279 }
280 let mut child_path = path.to_vec();
281 child_path.push(name);
282 let at = out.len();
283 self.collect(sub, &child_path, false, out);
284 children.push(at);
285 }
286 out[index].subcommands = children;
287 }
288
289 fn header(&mut self) {
291 let imports = if self
292 .field_types
293 .values()
294 .any(|ty| *ty == ValueType::Duration)
295 {
296 "import (\n\t\"time\"\n\n\t\"github.com/jdx/usage/go/argv\"\n)"
297 } else {
298 "import \"github.com/jdx/usage/go/argv\""
299 };
300 let _ = writeln!(
301 self.out,
302 "// Code generated by `usage generate go`. DO NOT EDIT.\n\
303 //\n\
304 // Binding tables for `{}`, read by\n\
305 // [github.com/jdx/usage/go/argv]. Regenerate rather than editing: the spec is\n\
306 // the definition, and a hand-edit here is a difference no reviewer can see.\n\
307 //\n\
308 // These are package-level variables holding plain data, so the linker lays them\n\
309 // out and nothing runs before main.\n\
310 \n\
311 package {}\n\
312 \n\
313 {imports}\n",
314 self.spec.bin, self.package
315 );
316
317 if let Some(version) = self
318 .spec
319 .version
320 .as_ref()
321 .or(self.spec.long_version.as_ref())
322 {
323 let _ = writeln!(
324 self.out,
325 "// Version is what the spec declares, so a caller answering `--version` has it\n\
326 // without the parse tables carrying a string binding never reads.\n\
327 const Version = {}\n",
328 go_string(version)
329 );
330 }
331 if let Some(version) = &self.spec.long_version {
332 let _ = writeln!(
333 self.out,
334 "// LongVersion is the extended text printed for `--version`; `-V` uses Version.\n\
335 const LongVersion = {}\n",
336 go_string(version)
337 );
338 }
339 }
340
341 fn constants(&mut self, commands: &[Emitted]) {
342 let _ = writeln!(
343 self.out,
344 "// Keys identify a table entry without a string comparison: switch on the Key an\n\
345 // event carries rather than on its Name, which is there for diagnostics.\n\
346 const ("
347 );
348 let mut entries: Vec<(&str, u64)> = Vec::new();
349 for e in commands {
350 entries.push((&e.named.key, e.named.number));
351 entries.extend(e.flags.iter().map(|(_, n)| (n.key.as_str(), n.number)));
352 entries.extend(
353 e.clause_flags
354 .iter()
355 .map(|(_, n)| (n.key.as_str(), n.number)),
356 );
357 entries.extend(e.args.iter().map(|(_, n)| (n.key.as_str(), n.number)));
358 entries.extend(
359 e.clause_args
360 .iter()
361 .map(|(_, n)| (n.key.as_str(), n.number)),
362 );
363 }
364 let width = entries.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
367 for (key, number) in entries {
368 let _ = writeln!(
369 self.out,
370 "\t{key}{:pad$} uint64 = {number}",
371 "",
372 pad = width - key.len()
373 );
374 }
375 let _ = writeln!(self.out, ")\n");
376 }
377
378 fn tables(&mut self, commands: &[Emitted]) {
379 let default_subcommand = self.spec.default_subcommand.as_ref().and_then(|name| {
386 let direct = || commands[0].subcommands.iter().map(|at| &commands[*at]);
387 direct()
391 .find(|e| &e.cmd.name == name)
392 .or_else(|| {
393 direct().find(|e| {
394 e.cmd.aliases.contains(name) || e.cmd.hidden_aliases.contains(name)
395 })
396 })
397 .map(|e| e.named.var.clone())
398 });
399
400 for (i, e) in commands.iter().enumerate() {
401 let doc = if e.root {
402 format!(
403 "// Root is the command tree for `{}`. Pass it to argv.New.",
404 self.spec.bin
405 )
406 } else {
407 format!("// {}", e.cmd.full_cmd.join(" "))
408 };
409 let mut lines = vec![
410 Line::Field("Name".into(), go_string(&e.cmd.name)),
411 Line::Field("Key".into(), e.named.key.clone()),
412 ];
413
414 let aliases: Vec<&String> = e
415 .cmd
416 .aliases
417 .iter()
418 .chain(e.cmd.hidden_aliases.iter())
419 .collect();
420 if !aliases.is_empty() {
421 let list = aliases
424 .iter()
425 .map(|a| go_string(a))
426 .collect::<Vec<_>>()
427 .join(", ");
428 lines.push(Line::Field("Aliases".into(), format!("[]string{{{list}}}")));
429 }
430
431 if !e.flags.is_empty() || !e.clause_flags.is_empty() {
432 let mut block = vec!["Flags: []*argv.Flag{".to_string()];
433 for (flag, named) in e.flags.iter().chain(&e.clause_flags) {
434 block.push(format!("\t{},", flag_literal(flag, named)));
435 }
436 block.push("},".to_string());
437 lines.push(Line::Block(block));
438 }
439
440 if !e.args.is_empty() {
441 let mut block = vec!["Args: []*argv.Arg{".to_string()];
442 for (arg, named) in &e.args {
443 block.push(format!("\t{},", arg_literal(arg, named)));
444 }
445 block.push("},".to_string());
446 lines.push(Line::Block(block));
447 }
448 if let Some(clause) = &e.cmd.clause {
449 let mut block = vec!["Clause: &argv.Clause{".to_string()];
450 block.push(format!("\tKey: {},", e.named.key));
451 block.push(format!("\tName: {},", go_string(&clause.name)));
452 block.push(format!(
453 "\tSeparator: {},",
454 go_string(clause.separator.as_deref().unwrap_or_default())
455 ));
456 block.push("\tFlags: []*argv.Flag{".to_string());
457 for (flag, named) in &e.clause_flags {
458 block.push(format!("\t\t{},", flag_literal(flag, named)));
459 }
460 block.push("\t},".to_string());
461 block.push("\tArgs: []*argv.Arg{".to_string());
462 for (arg, named) in &e.clause_args {
463 block.push(format!("\t\t{},", arg_literal(arg, named)));
464 }
465 block.push("\t},".to_string());
466 block.push("},".to_string());
467 lines.push(Line::Block(block));
468 }
469
470 if !e.subcommands.is_empty() {
471 let list = e
472 .subcommands
473 .iter()
474 .map(|at| commands[*at].named.var.clone())
475 .collect::<Vec<_>>()
476 .join(", ");
477 lines.push(Line::Field(
478 "Subcommands".into(),
479 format!("[]*argv.Command{{{list}}}"),
480 ));
481 }
482
483 if effective_unknown_flags(self.spec, commands, i) == UnknownFlags::Error {
486 lines.push(Line::Field(
487 "UnknownFlags".into(),
488 "argv.UnknownFlagsError".into(),
489 ));
490 }
491
492 if e.cmd.external_subcommand {
493 lines.push(Line::Field("ExternalSubcommand".into(), "true".into()));
494 }
495 if e.cmd.arg_required_else_help {
496 lines.push(Line::Field("ArgRequiredElseHelp".into(), "true".into()));
497 }
498 if e.cmd.disable_help_flag {
499 lines.push(Line::Field("DisableHelpFlag".into(), "true".into()));
500 }
501 if e.cmd.disable_help_subcommand {
502 lines.push(Line::Field("DisableHelpSubcommand".into(), "true".into()));
503 }
504 if e.cmd.disable_version_flag {
505 lines.push(Line::Field("DisableVersionFlag".into(), "true".into()));
506 }
507 if e.cmd.subcommand_negates_reqs {
508 lines.push(Line::Field("SubcommandNegatesReqs".into(), "true".into()));
509 }
510 if e.cmd.args_conflicts_with_subcommands {
511 lines.push(Line::Field(
512 "ArgsConflictWithSubcommands".into(),
513 "true".into(),
514 ));
515 }
516 if e.cmd.subcommand_precedence_over_arg {
517 lines.push(Line::Field(
518 "SubcommandPrecedenceOverArg".into(),
519 "true".into(),
520 ));
521 }
522 if e.cmd.allow_missing_positional {
523 lines.push(Line::Field("AllowMissingPositional".into(), "true".into()));
524 }
525 if e.cmd.dont_delimit_trailing_values {
526 lines.push(Line::Field(
527 "DontDelimitTrailingValues".into(),
528 "true".into(),
529 ));
530 }
531 if e.root {
532 if self.spec.default_subcommand_flags {
533 lines.push(Line::Field("DefaultSubcommandFlags".into(), "true".into()));
534 }
535 if self.spec.default_subcommand_on_empty {
536 lines.push(Line::Field(
537 "DefaultSubcommandOnEmpty".into(),
538 "true".into(),
539 ));
540 }
541 if let Some(var) = &default_subcommand {
542 lines.push(Line::Field("DefaultSubcommand".into(), var.clone()));
543 }
544 if self.spec.version.is_some() || self.spec.long_version.is_some() {
545 lines.push(Line::Field("Version".into(), "true".into()));
548 }
549 }
550
551 let _ = writeln!(self.out, "{doc}");
552 let _ = writeln!(self.out, "var {} = &argv.Command{{", e.named.var);
553 render(&mut self.out, "\t", &lines);
554 let _ = writeln!(self.out, "}}\n");
555 }
556 }
557}
558
559impl Emitter<'_> {
560 fn metadata(&mut self, commands: &[Emitted]) {
570 let mut by_key: BTreeMap<u64, String> = BTreeMap::new();
572 for e in commands {
573 for (flag, named) in e.flags.iter().chain(&e.clause_flags) {
574 by_key.insert(named.number, self.flag_meta(flag, named, e, commands));
575 }
576 for (arg, named) in &e.args {
577 by_key.insert(named.number, arg_meta(self.spec, arg, named, e, commands));
578 }
579 for (arg, named) in &e.clause_args {
580 by_key.insert(named.number, arg_meta(self.spec, arg, named, e, commands));
581 }
582 }
583
584 let total = commands
585 .iter()
586 .map(|e| 1 + e.flags.len() + e.clause_flags.len() + e.args.len() + e.clause_args.len())
587 .sum::<usize>() as u64;
588
589 let _ = writeln!(
590 self.out,
591 "// Meta is the cold table, read only by the rules that are decided once the\n\
592 // last token has been read: required, choices, the env-then-default fallback,\n\
593 // the var bounds, and the four that compare one entry against another. A parse\n\
594 // never touches it.\n\
595 //\n\
596 // Indexed by key, so entry Key sits at Meta[Key-1]. A command's slot is empty:\n\
597 // commands take keys too, and have no cold half.\n\
598 var Meta = argv.Metadata{{"
599 );
600 for key in 1..=total {
601 match by_key.get(&key) {
602 Some(entry) => {
603 let _ = writeln!(self.out, "\t{entry},");
604 }
605 None => {
606 let _ = writeln!(self.out, "\t{{}},");
607 }
608 }
609 }
610 let _ = writeln!(self.out, "}}\n");
611 }
612
613 fn flag_meta(
615 &self,
616 flag: &SpecFlag,
617 named: &Named,
618 owner: &Emitted,
619 commands: &[Emitted],
620 ) -> String {
621 let mut fields = vec![
622 format!("Key: {}", named.key),
623 format!("Name: {}", go_string(&flag.name)),
624 "Flag: true".to_string(),
625 ];
626 let scoped = owner
627 .clause_flags
628 .iter()
629 .any(|(_, candidate)| candidate.key == named.key);
630 if (!owner.cmd.args_override_self || scoped)
631 && !flag.var
632 && !flag.count
633 && !flag.arg.as_ref().is_some_and(|arg| arg.var)
634 {
635 fields.push("RejectDuplicate: true".to_string());
636 }
637 if flag.required {
638 fields.push("Required: true".to_string());
639 }
640 if flag.arg.is_none() {
641 fields.push("RequiresIfBoolean: true".to_string());
642 }
643 if let Some(long) = flag.long.first() {
647 fields.push(format!("Spelling: {}", go_string(&format!("--{long}"))));
648 } else if let Some(short) = flag.short.first() {
649 fields.push(format!("Spelling: {}", go_string(&format!("-{short}"))));
650 }
651 if let Some(value) = flag.arg.as_ref() {
654 fields.push(format!("ValueName: {}", go_string(&value.name)));
655 }
656 let named_value = flag
657 .arg
658 .as_ref()
659 .map(|a| a.name.as_str())
660 .unwrap_or(flag.name.as_str());
661 if let Some(kind) = complete_type(self.spec, named_value) {
662 fields.push(format!("CompleteType: {}", go_string(kind)));
663 }
664 if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) {
666 fields.push(format!(
667 "Choices: {}",
668 string_slice(&visible_choices(choices))
669 ));
670 fields.push(format!(
671 "AcceptedChoices: {}",
672 string_slice(&accepted_choices(choices))
673 ));
674 if choices.ignore_case {
675 fields.push("IgnoreCase: true".to_string());
676 }
677 if !choices.strict {
678 fields.push("AllowUnknownChoices: true".to_string());
679 }
680 }
681 let default = if !flag.default.is_empty() {
685 &flag.default
686 } else {
687 flag.arg
688 .as_ref()
689 .map(|a| &a.default)
690 .unwrap_or(&flag.default)
691 };
692 if !default.is_empty() {
693 fields.push(format!("Default: {}", string_slice(default)));
694 }
695 if let Some(env) = &flag.env {
696 fields.push(format!("Env: {}", go_string(env)));
697 }
698 if !flag.env_fallback.is_empty() {
699 fields.push(format!("EnvFallback: {}", string_slice(&flag.env_fallback)));
700 }
701 if !flag.deprecated_env.is_empty() {
702 fields.push(format!(
703 "DeprecatedEnv: {}",
704 string_slice(&flag.deprecated_env)
705 ));
706 }
707 let minimum = flag
708 .arg
709 .as_ref()
710 .filter(|arg| arg.var)
711 .and_then(|arg| arg.var_min)
712 .or(flag.var_min);
713 if let Some(min) = minimum {
714 fields.push(format!("VarMin: {}", clamp_var_max(min)));
715 }
716 if let Some(max) = flag.var_max {
719 fields.push(format!("VarMax: {}", clamp_var_max(max)));
720 }
721
722 for (label, names) in [
723 ("Conflicts", &flag.conflicts),
724 ("Overrides", &flag.overrides),
725 ("RequiredUnless", &flag.required_unless),
726 ("RequiredUnlessAll", &flag.required_unless_all),
727 ("RequiredIf", &flag.required_if),
728 ("Requires", &flag.requires),
729 ] {
730 let keys = resolve_relationship(names, owner, commands);
731 if !keys.is_empty() {
732 fields.push(format!("{label}: {}", key_slice(&keys)));
733 }
734 }
735 for (label, conditions) in [
736 ("RequiredIfEq", &flag.required_if_eq),
737 ("RequiredIfEqAll", &flag.required_if_eq_all),
738 ] {
739 let values = conditions
740 .iter()
741 .filter_map(|condition| {
742 resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
743 .into_iter()
744 .next()
745 .map(|key| {
746 format!("{{Key: {key}, Value: {}}}", go_string(&condition.value))
747 })
748 })
749 .collect::<Vec<_>>();
750 if !values.is_empty() {
751 fields.push(format!(
752 "{label}: []argv.ValueCondition{{{}}}",
753 values.join(", ")
754 ));
755 }
756 }
757 let requires_if = flag
758 .requires_if
759 .iter()
760 .filter_map(|condition| {
761 resolve_relationship(std::slice::from_ref(&condition.requires), owner, commands)
762 .into_iter()
763 .next()
764 .map(|key| format!("{{Value: {}, Key: {key}}}", go_string(&condition.value)))
765 })
766 .collect::<Vec<_>>();
767 if !requires_if.is_empty() {
768 fields.push(format!(
769 "RequiresIf: []argv.ValueRequirement{{{}}}",
770 requires_if.join(", ")
771 ));
772 }
773 let default_if = flag
774 .default_if
775 .iter()
776 .filter_map(|condition| {
777 resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
778 .into_iter()
779 .next()
780 .map(|key| match &condition.when {
781 None => format!("{{Key: {key}, Value: {}}}", go_string(&condition.value)),
782 Some(when) => format!(
783 "{{Key: {key}, When: {}, Value: {}}}",
784 go_string(when),
785 go_string(&condition.value)
786 ),
787 })
788 })
789 .collect::<Vec<_>>();
790 if !default_if.is_empty() {
791 fields.push(format!(
792 "DefaultIf: []argv.DefaultIf{{{}}}",
793 default_if.join(", ")
794 ));
795 }
796
797 format!("{{{}}}", fields.join(", "))
798 }
799}
800
801fn complete_type<'a>(spec: &'a Spec, name: &str) -> Option<&'a str> {
808 spec.complete
809 .get(&name.to_lowercase())
810 .and_then(|c| c.type_.as_deref())
811}
812
813fn arg_meta(
814 spec: &Spec,
815 arg: &SpecArg,
816 named: &Named,
817 owner: &Emitted,
818 commands: &[Emitted],
819) -> String {
820 let mut fields = vec![
821 format!("Key: {}", named.key),
822 format!("Name: {}", go_string(&arg.name)),
823 ];
824 if arg.required {
825 fields.push("Required: true".to_string());
826 }
827 if let Some(kind) = complete_type(spec, &arg.name) {
832 fields.push(format!("CompleteType: {}", go_string(kind)));
833 }
834 if let Some(choices) = &arg.choices {
835 fields.push(format!(
836 "Choices: {}",
837 string_slice(&visible_choices(choices))
838 ));
839 fields.push(format!(
840 "AcceptedChoices: {}",
841 string_slice(&accepted_choices(choices))
842 ));
843 if choices.ignore_case {
844 fields.push("IgnoreCase: true".to_string());
845 }
846 if !choices.strict {
847 fields.push("AllowUnknownChoices: true".to_string());
848 }
849 }
850 if !arg.default.is_empty() {
851 fields.push(format!("Default: {}", string_slice(&arg.default)));
852 }
853 if let Some(env) = &arg.env {
854 fields.push(format!("Env: {}", go_string(env)));
855 }
856 if !arg.env_fallback.is_empty() {
857 fields.push(format!("EnvFallback: {}", string_slice(&arg.env_fallback)));
858 }
859 if !arg.deprecated_env.is_empty() {
860 fields.push(format!(
861 "DeprecatedEnv: {}",
862 string_slice(&arg.deprecated_env)
863 ));
864 }
865 if let Some(min) = arg.var_min {
866 fields.push(format!("VarMin: {}", clamp_var_max(min)));
867 }
868 let conflicts = resolve_relationship(&arg.conflicts, owner, commands);
869 if !conflicts.is_empty() {
870 fields.push(format!("Conflicts: {}", key_slice(&conflicts)));
871 }
872 for (label, names) in [
873 ("Requires", &arg.requires),
874 ("RequiredIf", &arg.required_if),
875 ("RequiredUnless", &arg.required_unless),
876 ("RequiredUnlessAll", &arg.required_unless_all),
877 ] {
878 let keys = resolve_relationship(names, owner, commands);
879 if !keys.is_empty() {
880 fields.push(format!("{label}: {}", key_slice(&keys)));
881 }
882 }
883 for (label, conditions) in [
884 ("RequiredIfEq", &arg.required_if_eq),
885 ("RequiredIfEqAll", &arg.required_if_eq_all),
886 ] {
887 let values = conditions
888 .iter()
889 .filter_map(|condition| {
890 resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
891 .into_iter()
892 .next()
893 .map(|key| format!("{{Key: {key}, Value: {}}}", go_string(&condition.value)))
894 })
895 .collect::<Vec<_>>();
896 if !values.is_empty() {
897 fields.push(format!(
898 "{label}: []argv.ValueCondition{{{}}}",
899 values.join(", ")
900 ));
901 }
902 }
903 format!("{{{}}}", fields.join(", "))
907}
908
909fn resolve_relationship(names: &[String], owner: &Emitted, commands: &[Emitted]) -> Vec<String> {
920 let mut out = Vec::new();
921 for name in names {
922 let mut found = match_flag(owner, name, false);
926 if found.is_none() && !name.starts_with('-') {
927 found = owner
928 .args
929 .iter()
930 .chain(owner.clause_args.iter())
931 .find(|(arg, _)| arg.name == *name)
932 .map(|(_, named)| named.key.clone());
933 }
934 if found.is_none() {
935 let path = &owner.cmd.full_cmd;
936 for depth in (0..path.len()).rev() {
937 let ancestor = commands
938 .iter()
939 .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]);
940 if let Some(key) = ancestor.and_then(|a| match_flag(a, name, true)) {
941 found = Some(key);
942 break;
943 }
944 }
945 }
946 if let Some(key) = found {
947 out.push(key);
948 }
949 }
950 out
951}
952
953fn match_flag(cmd: &Emitted, name: &str, globals_only: bool) -> Option<String> {
960 let eligible = |flag: &SpecFlag| !globals_only || flag.global;
970
971 let (long, short, bare) = if let Some(rest) = name.strip_prefix("--") {
974 (Some(rest), None, None)
975 } else if let Some(rest) = name.strip_prefix('-') {
976 let mut chars = rest.chars();
977 match (chars.next(), chars.next()) {
978 (Some(c), None) => (None, Some(c), None),
979 _ => (None, None, None),
980 }
981 } else {
982 (None, None, Some(name))
983 };
984
985 let ordinary = cmd.flags.iter().chain(&cmd.clause_flags).find(|(flag, _)| {
986 if !eligible(flag) {
987 return false;
988 }
989 if let Some(bare) = bare {
990 return flag.name == bare;
991 }
992 if let Some(long) = long {
993 return flag.long.iter().any(|l| l == long);
994 }
995 short.is_some_and(|c| flag.short.contains(&c))
996 });
997 if let Some((_, named)) = ordinary {
998 return Some(named.key.clone());
999 }
1000
1001 cmd.flags
1005 .iter()
1006 .chain(&cmd.clause_flags)
1007 .find(|(flag, _)| eligible(flag) && flag.negate.as_deref() == Some(name))
1008 .map(|(_, named)| named.key.clone())
1009}
1010fn string_slice(values: &[String]) -> String {
1011 let list = values
1012 .iter()
1013 .map(|v| go_string(v))
1014 .collect::<Vec<_>>()
1015 .join(", ");
1016 format!("[]string{{{list}}}")
1017}
1018
1019fn accepted_choices(choices: &SpecChoices) -> Vec<String> {
1020 choices
1021 .choices
1022 .iter()
1023 .chain(
1024 choices
1025 .details
1026 .iter()
1027 .flat_map(|choice| choice.aliases.iter().map(|alias| &alias.value)),
1028 )
1029 .cloned()
1030 .collect()
1031}
1032
1033fn visible_choices(choices: &SpecChoices) -> Vec<String> {
1034 choices
1035 .choices
1036 .iter()
1037 .filter(|value| {
1038 !choices
1039 .details
1040 .iter()
1041 .any(|choice| choice.value == value.as_str() && choice.hide)
1042 })
1043 .chain(choices.details.iter().flat_map(|choice| {
1044 choice
1045 .aliases
1046 .iter()
1047 .filter(|alias| !alias.hide)
1048 .map(|alias| &alias.value)
1049 }))
1050 .cloned()
1051 .collect()
1052}
1053
1054fn key_slice(keys: &[String]) -> String {
1055 format!("[]uint64{{{}}}", keys.join(", "))
1056}
1057
1058impl Emitter<'_> {
1059 fn help_table(&mut self, commands: &[Emitted]) {
1066 let mut by_key: BTreeMap<u64, String> = BTreeMap::new();
1067 for e in commands {
1068 by_key.insert(e.named.number, command_help(e));
1069 for (flag, named) in e.flags.iter().chain(&e.clause_flags) {
1070 by_key.insert(named.number, flag_help(flag, named));
1071 }
1072 for (arg, named) in &e.args {
1073 by_key.insert(named.number, arg_help(arg, named));
1074 }
1075 for (arg, named) in &e.clause_args {
1076 by_key.insert(named.number, arg_help(arg, named));
1077 }
1078 }
1079
1080 let total = commands
1081 .iter()
1082 .map(|e| 1 + e.flags.len() + e.clause_flags.len() + e.args.len() + e.clause_args.len())
1083 .sum::<usize>() as u64;
1084
1085 let _ = writeln!(
1086 self.out,
1087 "// HelpText is the third table, read only when a page is rendered. Neither the\n\
1088 // parser nor the post-binding rules touch it, and a CLI that never prints help\n\
1089 // does not carry it: Go's linker drops an unreferenced table whole.\n\
1090 //\n\
1091 // Indexed by key, like the others.\n\
1092 var HelpText = argv.HelpTable{{"
1093 );
1094 for key in 1..=total {
1095 match by_key.get(&key) {
1096 Some(entry) => {
1097 let _ = writeln!(self.out, "\t{entry},");
1098 }
1099 None => {
1100 let _ = writeln!(self.out, "\t{{}},");
1101 }
1102 }
1103 }
1104 let _ = writeln!(self.out, "}}\n");
1105
1106 let mut fields = vec![
1107 format!("Name: {}", go_string(&self.spec.name)),
1108 format!("Bin: {}", go_string(&self.spec.bin)),
1109 ];
1110 if let Some(version) = self
1111 .spec
1112 .version
1113 .as_ref()
1114 .or(self.spec.long_version.as_ref())
1115 {
1116 fields.push(format!("Version: {}", go_string(version)));
1117 }
1118 if let Some(version) = &self.spec.long_version {
1119 fields.push(format!("LongVersion: {}", go_string(version)));
1120 }
1121 if let Some(about) = &self.spec.about {
1125 fields.push(format!("About: {}", go_string(about)));
1126 }
1127 if let Some(long) = &self.spec.about_long {
1128 fields.push(format!("LongAbout: {}", go_string(long)));
1129 }
1130 if let Some(author) = &self.spec.author {
1131 fields.push(format!("Author: {}", go_string(author)));
1132 }
1133 if let Some(license) = &self.spec.license {
1134 fields.push(format!("License: {}", go_string(license)));
1135 }
1136 if let Some(logo) = &self.spec.logo {
1139 fields.push(format!("Logo: {}", go_string(logo)));
1140 }
1141 if let Some(before) = &self.spec.before_help {
1142 fields.push(format!("BeforeHelp: {}", go_string(before)));
1143 }
1144 if let Some(after) = &self.spec.after_help {
1145 fields.push(format!("AfterHelp: {}", go_string(after)));
1146 }
1147 if let Some(before) = &self.spec.before_help_long {
1148 fields.push(format!("BeforeLongHelp: {}", go_string(before)));
1149 }
1150 if let Some(after) = &self.spec.after_help_long {
1151 fields.push(format!("AfterLongHelp: {}", go_string(after)));
1152 }
1153 if let Some(template) = &self.spec.help_template {
1155 fields.push(format!("HelpTemplate: {}", go_string(template)));
1156 }
1157 let _ = writeln!(
1158 self.out,
1159 "// HelpMeta is what a page needs from the spec's root rather than from any one\n\
1160 // command: root presentation metadata and CLI-wide layout.\n\
1161 var HelpMeta = argv.HelpSpec{{{}}}\n",
1162 fields.join(", ")
1163 );
1164 }
1165}
1166
1167fn command_help(e: &Emitted) -> String {
1169 let mut fields = vec![format!("Key: {}", e.named.key)];
1170 if e.cmd.hide {
1171 fields.push("Hide: true".to_string());
1172 }
1173 if let Some(heading) = &e.cmd.help_heading {
1174 fields.push(format!("Heading: {}", go_string(heading)));
1175 }
1176 if let Some(order) = e.cmd.display_order {
1177 fields.push(format!("DisplayOrder: {order}"));
1178 fields.push("DisplayOrderSet: true".to_string());
1179 }
1180 if let Some(help) = e.cmd.help.as_deref().or(e.cmd.help_long.as_deref()) {
1181 fields.push(format!("Short: {}", go_string(help)));
1182 }
1183 if let Some(long) = &e.cmd.help_long {
1184 fields.push(format!("Long: {}", go_string(long)));
1185 }
1186 if let Some(message) = &e.cmd.deprecated {
1187 fields.push(format!("Deprecated: {}", go_string(message)));
1188 }
1189 if let Some(at) = &e.cmd.deprecated_warn_at {
1190 fields.push(format!("DeprecatedWarnAt: {}", go_string(at)));
1191 }
1192 if let Some(at) = &e.cmd.deprecated_remove_at {
1193 fields.push(format!("DeprecatedRemoveAt: {}", go_string(at)));
1194 }
1195 if let Some(heading) = &e.cmd.subcommand_help_heading {
1196 fields.push(format!("SubcommandHelpHeading: {}", go_string(heading)));
1197 }
1198 if let Some(name) = &e.cmd.subcommand_value_name {
1199 fields.push(format!("SubcommandValueName: {}", go_string(name)));
1200 }
1201 if e.cmd.next_line_help {
1202 fields.push("NextLineHelp: true".to_string());
1203 }
1204 if e.cmd.flatten_help {
1205 fields.push("FlattenHelp: true".to_string());
1206 }
1207 if e.cmd.subcommand_required {
1208 fields.push("SubcommandRequired: true".to_string());
1209 }
1210 let visible: Vec<String> = e
1213 .cmd
1214 .aliases
1215 .iter()
1216 .filter(|a| !e.cmd.hidden_aliases.contains(a))
1217 .cloned()
1218 .collect();
1219 if !visible.is_empty() {
1220 fields.push(format!("VisibleAliases: {}", string_slice(&visible)));
1221 }
1222 if let Some(before) = &e.cmd.before_help {
1223 fields.push(format!("BeforeHelp: {}", go_string(before)));
1224 }
1225 if let Some(after) = &e.cmd.after_help {
1226 fields.push(format!("AfterHelp: {}", go_string(after)));
1227 }
1228 if let Some(before) = &e.cmd.before_help_long {
1232 fields.push(format!("BeforeLongHelp: {}", go_string(before)));
1233 }
1234 if let Some(after) = &e.cmd.after_help_long {
1235 fields.push(format!("AfterLongHelp: {}", go_string(after)));
1236 }
1237 if !e.cmd.examples.is_empty() {
1238 let items = e
1239 .cmd
1240 .examples
1241 .iter()
1242 .map(|x| {
1243 let mut parts = Vec::new();
1244 if let Some(header) = &x.header {
1245 parts.push(format!("Header: {}", go_string(header)));
1246 }
1247 parts.push(format!("Code: {}", go_string(&x.code)));
1248 if let Some(help) = &x.help {
1252 parts.push(format!("Help: {}", go_string(help)));
1253 }
1254 format!("{{{}}}", parts.join(", "))
1255 })
1256 .collect::<Vec<_>>()
1257 .join(", ");
1258 fields.push(format!("Examples: []argv.Example{{{items}}}"));
1259 }
1260 if !e.cmd.headings.is_empty() {
1264 let items = e
1265 .cmd
1266 .headings
1267 .iter()
1268 .map(|heading| {
1269 format!(
1270 "{{Title: {}, Help: {}}}",
1271 go_string(&heading.title),
1272 go_string(&heading.help)
1273 )
1274 })
1275 .collect::<Vec<_>>()
1276 .join(", ");
1277 fields.push(format!("Headings: []argv.Heading{{{items}}}"));
1278 }
1279 format!("{{{}}}", fields.join(", "))
1280}
1281
1282fn flag_help(flag: &SpecFlag, named: &Named) -> String {
1283 let mut fields = vec![format!("Key: {}", named.key)];
1284 if let Some(message) = &flag.deprecated {
1285 fields.push(format!("Deprecated: {}", go_string(message)));
1286 }
1287 if let Some(at) = &flag.deprecated_warn_at {
1288 fields.push(format!("DeprecatedWarnAt: {}", go_string(at)));
1289 }
1290 if let Some(at) = &flag.deprecated_remove_at {
1291 fields.push(format!("DeprecatedRemoveAt: {}", go_string(at)));
1292 }
1293 if flag.hide {
1294 fields.push("Hide: true".to_string());
1295 }
1296 if let Some(order) = flag.display_order {
1297 fields.push(format!("DisplayOrder: {order}"));
1298 fields.push("DisplayOrderSet: true".to_string());
1299 }
1300 for (name, hidden) in [
1301 ("HideDefaultValue", flag.hide_default_value),
1302 ("HideEnv", flag.hide_env),
1303 ("HideEnvValues", flag.hide_env_values),
1304 ("HidePossibleValues", flag.hide_possible_values),
1305 ("HideShortHelp", flag.hide_short_help),
1306 ("HideLongHelp", flag.hide_long_help),
1307 ] {
1308 if hidden {
1309 fields.push(format!("{name}: true"));
1310 }
1311 }
1312 if flag.required && flag.default.is_empty() {
1315 fields.push("Demanded: true".to_string());
1316 }
1317 if flag.var {
1318 fields.push("Repeatable: true".to_string());
1319 }
1320 if let Some(arg) = &flag.arg {
1321 if arg.name != flag.name {
1322 fields.push(format!("ValueName: {}", go_string(&arg.name)));
1323 }
1324 if arg.required && arg.default.is_empty() {
1328 fields.push("ValueDemanded: true".to_string());
1329 }
1330 if !arg.value_names.is_empty() {
1331 fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1332 }
1333 if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1334 fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1335 }
1336 }
1337 if let Some(help) = flag.help.as_deref().or(flag.help_first_line.as_deref()) {
1340 fields.push(format!("Short: {}", go_string(help)));
1341 }
1342 if let Some(long) = flag.help_long.as_deref().or(flag.help.as_deref()) {
1343 fields.push(format!("Long: {}", go_string(long)));
1344 }
1345 if let Some(heading) = &flag.help_heading {
1346 fields.push(format!("Heading: {}", go_string(heading)));
1347 }
1348 if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) {
1350 fields.push(format!(
1351 "Choices: {}",
1352 string_slice(&visible_choices(choices))
1353 ));
1354 }
1355 if let Some(env) = &flag.env {
1356 fields.push(format!("Env: {}", go_string(env)));
1357 }
1358 if !flag.env_fallback.is_empty() {
1359 fields.push(format!("EnvFallback: {}", string_slice(&flag.env_fallback)));
1360 }
1361 if !flag.deprecated_env.is_empty() {
1362 fields.push(format!(
1363 "DeprecatedEnv: {}",
1364 string_slice(&flag.deprecated_env)
1365 ));
1366 }
1367 let default = if !flag.default.is_empty() {
1368 &flag.default
1369 } else {
1370 flag.arg
1371 .as_ref()
1372 .map(|a| &a.default)
1373 .unwrap_or(&flag.default)
1374 };
1375 if !default.is_empty() {
1376 fields.push(format!("Default: {}", string_slice(default)));
1377 }
1378 format!("{{{}}}", fields.join(", "))
1379}
1380
1381fn arg_help(arg: &SpecArg, named: &Named) -> String {
1382 let mut fields = vec![format!("Key: {}", named.key)];
1383 if let Some(order) = arg.display_order {
1384 fields.push(format!("DisplayOrder: {order}"));
1385 fields.push("DisplayOrderSet: true".to_string());
1386 }
1387 if arg.hide {
1388 fields.push("Hide: true".to_string());
1389 }
1390 for (name, hidden) in [
1391 ("HideDefaultValue", arg.hide_default_value),
1392 ("HideEnv", arg.hide_env),
1393 ("HideEnvValues", arg.hide_env_values),
1394 ("HidePossibleValues", arg.hide_possible_values),
1395 ("HideShortHelp", arg.hide_short_help),
1396 ("HideLongHelp", arg.hide_long_help),
1397 ] {
1398 if hidden {
1399 fields.push(format!("{name}: true"));
1400 }
1401 }
1402 if arg.required && arg.default.is_empty() {
1403 fields.push("Demanded: true".to_string());
1404 }
1405 if !arg.value_names.is_empty() {
1406 fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1407 }
1408 if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1409 fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1410 }
1411 if let Some(help) = arg.help.as_deref().or(arg.help_first_line.as_deref()) {
1412 fields.push(format!("Short: {}", go_string(help)));
1413 }
1414 if let Some(long) = arg.help_long.as_deref().or(arg.help.as_deref()) {
1415 fields.push(format!("Long: {}", go_string(long)));
1416 }
1417 if let Some(heading) = &arg.help_heading {
1418 fields.push(format!("Heading: {}", go_string(heading)));
1419 }
1420 if let Some(choices) = &arg.choices {
1421 fields.push(format!(
1422 "Choices: {}",
1423 string_slice(&visible_choices(choices))
1424 ));
1425 }
1426 if let Some(env) = &arg.env {
1427 fields.push(format!("Env: {}", go_string(env)));
1428 }
1429 if !arg.env_fallback.is_empty() {
1430 fields.push(format!("EnvFallback: {}", string_slice(&arg.env_fallback)));
1431 }
1432 if !arg.deprecated_env.is_empty() {
1433 fields.push(format!(
1434 "DeprecatedEnv: {}",
1435 string_slice(&arg.deprecated_env)
1436 ));
1437 }
1438 if !arg.default.is_empty() {
1439 fields.push(format!("Default: {}", string_slice(&arg.default)));
1440 }
1441 format!("{{{}}}", fields.join(", "))
1442}
1443
1444enum Line {
1453 Field(String, String),
1455 Block(Vec<String>),
1457}
1458
1459fn render(out: &mut String, indent: &str, lines: &[Line]) {
1461 let mut run: Vec<(&String, &String)> = Vec::new();
1462
1463 fn flush(out: &mut String, indent: &str, run: &mut Vec<(&String, &String)>) {
1464 let width = run.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
1465 for (key, value) in run.iter() {
1466 let _ = writeln!(
1467 out,
1468 "{indent}{key}:{:width$} {value},",
1469 "",
1470 width = width - key.len()
1471 );
1472 }
1473 run.clear();
1474 }
1475
1476 for line in lines {
1477 match line {
1478 Line::Field(key, value) => run.push((key, value)),
1479 Line::Block(block) => {
1480 flush(out, indent, &mut run);
1481 for l in block {
1482 let _ = writeln!(out, "{indent}{l}");
1483 }
1484 }
1485 }
1486 }
1487 flush(out, indent, &mut run);
1488}
1489
1490struct Emitted {
1492 named: Named,
1493 cmd: SpecCommand,
1494 flags: Vec<(SpecFlag, Named)>,
1495 args: Vec<(SpecArg, Named)>,
1496 clause_flags: Vec<(SpecFlag, Named)>,
1497 clause_args: Vec<(SpecArg, Named)>,
1498 subcommands: Vec<usize>,
1500 root: bool,
1501}
1502
1503fn effective_unknown_flags(spec: &Spec, commands: &[Emitted], at: usize) -> UnknownFlags {
1510 let path = &commands[at].cmd.full_cmd;
1511 for depth in (0..=path.len()).rev() {
1512 let ancestor = commands
1513 .iter()
1514 .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]);
1515 if let Some(mode) = ancestor.and_then(|e| e.cmd.unknown_flags) {
1516 return mode;
1517 }
1518 }
1519 spec.unknown_flags.unwrap_or_default()
1520}
1521
1522fn flag_literal(flag: &SpecFlag, named: &Named) -> String {
1523 let mut fields = vec![
1524 format!("Key: {}", named.key),
1525 format!("Name: {}", go_string(&flag.name)),
1526 ];
1527 if !flag.long.is_empty() {
1528 let longs = flag
1529 .long
1530 .iter()
1531 .map(|l| go_string(l))
1532 .collect::<Vec<_>>()
1533 .join(", ");
1534 fields.push(format!("Longs: []string{{{longs}}}"));
1535 }
1536 if !flag.hidden_aliases.is_empty() {
1537 fields.push(format!(
1538 "HiddenLongs: {}",
1539 string_slice(&flag.hidden_aliases)
1540 ));
1541 }
1542 if !flag.short.is_empty() {
1543 let shorts = flag
1544 .short
1545 .iter()
1546 .map(|c| go_byte(*c))
1547 .collect::<Vec<_>>()
1548 .join(", ");
1549 fields.push(format!("Shorts: []byte{{{shorts}}}"));
1550 }
1551 if !flag.hidden_short_aliases.is_empty() {
1552 let shorts = flag
1553 .hidden_short_aliases
1554 .iter()
1555 .map(|c| go_byte(*c))
1556 .collect::<Vec<_>>()
1557 .join(", ");
1558 fields.push(format!("HiddenShorts: []byte{{{shorts}}}"));
1559 }
1560 if let Some(negate) = &flag.negate {
1561 fields.push(format!(
1564 "Negate: {}",
1565 go_string(negate.trim_start_matches('-'))
1566 ));
1567 }
1568 if flag.arg.is_some() {
1569 fields.push("TakesValue: true".to_string());
1570 }
1571 if flag.value_optional {
1572 fields.push("ValueOptional: true".to_string());
1573 }
1574 if flag.bool_value {
1575 fields.push("BoolValue: true".to_string());
1576 }
1577 let action = match flag.action {
1578 SpecFlagAction::Set => None,
1579 SpecFlagAction::Help => Some("argv.ActionHelp"),
1580 SpecFlagAction::HelpShort => Some("argv.ActionHelpShort"),
1581 SpecFlagAction::HelpLong => Some("argv.ActionHelpLong"),
1582 SpecFlagAction::HelpAll => Some("argv.ActionHelpAll"),
1583 SpecFlagAction::Version => Some("argv.ActionVersion"),
1584 };
1585 if let Some(action) = action {
1586 fields.push(format!("Action: {action}"));
1587 }
1588 if let Some(arg) = flag.arg.as_ref().filter(|a| a.var) {
1593 fields.push("Variadic: true".to_string());
1594 if let Some(max) = arg.var_max {
1595 fields.push(format!("VarMax: {}", clamp_var_max(max)));
1596 }
1597 }
1598 if flag.allow_hyphen_values() {
1599 fields.push("AllowHyphenValues: true".to_string());
1600 }
1601 if let Some(arg) = &flag.arg {
1602 if arg.allow_negative_numbers {
1603 fields.push("AllowNegativeNumbers: true".to_string());
1604 }
1605 if let Some(terminator) = &arg.value_terminator {
1606 fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1607 }
1608 if let Some(delimiter) = arg.delimiter {
1609 fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1610 }
1611 }
1612 if flag.require_equals {
1613 fields.push("RequireEquals: true".to_string());
1614 }
1615 if let Some(missing) = &flag.default_missing {
1616 fields.push(format!("DefaultMissing: {}", go_string(missing)));
1617 }
1618 if flag.global {
1619 fields.push("Global: true".to_string());
1620 }
1621 format!("{{{}}}", fields.join(", "))
1622}
1623
1624fn arg_literal(arg: &SpecArg, named: &Named) -> String {
1625 let mut fields = vec![
1626 format!("Key: {}", named.key),
1627 format!("Name: {}", go_string(&arg.name)),
1628 ];
1629 if let Some(sigil) = &arg.sigil {
1630 fields.push(format!("Sigil: {}", go_string(sigil)));
1631 }
1632 if arg.required {
1633 fields.push("Required: true".to_string());
1634 }
1635 if arg.var {
1636 fields.push("Var: true".to_string());
1637 if let Some(max) = arg.var_max {
1638 fields.push(format!("VarMax: {}", clamp_var_max(max)));
1639 }
1640 }
1641 if arg.allow_negative_numbers {
1642 fields.push("AllowNegativeNumbers: true".to_string());
1643 }
1644 if let Some(terminator) = &arg.value_terminator {
1645 fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1646 }
1647 if let Some(delimiter) = arg.delimiter {
1648 fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1649 }
1650 let double_dash = match arg.double_dash {
1651 SpecDoubleDashChoices::Required => Some("argv.DoubleDashRequired"),
1652 SpecDoubleDashChoices::Preserve => Some("argv.DoubleDashPreserve"),
1653 SpecDoubleDashChoices::Automatic => Some("argv.DoubleDashAutomatic"),
1654 _ => None,
1655 };
1656 if let Some(dd) = double_dash {
1657 fields.push(format!("DoubleDash: {dd}"));
1658 }
1659 format!("{{{}}}", fields.join(", "))
1660}
1661
1662fn clamp_var_max(max: usize) -> u32 {
1667 u32::try_from(max).unwrap_or(u32::MAX)
1668}
1669
1670const GO_KEYWORDS: &[&str] = &[
1675 "break",
1676 "case",
1677 "chan",
1678 "const",
1679 "continue",
1680 "default",
1681 "defer",
1682 "else",
1683 "fallthrough",
1684 "for",
1685 "func",
1686 "go",
1687 "goto",
1688 "if",
1689 "import",
1690 "interface",
1691 "map",
1692 "package",
1693 "range",
1694 "return",
1695 "select",
1696 "struct",
1697 "switch",
1698 "type",
1699 "var",
1700];
1701
1702const UNUSABLE_PACKAGE_NAMES: &[&str] = &["_", "init"];
1715
1716pub fn is_valid_package(name: &str) -> bool {
1721 !name.is_empty()
1722 && !GO_KEYWORDS.contains(&name)
1723 && !UNUSABLE_PACKAGE_NAMES.contains(&name)
1724 && !name.starts_with(|c: char| c.is_ascii_digit())
1725 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1726}
1727
1728fn field_name(name: &str) -> String {
1730 let ident = format!("{}", AsPascalCase(name));
1731 if ident.is_empty() || ident.starts_with(|c: char| c.is_ascii_digit()) {
1732 format!("X{ident}")
1733 } else {
1734 ident
1735 }
1736}
1737
1738fn package_ident(bin: &str) -> String {
1746 let lowered: String = bin
1747 .chars()
1748 .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
1749 .collect::<String>()
1750 .to_ascii_lowercase();
1751 if is_valid_package(&lowered) {
1752 lowered
1753 } else {
1754 format!("cli{lowered}")
1758 }
1759}
1760
1761fn go_string(s: &str) -> String {
1766 let mut out = String::with_capacity(s.len() + 2);
1767 out.push('"');
1768 for c in s.chars() {
1769 match c {
1770 '"' => out.push_str("\\\""),
1771 '\\' => out.push_str("\\\\"),
1772 '\n' => out.push_str("\\n"),
1773 '\r' => out.push_str("\\r"),
1774 '\t' => out.push_str("\\t"),
1775 c if (c as u32) < 0x20 || c as u32 == 0x7f => {
1776 let _ = write!(out, "\\x{:02x}", c as u32);
1777 }
1778 c => out.push(c),
1779 }
1780 }
1781 out.push('"');
1782 out
1783}
1784
1785fn go_byte(c: char) -> String {
1791 match c {
1792 '\'' => "'\\''".to_string(),
1793 '\\' => "'\\\\'".to_string(),
1794 c if c.is_ascii_graphic() => format!("'{c}'"),
1795 c => format!("0x{:02x}", (c as u32) & 0xff),
1796 }
1797}
1798
1799#[cfg(test)]
1800mod tests {
1801 use super::*;
1802
1803 fn entry_of(out: &str, name: &str) -> String {
1812 out.lines()
1813 .find(|l| l.contains(&format!("Name: \"{name}\", Flag: true")))
1814 .unwrap_or_default()
1815 .to_string()
1816 }
1817
1818 fn go(kdl: &str) -> String {
1819 let spec: Spec = kdl.parse().expect("the fixture spec should parse");
1820 generate(&spec, &GoOptions::default())
1821 }
1822
1823 #[test]
1824 fn a_whole_cli() {
1825 let out = go(r#"
1826name "ex"
1827bin "ex"
1828version "1.2.3"
1829long_version "1.2.3\ncommit abc123"
1830flag "-v --verbose" global=#true help="be loud"
1831flag "--color" negate="--no-color"
1832flag "-j --jobs <n>"
1833flag "--include <pattern>..." var_max=3
1834arg "<file>"
1835arg "[rest]..." var=#true
1836cmd "install" {
1837 alias "i"
1838 flag "-f --force"
1839 arg "<pkg>"
1840}
1841cmd "config" {
1842 cmd "ls" {
1843 flag "--no-header"
1844 }
1845}
1846"#);
1847 insta::assert_snapshot!(out);
1848 }
1849
1850 #[test]
1851 fn rich_choices_keep_acceptance_visibility_and_strictness_separate() {
1852 let out = go(r#"
1853name "ex"
1854bin "ex"
1855flag "--color <when>" {
1856 choices ignore_case=#true strict=#false {
1857 choice "always" {
1858 alias "yes"
1859 alias "on" hide=#true
1860 }
1861 choice "never" hide=#true
1862 }
1863}
1864"#);
1865 let entry = entry_of(&out, "color");
1866 assert!(
1867 entry.contains(r#"Choices: []string{"always", "yes"}"#),
1868 "{entry}"
1869 );
1870 assert!(
1871 entry.contains(r#"AcceptedChoices: []string{"always", "never", "yes", "on"}"#),
1872 "{entry}"
1873 );
1874 assert!(entry.contains("IgnoreCase: true"), "{entry}");
1875 assert!(entry.contains("AllowUnknownChoices: true"), "{entry}");
1876 }
1877
1878 #[test]
1880 fn unknown_flags_are_inherited_and_overridable() {
1881 let out = go(r#"
1882name "ex"
1883bin "ex"
1884unknown_flags "error"
1885cmd "strict" {
1886 cmd "deep" {}
1887}
1888cmd "exec" unknown_flags="value" {
1889 cmd "nested" {}
1890}
1891"#);
1892 insta::assert_snapshot!(out);
1893 }
1894
1895 #[test]
1898 fn colliding_names_get_distinct_identifiers() {
1899 let out = go(r#"
1900name "ex"
1901bin "ex"
1902cmd "macos-defaults" {
1903 flag "--apply"
1904}
1905cmd "macos" {
1906 cmd "defaults" {
1907 flag "--apply"
1908 }
1909}
1910"#);
1911 insta::assert_snapshot!(out);
1912 }
1913
1914 #[test]
1915 fn default_flag_routing_reaches_generated_go_tables() {
1916 let out = go(
1917 "name ex\nbin ex\ndefault_subcommand run\ndefault_subcommand_flags #true\ncmd run {}\n",
1918 );
1919 assert!(out.contains("DefaultSubcommandFlags: true"), "{out}");
1920 }
1921
1922 #[test]
1923 fn a_default_subcommand_points_into_the_tree() {
1924 let out = go(r#"
1925name "ex"
1926bin "ex"
1927default_subcommand "run"
1928arg "[task]"
1929cmd "run" {
1930 arg "[args]..." var=#true
1931}
1932"#);
1933 insta::assert_snapshot!(out);
1934 }
1935
1936 #[test]
1937 fn a_bin_name_that_is_not_an_identifier_still_gives_a_package() {
1938 assert_eq!(package_ident("my-cli"), "mycli");
1939 assert_eq!(package_ident("7zip"), "cli7zip");
1940 assert_eq!(package_ident(""), "cli");
1941 assert_eq!(package_ident("go"), "cligo");
1943 assert_eq!(package_ident("type"), "clitype");
1944 assert_eq!(package_ident("_"), "cli_");
1947 assert_eq!(package_ident("init"), "cliinit");
1948 assert_eq!(package_ident("__"), "__");
1950 assert_eq!(package_ident("initialize"), "initialize");
1951
1952 for bin in [
1956 "my-cli", "7zip", "", "go", "type", "_", "init", "__", "MiSe",
1957 ] {
1958 let out = package_ident(bin);
1959 assert!(is_valid_package(&out), "{bin:?} sanitized to {out:?}");
1960 }
1961 }
1962
1963 #[test]
1965 fn a_name_matching_a_generated_suffix_still_gets_its_own() {
1966 let out = go(r#"
1967name "ex"
1968bin "ex"
1969cmd "macos-defaults" {}
1970cmd "macos" {
1971 cmd "defaults" {}
1972}
1973cmd "macos-defaults2" {}
1974"#);
1975 assert_declares_each_constant_once(&out);
1980 }
1981
1982 fn constant_names(out: &str) -> Vec<&str> {
1984 out.lines()
1985 .skip_while(|l| !l.starts_with("const ("))
1986 .skip(1)
1987 .take_while(|l| !l.starts_with(')'))
1988 .filter_map(|l| l.split_whitespace().next())
1989 .collect()
1990 }
1991
1992 fn assert_declares_each_constant_once(out: &str) {
1994 let names = constant_names(out);
1995 assert!(!names.is_empty(), "no constants at all:\n{out}");
1996 let mut seen = std::collections::HashSet::new();
1997 for name in &names {
1998 assert!(seen.insert(*name), "{name} is declared twice:\n{out}");
1999 }
2000 }
2001
2002 #[test]
2003 fn a_package_that_would_not_compile_is_refused_rather_than_emitted() {
2004 assert!(is_valid_package("mycli"));
2005 assert!(is_valid_package("mise_tables"));
2006 assert!(!is_valid_package("my-pkg"));
2007 assert!(!is_valid_package("7zip"));
2008 assert!(!is_valid_package(""));
2009 assert!(!is_valid_package("range"));
2010 assert!(!is_valid_package("_"));
2011 assert!(!is_valid_package("init"));
2012 assert!(is_valid_package("__"));
2013
2014 let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
2016 let out = generate(
2017 &spec,
2018 &GoOptions {
2019 package: Some("my-pkg".into()),
2020 },
2021 );
2022 assert!(out.contains("package mypkg"), "{out}");
2023 }
2024
2025 #[test]
2039 fn the_long_pages_text_reaches_the_tables() {
2040 let out = go(r#"
2041name "ex"
2042bin "ex"
2043about "Short."
2044about_long "Long."
2045before_long_help "ROOT-BEFORE"
2046after_long_help "ROOT-AFTER"
2047cmd "run" help="Run it" {
2048 before_long_help "RUN-BEFORE"
2049 after_long_help "RUN-AFTER"
2050}
2051"#);
2052 let meta = out
2053 .lines()
2054 .find(|l| l.contains("var HelpMeta"))
2055 .expect("a root header is emitted");
2056 assert!(
2057 meta.contains(r#"About: "Short.""#) && meta.contains(r#"LongAbout: "Long.""#),
2058 "the two abouts are separate fields: {meta}"
2059 );
2060 assert!(
2061 meta.contains(r#"BeforeLongHelp: "ROOT-BEFORE""#)
2062 && meta.contains(r#"AfterLongHelp: "ROOT-AFTER""#),
2063 "the root's long brackets are emitted: {meta}"
2064 );
2065
2066 let run = out
2067 .lines()
2068 .find(|l| l.contains("Short: \"Run it\""))
2069 .expect("the command has a help entry");
2070 assert!(
2071 run.contains(r#"BeforeLongHelp: "RUN-BEFORE""#)
2072 && run.contains(r#"AfterLongHelp: "RUN-AFTER""#),
2073 "a command's long brackets are emitted: {run}"
2074 );
2075 }
2076
2077 #[test]
2085 fn an_examples_help_line_reaches_the_tables() {
2086 let out = go(r#"
2087name "ex"
2088bin "ex"
2089cmd "run" help="Run it" {
2090 example "ex run --fast" header="Speed" help="When you are in a hurry"
2091 example "ex run"
2092}
2093"#);
2094 let run = out
2095 .lines()
2096 .find(|l| l.contains("Examples: []argv.Example"))
2097 .expect("the command's examples are emitted");
2098 assert!(
2099 run.contains(
2100 r#"{Header: "Speed", Code: "ex run --fast", Help: "When you are in a hurry"}"#
2101 ),
2102 "all three fields are emitted: {run}"
2103 );
2104 assert!(
2106 run.contains(r#"{Code: "ex run"}"#),
2107 "an example with no header emits no header: {run}"
2108 );
2109 }
2110
2111 #[test]
2117 fn heading_prose_reaches_the_tables() {
2118 let out = go(r#"
2119name "ex"
2120bin "ex"
2121cmd "run" help="Run it" {
2122 heading "Filters" help="Filters accumulate from left to right."
2123 flag "--allow <NAME>" help="Allow it" help_heading="Filters"
2124}
2125"#);
2126 let run = out
2127 .lines()
2128 .find(|l| l.contains("Headings: []argv.Heading"))
2129 .expect("the command's headings are emitted");
2130 assert!(
2131 run.contains(r#"{Title: "Filters", Help: "Filters accumulate from left to right."}"#),
2132 "both fields are emitted: {run}"
2133 );
2134 }
2135
2136 #[test]
2139 fn a_long_about_alone_does_not_become_the_short_one() {
2140 let out = go(r#"
2141name "ex"
2142bin "ex"
2143about_long "Long only."
2144"#);
2145 let meta = out
2146 .lines()
2147 .find(|l| l.contains("var HelpMeta"))
2148 .expect("a root header is emitted");
2149 assert!(
2150 !meta.contains(", About: ") && meta.contains(r#"LongAbout: "Long only.""#),
2151 "only the long one is set: {meta}"
2152 );
2153 }
2154
2155 #[test]
2156 fn a_default_subcommand_ignores_a_deeper_command_of_the_same_name() {
2157 let out = go(r#"
2158name "ex"
2159bin "ex"
2160default_subcommand "run"
2161cmd "oci" {
2162 cmd "run" {}
2163}
2164cmd "run" {
2165 arg "[args]..." var=#true
2166}
2167"#);
2168 assert!(
2169 out.contains("DefaultSubcommand: cmdRun,"),
2170 "should point at the root's own `run`, got:\n{out}"
2171 );
2172 }
2173
2174 #[test]
2175 fn command_builtin_controls_reach_generated_go_tables() {
2176 let out = go(r#"
2177name "ex"
2178bin "ex"
2179disable_help_flag #true
2180disable_help_subcommand #true
2181disable_version_flag #true
2182"#);
2183 let root = out
2184 .split("var Root = &argv.Command{")
2185 .nth(1)
2186 .expect("the root command should be emitted")
2187 .split("}\n")
2188 .next()
2189 .unwrap();
2190 assert!(root.contains("DisableHelpFlag:"), "{root}");
2191 assert!(root.contains("DisableHelpSubcommand:"), "{root}");
2192 assert!(root.contains("DisableVersionFlag:"), "{root}");
2193 }
2194
2195 #[test]
2198 fn a_default_subcommand_prefers_a_name_to_another_commands_alias() {
2199 let ordered = |first: &str, second: &str| {
2200 go(&format!(
2201 r#"
2202name "ex"
2203bin "ex"
2204default_subcommand "run"
2205{first}
2206{second}
2207"#
2208 ))
2209 };
2210 let alpha = "cmd \"alpha\" {\n alias \"run\"\n}";
2211 let run = "cmd \"run\" {\n arg \"[args]...\" var=#true\n}";
2212 for out in [ordered(alpha, run), ordered(run, alpha)] {
2213 assert!(
2214 out.contains("DefaultSubcommand: cmdRun,"),
2215 "should point at the command named `run`, got:\n{out}"
2216 );
2217 }
2218 }
2219
2220 #[test]
2221 fn an_external_subcommand_is_emitted_on_the_command_that_declares_it() {
2222 let out = go(r#"
2223name "ex"
2224bin "ex"
2225external_subcommand #true
2226cmd "install"
2227cmd "exec" external_subcommand=#true
2228"#);
2229 let block = |var: &str| {
2230 let start = out
2231 .find(&format!("var {var} ="))
2232 .unwrap_or_else(|| panic!("{var} should be emitted, got:\n{out}"));
2233 let rest = &out[start..];
2234 let end = rest[1..]
2235 .find("\nvar ")
2236 .map(|i| i + 1)
2237 .unwrap_or(rest.len());
2238 &rest[..end]
2239 };
2240 assert!(
2241 block("Root").contains("ExternalSubcommand: true"),
2242 "the root should forward unmatched words:\n{}",
2243 block("Root")
2244 );
2245 assert!(
2246 block("cmdExec").contains("ExternalSubcommand: true"),
2247 "a nested command can forward too:\n{}",
2248 block("cmdExec")
2249 );
2250 assert!(
2251 !block("cmdInstall").contains("ExternalSubcommand"),
2252 "a command that does not declare it should not carry it:\n{}",
2253 block("cmdInstall")
2254 );
2255 }
2256
2257 #[test]
2258 fn arg_required_else_help_reaches_the_table_and_typed_front_door() {
2259 let out = go(r#"
2260name "ex"
2261bin "ex"
2262cmd "run" arg_required_else_help=#true {
2263 flag "--all"
2264}
2265"#);
2266 assert!(
2267 out.contains("ArgRequiredElseHelp: true"),
2268 "the command table should carry the policy:\n{out}"
2269 );
2270 assert!(
2271 out.contains("p.Command().ArgRequiredElseHelp && p.CommandStart() == len(args)"),
2272 "the typed parser should enforce it before fallbacks:\n{out}"
2273 );
2274 }
2275
2276 #[test]
2277 fn subcommand_negates_requirements_reaches_generated_go() {
2278 let out = go(
2279 "name \"ex\"\nbin \"ex\"\nsubcommand_negates_reqs #true\nflag \"--config\" required=#true\ncmd \"run\"\n",
2280 );
2281 assert!(out.contains("SubcommandNegatesReqs: true"), "{out}");
2282 assert!(
2283 out.contains("checkRequirements := i == len(chain)-1 || !cmd.SubcommandNegatesReqs"),
2284 "{out}"
2285 );
2286 assert!(
2287 out.contains("CheckRelationshipsWithValuesAndRequirements"),
2288 "{out}"
2289 );
2290 }
2291
2292 #[test]
2293 fn argument_subcommand_conflicts_reach_generated_go() {
2294 let out = go(
2295 "name \"ex\"\nbin \"ex\"\nargs_conflicts_with_subcommands #true\nflag \"--verbose\"\ncmd \"run\"\n",
2296 );
2297 assert!(out.contains("ArgsConflictWithSubcommands: true"), "{out}");
2298 }
2299
2300 #[test]
2301 fn allow_missing_positional_reaches_generated_go() {
2302 let out = go(
2303 "name \"ex\"\nbin \"ex\"\nallow_missing_positional #true\narg \"[optional]\"\narg \"<required>\"\n",
2304 );
2305 assert!(out.contains("AllowMissingPositional: true"), "{out}");
2306 assert!(out.contains("Name: \"optional\""), "{out}");
2307 assert!(out.contains("Name: \"required\", Required: true"), "{out}");
2308 }
2309
2310 #[test]
2311 fn optional_flag_values_reach_generated_go() {
2312 let out = go("name \"ex\"\nbin \"ex\"\nflag \"--color [WHEN]\" value_optional=#true\n");
2313 assert!(
2314 out.contains("TakesValue: true, ValueOptional: true"),
2315 "{out}"
2316 );
2317 }
2318
2319 #[test]
2320 fn explicit_boolean_values_reach_generated_go() {
2321 let out = go(
2322 "name \"ex\"\nbin \"ex\"\nflag \"--color\" negate=\"--no-color\" bool_value=#true\n",
2323 );
2324 assert!(out.contains("BoolValue: true"), "{out}");
2325 assert!(out.contains("if ev.Flag.BoolValue"), "{out}");
2326 assert!(
2327 out.contains("given[ev.Flag.Key] = []string{ev.Value}"),
2328 "{out}"
2329 );
2330 assert!(
2331 out.contains("(ev.Value == \"true\") != ev.Negated"),
2332 "{out}"
2333 );
2334 }
2335
2336 #[test]
2337 fn flag_actions_reach_generated_go() {
2338 let out = go(
2339 "name \"ex\"\nbin \"ex\"\nflag \"--help-all\" action=\"help_all\"\nflag \"--version\" action=\"version\"\n",
2340 );
2341 assert!(out.contains("Action: argv.ActionHelpAll"), "{out}");
2342 assert!(out.contains("Action: argv.ActionVersion"), "{out}");
2343 }
2344
2345 #[test]
2346 fn granular_help_hides_reach_generated_go() {
2347 let out = go(
2348 "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",
2349 );
2350 for field in [
2351 "HideDefaultValue: true",
2352 "HideEnv: true",
2353 "HideEnvValues: true",
2354 "HidePossibleValues: true",
2355 "HideShortHelp: true",
2356 "HideLongHelp: true",
2357 ] {
2358 assert!(out.contains(field), "missing {field}:\n{out}");
2359 }
2360 }
2361
2362 #[test]
2363 fn strict_duplicate_policy_reaches_metadata() {
2364 let permissive = go("name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\n");
2365 assert!(!permissive.contains("RejectDuplicate"), "{permissive}");
2366
2367 let strict =
2368 go("name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\n");
2369 assert!(strict.contains("RejectDuplicate: true"), "{strict}");
2370 }
2371
2372 #[test]
2373 fn clause_args_and_later_entries_reach_go_cold_tables() {
2374 let out = go(r#"
2375name "ex"
2376bin "ex"
2377cmd "run" {
2378 flag "--needs-task" requires="task"
2379 clause "items" separator=":::" {
2380 flag "--postinstall <command>"
2381 arg "<task>" help="Task to run"
2382 }
2383}
2384cmd "later" {
2385 flag "--mode <mode>" help="Later flag"
2386}
2387"#);
2388 let meta = out
2389 .split_once("var Meta = argv.Metadata{")
2390 .and_then(|(_, rest)| rest.split_once("var HelpText = argv.HelpTable{"))
2391 .map(|(meta, _)| meta)
2392 .expect("generated metadata and help tables");
2393 let help = out
2394 .split_once("var HelpText = argv.HelpTable{")
2395 .and_then(|(_, rest)| rest.split_once("var HelpMeta = argv.HelpSpec{"))
2396 .map(|(help, _)| help)
2397 .expect("generated help table and metadata");
2398
2399 assert!(
2400 meta.contains("Key: ArgRunItemsTask, Name: \"task\""),
2401 "{out}"
2402 );
2403 assert!(
2404 meta.lines().any(|line| {
2405 line.contains("Key: FlagRunItemsPostinstall")
2406 && line.contains("RejectDuplicate: true")
2407 }),
2408 "{out}"
2409 );
2410 assert!(
2411 meta.lines().any(|line| {
2412 line.contains("Key: FlagRunNeedsTask")
2413 && line.contains("Requires: []uint64{ArgRunItemsTask}")
2414 }),
2415 "{out}"
2416 );
2417 assert!(meta.contains("Key: FlagLaterMode, Name: \"mode\""), "{out}");
2418 assert!(
2419 help.contains("Key: ArgRunItemsTask, Demanded: true"),
2420 "{out}"
2421 );
2422 assert!(
2423 help.lines().any(|line| {
2424 line.contains("Key: FlagLaterMode") && line.contains("Short: \"Later flag\"")
2425 }),
2426 "{out}"
2427 );
2428 for generated in [
2429 "type RunCmdItemsClause struct {",
2430 "Postinstall string",
2431 "Flags: []*argv.Flag{",
2432 "clauseFlag := map[uint64]bool{FlagRunItemsPostinstall: true}",
2433 "case argv.KindClauseSeparator:",
2434 "for instanceIndex, instance := range clauseInstances[CmdRun] {",
2435 "if err := argv.Check(Meta.Lookup(key), values, instanceOccurrences[key]); err != nil {",
2436 "clauseSources := map[uint64]argv.Source{FlagRunItemsPostinstall: argv.Unset, ArgRunItemsTask: argv.Unset}",
2437 "cmdRunV.Items = append(cmdRunV.Items, item)",
2438 ] {
2439 assert!(out.contains(generated), "missing {generated:?}:\n{out}");
2440 }
2441 assert!(
2442 out.lines().any(|line| {
2443 line.contains("Items")
2444 && line.contains("[]RunCmdItemsClause")
2445 && line.contains("// clause items")
2446 }),
2447 "{out}"
2448 );
2449 }
2450
2451 #[test]
2452 fn strict_negated_flags_track_each_spelling_separately() {
2453 let out = go(
2454 "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n",
2455 );
2456 assert!(out.contains("polaritySeen := map[uint64]uint8{}"), "{out}");
2457 assert!(
2458 out.contains("polaritySeen[ev.Flag.Key]&polarity != 0"),
2459 "{out}"
2460 );
2461 assert!(out.contains("if duplicateSeen[key]"), "{out}");
2462 }
2463
2464 #[test]
2465 fn strict_global_duplicate_tracking_resets_at_subcommands() {
2466 let out = go(
2467 "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\" global=#true\ncmd \"run\" {\n args_override_self #false\n}\n",
2468 );
2469 assert!(
2470 out.contains("levelSeen = map[uint64]int{}"),
2471 "a subcommand should start a new duplicate scope:\n{out}"
2472 );
2473 assert!(out.contains("strictSeen[ev.Flag.Key] = true"), "{out}");
2474 assert!(out.contains("if strictSeen[key]"), "{out}");
2475 }
2476
2477 #[test]
2479 fn a_subcommand_named_root_does_not_collide_with_the_root() {
2480 let out = go(r#"
2481name "ex"
2482bin "ex"
2483cmd "root" {
2484 flag "--wat"
2485}
2486"#);
2487 let declared = |name: &str| {
2490 out.lines()
2491 .filter(|l| l.split_whitespace().next() == Some(name))
2492 .count()
2493 };
2494 assert_eq!(declared("CmdRoot"), 1, "CmdRoot declared twice:\n{out}");
2495 assert_eq!(declared("CmdRoot2"), 1, "no distinct key for it:\n{out}");
2496 assert_declares_each_constant_once(&out);
2497 }
2498
2499 #[test]
2504 fn only_the_per_occurrence_bound_reaches_the_table() {
2505 let out = go(r#"
2506name "ex"
2507bin "ex"
2508flag "--include <pattern>..." {
2509 arg "<pattern>..." var=#true var_min=2 var_max=2
2510}
2511flag "--tag <t>" var=#true var_max=1
2512"#);
2513 assert!(
2514 out.contains("Name: \"include\", Longs: []string{\"include\"}, TakesValue: true, Variadic: true, VarMax: 2"),
2515 "{out}"
2516 );
2517 assert!(
2518 out.contains("Name: \"include\", Flag: true") && out.contains("VarMin: 2"),
2519 "the nested value minimum must reach post-binding metadata:\n{out}"
2520 );
2521 let tag = out.lines().find(|l| l.contains("\"tag\"")).unwrap();
2522 assert!(!tag.contains("VarMax"), "occurrence bound leaked: {tag}");
2523 }
2524
2525 #[test]
2526 fn exact_arity_with_one_label_reaches_go_help() {
2527 let out = go(r#"
2528name "ex"
2529bin "ex"
2530flag "--pair <ITEM>..." {
2531 arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2532 value_names "ITEM"
2533 }
2534 }
2535arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2536 value_names "ITEM"
2537}
2538"#);
2539 assert_eq!(out.matches("ValueArity: 2").count(), 2, "{out}");
2540 assert_eq!(
2541 out.matches("ValueNames: []string{\"ITEM\"}").count(),
2542 2,
2543 "{out}"
2544 );
2545 }
2546
2547 #[test]
2548 fn scoped_flag_binding_preserves_counts_and_boolean_polarity() {
2549 let out = go(r#"
2550name "ex"
2551bin "ex"
2552clause "tools" {
2553 flag "--required" required=#true
2554 flag "-v --verbose" count=#true
2555 flag "--color" negate="--no-color" bool_value=#true
2556 arg "<tool>"
2557}
2558"#);
2559
2560 for generated in [
2561 "if clauseFlag[f.Key] {",
2562 "instanceOccurrences := clauseInstanceOccurrences[CmdRoot][instanceIndex]",
2563 "argv.Check(Meta.Lookup(key), values, instanceOccurrences[key])",
2564 "item.Required = !instanceNegated[FlagToolsRequired]",
2565 "item.Verbose = instanceOccurrences[FlagToolsVerbose]",
2566 "item.Color = (values[len(values)-1] == \"true\") != instanceNegated[FlagToolsColor]",
2567 "item.Color = !instanceNegated[FlagToolsColor]",
2568 ] {
2569 assert!(out.contains(generated), "missing {generated:?}:\n{out}");
2570 }
2571 }
2572
2573 #[test]
2574 fn a_bare_optional_scoped_flag_does_not_index_an_empty_value() {
2575 let out = go(r#"
2576name "ex"
2577bin "ex"
2578clause "tools" {
2579 flag "--label [LABEL]" value_optional=#true
2580 arg "<tool>"
2581}
2582"#);
2583
2584 assert!(
2585 out.contains(
2586 "if values := instance[FlagToolsLabel]; len(values) > 0 {\n\t\t\t\titem.Label = values[len(values)-1]"
2587 ),
2588 "{out}"
2589 );
2590 assert!(
2591 !out.contains("if values, ok := instance[FlagToolsLabel]; ok"),
2592 "{out}"
2593 );
2594 }
2595
2596 #[test]
2597 fn allow_hyphen_values_reaches_the_table() {
2598 let out = go(r#"
2599name "ex"
2600bin "ex"
2601flag "--args <ARGS>" allow_hyphen_values=#true
2602"#);
2603 assert!(
2604 out.contains("Name: \"args\", Longs: []string{\"args\"}, TakesValue: true, AllowHyphenValues: true"),
2605 "{out}"
2606 );
2607 }
2608
2609 #[test]
2610 fn require_equals_reaches_the_table() {
2611 let out = go(r#"
2612name "ex"
2613bin "ex"
2614flag "--inspect <PORT>" require_equals=#true
2615"#);
2616 assert!(
2617 out.contains("Name: \"inspect\", Longs: []string{\"inspect\"}, TakesValue: true, RequireEquals: true"),
2618 "{out}"
2619 );
2620 }
2621
2622 #[test]
2623 fn default_missing_reaches_the_table() {
2624 let out = go(r#"
2625name "ex"
2626bin "ex"
2627flag "--color <WHEN>" default_missing="always"
2628"#);
2629 assert!(
2630 out.contains(
2631 "Name: \"color\", Longs: []string{\"color\"}, TakesValue: true, DefaultMissing: \"always\""
2632 ),
2633 "{out}"
2634 );
2635 }
2636
2637 #[test]
2643 fn a_relationship_resolves_through_scope_and_negation() {
2644 let out = go(r#"
2645name "ex"
2646bin "ex"
2647flag "--quiet" global=#true
2648flag "--color" negate="--no-color"
2649flag "--plain" conflicts="--no-color"
2650cmd "run" {
2651 flag "--loud" conflicts="--quiet"
2652 flag "--solo" conflicts="--plain"
2653}
2654"#);
2655 assert!(out.contains("Conflicts: []uint64{FlagColor}"), "{out}");
2657 assert!(out.contains("Conflicts: []uint64{FlagQuiet}"), "{out}");
2659 assert!(
2662 !entry_of(&out, "solo").contains("Conflicts"),
2663 "a non-global should not resolve from below:\n{out}"
2664 );
2665 }
2666
2667 #[test]
2668 fn positional_conflicts_reach_go_metadata_in_both_directions() {
2669 let out = go(r#"
2670name "ex"
2671bin "ex"
2672flag "--from-file <file>" conflicts="value"
2673arg "[value]" conflicts="--from-file"
2674"#);
2675
2676 assert!(
2677 entry_of(&out, "from-file").contains("Conflicts: []uint64{ArgValue}"),
2678 "{out}"
2679 );
2680 assert!(
2681 out.lines().any(|line| {
2682 line.contains("{Key: ArgValue, Name: \"value\"")
2683 && line.contains("Conflicts: []uint64{FlagFromFile}")
2684 }),
2685 "{out}"
2686 );
2687 }
2688
2689 #[test]
2690 fn a_value_conditional_requirement_reaches_go_metadata() {
2691 let out = go(r#"
2692name "ex"
2693bin "ex"
2694flag "--format <format>" {
2695 requires_if "json" "--schema"
2696}
2697flag "--schema <file>"
2698"#);
2699 assert!(
2700 entry_of(&out, "format").contains(
2701 "RequiresIf: []argv.ValueRequirement{{Value: \"json\", Key: FlagSchema}}"
2702 ),
2703 "{out}"
2704 );
2705 assert!(
2706 out.contains("argv.CheckRelationshipsWithValues"),
2707 "the emitted parser must enforce the metadata:\n{out}"
2708 );
2709 }
2710
2711 #[test]
2712 fn required_if_eq_makes_generated_go_supply_values() {
2713 let out = go(r#"
2714name "ex"
2715bin "ex"
2716flag "--token <token>" {
2717 required_if_eq "--mode" "remote"
2718}
2719flag "--mode <mode>"
2720"#);
2721 assert!(
2722 entry_of(&out, "token").contains(
2723 "RequiredIfEq: []argv.ValueCondition{{Key: FlagMode, Value: \"remote\"}}"
2724 ),
2725 "{out}"
2726 );
2727 assert!(out.contains("resolved := map[uint64][]string{}"), "{out}");
2728 assert!(out.contains("argv.CheckRelationshipsWithValues"), "{out}");
2729 }
2730
2731 #[test]
2732 fn boolean_sources_are_normalized_for_value_relationships() {
2733 let out = go(r#"
2734name "ex"
2735bin "ex"
2736flag "--token <token>" {
2737 required_if_eq "--mode" "true"
2738}
2739flag "--mode" negate="--no-mode" bool_value=#true
2740"#);
2741 assert!(
2742 entry_of(&out, "mode").contains("RequiresIfBoolean: true"),
2743 "{out}"
2744 );
2745 }
2746
2747 #[test]
2748 fn a_conditional_default_reaches_go_metadata() {
2749 let out = go(r#"
2750name "ex"
2751bin "ex"
2752flag "--bin-names" {
2753 default_if "--json" "true"
2754 default_if "--output" "json" "pretty"
2755}
2756flag "--json"
2757flag "--output <fmt>"
2758"#);
2759 assert!(
2760 entry_of(&out, "bin-names")
2761 .contains("DefaultIf: []argv.DefaultIf{{Key: FlagJson, Value: \"true\"}"),
2762 "{out}"
2763 );
2764 assert!(
2765 entry_of(&out, "bin-names").contains("When: \"json\""),
2766 "{out}"
2767 );
2768 assert!(
2769 out.contains("argv.ApplyDefaultIf"),
2770 "the emitted parser must apply the metadata:\n{out}"
2771 );
2772 assert!(
2773 out.contains("negated[ev.Flag.Key] = ev.Negated"),
2774 "Equals default_if needs the negate form:\n{out}"
2775 );
2776 }
2777
2778 #[test]
2782 fn a_relationship_needs_the_right_form() {
2783 let out = go(r#"
2784name "ex"
2785bin "ex"
2786flag "-q --quiet"
2787flag "--color"
2788flag "--a" conflicts="--q"
2789flag "--b" conflicts="-color"
2790flag "--c" conflicts="-q"
2791flag "--d" conflicts="--color"
2792"#);
2793 assert!(!entry_of(&out, "a").contains("Conflicts"), "{out}");
2795 assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
2796 assert!(
2798 entry_of(&out, "c").contains("Conflicts: []uint64{FlagQuiet}"),
2799 "{out}"
2800 );
2801 assert!(
2802 entry_of(&out, "d").contains("Conflicts: []uint64{FlagColor}"),
2803 "{out}"
2804 );
2805 }
2806
2807 #[test]
2814 fn an_ordinary_form_beats_another_flags_negation() {
2815 let out = go(r#"
2816name "ex"
2817bin "ex"
2818flag "--a" negate="--zap"
2819flag "--zap"
2820flag "--p" conflicts="--zap"
2821"#);
2822 assert!(
2823 entry_of(&out, "p").contains("Conflicts: []uint64{FlagZap}"),
2824 "should name the flag `--zap` binds, not the one negating to it:\n{out}"
2825 );
2826 }
2827
2828 #[test]
2830 fn a_single_dash_negation_is_named_by_its_own_form() {
2831 let out = go(r#"
2832name "ex"
2833bin "ex"
2834flag "--tint" negate="-no-tint"
2835flag "--plain" conflicts="-no-tint"
2836flag "--other" conflicts="--no-tint"
2837"#);
2838 assert!(
2839 entry_of(&out, "plain").contains("Conflicts: []uint64{FlagTint}"),
2840 "the exact form should resolve:\n{out}"
2841 );
2842 assert!(
2844 !entry_of(&out, "other").contains("Conflicts"),
2845 "`--no-tint` is not how it was declared:\n{out}"
2846 );
2847 }
2848
2849 #[test]
2851 fn a_negation_is_matched_as_written() {
2852 let out = go(r#"
2853name "ex"
2854bin "ex"
2855flag "--color" negate="--no-color"
2856flag "--tint" negate="-no-tint"
2857flag "--a" conflicts="--no-color"
2858flag "--b" conflicts="--no-tint"
2859"#);
2860 assert!(
2861 entry_of(&out, "a").contains("Conflicts: []uint64{FlagColor}"),
2862 "{out}"
2863 );
2864 assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
2867 }
2868
2869 #[test]
2870 fn strings_are_escaped_to_go_rules() {
2871 assert_eq!(go_string(r#"a"b\c"#), r#""a\"b\\c""#);
2872 assert_eq!(go_string("tab\there"), r#""tab\there""#);
2873 assert_eq!(go_string("\u{7f}"), r#""\x7f""#);
2875 }
2876}