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