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 format!("{{{}}}", fields.join(", "))
1132}
1133
1134fn flag_help(flag: &SpecFlag, named: &Named) -> String {
1135 let mut fields = vec![format!("Key: {}", named.key)];
1136 if let Some(message) = &flag.deprecated {
1137 fields.push(format!("Deprecated: {}", go_string(message)));
1138 }
1139 if let Some(at) = &flag.deprecated_warn_at {
1140 fields.push(format!("DeprecatedWarnAt: {}", go_string(at)));
1141 }
1142 if let Some(at) = &flag.deprecated_remove_at {
1143 fields.push(format!("DeprecatedRemoveAt: {}", go_string(at)));
1144 }
1145 if flag.hide {
1146 fields.push("Hide: true".to_string());
1147 }
1148 if let Some(order) = flag.display_order {
1149 fields.push(format!("DisplayOrder: {order}"));
1150 fields.push("DisplayOrderSet: true".to_string());
1151 }
1152 for (name, hidden) in [
1153 ("HideDefaultValue", flag.hide_default_value),
1154 ("HideEnv", flag.hide_env),
1155 ("HideEnvValues", flag.hide_env_values),
1156 ("HidePossibleValues", flag.hide_possible_values),
1157 ("HideShortHelp", flag.hide_short_help),
1158 ("HideLongHelp", flag.hide_long_help),
1159 ] {
1160 if hidden {
1161 fields.push(format!("{name}: true"));
1162 }
1163 }
1164 if flag.required && flag.default.is_empty() {
1167 fields.push("Demanded: true".to_string());
1168 }
1169 if flag.var {
1170 fields.push("Repeatable: true".to_string());
1171 }
1172 if let Some(arg) = &flag.arg {
1173 if arg.name != flag.name {
1174 fields.push(format!("ValueName: {}", go_string(&arg.name)));
1175 }
1176 if arg.required && arg.default.is_empty() {
1180 fields.push("ValueDemanded: true".to_string());
1181 }
1182 if !arg.value_names.is_empty() {
1183 fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1184 }
1185 if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1186 fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1187 }
1188 }
1189 if let Some(help) = flag.help.as_deref().or(flag.help_first_line.as_deref()) {
1192 fields.push(format!("Short: {}", go_string(help)));
1193 }
1194 if let Some(long) = flag.help_long.as_deref().or(flag.help.as_deref()) {
1195 fields.push(format!("Long: {}", go_string(long)));
1196 }
1197 if let Some(heading) = &flag.help_heading {
1198 fields.push(format!("Heading: {}", go_string(heading)));
1199 }
1200 if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) {
1202 fields.push(format!(
1203 "Choices: {}",
1204 string_slice(&visible_choices(choices))
1205 ));
1206 }
1207 if let Some(env) = &flag.env {
1208 fields.push(format!("Env: {}", go_string(env)));
1209 }
1210 if !flag.env_fallback.is_empty() {
1211 fields.push(format!("EnvFallback: {}", string_slice(&flag.env_fallback)));
1212 }
1213 if !flag.deprecated_env.is_empty() {
1214 fields.push(format!(
1215 "DeprecatedEnv: {}",
1216 string_slice(&flag.deprecated_env)
1217 ));
1218 }
1219 let default = if !flag.default.is_empty() {
1220 &flag.default
1221 } else {
1222 flag.arg
1223 .as_ref()
1224 .map(|a| &a.default)
1225 .unwrap_or(&flag.default)
1226 };
1227 if !default.is_empty() {
1228 fields.push(format!("Default: {}", string_slice(default)));
1229 }
1230 format!("{{{}}}", fields.join(", "))
1231}
1232
1233fn arg_help(arg: &SpecArg, named: &Named) -> String {
1234 let mut fields = vec![format!("Key: {}", named.key)];
1235 if let Some(order) = arg.display_order {
1236 fields.push(format!("DisplayOrder: {order}"));
1237 fields.push("DisplayOrderSet: true".to_string());
1238 }
1239 if arg.hide {
1240 fields.push("Hide: true".to_string());
1241 }
1242 for (name, hidden) in [
1243 ("HideDefaultValue", arg.hide_default_value),
1244 ("HideEnv", arg.hide_env),
1245 ("HideEnvValues", arg.hide_env_values),
1246 ("HidePossibleValues", arg.hide_possible_values),
1247 ("HideShortHelp", arg.hide_short_help),
1248 ("HideLongHelp", arg.hide_long_help),
1249 ] {
1250 if hidden {
1251 fields.push(format!("{name}: true"));
1252 }
1253 }
1254 if arg.required && arg.default.is_empty() {
1255 fields.push("Demanded: true".to_string());
1256 }
1257 if !arg.value_names.is_empty() {
1258 fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1259 }
1260 if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1261 fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1262 }
1263 if let Some(help) = arg.help.as_deref().or(arg.help_first_line.as_deref()) {
1264 fields.push(format!("Short: {}", go_string(help)));
1265 }
1266 if let Some(long) = arg.help_long.as_deref().or(arg.help.as_deref()) {
1267 fields.push(format!("Long: {}", go_string(long)));
1268 }
1269 if let Some(heading) = &arg.help_heading {
1270 fields.push(format!("Heading: {}", go_string(heading)));
1271 }
1272 if let Some(choices) = &arg.choices {
1273 fields.push(format!(
1274 "Choices: {}",
1275 string_slice(&visible_choices(choices))
1276 ));
1277 }
1278 if let Some(env) = &arg.env {
1279 fields.push(format!("Env: {}", go_string(env)));
1280 }
1281 if !arg.env_fallback.is_empty() {
1282 fields.push(format!("EnvFallback: {}", string_slice(&arg.env_fallback)));
1283 }
1284 if !arg.deprecated_env.is_empty() {
1285 fields.push(format!(
1286 "DeprecatedEnv: {}",
1287 string_slice(&arg.deprecated_env)
1288 ));
1289 }
1290 if !arg.default.is_empty() {
1291 fields.push(format!("Default: {}", string_slice(&arg.default)));
1292 }
1293 format!("{{{}}}", fields.join(", "))
1294}
1295
1296enum Line {
1305 Field(String, String),
1307 Block(Vec<String>),
1309}
1310
1311fn render(out: &mut String, indent: &str, lines: &[Line]) {
1313 let mut run: Vec<(&String, &String)> = Vec::new();
1314
1315 fn flush(out: &mut String, indent: &str, run: &mut Vec<(&String, &String)>) {
1316 let width = run.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
1317 for (key, value) in run.iter() {
1318 let _ = writeln!(
1319 out,
1320 "{indent}{key}:{:width$} {value},",
1321 "",
1322 width = width - key.len()
1323 );
1324 }
1325 run.clear();
1326 }
1327
1328 for line in lines {
1329 match line {
1330 Line::Field(key, value) => run.push((key, value)),
1331 Line::Block(block) => {
1332 flush(out, indent, &mut run);
1333 for l in block {
1334 let _ = writeln!(out, "{indent}{l}");
1335 }
1336 }
1337 }
1338 }
1339 flush(out, indent, &mut run);
1340}
1341
1342struct Emitted {
1344 named: Named,
1345 cmd: SpecCommand,
1346 flags: Vec<(SpecFlag, Named)>,
1347 args: Vec<(SpecArg, Named)>,
1348 subcommands: Vec<usize>,
1350 root: bool,
1351}
1352
1353fn effective_unknown_flags(spec: &Spec, commands: &[Emitted], at: usize) -> UnknownFlags {
1360 let path = &commands[at].cmd.full_cmd;
1361 for depth in (0..=path.len()).rev() {
1362 let ancestor = commands
1363 .iter()
1364 .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]);
1365 if let Some(mode) = ancestor.and_then(|e| e.cmd.unknown_flags) {
1366 return mode;
1367 }
1368 }
1369 spec.unknown_flags.unwrap_or_default()
1370}
1371
1372fn flag_literal(flag: &SpecFlag, named: &Named) -> String {
1373 let mut fields = vec![
1374 format!("Key: {}", named.key),
1375 format!("Name: {}", go_string(&flag.name)),
1376 ];
1377 if !flag.long.is_empty() {
1378 let longs = flag
1379 .long
1380 .iter()
1381 .map(|l| go_string(l))
1382 .collect::<Vec<_>>()
1383 .join(", ");
1384 fields.push(format!("Longs: []string{{{longs}}}"));
1385 }
1386 if !flag.hidden_aliases.is_empty() {
1387 fields.push(format!(
1388 "HiddenLongs: {}",
1389 string_slice(&flag.hidden_aliases)
1390 ));
1391 }
1392 if !flag.short.is_empty() {
1393 let shorts = flag
1394 .short
1395 .iter()
1396 .map(|c| go_byte(*c))
1397 .collect::<Vec<_>>()
1398 .join(", ");
1399 fields.push(format!("Shorts: []byte{{{shorts}}}"));
1400 }
1401 if !flag.hidden_short_aliases.is_empty() {
1402 let shorts = flag
1403 .hidden_short_aliases
1404 .iter()
1405 .map(|c| go_byte(*c))
1406 .collect::<Vec<_>>()
1407 .join(", ");
1408 fields.push(format!("HiddenShorts: []byte{{{shorts}}}"));
1409 }
1410 if let Some(negate) = &flag.negate {
1411 fields.push(format!(
1414 "Negate: {}",
1415 go_string(negate.trim_start_matches('-'))
1416 ));
1417 }
1418 if flag.arg.is_some() {
1419 fields.push("TakesValue: true".to_string());
1420 }
1421 if flag.value_optional {
1422 fields.push("ValueOptional: true".to_string());
1423 }
1424 if flag.bool_value {
1425 fields.push("BoolValue: true".to_string());
1426 }
1427 let action = match flag.action {
1428 SpecFlagAction::Set => None,
1429 SpecFlagAction::Help => Some("argv.ActionHelp"),
1430 SpecFlagAction::HelpShort => Some("argv.ActionHelpShort"),
1431 SpecFlagAction::HelpLong => Some("argv.ActionHelpLong"),
1432 SpecFlagAction::HelpAll => Some("argv.ActionHelpAll"),
1433 SpecFlagAction::Version => Some("argv.ActionVersion"),
1434 };
1435 if let Some(action) = action {
1436 fields.push(format!("Action: {action}"));
1437 }
1438 if let Some(arg) = flag.arg.as_ref().filter(|a| a.var) {
1443 fields.push("Variadic: true".to_string());
1444 if let Some(max) = arg.var_max {
1445 fields.push(format!("VarMax: {}", clamp_var_max(max)));
1446 }
1447 }
1448 if flag.allow_hyphen_values() {
1449 fields.push("AllowHyphenValues: true".to_string());
1450 }
1451 if let Some(arg) = &flag.arg {
1452 if arg.allow_negative_numbers {
1453 fields.push("AllowNegativeNumbers: true".to_string());
1454 }
1455 if let Some(terminator) = &arg.value_terminator {
1456 fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1457 }
1458 if let Some(delimiter) = arg.delimiter {
1459 fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1460 }
1461 }
1462 if flag.require_equals {
1463 fields.push("RequireEquals: true".to_string());
1464 }
1465 if let Some(missing) = &flag.default_missing {
1466 fields.push(format!("DefaultMissing: {}", go_string(missing)));
1467 }
1468 if flag.global {
1469 fields.push("Global: true".to_string());
1470 }
1471 format!("{{{}}}", fields.join(", "))
1472}
1473
1474fn arg_literal(arg: &SpecArg, named: &Named) -> String {
1475 let mut fields = vec![
1476 format!("Key: {}", named.key),
1477 format!("Name: {}", go_string(&arg.name)),
1478 ];
1479 if arg.required {
1480 fields.push("Required: true".to_string());
1481 }
1482 if arg.var {
1483 fields.push("Var: true".to_string());
1484 if let Some(max) = arg.var_max {
1485 fields.push(format!("VarMax: {}", clamp_var_max(max)));
1486 }
1487 }
1488 if arg.allow_negative_numbers {
1489 fields.push("AllowNegativeNumbers: true".to_string());
1490 }
1491 if let Some(terminator) = &arg.value_terminator {
1492 fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1493 }
1494 if let Some(delimiter) = arg.delimiter {
1495 fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1496 }
1497 let double_dash = match arg.double_dash {
1498 SpecDoubleDashChoices::Required => Some("argv.DoubleDashRequired"),
1499 SpecDoubleDashChoices::Preserve => Some("argv.DoubleDashPreserve"),
1500 SpecDoubleDashChoices::Automatic => Some("argv.DoubleDashAutomatic"),
1501 _ => None,
1502 };
1503 if let Some(dd) = double_dash {
1504 fields.push(format!("DoubleDash: {dd}"));
1505 }
1506 format!("{{{}}}", fields.join(", "))
1507}
1508
1509fn clamp_var_max(max: usize) -> u32 {
1514 u32::try_from(max).unwrap_or(u32::MAX)
1515}
1516
1517const GO_KEYWORDS: &[&str] = &[
1522 "break",
1523 "case",
1524 "chan",
1525 "const",
1526 "continue",
1527 "default",
1528 "defer",
1529 "else",
1530 "fallthrough",
1531 "for",
1532 "func",
1533 "go",
1534 "goto",
1535 "if",
1536 "import",
1537 "interface",
1538 "map",
1539 "package",
1540 "range",
1541 "return",
1542 "select",
1543 "struct",
1544 "switch",
1545 "type",
1546 "var",
1547];
1548
1549const UNUSABLE_PACKAGE_NAMES: &[&str] = &["_", "init"];
1562
1563pub fn is_valid_package(name: &str) -> bool {
1568 !name.is_empty()
1569 && !GO_KEYWORDS.contains(&name)
1570 && !UNUSABLE_PACKAGE_NAMES.contains(&name)
1571 && !name.starts_with(|c: char| c.is_ascii_digit())
1572 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1573}
1574
1575fn field_name(name: &str) -> String {
1577 let ident = format!("{}", AsPascalCase(name));
1578 if ident.is_empty() || ident.starts_with(|c: char| c.is_ascii_digit()) {
1579 format!("X{ident}")
1580 } else {
1581 ident
1582 }
1583}
1584
1585fn package_ident(bin: &str) -> String {
1593 let lowered: String = bin
1594 .chars()
1595 .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
1596 .collect::<String>()
1597 .to_ascii_lowercase();
1598 if is_valid_package(&lowered) {
1599 lowered
1600 } else {
1601 format!("cli{lowered}")
1605 }
1606}
1607
1608fn go_string(s: &str) -> String {
1613 let mut out = String::with_capacity(s.len() + 2);
1614 out.push('"');
1615 for c in s.chars() {
1616 match c {
1617 '"' => out.push_str("\\\""),
1618 '\\' => out.push_str("\\\\"),
1619 '\n' => out.push_str("\\n"),
1620 '\r' => out.push_str("\\r"),
1621 '\t' => out.push_str("\\t"),
1622 c if (c as u32) < 0x20 || c as u32 == 0x7f => {
1623 let _ = write!(out, "\\x{:02x}", c as u32);
1624 }
1625 c => out.push(c),
1626 }
1627 }
1628 out.push('"');
1629 out
1630}
1631
1632fn go_byte(c: char) -> String {
1638 match c {
1639 '\'' => "'\\''".to_string(),
1640 '\\' => "'\\\\'".to_string(),
1641 c if c.is_ascii_graphic() => format!("'{c}'"),
1642 c => format!("0x{:02x}", (c as u32) & 0xff),
1643 }
1644}
1645
1646#[cfg(test)]
1647mod tests {
1648 use super::*;
1649
1650 fn entry_of(out: &str, name: &str) -> String {
1659 out.lines()
1660 .find(|l| l.contains(&format!("Name: \"{name}\", Flag: true")))
1661 .unwrap_or_default()
1662 .to_string()
1663 }
1664
1665 fn go(kdl: &str) -> String {
1666 let spec: Spec = kdl.parse().expect("the fixture spec should parse");
1667 generate(&spec, &GoOptions::default())
1668 }
1669
1670 #[test]
1671 fn a_whole_cli() {
1672 let out = go(r#"
1673name "ex"
1674bin "ex"
1675version "1.2.3"
1676long_version "1.2.3\ncommit abc123"
1677flag "-v --verbose" global=#true help="be loud"
1678flag "--color" negate="--no-color"
1679flag "-j --jobs <n>"
1680flag "--include <pattern>..." var_max=3
1681arg "<file>"
1682arg "[rest]..." var=#true
1683cmd "install" {
1684 alias "i"
1685 flag "-f --force"
1686 arg "<pkg>"
1687}
1688cmd "config" {
1689 cmd "ls" {
1690 flag "--no-header"
1691 }
1692}
1693"#);
1694 insta::assert_snapshot!(out);
1695 }
1696
1697 #[test]
1698 fn rich_choices_keep_acceptance_visibility_and_strictness_separate() {
1699 let out = go(r#"
1700name "ex"
1701bin "ex"
1702flag "--color <when>" {
1703 choices ignore_case=#true strict=#false {
1704 choice "always" {
1705 alias "yes"
1706 alias "on" hide=#true
1707 }
1708 choice "never" hide=#true
1709 }
1710}
1711"#);
1712 let entry = entry_of(&out, "color");
1713 assert!(
1714 entry.contains(r#"Choices: []string{"always", "yes"}"#),
1715 "{entry}"
1716 );
1717 assert!(
1718 entry.contains(r#"AcceptedChoices: []string{"always", "never", "yes", "on"}"#),
1719 "{entry}"
1720 );
1721 assert!(entry.contains("IgnoreCase: true"), "{entry}");
1722 assert!(entry.contains("AllowUnknownChoices: true"), "{entry}");
1723 }
1724
1725 #[test]
1727 fn unknown_flags_are_inherited_and_overridable() {
1728 let out = go(r#"
1729name "ex"
1730bin "ex"
1731unknown_flags "error"
1732cmd "strict" {
1733 cmd "deep" {}
1734}
1735cmd "exec" unknown_flags="value" {
1736 cmd "nested" {}
1737}
1738"#);
1739 insta::assert_snapshot!(out);
1740 }
1741
1742 #[test]
1745 fn colliding_names_get_distinct_identifiers() {
1746 let out = go(r#"
1747name "ex"
1748bin "ex"
1749cmd "macos-defaults" {
1750 flag "--apply"
1751}
1752cmd "macos" {
1753 cmd "defaults" {
1754 flag "--apply"
1755 }
1756}
1757"#);
1758 insta::assert_snapshot!(out);
1759 }
1760
1761 #[test]
1762 fn a_default_subcommand_points_into_the_tree() {
1763 let out = go(r#"
1764name "ex"
1765bin "ex"
1766default_subcommand "run"
1767arg "[task]"
1768cmd "run" {
1769 arg "[args]..." var=#true
1770}
1771"#);
1772 insta::assert_snapshot!(out);
1773 }
1774
1775 #[test]
1776 fn a_bin_name_that_is_not_an_identifier_still_gives_a_package() {
1777 assert_eq!(package_ident("my-cli"), "mycli");
1778 assert_eq!(package_ident("7zip"), "cli7zip");
1779 assert_eq!(package_ident(""), "cli");
1780 assert_eq!(package_ident("go"), "cligo");
1782 assert_eq!(package_ident("type"), "clitype");
1783 assert_eq!(package_ident("_"), "cli_");
1786 assert_eq!(package_ident("init"), "cliinit");
1787 assert_eq!(package_ident("__"), "__");
1789 assert_eq!(package_ident("initialize"), "initialize");
1790
1791 for bin in [
1795 "my-cli", "7zip", "", "go", "type", "_", "init", "__", "MiSe",
1796 ] {
1797 let out = package_ident(bin);
1798 assert!(is_valid_package(&out), "{bin:?} sanitized to {out:?}");
1799 }
1800 }
1801
1802 #[test]
1804 fn a_name_matching_a_generated_suffix_still_gets_its_own() {
1805 let out = go(r#"
1806name "ex"
1807bin "ex"
1808cmd "macos-defaults" {}
1809cmd "macos" {
1810 cmd "defaults" {}
1811}
1812cmd "macos-defaults2" {}
1813"#);
1814 assert_declares_each_constant_once(&out);
1819 }
1820
1821 fn constant_names(out: &str) -> Vec<&str> {
1823 out.lines()
1824 .skip_while(|l| !l.starts_with("const ("))
1825 .skip(1)
1826 .take_while(|l| !l.starts_with(')'))
1827 .filter_map(|l| l.split_whitespace().next())
1828 .collect()
1829 }
1830
1831 fn assert_declares_each_constant_once(out: &str) {
1833 let names = constant_names(out);
1834 assert!(!names.is_empty(), "no constants at all:\n{out}");
1835 let mut seen = std::collections::HashSet::new();
1836 for name in &names {
1837 assert!(seen.insert(*name), "{name} is declared twice:\n{out}");
1838 }
1839 }
1840
1841 #[test]
1842 fn a_package_that_would_not_compile_is_refused_rather_than_emitted() {
1843 assert!(is_valid_package("mycli"));
1844 assert!(is_valid_package("mise_tables"));
1845 assert!(!is_valid_package("my-pkg"));
1846 assert!(!is_valid_package("7zip"));
1847 assert!(!is_valid_package(""));
1848 assert!(!is_valid_package("range"));
1849 assert!(!is_valid_package("_"));
1850 assert!(!is_valid_package("init"));
1851 assert!(is_valid_package("__"));
1852
1853 let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
1855 let out = generate(
1856 &spec,
1857 &GoOptions {
1858 package: Some("my-pkg".into()),
1859 },
1860 );
1861 assert!(out.contains("package mypkg"), "{out}");
1862 }
1863
1864 #[test]
1878 fn the_long_pages_text_reaches_the_tables() {
1879 let out = go(r#"
1880name "ex"
1881bin "ex"
1882about "Short."
1883about_long "Long."
1884before_long_help "ROOT-BEFORE"
1885after_long_help "ROOT-AFTER"
1886cmd "run" help="Run it" {
1887 before_long_help "RUN-BEFORE"
1888 after_long_help "RUN-AFTER"
1889}
1890"#);
1891 let meta = out
1892 .lines()
1893 .find(|l| l.contains("var HelpMeta"))
1894 .expect("a root header is emitted");
1895 assert!(
1896 meta.contains(r#"About: "Short.""#) && meta.contains(r#"LongAbout: "Long.""#),
1897 "the two abouts are separate fields: {meta}"
1898 );
1899 assert!(
1900 meta.contains(r#"BeforeLongHelp: "ROOT-BEFORE""#)
1901 && meta.contains(r#"AfterLongHelp: "ROOT-AFTER""#),
1902 "the root's long brackets are emitted: {meta}"
1903 );
1904
1905 let run = out
1906 .lines()
1907 .find(|l| l.contains("Short: \"Run it\""))
1908 .expect("the command has a help entry");
1909 assert!(
1910 run.contains(r#"BeforeLongHelp: "RUN-BEFORE""#)
1911 && run.contains(r#"AfterLongHelp: "RUN-AFTER""#),
1912 "a command's long brackets are emitted: {run}"
1913 );
1914 }
1915
1916 #[test]
1924 fn an_examples_help_line_reaches_the_tables() {
1925 let out = go(r#"
1926name "ex"
1927bin "ex"
1928cmd "run" help="Run it" {
1929 example "ex run --fast" header="Speed" help="When you are in a hurry"
1930 example "ex run"
1931}
1932"#);
1933 let run = out
1934 .lines()
1935 .find(|l| l.contains("Examples: []argv.Example"))
1936 .expect("the command's examples are emitted");
1937 assert!(
1938 run.contains(
1939 r#"{Header: "Speed", Code: "ex run --fast", Help: "When you are in a hurry"}"#
1940 ),
1941 "all three fields are emitted: {run}"
1942 );
1943 assert!(
1945 run.contains(r#"{Code: "ex run"}"#),
1946 "an example with no header emits no header: {run}"
1947 );
1948 }
1949
1950 #[test]
1953 fn a_long_about_alone_does_not_become_the_short_one() {
1954 let out = go(r#"
1955name "ex"
1956bin "ex"
1957about_long "Long only."
1958"#);
1959 let meta = out
1960 .lines()
1961 .find(|l| l.contains("var HelpMeta"))
1962 .expect("a root header is emitted");
1963 assert!(
1964 !meta.contains(", About: ") && meta.contains(r#"LongAbout: "Long only.""#),
1965 "only the long one is set: {meta}"
1966 );
1967 }
1968
1969 #[test]
1970 fn a_default_subcommand_ignores_a_deeper_command_of_the_same_name() {
1971 let out = go(r#"
1972name "ex"
1973bin "ex"
1974default_subcommand "run"
1975cmd "oci" {
1976 cmd "run" {}
1977}
1978cmd "run" {
1979 arg "[args]..." var=#true
1980}
1981"#);
1982 assert!(
1983 out.contains("DefaultSubcommand: cmdRun,"),
1984 "should point at the root's own `run`, got:\n{out}"
1985 );
1986 }
1987
1988 #[test]
1989 fn command_builtin_controls_reach_generated_go_tables() {
1990 let out = go(r#"
1991name "ex"
1992bin "ex"
1993disable_help_flag #true
1994disable_help_subcommand #true
1995disable_version_flag #true
1996"#);
1997 let root = out
1998 .split("var Root = &argv.Command{")
1999 .nth(1)
2000 .expect("the root command should be emitted")
2001 .split("}\n")
2002 .next()
2003 .unwrap();
2004 assert!(root.contains("DisableHelpFlag:"), "{root}");
2005 assert!(root.contains("DisableHelpSubcommand:"), "{root}");
2006 assert!(root.contains("DisableVersionFlag:"), "{root}");
2007 }
2008
2009 #[test]
2012 fn a_default_subcommand_prefers_a_name_to_another_commands_alias() {
2013 let ordered = |first: &str, second: &str| {
2014 go(&format!(
2015 r#"
2016name "ex"
2017bin "ex"
2018default_subcommand "run"
2019{first}
2020{second}
2021"#
2022 ))
2023 };
2024 let alpha = "cmd \"alpha\" {\n alias \"run\"\n}";
2025 let run = "cmd \"run\" {\n arg \"[args]...\" var=#true\n}";
2026 for out in [ordered(alpha, run), ordered(run, alpha)] {
2027 assert!(
2028 out.contains("DefaultSubcommand: cmdRun,"),
2029 "should point at the command named `run`, got:\n{out}"
2030 );
2031 }
2032 }
2033
2034 #[test]
2035 fn an_external_subcommand_is_emitted_on_the_command_that_declares_it() {
2036 let out = go(r#"
2037name "ex"
2038bin "ex"
2039external_subcommand #true
2040cmd "install"
2041cmd "exec" external_subcommand=#true
2042"#);
2043 let block = |var: &str| {
2044 let start = out
2045 .find(&format!("var {var} ="))
2046 .unwrap_or_else(|| panic!("{var} should be emitted, got:\n{out}"));
2047 let rest = &out[start..];
2048 let end = rest[1..]
2049 .find("\nvar ")
2050 .map(|i| i + 1)
2051 .unwrap_or(rest.len());
2052 &rest[..end]
2053 };
2054 assert!(
2055 block("Root").contains("ExternalSubcommand: true"),
2056 "the root should forward unmatched words:\n{}",
2057 block("Root")
2058 );
2059 assert!(
2060 block("cmdExec").contains("ExternalSubcommand: true"),
2061 "a nested command can forward too:\n{}",
2062 block("cmdExec")
2063 );
2064 assert!(
2065 !block("cmdInstall").contains("ExternalSubcommand"),
2066 "a command that does not declare it should not carry it:\n{}",
2067 block("cmdInstall")
2068 );
2069 }
2070
2071 #[test]
2072 fn arg_required_else_help_reaches_the_table_and_typed_front_door() {
2073 let out = go(r#"
2074name "ex"
2075bin "ex"
2076cmd "run" arg_required_else_help=#true {
2077 flag "--all"
2078}
2079"#);
2080 assert!(
2081 out.contains("ArgRequiredElseHelp: true"),
2082 "the command table should carry the policy:\n{out}"
2083 );
2084 assert!(
2085 out.contains("p.Command().ArgRequiredElseHelp && p.CommandStart() == len(args)"),
2086 "the typed parser should enforce it before fallbacks:\n{out}"
2087 );
2088 }
2089
2090 #[test]
2091 fn subcommand_negates_requirements_reaches_generated_go() {
2092 let out = go(
2093 "name \"ex\"\nbin \"ex\"\nsubcommand_negates_reqs #true\nflag \"--config\" required=#true\ncmd \"run\"\n",
2094 );
2095 assert!(out.contains("SubcommandNegatesReqs: true"), "{out}");
2096 assert!(
2097 out.contains("checkRequirements := i == len(chain)-1 || !cmd.SubcommandNegatesReqs"),
2098 "{out}"
2099 );
2100 assert!(
2101 out.contains("CheckRelationshipsWithValuesAndRequirements"),
2102 "{out}"
2103 );
2104 }
2105
2106 #[test]
2107 fn argument_subcommand_conflicts_reach_generated_go() {
2108 let out = go(
2109 "name \"ex\"\nbin \"ex\"\nargs_conflicts_with_subcommands #true\nflag \"--verbose\"\ncmd \"run\"\n",
2110 );
2111 assert!(out.contains("ArgsConflictWithSubcommands: true"), "{out}");
2112 }
2113
2114 #[test]
2115 fn allow_missing_positional_reaches_generated_go() {
2116 let out = go(
2117 "name \"ex\"\nbin \"ex\"\nallow_missing_positional #true\narg \"[optional]\"\narg \"<required>\"\n",
2118 );
2119 assert!(out.contains("AllowMissingPositional: true"), "{out}");
2120 assert!(out.contains("Name: \"optional\""), "{out}");
2121 assert!(out.contains("Name: \"required\", Required: true"), "{out}");
2122 }
2123
2124 #[test]
2125 fn optional_flag_values_reach_generated_go() {
2126 let out = go("name \"ex\"\nbin \"ex\"\nflag \"--color [WHEN]\" value_optional=#true\n");
2127 assert!(
2128 out.contains("TakesValue: true, ValueOptional: true"),
2129 "{out}"
2130 );
2131 }
2132
2133 #[test]
2134 fn explicit_boolean_values_reach_generated_go() {
2135 let out = go(
2136 "name \"ex\"\nbin \"ex\"\nflag \"--color\" negate=\"--no-color\" bool_value=#true\n",
2137 );
2138 assert!(out.contains("BoolValue: true"), "{out}");
2139 assert!(out.contains("if ev.Flag.BoolValue"), "{out}");
2140 assert!(
2141 out.contains("given[ev.Flag.Key] = []string{ev.Value}"),
2142 "{out}"
2143 );
2144 assert!(
2145 out.contains("(ev.Value == \"true\") != ev.Negated"),
2146 "{out}"
2147 );
2148 }
2149
2150 #[test]
2151 fn flag_actions_reach_generated_go() {
2152 let out = go(
2153 "name \"ex\"\nbin \"ex\"\nflag \"--help-all\" action=\"help_all\"\nflag \"--version\" action=\"version\"\n",
2154 );
2155 assert!(out.contains("Action: argv.ActionHelpAll"), "{out}");
2156 assert!(out.contains("Action: argv.ActionVersion"), "{out}");
2157 }
2158
2159 #[test]
2160 fn granular_help_hides_reach_generated_go() {
2161 let out = go(
2162 "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",
2163 );
2164 for field in [
2165 "HideDefaultValue: true",
2166 "HideEnv: true",
2167 "HideEnvValues: true",
2168 "HidePossibleValues: true",
2169 "HideShortHelp: true",
2170 "HideLongHelp: true",
2171 ] {
2172 assert!(out.contains(field), "missing {field}:\n{out}");
2173 }
2174 }
2175
2176 #[test]
2177 fn strict_duplicate_policy_reaches_metadata() {
2178 let permissive = go("name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\n");
2179 assert!(!permissive.contains("RejectDuplicate"), "{permissive}");
2180
2181 let strict =
2182 go("name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\n");
2183 assert!(strict.contains("RejectDuplicate: true"), "{strict}");
2184 }
2185
2186 #[test]
2187 fn strict_negated_flags_track_each_spelling_separately() {
2188 let out = go(
2189 "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n",
2190 );
2191 assert!(out.contains("polaritySeen := map[uint64]uint8{}"), "{out}");
2192 assert!(
2193 out.contains("polaritySeen[ev.Flag.Key]&polarity != 0"),
2194 "{out}"
2195 );
2196 assert!(out.contains("if duplicateSeen[key]"), "{out}");
2197 }
2198
2199 #[test]
2200 fn strict_global_duplicate_tracking_resets_at_subcommands() {
2201 let out = go(
2202 "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\" global=#true\ncmd \"run\" {\n args_override_self #false\n}\n",
2203 );
2204 assert!(
2205 out.contains("levelSeen = map[uint64]int{}"),
2206 "a subcommand should start a new duplicate scope:\n{out}"
2207 );
2208 assert!(out.contains("strictSeen[ev.Flag.Key] = true"), "{out}");
2209 assert!(out.contains("if strictSeen[key]"), "{out}");
2210 }
2211
2212 #[test]
2214 fn a_subcommand_named_root_does_not_collide_with_the_root() {
2215 let out = go(r#"
2216name "ex"
2217bin "ex"
2218cmd "root" {
2219 flag "--wat"
2220}
2221"#);
2222 let declared = |name: &str| {
2225 out.lines()
2226 .filter(|l| l.split_whitespace().next() == Some(name))
2227 .count()
2228 };
2229 assert_eq!(declared("CmdRoot"), 1, "CmdRoot declared twice:\n{out}");
2230 assert_eq!(declared("CmdRoot2"), 1, "no distinct key for it:\n{out}");
2231 assert_declares_each_constant_once(&out);
2232 }
2233
2234 #[test]
2239 fn only_the_per_occurrence_bound_reaches_the_table() {
2240 let out = go(r#"
2241name "ex"
2242bin "ex"
2243flag "--include <pattern>..." {
2244 arg "<pattern>..." var=#true var_min=2 var_max=2
2245}
2246flag "--tag <t>" var=#true var_max=1
2247"#);
2248 assert!(
2249 out.contains("Name: \"include\", Longs: []string{\"include\"}, TakesValue: true, Variadic: true, VarMax: 2"),
2250 "{out}"
2251 );
2252 assert!(
2253 out.contains("Name: \"include\", Flag: true") && out.contains("VarMin: 2"),
2254 "the nested value minimum must reach post-binding metadata:\n{out}"
2255 );
2256 let tag = out.lines().find(|l| l.contains("\"tag\"")).unwrap();
2257 assert!(!tag.contains("VarMax"), "occurrence bound leaked: {tag}");
2258 }
2259
2260 #[test]
2261 fn exact_arity_with_one_label_reaches_go_help() {
2262 let out = go(r#"
2263name "ex"
2264bin "ex"
2265flag "--pair <ITEM>..." {
2266 arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2267 value_names "ITEM"
2268 }
2269}
2270arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2271 value_names "ITEM"
2272}
2273"#);
2274 assert_eq!(out.matches("ValueArity: 2").count(), 2, "{out}");
2275 assert_eq!(
2276 out.matches("ValueNames: []string{\"ITEM\"}").count(),
2277 2,
2278 "{out}"
2279 );
2280 }
2281
2282 #[test]
2283 fn allow_hyphen_values_reaches_the_table() {
2284 let out = go(r#"
2285name "ex"
2286bin "ex"
2287flag "--args <ARGS>" allow_hyphen_values=#true
2288"#);
2289 assert!(
2290 out.contains("Name: \"args\", Longs: []string{\"args\"}, TakesValue: true, AllowHyphenValues: true"),
2291 "{out}"
2292 );
2293 }
2294
2295 #[test]
2296 fn require_equals_reaches_the_table() {
2297 let out = go(r#"
2298name "ex"
2299bin "ex"
2300flag "--inspect <PORT>" require_equals=#true
2301"#);
2302 assert!(
2303 out.contains("Name: \"inspect\", Longs: []string{\"inspect\"}, TakesValue: true, RequireEquals: true"),
2304 "{out}"
2305 );
2306 }
2307
2308 #[test]
2309 fn default_missing_reaches_the_table() {
2310 let out = go(r#"
2311name "ex"
2312bin "ex"
2313flag "--color <WHEN>" default_missing="always"
2314"#);
2315 assert!(
2316 out.contains(
2317 "Name: \"color\", Longs: []string{\"color\"}, TakesValue: true, DefaultMissing: \"always\""
2318 ),
2319 "{out}"
2320 );
2321 }
2322
2323 #[test]
2329 fn a_relationship_resolves_through_scope_and_negation() {
2330 let out = go(r#"
2331name "ex"
2332bin "ex"
2333flag "--quiet" global=#true
2334flag "--color" negate="--no-color"
2335flag "--plain" conflicts="--no-color"
2336cmd "run" {
2337 flag "--loud" conflicts="--quiet"
2338 flag "--solo" conflicts="--plain"
2339}
2340"#);
2341 assert!(out.contains("Conflicts: []uint64{FlagColor}"), "{out}");
2343 assert!(out.contains("Conflicts: []uint64{FlagQuiet}"), "{out}");
2345 assert!(
2348 !entry_of(&out, "solo").contains("Conflicts"),
2349 "a non-global should not resolve from below:\n{out}"
2350 );
2351 }
2352
2353 #[test]
2354 fn positional_conflicts_reach_go_metadata_in_both_directions() {
2355 let out = go(r#"
2356name "ex"
2357bin "ex"
2358flag "--from-file <file>" conflicts="value"
2359arg "[value]" conflicts="--from-file"
2360"#);
2361
2362 assert!(
2363 entry_of(&out, "from-file").contains("Conflicts: []uint64{ArgValue}"),
2364 "{out}"
2365 );
2366 assert!(
2367 out.lines().any(|line| {
2368 line.contains("{Key: ArgValue, Name: \"value\"")
2369 && line.contains("Conflicts: []uint64{FlagFromFile}")
2370 }),
2371 "{out}"
2372 );
2373 }
2374
2375 #[test]
2376 fn a_value_conditional_requirement_reaches_go_metadata() {
2377 let out = go(r#"
2378name "ex"
2379bin "ex"
2380flag "--format <format>" {
2381 requires_if "json" "--schema"
2382}
2383flag "--schema <file>"
2384"#);
2385 assert!(
2386 entry_of(&out, "format").contains(
2387 "RequiresIf: []argv.ValueRequirement{{Value: \"json\", Key: FlagSchema}}"
2388 ),
2389 "{out}"
2390 );
2391 assert!(
2392 out.contains("argv.CheckRelationshipsWithValues"),
2393 "the emitted parser must enforce the metadata:\n{out}"
2394 );
2395 }
2396
2397 #[test]
2398 fn required_if_eq_makes_generated_go_supply_values() {
2399 let out = go(r#"
2400name "ex"
2401bin "ex"
2402flag "--token <token>" {
2403 required_if_eq "--mode" "remote"
2404}
2405flag "--mode <mode>"
2406"#);
2407 assert!(
2408 entry_of(&out, "token").contains(
2409 "RequiredIfEq: []argv.ValueCondition{{Key: FlagMode, Value: \"remote\"}}"
2410 ),
2411 "{out}"
2412 );
2413 assert!(out.contains("resolved := map[uint64][]string{}"), "{out}");
2414 assert!(out.contains("argv.CheckRelationshipsWithValues"), "{out}");
2415 }
2416
2417 #[test]
2418 fn boolean_sources_are_normalized_for_value_relationships() {
2419 let out = go(r#"
2420name "ex"
2421bin "ex"
2422flag "--token <token>" {
2423 required_if_eq "--mode" "true"
2424}
2425flag "--mode" negate="--no-mode" bool_value=#true
2426"#);
2427 assert!(
2428 entry_of(&out, "mode").contains("RequiresIfBoolean: true"),
2429 "{out}"
2430 );
2431 }
2432
2433 #[test]
2434 fn a_conditional_default_reaches_go_metadata() {
2435 let out = go(r#"
2436name "ex"
2437bin "ex"
2438flag "--bin-names" {
2439 default_if "--json" "true"
2440 default_if "--output" "json" "pretty"
2441}
2442flag "--json"
2443flag "--output <fmt>"
2444"#);
2445 assert!(
2446 entry_of(&out, "bin-names")
2447 .contains("DefaultIf: []argv.DefaultIf{{Key: FlagJson, Value: \"true\"}"),
2448 "{out}"
2449 );
2450 assert!(
2451 entry_of(&out, "bin-names").contains("When: \"json\""),
2452 "{out}"
2453 );
2454 assert!(
2455 out.contains("argv.ApplyDefaultIf"),
2456 "the emitted parser must apply the metadata:\n{out}"
2457 );
2458 assert!(
2459 out.contains("negated[ev.Flag.Key] = ev.Negated"),
2460 "Equals default_if needs the negate form:\n{out}"
2461 );
2462 }
2463
2464 #[test]
2468 fn a_relationship_needs_the_right_form() {
2469 let out = go(r#"
2470name "ex"
2471bin "ex"
2472flag "-q --quiet"
2473flag "--color"
2474flag "--a" conflicts="--q"
2475flag "--b" conflicts="-color"
2476flag "--c" conflicts="-q"
2477flag "--d" conflicts="--color"
2478"#);
2479 assert!(!entry_of(&out, "a").contains("Conflicts"), "{out}");
2481 assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
2482 assert!(
2484 entry_of(&out, "c").contains("Conflicts: []uint64{FlagQuiet}"),
2485 "{out}"
2486 );
2487 assert!(
2488 entry_of(&out, "d").contains("Conflicts: []uint64{FlagColor}"),
2489 "{out}"
2490 );
2491 }
2492
2493 #[test]
2500 fn an_ordinary_form_beats_another_flags_negation() {
2501 let out = go(r#"
2502name "ex"
2503bin "ex"
2504flag "--a" negate="--zap"
2505flag "--zap"
2506flag "--p" conflicts="--zap"
2507"#);
2508 assert!(
2509 entry_of(&out, "p").contains("Conflicts: []uint64{FlagZap}"),
2510 "should name the flag `--zap` binds, not the one negating to it:\n{out}"
2511 );
2512 }
2513
2514 #[test]
2516 fn a_single_dash_negation_is_named_by_its_own_form() {
2517 let out = go(r#"
2518name "ex"
2519bin "ex"
2520flag "--tint" negate="-no-tint"
2521flag "--plain" conflicts="-no-tint"
2522flag "--other" conflicts="--no-tint"
2523"#);
2524 assert!(
2525 entry_of(&out, "plain").contains("Conflicts: []uint64{FlagTint}"),
2526 "the exact form should resolve:\n{out}"
2527 );
2528 assert!(
2530 !entry_of(&out, "other").contains("Conflicts"),
2531 "`--no-tint` is not how it was declared:\n{out}"
2532 );
2533 }
2534
2535 #[test]
2537 fn a_negation_is_matched_as_written() {
2538 let out = go(r#"
2539name "ex"
2540bin "ex"
2541flag "--color" negate="--no-color"
2542flag "--tint" negate="-no-tint"
2543flag "--a" conflicts="--no-color"
2544flag "--b" conflicts="--no-tint"
2545"#);
2546 assert!(
2547 entry_of(&out, "a").contains("Conflicts: []uint64{FlagColor}"),
2548 "{out}"
2549 );
2550 assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
2553 }
2554
2555 #[test]
2556 fn strings_are_escaped_to_go_rules() {
2557 assert_eq!(go_string(r#"a"b\c"#), r#""a\"b\\c""#);
2558 assert_eq!(go_string("tab\there"), r#""tab\there""#);
2559 assert_eq!(go_string("\u{7f}"), r#""\x7f""#);
2561 }
2562}