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 let Some(var) = &default_subcommand {
495 lines.push(Line::Field("DefaultSubcommand".into(), var.clone()));
496 }
497 if self.spec.version.is_some() || self.spec.long_version.is_some() {
498 lines.push(Line::Field("Version".into(), "true".into()));
501 }
502 }
503
504 let _ = writeln!(self.out, "{doc}");
505 let _ = writeln!(self.out, "var {} = &argv.Command{{", e.named.var);
506 render(&mut self.out, "\t", &lines);
507 let _ = writeln!(self.out, "}}\n");
508 }
509 }
510}
511
512impl Emitter<'_> {
513 fn metadata(&mut self, commands: &[Emitted]) {
523 let mut by_key: BTreeMap<u64, String> = BTreeMap::new();
525 for e in commands {
526 for (flag, named) in e.flags.iter().chain(&e.clause_flags) {
527 by_key.insert(named.number, self.flag_meta(flag, named, e, commands));
528 }
529 for (arg, named) in &e.args {
530 by_key.insert(named.number, arg_meta(self.spec, arg, named, e, commands));
531 }
532 for (arg, named) in &e.clause_args {
533 by_key.insert(named.number, arg_meta(self.spec, arg, named, e, commands));
534 }
535 }
536
537 let total = commands
538 .iter()
539 .map(|e| 1 + e.flags.len() + e.clause_flags.len() + e.args.len() + e.clause_args.len())
540 .sum::<usize>() as u64;
541
542 let _ = writeln!(
543 self.out,
544 "// Meta is the cold table, read only by the rules that are decided once the\n\
545 // last token has been read: required, choices, the env-then-default fallback,\n\
546 // the var bounds, and the four that compare one entry against another. A parse\n\
547 // never touches it.\n\
548 //\n\
549 // Indexed by key, so entry Key sits at Meta[Key-1]. A command's slot is empty:\n\
550 // commands take keys too, and have no cold half.\n\
551 var Meta = argv.Metadata{{"
552 );
553 for key in 1..=total {
554 match by_key.get(&key) {
555 Some(entry) => {
556 let _ = writeln!(self.out, "\t{entry},");
557 }
558 None => {
559 let _ = writeln!(self.out, "\t{{}},");
560 }
561 }
562 }
563 let _ = writeln!(self.out, "}}\n");
564 }
565
566 fn flag_meta(
568 &self,
569 flag: &SpecFlag,
570 named: &Named,
571 owner: &Emitted,
572 commands: &[Emitted],
573 ) -> String {
574 let mut fields = vec![
575 format!("Key: {}", named.key),
576 format!("Name: {}", go_string(&flag.name)),
577 "Flag: true".to_string(),
578 ];
579 let scoped = owner
580 .clause_flags
581 .iter()
582 .any(|(_, candidate)| candidate.key == named.key);
583 if (!owner.cmd.args_override_self || scoped)
584 && !flag.var
585 && !flag.count
586 && !flag.arg.as_ref().is_some_and(|arg| arg.var)
587 {
588 fields.push("RejectDuplicate: true".to_string());
589 }
590 if flag.required {
591 fields.push("Required: true".to_string());
592 }
593 if flag.arg.is_none() {
594 fields.push("RequiresIfBoolean: true".to_string());
595 }
596 if let Some(long) = flag.long.first() {
600 fields.push(format!("Spelling: {}", go_string(&format!("--{long}"))));
601 } else if let Some(short) = flag.short.first() {
602 fields.push(format!("Spelling: {}", go_string(&format!("-{short}"))));
603 }
604 if let Some(value) = flag.arg.as_ref() {
607 fields.push(format!("ValueName: {}", go_string(&value.name)));
608 }
609 let named_value = flag
610 .arg
611 .as_ref()
612 .map(|a| a.name.as_str())
613 .unwrap_or(flag.name.as_str());
614 if let Some(kind) = complete_type(self.spec, named_value) {
615 fields.push(format!("CompleteType: {}", go_string(kind)));
616 }
617 if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) {
619 fields.push(format!(
620 "Choices: {}",
621 string_slice(&visible_choices(choices))
622 ));
623 fields.push(format!(
624 "AcceptedChoices: {}",
625 string_slice(&accepted_choices(choices))
626 ));
627 if choices.ignore_case {
628 fields.push("IgnoreCase: true".to_string());
629 }
630 if !choices.strict {
631 fields.push("AllowUnknownChoices: true".to_string());
632 }
633 }
634 let default = if !flag.default.is_empty() {
638 &flag.default
639 } else {
640 flag.arg
641 .as_ref()
642 .map(|a| &a.default)
643 .unwrap_or(&flag.default)
644 };
645 if !default.is_empty() {
646 fields.push(format!("Default: {}", string_slice(default)));
647 }
648 if let Some(env) = &flag.env {
649 fields.push(format!("Env: {}", go_string(env)));
650 }
651 if !flag.env_fallback.is_empty() {
652 fields.push(format!("EnvFallback: {}", string_slice(&flag.env_fallback)));
653 }
654 if !flag.deprecated_env.is_empty() {
655 fields.push(format!(
656 "DeprecatedEnv: {}",
657 string_slice(&flag.deprecated_env)
658 ));
659 }
660 let minimum = flag
661 .arg
662 .as_ref()
663 .filter(|arg| arg.var)
664 .and_then(|arg| arg.var_min)
665 .or(flag.var_min);
666 if let Some(min) = minimum {
667 fields.push(format!("VarMin: {}", clamp_var_max(min)));
668 }
669 if let Some(max) = flag.var_max {
672 fields.push(format!("VarMax: {}", clamp_var_max(max)));
673 }
674
675 for (label, names) in [
676 ("Conflicts", &flag.conflicts),
677 ("Overrides", &flag.overrides),
678 ("RequiredUnless", &flag.required_unless),
679 ("RequiredUnlessAll", &flag.required_unless_all),
680 ("RequiredIf", &flag.required_if),
681 ("Requires", &flag.requires),
682 ] {
683 let keys = resolve_relationship(names, owner, commands);
684 if !keys.is_empty() {
685 fields.push(format!("{label}: {}", key_slice(&keys)));
686 }
687 }
688 for (label, conditions) in [
689 ("RequiredIfEq", &flag.required_if_eq),
690 ("RequiredIfEqAll", &flag.required_if_eq_all),
691 ] {
692 let values = conditions
693 .iter()
694 .filter_map(|condition| {
695 resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
696 .into_iter()
697 .next()
698 .map(|key| {
699 format!("{{Key: {key}, Value: {}}}", go_string(&condition.value))
700 })
701 })
702 .collect::<Vec<_>>();
703 if !values.is_empty() {
704 fields.push(format!(
705 "{label}: []argv.ValueCondition{{{}}}",
706 values.join(", ")
707 ));
708 }
709 }
710 let requires_if = flag
711 .requires_if
712 .iter()
713 .filter_map(|condition| {
714 resolve_relationship(std::slice::from_ref(&condition.requires), owner, commands)
715 .into_iter()
716 .next()
717 .map(|key| format!("{{Value: {}, Key: {key}}}", go_string(&condition.value)))
718 })
719 .collect::<Vec<_>>();
720 if !requires_if.is_empty() {
721 fields.push(format!(
722 "RequiresIf: []argv.ValueRequirement{{{}}}",
723 requires_if.join(", ")
724 ));
725 }
726 let default_if = flag
727 .default_if
728 .iter()
729 .filter_map(|condition| {
730 resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
731 .into_iter()
732 .next()
733 .map(|key| match &condition.when {
734 None => format!("{{Key: {key}, Value: {}}}", go_string(&condition.value)),
735 Some(when) => format!(
736 "{{Key: {key}, When: {}, Value: {}}}",
737 go_string(when),
738 go_string(&condition.value)
739 ),
740 })
741 })
742 .collect::<Vec<_>>();
743 if !default_if.is_empty() {
744 fields.push(format!(
745 "DefaultIf: []argv.DefaultIf{{{}}}",
746 default_if.join(", ")
747 ));
748 }
749
750 format!("{{{}}}", fields.join(", "))
751 }
752}
753
754fn complete_type<'a>(spec: &'a Spec, name: &str) -> Option<&'a str> {
761 spec.complete
762 .get(&name.to_lowercase())
763 .and_then(|c| c.type_.as_deref())
764}
765
766fn arg_meta(
767 spec: &Spec,
768 arg: &SpecArg,
769 named: &Named,
770 owner: &Emitted,
771 commands: &[Emitted],
772) -> String {
773 let mut fields = vec![
774 format!("Key: {}", named.key),
775 format!("Name: {}", go_string(&arg.name)),
776 ];
777 if arg.required {
778 fields.push("Required: true".to_string());
779 }
780 if let Some(kind) = complete_type(spec, &arg.name) {
785 fields.push(format!("CompleteType: {}", go_string(kind)));
786 }
787 if let Some(choices) = &arg.choices {
788 fields.push(format!(
789 "Choices: {}",
790 string_slice(&visible_choices(choices))
791 ));
792 fields.push(format!(
793 "AcceptedChoices: {}",
794 string_slice(&accepted_choices(choices))
795 ));
796 if choices.ignore_case {
797 fields.push("IgnoreCase: true".to_string());
798 }
799 if !choices.strict {
800 fields.push("AllowUnknownChoices: true".to_string());
801 }
802 }
803 if !arg.default.is_empty() {
804 fields.push(format!("Default: {}", string_slice(&arg.default)));
805 }
806 if let Some(env) = &arg.env {
807 fields.push(format!("Env: {}", go_string(env)));
808 }
809 if !arg.env_fallback.is_empty() {
810 fields.push(format!("EnvFallback: {}", string_slice(&arg.env_fallback)));
811 }
812 if !arg.deprecated_env.is_empty() {
813 fields.push(format!(
814 "DeprecatedEnv: {}",
815 string_slice(&arg.deprecated_env)
816 ));
817 }
818 if let Some(min) = arg.var_min {
819 fields.push(format!("VarMin: {}", clamp_var_max(min)));
820 }
821 let conflicts = resolve_relationship(&arg.conflicts, owner, commands);
822 if !conflicts.is_empty() {
823 fields.push(format!("Conflicts: {}", key_slice(&conflicts)));
824 }
825 for (label, names) in [
826 ("Requires", &arg.requires),
827 ("RequiredIf", &arg.required_if),
828 ("RequiredUnless", &arg.required_unless),
829 ("RequiredUnlessAll", &arg.required_unless_all),
830 ] {
831 let keys = resolve_relationship(names, owner, commands);
832 if !keys.is_empty() {
833 fields.push(format!("{label}: {}", key_slice(&keys)));
834 }
835 }
836 for (label, conditions) in [
837 ("RequiredIfEq", &arg.required_if_eq),
838 ("RequiredIfEqAll", &arg.required_if_eq_all),
839 ] {
840 let values = conditions
841 .iter()
842 .filter_map(|condition| {
843 resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
844 .into_iter()
845 .next()
846 .map(|key| format!("{{Key: {key}, Value: {}}}", go_string(&condition.value)))
847 })
848 .collect::<Vec<_>>();
849 if !values.is_empty() {
850 fields.push(format!(
851 "{label}: []argv.ValueCondition{{{}}}",
852 values.join(", ")
853 ));
854 }
855 }
856 format!("{{{}}}", fields.join(", "))
860}
861
862fn resolve_relationship(names: &[String], owner: &Emitted, commands: &[Emitted]) -> Vec<String> {
873 let mut out = Vec::new();
874 for name in names {
875 let mut found = match_flag(owner, name, false);
879 if found.is_none() && !name.starts_with('-') {
880 found = owner
881 .args
882 .iter()
883 .chain(owner.clause_args.iter())
884 .find(|(arg, _)| arg.name == *name)
885 .map(|(_, named)| named.key.clone());
886 }
887 if found.is_none() {
888 let path = &owner.cmd.full_cmd;
889 for depth in (0..path.len()).rev() {
890 let ancestor = commands
891 .iter()
892 .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]);
893 if let Some(key) = ancestor.and_then(|a| match_flag(a, name, true)) {
894 found = Some(key);
895 break;
896 }
897 }
898 }
899 if let Some(key) = found {
900 out.push(key);
901 }
902 }
903 out
904}
905
906fn match_flag(cmd: &Emitted, name: &str, globals_only: bool) -> Option<String> {
913 let eligible = |flag: &SpecFlag| !globals_only || flag.global;
923
924 let (long, short, bare) = if let Some(rest) = name.strip_prefix("--") {
927 (Some(rest), None, None)
928 } else if let Some(rest) = name.strip_prefix('-') {
929 let mut chars = rest.chars();
930 match (chars.next(), chars.next()) {
931 (Some(c), None) => (None, Some(c), None),
932 _ => (None, None, None),
933 }
934 } else {
935 (None, None, Some(name))
936 };
937
938 let ordinary = cmd.flags.iter().chain(&cmd.clause_flags).find(|(flag, _)| {
939 if !eligible(flag) {
940 return false;
941 }
942 if let Some(bare) = bare {
943 return flag.name == bare;
944 }
945 if let Some(long) = long {
946 return flag.long.iter().any(|l| l == long);
947 }
948 short.is_some_and(|c| flag.short.contains(&c))
949 });
950 if let Some((_, named)) = ordinary {
951 return Some(named.key.clone());
952 }
953
954 cmd.flags
958 .iter()
959 .chain(&cmd.clause_flags)
960 .find(|(flag, _)| eligible(flag) && flag.negate.as_deref() == Some(name))
961 .map(|(_, named)| named.key.clone())
962}
963fn string_slice(values: &[String]) -> String {
964 let list = values
965 .iter()
966 .map(|v| go_string(v))
967 .collect::<Vec<_>>()
968 .join(", ");
969 format!("[]string{{{list}}}")
970}
971
972fn accepted_choices(choices: &SpecChoices) -> Vec<String> {
973 choices
974 .choices
975 .iter()
976 .chain(
977 choices
978 .details
979 .iter()
980 .flat_map(|choice| choice.aliases.iter().map(|alias| &alias.value)),
981 )
982 .cloned()
983 .collect()
984}
985
986fn visible_choices(choices: &SpecChoices) -> Vec<String> {
987 choices
988 .choices
989 .iter()
990 .filter(|value| {
991 !choices
992 .details
993 .iter()
994 .any(|choice| choice.value == value.as_str() && choice.hide)
995 })
996 .chain(choices.details.iter().flat_map(|choice| {
997 choice
998 .aliases
999 .iter()
1000 .filter(|alias| !alias.hide)
1001 .map(|alias| &alias.value)
1002 }))
1003 .cloned()
1004 .collect()
1005}
1006
1007fn key_slice(keys: &[String]) -> String {
1008 format!("[]uint64{{{}}}", keys.join(", "))
1009}
1010
1011impl Emitter<'_> {
1012 fn help_table(&mut self, commands: &[Emitted]) {
1019 let mut by_key: BTreeMap<u64, String> = BTreeMap::new();
1020 for e in commands {
1021 by_key.insert(e.named.number, command_help(e));
1022 for (flag, named) in e.flags.iter().chain(&e.clause_flags) {
1023 by_key.insert(named.number, flag_help(flag, named));
1024 }
1025 for (arg, named) in &e.args {
1026 by_key.insert(named.number, arg_help(arg, named));
1027 }
1028 for (arg, named) in &e.clause_args {
1029 by_key.insert(named.number, arg_help(arg, named));
1030 }
1031 }
1032
1033 let total = commands
1034 .iter()
1035 .map(|e| 1 + e.flags.len() + e.clause_flags.len() + e.args.len() + e.clause_args.len())
1036 .sum::<usize>() as u64;
1037
1038 let _ = writeln!(
1039 self.out,
1040 "// HelpText is the third table, read only when a page is rendered. Neither the\n\
1041 // parser nor the post-binding rules touch it, and a CLI that never prints help\n\
1042 // does not carry it: Go's linker drops an unreferenced table whole.\n\
1043 //\n\
1044 // Indexed by key, like the others.\n\
1045 var HelpText = argv.HelpTable{{"
1046 );
1047 for key in 1..=total {
1048 match by_key.get(&key) {
1049 Some(entry) => {
1050 let _ = writeln!(self.out, "\t{entry},");
1051 }
1052 None => {
1053 let _ = writeln!(self.out, "\t{{}},");
1054 }
1055 }
1056 }
1057 let _ = writeln!(self.out, "}}\n");
1058
1059 let mut fields = vec![
1060 format!("Name: {}", go_string(&self.spec.name)),
1061 format!("Bin: {}", go_string(&self.spec.bin)),
1062 ];
1063 if let Some(version) = self
1064 .spec
1065 .version
1066 .as_ref()
1067 .or(self.spec.long_version.as_ref())
1068 {
1069 fields.push(format!("Version: {}", go_string(version)));
1070 }
1071 if let Some(version) = &self.spec.long_version {
1072 fields.push(format!("LongVersion: {}", go_string(version)));
1073 }
1074 if let Some(about) = &self.spec.about {
1078 fields.push(format!("About: {}", go_string(about)));
1079 }
1080 if let Some(long) = &self.spec.about_long {
1081 fields.push(format!("LongAbout: {}", go_string(long)));
1082 }
1083 if let Some(author) = &self.spec.author {
1084 fields.push(format!("Author: {}", go_string(author)));
1085 }
1086 if let Some(license) = &self.spec.license {
1087 fields.push(format!("License: {}", go_string(license)));
1088 }
1089 if let Some(before) = &self.spec.before_help {
1090 fields.push(format!("BeforeHelp: {}", go_string(before)));
1091 }
1092 if let Some(after) = &self.spec.after_help {
1093 fields.push(format!("AfterHelp: {}", go_string(after)));
1094 }
1095 if let Some(before) = &self.spec.before_help_long {
1096 fields.push(format!("BeforeLongHelp: {}", go_string(before)));
1097 }
1098 if let Some(after) = &self.spec.after_help_long {
1099 fields.push(format!("AfterLongHelp: {}", go_string(after)));
1100 }
1101 if let Some(template) = &self.spec.help_template {
1103 fields.push(format!("HelpTemplate: {}", go_string(template)));
1104 }
1105 let _ = writeln!(
1106 self.out,
1107 "// HelpMeta is what a page needs from the spec's root rather than from any one\n\
1108 // command: the header, and the text that brackets every page.\n\
1109 var HelpMeta = argv.HelpSpec{{{}}}\n",
1110 fields.join(", ")
1111 );
1112 }
1113}
1114
1115fn command_help(e: &Emitted) -> String {
1117 let mut fields = vec![format!("Key: {}", e.named.key)];
1118 if e.cmd.hide {
1119 fields.push("Hide: true".to_string());
1120 }
1121 if let Some(heading) = &e.cmd.help_heading {
1122 fields.push(format!("Heading: {}", go_string(heading)));
1123 }
1124 if let Some(order) = e.cmd.display_order {
1125 fields.push(format!("DisplayOrder: {order}"));
1126 fields.push("DisplayOrderSet: true".to_string());
1127 }
1128 if let Some(help) = e.cmd.help.as_deref().or(e.cmd.help_long.as_deref()) {
1129 fields.push(format!("Short: {}", go_string(help)));
1130 }
1131 if let Some(long) = &e.cmd.help_long {
1132 fields.push(format!("Long: {}", go_string(long)));
1133 }
1134 if let Some(message) = &e.cmd.deprecated {
1135 fields.push(format!("Deprecated: {}", go_string(message)));
1136 }
1137 if let Some(at) = &e.cmd.deprecated_warn_at {
1138 fields.push(format!("DeprecatedWarnAt: {}", go_string(at)));
1139 }
1140 if let Some(at) = &e.cmd.deprecated_remove_at {
1141 fields.push(format!("DeprecatedRemoveAt: {}", go_string(at)));
1142 }
1143 if let Some(heading) = &e.cmd.subcommand_help_heading {
1144 fields.push(format!("SubcommandHelpHeading: {}", go_string(heading)));
1145 }
1146 if let Some(name) = &e.cmd.subcommand_value_name {
1147 fields.push(format!("SubcommandValueName: {}", go_string(name)));
1148 }
1149 if e.cmd.next_line_help {
1150 fields.push("NextLineHelp: true".to_string());
1151 }
1152 if e.cmd.flatten_help {
1153 fields.push("FlattenHelp: true".to_string());
1154 }
1155 if e.cmd.subcommand_required {
1156 fields.push("SubcommandRequired: true".to_string());
1157 }
1158 let visible: Vec<String> = e
1161 .cmd
1162 .aliases
1163 .iter()
1164 .filter(|a| !e.cmd.hidden_aliases.contains(a))
1165 .cloned()
1166 .collect();
1167 if !visible.is_empty() {
1168 fields.push(format!("VisibleAliases: {}", string_slice(&visible)));
1169 }
1170 if let Some(before) = &e.cmd.before_help {
1171 fields.push(format!("BeforeHelp: {}", go_string(before)));
1172 }
1173 if let Some(after) = &e.cmd.after_help {
1174 fields.push(format!("AfterHelp: {}", go_string(after)));
1175 }
1176 if let Some(before) = &e.cmd.before_help_long {
1180 fields.push(format!("BeforeLongHelp: {}", go_string(before)));
1181 }
1182 if let Some(after) = &e.cmd.after_help_long {
1183 fields.push(format!("AfterLongHelp: {}", go_string(after)));
1184 }
1185 if !e.cmd.examples.is_empty() {
1186 let items = e
1187 .cmd
1188 .examples
1189 .iter()
1190 .map(|x| {
1191 let mut parts = Vec::new();
1192 if let Some(header) = &x.header {
1193 parts.push(format!("Header: {}", go_string(header)));
1194 }
1195 parts.push(format!("Code: {}", go_string(&x.code)));
1196 if let Some(help) = &x.help {
1200 parts.push(format!("Help: {}", go_string(help)));
1201 }
1202 format!("{{{}}}", parts.join(", "))
1203 })
1204 .collect::<Vec<_>>()
1205 .join(", ");
1206 fields.push(format!("Examples: []argv.Example{{{items}}}"));
1207 }
1208 if !e.cmd.headings.is_empty() {
1212 let items = e
1213 .cmd
1214 .headings
1215 .iter()
1216 .map(|heading| {
1217 format!(
1218 "{{Title: {}, Help: {}}}",
1219 go_string(&heading.title),
1220 go_string(&heading.help)
1221 )
1222 })
1223 .collect::<Vec<_>>()
1224 .join(", ");
1225 fields.push(format!("Headings: []argv.Heading{{{items}}}"));
1226 }
1227 format!("{{{}}}", fields.join(", "))
1228}
1229
1230fn flag_help(flag: &SpecFlag, named: &Named) -> String {
1231 let mut fields = vec![format!("Key: {}", named.key)];
1232 if let Some(message) = &flag.deprecated {
1233 fields.push(format!("Deprecated: {}", go_string(message)));
1234 }
1235 if let Some(at) = &flag.deprecated_warn_at {
1236 fields.push(format!("DeprecatedWarnAt: {}", go_string(at)));
1237 }
1238 if let Some(at) = &flag.deprecated_remove_at {
1239 fields.push(format!("DeprecatedRemoveAt: {}", go_string(at)));
1240 }
1241 if flag.hide {
1242 fields.push("Hide: true".to_string());
1243 }
1244 if let Some(order) = flag.display_order {
1245 fields.push(format!("DisplayOrder: {order}"));
1246 fields.push("DisplayOrderSet: true".to_string());
1247 }
1248 for (name, hidden) in [
1249 ("HideDefaultValue", flag.hide_default_value),
1250 ("HideEnv", flag.hide_env),
1251 ("HideEnvValues", flag.hide_env_values),
1252 ("HidePossibleValues", flag.hide_possible_values),
1253 ("HideShortHelp", flag.hide_short_help),
1254 ("HideLongHelp", flag.hide_long_help),
1255 ] {
1256 if hidden {
1257 fields.push(format!("{name}: true"));
1258 }
1259 }
1260 if flag.required && flag.default.is_empty() {
1263 fields.push("Demanded: true".to_string());
1264 }
1265 if flag.var {
1266 fields.push("Repeatable: true".to_string());
1267 }
1268 if let Some(arg) = &flag.arg {
1269 if arg.name != flag.name {
1270 fields.push(format!("ValueName: {}", go_string(&arg.name)));
1271 }
1272 if arg.required && arg.default.is_empty() {
1276 fields.push("ValueDemanded: true".to_string());
1277 }
1278 if !arg.value_names.is_empty() {
1279 fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1280 }
1281 if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1282 fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1283 }
1284 }
1285 if let Some(help) = flag.help.as_deref().or(flag.help_first_line.as_deref()) {
1288 fields.push(format!("Short: {}", go_string(help)));
1289 }
1290 if let Some(long) = flag.help_long.as_deref().or(flag.help.as_deref()) {
1291 fields.push(format!("Long: {}", go_string(long)));
1292 }
1293 if let Some(heading) = &flag.help_heading {
1294 fields.push(format!("Heading: {}", go_string(heading)));
1295 }
1296 if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) {
1298 fields.push(format!(
1299 "Choices: {}",
1300 string_slice(&visible_choices(choices))
1301 ));
1302 }
1303 if let Some(env) = &flag.env {
1304 fields.push(format!("Env: {}", go_string(env)));
1305 }
1306 if !flag.env_fallback.is_empty() {
1307 fields.push(format!("EnvFallback: {}", string_slice(&flag.env_fallback)));
1308 }
1309 if !flag.deprecated_env.is_empty() {
1310 fields.push(format!(
1311 "DeprecatedEnv: {}",
1312 string_slice(&flag.deprecated_env)
1313 ));
1314 }
1315 let default = if !flag.default.is_empty() {
1316 &flag.default
1317 } else {
1318 flag.arg
1319 .as_ref()
1320 .map(|a| &a.default)
1321 .unwrap_or(&flag.default)
1322 };
1323 if !default.is_empty() {
1324 fields.push(format!("Default: {}", string_slice(default)));
1325 }
1326 format!("{{{}}}", fields.join(", "))
1327}
1328
1329fn arg_help(arg: &SpecArg, named: &Named) -> String {
1330 let mut fields = vec![format!("Key: {}", named.key)];
1331 if let Some(order) = arg.display_order {
1332 fields.push(format!("DisplayOrder: {order}"));
1333 fields.push("DisplayOrderSet: true".to_string());
1334 }
1335 if arg.hide {
1336 fields.push("Hide: true".to_string());
1337 }
1338 for (name, hidden) in [
1339 ("HideDefaultValue", arg.hide_default_value),
1340 ("HideEnv", arg.hide_env),
1341 ("HideEnvValues", arg.hide_env_values),
1342 ("HidePossibleValues", arg.hide_possible_values),
1343 ("HideShortHelp", arg.hide_short_help),
1344 ("HideLongHelp", arg.hide_long_help),
1345 ] {
1346 if hidden {
1347 fields.push(format!("{name}: true"));
1348 }
1349 }
1350 if arg.required && arg.default.is_empty() {
1351 fields.push("Demanded: true".to_string());
1352 }
1353 if !arg.value_names.is_empty() {
1354 fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1355 }
1356 if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1357 fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1358 }
1359 if let Some(help) = arg.help.as_deref().or(arg.help_first_line.as_deref()) {
1360 fields.push(format!("Short: {}", go_string(help)));
1361 }
1362 if let Some(long) = arg.help_long.as_deref().or(arg.help.as_deref()) {
1363 fields.push(format!("Long: {}", go_string(long)));
1364 }
1365 if let Some(heading) = &arg.help_heading {
1366 fields.push(format!("Heading: {}", go_string(heading)));
1367 }
1368 if let Some(choices) = &arg.choices {
1369 fields.push(format!(
1370 "Choices: {}",
1371 string_slice(&visible_choices(choices))
1372 ));
1373 }
1374 if let Some(env) = &arg.env {
1375 fields.push(format!("Env: {}", go_string(env)));
1376 }
1377 if !arg.env_fallback.is_empty() {
1378 fields.push(format!("EnvFallback: {}", string_slice(&arg.env_fallback)));
1379 }
1380 if !arg.deprecated_env.is_empty() {
1381 fields.push(format!(
1382 "DeprecatedEnv: {}",
1383 string_slice(&arg.deprecated_env)
1384 ));
1385 }
1386 if !arg.default.is_empty() {
1387 fields.push(format!("Default: {}", string_slice(&arg.default)));
1388 }
1389 format!("{{{}}}", fields.join(", "))
1390}
1391
1392enum Line {
1401 Field(String, String),
1403 Block(Vec<String>),
1405}
1406
1407fn render(out: &mut String, indent: &str, lines: &[Line]) {
1409 let mut run: Vec<(&String, &String)> = Vec::new();
1410
1411 fn flush(out: &mut String, indent: &str, run: &mut Vec<(&String, &String)>) {
1412 let width = run.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
1413 for (key, value) in run.iter() {
1414 let _ = writeln!(
1415 out,
1416 "{indent}{key}:{:width$} {value},",
1417 "",
1418 width = width - key.len()
1419 );
1420 }
1421 run.clear();
1422 }
1423
1424 for line in lines {
1425 match line {
1426 Line::Field(key, value) => run.push((key, value)),
1427 Line::Block(block) => {
1428 flush(out, indent, &mut run);
1429 for l in block {
1430 let _ = writeln!(out, "{indent}{l}");
1431 }
1432 }
1433 }
1434 }
1435 flush(out, indent, &mut run);
1436}
1437
1438struct Emitted {
1440 named: Named,
1441 cmd: SpecCommand,
1442 flags: Vec<(SpecFlag, Named)>,
1443 args: Vec<(SpecArg, Named)>,
1444 clause_flags: Vec<(SpecFlag, Named)>,
1445 clause_args: Vec<(SpecArg, Named)>,
1446 subcommands: Vec<usize>,
1448 root: bool,
1449}
1450
1451fn effective_unknown_flags(spec: &Spec, commands: &[Emitted], at: usize) -> UnknownFlags {
1458 let path = &commands[at].cmd.full_cmd;
1459 for depth in (0..=path.len()).rev() {
1460 let ancestor = commands
1461 .iter()
1462 .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]);
1463 if let Some(mode) = ancestor.and_then(|e| e.cmd.unknown_flags) {
1464 return mode;
1465 }
1466 }
1467 spec.unknown_flags.unwrap_or_default()
1468}
1469
1470fn flag_literal(flag: &SpecFlag, named: &Named) -> String {
1471 let mut fields = vec![
1472 format!("Key: {}", named.key),
1473 format!("Name: {}", go_string(&flag.name)),
1474 ];
1475 if !flag.long.is_empty() {
1476 let longs = flag
1477 .long
1478 .iter()
1479 .map(|l| go_string(l))
1480 .collect::<Vec<_>>()
1481 .join(", ");
1482 fields.push(format!("Longs: []string{{{longs}}}"));
1483 }
1484 if !flag.hidden_aliases.is_empty() {
1485 fields.push(format!(
1486 "HiddenLongs: {}",
1487 string_slice(&flag.hidden_aliases)
1488 ));
1489 }
1490 if !flag.short.is_empty() {
1491 let shorts = flag
1492 .short
1493 .iter()
1494 .map(|c| go_byte(*c))
1495 .collect::<Vec<_>>()
1496 .join(", ");
1497 fields.push(format!("Shorts: []byte{{{shorts}}}"));
1498 }
1499 if !flag.hidden_short_aliases.is_empty() {
1500 let shorts = flag
1501 .hidden_short_aliases
1502 .iter()
1503 .map(|c| go_byte(*c))
1504 .collect::<Vec<_>>()
1505 .join(", ");
1506 fields.push(format!("HiddenShorts: []byte{{{shorts}}}"));
1507 }
1508 if let Some(negate) = &flag.negate {
1509 fields.push(format!(
1512 "Negate: {}",
1513 go_string(negate.trim_start_matches('-'))
1514 ));
1515 }
1516 if flag.arg.is_some() {
1517 fields.push("TakesValue: true".to_string());
1518 }
1519 if flag.value_optional {
1520 fields.push("ValueOptional: true".to_string());
1521 }
1522 if flag.bool_value {
1523 fields.push("BoolValue: true".to_string());
1524 }
1525 let action = match flag.action {
1526 SpecFlagAction::Set => None,
1527 SpecFlagAction::Help => Some("argv.ActionHelp"),
1528 SpecFlagAction::HelpShort => Some("argv.ActionHelpShort"),
1529 SpecFlagAction::HelpLong => Some("argv.ActionHelpLong"),
1530 SpecFlagAction::HelpAll => Some("argv.ActionHelpAll"),
1531 SpecFlagAction::Version => Some("argv.ActionVersion"),
1532 };
1533 if let Some(action) = action {
1534 fields.push(format!("Action: {action}"));
1535 }
1536 if let Some(arg) = flag.arg.as_ref().filter(|a| a.var) {
1541 fields.push("Variadic: true".to_string());
1542 if let Some(max) = arg.var_max {
1543 fields.push(format!("VarMax: {}", clamp_var_max(max)));
1544 }
1545 }
1546 if flag.allow_hyphen_values() {
1547 fields.push("AllowHyphenValues: true".to_string());
1548 }
1549 if let Some(arg) = &flag.arg {
1550 if arg.allow_negative_numbers {
1551 fields.push("AllowNegativeNumbers: true".to_string());
1552 }
1553 if let Some(terminator) = &arg.value_terminator {
1554 fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1555 }
1556 if let Some(delimiter) = arg.delimiter {
1557 fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1558 }
1559 }
1560 if flag.require_equals {
1561 fields.push("RequireEquals: true".to_string());
1562 }
1563 if let Some(missing) = &flag.default_missing {
1564 fields.push(format!("DefaultMissing: {}", go_string(missing)));
1565 }
1566 if flag.global {
1567 fields.push("Global: true".to_string());
1568 }
1569 format!("{{{}}}", fields.join(", "))
1570}
1571
1572fn arg_literal(arg: &SpecArg, named: &Named) -> String {
1573 let mut fields = vec![
1574 format!("Key: {}", named.key),
1575 format!("Name: {}", go_string(&arg.name)),
1576 ];
1577 if let Some(sigil) = &arg.sigil {
1578 fields.push(format!("Sigil: {}", go_string(sigil)));
1579 }
1580 if arg.required {
1581 fields.push("Required: true".to_string());
1582 }
1583 if arg.var {
1584 fields.push("Var: true".to_string());
1585 if let Some(max) = arg.var_max {
1586 fields.push(format!("VarMax: {}", clamp_var_max(max)));
1587 }
1588 }
1589 if arg.allow_negative_numbers {
1590 fields.push("AllowNegativeNumbers: true".to_string());
1591 }
1592 if let Some(terminator) = &arg.value_terminator {
1593 fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1594 }
1595 if let Some(delimiter) = arg.delimiter {
1596 fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1597 }
1598 let double_dash = match arg.double_dash {
1599 SpecDoubleDashChoices::Required => Some("argv.DoubleDashRequired"),
1600 SpecDoubleDashChoices::Preserve => Some("argv.DoubleDashPreserve"),
1601 SpecDoubleDashChoices::Automatic => Some("argv.DoubleDashAutomatic"),
1602 _ => None,
1603 };
1604 if let Some(dd) = double_dash {
1605 fields.push(format!("DoubleDash: {dd}"));
1606 }
1607 format!("{{{}}}", fields.join(", "))
1608}
1609
1610fn clamp_var_max(max: usize) -> u32 {
1615 u32::try_from(max).unwrap_or(u32::MAX)
1616}
1617
1618const GO_KEYWORDS: &[&str] = &[
1623 "break",
1624 "case",
1625 "chan",
1626 "const",
1627 "continue",
1628 "default",
1629 "defer",
1630 "else",
1631 "fallthrough",
1632 "for",
1633 "func",
1634 "go",
1635 "goto",
1636 "if",
1637 "import",
1638 "interface",
1639 "map",
1640 "package",
1641 "range",
1642 "return",
1643 "select",
1644 "struct",
1645 "switch",
1646 "type",
1647 "var",
1648];
1649
1650const UNUSABLE_PACKAGE_NAMES: &[&str] = &["_", "init"];
1663
1664pub fn is_valid_package(name: &str) -> bool {
1669 !name.is_empty()
1670 && !GO_KEYWORDS.contains(&name)
1671 && !UNUSABLE_PACKAGE_NAMES.contains(&name)
1672 && !name.starts_with(|c: char| c.is_ascii_digit())
1673 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1674}
1675
1676fn field_name(name: &str) -> String {
1678 let ident = format!("{}", AsPascalCase(name));
1679 if ident.is_empty() || ident.starts_with(|c: char| c.is_ascii_digit()) {
1680 format!("X{ident}")
1681 } else {
1682 ident
1683 }
1684}
1685
1686fn package_ident(bin: &str) -> String {
1694 let lowered: String = bin
1695 .chars()
1696 .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
1697 .collect::<String>()
1698 .to_ascii_lowercase();
1699 if is_valid_package(&lowered) {
1700 lowered
1701 } else {
1702 format!("cli{lowered}")
1706 }
1707}
1708
1709fn go_string(s: &str) -> String {
1714 let mut out = String::with_capacity(s.len() + 2);
1715 out.push('"');
1716 for c in s.chars() {
1717 match c {
1718 '"' => out.push_str("\\\""),
1719 '\\' => out.push_str("\\\\"),
1720 '\n' => out.push_str("\\n"),
1721 '\r' => out.push_str("\\r"),
1722 '\t' => out.push_str("\\t"),
1723 c if (c as u32) < 0x20 || c as u32 == 0x7f => {
1724 let _ = write!(out, "\\x{:02x}", c as u32);
1725 }
1726 c => out.push(c),
1727 }
1728 }
1729 out.push('"');
1730 out
1731}
1732
1733fn go_byte(c: char) -> String {
1739 match c {
1740 '\'' => "'\\''".to_string(),
1741 '\\' => "'\\\\'".to_string(),
1742 c if c.is_ascii_graphic() => format!("'{c}'"),
1743 c => format!("0x{:02x}", (c as u32) & 0xff),
1744 }
1745}
1746
1747#[cfg(test)]
1748mod tests {
1749 use super::*;
1750
1751 fn entry_of(out: &str, name: &str) -> String {
1760 out.lines()
1761 .find(|l| l.contains(&format!("Name: \"{name}\", Flag: true")))
1762 .unwrap_or_default()
1763 .to_string()
1764 }
1765
1766 fn go(kdl: &str) -> String {
1767 let spec: Spec = kdl.parse().expect("the fixture spec should parse");
1768 generate(&spec, &GoOptions::default())
1769 }
1770
1771 #[test]
1772 fn a_whole_cli() {
1773 let out = go(r#"
1774name "ex"
1775bin "ex"
1776version "1.2.3"
1777long_version "1.2.3\ncommit abc123"
1778flag "-v --verbose" global=#true help="be loud"
1779flag "--color" negate="--no-color"
1780flag "-j --jobs <n>"
1781flag "--include <pattern>..." var_max=3
1782arg "<file>"
1783arg "[rest]..." var=#true
1784cmd "install" {
1785 alias "i"
1786 flag "-f --force"
1787 arg "<pkg>"
1788}
1789cmd "config" {
1790 cmd "ls" {
1791 flag "--no-header"
1792 }
1793}
1794"#);
1795 insta::assert_snapshot!(out);
1796 }
1797
1798 #[test]
1799 fn rich_choices_keep_acceptance_visibility_and_strictness_separate() {
1800 let out = go(r#"
1801name "ex"
1802bin "ex"
1803flag "--color <when>" {
1804 choices ignore_case=#true strict=#false {
1805 choice "always" {
1806 alias "yes"
1807 alias "on" hide=#true
1808 }
1809 choice "never" hide=#true
1810 }
1811}
1812"#);
1813 let entry = entry_of(&out, "color");
1814 assert!(
1815 entry.contains(r#"Choices: []string{"always", "yes"}"#),
1816 "{entry}"
1817 );
1818 assert!(
1819 entry.contains(r#"AcceptedChoices: []string{"always", "never", "yes", "on"}"#),
1820 "{entry}"
1821 );
1822 assert!(entry.contains("IgnoreCase: true"), "{entry}");
1823 assert!(entry.contains("AllowUnknownChoices: true"), "{entry}");
1824 }
1825
1826 #[test]
1828 fn unknown_flags_are_inherited_and_overridable() {
1829 let out = go(r#"
1830name "ex"
1831bin "ex"
1832unknown_flags "error"
1833cmd "strict" {
1834 cmd "deep" {}
1835}
1836cmd "exec" unknown_flags="value" {
1837 cmd "nested" {}
1838}
1839"#);
1840 insta::assert_snapshot!(out);
1841 }
1842
1843 #[test]
1846 fn colliding_names_get_distinct_identifiers() {
1847 let out = go(r#"
1848name "ex"
1849bin "ex"
1850cmd "macos-defaults" {
1851 flag "--apply"
1852}
1853cmd "macos" {
1854 cmd "defaults" {
1855 flag "--apply"
1856 }
1857}
1858"#);
1859 insta::assert_snapshot!(out);
1860 }
1861
1862 #[test]
1863 fn a_default_subcommand_points_into_the_tree() {
1864 let out = go(r#"
1865name "ex"
1866bin "ex"
1867default_subcommand "run"
1868arg "[task]"
1869cmd "run" {
1870 arg "[args]..." var=#true
1871}
1872"#);
1873 insta::assert_snapshot!(out);
1874 }
1875
1876 #[test]
1877 fn a_bin_name_that_is_not_an_identifier_still_gives_a_package() {
1878 assert_eq!(package_ident("my-cli"), "mycli");
1879 assert_eq!(package_ident("7zip"), "cli7zip");
1880 assert_eq!(package_ident(""), "cli");
1881 assert_eq!(package_ident("go"), "cligo");
1883 assert_eq!(package_ident("type"), "clitype");
1884 assert_eq!(package_ident("_"), "cli_");
1887 assert_eq!(package_ident("init"), "cliinit");
1888 assert_eq!(package_ident("__"), "__");
1890 assert_eq!(package_ident("initialize"), "initialize");
1891
1892 for bin in [
1896 "my-cli", "7zip", "", "go", "type", "_", "init", "__", "MiSe",
1897 ] {
1898 let out = package_ident(bin);
1899 assert!(is_valid_package(&out), "{bin:?} sanitized to {out:?}");
1900 }
1901 }
1902
1903 #[test]
1905 fn a_name_matching_a_generated_suffix_still_gets_its_own() {
1906 let out = go(r#"
1907name "ex"
1908bin "ex"
1909cmd "macos-defaults" {}
1910cmd "macos" {
1911 cmd "defaults" {}
1912}
1913cmd "macos-defaults2" {}
1914"#);
1915 assert_declares_each_constant_once(&out);
1920 }
1921
1922 fn constant_names(out: &str) -> Vec<&str> {
1924 out.lines()
1925 .skip_while(|l| !l.starts_with("const ("))
1926 .skip(1)
1927 .take_while(|l| !l.starts_with(')'))
1928 .filter_map(|l| l.split_whitespace().next())
1929 .collect()
1930 }
1931
1932 fn assert_declares_each_constant_once(out: &str) {
1934 let names = constant_names(out);
1935 assert!(!names.is_empty(), "no constants at all:\n{out}");
1936 let mut seen = std::collections::HashSet::new();
1937 for name in &names {
1938 assert!(seen.insert(*name), "{name} is declared twice:\n{out}");
1939 }
1940 }
1941
1942 #[test]
1943 fn a_package_that_would_not_compile_is_refused_rather_than_emitted() {
1944 assert!(is_valid_package("mycli"));
1945 assert!(is_valid_package("mise_tables"));
1946 assert!(!is_valid_package("my-pkg"));
1947 assert!(!is_valid_package("7zip"));
1948 assert!(!is_valid_package(""));
1949 assert!(!is_valid_package("range"));
1950 assert!(!is_valid_package("_"));
1951 assert!(!is_valid_package("init"));
1952 assert!(is_valid_package("__"));
1953
1954 let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
1956 let out = generate(
1957 &spec,
1958 &GoOptions {
1959 package: Some("my-pkg".into()),
1960 },
1961 );
1962 assert!(out.contains("package mypkg"), "{out}");
1963 }
1964
1965 #[test]
1979 fn the_long_pages_text_reaches_the_tables() {
1980 let out = go(r#"
1981name "ex"
1982bin "ex"
1983about "Short."
1984about_long "Long."
1985before_long_help "ROOT-BEFORE"
1986after_long_help "ROOT-AFTER"
1987cmd "run" help="Run it" {
1988 before_long_help "RUN-BEFORE"
1989 after_long_help "RUN-AFTER"
1990}
1991"#);
1992 let meta = out
1993 .lines()
1994 .find(|l| l.contains("var HelpMeta"))
1995 .expect("a root header is emitted");
1996 assert!(
1997 meta.contains(r#"About: "Short.""#) && meta.contains(r#"LongAbout: "Long.""#),
1998 "the two abouts are separate fields: {meta}"
1999 );
2000 assert!(
2001 meta.contains(r#"BeforeLongHelp: "ROOT-BEFORE""#)
2002 && meta.contains(r#"AfterLongHelp: "ROOT-AFTER""#),
2003 "the root's long brackets are emitted: {meta}"
2004 );
2005
2006 let run = out
2007 .lines()
2008 .find(|l| l.contains("Short: \"Run it\""))
2009 .expect("the command has a help entry");
2010 assert!(
2011 run.contains(r#"BeforeLongHelp: "RUN-BEFORE""#)
2012 && run.contains(r#"AfterLongHelp: "RUN-AFTER""#),
2013 "a command's long brackets are emitted: {run}"
2014 );
2015 }
2016
2017 #[test]
2025 fn an_examples_help_line_reaches_the_tables() {
2026 let out = go(r#"
2027name "ex"
2028bin "ex"
2029cmd "run" help="Run it" {
2030 example "ex run --fast" header="Speed" help="When you are in a hurry"
2031 example "ex run"
2032}
2033"#);
2034 let run = out
2035 .lines()
2036 .find(|l| l.contains("Examples: []argv.Example"))
2037 .expect("the command's examples are emitted");
2038 assert!(
2039 run.contains(
2040 r#"{Header: "Speed", Code: "ex run --fast", Help: "When you are in a hurry"}"#
2041 ),
2042 "all three fields are emitted: {run}"
2043 );
2044 assert!(
2046 run.contains(r#"{Code: "ex run"}"#),
2047 "an example with no header emits no header: {run}"
2048 );
2049 }
2050
2051 #[test]
2057 fn heading_prose_reaches_the_tables() {
2058 let out = go(r#"
2059name "ex"
2060bin "ex"
2061cmd "run" help="Run it" {
2062 heading "Filters" help="Filters accumulate from left to right."
2063 flag "--allow <NAME>" help="Allow it" help_heading="Filters"
2064}
2065"#);
2066 let run = out
2067 .lines()
2068 .find(|l| l.contains("Headings: []argv.Heading"))
2069 .expect("the command's headings are emitted");
2070 assert!(
2071 run.contains(r#"{Title: "Filters", Help: "Filters accumulate from left to right."}"#),
2072 "both fields are emitted: {run}"
2073 );
2074 }
2075
2076 #[test]
2079 fn a_long_about_alone_does_not_become_the_short_one() {
2080 let out = go(r#"
2081name "ex"
2082bin "ex"
2083about_long "Long only."
2084"#);
2085 let meta = out
2086 .lines()
2087 .find(|l| l.contains("var HelpMeta"))
2088 .expect("a root header is emitted");
2089 assert!(
2090 !meta.contains(", About: ") && meta.contains(r#"LongAbout: "Long only.""#),
2091 "only the long one is set: {meta}"
2092 );
2093 }
2094
2095 #[test]
2096 fn a_default_subcommand_ignores_a_deeper_command_of_the_same_name() {
2097 let out = go(r#"
2098name "ex"
2099bin "ex"
2100default_subcommand "run"
2101cmd "oci" {
2102 cmd "run" {}
2103}
2104cmd "run" {
2105 arg "[args]..." var=#true
2106}
2107"#);
2108 assert!(
2109 out.contains("DefaultSubcommand: cmdRun,"),
2110 "should point at the root's own `run`, got:\n{out}"
2111 );
2112 }
2113
2114 #[test]
2115 fn command_builtin_controls_reach_generated_go_tables() {
2116 let out = go(r#"
2117name "ex"
2118bin "ex"
2119disable_help_flag #true
2120disable_help_subcommand #true
2121disable_version_flag #true
2122"#);
2123 let root = out
2124 .split("var Root = &argv.Command{")
2125 .nth(1)
2126 .expect("the root command should be emitted")
2127 .split("}\n")
2128 .next()
2129 .unwrap();
2130 assert!(root.contains("DisableHelpFlag:"), "{root}");
2131 assert!(root.contains("DisableHelpSubcommand:"), "{root}");
2132 assert!(root.contains("DisableVersionFlag:"), "{root}");
2133 }
2134
2135 #[test]
2138 fn a_default_subcommand_prefers_a_name_to_another_commands_alias() {
2139 let ordered = |first: &str, second: &str| {
2140 go(&format!(
2141 r#"
2142name "ex"
2143bin "ex"
2144default_subcommand "run"
2145{first}
2146{second}
2147"#
2148 ))
2149 };
2150 let alpha = "cmd \"alpha\" {\n alias \"run\"\n}";
2151 let run = "cmd \"run\" {\n arg \"[args]...\" var=#true\n}";
2152 for out in [ordered(alpha, run), ordered(run, alpha)] {
2153 assert!(
2154 out.contains("DefaultSubcommand: cmdRun,"),
2155 "should point at the command named `run`, got:\n{out}"
2156 );
2157 }
2158 }
2159
2160 #[test]
2161 fn an_external_subcommand_is_emitted_on_the_command_that_declares_it() {
2162 let out = go(r#"
2163name "ex"
2164bin "ex"
2165external_subcommand #true
2166cmd "install"
2167cmd "exec" external_subcommand=#true
2168"#);
2169 let block = |var: &str| {
2170 let start = out
2171 .find(&format!("var {var} ="))
2172 .unwrap_or_else(|| panic!("{var} should be emitted, got:\n{out}"));
2173 let rest = &out[start..];
2174 let end = rest[1..]
2175 .find("\nvar ")
2176 .map(|i| i + 1)
2177 .unwrap_or(rest.len());
2178 &rest[..end]
2179 };
2180 assert!(
2181 block("Root").contains("ExternalSubcommand: true"),
2182 "the root should forward unmatched words:\n{}",
2183 block("Root")
2184 );
2185 assert!(
2186 block("cmdExec").contains("ExternalSubcommand: true"),
2187 "a nested command can forward too:\n{}",
2188 block("cmdExec")
2189 );
2190 assert!(
2191 !block("cmdInstall").contains("ExternalSubcommand"),
2192 "a command that does not declare it should not carry it:\n{}",
2193 block("cmdInstall")
2194 );
2195 }
2196
2197 #[test]
2198 fn arg_required_else_help_reaches_the_table_and_typed_front_door() {
2199 let out = go(r#"
2200name "ex"
2201bin "ex"
2202cmd "run" arg_required_else_help=#true {
2203 flag "--all"
2204}
2205"#);
2206 assert!(
2207 out.contains("ArgRequiredElseHelp: true"),
2208 "the command table should carry the policy:\n{out}"
2209 );
2210 assert!(
2211 out.contains("p.Command().ArgRequiredElseHelp && p.CommandStart() == len(args)"),
2212 "the typed parser should enforce it before fallbacks:\n{out}"
2213 );
2214 }
2215
2216 #[test]
2217 fn subcommand_negates_requirements_reaches_generated_go() {
2218 let out = go(
2219 "name \"ex\"\nbin \"ex\"\nsubcommand_negates_reqs #true\nflag \"--config\" required=#true\ncmd \"run\"\n",
2220 );
2221 assert!(out.contains("SubcommandNegatesReqs: true"), "{out}");
2222 assert!(
2223 out.contains("checkRequirements := i == len(chain)-1 || !cmd.SubcommandNegatesReqs"),
2224 "{out}"
2225 );
2226 assert!(
2227 out.contains("CheckRelationshipsWithValuesAndRequirements"),
2228 "{out}"
2229 );
2230 }
2231
2232 #[test]
2233 fn argument_subcommand_conflicts_reach_generated_go() {
2234 let out = go(
2235 "name \"ex\"\nbin \"ex\"\nargs_conflicts_with_subcommands #true\nflag \"--verbose\"\ncmd \"run\"\n",
2236 );
2237 assert!(out.contains("ArgsConflictWithSubcommands: true"), "{out}");
2238 }
2239
2240 #[test]
2241 fn allow_missing_positional_reaches_generated_go() {
2242 let out = go(
2243 "name \"ex\"\nbin \"ex\"\nallow_missing_positional #true\narg \"[optional]\"\narg \"<required>\"\n",
2244 );
2245 assert!(out.contains("AllowMissingPositional: true"), "{out}");
2246 assert!(out.contains("Name: \"optional\""), "{out}");
2247 assert!(out.contains("Name: \"required\", Required: true"), "{out}");
2248 }
2249
2250 #[test]
2251 fn optional_flag_values_reach_generated_go() {
2252 let out = go("name \"ex\"\nbin \"ex\"\nflag \"--color [WHEN]\" value_optional=#true\n");
2253 assert!(
2254 out.contains("TakesValue: true, ValueOptional: true"),
2255 "{out}"
2256 );
2257 }
2258
2259 #[test]
2260 fn explicit_boolean_values_reach_generated_go() {
2261 let out = go(
2262 "name \"ex\"\nbin \"ex\"\nflag \"--color\" negate=\"--no-color\" bool_value=#true\n",
2263 );
2264 assert!(out.contains("BoolValue: true"), "{out}");
2265 assert!(out.contains("if ev.Flag.BoolValue"), "{out}");
2266 assert!(
2267 out.contains("given[ev.Flag.Key] = []string{ev.Value}"),
2268 "{out}"
2269 );
2270 assert!(
2271 out.contains("(ev.Value == \"true\") != ev.Negated"),
2272 "{out}"
2273 );
2274 }
2275
2276 #[test]
2277 fn flag_actions_reach_generated_go() {
2278 let out = go(
2279 "name \"ex\"\nbin \"ex\"\nflag \"--help-all\" action=\"help_all\"\nflag \"--version\" action=\"version\"\n",
2280 );
2281 assert!(out.contains("Action: argv.ActionHelpAll"), "{out}");
2282 assert!(out.contains("Action: argv.ActionVersion"), "{out}");
2283 }
2284
2285 #[test]
2286 fn granular_help_hides_reach_generated_go() {
2287 let out = go(
2288 "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",
2289 );
2290 for field in [
2291 "HideDefaultValue: true",
2292 "HideEnv: true",
2293 "HideEnvValues: true",
2294 "HidePossibleValues: true",
2295 "HideShortHelp: true",
2296 "HideLongHelp: true",
2297 ] {
2298 assert!(out.contains(field), "missing {field}:\n{out}");
2299 }
2300 }
2301
2302 #[test]
2303 fn strict_duplicate_policy_reaches_metadata() {
2304 let permissive = go("name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\n");
2305 assert!(!permissive.contains("RejectDuplicate"), "{permissive}");
2306
2307 let strict =
2308 go("name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\n");
2309 assert!(strict.contains("RejectDuplicate: true"), "{strict}");
2310 }
2311
2312 #[test]
2313 fn clause_args_and_later_entries_reach_go_cold_tables() {
2314 let out = go(r#"
2315name "ex"
2316bin "ex"
2317cmd "run" {
2318 flag "--needs-task" requires="task"
2319 clause "items" separator=":::" {
2320 flag "--postinstall <command>"
2321 arg "<task>" help="Task to run"
2322 }
2323}
2324cmd "later" {
2325 flag "--mode <mode>" help="Later flag"
2326}
2327"#);
2328 let meta = out
2329 .split_once("var Meta = argv.Metadata{")
2330 .and_then(|(_, rest)| rest.split_once("var HelpText = argv.HelpTable{"))
2331 .map(|(meta, _)| meta)
2332 .expect("generated metadata and help tables");
2333 let help = out
2334 .split_once("var HelpText = argv.HelpTable{")
2335 .and_then(|(_, rest)| rest.split_once("var HelpMeta = argv.HelpSpec{"))
2336 .map(|(help, _)| help)
2337 .expect("generated help table and metadata");
2338
2339 assert!(
2340 meta.contains("Key: ArgRunItemsTask, Name: \"task\""),
2341 "{out}"
2342 );
2343 assert!(
2344 meta.lines().any(|line| {
2345 line.contains("Key: FlagRunItemsPostinstall")
2346 && line.contains("RejectDuplicate: true")
2347 }),
2348 "{out}"
2349 );
2350 assert!(
2351 meta.lines().any(|line| {
2352 line.contains("Key: FlagRunNeedsTask")
2353 && line.contains("Requires: []uint64{ArgRunItemsTask}")
2354 }),
2355 "{out}"
2356 );
2357 assert!(meta.contains("Key: FlagLaterMode, Name: \"mode\""), "{out}");
2358 assert!(
2359 help.contains("Key: ArgRunItemsTask, Demanded: true"),
2360 "{out}"
2361 );
2362 assert!(
2363 help.lines().any(|line| {
2364 line.contains("Key: FlagLaterMode") && line.contains("Short: \"Later flag\"")
2365 }),
2366 "{out}"
2367 );
2368 for generated in [
2369 "type RunCmdItemsClause struct {",
2370 "Postinstall string",
2371 "Flags: []*argv.Flag{",
2372 "clauseFlag := map[uint64]bool{FlagRunItemsPostinstall: true}",
2373 "case argv.KindClauseSeparator:",
2374 "for instanceIndex, instance := range clauseInstances[CmdRun] {",
2375 "if err := argv.Check(Meta.Lookup(key), values, instanceOccurrences[key]); err != nil {",
2376 "clauseSources := map[uint64]argv.Source{FlagRunItemsPostinstall: argv.Unset, ArgRunItemsTask: argv.Unset}",
2377 "cmdRunV.Items = append(cmdRunV.Items, item)",
2378 ] {
2379 assert!(out.contains(generated), "missing {generated:?}:\n{out}");
2380 }
2381 assert!(
2382 out.lines().any(|line| {
2383 line.contains("Items")
2384 && line.contains("[]RunCmdItemsClause")
2385 && line.contains("// clause items")
2386 }),
2387 "{out}"
2388 );
2389 }
2390
2391 #[test]
2392 fn strict_negated_flags_track_each_spelling_separately() {
2393 let out = go(
2394 "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n",
2395 );
2396 assert!(out.contains("polaritySeen := map[uint64]uint8{}"), "{out}");
2397 assert!(
2398 out.contains("polaritySeen[ev.Flag.Key]&polarity != 0"),
2399 "{out}"
2400 );
2401 assert!(out.contains("if duplicateSeen[key]"), "{out}");
2402 }
2403
2404 #[test]
2405 fn strict_global_duplicate_tracking_resets_at_subcommands() {
2406 let out = go(
2407 "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\" global=#true\ncmd \"run\" {\n args_override_self #false\n}\n",
2408 );
2409 assert!(
2410 out.contains("levelSeen = map[uint64]int{}"),
2411 "a subcommand should start a new duplicate scope:\n{out}"
2412 );
2413 assert!(out.contains("strictSeen[ev.Flag.Key] = true"), "{out}");
2414 assert!(out.contains("if strictSeen[key]"), "{out}");
2415 }
2416
2417 #[test]
2419 fn a_subcommand_named_root_does_not_collide_with_the_root() {
2420 let out = go(r#"
2421name "ex"
2422bin "ex"
2423cmd "root" {
2424 flag "--wat"
2425}
2426"#);
2427 let declared = |name: &str| {
2430 out.lines()
2431 .filter(|l| l.split_whitespace().next() == Some(name))
2432 .count()
2433 };
2434 assert_eq!(declared("CmdRoot"), 1, "CmdRoot declared twice:\n{out}");
2435 assert_eq!(declared("CmdRoot2"), 1, "no distinct key for it:\n{out}");
2436 assert_declares_each_constant_once(&out);
2437 }
2438
2439 #[test]
2444 fn only_the_per_occurrence_bound_reaches_the_table() {
2445 let out = go(r#"
2446name "ex"
2447bin "ex"
2448flag "--include <pattern>..." {
2449 arg "<pattern>..." var=#true var_min=2 var_max=2
2450}
2451flag "--tag <t>" var=#true var_max=1
2452"#);
2453 assert!(
2454 out.contains("Name: \"include\", Longs: []string{\"include\"}, TakesValue: true, Variadic: true, VarMax: 2"),
2455 "{out}"
2456 );
2457 assert!(
2458 out.contains("Name: \"include\", Flag: true") && out.contains("VarMin: 2"),
2459 "the nested value minimum must reach post-binding metadata:\n{out}"
2460 );
2461 let tag = out.lines().find(|l| l.contains("\"tag\"")).unwrap();
2462 assert!(!tag.contains("VarMax"), "occurrence bound leaked: {tag}");
2463 }
2464
2465 #[test]
2466 fn exact_arity_with_one_label_reaches_go_help() {
2467 let out = go(r#"
2468name "ex"
2469bin "ex"
2470flag "--pair <ITEM>..." {
2471 arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2472 value_names "ITEM"
2473 }
2474 }
2475arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2476 value_names "ITEM"
2477}
2478"#);
2479 assert_eq!(out.matches("ValueArity: 2").count(), 2, "{out}");
2480 assert_eq!(
2481 out.matches("ValueNames: []string{\"ITEM\"}").count(),
2482 2,
2483 "{out}"
2484 );
2485 }
2486
2487 #[test]
2488 fn scoped_flag_binding_preserves_counts_and_boolean_polarity() {
2489 let out = go(r#"
2490name "ex"
2491bin "ex"
2492clause "tools" {
2493 flag "--required" required=#true
2494 flag "-v --verbose" count=#true
2495 flag "--color" negate="--no-color" bool_value=#true
2496 arg "<tool>"
2497}
2498"#);
2499
2500 for generated in [
2501 "if clauseFlag[f.Key] {",
2502 "instanceOccurrences := clauseInstanceOccurrences[CmdRoot][instanceIndex]",
2503 "argv.Check(Meta.Lookup(key), values, instanceOccurrences[key])",
2504 "item.Required = !instanceNegated[FlagToolsRequired]",
2505 "item.Verbose = instanceOccurrences[FlagToolsVerbose]",
2506 "item.Color = (values[len(values)-1] == \"true\") != instanceNegated[FlagToolsColor]",
2507 "item.Color = !instanceNegated[FlagToolsColor]",
2508 ] {
2509 assert!(out.contains(generated), "missing {generated:?}:\n{out}");
2510 }
2511 }
2512
2513 #[test]
2514 fn a_bare_optional_scoped_flag_does_not_index_an_empty_value() {
2515 let out = go(r#"
2516name "ex"
2517bin "ex"
2518clause "tools" {
2519 flag "--label [LABEL]" value_optional=#true
2520 arg "<tool>"
2521}
2522"#);
2523
2524 assert!(
2525 out.contains(
2526 "if values := instance[FlagToolsLabel]; len(values) > 0 {\n\t\t\t\titem.Label = values[len(values)-1]"
2527 ),
2528 "{out}"
2529 );
2530 assert!(
2531 !out.contains("if values, ok := instance[FlagToolsLabel]; ok"),
2532 "{out}"
2533 );
2534 }
2535
2536 #[test]
2537 fn allow_hyphen_values_reaches_the_table() {
2538 let out = go(r#"
2539name "ex"
2540bin "ex"
2541flag "--args <ARGS>" allow_hyphen_values=#true
2542"#);
2543 assert!(
2544 out.contains("Name: \"args\", Longs: []string{\"args\"}, TakesValue: true, AllowHyphenValues: true"),
2545 "{out}"
2546 );
2547 }
2548
2549 #[test]
2550 fn require_equals_reaches_the_table() {
2551 let out = go(r#"
2552name "ex"
2553bin "ex"
2554flag "--inspect <PORT>" require_equals=#true
2555"#);
2556 assert!(
2557 out.contains("Name: \"inspect\", Longs: []string{\"inspect\"}, TakesValue: true, RequireEquals: true"),
2558 "{out}"
2559 );
2560 }
2561
2562 #[test]
2563 fn default_missing_reaches_the_table() {
2564 let out = go(r#"
2565name "ex"
2566bin "ex"
2567flag "--color <WHEN>" default_missing="always"
2568"#);
2569 assert!(
2570 out.contains(
2571 "Name: \"color\", Longs: []string{\"color\"}, TakesValue: true, DefaultMissing: \"always\""
2572 ),
2573 "{out}"
2574 );
2575 }
2576
2577 #[test]
2583 fn a_relationship_resolves_through_scope_and_negation() {
2584 let out = go(r#"
2585name "ex"
2586bin "ex"
2587flag "--quiet" global=#true
2588flag "--color" negate="--no-color"
2589flag "--plain" conflicts="--no-color"
2590cmd "run" {
2591 flag "--loud" conflicts="--quiet"
2592 flag "--solo" conflicts="--plain"
2593}
2594"#);
2595 assert!(out.contains("Conflicts: []uint64{FlagColor}"), "{out}");
2597 assert!(out.contains("Conflicts: []uint64{FlagQuiet}"), "{out}");
2599 assert!(
2602 !entry_of(&out, "solo").contains("Conflicts"),
2603 "a non-global should not resolve from below:\n{out}"
2604 );
2605 }
2606
2607 #[test]
2608 fn positional_conflicts_reach_go_metadata_in_both_directions() {
2609 let out = go(r#"
2610name "ex"
2611bin "ex"
2612flag "--from-file <file>" conflicts="value"
2613arg "[value]" conflicts="--from-file"
2614"#);
2615
2616 assert!(
2617 entry_of(&out, "from-file").contains("Conflicts: []uint64{ArgValue}"),
2618 "{out}"
2619 );
2620 assert!(
2621 out.lines().any(|line| {
2622 line.contains("{Key: ArgValue, Name: \"value\"")
2623 && line.contains("Conflicts: []uint64{FlagFromFile}")
2624 }),
2625 "{out}"
2626 );
2627 }
2628
2629 #[test]
2630 fn a_value_conditional_requirement_reaches_go_metadata() {
2631 let out = go(r#"
2632name "ex"
2633bin "ex"
2634flag "--format <format>" {
2635 requires_if "json" "--schema"
2636}
2637flag "--schema <file>"
2638"#);
2639 assert!(
2640 entry_of(&out, "format").contains(
2641 "RequiresIf: []argv.ValueRequirement{{Value: \"json\", Key: FlagSchema}}"
2642 ),
2643 "{out}"
2644 );
2645 assert!(
2646 out.contains("argv.CheckRelationshipsWithValues"),
2647 "the emitted parser must enforce the metadata:\n{out}"
2648 );
2649 }
2650
2651 #[test]
2652 fn required_if_eq_makes_generated_go_supply_values() {
2653 let out = go(r#"
2654name "ex"
2655bin "ex"
2656flag "--token <token>" {
2657 required_if_eq "--mode" "remote"
2658}
2659flag "--mode <mode>"
2660"#);
2661 assert!(
2662 entry_of(&out, "token").contains(
2663 "RequiredIfEq: []argv.ValueCondition{{Key: FlagMode, Value: \"remote\"}}"
2664 ),
2665 "{out}"
2666 );
2667 assert!(out.contains("resolved := map[uint64][]string{}"), "{out}");
2668 assert!(out.contains("argv.CheckRelationshipsWithValues"), "{out}");
2669 }
2670
2671 #[test]
2672 fn boolean_sources_are_normalized_for_value_relationships() {
2673 let out = go(r#"
2674name "ex"
2675bin "ex"
2676flag "--token <token>" {
2677 required_if_eq "--mode" "true"
2678}
2679flag "--mode" negate="--no-mode" bool_value=#true
2680"#);
2681 assert!(
2682 entry_of(&out, "mode").contains("RequiresIfBoolean: true"),
2683 "{out}"
2684 );
2685 }
2686
2687 #[test]
2688 fn a_conditional_default_reaches_go_metadata() {
2689 let out = go(r#"
2690name "ex"
2691bin "ex"
2692flag "--bin-names" {
2693 default_if "--json" "true"
2694 default_if "--output" "json" "pretty"
2695}
2696flag "--json"
2697flag "--output <fmt>"
2698"#);
2699 assert!(
2700 entry_of(&out, "bin-names")
2701 .contains("DefaultIf: []argv.DefaultIf{{Key: FlagJson, Value: \"true\"}"),
2702 "{out}"
2703 );
2704 assert!(
2705 entry_of(&out, "bin-names").contains("When: \"json\""),
2706 "{out}"
2707 );
2708 assert!(
2709 out.contains("argv.ApplyDefaultIf"),
2710 "the emitted parser must apply the metadata:\n{out}"
2711 );
2712 assert!(
2713 out.contains("negated[ev.Flag.Key] = ev.Negated"),
2714 "Equals default_if needs the negate form:\n{out}"
2715 );
2716 }
2717
2718 #[test]
2722 fn a_relationship_needs_the_right_form() {
2723 let out = go(r#"
2724name "ex"
2725bin "ex"
2726flag "-q --quiet"
2727flag "--color"
2728flag "--a" conflicts="--q"
2729flag "--b" conflicts="-color"
2730flag "--c" conflicts="-q"
2731flag "--d" conflicts="--color"
2732"#);
2733 assert!(!entry_of(&out, "a").contains("Conflicts"), "{out}");
2735 assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
2736 assert!(
2738 entry_of(&out, "c").contains("Conflicts: []uint64{FlagQuiet}"),
2739 "{out}"
2740 );
2741 assert!(
2742 entry_of(&out, "d").contains("Conflicts: []uint64{FlagColor}"),
2743 "{out}"
2744 );
2745 }
2746
2747 #[test]
2754 fn an_ordinary_form_beats_another_flags_negation() {
2755 let out = go(r#"
2756name "ex"
2757bin "ex"
2758flag "--a" negate="--zap"
2759flag "--zap"
2760flag "--p" conflicts="--zap"
2761"#);
2762 assert!(
2763 entry_of(&out, "p").contains("Conflicts: []uint64{FlagZap}"),
2764 "should name the flag `--zap` binds, not the one negating to it:\n{out}"
2765 );
2766 }
2767
2768 #[test]
2770 fn a_single_dash_negation_is_named_by_its_own_form() {
2771 let out = go(r#"
2772name "ex"
2773bin "ex"
2774flag "--tint" negate="-no-tint"
2775flag "--plain" conflicts="-no-tint"
2776flag "--other" conflicts="--no-tint"
2777"#);
2778 assert!(
2779 entry_of(&out, "plain").contains("Conflicts: []uint64{FlagTint}"),
2780 "the exact form should resolve:\n{out}"
2781 );
2782 assert!(
2784 !entry_of(&out, "other").contains("Conflicts"),
2785 "`--no-tint` is not how it was declared:\n{out}"
2786 );
2787 }
2788
2789 #[test]
2791 fn a_negation_is_matched_as_written() {
2792 let out = go(r#"
2793name "ex"
2794bin "ex"
2795flag "--color" negate="--no-color"
2796flag "--tint" negate="-no-tint"
2797flag "--a" conflicts="--no-color"
2798flag "--b" conflicts="--no-tint"
2799"#);
2800 assert!(
2801 entry_of(&out, "a").contains("Conflicts: []uint64{FlagColor}"),
2802 "{out}"
2803 );
2804 assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
2807 }
2808
2809 #[test]
2810 fn strings_are_escaped_to_go_rules() {
2811 assert_eq!(go_string(r#"a"b\c"#), r#""a\"b\\c""#);
2812 assert_eq!(go_string("tab\there"), r#""tab\there""#);
2813 assert_eq!(go_string("\u{7f}"), r#""\x7f""#);
2815 }
2816}