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