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