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, SpecComplete, SpecDoubleDashChoices, SpecFlag,
46 SpecFlagAction,
47};
48
49#[derive(Debug, Clone, Default)]
51pub struct GoOptions {
52 pub package: Option<String>,
59}
60
61pub fn generate(spec: &Spec, opts: &GoOptions) -> String {
68 Emitter::new(spec, opts).run()
69}
70
71fn has_mount(cmd: &SpecCommand) -> bool {
72 !cmd.mounts.is_empty() || cmd.subcommands.values().any(has_mount)
73}
74
75pub fn generate_with_types(
80 spec: &Spec,
81 opts: &GoOptions,
82 field_types: &BTreeMap<String, ValueType>,
83) -> Result<String, String> {
84 if spec.unknown_env.is_some() && has_mount(&spec.cmd) {
88 return Err(
89 "unknown_env cannot be combined with mount in the Go generator: the variables a \
90 mounted command declares are not known at generation time"
91 .to_string(),
92 );
93 }
94 if field_types.is_empty() {
95 return Ok(generate(spec, opts));
96 }
97 let mut probe = Emitter::new(spec, opts);
98 let mut commands = Vec::new();
99 probe.collect(&spec.cmd, &[], true, &mut commands);
100 types::validate(&commands, field_types)?;
101 let mut emitter = Emitter::new(spec, opts);
102 emitter.field_types = field_types.clone();
103 Ok(emitter.run())
104}
105
106struct Named {
109 key: String,
110 var: String,
111 number: u64,
112}
113
114struct Emitter<'a> {
115 spec: &'a Spec,
116 field_types: BTreeMap<String, ValueType>,
117 package: String,
118 taken: HashMap<String, u32>,
121 next_key: u64,
123 out: String,
124}
125
126impl<'a> Emitter<'a> {
127 fn new(spec: &'a Spec, opts: &GoOptions) -> Self {
129 let package = match opts.package.as_deref() {
133 Some(name) if is_valid_package(name) => name.to_string(),
134 Some(name) => package_ident(name),
135 None => package_ident(&spec.bin),
136 };
137 Emitter {
138 spec,
139 field_types: BTreeMap::new(),
140 package,
141 taken: HashMap::new(),
142 next_key: 0,
143 out: String::new(),
144 }
145 }
146
147 fn unique(&mut self, base: &str) -> String {
159 let mut n = self.taken.get(base).copied().unwrap_or(0);
160 loop {
161 n += 1;
162 let candidate = if n == 1 {
163 base.to_string()
164 } else {
165 format!("{base}{n}")
166 };
167 if !self.taken.contains_key(&candidate) {
168 self.taken.insert(base.to_string(), n);
169 self.taken.entry(candidate.clone()).or_insert(0);
172 return candidate;
173 }
174 }
175 }
176
177 fn name(&mut self, prefix: &str, path: &[&str], own: &str) -> Named {
178 let mut base = String::from(prefix);
179 for segment in path {
180 let _ = write!(base, "{}", AsPascalCase(segment));
181 }
182 let _ = write!(base, "{}", AsPascalCase(own));
183 let key = self.unique(&base);
184 self.next_key += 1;
185 Named {
186 var: format!("cmd{}", &key[prefix.len()..]),
187 key,
188 number: self.next_key,
189 }
190 }
191
192 fn run(mut self) -> String {
194 let mut commands = Vec::new();
198 self.collect(&self.spec.cmd.clone(), &[], true, &mut commands);
199
200 self.header();
201 self.constants(&commands);
202 self.tables(&commands);
203 self.metadata(&commands);
204 self.help_table(&commands);
205 self.unknown_env();
206 structs::emit(
207 &mut self.out,
208 &commands,
209 &self.field_types,
210 self.spec.unknown_env.is_some(),
211 );
212
213 let trimmed = self.out.trim_end().len();
217 self.out.truncate(trimmed);
218 self.out.push('\n');
219 self.out
220 }
221
222 fn unknown_env(&mut self) {
228 let Some(decl) = &self.spec.unknown_env else {
229 return;
230 };
231 let mut known: Vec<String> = crate::spec::unknown_env::declared_env_names(self.spec)
232 .into_iter()
233 .map(str::to_string)
234 .chain(decl.allow.iter().cloned())
235 .collect();
236 known.sort();
237 known.dedup();
238 let _ = writeln!(
239 self.out,
240 "// UnknownEnvPrefix scopes the spec's unknown_env check: a variable under it that\n\
241 // nothing declares fails the parse.\n\
242 const UnknownEnvPrefix = {}\n\n\
243 // UnknownEnvKnown is every variable the spec declares, anywhere in the command\n\
244 // tree or its config, and every one it allows.\n\
245 var UnknownEnvKnown = {}\n",
246 go_string(&decl.prefix),
247 string_slice(&known)
248 );
249 }
250
251 fn collect(&mut self, cmd: &SpecCommand, path: &[&str], root: bool, out: &mut Vec<Emitted>) {
254 let named = if root {
255 self.next_key += 1;
256 Named {
257 key: self.unique("CmdRoot"),
262 var: "Root".to_string(),
263 number: self.next_key,
264 }
265 } else {
266 self.name("Cmd", &path[..path.len() - 1], path[path.len() - 1])
267 };
268
269 let flags = cmd
270 .flags
271 .iter()
272 .map(|f| (f.clone(), self.name("Flag", path, &f.name)))
273 .collect::<Vec<_>>();
274 let args = cmd
275 .args
276 .iter()
277 .map(|a| (a.clone(), self.name("Arg", path, &a.name)))
278 .collect::<Vec<_>>();
279 let clause_args = cmd
280 .clause
281 .as_ref()
282 .map(|clause| {
283 clause
284 .args
285 .iter()
286 .map(|a| {
287 (
288 a.clone(),
289 self.name("Arg", path, &format!("{}-{}", clause.name, a.name)),
290 )
291 })
292 .collect::<Vec<_>>()
293 })
294 .unwrap_or_default();
295 let clause_flags = cmd
296 .clause
297 .as_ref()
298 .map(|clause| {
299 clause
300 .flags
301 .iter()
302 .map(|flag| {
303 (
304 flag.clone(),
305 self.name("Flag", path, &format!("{}-{}", clause.name, flag.name)),
306 )
307 })
308 .collect::<Vec<_>>()
309 })
310 .unwrap_or_default();
311
312 let index = out.len();
313 out.push(Emitted {
314 named,
315 cmd: cmd.clone(),
316 flags,
317 args,
318 clause_flags,
319 clause_args,
320 subcommands: Vec::new(),
321 root,
322 });
323
324 let mut children = Vec::new();
328 for (name, sub) in &cmd.subcommands {
329 if name != &sub.name {
333 continue;
334 }
335 let mut child_path = path.to_vec();
336 child_path.push(name);
337 let at = out.len();
338 self.collect(sub, &child_path, false, out);
339 children.push(at);
340 }
341 out[index].subcommands = children;
342 }
343
344 fn header(&mut self) {
346 let imports = if self
347 .field_types
348 .values()
349 .any(|ty| *ty == ValueType::Duration)
350 {
351 "import (\n\t\"time\"\n\n\t\"github.com/jdx/usage/go/argv\"\n)"
352 } else {
353 "import \"github.com/jdx/usage/go/argv\""
354 };
355 let _ = writeln!(
356 self.out,
357 "// Code generated by `usage generate go`. DO NOT EDIT.\n\
358 //\n\
359 // Binding tables for `{}`, read by\n\
360 // [github.com/jdx/usage/go/argv]. Regenerate rather than editing: the spec is\n\
361 // the definition, and a hand-edit here is a difference no reviewer can see.\n\
362 //\n\
363 // These are package-level variables holding plain data, so the linker lays them\n\
364 // out and nothing runs before main.\n\
365 \n\
366 package {}\n\
367 \n\
368 {imports}\n",
369 self.spec.bin, self.package
370 );
371
372 if let Some(version) = self
373 .spec
374 .version
375 .as_ref()
376 .or(self.spec.long_version.as_ref())
377 {
378 let _ = writeln!(
379 self.out,
380 "// Version is what the spec declares, so a caller answering `--version` has it\n\
381 // without the parse tables carrying a string binding never reads.\n\
382 const Version = {}\n",
383 go_string(version)
384 );
385 }
386 if let Some(version) = &self.spec.long_version {
387 let _ = writeln!(
388 self.out,
389 "// LongVersion is the extended text printed for `--version`; `-V` uses Version.\n\
390 const LongVersion = {}\n",
391 go_string(version)
392 );
393 }
394 }
395
396 fn constants(&mut self, commands: &[Emitted]) {
397 let _ = writeln!(
398 self.out,
399 "// Keys identify a table entry without a string comparison: switch on the Key an\n\
400 // event carries rather than on its Name, which is there for diagnostics.\n\
401 const ("
402 );
403 let mut entries: Vec<(&str, u64)> = Vec::new();
404 for e in commands {
405 entries.push((&e.named.key, e.named.number));
406 entries.extend(e.flags.iter().map(|(_, n)| (n.key.as_str(), n.number)));
407 entries.extend(
408 e.clause_flags
409 .iter()
410 .map(|(_, n)| (n.key.as_str(), n.number)),
411 );
412 entries.extend(e.args.iter().map(|(_, n)| (n.key.as_str(), n.number)));
413 entries.extend(
414 e.clause_args
415 .iter()
416 .map(|(_, n)| (n.key.as_str(), n.number)),
417 );
418 }
419 let width = entries.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
422 for (key, number) in entries {
423 let _ = writeln!(
424 self.out,
425 "\t{key}{:pad$} uint64 = {number}",
426 "",
427 pad = width - key.len()
428 );
429 }
430 let _ = writeln!(self.out, ")\n");
431 }
432
433 fn tables(&mut self, commands: &[Emitted]) {
434 let default_subcommand = self.spec.default_subcommand.as_ref().and_then(|name| {
441 let direct = || commands[0].subcommands.iter().map(|at| &commands[*at]);
442 direct()
446 .find(|e| &e.cmd.name == name)
447 .or_else(|| {
448 direct().find(|e| {
449 e.cmd.aliases.contains(name) || e.cmd.hidden_aliases.contains(name)
450 })
451 })
452 .map(|e| e.named.var.clone())
453 });
454
455 for (i, e) in commands.iter().enumerate() {
456 let doc = if e.root {
457 format!(
458 "// Root is the command tree for `{}`. Pass it to argv.New.",
459 self.spec.bin
460 )
461 } else {
462 format!("// {}", e.cmd.full_cmd.join(" "))
463 };
464 let mut lines = vec![
465 Line::Field("Name".into(), go_string(&e.cmd.name)),
466 Line::Field("Key".into(), e.named.key.clone()),
467 ];
468
469 let aliases: Vec<&String> = e
470 .cmd
471 .aliases
472 .iter()
473 .chain(e.cmd.hidden_aliases.iter())
474 .collect();
475 if !aliases.is_empty() {
476 let list = aliases
479 .iter()
480 .map(|a| go_string(a))
481 .collect::<Vec<_>>()
482 .join(", ");
483 lines.push(Line::Field("Aliases".into(), format!("[]string{{{list}}}")));
484 }
485
486 if !e.flags.is_empty() || !e.clause_flags.is_empty() {
487 let mut block = vec!["Flags: []*argv.Flag{".to_string()];
488 for (flag, named) in e.flags.iter().chain(&e.clause_flags) {
489 block.push(format!("\t{},", flag_literal(flag, named)));
490 }
491 block.push("},".to_string());
492 lines.push(Line::Block(block));
493 }
494
495 if !e.args.is_empty() {
496 let mut block = vec!["Args: []*argv.Arg{".to_string()];
497 for (arg, named) in &e.args {
498 block.push(format!("\t{},", arg_literal(arg, named)));
499 }
500 block.push("},".to_string());
501 lines.push(Line::Block(block));
502 }
503 if let Some(clause) = &e.cmd.clause {
504 let mut block = vec!["Clause: &argv.Clause{".to_string()];
505 block.push(format!("\tKey: {},", e.named.key));
506 block.push(format!("\tName: {},", go_string(&clause.name)));
507 block.push(format!(
508 "\tSeparator: {},",
509 go_string(clause.separator.as_deref().unwrap_or_default())
510 ));
511 block.push("\tFlags: []*argv.Flag{".to_string());
512 for (flag, named) in &e.clause_flags {
513 block.push(format!("\t\t{},", flag_literal(flag, named)));
514 }
515 block.push("\t},".to_string());
516 block.push("\tArgs: []*argv.Arg{".to_string());
517 for (arg, named) in &e.clause_args {
518 block.push(format!("\t\t{},", arg_literal(arg, named)));
519 }
520 block.push("\t},".to_string());
521 block.push("},".to_string());
522 lines.push(Line::Block(block));
523 }
524
525 if !e.subcommands.is_empty() {
526 let list = e
527 .subcommands
528 .iter()
529 .map(|at| commands[*at].named.var.clone())
530 .collect::<Vec<_>>()
531 .join(", ");
532 lines.push(Line::Field(
533 "Subcommands".into(),
534 format!("[]*argv.Command{{{list}}}"),
535 ));
536 }
537
538 if effective_unknown_flags(self.spec, commands, i) == UnknownFlags::Error {
541 lines.push(Line::Field(
542 "UnknownFlags".into(),
543 "argv.UnknownFlagsError".into(),
544 ));
545 }
546
547 if e.cmd.external_subcommand {
548 lines.push(Line::Field("ExternalSubcommand".into(), "true".into()));
549 }
550 if e.cmd.arg_required_else_help {
551 lines.push(Line::Field("ArgRequiredElseHelp".into(), "true".into()));
552 }
553 if e.cmd.disable_help_flag {
554 lines.push(Line::Field("DisableHelpFlag".into(), "true".into()));
555 }
556 if e.cmd.disable_help_subcommand {
557 lines.push(Line::Field("DisableHelpSubcommand".into(), "true".into()));
558 }
559 if e.cmd.disable_version_flag {
560 lines.push(Line::Field("DisableVersionFlag".into(), "true".into()));
561 }
562 if e.cmd.subcommand_negates_reqs {
563 lines.push(Line::Field("SubcommandNegatesReqs".into(), "true".into()));
564 }
565 if e.cmd.args_conflicts_with_subcommands {
566 lines.push(Line::Field(
567 "ArgsConflictWithSubcommands".into(),
568 "true".into(),
569 ));
570 }
571 if e.cmd.subcommand_precedence_over_arg {
572 lines.push(Line::Field(
573 "SubcommandPrecedenceOverArg".into(),
574 "true".into(),
575 ));
576 }
577 if e.cmd.allow_missing_positional {
578 lines.push(Line::Field("AllowMissingPositional".into(), "true".into()));
579 }
580 if e.cmd.dont_delimit_trailing_values {
581 lines.push(Line::Field(
582 "DontDelimitTrailingValues".into(),
583 "true".into(),
584 ));
585 }
586 if e.root {
587 if self.spec.default_subcommand_flags {
588 lines.push(Line::Field("DefaultSubcommandFlags".into(), "true".into()));
589 }
590 if self.spec.default_subcommand_on_empty {
591 lines.push(Line::Field(
592 "DefaultSubcommandOnEmpty".into(),
593 "true".into(),
594 ));
595 }
596 if let Some(var) = &default_subcommand {
597 lines.push(Line::Field("DefaultSubcommand".into(), var.clone()));
598 }
599 if self.spec.version.is_some() || self.spec.long_version.is_some() {
600 lines.push(Line::Field("Version".into(), "true".into()));
603 }
604 }
605
606 let _ = writeln!(self.out, "{doc}");
607 let _ = writeln!(self.out, "var {} = &argv.Command{{", e.named.var);
608 render(&mut self.out, "\t", &lines);
609 let _ = writeln!(self.out, "}}\n");
610 }
611 }
612}
613
614impl Emitter<'_> {
615 fn metadata(&mut self, commands: &[Emitted]) {
625 let mut by_key: BTreeMap<u64, String> = BTreeMap::new();
627 for e in commands {
628 for (flag, named) in e.flags.iter().chain(&e.clause_flags) {
629 by_key.insert(named.number, self.flag_meta(flag, named, e, commands));
630 }
631 for (arg, named) in &e.args {
632 by_key.insert(named.number, arg_meta(self.spec, arg, named, e, commands));
633 }
634 for (arg, named) in &e.clause_args {
635 by_key.insert(named.number, arg_meta(self.spec, arg, named, e, commands));
636 }
637 }
638
639 let total = commands
640 .iter()
641 .map(|e| 1 + e.flags.len() + e.clause_flags.len() + e.args.len() + e.clause_args.len())
642 .sum::<usize>() as u64;
643
644 let _ = writeln!(
645 self.out,
646 "// Meta is the cold table, read only by the rules that are decided once the\n\
647 // last token has been read: required, choices, the env-then-default fallback,\n\
648 // the var bounds, and the four that compare one entry against another. A parse\n\
649 // never touches it.\n\
650 //\n\
651 // Indexed by key, so entry Key sits at Meta[Key-1]. A command's slot is empty:\n\
652 // commands take keys too, and have no cold half.\n\
653 var Meta = argv.Metadata{{"
654 );
655 for key in 1..=total {
656 match by_key.get(&key) {
657 Some(entry) => {
658 let _ = writeln!(self.out, "\t{entry},");
659 }
660 None => {
661 let _ = writeln!(self.out, "\t{{}},");
662 }
663 }
664 }
665 let _ = writeln!(self.out, "}}\n");
666 }
667
668 fn flag_meta(
670 &self,
671 flag: &SpecFlag,
672 named: &Named,
673 owner: &Emitted,
674 commands: &[Emitted],
675 ) -> String {
676 let mut fields = vec![
677 format!("Key: {}", named.key),
678 format!("Name: {}", go_string(&flag.name)),
679 "Flag: true".to_string(),
680 ];
681 let scoped = owner
682 .clause_flags
683 .iter()
684 .any(|(_, candidate)| candidate.key == named.key);
685 if (!owner.cmd.args_override_self || scoped)
686 && !flag.var
687 && !flag.count
688 && !flag.arg.as_ref().is_some_and(|arg| arg.var)
689 {
690 fields.push("RejectDuplicate: true".to_string());
691 }
692 if flag.required {
693 fields.push("Required: true".to_string());
694 }
695 if flag.arg.is_none() {
696 fields.push("RequiresIfBoolean: true".to_string());
697 }
698 if let Some(long) = flag.long.first() {
702 fields.push(format!("Spelling: {}", go_string(&format!("--{long}"))));
703 } else if let Some(short) = flag.short.first() {
704 fields.push(format!("Spelling: {}", go_string(&format!("-{short}"))));
705 }
706 if let Some(value) = flag.arg.as_ref() {
709 fields.push(format!("ValueName: {}", go_string(&value.name)));
710 }
711 let named_value = flag
712 .arg
713 .as_ref()
714 .map(|a| a.name.as_str())
715 .unwrap_or(flag.name.as_str());
716 let inline = flag.arg.as_ref().and_then(|a| a.complete.as_ref());
717 if let Some(kind) = complete_type(self.spec, inline, named_value) {
718 fields.push(format!("CompleteType: {}", go_string(kind)));
719 }
720 if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) {
722 fields.push(format!(
723 "Choices: {}",
724 string_slice(&visible_choices(choices))
725 ));
726 fields.push(format!(
727 "AcceptedChoices: {}",
728 string_slice(&accepted_choices(choices))
729 ));
730 if choices.ignore_case {
731 fields.push("IgnoreCase: true".to_string());
732 }
733 if accepts_unlisted(choices) {
734 fields.push("AllowUnknownChoices: true".to_string());
735 }
736 }
737 let default = if !flag.default.is_empty() {
741 &flag.default
742 } else {
743 flag.arg
744 .as_ref()
745 .map(|a| &a.default)
746 .unwrap_or(&flag.default)
747 };
748 if !default.is_empty() {
749 fields.push(format!("Default: {}", string_slice(default)));
750 }
751 if let Some(env) = &flag.env {
752 fields.push(format!("Env: {}", go_string(env)));
753 }
754 if !flag.env_fallback.is_empty() {
755 fields.push(format!("EnvFallback: {}", string_slice(&flag.env_fallback)));
756 }
757 if !flag.deprecated_env.is_empty() {
758 fields.push(format!(
759 "DeprecatedEnv: {}",
760 string_slice(&flag.deprecated_env)
761 ));
762 }
763 let minimum = flag
764 .arg
765 .as_ref()
766 .filter(|arg| arg.var)
767 .and_then(|arg| arg.var_min)
768 .or(flag.var_min);
769 if let Some(min) = minimum {
770 fields.push(format!("VarMin: {}", clamp_var_max(min)));
771 }
772 if let Some(max) = flag.var_max {
775 fields.push(format!("VarMax: {}", clamp_var_max(max)));
776 }
777
778 for (label, names) in [
779 ("Conflicts", &flag.conflicts),
780 ("Overrides", &flag.overrides),
781 ("RequiredUnless", &flag.required_unless),
782 ("RequiredUnlessAll", &flag.required_unless_all),
783 ("RequiredIf", &flag.required_if),
784 ("Requires", &flag.requires),
785 ] {
786 let keys = resolve_relationship(names, owner, commands);
787 if !keys.is_empty() {
788 fields.push(format!("{label}: {}", key_slice(&keys)));
789 }
790 }
791 for (label, conditions) in [
792 ("RequiredIfEq", &flag.required_if_eq),
793 ("RequiredIfEqAll", &flag.required_if_eq_all),
794 ] {
795 let values = conditions
796 .iter()
797 .filter_map(|condition| {
798 resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
799 .into_iter()
800 .next()
801 .map(|key| {
802 format!("{{Key: {key}, Value: {}}}", go_string(&condition.value))
803 })
804 })
805 .collect::<Vec<_>>();
806 if !values.is_empty() {
807 fields.push(format!(
808 "{label}: []argv.ValueCondition{{{}}}",
809 values.join(", ")
810 ));
811 }
812 }
813 let requires_if = flag
814 .requires_if
815 .iter()
816 .filter_map(|condition| {
817 resolve_relationship(std::slice::from_ref(&condition.requires), owner, commands)
818 .into_iter()
819 .next()
820 .map(|key| format!("{{Value: {}, Key: {key}}}", go_string(&condition.value)))
821 })
822 .collect::<Vec<_>>();
823 if !requires_if.is_empty() {
824 fields.push(format!(
825 "RequiresIf: []argv.ValueRequirement{{{}}}",
826 requires_if.join(", ")
827 ));
828 }
829 let default_if = flag
830 .default_if
831 .iter()
832 .filter_map(|condition| {
833 resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
834 .into_iter()
835 .next()
836 .map(|key| match &condition.when {
837 None => format!("{{Key: {key}, Value: {}}}", go_string(&condition.value)),
838 Some(when) => format!(
839 "{{Key: {key}, When: {}, Value: {}}}",
840 go_string(when),
841 go_string(&condition.value)
842 ),
843 })
844 })
845 .collect::<Vec<_>>();
846 if !default_if.is_empty() {
847 fields.push(format!(
848 "DefaultIf: []argv.DefaultIf{{{}}}",
849 default_if.join(", ")
850 ));
851 }
852
853 format!("{{{}}}", fields.join(", "))
854 }
855}
856
857fn complete_type<'a>(
865 spec: &'a Spec,
866 inline: Option<&'a SpecComplete>,
867 name: &str,
868) -> Option<&'a str> {
869 inline
870 .or_else(|| spec.complete.get(&name.to_lowercase()))
871 .and_then(|c| c.type_.as_deref())
872}
873
874fn arg_meta(
875 spec: &Spec,
876 arg: &SpecArg,
877 named: &Named,
878 owner: &Emitted,
879 commands: &[Emitted],
880) -> String {
881 let mut fields = vec![
882 format!("Key: {}", named.key),
883 format!("Name: {}", go_string(&arg.name)),
884 ];
885 if arg.required {
886 fields.push("Required: true".to_string());
887 }
888 if let Some(kind) = complete_type(spec, arg.complete.as_ref(), &arg.name) {
893 fields.push(format!("CompleteType: {}", go_string(kind)));
894 }
895 if let Some(choices) = &arg.choices {
896 fields.push(format!(
897 "Choices: {}",
898 string_slice(&visible_choices(choices))
899 ));
900 fields.push(format!(
901 "AcceptedChoices: {}",
902 string_slice(&accepted_choices(choices))
903 ));
904 if choices.ignore_case {
905 fields.push("IgnoreCase: true".to_string());
906 }
907 if accepts_unlisted(choices) {
908 fields.push("AllowUnknownChoices: true".to_string());
909 }
910 }
911 if !arg.default.is_empty() {
912 fields.push(format!("Default: {}", string_slice(&arg.default)));
913 }
914 if let Some(env) = &arg.env {
915 fields.push(format!("Env: {}", go_string(env)));
916 }
917 if !arg.env_fallback.is_empty() {
918 fields.push(format!("EnvFallback: {}", string_slice(&arg.env_fallback)));
919 }
920 if !arg.deprecated_env.is_empty() {
921 fields.push(format!(
922 "DeprecatedEnv: {}",
923 string_slice(&arg.deprecated_env)
924 ));
925 }
926 if let Some(min) = arg.var_min {
927 fields.push(format!("VarMin: {}", clamp_var_max(min)));
928 }
929 let conflicts = resolve_relationship(&arg.conflicts, owner, commands);
930 if !conflicts.is_empty() {
931 fields.push(format!("Conflicts: {}", key_slice(&conflicts)));
932 }
933 for (label, names) in [
934 ("Requires", &arg.requires),
935 ("RequiredIf", &arg.required_if),
936 ("RequiredUnless", &arg.required_unless),
937 ("RequiredUnlessAll", &arg.required_unless_all),
938 ] {
939 let keys = resolve_relationship(names, owner, commands);
940 if !keys.is_empty() {
941 fields.push(format!("{label}: {}", key_slice(&keys)));
942 }
943 }
944 for (label, conditions) in [
945 ("RequiredIfEq", &arg.required_if_eq),
946 ("RequiredIfEqAll", &arg.required_if_eq_all),
947 ] {
948 let values = conditions
949 .iter()
950 .filter_map(|condition| {
951 resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
952 .into_iter()
953 .next()
954 .map(|key| format!("{{Key: {key}, Value: {}}}", go_string(&condition.value)))
955 })
956 .collect::<Vec<_>>();
957 if !values.is_empty() {
958 fields.push(format!(
959 "{label}: []argv.ValueCondition{{{}}}",
960 values.join(", ")
961 ));
962 }
963 }
964 format!("{{{}}}", fields.join(", "))
968}
969
970fn resolve_relationship(names: &[String], owner: &Emitted, commands: &[Emitted]) -> Vec<String> {
981 let mut out = Vec::new();
982 for name in names {
983 let mut found = match_flag(owner, name, false);
987 if found.is_none() && !name.starts_with('-') {
988 found = owner
989 .args
990 .iter()
991 .chain(owner.clause_args.iter())
992 .find(|(arg, _)| arg.name == *name)
993 .map(|(_, named)| named.key.clone());
994 }
995 if found.is_none() {
996 let path = &owner.cmd.full_cmd;
997 for depth in (0..path.len()).rev() {
998 let ancestor = commands
999 .iter()
1000 .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]);
1001 if let Some(key) = ancestor.and_then(|a| match_flag(a, name, true)) {
1002 found = Some(key);
1003 break;
1004 }
1005 }
1006 }
1007 if let Some(key) = found {
1008 out.push(key);
1009 }
1010 }
1011 out
1012}
1013
1014fn match_flag(cmd: &Emitted, name: &str, globals_only: bool) -> Option<String> {
1021 let eligible = |flag: &SpecFlag| !globals_only || flag.global;
1031
1032 let (long, short, bare) = if let Some(rest) = name.strip_prefix("--") {
1035 (Some(rest), None, None)
1036 } else if let Some(rest) = name.strip_prefix('-') {
1037 let mut chars = rest.chars();
1038 match (chars.next(), chars.next()) {
1039 (Some(c), None) => (None, Some(c), None),
1040 _ => (None, None, None),
1041 }
1042 } else {
1043 (None, None, Some(name))
1044 };
1045
1046 let ordinary = cmd.flags.iter().chain(&cmd.clause_flags).find(|(flag, _)| {
1047 if !eligible(flag) {
1048 return false;
1049 }
1050 if let Some(bare) = bare {
1051 return flag.name == bare;
1052 }
1053 if let Some(long) = long {
1054 return flag.long.iter().any(|l| l == long);
1055 }
1056 short.is_some_and(|c| flag.short.contains(&c))
1057 });
1058 if let Some((_, named)) = ordinary {
1059 return Some(named.key.clone());
1060 }
1061
1062 cmd.flags
1066 .iter()
1067 .chain(&cmd.clause_flags)
1068 .find(|(flag, _)| eligible(flag) && flag.negate.as_deref() == Some(name))
1069 .map(|(_, named)| named.key.clone())
1070}
1071fn string_slice(values: &[String]) -> String {
1072 let list = values
1073 .iter()
1074 .map(|v| go_string(v))
1075 .collect::<Vec<_>>()
1076 .join(", ");
1077 format!("[]string{{{list}}}")
1078}
1079
1080fn accepts_unlisted(choices: &SpecChoices) -> bool {
1086 !choices.strict || choices.run().is_some()
1087}
1088
1089fn accepted_choices(choices: &SpecChoices) -> Vec<String> {
1090 choices
1091 .choices
1092 .iter()
1093 .chain(
1094 choices
1095 .details
1096 .iter()
1097 .flat_map(|choice| choice.aliases.iter().map(|alias| &alias.value)),
1098 )
1099 .cloned()
1100 .collect()
1101}
1102
1103fn visible_choices(choices: &SpecChoices) -> Vec<String> {
1104 choices
1105 .choices
1106 .iter()
1107 .filter(|value| {
1108 !choices
1109 .details
1110 .iter()
1111 .any(|choice| choice.value == value.as_str() && choice.hide)
1112 })
1113 .chain(choices.details.iter().flat_map(|choice| {
1114 choice
1115 .aliases
1116 .iter()
1117 .filter(|alias| !alias.hide)
1118 .map(|alias| &alias.value)
1119 }))
1120 .cloned()
1121 .collect()
1122}
1123
1124fn key_slice(keys: &[String]) -> String {
1125 format!("[]uint64{{{}}}", keys.join(", "))
1126}
1127
1128impl Emitter<'_> {
1129 fn help_table(&mut self, commands: &[Emitted]) {
1136 let mut by_key: BTreeMap<u64, String> = BTreeMap::new();
1137 for e in commands {
1138 by_key.insert(e.named.number, command_help(e));
1139 for (flag, named) in e.flags.iter().chain(&e.clause_flags) {
1140 by_key.insert(named.number, flag_help(flag, named));
1141 }
1142 for (arg, named) in &e.args {
1143 by_key.insert(named.number, arg_help(arg, named));
1144 }
1145 for (arg, named) in &e.clause_args {
1146 by_key.insert(named.number, arg_help(arg, named));
1147 }
1148 }
1149
1150 let total = commands
1151 .iter()
1152 .map(|e| 1 + e.flags.len() + e.clause_flags.len() + e.args.len() + e.clause_args.len())
1153 .sum::<usize>() as u64;
1154
1155 let _ = writeln!(
1156 self.out,
1157 "// HelpText is the third table, read only when a page is rendered. Neither the\n\
1158 // parser nor the post-binding rules touch it, and a CLI that never prints help\n\
1159 // does not carry it: Go's linker drops an unreferenced table whole.\n\
1160 //\n\
1161 // Indexed by key, like the others.\n\
1162 var HelpText = argv.HelpTable{{"
1163 );
1164 for key in 1..=total {
1165 match by_key.get(&key) {
1166 Some(entry) => {
1167 let _ = writeln!(self.out, "\t{entry},");
1168 }
1169 None => {
1170 let _ = writeln!(self.out, "\t{{}},");
1171 }
1172 }
1173 }
1174 let _ = writeln!(self.out, "}}\n");
1175
1176 let mut fields = vec![
1177 format!("Name: {}", go_string(&self.spec.name)),
1178 format!("Bin: {}", go_string(&self.spec.bin)),
1179 ];
1180 if let Some(version) = self
1181 .spec
1182 .version
1183 .as_ref()
1184 .or(self.spec.long_version.as_ref())
1185 {
1186 fields.push(format!("Version: {}", go_string(version)));
1187 }
1188 if let Some(version) = &self.spec.long_version {
1189 fields.push(format!("LongVersion: {}", go_string(version)));
1190 }
1191 if let Some(about) = &self.spec.about {
1195 fields.push(format!("About: {}", go_string(about)));
1196 }
1197 if let Some(long) = &self.spec.about_long {
1198 fields.push(format!("LongAbout: {}", go_string(long)));
1199 }
1200 if let Some(author) = &self.spec.author {
1201 fields.push(format!("Author: {}", go_string(author)));
1202 }
1203 if let Some(license) = &self.spec.license {
1204 fields.push(format!("License: {}", go_string(license)));
1205 }
1206 if let Some(logo) = &self.spec.logo {
1209 fields.push(format!("Logo: {}", go_string(logo)));
1210 }
1211 if let Some(before) = &self.spec.before_help {
1212 fields.push(format!("BeforeHelp: {}", go_string(before)));
1213 }
1214 if let Some(after) = &self.spec.after_help {
1215 fields.push(format!("AfterHelp: {}", go_string(after)));
1216 }
1217 if let Some(before) = &self.spec.before_help_long {
1218 fields.push(format!("BeforeLongHelp: {}", go_string(before)));
1219 }
1220 if let Some(after) = &self.spec.after_help_long {
1221 fields.push(format!("AfterLongHelp: {}", go_string(after)));
1222 }
1223 if let Some(template) = &self.spec.help_template {
1225 fields.push(format!("HelpTemplate: {}", go_string(template)));
1226 }
1227 let _ = writeln!(
1228 self.out,
1229 "// HelpMeta is what a page needs from the spec's root rather than from any one\n\
1230 // command: root presentation metadata and CLI-wide layout.\n\
1231 var HelpMeta = argv.HelpSpec{{{}}}\n",
1232 fields.join(", ")
1233 );
1234 }
1235}
1236
1237fn command_help(e: &Emitted) -> String {
1239 let mut fields = vec![format!("Key: {}", e.named.key)];
1240 if e.cmd.hide {
1241 fields.push("Hide: true".to_string());
1242 }
1243 if let Some(heading) = &e.cmd.help_heading {
1244 fields.push(format!("Heading: {}", go_string(heading)));
1245 }
1246 if let Some(order) = e.cmd.display_order {
1247 fields.push(format!("DisplayOrder: {order}"));
1248 fields.push("DisplayOrderSet: true".to_string());
1249 }
1250 if let Some(help) = e.cmd.help.as_deref().or(e.cmd.help_long.as_deref()) {
1251 fields.push(format!("Short: {}", go_string(help)));
1252 }
1253 if let Some(long) = &e.cmd.help_long {
1254 fields.push(format!("Long: {}", go_string(long)));
1255 }
1256 if let Some(message) = &e.cmd.deprecated {
1257 fields.push(format!("Deprecated: {}", go_string(message)));
1258 }
1259 if let Some(at) = &e.cmd.deprecated_warn_at {
1260 fields.push(format!("DeprecatedWarnAt: {}", go_string(at)));
1261 }
1262 if let Some(at) = &e.cmd.deprecated_remove_at {
1263 fields.push(format!("DeprecatedRemoveAt: {}", go_string(at)));
1264 }
1265 if let Some(heading) = &e.cmd.subcommand_help_heading {
1266 fields.push(format!("SubcommandHelpHeading: {}", go_string(heading)));
1267 }
1268 if let Some(name) = &e.cmd.subcommand_value_name {
1269 fields.push(format!("SubcommandValueName: {}", go_string(name)));
1270 }
1271 if e.cmd.next_line_help {
1272 fields.push("NextLineHelp: true".to_string());
1273 }
1274 if e.cmd.flatten_help {
1275 fields.push("FlattenHelp: true".to_string());
1276 }
1277 if e.cmd.subcommand_required {
1278 fields.push("SubcommandRequired: true".to_string());
1279 }
1280 let visible: Vec<String> = e
1283 .cmd
1284 .aliases
1285 .iter()
1286 .filter(|a| !e.cmd.hidden_aliases.contains(a))
1287 .cloned()
1288 .collect();
1289 if !visible.is_empty() {
1290 fields.push(format!("VisibleAliases: {}", string_slice(&visible)));
1291 }
1292 if let Some(before) = &e.cmd.before_help {
1293 fields.push(format!("BeforeHelp: {}", go_string(before)));
1294 }
1295 if let Some(after) = &e.cmd.after_help {
1296 fields.push(format!("AfterHelp: {}", go_string(after)));
1297 }
1298 if let Some(before) = &e.cmd.before_help_long {
1302 fields.push(format!("BeforeLongHelp: {}", go_string(before)));
1303 }
1304 if let Some(after) = &e.cmd.after_help_long {
1305 fields.push(format!("AfterLongHelp: {}", go_string(after)));
1306 }
1307 if !e.cmd.examples.is_empty() {
1308 let items = e
1309 .cmd
1310 .examples
1311 .iter()
1312 .map(|x| {
1313 let mut parts = Vec::new();
1314 if let Some(header) = &x.header {
1315 parts.push(format!("Header: {}", go_string(header)));
1316 }
1317 parts.push(format!("Code: {}", go_string(&x.code)));
1318 if let Some(help) = &x.help {
1322 parts.push(format!("Help: {}", go_string(help)));
1323 }
1324 format!("{{{}}}", parts.join(", "))
1325 })
1326 .collect::<Vec<_>>()
1327 .join(", ");
1328 fields.push(format!("Examples: []argv.Example{{{items}}}"));
1329 }
1330 if !e.cmd.headings.is_empty() {
1334 let items = e
1335 .cmd
1336 .headings
1337 .iter()
1338 .map(|heading| {
1339 format!(
1340 "{{Title: {}, Help: {}}}",
1341 go_string(&heading.title),
1342 go_string(&heading.help)
1343 )
1344 })
1345 .collect::<Vec<_>>()
1346 .join(", ");
1347 fields.push(format!("Headings: []argv.Heading{{{items}}}"));
1348 }
1349 format!("{{{}}}", fields.join(", "))
1350}
1351
1352fn flag_help(flag: &SpecFlag, named: &Named) -> String {
1353 let mut fields = vec![format!("Key: {}", named.key)];
1354 if let Some(message) = &flag.deprecated {
1355 fields.push(format!("Deprecated: {}", go_string(message)));
1356 }
1357 if let Some(at) = &flag.deprecated_warn_at {
1358 fields.push(format!("DeprecatedWarnAt: {}", go_string(at)));
1359 }
1360 if let Some(at) = &flag.deprecated_remove_at {
1361 fields.push(format!("DeprecatedRemoveAt: {}", go_string(at)));
1362 }
1363 if flag.hide {
1364 fields.push("Hide: true".to_string());
1365 }
1366 if let Some(order) = flag.display_order {
1367 fields.push(format!("DisplayOrder: {order}"));
1368 fields.push("DisplayOrderSet: true".to_string());
1369 }
1370 for (name, hidden) in [
1371 ("HideDefaultValue", flag.hide_default_value),
1372 ("HideEnv", flag.hide_env),
1373 ("HideEnvValues", flag.hide_env_values),
1374 ("HidePossibleValues", flag.hide_possible_values),
1375 ("HideShortHelp", flag.hide_short_help),
1376 ("HideLongHelp", flag.hide_long_help),
1377 ] {
1378 if hidden {
1379 fields.push(format!("{name}: true"));
1380 }
1381 }
1382 if flag.required && flag.default.is_empty() {
1385 fields.push("Demanded: true".to_string());
1386 }
1387 if flag.var {
1388 fields.push("Repeatable: true".to_string());
1389 }
1390 if let Some(arg) = &flag.arg {
1391 if arg.name != flag.name {
1392 fields.push(format!("ValueName: {}", go_string(&arg.name)));
1393 }
1394 if arg.required && arg.default.is_empty() {
1398 fields.push("ValueDemanded: true".to_string());
1399 }
1400 if !arg.value_names.is_empty() {
1401 fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1402 }
1403 if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1404 fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1405 }
1406 }
1407 if let Some(help) = flag.help.as_deref().or(flag.help_first_line.as_deref()) {
1410 fields.push(format!("Short: {}", go_string(help)));
1411 }
1412 if let Some(long) = flag.help_long.as_deref().or(flag.help.as_deref()) {
1413 fields.push(format!("Long: {}", go_string(long)));
1414 }
1415 if let Some(heading) = &flag.help_heading {
1416 fields.push(format!("Heading: {}", go_string(heading)));
1417 }
1418 if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) {
1420 fields.push(format!(
1421 "Choices: {}",
1422 string_slice(&visible_choices(choices))
1423 ));
1424 }
1425 if let Some(env) = &flag.env {
1426 fields.push(format!("Env: {}", go_string(env)));
1427 }
1428 if !flag.env_fallback.is_empty() {
1429 fields.push(format!("EnvFallback: {}", string_slice(&flag.env_fallback)));
1430 }
1431 if !flag.deprecated_env.is_empty() {
1432 fields.push(format!(
1433 "DeprecatedEnv: {}",
1434 string_slice(&flag.deprecated_env)
1435 ));
1436 }
1437 let default = if !flag.default.is_empty() {
1438 &flag.default
1439 } else {
1440 flag.arg
1441 .as_ref()
1442 .map(|a| &a.default)
1443 .unwrap_or(&flag.default)
1444 };
1445 if !default.is_empty() {
1446 fields.push(format!("Default: {}", string_slice(default)));
1447 }
1448 format!("{{{}}}", fields.join(", "))
1449}
1450
1451fn arg_help(arg: &SpecArg, named: &Named) -> String {
1452 let mut fields = vec![format!("Key: {}", named.key)];
1453 if let Some(order) = arg.display_order {
1454 fields.push(format!("DisplayOrder: {order}"));
1455 fields.push("DisplayOrderSet: true".to_string());
1456 }
1457 if arg.hide {
1458 fields.push("Hide: true".to_string());
1459 }
1460 for (name, hidden) in [
1461 ("HideDefaultValue", arg.hide_default_value),
1462 ("HideEnv", arg.hide_env),
1463 ("HideEnvValues", arg.hide_env_values),
1464 ("HidePossibleValues", arg.hide_possible_values),
1465 ("HideShortHelp", arg.hide_short_help),
1466 ("HideLongHelp", arg.hide_long_help),
1467 ] {
1468 if hidden {
1469 fields.push(format!("{name}: true"));
1470 }
1471 }
1472 if arg.required && arg.default.is_empty() {
1473 fields.push("Demanded: true".to_string());
1474 }
1475 if !arg.value_names.is_empty() {
1476 fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1477 }
1478 if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1479 fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1480 }
1481 if let Some(help) = arg.help.as_deref().or(arg.help_first_line.as_deref()) {
1482 fields.push(format!("Short: {}", go_string(help)));
1483 }
1484 if let Some(long) = arg.help_long.as_deref().or(arg.help.as_deref()) {
1485 fields.push(format!("Long: {}", go_string(long)));
1486 }
1487 if let Some(heading) = &arg.help_heading {
1488 fields.push(format!("Heading: {}", go_string(heading)));
1489 }
1490 if let Some(choices) = &arg.choices {
1491 fields.push(format!(
1492 "Choices: {}",
1493 string_slice(&visible_choices(choices))
1494 ));
1495 }
1496 if let Some(env) = &arg.env {
1497 fields.push(format!("Env: {}", go_string(env)));
1498 }
1499 if !arg.env_fallback.is_empty() {
1500 fields.push(format!("EnvFallback: {}", string_slice(&arg.env_fallback)));
1501 }
1502 if !arg.deprecated_env.is_empty() {
1503 fields.push(format!(
1504 "DeprecatedEnv: {}",
1505 string_slice(&arg.deprecated_env)
1506 ));
1507 }
1508 if !arg.default.is_empty() {
1509 fields.push(format!("Default: {}", string_slice(&arg.default)));
1510 }
1511 format!("{{{}}}", fields.join(", "))
1512}
1513
1514enum Line {
1523 Field(String, String),
1525 Block(Vec<String>),
1527}
1528
1529fn render(out: &mut String, indent: &str, lines: &[Line]) {
1531 let mut run: Vec<(&String, &String)> = Vec::new();
1532
1533 fn flush(out: &mut String, indent: &str, run: &mut Vec<(&String, &String)>) {
1534 let width = run.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
1535 for (key, value) in run.iter() {
1536 let _ = writeln!(
1537 out,
1538 "{indent}{key}:{:width$} {value},",
1539 "",
1540 width = width - key.len()
1541 );
1542 }
1543 run.clear();
1544 }
1545
1546 for line in lines {
1547 match line {
1548 Line::Field(key, value) => run.push((key, value)),
1549 Line::Block(block) => {
1550 flush(out, indent, &mut run);
1551 for l in block {
1552 let _ = writeln!(out, "{indent}{l}");
1553 }
1554 }
1555 }
1556 }
1557 flush(out, indent, &mut run);
1558}
1559
1560struct Emitted {
1562 named: Named,
1563 cmd: SpecCommand,
1564 flags: Vec<(SpecFlag, Named)>,
1565 args: Vec<(SpecArg, Named)>,
1566 clause_flags: Vec<(SpecFlag, Named)>,
1567 clause_args: Vec<(SpecArg, Named)>,
1568 subcommands: Vec<usize>,
1570 root: bool,
1571}
1572
1573fn effective_unknown_flags(spec: &Spec, commands: &[Emitted], at: usize) -> UnknownFlags {
1580 let path = &commands[at].cmd.full_cmd;
1581 for depth in (0..=path.len()).rev() {
1582 let ancestor = commands
1583 .iter()
1584 .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]);
1585 if let Some(mode) = ancestor.and_then(|e| e.cmd.unknown_flags) {
1586 return mode;
1587 }
1588 }
1589 spec.unknown_flags.unwrap_or_default()
1590}
1591
1592fn flag_literal(flag: &SpecFlag, named: &Named) -> String {
1593 let mut fields = vec![
1594 format!("Key: {}", named.key),
1595 format!("Name: {}", go_string(&flag.name)),
1596 ];
1597 if !flag.long.is_empty() {
1598 let longs = flag
1599 .long
1600 .iter()
1601 .map(|l| go_string(l))
1602 .collect::<Vec<_>>()
1603 .join(", ");
1604 fields.push(format!("Longs: []string{{{longs}}}"));
1605 }
1606 if !flag.hidden_aliases.is_empty() {
1607 fields.push(format!(
1608 "HiddenLongs: {}",
1609 string_slice(&flag.hidden_aliases)
1610 ));
1611 }
1612 if !flag.short.is_empty() {
1613 let shorts = flag
1614 .short
1615 .iter()
1616 .map(|c| go_byte(*c))
1617 .collect::<Vec<_>>()
1618 .join(", ");
1619 fields.push(format!("Shorts: []byte{{{shorts}}}"));
1620 }
1621 if !flag.hidden_short_aliases.is_empty() {
1622 let shorts = flag
1623 .hidden_short_aliases
1624 .iter()
1625 .map(|c| go_byte(*c))
1626 .collect::<Vec<_>>()
1627 .join(", ");
1628 fields.push(format!("HiddenShorts: []byte{{{shorts}}}"));
1629 }
1630 if let Some(negate) = &flag.negate {
1631 fields.push(format!(
1634 "Negate: {}",
1635 go_string(negate.trim_start_matches('-'))
1636 ));
1637 }
1638 if flag.arg.is_some() {
1639 fields.push("TakesValue: true".to_string());
1640 }
1641 if flag.value_optional {
1642 fields.push("ValueOptional: true".to_string());
1643 }
1644 if flag.bool_value {
1645 fields.push("BoolValue: true".to_string());
1646 }
1647 let action = match flag.action {
1648 SpecFlagAction::Set => None,
1649 SpecFlagAction::Help => Some("argv.ActionHelp"),
1650 SpecFlagAction::HelpShort => Some("argv.ActionHelpShort"),
1651 SpecFlagAction::HelpLong => Some("argv.ActionHelpLong"),
1652 SpecFlagAction::HelpAll => Some("argv.ActionHelpAll"),
1653 SpecFlagAction::Version => Some("argv.ActionVersion"),
1654 };
1655 if let Some(action) = action {
1656 fields.push(format!("Action: {action}"));
1657 }
1658 if let Some(arg) = flag.arg.as_ref().filter(|a| a.var) {
1663 fields.push("Variadic: true".to_string());
1664 if let Some(max) = arg.var_max {
1665 fields.push(format!("VarMax: {}", clamp_var_max(max)));
1666 }
1667 }
1668 if flag.allow_hyphen_values() {
1669 fields.push("AllowHyphenValues: true".to_string());
1670 }
1671 if let Some(arg) = &flag.arg {
1672 if arg.allow_negative_numbers {
1673 fields.push("AllowNegativeNumbers: true".to_string());
1674 }
1675 if let Some(terminator) = &arg.value_terminator {
1676 fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1677 }
1678 if let Some(delimiter) = arg.delimiter {
1679 fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1680 }
1681 }
1682 if flag.require_equals {
1683 fields.push("RequireEquals: true".to_string());
1684 }
1685 if let Some(missing) = &flag.default_missing {
1686 fields.push(format!("DefaultMissing: {}", go_string(missing)));
1687 }
1688 if flag.global {
1689 fields.push("Global: true".to_string());
1690 }
1691 format!("{{{}}}", fields.join(", "))
1692}
1693
1694fn arg_literal(arg: &SpecArg, named: &Named) -> String {
1695 let mut fields = vec![
1696 format!("Key: {}", named.key),
1697 format!("Name: {}", go_string(&arg.name)),
1698 ];
1699 if let Some(sigil) = &arg.sigil {
1700 fields.push(format!("Sigil: {}", go_string(sigil)));
1701 }
1702 if arg.required {
1703 fields.push("Required: true".to_string());
1704 }
1705 if arg.var {
1706 fields.push("Var: true".to_string());
1707 if let Some(max) = arg.var_max {
1708 fields.push(format!("VarMax: {}", clamp_var_max(max)));
1709 }
1710 }
1711 if arg.allow_negative_numbers {
1712 fields.push("AllowNegativeNumbers: true".to_string());
1713 }
1714 if let Some(terminator) = &arg.value_terminator {
1715 fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1716 }
1717 if let Some(delimiter) = arg.delimiter {
1718 fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1719 }
1720 let double_dash = match arg.double_dash {
1721 SpecDoubleDashChoices::Required => Some("argv.DoubleDashRequired"),
1722 SpecDoubleDashChoices::Preserve => Some("argv.DoubleDashPreserve"),
1723 SpecDoubleDashChoices::Automatic => Some("argv.DoubleDashAutomatic"),
1724 _ => None,
1725 };
1726 if let Some(dd) = double_dash {
1727 fields.push(format!("DoubleDash: {dd}"));
1728 }
1729 format!("{{{}}}", fields.join(", "))
1730}
1731
1732fn clamp_var_max(max: usize) -> u32 {
1737 u32::try_from(max).unwrap_or(u32::MAX)
1738}
1739
1740const GO_KEYWORDS: &[&str] = &[
1745 "break",
1746 "case",
1747 "chan",
1748 "const",
1749 "continue",
1750 "default",
1751 "defer",
1752 "else",
1753 "fallthrough",
1754 "for",
1755 "func",
1756 "go",
1757 "goto",
1758 "if",
1759 "import",
1760 "interface",
1761 "map",
1762 "package",
1763 "range",
1764 "return",
1765 "select",
1766 "struct",
1767 "switch",
1768 "type",
1769 "var",
1770];
1771
1772const UNUSABLE_PACKAGE_NAMES: &[&str] = &["_", "init"];
1785
1786pub fn is_valid_package(name: &str) -> bool {
1791 !name.is_empty()
1792 && !GO_KEYWORDS.contains(&name)
1793 && !UNUSABLE_PACKAGE_NAMES.contains(&name)
1794 && !name.starts_with(|c: char| c.is_ascii_digit())
1795 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1796}
1797
1798fn field_name(name: &str) -> String {
1800 let ident = format!("{}", AsPascalCase(name));
1801 if ident.is_empty() || ident.starts_with(|c: char| c.is_ascii_digit()) {
1802 format!("X{ident}")
1803 } else {
1804 ident
1805 }
1806}
1807
1808fn package_ident(bin: &str) -> String {
1816 let lowered: String = bin
1817 .chars()
1818 .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
1819 .collect::<String>()
1820 .to_ascii_lowercase();
1821 if is_valid_package(&lowered) {
1822 lowered
1823 } else {
1824 format!("cli{lowered}")
1828 }
1829}
1830
1831fn go_string(s: &str) -> String {
1836 let mut out = String::with_capacity(s.len() + 2);
1837 out.push('"');
1838 for c in s.chars() {
1839 match c {
1840 '"' => out.push_str("\\\""),
1841 '\\' => out.push_str("\\\\"),
1842 '\n' => out.push_str("\\n"),
1843 '\r' => out.push_str("\\r"),
1844 '\t' => out.push_str("\\t"),
1845 c if (c as u32) < 0x20 || c as u32 == 0x7f => {
1846 let _ = write!(out, "\\x{:02x}", c as u32);
1847 }
1848 c => out.push(c),
1849 }
1850 }
1851 out.push('"');
1852 out
1853}
1854
1855fn go_byte(c: char) -> String {
1861 match c {
1862 '\'' => "'\\''".to_string(),
1863 '\\' => "'\\\\'".to_string(),
1864 c if c.is_ascii_graphic() => format!("'{c}'"),
1865 c => format!("0x{:02x}", (c as u32) & 0xff),
1866 }
1867}
1868
1869#[cfg(test)]
1870mod tests {
1871 use super::*;
1872
1873 fn entry_of(out: &str, name: &str) -> String {
1882 out.lines()
1883 .find(|l| l.contains(&format!("Name: \"{name}\", Flag: true")))
1884 .unwrap_or_default()
1885 .to_string()
1886 }
1887
1888 fn go(kdl: &str) -> String {
1889 let spec: Spec = kdl.parse().expect("the fixture spec should parse");
1890 generate(&spec, &GoOptions::default())
1891 }
1892
1893 #[test]
1894 fn a_whole_cli() {
1895 let out = go(r#"
1896name "ex"
1897bin "ex"
1898version "1.2.3"
1899long_version "1.2.3\ncommit abc123"
1900flag "-v --verbose" global=#true help="be loud"
1901flag "--color" negate="--no-color"
1902flag "-j --jobs <n>"
1903flag "--include <pattern>..." var_max=3
1904arg "<file>"
1905arg "[rest]..." var=#true
1906cmd "install" {
1907 alias "i"
1908 flag "-f --force"
1909 arg "<pkg>"
1910}
1911cmd "config" {
1912 cmd "ls" {
1913 flag "--no-header"
1914 }
1915}
1916"#);
1917 insta::assert_snapshot!(out);
1918 }
1919
1920 #[test]
1921 fn rich_choices_keep_acceptance_visibility_and_strictness_separate() {
1922 let out = go(r#"
1923name "ex"
1924bin "ex"
1925flag "--color <when>" {
1926 choices ignore_case=#true strict=#false {
1927 choice "always" {
1928 alias "yes"
1929 alias "on" hide=#true
1930 }
1931 choice "never" hide=#true
1932 }
1933}
1934"#);
1935 let entry = entry_of(&out, "color");
1936 assert!(
1937 entry.contains(r#"Choices: []string{"always", "yes"}"#),
1938 "{entry}"
1939 );
1940 assert!(
1941 entry.contains(r#"AcceptedChoices: []string{"always", "never", "yes", "on"}"#),
1942 "{entry}"
1943 );
1944 assert!(entry.contains("IgnoreCase: true"), "{entry}");
1945 assert!(entry.contains("AllowUnknownChoices: true"), "{entry}");
1946 }
1947
1948 #[test]
1949 fn choices_from_a_command_are_left_unchecked_rather_than_run() {
1950 let out = go(r#"
1952name "ex"
1953bin "ex"
1954flag "--service <name>" {
1955 choices "local" run="exit 1"
1956}
1957"#);
1958 let entry = entry_of(&out, "service");
1959 assert!(entry.contains(r#"Choices: []string{"local"}"#), "{entry}");
1960 assert!(entry.contains("AllowUnknownChoices: true"), "{entry}");
1961 }
1962
1963 #[test]
1965 fn unknown_flags_are_inherited_and_overridable() {
1966 let out = go(r#"
1967name "ex"
1968bin "ex"
1969unknown_flags "error"
1970cmd "strict" {
1971 cmd "deep" {}
1972}
1973cmd "exec" unknown_flags="value" {
1974 cmd "nested" {}
1975}
1976"#);
1977 insta::assert_snapshot!(out);
1978 }
1979
1980 #[test]
1983 fn unknown_env_reaches_generated_go_with_every_declared_name() {
1984 let out = go(r#"
1985name "ex"
1986bin "ex"
1987unknown_env prefix="EX_" {
1988 allow "EX_LIBRARY"
1989}
1990flag "--port <port>" env="EX_PORT" env_fallback="EX_OLD_PORT"
1991cmd "serve" {
1992 flag "--tls" deprecated_env="EX_SSL"
1993 clause "route" {
1994 flag "--path <path>" env="EX_ROUTE_PATH"
1995 arg "<route>"
1996 }
1997}
1998cmd "publish" {
1999 arg "[dir]" env="EX_DIR"
2000}
2001config {
2002 prop "jobs" env="EX_JOBS"
2003}
2004"#);
2005 assert!(out.contains("const UnknownEnvPrefix = \"EX_\""), "{out}");
2006 assert!(
2007 out.contains(
2008 "var UnknownEnvKnown = []string{\"EX_DIR\", \"EX_JOBS\", \"EX_LIBRARY\", \"EX_OLD_PORT\", \"EX_PORT\", \"EX_ROUTE_PATH\", \"EX_SSL\"}"
2009 ),
2010 "{out}"
2011 );
2012 assert!(
2013 out.contains(
2014 "argv.RefuseUnknownEnv(UnknownEnvPrefix, UnknownEnvKnown, argv.EnvNames())"
2015 ),
2016 "{out}"
2017 );
2018
2019 let without = go("name \"ex\"\nbin \"ex\"\nflag \"--port <port>\" env=\"EX_PORT\"\n");
2020 assert!(!without.contains("UnknownEnv"), "{without}");
2021 }
2022
2023 #[test]
2026 fn unknown_env_with_a_mount_is_refused() {
2027 let spec: Spec = "name \"ex\"\nbin \"ex\"\nunknown_env prefix=\"EX_\"\ncmd \"plugin\" {\n mount run=\"ex-plugin --usage\"\n}\n"
2028 .parse()
2029 .unwrap();
2030 let err = generate_with_types(&spec, &GoOptions::default(), &BTreeMap::new()).unwrap_err();
2031 assert!(err.contains("mount"), "{err}");
2032
2033 let spec: Spec =
2034 "name \"ex\"\nbin \"ex\"\ncmd \"plugin\" {\n mount run=\"ex-plugin --usage\"\n}\n"
2035 .parse()
2036 .unwrap();
2037 assert!(generate_with_types(&spec, &GoOptions::default(), &BTreeMap::new()).is_ok());
2038 }
2039
2040 #[test]
2043 fn colliding_names_get_distinct_identifiers() {
2044 let out = go(r#"
2045name "ex"
2046bin "ex"
2047cmd "macos-defaults" {
2048 flag "--apply"
2049}
2050cmd "macos" {
2051 cmd "defaults" {
2052 flag "--apply"
2053 }
2054}
2055"#);
2056 insta::assert_snapshot!(out);
2057 }
2058
2059 #[test]
2060 fn default_flag_routing_reaches_generated_go_tables() {
2061 let out = go(
2062 "name ex\nbin ex\ndefault_subcommand run\ndefault_subcommand_flags #true\ncmd run {}\n",
2063 );
2064 assert!(out.contains("DefaultSubcommandFlags: true"), "{out}");
2065 }
2066
2067 #[test]
2068 fn a_default_subcommand_points_into_the_tree() {
2069 let out = go(r#"
2070name "ex"
2071bin "ex"
2072default_subcommand "run"
2073arg "[task]"
2074cmd "run" {
2075 arg "[args]..." var=#true
2076}
2077"#);
2078 insta::assert_snapshot!(out);
2079 }
2080
2081 #[test]
2082 fn a_bin_name_that_is_not_an_identifier_still_gives_a_package() {
2083 assert_eq!(package_ident("my-cli"), "mycli");
2084 assert_eq!(package_ident("7zip"), "cli7zip");
2085 assert_eq!(package_ident(""), "cli");
2086 assert_eq!(package_ident("go"), "cligo");
2088 assert_eq!(package_ident("type"), "clitype");
2089 assert_eq!(package_ident("_"), "cli_");
2092 assert_eq!(package_ident("init"), "cliinit");
2093 assert_eq!(package_ident("__"), "__");
2095 assert_eq!(package_ident("initialize"), "initialize");
2096
2097 for bin in [
2101 "my-cli", "7zip", "", "go", "type", "_", "init", "__", "MiSe",
2102 ] {
2103 let out = package_ident(bin);
2104 assert!(is_valid_package(&out), "{bin:?} sanitized to {out:?}");
2105 }
2106 }
2107
2108 #[test]
2110 fn a_name_matching_a_generated_suffix_still_gets_its_own() {
2111 let out = go(r#"
2112name "ex"
2113bin "ex"
2114cmd "macos-defaults" {}
2115cmd "macos" {
2116 cmd "defaults" {}
2117}
2118cmd "macos-defaults2" {}
2119"#);
2120 assert_declares_each_constant_once(&out);
2125 }
2126
2127 fn constant_names(out: &str) -> Vec<&str> {
2129 out.lines()
2130 .skip_while(|l| !l.starts_with("const ("))
2131 .skip(1)
2132 .take_while(|l| !l.starts_with(')'))
2133 .filter_map(|l| l.split_whitespace().next())
2134 .collect()
2135 }
2136
2137 fn assert_declares_each_constant_once(out: &str) {
2139 let names = constant_names(out);
2140 assert!(!names.is_empty(), "no constants at all:\n{out}");
2141 let mut seen = std::collections::HashSet::new();
2142 for name in &names {
2143 assert!(seen.insert(*name), "{name} is declared twice:\n{out}");
2144 }
2145 }
2146
2147 #[test]
2148 fn a_package_that_would_not_compile_is_refused_rather_than_emitted() {
2149 assert!(is_valid_package("mycli"));
2150 assert!(is_valid_package("mise_tables"));
2151 assert!(!is_valid_package("my-pkg"));
2152 assert!(!is_valid_package("7zip"));
2153 assert!(!is_valid_package(""));
2154 assert!(!is_valid_package("range"));
2155 assert!(!is_valid_package("_"));
2156 assert!(!is_valid_package("init"));
2157 assert!(is_valid_package("__"));
2158
2159 let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
2161 let out = generate(
2162 &spec,
2163 &GoOptions {
2164 package: Some("my-pkg".into()),
2165 },
2166 );
2167 assert!(out.contains("package mypkg"), "{out}");
2168 }
2169
2170 #[test]
2184 fn the_long_pages_text_reaches_the_tables() {
2185 let out = go(r#"
2186name "ex"
2187bin "ex"
2188about "Short."
2189about_long "Long."
2190before_long_help "ROOT-BEFORE"
2191after_long_help "ROOT-AFTER"
2192cmd "run" help="Run it" {
2193 before_long_help "RUN-BEFORE"
2194 after_long_help "RUN-AFTER"
2195}
2196"#);
2197 let meta = out
2198 .lines()
2199 .find(|l| l.contains("var HelpMeta"))
2200 .expect("a root header is emitted");
2201 assert!(
2202 meta.contains(r#"About: "Short.""#) && meta.contains(r#"LongAbout: "Long.""#),
2203 "the two abouts are separate fields: {meta}"
2204 );
2205 assert!(
2206 meta.contains(r#"BeforeLongHelp: "ROOT-BEFORE""#)
2207 && meta.contains(r#"AfterLongHelp: "ROOT-AFTER""#),
2208 "the root's long brackets are emitted: {meta}"
2209 );
2210
2211 let run = out
2212 .lines()
2213 .find(|l| l.contains("Short: \"Run it\""))
2214 .expect("the command has a help entry");
2215 assert!(
2216 run.contains(r#"BeforeLongHelp: "RUN-BEFORE""#)
2217 && run.contains(r#"AfterLongHelp: "RUN-AFTER""#),
2218 "a command's long brackets are emitted: {run}"
2219 );
2220 }
2221
2222 #[test]
2230 fn an_examples_help_line_reaches_the_tables() {
2231 let out = go(r#"
2232name "ex"
2233bin "ex"
2234cmd "run" help="Run it" {
2235 example "ex run --fast" header="Speed" help="When you are in a hurry"
2236 example "ex run"
2237}
2238"#);
2239 let run = out
2240 .lines()
2241 .find(|l| l.contains("Examples: []argv.Example"))
2242 .expect("the command's examples are emitted");
2243 assert!(
2244 run.contains(
2245 r#"{Header: "Speed", Code: "ex run --fast", Help: "When you are in a hurry"}"#
2246 ),
2247 "all three fields are emitted: {run}"
2248 );
2249 assert!(
2251 run.contains(r#"{Code: "ex run"}"#),
2252 "an example with no header emits no header: {run}"
2253 );
2254 }
2255
2256 #[test]
2262 fn heading_prose_reaches_the_tables() {
2263 let out = go(r#"
2264name "ex"
2265bin "ex"
2266cmd "run" help="Run it" {
2267 heading "Filters" help="Filters accumulate from left to right."
2268 flag "--allow <NAME>" help="Allow it" help_heading="Filters"
2269}
2270"#);
2271 let run = out
2272 .lines()
2273 .find(|l| l.contains("Headings: []argv.Heading"))
2274 .expect("the command's headings are emitted");
2275 assert!(
2276 run.contains(r#"{Title: "Filters", Help: "Filters accumulate from left to right."}"#),
2277 "both fields are emitted: {run}"
2278 );
2279 }
2280
2281 #[test]
2284 fn a_long_about_alone_does_not_become_the_short_one() {
2285 let out = go(r#"
2286name "ex"
2287bin "ex"
2288about_long "Long only."
2289"#);
2290 let meta = out
2291 .lines()
2292 .find(|l| l.contains("var HelpMeta"))
2293 .expect("a root header is emitted");
2294 assert!(
2295 !meta.contains(", About: ") && meta.contains(r#"LongAbout: "Long only.""#),
2296 "only the long one is set: {meta}"
2297 );
2298 }
2299
2300 #[test]
2301 fn a_default_subcommand_ignores_a_deeper_command_of_the_same_name() {
2302 let out = go(r#"
2303name "ex"
2304bin "ex"
2305default_subcommand "run"
2306cmd "oci" {
2307 cmd "run" {}
2308}
2309cmd "run" {
2310 arg "[args]..." var=#true
2311}
2312"#);
2313 assert!(
2314 out.contains("DefaultSubcommand: cmdRun,"),
2315 "should point at the root's own `run`, got:\n{out}"
2316 );
2317 }
2318
2319 #[test]
2320 fn command_builtin_controls_reach_generated_go_tables() {
2321 let out = go(r#"
2322name "ex"
2323bin "ex"
2324disable_help_flag #true
2325disable_help_subcommand #true
2326disable_version_flag #true
2327"#);
2328 let root = out
2329 .split("var Root = &argv.Command{")
2330 .nth(1)
2331 .expect("the root command should be emitted")
2332 .split("}\n")
2333 .next()
2334 .unwrap();
2335 assert!(root.contains("DisableHelpFlag:"), "{root}");
2336 assert!(root.contains("DisableHelpSubcommand:"), "{root}");
2337 assert!(root.contains("DisableVersionFlag:"), "{root}");
2338 }
2339
2340 #[test]
2343 fn a_default_subcommand_prefers_a_name_to_another_commands_alias() {
2344 let ordered = |first: &str, second: &str| {
2345 go(&format!(
2346 r#"
2347name "ex"
2348bin "ex"
2349default_subcommand "run"
2350{first}
2351{second}
2352"#
2353 ))
2354 };
2355 let alpha = "cmd \"alpha\" {\n alias \"run\"\n}";
2356 let run = "cmd \"run\" {\n arg \"[args]...\" var=#true\n}";
2357 for out in [ordered(alpha, run), ordered(run, alpha)] {
2358 assert!(
2359 out.contains("DefaultSubcommand: cmdRun,"),
2360 "should point at the command named `run`, got:\n{out}"
2361 );
2362 }
2363 }
2364
2365 #[test]
2366 fn an_external_subcommand_is_emitted_on_the_command_that_declares_it() {
2367 let out = go(r#"
2368name "ex"
2369bin "ex"
2370external_subcommand #true
2371cmd "install"
2372cmd "exec" external_subcommand=#true
2373"#);
2374 let block = |var: &str| {
2375 let start = out
2376 .find(&format!("var {var} ="))
2377 .unwrap_or_else(|| panic!("{var} should be emitted, got:\n{out}"));
2378 let rest = &out[start..];
2379 let end = rest[1..]
2380 .find("\nvar ")
2381 .map(|i| i + 1)
2382 .unwrap_or(rest.len());
2383 &rest[..end]
2384 };
2385 assert!(
2386 block("Root").contains("ExternalSubcommand: true"),
2387 "the root should forward unmatched words:\n{}",
2388 block("Root")
2389 );
2390 assert!(
2391 block("cmdExec").contains("ExternalSubcommand: true"),
2392 "a nested command can forward too:\n{}",
2393 block("cmdExec")
2394 );
2395 assert!(
2396 !block("cmdInstall").contains("ExternalSubcommand"),
2397 "a command that does not declare it should not carry it:\n{}",
2398 block("cmdInstall")
2399 );
2400 }
2401
2402 #[test]
2403 fn arg_required_else_help_reaches_the_table_and_typed_front_door() {
2404 let out = go(r#"
2405name "ex"
2406bin "ex"
2407cmd "run" arg_required_else_help=#true {
2408 flag "--all"
2409}
2410"#);
2411 assert!(
2412 out.contains("ArgRequiredElseHelp: true"),
2413 "the command table should carry the policy:\n{out}"
2414 );
2415 assert!(
2416 out.contains("p.Command().ArgRequiredElseHelp && p.CommandStart() == len(args)"),
2417 "the typed parser should enforce it before fallbacks:\n{out}"
2418 );
2419 }
2420
2421 #[test]
2422 fn subcommand_negates_requirements_reaches_generated_go() {
2423 let out = go(
2424 "name \"ex\"\nbin \"ex\"\nsubcommand_negates_reqs #true\nflag \"--config\" required=#true\ncmd \"run\"\n",
2425 );
2426 assert!(out.contains("SubcommandNegatesReqs: true"), "{out}");
2427 assert!(
2428 out.contains("checkRequirements := i == len(chain)-1 || !cmd.SubcommandNegatesReqs"),
2429 "{out}"
2430 );
2431 assert!(
2432 out.contains("CheckRelationshipsWithValuesAndRequirements"),
2433 "{out}"
2434 );
2435 }
2436
2437 #[test]
2438 fn argument_subcommand_conflicts_reach_generated_go() {
2439 let out = go(
2440 "name \"ex\"\nbin \"ex\"\nargs_conflicts_with_subcommands #true\nflag \"--verbose\"\ncmd \"run\"\n",
2441 );
2442 assert!(out.contains("ArgsConflictWithSubcommands: true"), "{out}");
2443 }
2444
2445 #[test]
2446 fn allow_missing_positional_reaches_generated_go() {
2447 let out = go(
2448 "name \"ex\"\nbin \"ex\"\nallow_missing_positional #true\narg \"[optional]\"\narg \"<required>\"\n",
2449 );
2450 assert!(out.contains("AllowMissingPositional: true"), "{out}");
2451 assert!(out.contains("Name: \"optional\""), "{out}");
2452 assert!(out.contains("Name: \"required\", Required: true"), "{out}");
2453 }
2454
2455 #[test]
2456 fn optional_flag_values_reach_generated_go() {
2457 let out = go("name \"ex\"\nbin \"ex\"\nflag \"--color [WHEN]\" value_optional=#true\n");
2458 assert!(
2459 out.contains("TakesValue: true, ValueOptional: true"),
2460 "{out}"
2461 );
2462 }
2463
2464 #[test]
2465 fn explicit_boolean_values_reach_generated_go() {
2466 let out = go(
2467 "name \"ex\"\nbin \"ex\"\nflag \"--color\" negate=\"--no-color\" bool_value=#true\n",
2468 );
2469 assert!(out.contains("BoolValue: true"), "{out}");
2470 assert!(out.contains("if ev.Flag.BoolValue"), "{out}");
2471 assert!(
2472 out.contains("given[ev.Flag.Key] = []string{ev.Value}"),
2473 "{out}"
2474 );
2475 assert!(
2476 out.contains("(ev.Value == \"true\") != ev.Negated"),
2477 "{out}"
2478 );
2479 }
2480
2481 #[test]
2482 fn flag_actions_reach_generated_go() {
2483 let out = go(
2484 "name \"ex\"\nbin \"ex\"\nflag \"--help-all\" action=\"help_all\"\nflag \"--version\" action=\"version\"\n",
2485 );
2486 assert!(out.contains("Action: argv.ActionHelpAll"), "{out}");
2487 assert!(out.contains("Action: argv.ActionVersion"), "{out}");
2488 }
2489
2490 #[test]
2491 fn granular_help_hides_reach_generated_go() {
2492 let out = go(
2493 "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",
2494 );
2495 for field in [
2496 "HideDefaultValue: true",
2497 "HideEnv: true",
2498 "HideEnvValues: true",
2499 "HidePossibleValues: true",
2500 "HideShortHelp: true",
2501 "HideLongHelp: true",
2502 ] {
2503 assert!(out.contains(field), "missing {field}:\n{out}");
2504 }
2505 }
2506
2507 #[test]
2508 fn strict_duplicate_policy_reaches_metadata() {
2509 let permissive = go("name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\n");
2510 assert!(!permissive.contains("RejectDuplicate"), "{permissive}");
2511
2512 let strict =
2513 go("name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\n");
2514 assert!(strict.contains("RejectDuplicate: true"), "{strict}");
2515 }
2516
2517 #[test]
2518 fn clause_args_and_later_entries_reach_go_cold_tables() {
2519 let out = go(r#"
2520name "ex"
2521bin "ex"
2522cmd "run" {
2523 flag "--needs-task" requires="task"
2524 clause "items" separator=":::" {
2525 flag "--postinstall <command>"
2526 arg "<task>" help="Task to run"
2527 }
2528}
2529cmd "later" {
2530 flag "--mode <mode>" help="Later flag"
2531}
2532"#);
2533 let meta = out
2534 .split_once("var Meta = argv.Metadata{")
2535 .and_then(|(_, rest)| rest.split_once("var HelpText = argv.HelpTable{"))
2536 .map(|(meta, _)| meta)
2537 .expect("generated metadata and help tables");
2538 let help = out
2539 .split_once("var HelpText = argv.HelpTable{")
2540 .and_then(|(_, rest)| rest.split_once("var HelpMeta = argv.HelpSpec{"))
2541 .map(|(help, _)| help)
2542 .expect("generated help table and metadata");
2543
2544 assert!(
2545 meta.contains("Key: ArgRunItemsTask, Name: \"task\""),
2546 "{out}"
2547 );
2548 assert!(
2549 meta.lines().any(|line| {
2550 line.contains("Key: FlagRunItemsPostinstall")
2551 && line.contains("RejectDuplicate: true")
2552 }),
2553 "{out}"
2554 );
2555 assert!(
2556 meta.lines().any(|line| {
2557 line.contains("Key: FlagRunNeedsTask")
2558 && line.contains("Requires: []uint64{ArgRunItemsTask}")
2559 }),
2560 "{out}"
2561 );
2562 assert!(meta.contains("Key: FlagLaterMode, Name: \"mode\""), "{out}");
2563 assert!(
2564 help.contains("Key: ArgRunItemsTask, Demanded: true"),
2565 "{out}"
2566 );
2567 assert!(
2568 help.lines().any(|line| {
2569 line.contains("Key: FlagLaterMode") && line.contains("Short: \"Later flag\"")
2570 }),
2571 "{out}"
2572 );
2573 for generated in [
2574 "type RunCmdItemsClause struct {",
2575 "Postinstall string",
2576 "Flags: []*argv.Flag{",
2577 "clauseFlag := map[uint64]bool{FlagRunItemsPostinstall: true}",
2578 "case argv.KindClauseSeparator:",
2579 "for instanceIndex, instance := range clauseInstances[CmdRun] {",
2580 "if err := argv.Check(Meta.Lookup(key), values, instanceOccurrences[key]); err != nil {",
2581 "clauseSources := map[uint64]argv.Source{FlagRunItemsPostinstall: argv.Unset, ArgRunItemsTask: argv.Unset}",
2582 "cmdRunV.Items = append(cmdRunV.Items, item)",
2583 ] {
2584 assert!(out.contains(generated), "missing {generated:?}:\n{out}");
2585 }
2586 assert!(
2587 out.lines().any(|line| {
2588 line.contains("Items")
2589 && line.contains("[]RunCmdItemsClause")
2590 && line.contains("// clause items")
2591 }),
2592 "{out}"
2593 );
2594 }
2595
2596 #[test]
2597 fn strict_negated_flags_track_each_spelling_separately() {
2598 let out = go(
2599 "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n",
2600 );
2601 assert!(out.contains("polaritySeen := map[uint64]uint8{}"), "{out}");
2602 assert!(
2603 out.contains("polaritySeen[ev.Flag.Key]&polarity != 0"),
2604 "{out}"
2605 );
2606 assert!(out.contains("if duplicateSeen[key]"), "{out}");
2607 }
2608
2609 #[test]
2610 fn strict_global_duplicate_tracking_resets_at_subcommands() {
2611 let out = go(
2612 "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\" global=#true\ncmd \"run\" {\n args_override_self #false\n}\n",
2613 );
2614 assert!(
2615 out.contains("levelSeen = map[uint64]int{}"),
2616 "a subcommand should start a new duplicate scope:\n{out}"
2617 );
2618 assert!(out.contains("strictSeen[ev.Flag.Key] = true"), "{out}");
2619 assert!(out.contains("if strictSeen[key]"), "{out}");
2620 }
2621
2622 #[test]
2624 fn a_subcommand_named_root_does_not_collide_with_the_root() {
2625 let out = go(r#"
2626name "ex"
2627bin "ex"
2628cmd "root" {
2629 flag "--wat"
2630}
2631"#);
2632 let declared = |name: &str| {
2635 out.lines()
2636 .filter(|l| l.split_whitespace().next() == Some(name))
2637 .count()
2638 };
2639 assert_eq!(declared("CmdRoot"), 1, "CmdRoot declared twice:\n{out}");
2640 assert_eq!(declared("CmdRoot2"), 1, "no distinct key for it:\n{out}");
2641 assert_declares_each_constant_once(&out);
2642 }
2643
2644 #[test]
2649 fn only_the_per_occurrence_bound_reaches_the_table() {
2650 let out = go(r#"
2651name "ex"
2652bin "ex"
2653flag "--include <pattern>..." {
2654 arg "<pattern>..." var=#true var_min=2 var_max=2
2655}
2656flag "--tag <t>" var=#true var_max=1
2657"#);
2658 assert!(
2659 out.contains("Name: \"include\", Longs: []string{\"include\"}, TakesValue: true, Variadic: true, VarMax: 2"),
2660 "{out}"
2661 );
2662 assert!(
2663 out.contains("Name: \"include\", Flag: true") && out.contains("VarMin: 2"),
2664 "the nested value minimum must reach post-binding metadata:\n{out}"
2665 );
2666 let tag = out.lines().find(|l| l.contains("\"tag\"")).unwrap();
2667 assert!(!tag.contains("VarMax"), "occurrence bound leaked: {tag}");
2668 }
2669
2670 #[test]
2671 fn exact_arity_with_one_label_reaches_go_help() {
2672 let out = go(r#"
2673name "ex"
2674bin "ex"
2675flag "--pair <ITEM>..." {
2676 arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2677 value_names "ITEM"
2678 }
2679 }
2680arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2681 value_names "ITEM"
2682}
2683"#);
2684 assert_eq!(out.matches("ValueArity: 2").count(), 2, "{out}");
2685 assert_eq!(
2686 out.matches("ValueNames: []string{\"ITEM\"}").count(),
2687 2,
2688 "{out}"
2689 );
2690 }
2691
2692 #[test]
2693 fn scoped_flag_binding_preserves_counts_and_boolean_polarity() {
2694 let out = go(r#"
2695name "ex"
2696bin "ex"
2697clause "tools" {
2698 flag "--required" required=#true
2699 flag "-v --verbose" count=#true
2700 flag "--color" negate="--no-color" bool_value=#true
2701 arg "<tool>"
2702}
2703"#);
2704
2705 for generated in [
2706 "if clauseFlag[f.Key] {",
2707 "instanceOccurrences := clauseInstanceOccurrences[CmdRoot][instanceIndex]",
2708 "argv.Check(Meta.Lookup(key), values, instanceOccurrences[key])",
2709 "item.Required = !instanceNegated[FlagToolsRequired]",
2710 "item.Verbose = instanceOccurrences[FlagToolsVerbose]",
2711 "item.Color = (values[len(values)-1] == \"true\") != instanceNegated[FlagToolsColor]",
2712 "item.Color = !instanceNegated[FlagToolsColor]",
2713 ] {
2714 assert!(out.contains(generated), "missing {generated:?}:\n{out}");
2715 }
2716 }
2717
2718 #[test]
2719 fn a_bare_optional_scoped_flag_does_not_index_an_empty_value() {
2720 let out = go(r#"
2721name "ex"
2722bin "ex"
2723clause "tools" {
2724 flag "--label [LABEL]" value_optional=#true
2725 arg "<tool>"
2726}
2727"#);
2728
2729 assert!(
2730 out.contains(
2731 "if values := instance[FlagToolsLabel]; len(values) > 0 {\n\t\t\t\titem.Label = values[len(values)-1]"
2732 ),
2733 "{out}"
2734 );
2735 assert!(
2736 !out.contains("if values, ok := instance[FlagToolsLabel]; ok"),
2737 "{out}"
2738 );
2739 }
2740
2741 #[test]
2742 fn allow_hyphen_values_reaches_the_table() {
2743 let out = go(r#"
2744name "ex"
2745bin "ex"
2746flag "--args <ARGS>" allow_hyphen_values=#true
2747"#);
2748 assert!(
2749 out.contains("Name: \"args\", Longs: []string{\"args\"}, TakesValue: true, AllowHyphenValues: true"),
2750 "{out}"
2751 );
2752 }
2753
2754 #[test]
2755 fn require_equals_reaches_the_table() {
2756 let out = go(r#"
2757name "ex"
2758bin "ex"
2759flag "--inspect <PORT>" require_equals=#true
2760"#);
2761 assert!(
2762 out.contains("Name: \"inspect\", Longs: []string{\"inspect\"}, TakesValue: true, RequireEquals: true"),
2763 "{out}"
2764 );
2765 }
2766
2767 #[test]
2768 fn default_missing_reaches_the_table() {
2769 let out = go(r#"
2770name "ex"
2771bin "ex"
2772flag "--color <WHEN>" default_missing="always"
2773"#);
2774 assert!(
2775 out.contains(
2776 "Name: \"color\", Longs: []string{\"color\"}, TakesValue: true, DefaultMissing: \"always\""
2777 ),
2778 "{out}"
2779 );
2780 }
2781
2782 #[test]
2788 fn a_relationship_resolves_through_scope_and_negation() {
2789 let out = go(r#"
2790name "ex"
2791bin "ex"
2792flag "--quiet" global=#true
2793flag "--color" negate="--no-color"
2794flag "--plain" conflicts="--no-color"
2795cmd "run" {
2796 flag "--loud" conflicts="--quiet"
2797 flag "--solo" conflicts="--plain"
2798}
2799"#);
2800 assert!(out.contains("Conflicts: []uint64{FlagColor}"), "{out}");
2802 assert!(out.contains("Conflicts: []uint64{FlagQuiet}"), "{out}");
2804 assert!(
2807 !entry_of(&out, "solo").contains("Conflicts"),
2808 "a non-global should not resolve from below:\n{out}"
2809 );
2810 }
2811
2812 #[test]
2813 fn positional_conflicts_reach_go_metadata_in_both_directions() {
2814 let out = go(r#"
2815name "ex"
2816bin "ex"
2817flag "--from-file <file>" conflicts="value"
2818arg "[value]" conflicts="--from-file"
2819"#);
2820
2821 assert!(
2822 entry_of(&out, "from-file").contains("Conflicts: []uint64{ArgValue}"),
2823 "{out}"
2824 );
2825 assert!(
2826 out.lines().any(|line| {
2827 line.contains("{Key: ArgValue, Name: \"value\"")
2828 && line.contains("Conflicts: []uint64{FlagFromFile}")
2829 }),
2830 "{out}"
2831 );
2832 }
2833
2834 #[test]
2835 fn a_value_conditional_requirement_reaches_go_metadata() {
2836 let out = go(r#"
2837name "ex"
2838bin "ex"
2839flag "--format <format>" {
2840 requires_if "json" "--schema"
2841}
2842flag "--schema <file>"
2843"#);
2844 assert!(
2845 entry_of(&out, "format").contains(
2846 "RequiresIf: []argv.ValueRequirement{{Value: \"json\", Key: FlagSchema}}"
2847 ),
2848 "{out}"
2849 );
2850 assert!(
2851 out.contains("argv.CheckRelationshipsWithValues"),
2852 "the emitted parser must enforce the metadata:\n{out}"
2853 );
2854 }
2855
2856 #[test]
2857 fn required_if_eq_makes_generated_go_supply_values() {
2858 let out = go(r#"
2859name "ex"
2860bin "ex"
2861flag "--token <token>" {
2862 required_if_eq "--mode" "remote"
2863}
2864flag "--mode <mode>"
2865"#);
2866 assert!(
2867 entry_of(&out, "token").contains(
2868 "RequiredIfEq: []argv.ValueCondition{{Key: FlagMode, Value: \"remote\"}}"
2869 ),
2870 "{out}"
2871 );
2872 assert!(out.contains("resolved := map[uint64][]string{}"), "{out}");
2873 assert!(out.contains("argv.CheckRelationshipsWithValues"), "{out}");
2874 }
2875
2876 #[test]
2877 fn boolean_sources_are_normalized_for_value_relationships() {
2878 let out = go(r#"
2879name "ex"
2880bin "ex"
2881flag "--token <token>" {
2882 required_if_eq "--mode" "true"
2883}
2884flag "--mode" negate="--no-mode" bool_value=#true
2885"#);
2886 assert!(
2887 entry_of(&out, "mode").contains("RequiresIfBoolean: true"),
2888 "{out}"
2889 );
2890 }
2891
2892 #[test]
2893 fn a_conditional_default_reaches_go_metadata() {
2894 let out = go(r#"
2895name "ex"
2896bin "ex"
2897flag "--bin-names" {
2898 default_if "--json" "true"
2899 default_if "--output" "json" "pretty"
2900}
2901flag "--json"
2902flag "--output <fmt>"
2903"#);
2904 assert!(
2905 entry_of(&out, "bin-names")
2906 .contains("DefaultIf: []argv.DefaultIf{{Key: FlagJson, Value: \"true\"}"),
2907 "{out}"
2908 );
2909 assert!(
2910 entry_of(&out, "bin-names").contains("When: \"json\""),
2911 "{out}"
2912 );
2913 assert!(
2914 out.contains("argv.ApplyDefaultIf"),
2915 "the emitted parser must apply the metadata:\n{out}"
2916 );
2917 assert!(
2918 out.contains("negated[ev.Flag.Key] = ev.Negated"),
2919 "Equals default_if needs the negate form:\n{out}"
2920 );
2921 }
2922
2923 #[test]
2927 fn a_relationship_needs_the_right_form() {
2928 let out = go(r#"
2929name "ex"
2930bin "ex"
2931flag "-q --quiet"
2932flag "--color"
2933flag "--a" conflicts="--q"
2934flag "--b" conflicts="-color"
2935flag "--c" conflicts="-q"
2936flag "--d" conflicts="--color"
2937"#);
2938 assert!(!entry_of(&out, "a").contains("Conflicts"), "{out}");
2940 assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
2941 assert!(
2943 entry_of(&out, "c").contains("Conflicts: []uint64{FlagQuiet}"),
2944 "{out}"
2945 );
2946 assert!(
2947 entry_of(&out, "d").contains("Conflicts: []uint64{FlagColor}"),
2948 "{out}"
2949 );
2950 }
2951
2952 #[test]
2959 fn an_ordinary_form_beats_another_flags_negation() {
2960 let out = go(r#"
2961name "ex"
2962bin "ex"
2963flag "--a" negate="--zap"
2964flag "--zap"
2965flag "--p" conflicts="--zap"
2966"#);
2967 assert!(
2968 entry_of(&out, "p").contains("Conflicts: []uint64{FlagZap}"),
2969 "should name the flag `--zap` binds, not the one negating to it:\n{out}"
2970 );
2971 }
2972
2973 #[test]
2975 fn a_single_dash_negation_is_named_by_its_own_form() {
2976 let out = go(r#"
2977name "ex"
2978bin "ex"
2979flag "--tint" negate="-no-tint"
2980flag "--plain" conflicts="-no-tint"
2981flag "--other" conflicts="--no-tint"
2982"#);
2983 assert!(
2984 entry_of(&out, "plain").contains("Conflicts: []uint64{FlagTint}"),
2985 "the exact form should resolve:\n{out}"
2986 );
2987 assert!(
2989 !entry_of(&out, "other").contains("Conflicts"),
2990 "`--no-tint` is not how it was declared:\n{out}"
2991 );
2992 }
2993
2994 #[test]
2996 fn a_negation_is_matched_as_written() {
2997 let out = go(r#"
2998name "ex"
2999bin "ex"
3000flag "--color" negate="--no-color"
3001flag "--tint" negate="-no-tint"
3002flag "--a" conflicts="--no-color"
3003flag "--b" conflicts="--no-tint"
3004"#);
3005 assert!(
3006 entry_of(&out, "a").contains("Conflicts: []uint64{FlagColor}"),
3007 "{out}"
3008 );
3009 assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
3012 }
3013
3014 #[test]
3015 fn a_complete_inside_an_arg_is_its_complete_type() {
3016 let out = go(r#"
3017name "ex"
3018bin "ex"
3019complete "target" type="file"
3020arg "<target>" {
3021 complete type="dir"
3022}
3023flag "--into <where>" {
3024 complete type="dir"
3025}
3026"#);
3027 let target = out
3028 .lines()
3029 .find(|l| l.contains("Name: \"target\"") && l.contains("CompleteType"))
3030 .unwrap_or_default();
3031 assert!(target.contains("CompleteType: \"dir\""), "{out}");
3032 assert!(
3033 entry_of(&out, "into").contains("CompleteType: \"dir\""),
3034 "{out}"
3035 );
3036 }
3037
3038 #[test]
3039 fn strings_are_escaped_to_go_rules() {
3040 assert_eq!(go_string(r#"a"b\c"#), r#""a\"b\\c""#);
3041 assert_eq!(go_string("tab\there"), r#""tab\there""#);
3042 assert_eq!(go_string("\u{7f}"), r#""\x7f""#);
3044 }
3045}