//! A struct per command, and the `Parse` that fills them.
//!
//! The front door. An author calls `Parse(os.Args[1:])` and gets a value with
//! fields rather than a loop over events; everything below — binding, the
//! post-binding rules, the three tables — is unchanged, and this is the shape
//! that makes it usable without knowing any of it.
//!
//! Fields are `string`, `bool` and `[]string`, because that is what a usage spec
//! knows. A spec says what a value is *called* and never what type it is, so
//! turning `"8"` into an `int` stays the caller's business — `argv.Int` and its
//! neighbours exist for exactly that, and inferring a type from an argument's
//! name would be guessing.
use std::collections::{HashMap, HashSet};
use std::fmt::Write as _;
use super::{field_name, Emitted};
use crate::{SpecArg, SpecFlag};
/// The field each entry is assigned to, by key.
///
/// Worked out once and shared by the struct declarations and by `Parse`, so the
/// two cannot disagree about where a value goes.
type Fields = HashMap<String, String>;
/// Write every command's struct, then `Parse`.
pub(super) fn emit(out: &mut String, commands: &[Emitted]) {
let mut assigned: Fields = HashMap::new();
for e in commands {
let doc = if e.root {
format!("// {} is the whole command line.", name(e))
} else {
format!("// {} is `{}`.", name(e), e.cmd.full_cmd.join(" "))
};
let _ = writeln!(out, "{doc}");
let _ = writeln!(out, "type {} struct {{", name(e));
let mut fields: Vec<(String, String, String)> = Vec::new();
let mut taken: HashSet<String> = HashSet::new();
// A field name has to be unique *within its struct*, and a command can
// declare a `--shell` flag beside a `shell` subcommand — mise does, and
// does the same with `version`, `command`, `env` and `tool`. The kind is
// what disambiguates, because `ShellCmd` says which one it is where
// `Shell2` says only that there were two.
let mut claim = |base: String, suffix: &str| -> String {
if taken.insert(base.clone()) {
return base;
}
let kinded = format!("{base}{suffix}");
if taken.insert(kinded.clone()) {
return kinded;
}
for n in 2.. {
let numbered = format!("{kinded}{n}");
if taken.insert(numbered.clone()) {
return numbered;
}
}
unreachable!("the loop returns")
};
for (flag, named) in &e.flags {
let field = claim(field_name(&flag.name), "Flag");
fields.push((
field.clone(),
flag_type(flag).to_string(),
named.key.clone(),
));
assigned.insert(named.key.clone(), field);
}
for (arg, named) in &e.args {
let field = claim(field_name(&arg.name), "Arg");
fields.push((field.clone(), arg_type(arg).to_string(), named.key.clone()));
assigned.insert(named.key.clone(), field);
}
for at in &e.subcommands {
let sub = &commands[*at];
// A pointer, and at most one is set: the command line selects one path
// down the tree.
let field = claim(field_name(&sub.cmd.name), "Cmd");
fields.push((
field.clone(),
format!("*{}", name(sub)),
sub.named.key.clone(),
));
assigned.insert(sub.named.key.clone(), field);
}
// gofmt aligns a run of field declarations into columns, so this does too.
let name_col = fields.iter().map(|(n, _, _)| n.len()).max().unwrap_or(0);
let type_col = fields.iter().map(|(_, t, _)| t.len()).max().unwrap_or(0);
for (field, ty, key) in &fields {
let _ = writeln!(out, "\t{field:<name_col$} {ty:<type_col$} // {key}");
}
let _ = writeln!(out, "}}\n");
}
parse_fn(out, commands, &assigned);
}
fn parse_fn(out: &mut String, commands: &[Emitted], assigned: &Fields) {
let root = &commands[0];
let mut strict_keys = commands
.iter()
.flat_map(|command| {
command
.flags
.iter()
.filter(|(flag, _)| {
!command.cmd.args_override_self
&& !flag.var
&& !flag.count
&& !flag.arg.as_ref().is_some_and(|arg| arg.var)
})
.map(|(_, named)| named.key.as_str())
})
.collect::<Vec<_>>();
strict_keys.sort_unstable();
strict_keys.dedup();
let mut strict_negate_keys = commands
.iter()
.flat_map(|command| {
command
.flags
.iter()
.filter(|(flag, _)| !command.cmd.args_override_self && flag.negate.is_some())
.map(|(_, named)| named.key.as_str())
})
.collect::<Vec<_>>();
strict_negate_keys.sort_unstable();
strict_negate_keys.dedup();
let duplicate_state = if strict_keys.is_empty() {
String::new()
} else {
let polarity = if strict_negate_keys.is_empty() {
""
} else {
"\tpolaritySeen := map[uint64]uint8{}\n"
};
format!(
"\tlevelSeen := map[uint64]int{{}}\n{polarity}\tstrictSeen := map[uint64]bool{{}}\n\tduplicateSeen := map[uint64]bool{{}}\n"
)
};
let duplicate_event = if strict_keys.is_empty() {
String::new()
} else {
let keys = strict_keys.join(", ");
let per_kind = if strict_negate_keys.is_empty() {
"\t\t\t\tlevelSeen[ev.Flag.Key]++\n\t\t\t\tif levelSeen[ev.Flag.Key] > 1 {\n\t\t\t\t\tduplicateSeen[ev.Flag.Key] = true\n\t\t\t\t}\n".to_string()
} else {
let negate_keys = strict_negate_keys.join(", ");
format!(
"\t\t\t\tswitch ev.Flag.Key {{\n\t\t\t\tcase {negate_keys}:\n\t\t\t\t\tpolarity := uint8(1)\n\t\t\t\t\tif ev.Negated {{\n\t\t\t\t\t\tpolarity = 2\n\t\t\t\t\t}}\n\t\t\t\t\tif polaritySeen[ev.Flag.Key]&polarity != 0 {{\n\t\t\t\t\t\tduplicateSeen[ev.Flag.Key] = true\n\t\t\t\t\t}}\n\t\t\t\t\tpolaritySeen[ev.Flag.Key] |= polarity\n\t\t\t\tdefault:\n\t\t\t\t\tlevelSeen[ev.Flag.Key]++\n\t\t\t\t\tif levelSeen[ev.Flag.Key] > 1 {{\n\t\t\t\t\t\tduplicateSeen[ev.Flag.Key] = true\n\t\t\t\t\t}}\n\t\t\t\t}}\n"
)
};
format!(
"\t\t\tswitch ev.Flag.Key {{\n\t\t\tcase {keys}:\n\t\t\t\tstrictSeen[ev.Flag.Key] = true\n{per_kind}\t\t\t}}\n"
)
};
let duplicate_occurrences = if strict_keys.is_empty() {
""
} else {
"\t\toccurrences := seen[key]\n\t\tif strictSeen[key] {\n\t\t\toccurrences = 1\n\t\t\tif duplicateSeen[key] {\n\t\t\t\toccurrences = 2\n\t\t\t}\n\t\t}\n"
};
let check_occurrences = if strict_keys.is_empty() {
"seen[key]"
} else {
"occurrences"
};
let duplicate_command = if strict_keys.is_empty() {
String::new()
} else {
let polarity = if strict_negate_keys.is_empty() {
""
} else {
"\t\t\tpolaritySeen = map[uint64]uint8{}\n"
};
format!("\t\t\tlevelSeen = map[uint64]int{{}}\n{polarity}")
};
let has_relationship_values = commands.iter().any(|command| {
command.flags.iter().any(|(flag, _)| {
!flag.requires_if.is_empty()
|| !flag.required_if_eq.is_empty()
|| !flag.required_if_eq_all.is_empty()
}) || command
.args
.iter()
.any(|(arg, _)| !arg.required_if_eq.is_empty() || !arg.required_if_eq_all.is_empty())
});
let has_default_if = commands
.iter()
.flat_map(|command| command.flags.iter())
.any(|(flag, _)| !flag.default_if.is_empty());
let needs_negated = has_relationship_values || has_default_if;
let conditional_state = if needs_negated {
"\tnegated := map[uint64]bool{}\n"
} else {
""
};
let conditional_resolved = if has_relationship_values {
"\tresolved := map[uint64][]string{}\n"
} else {
""
};
let conditional_value = if has_relationship_values {
"\t\tresolved[key] = values\n"
} else {
""
};
let _ = writeln!(
out,
"// Parse binds a command line and fills the structs above.\n\
//\n\
// The rules decided once the last token has been read run here too, so a\n\
// missing required flag or a value outside its choices comes back rather than\n\
// reaching your code. A returned error is an *argv.Error; render it with\n\
// argv.Render.\n\
//\n\
// Help and version arrive as errors, because a parse that stops to print a page\n\
// has produced no value. Check the code before treating one as a failure.\n\
func Parse(args []string) (*{}, error) {{",
name(root)
);
let _ = writeln!(out, "\tout := &{}{{}}", name(root));
// One variable per command, assigned on descent. A flag's key can only arrive
// after its command was selected, so the variable is set by the time anything
// reads it.
for e in commands.iter().skip(1) {
let _ = writeln!(out, "\tvar {}V *{}", e.named.var, name(e));
}
let _ = writeln!(
out,
"\n\t// Collected by key, so the post-binding rules can judge what arrived\n\
\t// before any of it is handed back.\n\
\tgiven := map[uint64][]string{{}}\n\
\tseen := map[uint64]int{{}}\n\
{duplicate_state}\
{conditional_state}\
\tchain := []*argv.Command{{Root}}\n\
\n\tp := argv.New(Root, args)\n\
\tfor p.Next() {{\n\
\t\tev := p.Event()\n\
\t\tswitch ev.Kind {{\n\
\t\tcase argv.KindCommand:\n\
{duplicate_command}\
\t\t\tchain = append(chain, ev.Command)\n\
\t\t\tswitch ev.Command.Key {{"
);
for (i, e) in commands.iter().enumerate().skip(1) {
let owner = match parent_of(commands, i) {
Some(0) | None => "out".to_string(),
Some(at) => format!("{}V", commands[at].named.var),
};
let _ = writeln!(
out,
"\t\t\tcase {}:\n\t\t\t\t{v}V = &{}{{}}\n\t\t\t\t{owner}.{} = {v}V",
e.named.key,
name(e),
assigned[&e.named.key],
v = e.named.var
);
}
let conditional_event = if needs_negated {
"\t\t\tnegated[ev.Flag.Key] = ev.Negated\n"
} else {
""
};
let _ = writeln!(
out,
"\t\t\t}}\n\t\tcase argv.KindFlag:\n\t\t\tseen[ev.Flag.Key]++\n\
{duplicate_event}\
{conditional_event}\
\t\t\tif ev.Flag.BoolValue {{\n\
\t\t\t\t// Boolean binding is last-one-wins. Replace an earlier attached value even\n\
\t\t\t\t// when the last occurrence is bare, so relationship polarity follows the field.\n\
\t\t\t\tif ev.HasValue {{\n\
\t\t\t\t\tgiven[ev.Flag.Key] = []string{{ev.Value}}\n\
\t\t\t\t}} else {{\n\
\t\t\t\t\tgiven[ev.Flag.Key] = []string{{}}\n\
\t\t\t\t}}\n\
\t\t\t}} else if ev.HasValue {{\n\
\t\t\t\tgiven[ev.Flag.Key] = append(given[ev.Flag.Key], argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...)\n\
\t\t\t}} else if given[ev.Flag.Key] == nil {{\n\
\t\t\t\t// Given without a value is still given, and nil would read as\n\
\t\t\t\t// absent when the fallbacks are applied.\n\
\t\t\t\tgiven[ev.Flag.Key] = []string{{}}\n\t\t\t}}\n\
\t\t\tswitch ev.Flag.Key {{"
);
for e in commands {
let owner = owner_of(e);
for (flag, named) in &e.flags {
let _ = writeln!(
out,
"\t\t\tcase {}:\n{}",
named.key,
flag_assign(flag, &owner, &assigned[&named.key])
);
}
}
let _ = writeln!(
out,
"\t\t\t}}\n\t\tcase argv.KindArg:\n\t\t\tseen[ev.Arg.Key]++\n\
\t\t\tvalues := argv.SplitValue(ev.Value, ev.Arg.Delimiter, ev.Delimit)\n\
\t\t\tgiven[ev.Arg.Key] = append(given[ev.Arg.Key], values...)\n\
\t\t\tswitch ev.Arg.Key {{"
);
for e in commands {
let owner = owner_of(e);
for (arg, named) in &e.args {
let field = &assigned[&named.key];
let assign = if arg.var {
format!("\t\t\t\t{owner}.{field} = append({owner}.{field}, values...)")
} else {
format!("\t\t\t\t{owner}.{field} = ev.Value")
};
let _ = writeln!(out, "\t\t\tcase {}:\n{assign}", named.key);
}
}
let negated_arg = if needs_negated { "negated" } else { "nil" };
let _ = writeln!(
out,
"\t\t\t}}\n\t\t}}\n\t}}\n\
\tif err := p.Err(); err != nil {{\n\t\treturn nil, err\n\t}}\n\
\tif p.Command().ArgRequiredElseHelp && p.CommandStart() == len(args) {{\n\
\t\treturn nil, &argv.Error{{Code: argv.CodeHelp, Cmd: p.Command()}}\n\t}}\n\
\n\t// Only the commands the words actually selected are judged: a required\n\
\t// flag on a command nobody ran is not missing.\n\
\tvar scope []uint64\n\
\trequirements := map[uint64]bool{{}}\n\
\tfor i, cmd := range chain {{\n\
\t\tcheckRequirements := i == len(chain)-1 || !cmd.SubcommandNegatesReqs\n\
\t\tfor _, f := range cmd.Flags {{\n\t\t\tscope = append(scope, f.Key)\n\t\t\trequirements[f.Key] = checkRequirements\n\t\t}}\n\
\t\tfor _, a := range cmd.Args {{\n\t\t\tscope = append(scope, a.Key)\n\t\t\trequirements[a.Key] = checkRequirements\n\t\t}}\n\
\t}}\n\
\tsources := map[uint64]argv.Source{{}}\n\
\tfilled := map[uint64][]string{{}}\n\
{conditional_resolved}\
\tfor _, key := range scope {{\n\
\t\tvalues, source := argv.Fill(Meta.Lookup(key), given[key], argv.LookupEnv)\n\
\t\tfilled[key] = values\n\
\t\tsources[key] = source\n\
\t}}\n\
\targv.ApplyDefaultIf(Meta, scope, filled, sources, {negated_arg})\n\
\tfor _, key := range scope {{\n\
\t\tvalues, source := filled[key], sources[key]\n\
{conditional_value}\
{duplicate_occurrences}\
\t\tentryMeta := Meta.Lookup(key)\n\
\t\tif entryMeta != nil && !requirements[key] {{\n\t\t\tcopy := *entryMeta\n\t\t\tcopy.Required = false\n\t\t\tentryMeta = ©\n\t\t}}\n\
\t\tif err := argv.Check(entryMeta, values, {check_occurrences}); err != nil {{\n\
\t\t\treturn nil, err\n\t\t}}\n\
\t\t// What the environment or a default supplied has to reach the field\n\
\t\t// too. A front door that enforces a default and then hands back the\n\
\t\t// zero value is worse than one that has no defaults at all.\n\
\t\t//\n\
\t\t// Written here rather than in a function of its own because a\n\
\t\t// subcommand's struct is reachable only from inside this one: the\n\
\t\t// variable holding it is local, and only the keys of commands the\n\
\t\t// words selected are in scope, so it is never nil when its key is.\n\
\t\tif source == argv.FromEnv || source == argv.FromDefault {{\n\
\t\t\tswitch key {{"
);
fallback_cases(out, commands, assigned);
let _ = writeln!(out, "\t\t\t}}\n\t\t}}\n\t}}");
if has_relationship_values {
let _ = writeln!(
out,
"\tif err := argv.CheckRelationshipsWithValuesAndRequirements(Meta, scope, func(k uint64) argv.Source {{\n\
\t\treturn sources[k]\n\t}}, func(k uint64) []string {{\n\
\t\treturn argv.RelationshipValues(Meta.Lookup(k), resolved[k], sources[k], negated[k])\n\
\t}}, func(k uint64) bool {{ return requirements[k] }}); err != nil {{\n\t\treturn nil, err\n\t}}"
);
} else {
let _ = writeln!(
out,
"\tif err := argv.CheckRelationshipsWithValuesAndRequirements(Meta, scope, func(k uint64) argv.Source {{\n\
\t\treturn sources[k]\n\t}}, nil, func(k uint64) bool {{ return requirements[k] }}); err != nil {{\n\t\treturn nil, err\n\t}}"
);
}
let _ = writeln!(out, "\treturn out, nil\n}}\n");
}
/// The cases that put an `env` or `default` value into its field.
fn fallback_cases(out: &mut String, commands: &[Emitted], assigned: &Fields) {
for e in commands {
let owner = owner_of(e);
for (flag, named) in &e.flags {
let field = &assigned[&named.key];
let assign = match flag_type(flag) {
// A value-less flag has nowhere to put text, so the variable is
// read as a yes or a no — by usage-lib's allow-list, which is
// narrower than Go's own spellings on purpose.
"bool" => format!(
"\t\t\t\tif source == argv.FromEnv {{\n\
\t\t\t\t\t{owner}.{field} = argv.EnvTruth(values[0])\n\
\t\t\t\t}} else {{\n\t\t\t\t\t{owner}.{field} = values[0] == \"true\"\n\t\t\t\t}}"
),
"[]string" => {
format!("\t\t\t\t{owner}.{field} = append({owner}.{field}, values...)")
}
// A count is occurrences, which nothing but the command line has.
"int" => continue,
_ => format!("\t\t\t\t{owner}.{field} = values[len(values)-1]"),
};
let _ = writeln!(out, "\t\t\tcase {}:\n{assign}", named.key);
}
for (arg, named) in &e.args {
let field = &assigned[&named.key];
let assign = if arg.var {
format!("\t\t\t\t{owner}.{field} = append({owner}.{field}, values...)")
} else {
format!("\t\t\t\t{owner}.{field} = values[len(values)-1]")
};
let _ = writeln!(out, "\t\t\tcase {}:\n{assign}", named.key);
}
}
}
/// Where a command's own entries are assigned.
fn owner_of(e: &Emitted) -> String {
if e.root {
"out".to_string()
} else {
format!("{}V", e.named.var)
}
}
/// The index of the command that declares this one as a subcommand.
fn parent_of(commands: &[Emitted], at: usize) -> Option<usize> {
let path = &commands[at].cmd.full_cmd;
if path.len() <= 1 {
return Some(0);
}
commands
.iter()
.position(|e| e.cmd.full_cmd[..] == path[..path.len() - 1])
}
fn name(e: &Emitted) -> String {
if e.root {
return "Cli".to_string();
}
format!("{}Cmd", &e.named.key["Cmd".len()..])
}
fn flag_type(flag: &SpecFlag) -> &'static str {
match () {
_ if flag.count => "int",
_ if flag.arg.is_none() => "bool",
_ if flag.var || flag.arg.as_ref().is_some_and(|a| a.var) => "[]string",
_ => "string",
}
}
fn arg_type(arg: &SpecArg) -> &'static str {
if arg.var {
"[]string"
} else {
"string"
}
}
fn flag_assign(flag: &SpecFlag, owner: &str, field: &str) -> String {
match flag_type(flag) {
// A count is the number of occurrences, which is what the parser reports
// one event at a time.
"int" => format!("\t\t\t\t{owner}.{field}++"),
"bool" if flag.bool_value => format!(
"\t\t\t\tif ev.HasValue {{\n\t\t\t\t\t{owner}.{field} = (ev.Value == \"true\") != ev.Negated\n\t\t\t\t}} else {{\n\t\t\t\t\t{owner}.{field} = !ev.Negated\n\t\t\t\t}}"
),
"bool" => format!("\t\t\t\t{owner}.{field} = !ev.Negated"),
"[]string" => format!(
"\t\t\t\tif ev.HasValue {{\n\t\t\t\t\t{owner}.{field} = append({owner}.{field}, argv.SplitValue(ev.Value, ev.Flag.Delimiter, true)...)\n\t\t\t\t}}"
),
_ => format!("\t\t\t\t{owner}.{field} = ev.Value"),
}
}