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