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
213 let index = out.len();
214 out.push(Emitted {
215 named,
216 cmd: cmd.clone(),
217 flags,
218 args,
219 clause_args,
220 subcommands: Vec::new(),
221 root,
222 });
223
224 let mut children = Vec::new();
228 for (name, sub) in &cmd.subcommands {
229 if name != &sub.name {
233 continue;
234 }
235 let mut child_path = path.to_vec();
236 child_path.push(name);
237 let at = out.len();
238 self.collect(sub, &child_path, false, out);
239 children.push(at);
240 }
241 out[index].subcommands = children;
242 }
243
244 fn header(&mut self) {
245 let _ = writeln!(
246 self.out,
247 "// Code generated by `usage generate go`. DO NOT EDIT.\n\
248 //\n\
249 // Binding tables for `{}`, read by\n\
250 // [github.com/jdx/usage/go/argv]. Regenerate rather than editing: the spec is\n\
251 // the definition, and a hand-edit here is a difference no reviewer can see.\n\
252 //\n\
253 // These are package-level variables holding plain data, so the linker lays them\n\
254 // out and nothing runs before main.\n\
255 \n\
256 package {}\n\
257 \n\
258 import \"github.com/jdx/usage/go/argv\"\n",
259 self.spec.bin, self.package
260 );
261
262 if let Some(version) = self
263 .spec
264 .version
265 .as_ref()
266 .or(self.spec.long_version.as_ref())
267 {
268 let _ = writeln!(
269 self.out,
270 "// Version is what the spec declares, so a caller answering `--version` has it\n\
271 // without the parse tables carrying a string binding never reads.\n\
272 const Version = {}\n",
273 go_string(version)
274 );
275 }
276 if let Some(version) = &self.spec.long_version {
277 let _ = writeln!(
278 self.out,
279 "// LongVersion is the extended text printed for `--version`; `-V` uses Version.\n\
280 const LongVersion = {}\n",
281 go_string(version)
282 );
283 }
284 }
285
286 fn constants(&mut self, commands: &[Emitted]) {
287 let _ = writeln!(
288 self.out,
289 "// Keys identify a table entry without a string comparison: switch on the Key an\n\
290 // event carries rather than on its Name, which is there for diagnostics.\n\
291 const ("
292 );
293 let mut entries: Vec<(&str, u64)> = Vec::new();
294 for e in commands {
295 entries.push((&e.named.key, e.named.number));
296 entries.extend(e.flags.iter().map(|(_, n)| (n.key.as_str(), n.number)));
297 entries.extend(e.args.iter().map(|(_, n)| (n.key.as_str(), n.number)));
298 entries.extend(
299 e.clause_args
300 .iter()
301 .map(|(_, n)| (n.key.as_str(), n.number)),
302 );
303 }
304 let width = entries.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
307 for (key, number) in entries {
308 let _ = writeln!(
309 self.out,
310 "\t{key}{:pad$} uint64 = {number}",
311 "",
312 pad = width - key.len()
313 );
314 }
315 let _ = writeln!(self.out, ")\n");
316 }
317
318 fn tables(&mut self, commands: &[Emitted]) {
319 let default_subcommand = self.spec.default_subcommand.as_ref().and_then(|name| {
326 let direct = || commands[0].subcommands.iter().map(|at| &commands[*at]);
327 direct()
331 .find(|e| &e.cmd.name == name)
332 .or_else(|| {
333 direct().find(|e| {
334 e.cmd.aliases.contains(name) || e.cmd.hidden_aliases.contains(name)
335 })
336 })
337 .map(|e| e.named.var.clone())
338 });
339
340 for (i, e) in commands.iter().enumerate() {
341 let doc = if e.root {
342 format!(
343 "// Root is the command tree for `{}`. Pass it to argv.New.",
344 self.spec.bin
345 )
346 } else {
347 format!("// {}", e.cmd.full_cmd.join(" "))
348 };
349 let mut lines = vec![
350 Line::Field("Name".into(), go_string(&e.cmd.name)),
351 Line::Field("Key".into(), e.named.key.clone()),
352 ];
353
354 let aliases: Vec<&String> = e
355 .cmd
356 .aliases
357 .iter()
358 .chain(e.cmd.hidden_aliases.iter())
359 .collect();
360 if !aliases.is_empty() {
361 let list = aliases
364 .iter()
365 .map(|a| go_string(a))
366 .collect::<Vec<_>>()
367 .join(", ");
368 lines.push(Line::Field("Aliases".into(), format!("[]string{{{list}}}")));
369 }
370
371 if !e.flags.is_empty() {
372 let mut block = vec!["Flags: []*argv.Flag{".to_string()];
373 for (flag, named) in &e.flags {
374 block.push(format!("\t{},", flag_literal(flag, named)));
375 }
376 block.push("},".to_string());
377 lines.push(Line::Block(block));
378 }
379
380 if !e.args.is_empty() {
381 let mut block = vec!["Args: []*argv.Arg{".to_string()];
382 for (arg, named) in &e.args {
383 block.push(format!("\t{},", arg_literal(arg, named)));
384 }
385 block.push("},".to_string());
386 lines.push(Line::Block(block));
387 }
388 if let Some(clause) = &e.cmd.clause {
389 let mut block = vec!["Clause: &argv.Clause{".to_string()];
390 block.push(format!("\tKey: {},", e.named.key));
391 block.push(format!("\tName: {},", go_string(&clause.name)));
392 block.push(format!("\tSeparator: {},", go_string(&clause.separator)));
393 block.push("\tArgs: []*argv.Arg{".to_string());
394 for (arg, named) in &e.clause_args {
395 block.push(format!("\t\t{},", arg_literal(arg, named)));
396 }
397 block.push("\t},".to_string());
398 block.push("},".to_string());
399 lines.push(Line::Block(block));
400 }
401
402 if !e.subcommands.is_empty() {
403 let list = e
404 .subcommands
405 .iter()
406 .map(|at| commands[*at].named.var.clone())
407 .collect::<Vec<_>>()
408 .join(", ");
409 lines.push(Line::Field(
410 "Subcommands".into(),
411 format!("[]*argv.Command{{{list}}}"),
412 ));
413 }
414
415 if effective_unknown_flags(self.spec, commands, i) == UnknownFlags::Error {
418 lines.push(Line::Field(
419 "UnknownFlags".into(),
420 "argv.UnknownFlagsError".into(),
421 ));
422 }
423
424 if e.cmd.external_subcommand {
425 lines.push(Line::Field("ExternalSubcommand".into(), "true".into()));
426 }
427 if e.cmd.arg_required_else_help {
428 lines.push(Line::Field("ArgRequiredElseHelp".into(), "true".into()));
429 }
430 if e.cmd.disable_help_flag {
431 lines.push(Line::Field("DisableHelpFlag".into(), "true".into()));
432 }
433 if e.cmd.disable_help_subcommand {
434 lines.push(Line::Field("DisableHelpSubcommand".into(), "true".into()));
435 }
436 if e.cmd.disable_version_flag {
437 lines.push(Line::Field("DisableVersionFlag".into(), "true".into()));
438 }
439 if e.cmd.subcommand_negates_reqs {
440 lines.push(Line::Field("SubcommandNegatesReqs".into(), "true".into()));
441 }
442 if e.cmd.args_conflicts_with_subcommands {
443 lines.push(Line::Field(
444 "ArgsConflictWithSubcommands".into(),
445 "true".into(),
446 ));
447 }
448 if e.cmd.subcommand_precedence_over_arg {
449 lines.push(Line::Field(
450 "SubcommandPrecedenceOverArg".into(),
451 "true".into(),
452 ));
453 }
454 if e.cmd.allow_missing_positional {
455 lines.push(Line::Field("AllowMissingPositional".into(), "true".into()));
456 }
457 if e.cmd.dont_delimit_trailing_values {
458 lines.push(Line::Field(
459 "DontDelimitTrailingValues".into(),
460 "true".into(),
461 ));
462 }
463 if e.root {
464 if let Some(var) = &default_subcommand {
465 lines.push(Line::Field("DefaultSubcommand".into(), var.clone()));
466 }
467 if self.spec.version.is_some() || self.spec.long_version.is_some() {
468 lines.push(Line::Field("Version".into(), "true".into()));
471 }
472 }
473
474 let _ = writeln!(self.out, "{doc}");
475 let _ = writeln!(self.out, "var {} = &argv.Command{{", e.named.var);
476 render(&mut self.out, "\t", &lines);
477 let _ = writeln!(self.out, "}}\n");
478 }
479 }
480}
481
482impl Emitter<'_> {
483 fn metadata(&mut self, commands: &[Emitted]) {
493 let mut by_key: BTreeMap<u64, String> = BTreeMap::new();
495 for e in commands {
496 for (flag, named) in &e.flags {
497 by_key.insert(named.number, self.flag_meta(flag, named, e, commands));
498 }
499 for (arg, named) in &e.args {
500 by_key.insert(named.number, arg_meta(self.spec, arg, named, e, commands));
501 }
502 for (arg, named) in &e.clause_args {
503 by_key.insert(named.number, arg_meta(self.spec, arg, named, e, commands));
504 }
505 }
506
507 let total = commands
508 .iter()
509 .map(|e| 1 + e.flags.len() + e.args.len() + e.clause_args.len())
510 .sum::<usize>() as u64;
511
512 let _ = writeln!(
513 self.out,
514 "// Meta is the cold table, read only by the rules that are decided once the\n\
515 // last token has been read: required, choices, the env-then-default fallback,\n\
516 // the var bounds, and the four that compare one entry against another. A parse\n\
517 // never touches it.\n\
518 //\n\
519 // Indexed by key, so entry Key sits at Meta[Key-1]. A command's slot is empty:\n\
520 // commands take keys too, and have no cold half.\n\
521 var Meta = argv.Metadata{{"
522 );
523 for key in 1..=total {
524 match by_key.get(&key) {
525 Some(entry) => {
526 let _ = writeln!(self.out, "\t{entry},");
527 }
528 None => {
529 let _ = writeln!(self.out, "\t{{}},");
530 }
531 }
532 }
533 let _ = writeln!(self.out, "}}\n");
534 }
535
536 fn flag_meta(
538 &self,
539 flag: &SpecFlag,
540 named: &Named,
541 owner: &Emitted,
542 commands: &[Emitted],
543 ) -> String {
544 let mut fields = vec![
545 format!("Key: {}", named.key),
546 format!("Name: {}", go_string(&flag.name)),
547 "Flag: true".to_string(),
548 ];
549 if !owner.cmd.args_override_self
550 && !flag.var
551 && !flag.count
552 && !flag.arg.as_ref().is_some_and(|arg| arg.var)
553 {
554 fields.push("RejectDuplicate: true".to_string());
555 }
556 if flag.required {
557 fields.push("Required: true".to_string());
558 }
559 if flag.arg.is_none() {
560 fields.push("RequiresIfBoolean: true".to_string());
561 }
562 if let Some(long) = flag.long.first() {
566 fields.push(format!("Spelling: {}", go_string(&format!("--{long}"))));
567 } else if let Some(short) = flag.short.first() {
568 fields.push(format!("Spelling: {}", go_string(&format!("-{short}"))));
569 }
570 if let Some(value) = flag.arg.as_ref() {
573 fields.push(format!("ValueName: {}", go_string(&value.name)));
574 }
575 let named_value = flag
576 .arg
577 .as_ref()
578 .map(|a| a.name.as_str())
579 .unwrap_or(flag.name.as_str());
580 if let Some(kind) = complete_type(self.spec, named_value) {
581 fields.push(format!("CompleteType: {}", go_string(kind)));
582 }
583 if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) {
585 fields.push(format!(
586 "Choices: {}",
587 string_slice(&visible_choices(choices))
588 ));
589 fields.push(format!(
590 "AcceptedChoices: {}",
591 string_slice(&accepted_choices(choices))
592 ));
593 if choices.ignore_case {
594 fields.push("IgnoreCase: true".to_string());
595 }
596 if !choices.strict {
597 fields.push("AllowUnknownChoices: true".to_string());
598 }
599 }
600 let default = if !flag.default.is_empty() {
604 &flag.default
605 } else {
606 flag.arg
607 .as_ref()
608 .map(|a| &a.default)
609 .unwrap_or(&flag.default)
610 };
611 if !default.is_empty() {
612 fields.push(format!("Default: {}", string_slice(default)));
613 }
614 if let Some(env) = &flag.env {
615 fields.push(format!("Env: {}", go_string(env)));
616 }
617 if !flag.env_fallback.is_empty() {
618 fields.push(format!("EnvFallback: {}", string_slice(&flag.env_fallback)));
619 }
620 if !flag.deprecated_env.is_empty() {
621 fields.push(format!(
622 "DeprecatedEnv: {}",
623 string_slice(&flag.deprecated_env)
624 ));
625 }
626 let minimum = flag
627 .arg
628 .as_ref()
629 .filter(|arg| arg.var)
630 .and_then(|arg| arg.var_min)
631 .or(flag.var_min);
632 if let Some(min) = minimum {
633 fields.push(format!("VarMin: {}", clamp_var_max(min)));
634 }
635 if let Some(max) = flag.var_max {
638 fields.push(format!("VarMax: {}", clamp_var_max(max)));
639 }
640
641 for (label, names) in [
642 ("Conflicts", &flag.conflicts),
643 ("Overrides", &flag.overrides),
644 ("RequiredUnless", &flag.required_unless),
645 ("RequiredUnlessAll", &flag.required_unless_all),
646 ("RequiredIf", &flag.required_if),
647 ("Requires", &flag.requires),
648 ] {
649 let keys = resolve_relationship(names, owner, commands);
650 if !keys.is_empty() {
651 fields.push(format!("{label}: {}", key_slice(&keys)));
652 }
653 }
654 for (label, conditions) in [
655 ("RequiredIfEq", &flag.required_if_eq),
656 ("RequiredIfEqAll", &flag.required_if_eq_all),
657 ] {
658 let values = conditions
659 .iter()
660 .filter_map(|condition| {
661 resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
662 .into_iter()
663 .next()
664 .map(|key| {
665 format!("{{Key: {key}, Value: {}}}", go_string(&condition.value))
666 })
667 })
668 .collect::<Vec<_>>();
669 if !values.is_empty() {
670 fields.push(format!(
671 "{label}: []argv.ValueCondition{{{}}}",
672 values.join(", ")
673 ));
674 }
675 }
676 let requires_if = flag
677 .requires_if
678 .iter()
679 .filter_map(|condition| {
680 resolve_relationship(std::slice::from_ref(&condition.requires), owner, commands)
681 .into_iter()
682 .next()
683 .map(|key| format!("{{Value: {}, Key: {key}}}", go_string(&condition.value)))
684 })
685 .collect::<Vec<_>>();
686 if !requires_if.is_empty() {
687 fields.push(format!(
688 "RequiresIf: []argv.ValueRequirement{{{}}}",
689 requires_if.join(", ")
690 ));
691 }
692 let default_if = flag
693 .default_if
694 .iter()
695 .filter_map(|condition| {
696 resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
697 .into_iter()
698 .next()
699 .map(|key| match &condition.when {
700 None => format!("{{Key: {key}, Value: {}}}", go_string(&condition.value)),
701 Some(when) => format!(
702 "{{Key: {key}, When: {}, Value: {}}}",
703 go_string(when),
704 go_string(&condition.value)
705 ),
706 })
707 })
708 .collect::<Vec<_>>();
709 if !default_if.is_empty() {
710 fields.push(format!(
711 "DefaultIf: []argv.DefaultIf{{{}}}",
712 default_if.join(", ")
713 ));
714 }
715
716 format!("{{{}}}", fields.join(", "))
717 }
718}
719
720fn complete_type<'a>(spec: &'a Spec, name: &str) -> Option<&'a str> {
727 spec.complete
728 .get(&name.to_lowercase())
729 .and_then(|c| c.type_.as_deref())
730}
731
732fn arg_meta(
733 spec: &Spec,
734 arg: &SpecArg,
735 named: &Named,
736 owner: &Emitted,
737 commands: &[Emitted],
738) -> String {
739 let mut fields = vec![
740 format!("Key: {}", named.key),
741 format!("Name: {}", go_string(&arg.name)),
742 ];
743 if arg.required {
744 fields.push("Required: true".to_string());
745 }
746 if let Some(kind) = complete_type(spec, &arg.name) {
751 fields.push(format!("CompleteType: {}", go_string(kind)));
752 }
753 if let Some(choices) = &arg.choices {
754 fields.push(format!(
755 "Choices: {}",
756 string_slice(&visible_choices(choices))
757 ));
758 fields.push(format!(
759 "AcceptedChoices: {}",
760 string_slice(&accepted_choices(choices))
761 ));
762 if choices.ignore_case {
763 fields.push("IgnoreCase: true".to_string());
764 }
765 if !choices.strict {
766 fields.push("AllowUnknownChoices: true".to_string());
767 }
768 }
769 if !arg.default.is_empty() {
770 fields.push(format!("Default: {}", string_slice(&arg.default)));
771 }
772 if let Some(env) = &arg.env {
773 fields.push(format!("Env: {}", go_string(env)));
774 }
775 if !arg.env_fallback.is_empty() {
776 fields.push(format!("EnvFallback: {}", string_slice(&arg.env_fallback)));
777 }
778 if !arg.deprecated_env.is_empty() {
779 fields.push(format!(
780 "DeprecatedEnv: {}",
781 string_slice(&arg.deprecated_env)
782 ));
783 }
784 if let Some(min) = arg.var_min {
785 fields.push(format!("VarMin: {}", clamp_var_max(min)));
786 }
787 let conflicts = resolve_relationship(&arg.conflicts, owner, commands);
788 if !conflicts.is_empty() {
789 fields.push(format!("Conflicts: {}", key_slice(&conflicts)));
790 }
791 for (label, names) in [
792 ("Requires", &arg.requires),
793 ("RequiredIf", &arg.required_if),
794 ("RequiredUnless", &arg.required_unless),
795 ("RequiredUnlessAll", &arg.required_unless_all),
796 ] {
797 let keys = resolve_relationship(names, owner, commands);
798 if !keys.is_empty() {
799 fields.push(format!("{label}: {}", key_slice(&keys)));
800 }
801 }
802 for (label, conditions) in [
803 ("RequiredIfEq", &arg.required_if_eq),
804 ("RequiredIfEqAll", &arg.required_if_eq_all),
805 ] {
806 let values = conditions
807 .iter()
808 .filter_map(|condition| {
809 resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
810 .into_iter()
811 .next()
812 .map(|key| format!("{{Key: {key}, Value: {}}}", go_string(&condition.value)))
813 })
814 .collect::<Vec<_>>();
815 if !values.is_empty() {
816 fields.push(format!(
817 "{label}: []argv.ValueCondition{{{}}}",
818 values.join(", ")
819 ));
820 }
821 }
822 format!("{{{}}}", fields.join(", "))
826}
827
828fn resolve_relationship(names: &[String], owner: &Emitted, commands: &[Emitted]) -> Vec<String> {
839 let mut out = Vec::new();
840 for name in names {
841 let mut found = match_flag(owner, name, false);
845 if found.is_none() && !name.starts_with('-') {
846 found = owner
847 .args
848 .iter()
849 .chain(owner.clause_args.iter())
850 .find(|(arg, _)| arg.name == *name)
851 .map(|(_, named)| named.key.clone());
852 }
853 if found.is_none() {
854 let path = &owner.cmd.full_cmd;
855 for depth in (0..path.len()).rev() {
856 let ancestor = commands
857 .iter()
858 .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]);
859 if let Some(key) = ancestor.and_then(|a| match_flag(a, name, true)) {
860 found = Some(key);
861 break;
862 }
863 }
864 }
865 if let Some(key) = found {
866 out.push(key);
867 }
868 }
869 out
870}
871
872fn match_flag(cmd: &Emitted, name: &str, globals_only: bool) -> Option<String> {
879 let eligible = |flag: &SpecFlag| !globals_only || flag.global;
889
890 let (long, short, bare) = if let Some(rest) = name.strip_prefix("--") {
893 (Some(rest), None, None)
894 } else if let Some(rest) = name.strip_prefix('-') {
895 let mut chars = rest.chars();
896 match (chars.next(), chars.next()) {
897 (Some(c), None) => (None, Some(c), None),
898 _ => (None, None, None),
899 }
900 } else {
901 (None, None, Some(name))
902 };
903
904 let ordinary = cmd.flags.iter().find(|(flag, _)| {
905 if !eligible(flag) {
906 return false;
907 }
908 if let Some(bare) = bare {
909 return flag.name == bare;
910 }
911 if let Some(long) = long {
912 return flag.long.iter().any(|l| l == long);
913 }
914 short.is_some_and(|c| flag.short.contains(&c))
915 });
916 if let Some((_, named)) = ordinary {
917 return Some(named.key.clone());
918 }
919
920 cmd.flags
924 .iter()
925 .find(|(flag, _)| eligible(flag) && flag.negate.as_deref() == Some(name))
926 .map(|(_, named)| named.key.clone())
927}
928fn string_slice(values: &[String]) -> String {
929 let list = values
930 .iter()
931 .map(|v| go_string(v))
932 .collect::<Vec<_>>()
933 .join(", ");
934 format!("[]string{{{list}}}")
935}
936
937fn accepted_choices(choices: &SpecChoices) -> Vec<String> {
938 choices
939 .choices
940 .iter()
941 .chain(
942 choices
943 .details
944 .iter()
945 .flat_map(|choice| choice.aliases.iter().map(|alias| &alias.value)),
946 )
947 .cloned()
948 .collect()
949}
950
951fn visible_choices(choices: &SpecChoices) -> Vec<String> {
952 choices
953 .choices
954 .iter()
955 .filter(|value| {
956 !choices
957 .details
958 .iter()
959 .any(|choice| choice.value == value.as_str() && choice.hide)
960 })
961 .chain(choices.details.iter().flat_map(|choice| {
962 choice
963 .aliases
964 .iter()
965 .filter(|alias| !alias.hide)
966 .map(|alias| &alias.value)
967 }))
968 .cloned()
969 .collect()
970}
971
972fn key_slice(keys: &[String]) -> String {
973 format!("[]uint64{{{}}}", keys.join(", "))
974}
975
976impl Emitter<'_> {
977 fn help_table(&mut self, commands: &[Emitted]) {
984 let mut by_key: BTreeMap<u64, String> = BTreeMap::new();
985 for e in commands {
986 by_key.insert(e.named.number, command_help(e));
987 for (flag, named) in &e.flags {
988 by_key.insert(named.number, flag_help(flag, named));
989 }
990 for (arg, named) in &e.args {
991 by_key.insert(named.number, arg_help(arg, named));
992 }
993 for (arg, named) in &e.clause_args {
994 by_key.insert(named.number, arg_help(arg, named));
995 }
996 }
997
998 let total = commands
999 .iter()
1000 .map(|e| 1 + e.flags.len() + e.args.len() + e.clause_args.len())
1001 .sum::<usize>() as u64;
1002
1003 let _ = writeln!(
1004 self.out,
1005 "// HelpText is the third table, read only when a page is rendered. Neither the\n\
1006 // parser nor the post-binding rules touch it, and a CLI that never prints help\n\
1007 // does not carry it: Go's linker drops an unreferenced table whole.\n\
1008 //\n\
1009 // Indexed by key, like the others.\n\
1010 var HelpText = argv.HelpTable{{"
1011 );
1012 for key in 1..=total {
1013 match by_key.get(&key) {
1014 Some(entry) => {
1015 let _ = writeln!(self.out, "\t{entry},");
1016 }
1017 None => {
1018 let _ = writeln!(self.out, "\t{{}},");
1019 }
1020 }
1021 }
1022 let _ = writeln!(self.out, "}}\n");
1023
1024 let mut fields = vec![
1025 format!("Name: {}", go_string(&self.spec.name)),
1026 format!("Bin: {}", go_string(&self.spec.bin)),
1027 ];
1028 if let Some(version) = self
1029 .spec
1030 .version
1031 .as_ref()
1032 .or(self.spec.long_version.as_ref())
1033 {
1034 fields.push(format!("Version: {}", go_string(version)));
1035 }
1036 if let Some(version) = &self.spec.long_version {
1037 fields.push(format!("LongVersion: {}", go_string(version)));
1038 }
1039 if let Some(about) = &self.spec.about {
1043 fields.push(format!("About: {}", go_string(about)));
1044 }
1045 if let Some(long) = &self.spec.about_long {
1046 fields.push(format!("LongAbout: {}", go_string(long)));
1047 }
1048 if let Some(author) = &self.spec.author {
1049 fields.push(format!("Author: {}", go_string(author)));
1050 }
1051 if let Some(license) = &self.spec.license {
1052 fields.push(format!("License: {}", go_string(license)));
1053 }
1054 if let Some(before) = &self.spec.before_help {
1055 fields.push(format!("BeforeHelp: {}", go_string(before)));
1056 }
1057 if let Some(after) = &self.spec.after_help {
1058 fields.push(format!("AfterHelp: {}", go_string(after)));
1059 }
1060 if let Some(before) = &self.spec.before_help_long {
1061 fields.push(format!("BeforeLongHelp: {}", go_string(before)));
1062 }
1063 if let Some(after) = &self.spec.after_help_long {
1064 fields.push(format!("AfterLongHelp: {}", go_string(after)));
1065 }
1066 if let Some(template) = &self.spec.help_template {
1068 fields.push(format!("HelpTemplate: {}", go_string(template)));
1069 }
1070 let _ = writeln!(
1071 self.out,
1072 "// HelpMeta is what a page needs from the spec's root rather than from any one\n\
1073 // command: the header, and the text that brackets every page.\n\
1074 var HelpMeta = argv.HelpSpec{{{}}}\n",
1075 fields.join(", ")
1076 );
1077 }
1078}
1079
1080fn command_help(e: &Emitted) -> String {
1082 let mut fields = vec![format!("Key: {}", e.named.key)];
1083 if e.cmd.hide {
1084 fields.push("Hide: true".to_string());
1085 }
1086 if let Some(heading) = &e.cmd.help_heading {
1087 fields.push(format!("Heading: {}", go_string(heading)));
1088 }
1089 if let Some(order) = e.cmd.display_order {
1090 fields.push(format!("DisplayOrder: {order}"));
1091 fields.push("DisplayOrderSet: true".to_string());
1092 }
1093 if let Some(help) = e.cmd.help.as_deref().or(e.cmd.help_long.as_deref()) {
1094 fields.push(format!("Short: {}", go_string(help)));
1095 }
1096 if let Some(long) = &e.cmd.help_long {
1097 fields.push(format!("Long: {}", go_string(long)));
1098 }
1099 if let Some(message) = &e.cmd.deprecated {
1100 fields.push(format!("Deprecated: {}", go_string(message)));
1101 }
1102 if let Some(at) = &e.cmd.deprecated_warn_at {
1103 fields.push(format!("DeprecatedWarnAt: {}", go_string(at)));
1104 }
1105 if let Some(at) = &e.cmd.deprecated_remove_at {
1106 fields.push(format!("DeprecatedRemoveAt: {}", go_string(at)));
1107 }
1108 if let Some(heading) = &e.cmd.subcommand_help_heading {
1109 fields.push(format!("SubcommandHelpHeading: {}", go_string(heading)));
1110 }
1111 if let Some(name) = &e.cmd.subcommand_value_name {
1112 fields.push(format!("SubcommandValueName: {}", go_string(name)));
1113 }
1114 if e.cmd.next_line_help {
1115 fields.push("NextLineHelp: true".to_string());
1116 }
1117 if e.cmd.flatten_help {
1118 fields.push("FlattenHelp: true".to_string());
1119 }
1120 if e.cmd.subcommand_required {
1121 fields.push("SubcommandRequired: true".to_string());
1122 }
1123 let visible: Vec<String> = e
1126 .cmd
1127 .aliases
1128 .iter()
1129 .filter(|a| !e.cmd.hidden_aliases.contains(a))
1130 .cloned()
1131 .collect();
1132 if !visible.is_empty() {
1133 fields.push(format!("VisibleAliases: {}", string_slice(&visible)));
1134 }
1135 if let Some(before) = &e.cmd.before_help {
1136 fields.push(format!("BeforeHelp: {}", go_string(before)));
1137 }
1138 if let Some(after) = &e.cmd.after_help {
1139 fields.push(format!("AfterHelp: {}", go_string(after)));
1140 }
1141 if let Some(before) = &e.cmd.before_help_long {
1145 fields.push(format!("BeforeLongHelp: {}", go_string(before)));
1146 }
1147 if let Some(after) = &e.cmd.after_help_long {
1148 fields.push(format!("AfterLongHelp: {}", go_string(after)));
1149 }
1150 if !e.cmd.examples.is_empty() {
1151 let items = e
1152 .cmd
1153 .examples
1154 .iter()
1155 .map(|x| {
1156 let mut parts = Vec::new();
1157 if let Some(header) = &x.header {
1158 parts.push(format!("Header: {}", go_string(header)));
1159 }
1160 parts.push(format!("Code: {}", go_string(&x.code)));
1161 if let Some(help) = &x.help {
1165 parts.push(format!("Help: {}", go_string(help)));
1166 }
1167 format!("{{{}}}", parts.join(", "))
1168 })
1169 .collect::<Vec<_>>()
1170 .join(", ");
1171 fields.push(format!("Examples: []argv.Example{{{items}}}"));
1172 }
1173 if !e.cmd.headings.is_empty() {
1177 let items = e
1178 .cmd
1179 .headings
1180 .iter()
1181 .map(|heading| {
1182 format!(
1183 "{{Title: {}, Help: {}}}",
1184 go_string(&heading.title),
1185 go_string(&heading.help)
1186 )
1187 })
1188 .collect::<Vec<_>>()
1189 .join(", ");
1190 fields.push(format!("Headings: []argv.Heading{{{items}}}"));
1191 }
1192 format!("{{{}}}", fields.join(", "))
1193}
1194
1195fn flag_help(flag: &SpecFlag, named: &Named) -> String {
1196 let mut fields = vec![format!("Key: {}", named.key)];
1197 if let Some(message) = &flag.deprecated {
1198 fields.push(format!("Deprecated: {}", go_string(message)));
1199 }
1200 if let Some(at) = &flag.deprecated_warn_at {
1201 fields.push(format!("DeprecatedWarnAt: {}", go_string(at)));
1202 }
1203 if let Some(at) = &flag.deprecated_remove_at {
1204 fields.push(format!("DeprecatedRemoveAt: {}", go_string(at)));
1205 }
1206 if flag.hide {
1207 fields.push("Hide: true".to_string());
1208 }
1209 if let Some(order) = flag.display_order {
1210 fields.push(format!("DisplayOrder: {order}"));
1211 fields.push("DisplayOrderSet: true".to_string());
1212 }
1213 for (name, hidden) in [
1214 ("HideDefaultValue", flag.hide_default_value),
1215 ("HideEnv", flag.hide_env),
1216 ("HideEnvValues", flag.hide_env_values),
1217 ("HidePossibleValues", flag.hide_possible_values),
1218 ("HideShortHelp", flag.hide_short_help),
1219 ("HideLongHelp", flag.hide_long_help),
1220 ] {
1221 if hidden {
1222 fields.push(format!("{name}: true"));
1223 }
1224 }
1225 if flag.required && flag.default.is_empty() {
1228 fields.push("Demanded: true".to_string());
1229 }
1230 if flag.var {
1231 fields.push("Repeatable: true".to_string());
1232 }
1233 if let Some(arg) = &flag.arg {
1234 if arg.name != flag.name {
1235 fields.push(format!("ValueName: {}", go_string(&arg.name)));
1236 }
1237 if arg.required && arg.default.is_empty() {
1241 fields.push("ValueDemanded: true".to_string());
1242 }
1243 if !arg.value_names.is_empty() {
1244 fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1245 }
1246 if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1247 fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1248 }
1249 }
1250 if let Some(help) = flag.help.as_deref().or(flag.help_first_line.as_deref()) {
1253 fields.push(format!("Short: {}", go_string(help)));
1254 }
1255 if let Some(long) = flag.help_long.as_deref().or(flag.help.as_deref()) {
1256 fields.push(format!("Long: {}", go_string(long)));
1257 }
1258 if let Some(heading) = &flag.help_heading {
1259 fields.push(format!("Heading: {}", go_string(heading)));
1260 }
1261 if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) {
1263 fields.push(format!(
1264 "Choices: {}",
1265 string_slice(&visible_choices(choices))
1266 ));
1267 }
1268 if let Some(env) = &flag.env {
1269 fields.push(format!("Env: {}", go_string(env)));
1270 }
1271 if !flag.env_fallback.is_empty() {
1272 fields.push(format!("EnvFallback: {}", string_slice(&flag.env_fallback)));
1273 }
1274 if !flag.deprecated_env.is_empty() {
1275 fields.push(format!(
1276 "DeprecatedEnv: {}",
1277 string_slice(&flag.deprecated_env)
1278 ));
1279 }
1280 let default = if !flag.default.is_empty() {
1281 &flag.default
1282 } else {
1283 flag.arg
1284 .as_ref()
1285 .map(|a| &a.default)
1286 .unwrap_or(&flag.default)
1287 };
1288 if !default.is_empty() {
1289 fields.push(format!("Default: {}", string_slice(default)));
1290 }
1291 format!("{{{}}}", fields.join(", "))
1292}
1293
1294fn arg_help(arg: &SpecArg, named: &Named) -> String {
1295 let mut fields = vec![format!("Key: {}", named.key)];
1296 if let Some(order) = arg.display_order {
1297 fields.push(format!("DisplayOrder: {order}"));
1298 fields.push("DisplayOrderSet: true".to_string());
1299 }
1300 if arg.hide {
1301 fields.push("Hide: true".to_string());
1302 }
1303 for (name, hidden) in [
1304 ("HideDefaultValue", arg.hide_default_value),
1305 ("HideEnv", arg.hide_env),
1306 ("HideEnvValues", arg.hide_env_values),
1307 ("HidePossibleValues", arg.hide_possible_values),
1308 ("HideShortHelp", arg.hide_short_help),
1309 ("HideLongHelp", arg.hide_long_help),
1310 ] {
1311 if hidden {
1312 fields.push(format!("{name}: true"));
1313 }
1314 }
1315 if arg.required && arg.default.is_empty() {
1316 fields.push("Demanded: true".to_string());
1317 }
1318 if !arg.value_names.is_empty() {
1319 fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1320 }
1321 if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1322 fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1323 }
1324 if let Some(help) = arg.help.as_deref().or(arg.help_first_line.as_deref()) {
1325 fields.push(format!("Short: {}", go_string(help)));
1326 }
1327 if let Some(long) = arg.help_long.as_deref().or(arg.help.as_deref()) {
1328 fields.push(format!("Long: {}", go_string(long)));
1329 }
1330 if let Some(heading) = &arg.help_heading {
1331 fields.push(format!("Heading: {}", go_string(heading)));
1332 }
1333 if let Some(choices) = &arg.choices {
1334 fields.push(format!(
1335 "Choices: {}",
1336 string_slice(&visible_choices(choices))
1337 ));
1338 }
1339 if let Some(env) = &arg.env {
1340 fields.push(format!("Env: {}", go_string(env)));
1341 }
1342 if !arg.env_fallback.is_empty() {
1343 fields.push(format!("EnvFallback: {}", string_slice(&arg.env_fallback)));
1344 }
1345 if !arg.deprecated_env.is_empty() {
1346 fields.push(format!(
1347 "DeprecatedEnv: {}",
1348 string_slice(&arg.deprecated_env)
1349 ));
1350 }
1351 if !arg.default.is_empty() {
1352 fields.push(format!("Default: {}", string_slice(&arg.default)));
1353 }
1354 format!("{{{}}}", fields.join(", "))
1355}
1356
1357enum Line {
1366 Field(String, String),
1368 Block(Vec<String>),
1370}
1371
1372fn render(out: &mut String, indent: &str, lines: &[Line]) {
1374 let mut run: Vec<(&String, &String)> = Vec::new();
1375
1376 fn flush(out: &mut String, indent: &str, run: &mut Vec<(&String, &String)>) {
1377 let width = run.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
1378 for (key, value) in run.iter() {
1379 let _ = writeln!(
1380 out,
1381 "{indent}{key}:{:width$} {value},",
1382 "",
1383 width = width - key.len()
1384 );
1385 }
1386 run.clear();
1387 }
1388
1389 for line in lines {
1390 match line {
1391 Line::Field(key, value) => run.push((key, value)),
1392 Line::Block(block) => {
1393 flush(out, indent, &mut run);
1394 for l in block {
1395 let _ = writeln!(out, "{indent}{l}");
1396 }
1397 }
1398 }
1399 }
1400 flush(out, indent, &mut run);
1401}
1402
1403struct Emitted {
1405 named: Named,
1406 cmd: SpecCommand,
1407 flags: Vec<(SpecFlag, Named)>,
1408 args: Vec<(SpecArg, Named)>,
1409 clause_args: Vec<(SpecArg, Named)>,
1410 subcommands: Vec<usize>,
1412 root: bool,
1413}
1414
1415fn effective_unknown_flags(spec: &Spec, commands: &[Emitted], at: usize) -> UnknownFlags {
1422 let path = &commands[at].cmd.full_cmd;
1423 for depth in (0..=path.len()).rev() {
1424 let ancestor = commands
1425 .iter()
1426 .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]);
1427 if let Some(mode) = ancestor.and_then(|e| e.cmd.unknown_flags) {
1428 return mode;
1429 }
1430 }
1431 spec.unknown_flags.unwrap_or_default()
1432}
1433
1434fn flag_literal(flag: &SpecFlag, named: &Named) -> String {
1435 let mut fields = vec![
1436 format!("Key: {}", named.key),
1437 format!("Name: {}", go_string(&flag.name)),
1438 ];
1439 if !flag.long.is_empty() {
1440 let longs = flag
1441 .long
1442 .iter()
1443 .map(|l| go_string(l))
1444 .collect::<Vec<_>>()
1445 .join(", ");
1446 fields.push(format!("Longs: []string{{{longs}}}"));
1447 }
1448 if !flag.hidden_aliases.is_empty() {
1449 fields.push(format!(
1450 "HiddenLongs: {}",
1451 string_slice(&flag.hidden_aliases)
1452 ));
1453 }
1454 if !flag.short.is_empty() {
1455 let shorts = flag
1456 .short
1457 .iter()
1458 .map(|c| go_byte(*c))
1459 .collect::<Vec<_>>()
1460 .join(", ");
1461 fields.push(format!("Shorts: []byte{{{shorts}}}"));
1462 }
1463 if !flag.hidden_short_aliases.is_empty() {
1464 let shorts = flag
1465 .hidden_short_aliases
1466 .iter()
1467 .map(|c| go_byte(*c))
1468 .collect::<Vec<_>>()
1469 .join(", ");
1470 fields.push(format!("HiddenShorts: []byte{{{shorts}}}"));
1471 }
1472 if let Some(negate) = &flag.negate {
1473 fields.push(format!(
1476 "Negate: {}",
1477 go_string(negate.trim_start_matches('-'))
1478 ));
1479 }
1480 if flag.arg.is_some() {
1481 fields.push("TakesValue: true".to_string());
1482 }
1483 if flag.value_optional {
1484 fields.push("ValueOptional: true".to_string());
1485 }
1486 if flag.bool_value {
1487 fields.push("BoolValue: true".to_string());
1488 }
1489 let action = match flag.action {
1490 SpecFlagAction::Set => None,
1491 SpecFlagAction::Help => Some("argv.ActionHelp"),
1492 SpecFlagAction::HelpShort => Some("argv.ActionHelpShort"),
1493 SpecFlagAction::HelpLong => Some("argv.ActionHelpLong"),
1494 SpecFlagAction::HelpAll => Some("argv.ActionHelpAll"),
1495 SpecFlagAction::Version => Some("argv.ActionVersion"),
1496 };
1497 if let Some(action) = action {
1498 fields.push(format!("Action: {action}"));
1499 }
1500 if let Some(arg) = flag.arg.as_ref().filter(|a| a.var) {
1505 fields.push("Variadic: true".to_string());
1506 if let Some(max) = arg.var_max {
1507 fields.push(format!("VarMax: {}", clamp_var_max(max)));
1508 }
1509 }
1510 if flag.allow_hyphen_values() {
1511 fields.push("AllowHyphenValues: true".to_string());
1512 }
1513 if let Some(arg) = &flag.arg {
1514 if arg.allow_negative_numbers {
1515 fields.push("AllowNegativeNumbers: true".to_string());
1516 }
1517 if let Some(terminator) = &arg.value_terminator {
1518 fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1519 }
1520 if let Some(delimiter) = arg.delimiter {
1521 fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1522 }
1523 }
1524 if flag.require_equals {
1525 fields.push("RequireEquals: true".to_string());
1526 }
1527 if let Some(missing) = &flag.default_missing {
1528 fields.push(format!("DefaultMissing: {}", go_string(missing)));
1529 }
1530 if flag.global {
1531 fields.push("Global: true".to_string());
1532 }
1533 format!("{{{}}}", fields.join(", "))
1534}
1535
1536fn arg_literal(arg: &SpecArg, named: &Named) -> String {
1537 let mut fields = vec![
1538 format!("Key: {}", named.key),
1539 format!("Name: {}", go_string(&arg.name)),
1540 ];
1541 if let Some(sigil) = &arg.sigil {
1542 fields.push(format!("Sigil: {}", go_string(sigil)));
1543 }
1544 if arg.required {
1545 fields.push("Required: true".to_string());
1546 }
1547 if arg.var {
1548 fields.push("Var: true".to_string());
1549 if let Some(max) = arg.var_max {
1550 fields.push(format!("VarMax: {}", clamp_var_max(max)));
1551 }
1552 }
1553 if arg.allow_negative_numbers {
1554 fields.push("AllowNegativeNumbers: true".to_string());
1555 }
1556 if let Some(terminator) = &arg.value_terminator {
1557 fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1558 }
1559 if let Some(delimiter) = arg.delimiter {
1560 fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1561 }
1562 let double_dash = match arg.double_dash {
1563 SpecDoubleDashChoices::Required => Some("argv.DoubleDashRequired"),
1564 SpecDoubleDashChoices::Preserve => Some("argv.DoubleDashPreserve"),
1565 SpecDoubleDashChoices::Automatic => Some("argv.DoubleDashAutomatic"),
1566 _ => None,
1567 };
1568 if let Some(dd) = double_dash {
1569 fields.push(format!("DoubleDash: {dd}"));
1570 }
1571 format!("{{{}}}", fields.join(", "))
1572}
1573
1574fn clamp_var_max(max: usize) -> u32 {
1579 u32::try_from(max).unwrap_or(u32::MAX)
1580}
1581
1582const GO_KEYWORDS: &[&str] = &[
1587 "break",
1588 "case",
1589 "chan",
1590 "const",
1591 "continue",
1592 "default",
1593 "defer",
1594 "else",
1595 "fallthrough",
1596 "for",
1597 "func",
1598 "go",
1599 "goto",
1600 "if",
1601 "import",
1602 "interface",
1603 "map",
1604 "package",
1605 "range",
1606 "return",
1607 "select",
1608 "struct",
1609 "switch",
1610 "type",
1611 "var",
1612];
1613
1614const UNUSABLE_PACKAGE_NAMES: &[&str] = &["_", "init"];
1627
1628pub fn is_valid_package(name: &str) -> bool {
1633 !name.is_empty()
1634 && !GO_KEYWORDS.contains(&name)
1635 && !UNUSABLE_PACKAGE_NAMES.contains(&name)
1636 && !name.starts_with(|c: char| c.is_ascii_digit())
1637 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1638}
1639
1640fn field_name(name: &str) -> String {
1642 let ident = format!("{}", AsPascalCase(name));
1643 if ident.is_empty() || ident.starts_with(|c: char| c.is_ascii_digit()) {
1644 format!("X{ident}")
1645 } else {
1646 ident
1647 }
1648}
1649
1650fn package_ident(bin: &str) -> String {
1658 let lowered: String = bin
1659 .chars()
1660 .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
1661 .collect::<String>()
1662 .to_ascii_lowercase();
1663 if is_valid_package(&lowered) {
1664 lowered
1665 } else {
1666 format!("cli{lowered}")
1670 }
1671}
1672
1673fn go_string(s: &str) -> String {
1678 let mut out = String::with_capacity(s.len() + 2);
1679 out.push('"');
1680 for c in s.chars() {
1681 match c {
1682 '"' => out.push_str("\\\""),
1683 '\\' => out.push_str("\\\\"),
1684 '\n' => out.push_str("\\n"),
1685 '\r' => out.push_str("\\r"),
1686 '\t' => out.push_str("\\t"),
1687 c if (c as u32) < 0x20 || c as u32 == 0x7f => {
1688 let _ = write!(out, "\\x{:02x}", c as u32);
1689 }
1690 c => out.push(c),
1691 }
1692 }
1693 out.push('"');
1694 out
1695}
1696
1697fn go_byte(c: char) -> String {
1703 match c {
1704 '\'' => "'\\''".to_string(),
1705 '\\' => "'\\\\'".to_string(),
1706 c if c.is_ascii_graphic() => format!("'{c}'"),
1707 c => format!("0x{:02x}", (c as u32) & 0xff),
1708 }
1709}
1710
1711#[cfg(test)]
1712mod tests {
1713 use super::*;
1714
1715 fn entry_of(out: &str, name: &str) -> String {
1724 out.lines()
1725 .find(|l| l.contains(&format!("Name: \"{name}\", Flag: true")))
1726 .unwrap_or_default()
1727 .to_string()
1728 }
1729
1730 fn go(kdl: &str) -> String {
1731 let spec: Spec = kdl.parse().expect("the fixture spec should parse");
1732 generate(&spec, &GoOptions::default())
1733 }
1734
1735 #[test]
1736 fn a_whole_cli() {
1737 let out = go(r#"
1738name "ex"
1739bin "ex"
1740version "1.2.3"
1741long_version "1.2.3\ncommit abc123"
1742flag "-v --verbose" global=#true help="be loud"
1743flag "--color" negate="--no-color"
1744flag "-j --jobs <n>"
1745flag "--include <pattern>..." var_max=3
1746arg "<file>"
1747arg "[rest]..." var=#true
1748cmd "install" {
1749 alias "i"
1750 flag "-f --force"
1751 arg "<pkg>"
1752}
1753cmd "config" {
1754 cmd "ls" {
1755 flag "--no-header"
1756 }
1757}
1758"#);
1759 insta::assert_snapshot!(out);
1760 }
1761
1762 #[test]
1763 fn rich_choices_keep_acceptance_visibility_and_strictness_separate() {
1764 let out = go(r#"
1765name "ex"
1766bin "ex"
1767flag "--color <when>" {
1768 choices ignore_case=#true strict=#false {
1769 choice "always" {
1770 alias "yes"
1771 alias "on" hide=#true
1772 }
1773 choice "never" hide=#true
1774 }
1775}
1776"#);
1777 let entry = entry_of(&out, "color");
1778 assert!(
1779 entry.contains(r#"Choices: []string{"always", "yes"}"#),
1780 "{entry}"
1781 );
1782 assert!(
1783 entry.contains(r#"AcceptedChoices: []string{"always", "never", "yes", "on"}"#),
1784 "{entry}"
1785 );
1786 assert!(entry.contains("IgnoreCase: true"), "{entry}");
1787 assert!(entry.contains("AllowUnknownChoices: true"), "{entry}");
1788 }
1789
1790 #[test]
1792 fn unknown_flags_are_inherited_and_overridable() {
1793 let out = go(r#"
1794name "ex"
1795bin "ex"
1796unknown_flags "error"
1797cmd "strict" {
1798 cmd "deep" {}
1799}
1800cmd "exec" unknown_flags="value" {
1801 cmd "nested" {}
1802}
1803"#);
1804 insta::assert_snapshot!(out);
1805 }
1806
1807 #[test]
1810 fn colliding_names_get_distinct_identifiers() {
1811 let out = go(r#"
1812name "ex"
1813bin "ex"
1814cmd "macos-defaults" {
1815 flag "--apply"
1816}
1817cmd "macos" {
1818 cmd "defaults" {
1819 flag "--apply"
1820 }
1821}
1822"#);
1823 insta::assert_snapshot!(out);
1824 }
1825
1826 #[test]
1827 fn a_default_subcommand_points_into_the_tree() {
1828 let out = go(r#"
1829name "ex"
1830bin "ex"
1831default_subcommand "run"
1832arg "[task]"
1833cmd "run" {
1834 arg "[args]..." var=#true
1835}
1836"#);
1837 insta::assert_snapshot!(out);
1838 }
1839
1840 #[test]
1841 fn a_bin_name_that_is_not_an_identifier_still_gives_a_package() {
1842 assert_eq!(package_ident("my-cli"), "mycli");
1843 assert_eq!(package_ident("7zip"), "cli7zip");
1844 assert_eq!(package_ident(""), "cli");
1845 assert_eq!(package_ident("go"), "cligo");
1847 assert_eq!(package_ident("type"), "clitype");
1848 assert_eq!(package_ident("_"), "cli_");
1851 assert_eq!(package_ident("init"), "cliinit");
1852 assert_eq!(package_ident("__"), "__");
1854 assert_eq!(package_ident("initialize"), "initialize");
1855
1856 for bin in [
1860 "my-cli", "7zip", "", "go", "type", "_", "init", "__", "MiSe",
1861 ] {
1862 let out = package_ident(bin);
1863 assert!(is_valid_package(&out), "{bin:?} sanitized to {out:?}");
1864 }
1865 }
1866
1867 #[test]
1869 fn a_name_matching_a_generated_suffix_still_gets_its_own() {
1870 let out = go(r#"
1871name "ex"
1872bin "ex"
1873cmd "macos-defaults" {}
1874cmd "macos" {
1875 cmd "defaults" {}
1876}
1877cmd "macos-defaults2" {}
1878"#);
1879 assert_declares_each_constant_once(&out);
1884 }
1885
1886 fn constant_names(out: &str) -> Vec<&str> {
1888 out.lines()
1889 .skip_while(|l| !l.starts_with("const ("))
1890 .skip(1)
1891 .take_while(|l| !l.starts_with(')'))
1892 .filter_map(|l| l.split_whitespace().next())
1893 .collect()
1894 }
1895
1896 fn assert_declares_each_constant_once(out: &str) {
1898 let names = constant_names(out);
1899 assert!(!names.is_empty(), "no constants at all:\n{out}");
1900 let mut seen = std::collections::HashSet::new();
1901 for name in &names {
1902 assert!(seen.insert(*name), "{name} is declared twice:\n{out}");
1903 }
1904 }
1905
1906 #[test]
1907 fn a_package_that_would_not_compile_is_refused_rather_than_emitted() {
1908 assert!(is_valid_package("mycli"));
1909 assert!(is_valid_package("mise_tables"));
1910 assert!(!is_valid_package("my-pkg"));
1911 assert!(!is_valid_package("7zip"));
1912 assert!(!is_valid_package(""));
1913 assert!(!is_valid_package("range"));
1914 assert!(!is_valid_package("_"));
1915 assert!(!is_valid_package("init"));
1916 assert!(is_valid_package("__"));
1917
1918 let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
1920 let out = generate(
1921 &spec,
1922 &GoOptions {
1923 package: Some("my-pkg".into()),
1924 },
1925 );
1926 assert!(out.contains("package mypkg"), "{out}");
1927 }
1928
1929 #[test]
1943 fn the_long_pages_text_reaches_the_tables() {
1944 let out = go(r#"
1945name "ex"
1946bin "ex"
1947about "Short."
1948about_long "Long."
1949before_long_help "ROOT-BEFORE"
1950after_long_help "ROOT-AFTER"
1951cmd "run" help="Run it" {
1952 before_long_help "RUN-BEFORE"
1953 after_long_help "RUN-AFTER"
1954}
1955"#);
1956 let meta = out
1957 .lines()
1958 .find(|l| l.contains("var HelpMeta"))
1959 .expect("a root header is emitted");
1960 assert!(
1961 meta.contains(r#"About: "Short.""#) && meta.contains(r#"LongAbout: "Long.""#),
1962 "the two abouts are separate fields: {meta}"
1963 );
1964 assert!(
1965 meta.contains(r#"BeforeLongHelp: "ROOT-BEFORE""#)
1966 && meta.contains(r#"AfterLongHelp: "ROOT-AFTER""#),
1967 "the root's long brackets are emitted: {meta}"
1968 );
1969
1970 let run = out
1971 .lines()
1972 .find(|l| l.contains("Short: \"Run it\""))
1973 .expect("the command has a help entry");
1974 assert!(
1975 run.contains(r#"BeforeLongHelp: "RUN-BEFORE""#)
1976 && run.contains(r#"AfterLongHelp: "RUN-AFTER""#),
1977 "a command's long brackets are emitted: {run}"
1978 );
1979 }
1980
1981 #[test]
1989 fn an_examples_help_line_reaches_the_tables() {
1990 let out = go(r#"
1991name "ex"
1992bin "ex"
1993cmd "run" help="Run it" {
1994 example "ex run --fast" header="Speed" help="When you are in a hurry"
1995 example "ex run"
1996}
1997"#);
1998 let run = out
1999 .lines()
2000 .find(|l| l.contains("Examples: []argv.Example"))
2001 .expect("the command's examples are emitted");
2002 assert!(
2003 run.contains(
2004 r#"{Header: "Speed", Code: "ex run --fast", Help: "When you are in a hurry"}"#
2005 ),
2006 "all three fields are emitted: {run}"
2007 );
2008 assert!(
2010 run.contains(r#"{Code: "ex run"}"#),
2011 "an example with no header emits no header: {run}"
2012 );
2013 }
2014
2015 #[test]
2021 fn heading_prose_reaches_the_tables() {
2022 let out = go(r#"
2023name "ex"
2024bin "ex"
2025cmd "run" help="Run it" {
2026 heading "Filters" help="Filters accumulate from left to right."
2027 flag "--allow <NAME>" help="Allow it" help_heading="Filters"
2028}
2029"#);
2030 let run = out
2031 .lines()
2032 .find(|l| l.contains("Headings: []argv.Heading"))
2033 .expect("the command's headings are emitted");
2034 assert!(
2035 run.contains(r#"{Title: "Filters", Help: "Filters accumulate from left to right."}"#),
2036 "both fields are emitted: {run}"
2037 );
2038 }
2039
2040 #[test]
2043 fn a_long_about_alone_does_not_become_the_short_one() {
2044 let out = go(r#"
2045name "ex"
2046bin "ex"
2047about_long "Long only."
2048"#);
2049 let meta = out
2050 .lines()
2051 .find(|l| l.contains("var HelpMeta"))
2052 .expect("a root header is emitted");
2053 assert!(
2054 !meta.contains(", About: ") && meta.contains(r#"LongAbout: "Long only.""#),
2055 "only the long one is set: {meta}"
2056 );
2057 }
2058
2059 #[test]
2060 fn a_default_subcommand_ignores_a_deeper_command_of_the_same_name() {
2061 let out = go(r#"
2062name "ex"
2063bin "ex"
2064default_subcommand "run"
2065cmd "oci" {
2066 cmd "run" {}
2067}
2068cmd "run" {
2069 arg "[args]..." var=#true
2070}
2071"#);
2072 assert!(
2073 out.contains("DefaultSubcommand: cmdRun,"),
2074 "should point at the root's own `run`, got:\n{out}"
2075 );
2076 }
2077
2078 #[test]
2079 fn command_builtin_controls_reach_generated_go_tables() {
2080 let out = go(r#"
2081name "ex"
2082bin "ex"
2083disable_help_flag #true
2084disable_help_subcommand #true
2085disable_version_flag #true
2086"#);
2087 let root = out
2088 .split("var Root = &argv.Command{")
2089 .nth(1)
2090 .expect("the root command should be emitted")
2091 .split("}\n")
2092 .next()
2093 .unwrap();
2094 assert!(root.contains("DisableHelpFlag:"), "{root}");
2095 assert!(root.contains("DisableHelpSubcommand:"), "{root}");
2096 assert!(root.contains("DisableVersionFlag:"), "{root}");
2097 }
2098
2099 #[test]
2102 fn a_default_subcommand_prefers_a_name_to_another_commands_alias() {
2103 let ordered = |first: &str, second: &str| {
2104 go(&format!(
2105 r#"
2106name "ex"
2107bin "ex"
2108default_subcommand "run"
2109{first}
2110{second}
2111"#
2112 ))
2113 };
2114 let alpha = "cmd \"alpha\" {\n alias \"run\"\n}";
2115 let run = "cmd \"run\" {\n arg \"[args]...\" var=#true\n}";
2116 for out in [ordered(alpha, run), ordered(run, alpha)] {
2117 assert!(
2118 out.contains("DefaultSubcommand: cmdRun,"),
2119 "should point at the command named `run`, got:\n{out}"
2120 );
2121 }
2122 }
2123
2124 #[test]
2125 fn an_external_subcommand_is_emitted_on_the_command_that_declares_it() {
2126 let out = go(r#"
2127name "ex"
2128bin "ex"
2129external_subcommand #true
2130cmd "install"
2131cmd "exec" external_subcommand=#true
2132"#);
2133 let block = |var: &str| {
2134 let start = out
2135 .find(&format!("var {var} ="))
2136 .unwrap_or_else(|| panic!("{var} should be emitted, got:\n{out}"));
2137 let rest = &out[start..];
2138 let end = rest[1..]
2139 .find("\nvar ")
2140 .map(|i| i + 1)
2141 .unwrap_or(rest.len());
2142 &rest[..end]
2143 };
2144 assert!(
2145 block("Root").contains("ExternalSubcommand: true"),
2146 "the root should forward unmatched words:\n{}",
2147 block("Root")
2148 );
2149 assert!(
2150 block("cmdExec").contains("ExternalSubcommand: true"),
2151 "a nested command can forward too:\n{}",
2152 block("cmdExec")
2153 );
2154 assert!(
2155 !block("cmdInstall").contains("ExternalSubcommand"),
2156 "a command that does not declare it should not carry it:\n{}",
2157 block("cmdInstall")
2158 );
2159 }
2160
2161 #[test]
2162 fn arg_required_else_help_reaches_the_table_and_typed_front_door() {
2163 let out = go(r#"
2164name "ex"
2165bin "ex"
2166cmd "run" arg_required_else_help=#true {
2167 flag "--all"
2168}
2169"#);
2170 assert!(
2171 out.contains("ArgRequiredElseHelp: true"),
2172 "the command table should carry the policy:\n{out}"
2173 );
2174 assert!(
2175 out.contains("p.Command().ArgRequiredElseHelp && p.CommandStart() == len(args)"),
2176 "the typed parser should enforce it before fallbacks:\n{out}"
2177 );
2178 }
2179
2180 #[test]
2181 fn subcommand_negates_requirements_reaches_generated_go() {
2182 let out = go(
2183 "name \"ex\"\nbin \"ex\"\nsubcommand_negates_reqs #true\nflag \"--config\" required=#true\ncmd \"run\"\n",
2184 );
2185 assert!(out.contains("SubcommandNegatesReqs: true"), "{out}");
2186 assert!(
2187 out.contains("checkRequirements := i == len(chain)-1 || !cmd.SubcommandNegatesReqs"),
2188 "{out}"
2189 );
2190 assert!(
2191 out.contains("CheckRelationshipsWithValuesAndRequirements"),
2192 "{out}"
2193 );
2194 }
2195
2196 #[test]
2197 fn argument_subcommand_conflicts_reach_generated_go() {
2198 let out = go(
2199 "name \"ex\"\nbin \"ex\"\nargs_conflicts_with_subcommands #true\nflag \"--verbose\"\ncmd \"run\"\n",
2200 );
2201 assert!(out.contains("ArgsConflictWithSubcommands: true"), "{out}");
2202 }
2203
2204 #[test]
2205 fn allow_missing_positional_reaches_generated_go() {
2206 let out = go(
2207 "name \"ex\"\nbin \"ex\"\nallow_missing_positional #true\narg \"[optional]\"\narg \"<required>\"\n",
2208 );
2209 assert!(out.contains("AllowMissingPositional: true"), "{out}");
2210 assert!(out.contains("Name: \"optional\""), "{out}");
2211 assert!(out.contains("Name: \"required\", Required: true"), "{out}");
2212 }
2213
2214 #[test]
2215 fn optional_flag_values_reach_generated_go() {
2216 let out = go("name \"ex\"\nbin \"ex\"\nflag \"--color [WHEN]\" value_optional=#true\n");
2217 assert!(
2218 out.contains("TakesValue: true, ValueOptional: true"),
2219 "{out}"
2220 );
2221 }
2222
2223 #[test]
2224 fn explicit_boolean_values_reach_generated_go() {
2225 let out = go(
2226 "name \"ex\"\nbin \"ex\"\nflag \"--color\" negate=\"--no-color\" bool_value=#true\n",
2227 );
2228 assert!(out.contains("BoolValue: true"), "{out}");
2229 assert!(out.contains("if ev.Flag.BoolValue"), "{out}");
2230 assert!(
2231 out.contains("given[ev.Flag.Key] = []string{ev.Value}"),
2232 "{out}"
2233 );
2234 assert!(
2235 out.contains("(ev.Value == \"true\") != ev.Negated"),
2236 "{out}"
2237 );
2238 }
2239
2240 #[test]
2241 fn flag_actions_reach_generated_go() {
2242 let out = go(
2243 "name \"ex\"\nbin \"ex\"\nflag \"--help-all\" action=\"help_all\"\nflag \"--version\" action=\"version\"\n",
2244 );
2245 assert!(out.contains("Action: argv.ActionHelpAll"), "{out}");
2246 assert!(out.contains("Action: argv.ActionVersion"), "{out}");
2247 }
2248
2249 #[test]
2250 fn granular_help_hides_reach_generated_go() {
2251 let out = go(
2252 "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",
2253 );
2254 for field in [
2255 "HideDefaultValue: true",
2256 "HideEnv: true",
2257 "HideEnvValues: true",
2258 "HidePossibleValues: true",
2259 "HideShortHelp: true",
2260 "HideLongHelp: true",
2261 ] {
2262 assert!(out.contains(field), "missing {field}:\n{out}");
2263 }
2264 }
2265
2266 #[test]
2267 fn strict_duplicate_policy_reaches_metadata() {
2268 let permissive = go("name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\n");
2269 assert!(!permissive.contains("RejectDuplicate"), "{permissive}");
2270
2271 let strict =
2272 go("name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\n");
2273 assert!(strict.contains("RejectDuplicate: true"), "{strict}");
2274 }
2275
2276 #[test]
2277 fn clause_args_and_later_entries_reach_go_cold_tables() {
2278 let out = go(r#"
2279name "ex"
2280bin "ex"
2281cmd "run" {
2282 flag "--needs-task" requires="task"
2283 clause "items" separator=":::" {
2284 arg "<task>" help="Task to run"
2285 }
2286}
2287cmd "later" {
2288 flag "--mode <mode>" help="Later flag"
2289}
2290"#);
2291 let meta = out
2292 .split_once("var Meta = argv.Metadata{")
2293 .and_then(|(_, rest)| rest.split_once("var HelpText = argv.HelpTable{"))
2294 .map(|(meta, _)| meta)
2295 .expect("generated metadata and help tables");
2296 let help = out
2297 .split_once("var HelpText = argv.HelpTable{")
2298 .and_then(|(_, rest)| rest.split_once("var HelpMeta = argv.HelpSpec{"))
2299 .map(|(help, _)| help)
2300 .expect("generated help table and metadata");
2301
2302 assert!(
2303 meta.contains("Key: ArgRunItemsTask, Name: \"task\""),
2304 "{out}"
2305 );
2306 assert!(
2307 meta.lines().any(|line| {
2308 line.contains("Key: FlagRunNeedsTask")
2309 && line.contains("Requires: []uint64{ArgRunItemsTask}")
2310 }),
2311 "{out}"
2312 );
2313 assert!(meta.contains("Key: FlagLaterMode, Name: \"mode\""), "{out}");
2314 assert!(
2315 help.contains("Key: ArgRunItemsTask, Demanded: true"),
2316 "{out}"
2317 );
2318 assert!(
2319 help.lines().any(|line| {
2320 line.contains("Key: FlagLaterMode") && line.contains("Short: \"Later flag\"")
2321 }),
2322 "{out}"
2323 );
2324 for generated in [
2325 "type RunCmdItemsClause struct {",
2326 "case argv.KindClauseSeparator:",
2327 "for _, instance := range clauseInstances[CmdRun] {",
2328 "if err := argv.Check(Meta.Lookup(key), values, 0); err != nil {",
2329 "clauseSources := map[uint64]argv.Source{ArgRunItemsTask: argv.Unset}",
2330 "cmdRunV.Items = append(cmdRunV.Items, item)",
2331 ] {
2332 assert!(out.contains(generated), "missing {generated:?}:\n{out}");
2333 }
2334 assert!(
2335 out.lines().any(|line| {
2336 line.contains("Items")
2337 && line.contains("[]RunCmdItemsClause")
2338 && line.contains("// clause items")
2339 }),
2340 "{out}"
2341 );
2342 }
2343
2344 #[test]
2345 fn strict_negated_flags_track_each_spelling_separately() {
2346 let out = go(
2347 "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n",
2348 );
2349 assert!(out.contains("polaritySeen := map[uint64]uint8{}"), "{out}");
2350 assert!(
2351 out.contains("polaritySeen[ev.Flag.Key]&polarity != 0"),
2352 "{out}"
2353 );
2354 assert!(out.contains("if duplicateSeen[key]"), "{out}");
2355 }
2356
2357 #[test]
2358 fn strict_global_duplicate_tracking_resets_at_subcommands() {
2359 let out = go(
2360 "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\" global=#true\ncmd \"run\" {\n args_override_self #false\n}\n",
2361 );
2362 assert!(
2363 out.contains("levelSeen = map[uint64]int{}"),
2364 "a subcommand should start a new duplicate scope:\n{out}"
2365 );
2366 assert!(out.contains("strictSeen[ev.Flag.Key] = true"), "{out}");
2367 assert!(out.contains("if strictSeen[key]"), "{out}");
2368 }
2369
2370 #[test]
2372 fn a_subcommand_named_root_does_not_collide_with_the_root() {
2373 let out = go(r#"
2374name "ex"
2375bin "ex"
2376cmd "root" {
2377 flag "--wat"
2378}
2379"#);
2380 let declared = |name: &str| {
2383 out.lines()
2384 .filter(|l| l.split_whitespace().next() == Some(name))
2385 .count()
2386 };
2387 assert_eq!(declared("CmdRoot"), 1, "CmdRoot declared twice:\n{out}");
2388 assert_eq!(declared("CmdRoot2"), 1, "no distinct key for it:\n{out}");
2389 assert_declares_each_constant_once(&out);
2390 }
2391
2392 #[test]
2397 fn only_the_per_occurrence_bound_reaches_the_table() {
2398 let out = go(r#"
2399name "ex"
2400bin "ex"
2401flag "--include <pattern>..." {
2402 arg "<pattern>..." var=#true var_min=2 var_max=2
2403}
2404flag "--tag <t>" var=#true var_max=1
2405"#);
2406 assert!(
2407 out.contains("Name: \"include\", Longs: []string{\"include\"}, TakesValue: true, Variadic: true, VarMax: 2"),
2408 "{out}"
2409 );
2410 assert!(
2411 out.contains("Name: \"include\", Flag: true") && out.contains("VarMin: 2"),
2412 "the nested value minimum must reach post-binding metadata:\n{out}"
2413 );
2414 let tag = out.lines().find(|l| l.contains("\"tag\"")).unwrap();
2415 assert!(!tag.contains("VarMax"), "occurrence bound leaked: {tag}");
2416 }
2417
2418 #[test]
2419 fn exact_arity_with_one_label_reaches_go_help() {
2420 let out = go(r#"
2421name "ex"
2422bin "ex"
2423flag "--pair <ITEM>..." {
2424 arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2425 value_names "ITEM"
2426 }
2427}
2428arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2429 value_names "ITEM"
2430}
2431"#);
2432 assert_eq!(out.matches("ValueArity: 2").count(), 2, "{out}");
2433 assert_eq!(
2434 out.matches("ValueNames: []string{\"ITEM\"}").count(),
2435 2,
2436 "{out}"
2437 );
2438 }
2439
2440 #[test]
2441 fn allow_hyphen_values_reaches_the_table() {
2442 let out = go(r#"
2443name "ex"
2444bin "ex"
2445flag "--args <ARGS>" allow_hyphen_values=#true
2446"#);
2447 assert!(
2448 out.contains("Name: \"args\", Longs: []string{\"args\"}, TakesValue: true, AllowHyphenValues: true"),
2449 "{out}"
2450 );
2451 }
2452
2453 #[test]
2454 fn require_equals_reaches_the_table() {
2455 let out = go(r#"
2456name "ex"
2457bin "ex"
2458flag "--inspect <PORT>" require_equals=#true
2459"#);
2460 assert!(
2461 out.contains("Name: \"inspect\", Longs: []string{\"inspect\"}, TakesValue: true, RequireEquals: true"),
2462 "{out}"
2463 );
2464 }
2465
2466 #[test]
2467 fn default_missing_reaches_the_table() {
2468 let out = go(r#"
2469name "ex"
2470bin "ex"
2471flag "--color <WHEN>" default_missing="always"
2472"#);
2473 assert!(
2474 out.contains(
2475 "Name: \"color\", Longs: []string{\"color\"}, TakesValue: true, DefaultMissing: \"always\""
2476 ),
2477 "{out}"
2478 );
2479 }
2480
2481 #[test]
2487 fn a_relationship_resolves_through_scope_and_negation() {
2488 let out = go(r#"
2489name "ex"
2490bin "ex"
2491flag "--quiet" global=#true
2492flag "--color" negate="--no-color"
2493flag "--plain" conflicts="--no-color"
2494cmd "run" {
2495 flag "--loud" conflicts="--quiet"
2496 flag "--solo" conflicts="--plain"
2497}
2498"#);
2499 assert!(out.contains("Conflicts: []uint64{FlagColor}"), "{out}");
2501 assert!(out.contains("Conflicts: []uint64{FlagQuiet}"), "{out}");
2503 assert!(
2506 !entry_of(&out, "solo").contains("Conflicts"),
2507 "a non-global should not resolve from below:\n{out}"
2508 );
2509 }
2510
2511 #[test]
2512 fn positional_conflicts_reach_go_metadata_in_both_directions() {
2513 let out = go(r#"
2514name "ex"
2515bin "ex"
2516flag "--from-file <file>" conflicts="value"
2517arg "[value]" conflicts="--from-file"
2518"#);
2519
2520 assert!(
2521 entry_of(&out, "from-file").contains("Conflicts: []uint64{ArgValue}"),
2522 "{out}"
2523 );
2524 assert!(
2525 out.lines().any(|line| {
2526 line.contains("{Key: ArgValue, Name: \"value\"")
2527 && line.contains("Conflicts: []uint64{FlagFromFile}")
2528 }),
2529 "{out}"
2530 );
2531 }
2532
2533 #[test]
2534 fn a_value_conditional_requirement_reaches_go_metadata() {
2535 let out = go(r#"
2536name "ex"
2537bin "ex"
2538flag "--format <format>" {
2539 requires_if "json" "--schema"
2540}
2541flag "--schema <file>"
2542"#);
2543 assert!(
2544 entry_of(&out, "format").contains(
2545 "RequiresIf: []argv.ValueRequirement{{Value: \"json\", Key: FlagSchema}}"
2546 ),
2547 "{out}"
2548 );
2549 assert!(
2550 out.contains("argv.CheckRelationshipsWithValues"),
2551 "the emitted parser must enforce the metadata:\n{out}"
2552 );
2553 }
2554
2555 #[test]
2556 fn required_if_eq_makes_generated_go_supply_values() {
2557 let out = go(r#"
2558name "ex"
2559bin "ex"
2560flag "--token <token>" {
2561 required_if_eq "--mode" "remote"
2562}
2563flag "--mode <mode>"
2564"#);
2565 assert!(
2566 entry_of(&out, "token").contains(
2567 "RequiredIfEq: []argv.ValueCondition{{Key: FlagMode, Value: \"remote\"}}"
2568 ),
2569 "{out}"
2570 );
2571 assert!(out.contains("resolved := map[uint64][]string{}"), "{out}");
2572 assert!(out.contains("argv.CheckRelationshipsWithValues"), "{out}");
2573 }
2574
2575 #[test]
2576 fn boolean_sources_are_normalized_for_value_relationships() {
2577 let out = go(r#"
2578name "ex"
2579bin "ex"
2580flag "--token <token>" {
2581 required_if_eq "--mode" "true"
2582}
2583flag "--mode" negate="--no-mode" bool_value=#true
2584"#);
2585 assert!(
2586 entry_of(&out, "mode").contains("RequiresIfBoolean: true"),
2587 "{out}"
2588 );
2589 }
2590
2591 #[test]
2592 fn a_conditional_default_reaches_go_metadata() {
2593 let out = go(r#"
2594name "ex"
2595bin "ex"
2596flag "--bin-names" {
2597 default_if "--json" "true"
2598 default_if "--output" "json" "pretty"
2599}
2600flag "--json"
2601flag "--output <fmt>"
2602"#);
2603 assert!(
2604 entry_of(&out, "bin-names")
2605 .contains("DefaultIf: []argv.DefaultIf{{Key: FlagJson, Value: \"true\"}"),
2606 "{out}"
2607 );
2608 assert!(
2609 entry_of(&out, "bin-names").contains("When: \"json\""),
2610 "{out}"
2611 );
2612 assert!(
2613 out.contains("argv.ApplyDefaultIf"),
2614 "the emitted parser must apply the metadata:\n{out}"
2615 );
2616 assert!(
2617 out.contains("negated[ev.Flag.Key] = ev.Negated"),
2618 "Equals default_if needs the negate form:\n{out}"
2619 );
2620 }
2621
2622 #[test]
2626 fn a_relationship_needs_the_right_form() {
2627 let out = go(r#"
2628name "ex"
2629bin "ex"
2630flag "-q --quiet"
2631flag "--color"
2632flag "--a" conflicts="--q"
2633flag "--b" conflicts="-color"
2634flag "--c" conflicts="-q"
2635flag "--d" conflicts="--color"
2636"#);
2637 assert!(!entry_of(&out, "a").contains("Conflicts"), "{out}");
2639 assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
2640 assert!(
2642 entry_of(&out, "c").contains("Conflicts: []uint64{FlagQuiet}"),
2643 "{out}"
2644 );
2645 assert!(
2646 entry_of(&out, "d").contains("Conflicts: []uint64{FlagColor}"),
2647 "{out}"
2648 );
2649 }
2650
2651 #[test]
2658 fn an_ordinary_form_beats_another_flags_negation() {
2659 let out = go(r#"
2660name "ex"
2661bin "ex"
2662flag "--a" negate="--zap"
2663flag "--zap"
2664flag "--p" conflicts="--zap"
2665"#);
2666 assert!(
2667 entry_of(&out, "p").contains("Conflicts: []uint64{FlagZap}"),
2668 "should name the flag `--zap` binds, not the one negating to it:\n{out}"
2669 );
2670 }
2671
2672 #[test]
2674 fn a_single_dash_negation_is_named_by_its_own_form() {
2675 let out = go(r#"
2676name "ex"
2677bin "ex"
2678flag "--tint" negate="-no-tint"
2679flag "--plain" conflicts="-no-tint"
2680flag "--other" conflicts="--no-tint"
2681"#);
2682 assert!(
2683 entry_of(&out, "plain").contains("Conflicts: []uint64{FlagTint}"),
2684 "the exact form should resolve:\n{out}"
2685 );
2686 assert!(
2688 !entry_of(&out, "other").contains("Conflicts"),
2689 "`--no-tint` is not how it was declared:\n{out}"
2690 );
2691 }
2692
2693 #[test]
2695 fn a_negation_is_matched_as_written() {
2696 let out = go(r#"
2697name "ex"
2698bin "ex"
2699flag "--color" negate="--no-color"
2700flag "--tint" negate="-no-tint"
2701flag "--a" conflicts="--no-color"
2702flag "--b" conflicts="--no-tint"
2703"#);
2704 assert!(
2705 entry_of(&out, "a").contains("Conflicts: []uint64{FlagColor}"),
2706 "{out}"
2707 );
2708 assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
2711 }
2712
2713 #[test]
2714 fn strings_are_escaped_to_go_rules() {
2715 assert_eq!(go_string(r#"a"b\c"#), r#""a\"b\\c""#);
2716 assert_eq!(go_string("tab\there"), r#""tab\there""#);
2717 assert_eq!(go_string("\u{7f}"), r#""\x7f""#);
2719 }
2720}