use core::fmt::Write as _;
use crate::UnknownFlags;
use crate::{Arg, Command, DoubleDash, Flag};
pub(crate) fn flag_forms_overlap(a: &Flag<'_>, b: &Flag<'_>) -> bool {
a.longs.iter().any(|name| b.longs.contains(name))
|| a.shorts.iter().any(|name| b.shorts.contains(name))
|| a.negate.is_some_and(|name| {
b.longs.contains(&name) || b.negate.is_some_and(|other| other == name)
})
|| b.negate.is_some_and(|name| a.longs.contains(&name))
}
fn duplicate_key(cmd: &Command<'_>) -> Option<u64> {
let mut keys: std::vec::Vec<u64> = cmd
.flags
.iter()
.map(|f| f.key)
.chain(cmd.args.iter().map(|a| a.key))
.collect();
keys.sort_unstable();
if let Some(pair) = keys.windows(2).find(|pair| pair[0] == pair[1]) {
return Some(pair[0]);
}
cmd.subcommands.iter().find_map(|sub| duplicate_key(sub))
}
fn duplicate_flag_form(cmd: &Command<'_>) -> Option<std::string::String> {
let mut forms: std::vec::Vec<std::string::String> = std::vec::Vec::new();
for flag in cmd.flags {
for long in flag.longs.iter().chain(flag.negate.iter()) {
forms.push(std::format!("--{long}"));
}
for short in flag.shorts {
forms.push(std::format!("-{}", *short as char));
}
}
forms.sort_unstable();
if let Some(pair) = forms.windows(2).find(|pair| pair[0] == pair[1]) {
return Some(pair[0].clone());
}
cmd.subcommands
.iter()
.find_map(|sub| duplicate_flag_form(sub))
}
fn duplicate_group_name(meta: &CommandMeta<'_>) -> Option<std::string::String> {
let mut names: std::vec::Vec<&str> = meta.groups.iter().map(|g| g.name).collect();
names.sort_unstable();
if let Some(pair) = names.windows(2).find(|pair| pair[0] == pair[1]) {
return Some(pair[0].to_string());
}
meta.subcommands
.iter()
.find_map(|sub| duplicate_group_name(sub))
}
fn unfillable_arg<'a>(cmd: &Command<'a>) -> Option<&'a str> {
let mut variadic: Option<&Arg<'_>> = None;
for arg in cmd.args {
let stopped_by_separator = arg.double_dash == DoubleDash::Required;
if let Some(before) = variadic {
let separator_is_gone = matches!(
before.double_dash,
DoubleDash::Required
| DoubleDash::Preserve
);
if !stopped_by_separator || separator_is_gone {
return Some(arg.name);
}
}
if arg.var && arg.var_max.is_none() {
variadic = Some(arg);
}
}
cmd.subcommands.iter().find_map(|sub| unfillable_arg(sub))
}
#[derive(Debug, Clone, Copy)]
pub struct CompleteCtx<'a> {
pub words: &'a [String],
pub cword: usize,
pub prefix: &'a str,
pub command_path: &'a [(&'a crate::Command<'a>, &'a [String])],
pub command_words: &'a [String],
}
impl<'a> CompleteCtx<'a> {
pub fn command_for(
&self,
declaration: &crate::Command<'_>,
) -> Option<(&'a crate::Command<'a>, &'a [String])> {
self.command_path
.iter()
.find(|(cmd, _)| cmd.key == declaration.key)
.or_else(|| {
let has_fields = !declaration.flags.is_empty() || !declaration.args.is_empty();
self.command_path.iter().find(|(cmd, _)| {
has_fields
&& declaration.flags.iter().all(|field| {
cmd.flags.iter().any(|candidate| candidate.key == field.key)
})
&& declaration.args.iter().all(|field| {
cmd.args.iter().any(|candidate| candidate.key == field.key)
})
})
})
.map(|(cmd, words)| (*cmd, *words))
}
pub fn words_for(&self, command: &crate::Command<'_>) -> &'a [String] {
self.command_for(command)
.map(|(_, words)| words)
.unwrap_or(self.command_words)
}
pub fn command_words_start(&self) -> &'a [String] {
let start = 1.min(self.cword);
self.words.get(start..self.cword).unwrap_or(&[])
}
pub fn previous(&self) -> Option<&'a str> {
self.cword
.checked_sub(1)
.and_then(|i| self.words.get(i))
.map(String::as_str)
}
}
pub type Completer = fn(&CompleteCtx<'_>) -> Vec<Candidate<'static>>;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Candidate<'a> {
pub value: String,
pub description: Option<::std::borrow::Cow<'a, str>>,
}
impl Candidate<'_> {
pub fn new(value: impl Into<String>) -> Self {
Self {
value: value.into(),
description: None,
}
}
pub fn described(value: impl Into<String>, description: impl Into<String>) -> Self {
Self {
value: value.into(),
description: Some(::std::borrow::Cow::Owned(description.into())),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Spec<'a> {
pub name: &'a str,
pub bin: Option<&'a str>,
pub version: Option<&'a str>,
pub long_version: Option<&'a str>,
pub author: Option<&'a str>,
pub license: Option<&'a str>,
pub repository: Option<&'a str>,
pub source_code_link_template: Option<&'a str>,
pub min_usage_version: Option<&'a str>,
pub about: Option<&'a str>,
pub long_about: Option<&'a str>,
pub usage: Option<&'a str>,
pub help_template: Option<&'a str>,
pub default_subcommand: Option<&'a str>,
pub multicall: bool,
pub views: &'a [ViewMeta<'a>],
pub root: &'a CommandMeta<'a>,
}
#[derive(Debug, Clone, Copy)]
pub struct ViewMeta<'a> {
pub id: &'a str,
pub name: &'a str,
pub bin: &'a str,
pub root: &'a str,
pub all_globals: bool,
pub globals: &'a [&'a str],
}
impl ViewMeta<'_> {
pub fn carries(self, flag: &crate::Flag<'_>) -> bool {
flag.global
&& (self.all_globals
|| self.globals.iter().any(|selector| {
selector
.strip_prefix("--")
.is_some_and(|long| flag.longs.contains(&long))
|| selector
.strip_prefix('-')
.filter(|short| short.len() == 1)
.and_then(|short| short.as_bytes().first().copied())
.is_some_and(|short| flag.shorts.contains(&short))
}))
}
}
pub fn view_for_program<'a>(
spec: &'a Spec<'a>,
argv0: &std::ffi::OsStr,
) -> Option<&'a ViewMeta<'a>> {
let basename = crate::multicall_basename(argv0.to_str()?);
if basename == crate::multicall_basename(spec.name)
|| spec
.bin
.is_some_and(|bin| basename == crate::multicall_basename(bin))
{
return None;
}
spec.views.iter().find(|view| {
basename == crate::multicall_basename(view.bin)
|| basename == crate::multicall_basename(view.id)
})
}
#[derive(Debug, Clone, Copy)]
pub enum CommandSelector<'a> {
Any,
Path(&'a str),
Key(u64),
}
impl CommandSelector<'_> {
pub(crate) fn matches(&self, meta: &CommandMeta<'_>, path: &[&str]) -> bool {
match self {
Self::Any => true,
Self::Path(expected) => expected.split_ascii_whitespace().eq(path.iter().copied()),
Self::Key(key) => meta.cmd.key == *key,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct CommandOverlay<'a> {
pub command: CommandSelector<'a>,
pub effect: Effect,
}
impl<'a> CommandOverlay<'a> {
pub const fn effect(path: &'a str, effect: Effect) -> Self {
Self {
command: CommandSelector::Path(path),
effect,
}
}
pub const fn effect_for(key: u64, effect: Effect) -> Self {
Self {
command: CommandSelector::Key(key),
effect,
}
}
}
#[derive(Debug, Clone)]
pub struct SpecView<'a> {
base: &'a Spec<'a>,
name: Option<&'a str>,
bin: Option<&'a str>,
version: Option<&'a str>,
omit_version: bool,
commands: Vec<CommandOverlay<'a>>,
}
impl<'a> SpecView<'a> {
pub const fn new(base: &'a Spec<'a>) -> Self {
Self {
base,
name: None,
bin: None,
version: None,
omit_version: false,
commands: Vec::new(),
}
}
fn reborrow<'b>(self) -> SpecView<'b>
where
'a: 'b,
{
SpecView {
base: self.base,
name: self.name,
bin: self.bin,
version: self.version,
omit_version: self.omit_version,
commands: self.commands,
}
}
pub fn name<'b, 'c>(self, name: &'b str) -> SpecView<'c>
where
'a: 'c,
'b: 'c,
{
let mut view = self.reborrow();
view.name = Some(name);
view
}
pub fn bin<'b, 'c>(self, bin: &'b str) -> SpecView<'c>
where
'a: 'c,
'b: 'c,
{
let mut view = self.reborrow();
view.bin = Some(bin);
view
}
pub fn version<'b, 'c>(self, version: &'b str) -> SpecView<'c>
where
'a: 'c,
'b: 'c,
{
let mut view = self.reborrow();
view.version = Some(version);
view.omit_version = false;
view
}
pub fn omit_version(mut self) -> Self {
self.version = None;
self.omit_version = true;
self
}
pub fn overlay<'b, 'c>(self, commands: &'b [CommandOverlay<'b>]) -> SpecView<'c>
where
'a: 'c,
'b: 'c,
{
let mut view = self.reborrow();
view.commands.extend_from_slice(commands);
view
}
pub const fn spec(&self) -> Spec<'a> {
Spec {
name: match self.name {
Some(name) => name,
None => self.base.name,
},
bin: match self.bin {
Some(bin) => Some(bin),
None => self.base.bin,
},
version: if self.omit_version {
None
} else {
match self.version {
Some(version) => Some(version),
None => self.base.version,
}
},
long_version: if self.omit_version {
None
} else if self.version.is_some() {
None
} else {
self.base.long_version
},
author: self.base.author,
license: self.base.license,
repository: self.base.repository,
source_code_link_template: self.base.source_code_link_template,
min_usage_version: self.base.min_usage_version,
about: self.base.about,
long_about: self.base.long_about,
usage: self.base.usage,
help_template: self.base.help_template,
default_subcommand: self.base.default_subcommand,
multicall: self.base.multicall,
views: self.base.views,
root: self.base.root,
}
}
pub fn to_kdl(self) -> String {
let spec = self.spec();
spec.render_kdl_with(&self.commands)
}
}
impl Spec<'_> {
pub const EMPTY: Spec<'static> = Spec {
name: "",
bin: None,
version: None,
long_version: None,
author: None,
license: None,
repository: None,
source_code_link_template: None,
min_usage_version: None,
about: None,
long_about: None,
usage: None,
help_template: None,
default_subcommand: None,
multicall: false,
views: &[],
root: &CommandMeta::EMPTY,
};
pub const fn view(&self) -> SpecView<'_> {
SpecView::new(self)
}
}
pub const fn concat_flag_metas<const N: usize>(
groups: &[&[FlagMeta<'static>]],
) -> [FlagMeta<'static>; N] {
let mut out = [FlagMeta::EMPTY; N];
let mut at = 0;
let mut g = 0;
while g < groups.len() {
let group = groups[g];
let mut i = 0;
while i < group.len() {
out[at] = group[i];
at += 1;
i += 1;
}
g += 1;
}
assert!(
at == N,
"`N` must be `table_len` of the same groups, or the metadata would describe a flag \
that does not exist"
);
out
}
pub const fn concat_arg_metas<const N: usize>(
groups: &[&[ArgMeta<'static>]],
) -> [ArgMeta<'static>; N] {
let mut out = [ArgMeta::EMPTY; N];
let mut at = 0;
let mut g = 0;
while g < groups.len() {
let group = groups[g];
let mut i = 0;
while i < group.len() {
out[at] = group[i];
at += 1;
i += 1;
}
g += 1;
}
assert!(
at == N,
"`N` must be `table_len` of the same groups, or the metadata would describe an \
argument that does not exist"
);
out
}
pub const fn concat_aliases<const N: usize>(groups: &[&[&'static str]]) -> [&'static str; N] {
let mut out = [""; N];
let mut at = 0;
let mut g = 0;
while g < groups.len() {
let group = groups[g];
let mut i = 0;
while i < group.len() {
out[at] = group[i];
at += 1;
i += 1;
}
g += 1;
}
assert!(
at == N,
"`N` must be `table_len` of the same groups, or an alias would be empty"
);
out
}
#[derive(Debug, Clone, Copy)]
pub struct GroupMeta<'a> {
pub name: &'a str,
pub members: &'a [&'a str],
pub required: bool,
pub multiple: bool,
}
impl GroupMeta<'_> {
pub const EMPTY: GroupMeta<'static> = GroupMeta {
name: "",
members: &[],
required: false,
multiple: false,
};
}
pub const fn concat_group_metas<const N: usize>(
groups: &[&[GroupMeta<'static>]],
) -> [GroupMeta<'static>; N] {
let mut out = [GroupMeta::EMPTY; N];
let mut at = 0;
let mut g = 0;
while g < groups.len() {
let group = groups[g];
let mut i = 0;
while i < group.len() {
let mut seen = 0;
while seen < at {
assert!(
!crate::str_eq(out[seen].name, group[i].name),
"two flattened groups on one command have the same name"
);
seen += 1;
}
out[at] = group[i];
at += 1;
i += 1;
}
g += 1;
}
assert!(
at == N,
"`N` must be `table_len` of the same groups, or the metadata would describe a \
group that does not exist"
);
out
}
#[derive(Debug, Clone, Copy)]
pub struct CommandMeta<'a> {
pub cmd: &'a Command<'a>,
pub about: Option<&'a str>,
pub long_about: Option<&'a str>,
pub deprecated: Option<&'a str>,
pub deprecated_warn_at: Option<&'a str>,
pub deprecated_remove_at: Option<&'a str>,
pub hidden_aliases: &'a [&'a str],
pub hide: bool,
pub help_heading: Option<&'a str>,
pub display_order: Option<usize>,
pub effect: Option<Effect>,
pub mount: Option<&'a str>,
pub restart_token: Option<&'a str>,
pub subcommand_required: bool,
pub subcommand_help_heading: Option<&'a str>,
pub subcommand_value_name: Option<&'a str>,
pub next_line_help: bool,
pub flatten_help: bool,
pub term_width: Option<usize>,
pub max_term_width: Option<usize>,
pub args_override_self: bool,
pub before_help: Option<&'a str>,
pub before_long_help: Option<&'a str>,
pub after_help: Option<&'a str>,
pub after_long_help: Option<&'a str>,
pub examples: &'a [Example<'a>],
pub flags: &'a [FlagMeta<'a>],
pub args: &'a [ArgMeta<'a>],
pub subcommands: &'a [&'a CommandMeta<'a>],
pub groups: &'a [GroupMeta<'a>],
pub flatten_groups: &'a [FlattenGroup<'a>],
}
impl CommandMeta<'_> {
pub const EMPTY: CommandMeta<'static> = CommandMeta {
cmd: &Command::EMPTY,
about: None,
long_about: None,
deprecated: None,
deprecated_warn_at: None,
deprecated_remove_at: None,
hidden_aliases: &[],
hide: false,
help_heading: None,
display_order: None,
effect: None,
mount: None,
restart_token: None,
subcommand_required: false,
subcommand_help_heading: None,
subcommand_value_name: None,
next_line_help: false,
flatten_help: false,
term_width: None,
max_term_width: None,
args_override_self: true,
before_help: None,
before_long_help: None,
after_help: None,
after_long_help: None,
examples: &[],
groups: &[],
flags: &[],
args: &[],
subcommands: &[],
flatten_groups: &[],
};
}
#[derive(Debug, Clone, Copy)]
pub struct FlattenGroup<'a> {
pub name: &'a str,
pub start: usize,
pub meta: &'a CommandMeta<'a>,
}
#[derive(Debug, Clone, Copy)]
pub struct FlagMeta<'a> {
pub flag: &'a Flag<'a>,
pub display_order: Option<usize>,
pub hidden_shorts: &'a [u8],
pub hidden_longs: &'a [&'a str],
pub help: Option<&'a str>,
pub long_help: Option<&'a str>,
pub deprecated: Option<&'a str>,
pub deprecated_warn_at: Option<&'a str>,
pub deprecated_remove_at: Option<&'a str>,
pub value_name: Option<&'a str>,
pub value_names: &'a [&'a str],
pub env: Option<&'a str>,
pub env_fallback: &'a [&'a str],
pub deprecated_env: &'a [&'a str],
pub default: &'a [&'a str],
pub accepted_choices: &'a [&'a str],
pub choices: &'a [&'a str],
pub choice_aliases: &'a [(&'a str, &'a str)],
pub choice_details: &'a [ChoiceMeta<'a>],
pub ignore_case: bool,
pub allow_unknown_choices: bool,
pub validate: Option<&'a str>,
pub validate_error: Option<&'a str>,
pub required: bool,
pub value_optional: bool,
pub hide: bool,
pub hide_default_value: bool,
pub hide_env: bool,
pub hide_env_values: bool,
pub hide_possible_values: bool,
pub hide_short_help: bool,
pub hide_long_help: bool,
pub count: bool,
pub complete: Option<Completer>,
pub complete_type: Option<&'a str>,
pub repeatable: bool,
pub var_min: Option<usize>,
pub var_max: Option<usize>,
pub value_var_min: Option<usize>,
pub value_var_max: Option<usize>,
pub overrides: &'a [&'a str],
pub conflicts: &'a [&'a str],
pub delimiter: Option<char>,
pub exclusive: bool,
pub requires: &'a [&'a str],
pub requires_if: &'a [RequiresIf<'a>],
pub default_if: &'a [DefaultIf<'a>],
pub required_if: &'a [&'a str],
pub required_if_eq: &'a [RequiredIfEq<'a>],
pub required_if_eq_all: &'a [RequiredIfEq<'a>],
pub required_unless: &'a [&'a str],
pub required_unless_all: &'a [&'a str],
pub help_heading: Option<&'a str>,
pub effect: Option<Effect>,
}
impl FlagMeta<'_> {
pub const EMPTY: FlagMeta<'static> = FlagMeta {
complete: None,
complete_type: None,
flag: &Flag::BOOL,
display_order: None,
hidden_shorts: &[],
hidden_longs: &[],
help: None,
long_help: None,
deprecated: None,
deprecated_warn_at: None,
deprecated_remove_at: None,
value_name: None,
value_names: &[],
env: None,
env_fallback: &[],
deprecated_env: &[],
default: &[],
accepted_choices: &[],
choices: &[],
choice_aliases: &[],
choice_details: &[],
ignore_case: false,
allow_unknown_choices: false,
validate: None,
validate_error: None,
required: false,
value_optional: false,
hide: false,
hide_default_value: false,
hide_env: false,
hide_env_values: false,
hide_possible_values: false,
hide_short_help: false,
hide_long_help: false,
count: false,
repeatable: false,
var_min: None,
var_max: None,
value_var_min: None,
value_var_max: None,
overrides: &[],
conflicts: &[],
delimiter: None,
exclusive: false,
requires: &[],
requires_if: &[],
default_if: &[],
required_if: &[],
required_if_eq: &[],
required_if_eq_all: &[],
required_unless: &[],
required_unless_all: &[],
help_heading: None,
effect: None,
};
}
#[derive(Debug, Clone, Copy)]
pub struct RequiresIf<'a> {
pub value: &'a str,
pub requires: &'a str,
}
#[derive(Debug, Clone, Copy)]
pub struct RequiredIfEq<'a> {
pub selector: &'a str,
pub value: &'a str,
}
#[derive(Debug, Clone, Copy)]
pub struct DefaultIf<'a> {
pub selector: &'a str,
pub when: Option<&'a str>,
pub value: &'a str,
}
#[derive(Debug, Clone, Copy)]
pub struct ArgMeta<'a> {
pub arg: &'a Arg<'a>,
pub display_order: Option<usize>,
pub value_names: &'a [&'a str],
pub help: Option<&'a str>,
pub long_help: Option<&'a str>,
pub env: Option<&'a str>,
pub env_fallback: &'a [&'a str],
pub deprecated_env: &'a [&'a str],
pub default: &'a [&'a str],
pub accepted_choices: &'a [&'a str],
pub choices: &'a [&'a str],
pub choice_aliases: &'a [(&'a str, &'a str)],
pub choice_details: &'a [ChoiceMeta<'a>],
pub ignore_case: bool,
pub allow_unknown_choices: bool,
pub validate: Option<&'a str>,
pub validate_error: Option<&'a str>,
pub required: bool,
pub hide: bool,
pub hide_default_value: bool,
pub hide_env: bool,
pub hide_env_values: bool,
pub hide_possible_values: bool,
pub hide_short_help: bool,
pub hide_long_help: bool,
pub conflicts: &'a [&'a str],
pub requires: &'a [&'a str],
pub required_if: &'a [&'a str],
pub required_if_eq: &'a [RequiredIfEq<'a>],
pub required_if_eq_all: &'a [RequiredIfEq<'a>],
pub required_unless: &'a [&'a str],
pub required_unless_all: &'a [&'a str],
pub var_min: Option<usize>,
pub var_max: Option<usize>,
pub delimiter: Option<char>,
pub help_heading: Option<&'a str>,
pub complete: Option<Completer>,
pub complete_type: Option<&'a str>,
}
impl ArgMeta<'_> {
pub const EMPTY: ArgMeta<'static> = ArgMeta {
complete: None,
complete_type: None,
arg: &Arg::REQUIRED,
display_order: None,
value_names: &[],
help: None,
long_help: None,
env: None,
env_fallback: &[],
deprecated_env: &[],
default: &[],
accepted_choices: &[],
choices: &[],
choice_aliases: &[],
choice_details: &[],
ignore_case: false,
allow_unknown_choices: false,
validate: None,
validate_error: None,
required: true,
hide: false,
hide_default_value: false,
hide_env: false,
hide_env_values: false,
hide_possible_values: false,
hide_short_help: false,
hide_long_help: false,
conflicts: &[],
requires: &[],
required_if: &[],
required_if_eq: &[],
required_if_eq_all: &[],
required_unless: &[],
required_unless_all: &[],
var_min: None,
var_max: None,
delimiter: None,
help_heading: None,
};
}
#[derive(Debug, Clone, Copy)]
pub struct Example<'a> {
pub code: &'a str,
pub header: Option<&'a str>,
pub help: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Effect {
Read,
Write,
Destructive,
}
impl Effect {
pub fn as_str(self) -> &'static str {
match self {
Effect::Read => "read",
Effect::Write => "write",
Effect::Destructive => "destructive",
}
}
}
impl Spec<'_> {
pub fn to_kdl(&self) -> String {
self.render_kdl_with(&[])
}
fn render_kdl_with(&self, overlays: &[CommandOverlay<'_>]) -> String {
debug_assert!(
duplicate_key(self.root.cmd).is_none(),
"two things on the same command share a key ({:?}), so a parse would bind the \
wrong one. A derive builds keys from a hash of the type they came from, so this \
means two type names collided — or one struct was flattened into the same \
command twice.",
duplicate_key(self.root.cmd)
);
debug_assert!(
duplicate_flag_form(self.root.cmd).is_none(),
"two flags on the same command answer to {:?}, so only one of them could ever \
be reached. With `flatten` this is the collision neither expansion can see: \
the parent and the struct it flattens each declared it.",
duplicate_flag_form(self.root.cmd)
);
assert!(
duplicate_group_name(self.root).is_none(),
"two groups on the same command are called {:?}, so each would enforce only \
its own members and one from either side would satisfy neither. With \
`flatten` this is the collision neither expansion can see: the parent and \
the struct it flattens each declared it. Give one of them another name.",
duplicate_group_name(self.root)
);
debug_assert!(
unfillable_arg(self.root.cmd).is_none(),
"no word could ever reach the argument {:?}, because an unbounded variadic before \
it takes every remaining one. With `flatten` this is the arrangement neither \
expansion can see: the variadic and the argument after it were declared in \
different structs. Give the variadic a `var_max` — or, if the variadic leaves the \
`--` alone, make the later argument fillable only after one. A variadic that \
already requires a separator has spent it, and one declaring `preserve` takes it \
as a value, so neither can be stopped that way.",
unfillable_arg(self.root.cmd)
);
let mut out = String::new();
let _ = self.write_kdl(&mut out, overlays);
out
}
fn write_kdl(&self, out: &mut String, overlays: &[CommandOverlay<'_>]) -> core::fmt::Result {
if let Some(min) = self.min_usage_version {
prop(out, "min_usage_version", min)?;
}
prop(out, "name", self.name)?;
prop(out, "bin", self.bin.unwrap_or(self.name))?;
if let Some(version) = self.version {
prop(out, "version", version)?;
}
if let Some(version) = self.long_version {
prop(out, "long_version", version)?;
}
if let Some(author) = self.author {
prop(out, "author", author)?;
}
if let Some(license) = self.license {
prop(out, "license", license)?;
}
if let Some(repository) = self.repository {
prop(out, "repository", repository)?;
}
if let Some(template) = self.source_code_link_template {
prop(out, "source_code_link_template", template)?;
}
if let Some(about) = self.about.or(self.root.about) {
prop(out, "about", about)?;
}
if let Some(long_about) = self.long_about.or(self.root.long_about) {
prop(out, "long_about", long_about)?;
}
if let Some(message) = self.root.deprecated {
prop(out, "deprecated", message)?;
}
if let Some(at) = self.root.deprecated_warn_at {
prop(out, "deprecated_warn_at", at)?;
}
if let Some(at) = self.root.deprecated_remove_at {
prop(out, "deprecated_remove_at", at)?;
}
if let Some(usage) = self.usage {
prop(out, "usage", usage)?;
}
if let Some(template) = self.help_template {
prop(out, "help_template", template)?;
}
if self.root.cmd.unknown_flags == Some(UnknownFlags::Error) {
prop(out, "unknown_flags", "error")?;
}
if let Some(default_subcommand) = self.default_subcommand {
prop(out, "default_subcommand", default_subcommand)?;
}
if self.multicall {
writeln!(out, "multicall #true")?;
}
for view in self.views {
write!(out, "view {}", quoted(view.id))?;
if view.name != view.id {
write!(out, " name={}", quoted(view.name))?;
}
if view.bin != view.name {
write!(out, " bin={}", quoted(view.bin))?;
}
write!(out, " root={}", quoted(view.root))?;
if view.all_globals {
out.push_str(" globals=#true");
}
if view.globals.is_empty() {
out.push('\n');
} else {
out.push_str(" {\n global");
for selector in view.globals {
write!(out, " {}", quoted(selector))?;
}
out.push_str("\n}\n");
}
}
if self.root.cmd.external_subcommand {
writeln!(out, "external_subcommand #true")?;
}
if self.root.cmd.arg_required_else_help {
writeln!(out, "arg_required_else_help #true")?;
}
if self.root.cmd.disable_help_flag {
writeln!(out, "disable_help_flag #true")?;
}
if self.root.cmd.disable_help_subcommand {
writeln!(out, "disable_help_subcommand #true")?;
}
if self.root.cmd.disable_version_flag {
writeln!(out, "disable_version_flag #true")?;
}
if self.root.cmd.dont_delimit_trailing_values {
writeln!(out, "dont_delimit_trailing_values #true")?;
}
if !self.root.args_override_self {
writeln!(out, "args_override_self #false")?;
}
if self.root.cmd.subcommand_negates_reqs {
writeln!(out, "subcommand_negates_reqs #true")?;
}
if self.root.cmd.args_conflicts_with_subcommands {
writeln!(out, "args_conflicts_with_subcommands #true")?;
}
if self.root.cmd.subcommand_precedence_over_arg {
writeln!(out, "subcommand_precedence_over_arg #true")?;
}
if self.root.cmd.allow_missing_positional {
writeln!(out, "allow_missing_positional #true")?;
}
if self.root.subcommand_required && !self.root.cmd.subcommands.is_empty() {
writeln!(out, "subcommand_required #true")?;
}
if let Some(heading) = self.root.subcommand_help_heading {
prop(out, "subcommand_help_heading", heading)?;
}
if let Some(name) = self.root.subcommand_value_name {
prop(out, "subcommand_value_name", name)?;
}
if self.root.next_line_help {
writeln!(out, "next_line_help #true")?;
}
if self.root.flatten_help {
writeln!(out, "flatten_help #true")?;
}
if let Some(width) = self.root.term_width {
writeln!(out, "term_width {width}")?;
}
if let Some(width) = self.root.max_term_width {
writeln!(out, "max_term_width {width}")?;
}
for (node, text) in [
("before_help", self.root.before_help),
("before_long_help", self.root.before_long_help),
("after_help", self.root.after_help),
("after_long_help", self.root.after_long_help),
] {
if let Some(text) = text {
prop(out, node, text)?;
}
}
debug_assert!(
self.root.mount.is_none(),
"a mount on the root command cannot be written: the spec accepts \
`mount` only inside a `cmd` block"
);
debug_assert!(
self.root.effect.is_none()
&& !self.root.hide
&& self.root.restart_token.is_none()
&& self.root.cmd.aliases.is_empty()
&& self.root.hidden_aliases.is_empty(),
"the root command cannot carry an effect, hide, a restart token, or \
aliases: the spec accepts those only inside a `cmd` block"
);
for example in self.root.examples {
write_example(out, example, 0)?;
}
let w = Writing {
bin: self.bin.unwrap_or(self.name),
overlays,
sets: Flagsets::collect(self.root),
};
for entry in w.sets.written() {
writeln!(out, "flagset {} {{", quoted(entry.name))?;
write_flag_layout(out, entry.meta, 1, &w.sets)?;
out.push_str("}\n");
}
let mut path = Vec::new();
write_body(out, self.root, 0, UnknownFlags::Value, &w, &mut path)
}
}
fn same_group(a: &CommandMeta<'_>, b: &CommandMeta<'_>) -> bool {
core::ptr::eq(a, b)
|| (a.flags.len() == b.flags.len()
&& a.flags
.iter()
.zip(b.flags)
.all(|(x, y)| x.flag.key == y.flag.key))
}
struct Flagsets<'a> {
entries: Vec<FlagsetEntry<'a>>,
}
struct FlagsetEntry<'a> {
name: &'a str,
meta: &'a CommandMeta<'a>,
ambiguous: bool,
}
impl<'a> Flagsets<'a> {
fn collect(root: &'a CommandMeta<'a>) -> Self {
let mut sets = Self {
entries: Vec::new(),
};
sets.walk(root);
sets
}
fn walk(&mut self, meta: &'a CommandMeta<'a>) {
for group in meta.flatten_groups {
self.record(group);
}
for sub in meta.subcommands {
self.walk(sub);
}
}
fn record(&mut self, group: &'a FlattenGroup<'a>) {
if let Some(entry) = self.entries.iter_mut().find(|e| e.name == group.name) {
if same_group(entry.meta, group.meta) {
return;
}
entry.ambiguous = true;
for nested in group.meta.flatten_groups {
self.record(nested);
}
return;
}
self.entries.push(FlagsetEntry {
name: group.name,
meta: group.meta,
ambiguous: false,
});
for nested in group.meta.flatten_groups {
self.record(nested);
}
}
fn written(&self) -> impl Iterator<Item = &FlagsetEntry<'a>> {
self.entries.iter().filter(|e| Self::worth_writing(e))
}
fn covers(&self, group: &FlattenGroup<'_>) -> bool {
self.entries.iter().any(|e| {
e.name == group.name && Self::worth_writing(e) && same_group(e.meta, group.meta)
})
}
fn worth_writing(entry: &FlagsetEntry<'_>) -> bool {
!entry.ambiguous && !entry.meta.flags.is_empty()
}
}
struct Writing<'a, 'o> {
bin: &'a str,
overlays: &'o [CommandOverlay<'o>],
sets: Flagsets<'a>,
}
fn write_flag_layout(
out: &mut String,
meta: &CommandMeta<'_>,
depth: usize,
sets: &Flagsets<'_>,
) -> core::fmt::Result {
let mut i = 0;
while i < meta.flags.len() {
let group = meta
.flatten_groups
.iter()
.find(|g| g.start == i && !g.meta.flags.is_empty());
match group {
Some(group) if sets.covers(group) => {
indent(out, depth)?;
writeln!(out, "use {}", quoted(group.name))?;
i += group.meta.flags.len();
}
Some(group) => {
write_flag_layout(out, group.meta, depth, sets)?;
i += group.meta.flags.len();
}
None => {
debug_assert!(
meta.cmd
.flags
.get(i)
.is_some_and(|f| core::ptr::eq(*f, meta.flags[i].flag)),
"flag metadata is out of step with the parse table"
);
write_flag(out, &meta.flags[i], depth)?;
i += 1;
}
}
}
Ok(())
}
fn write_body<'a>(
out: &mut String,
meta: &CommandMeta<'a>,
depth: usize,
inherited_unknown_flags: UnknownFlags,
w: &Writing<'_, '_>,
path: &mut Vec<&'a str>,
) -> core::fmt::Result {
let enclosing_unknown_flags = meta.cmd.unknown_flags.unwrap_or(inherited_unknown_flags);
debug_assert_eq!(
meta.cmd.flags.len(),
meta.flags.len(),
"every flag in the parse table needs metadata, or it will not be written"
);
debug_assert_eq!(
meta.cmd.args.len(),
meta.args.len(),
"every argument in the parse table needs metadata"
);
debug_assert_eq!(
meta.cmd.subcommands.len(),
meta.subcommands.len(),
"every subcommand in the parse table needs metadata"
);
write_flag_layout(out, meta, depth, &w.sets)?;
for (i, arg) in meta.args.iter().enumerate() {
debug_assert!(
meta.cmd
.args
.get(i)
.is_some_and(|a| core::ptr::eq(*a, arg.arg)),
"argument metadata is out of step with the parse table"
);
write_arg(out, arg, depth)?;
}
write_completion_types(out, meta, depth)?;
for group in meta.groups {
write_group(out, group, depth)?;
}
#[cfg(feature = "complete")]
write_completers(out, meta, w.bin, depth)?;
for sub in meta.subcommands {
write_command(out, sub, depth, enclosing_unknown_flags, w, path)?;
}
Ok(())
}
fn write_completion_types<'a>(
out: &mut String,
meta: &CommandMeta<'a>,
depth: usize,
) -> core::fmt::Result {
let mut written: Vec<(String, &'a str)> = Vec::new();
for arg in meta.args {
if let Some(type_) = arg.complete_type {
write_completion_type(
out,
&mut written,
arg.arg.name.to_ascii_lowercase(),
type_,
depth,
)?;
}
}
for flag in meta.flags {
if let Some(type_) = flag.complete_type {
let name = flag
.value_name
.unwrap_or(flag.flag.name)
.to_ascii_lowercase();
write_completion_type(out, &mut written, name, type_, depth)?;
}
}
Ok(())
}
fn write_completion_type<'a>(
out: &mut String,
written: &mut Vec<(String, &'a str)>,
name: String,
type_: &'a str,
depth: usize,
) -> core::fmt::Result {
if written
.iter()
.any(|(written_name, written_type)| written_name == &name && *written_type == type_)
{
return Ok(());
}
indent(out, depth)?;
writeln!(out, "complete {} type={}", quoted(&name), quoted(type_))?;
written.push((name, type_));
Ok(())
}
fn write_command<'a>(
out: &mut String,
meta: &CommandMeta<'a>,
depth: usize,
inherited_unknown_flags: UnknownFlags,
w: &Writing<'_, '_>,
path: &mut Vec<&'a str>,
) -> core::fmt::Result {
path.push(meta.cmd.name);
indent(out, depth)?;
write!(out, "cmd {}", quoted(meta.cmd.name))?;
if let Some(help) = meta.about {
write!(out, " help={}", quoted(help))?;
}
if let Some(deprecated) = meta.deprecated {
write!(out, " deprecated={}", quoted(deprecated))?;
}
if let Some(at) = meta.deprecated_warn_at {
write!(out, " deprecated_warn_at={}", quoted(at))?;
}
if let Some(at) = meta.deprecated_remove_at {
write!(out, " deprecated_remove_at={}", quoted(at))?;
}
if meta.hide {
out.push_str(" hide=#true");
}
if let Some(heading) = meta.help_heading {
write!(out, " help_heading={}", quoted(heading))?;
}
let effect = w
.overlays
.iter()
.rev()
.find(|overlay| overlay.command.matches(meta, path))
.map(|overlay| overlay.effect)
.or(meta.effect);
if let Some(effect) = effect {
write!(out, " effect={}", quoted(effect.as_str()))?;
}
let effective_unknown_flags = meta.cmd.unknown_flags.unwrap_or(inherited_unknown_flags);
if effective_unknown_flags != inherited_unknown_flags {
write!(
out,
" unknown_flags={}",
quoted(match effective_unknown_flags {
UnknownFlags::Value => "value",
UnknownFlags::Error => "error",
})
)?;
}
if let Some(token) = meta.restart_token {
write!(out, " restart_token={}", quoted(token))?;
}
if meta.subcommand_required && !meta.cmd.subcommands.is_empty() {
out.push_str(" subcommand_required=#true");
}
if let Some(order) = meta.display_order {
write!(out, " display_order={order}")?;
}
if let Some(heading) = meta.subcommand_help_heading {
write!(out, " subcommand_help_heading={}", quoted(heading))?;
}
if let Some(name) = meta.subcommand_value_name {
write!(out, " subcommand_value_name={}", quoted(name))?;
}
if meta.next_line_help {
out.push_str(" next_line_help=#true");
}
if meta.flatten_help {
out.push_str(" flatten_help=#true");
}
if let Some(width) = meta.term_width {
write!(out, " term_width={width}")?;
}
if let Some(width) = meta.max_term_width {
write!(out, " max_term_width={width}")?;
}
if meta.cmd.external_subcommand {
out.push_str(" external_subcommand=#true");
}
if meta.cmd.arg_required_else_help {
out.push_str(" arg_required_else_help=#true");
}
if meta.cmd.disable_help_flag {
out.push_str(" disable_help_flag=#true");
}
if meta.cmd.disable_help_subcommand {
out.push_str(" disable_help_subcommand=#true");
}
if meta.cmd.disable_version_flag {
out.push_str(" disable_version_flag=#true");
}
if meta.cmd.dont_delimit_trailing_values {
out.push_str(" dont_delimit_trailing_values=#true");
}
if !meta.args_override_self {
out.push_str(" args_override_self=#false");
}
if meta.cmd.subcommand_negates_reqs {
out.push_str(" subcommand_negates_reqs=#true");
}
if meta.cmd.args_conflicts_with_subcommands {
out.push_str(" args_conflicts_with_subcommands=#true");
}
if meta.cmd.subcommand_precedence_over_arg {
out.push_str(" subcommand_precedence_over_arg=#true");
}
if meta.cmd.allow_missing_positional {
out.push_str(" allow_missing_positional=#true");
}
out.push_str(" {\n");
let inner = depth + 1;
for alias in meta.cmd.aliases {
indent(out, inner)?;
write!(out, "alias {}", quoted(alias))?;
if meta.hidden_aliases.contains(alias) {
out.push_str(" hide=#true");
}
out.push('\n');
}
if let Some(long_about) = meta.long_about {
indent(out, inner)?;
writeln!(out, "long_help {}", quoted(long_about))?;
}
for (node, text) in [
("before_help", meta.before_help),
("before_long_help", meta.before_long_help),
("after_help", meta.after_help),
("after_long_help", meta.after_long_help),
] {
if let Some(text) = text {
indent(out, inner)?;
writeln!(out, "{node} {}", quoted(text))?;
}
}
if let Some(mount) = meta.mount {
indent(out, inner)?;
writeln!(out, "mount run={}", quoted(mount))?;
}
for example in meta.examples {
write_example(out, example, inner)?;
}
write_body(out, meta, inner, effective_unknown_flags, w, path)?;
indent(out, depth)?;
out.push_str("}\n");
path.pop();
Ok(())
}
fn write_group(out: &mut String, group: &GroupMeta<'_>, depth: usize) -> core::fmt::Result {
indent(out, depth)?;
write!(out, "group {}", quoted(group.name))?;
for member in group.members {
write!(out, " {}", quoted(member))?;
}
if group.required {
out.push_str(" required=#true");
}
if group.multiple {
out.push_str(" multiple=#true");
}
out.push('\n');
Ok(())
}
fn write_example(out: &mut String, example: &Example<'_>, depth: usize) -> core::fmt::Result {
indent(out, depth)?;
write!(out, "example {}", quoted(example.code))?;
if let Some(header) = example.header {
write!(out, " header={}", quoted(header))?;
}
if let Some(help) = example.help {
write!(out, " help={}", quoted(help))?;
}
out.push('\n');
Ok(())
}
#[cfg(feature = "complete")]
fn write_completers(
out: &mut String,
meta: &CommandMeta<'_>,
bin: &str,
depth: usize,
) -> core::fmt::Result {
for name in crate::complete::completers_on(meta) {
indent(out, depth)?;
write!(out, "complete {}", quoted(&name))?;
write!(
out,
" run={}",
quoted(&format!(
"{bin} __complete_word__ --candidates {name} --line={{{{ words | shell_join | shell_quote }}}}"
))
)?;
writeln!(out)?;
}
Ok(())
}
fn write_flag(out: &mut String, meta: &FlagMeta<'_>, depth: usize) -> core::fmt::Result {
indent(out, depth)?;
write!(out, "flag {}", quoted(&flag_forms(meta)))?;
if let Some(help) = meta.help {
write!(out, " help={}", quoted(help))?;
}
if let Some(deprecated) = meta.deprecated {
write!(out, " deprecated={}", quoted(deprecated))?;
}
if let Some(at) = meta.deprecated_warn_at {
write!(out, " deprecated_warn_at={}", quoted(at))?;
}
if let Some(at) = meta.deprecated_remove_at {
write!(out, " deprecated_remove_at={}", quoted(at))?;
}
if meta.required {
out.push_str(" required=#true");
}
if meta.flag.global {
out.push_str(" global=#true");
}
if meta.hide {
out.push_str(" hide=#true");
}
write_help_hides(
out,
meta.hide_default_value,
meta.hide_env,
meta.hide_env_values,
meta.hide_possible_values,
meta.hide_short_help,
meta.hide_long_help,
)?;
if meta.count {
out.push_str(" count=#true");
}
if meta.flag.action != crate::ArgAction::Set {
write!(
out,
" action={}",
quoted(match meta.flag.action {
crate::ArgAction::Set => "set",
crate::ArgAction::Help => "help",
crate::ArgAction::HelpShort => "help_short",
crate::ArgAction::HelpLong => "help_long",
crate::ArgAction::HelpAll => "help_all",
crate::ArgAction::Version => "version",
})
)?;
}
if meta.repeatable {
out.push_str(" var=#true");
}
if let Some(min) = meta.var_min {
write!(out, " var_min={min}")?;
}
if let Some(max) = meta.var_max {
write!(out, " var_max={max}")?;
}
if let Some(negate) = meta.flag.negate {
write!(out, " negate={}", quoted(&format!("--{negate}")))?;
}
if let Some(heading) = meta.help_heading {
write!(out, " help_heading={}", quoted(heading))?;
}
if let Some(order) = meta.display_order {
write!(out, " display_order={order}")?;
}
if let Some(effect) = meta.effect {
write!(out, " effect={}", quoted(effect.as_str()))?;
}
if let Some(env) = meta.env {
write!(out, " env={}", quoted(env))?;
}
write_single_list(out, "env_fallback", meta.env_fallback)?;
write_single_list(out, "deprecated_env", meta.deprecated_env)?;
write_single_default(out, meta.default)?;
write_single_list(out, "overrides", meta.overrides)?;
write_single_list(out, "conflicts", meta.conflicts)?;
if meta.exclusive {
out.push_str(" exclusive=#true");
}
if let Some(delimiter) = meta.delimiter {
write!(out, " delimiter={}", quoted(&delimiter.to_string()))?;
}
if meta.flag.allow_hyphen_values {
out.push_str(" allow_hyphen_values=#true");
}
if meta.flag.allow_negative_numbers {
out.push_str(" allow_negative_numbers=#true");
}
if let Some(terminator) = meta.flag.value_terminator {
write!(
out,
" value_terminator={}",
quoted(::core::str::from_utf8(terminator).unwrap_or_default())
)?;
}
if meta.flag.require_equals {
out.push_str(" require_equals=#true");
}
if meta.flag.value_optional {
out.push_str(" value_optional=#true");
}
if meta.flag.bool_value {
out.push_str(" bool_value=#true");
}
if let Some(missing) = meta.flag.default_missing {
write!(
out,
" default_missing={}",
quoted(::core::str::from_utf8(missing).unwrap_or_default())
)?;
}
write_single_list(out, "requires", meta.requires)?;
write_single_list(out, "required_if", meta.required_if)?;
write_single_list(out, "required_unless", meta.required_unless)?;
write_single_list(out, "required_unless_all", meta.required_unless_all)?;
let has_children = meta.long_help.is_some()
|| !meta.hidden_shorts.is_empty()
|| !meta.hidden_longs.is_empty()
|| meta.flag.takes_value
|| !meta.choices.is_empty()
|| meta.default.len() > 1
|| meta.overrides.len() > 1
|| meta.conflicts.len() > 1
|| meta.requires.len() > 1
|| !meta.requires_if.is_empty()
|| !meta.default_if.is_empty()
|| !meta.required_if_eq.is_empty()
|| !meta.required_if_eq_all.is_empty()
|| meta.required_if.len() > 1
|| meta.required_unless.len() > 1
|| meta.required_unless_all.len() > 1
|| meta.env_fallback.len() > 1
|| meta.deprecated_env.len() > 1;
if !has_children {
out.push('\n');
return Ok(());
}
out.push_str(" {\n");
let inner = depth + 1;
if let Some(long_help) = meta.long_help {
indent(out, inner)?;
writeln!(out, "long_help {}", quoted(long_help))?;
}
if !meta.hidden_shorts.is_empty() || !meta.hidden_longs.is_empty() {
indent(out, inner)?;
out.push_str("alias");
for alias in meta.hidden_shorts {
write!(out, " {}", quoted(&format!("-{}", *alias as char)))?;
}
for alias in meta.hidden_longs {
write!(out, " {}", quoted(&format!("--{alias}")))?;
}
out.push_str(" hide=#true\n");
}
write_many_defaults(out, meta.default, inner)?;
write_many_list(out, "overrides", meta.overrides, inner)?;
write_many_list(out, "conflicts", meta.conflicts, inner)?;
write_many_list(out, "requires", meta.requires, inner)?;
for condition in meta.requires_if {
indent(out, inner)?;
writeln!(
out,
"requires_if {} {}",
quoted(condition.value),
quoted(condition.requires)
)?;
}
for condition in meta.default_if {
indent(out, inner)?;
match condition.when {
None => writeln!(
out,
"default_if {} {}",
quoted(condition.selector),
quoted(condition.value)
)?,
Some(when) => writeln!(
out,
"default_if {} {} {}",
quoted(condition.selector),
quoted(when),
quoted(condition.value)
)?,
}
}
for condition in meta.required_if_eq {
indent(out, inner)?;
writeln!(
out,
"required_if_eq {} {}",
quoted(condition.selector),
quoted(condition.value)
)?;
}
if !meta.required_if_eq_all.is_empty() {
indent(out, inner)?;
out.push_str("required_if_eq_all");
for condition in meta.required_if_eq_all {
write!(
out,
" {} {}",
quoted(condition.selector),
quoted(condition.value)
)?;
}
out.push('\n');
}
write_many_list(out, "required_if", meta.required_if, inner)?;
write_many_list(out, "required_unless", meta.required_unless, inner)?;
write_many_list(out, "required_unless_all", meta.required_unless_all, inner)?;
write_many_list(out, "env_fallback", meta.env_fallback, inner)?;
write_many_list(out, "deprecated_env", meta.deprecated_env, inner)?;
if meta.flag.takes_value {
indent(out, inner)?;
let exact = exact_arity(meta.value_var_min, meta.value_var_max);
let rendered = if meta.value_names.len() <= 1 && exact.is_some_and(|n| n > 1) {
let name = meta
.value_names
.first()
.copied()
.or(meta.value_name)
.unwrap_or(meta.flag.name);
(0..exact.unwrap())
.map(|_| placeholder(name, false, meta.value_optional))
.collect::<Vec<_>>()
.join(" ")
} else if meta.value_names.len() <= 1 {
let name = meta
.value_names
.first()
.copied()
.or(meta.value_name)
.unwrap_or(meta.flag.name);
placeholder(name, meta.flag.variadic, meta.value_optional)
} else {
meta.value_names
.iter()
.map(|name| placeholder(name, false, meta.value_optional))
.collect::<Vec<_>>()
.join(" ")
};
write!(out, "arg {}", quoted(&rendered))?;
if let Some(min) = meta.value_var_min {
write!(out, " var_min={min}")?;
}
if let Some(max) = meta.value_var_max {
write!(out, " var_max={max}")?;
}
if meta.value_optional {
out.push_str(" required=#false");
}
if let Some(validate) = meta.validate {
write!(out, " validate={}", quoted(validate))?;
}
if meta.validate.is_some() {
if let Some(error) = meta.validate_error {
write!(out, " validate_error={}", quoted(error))?;
}
}
if meta.choices.is_empty()
&& meta.accepted_choices.is_empty()
&& meta.choice_details.is_empty()
{
out.push('\n');
} else {
out.push_str(" {\n");
write_choices(
out,
meta.choices,
meta.accepted_choices,
meta.choice_aliases,
meta.choice_details,
(meta.ignore_case, meta.allow_unknown_choices),
inner + 1,
)?;
indent(out, inner)?;
out.push_str("}\n");
}
} else if !meta.choices.is_empty()
|| !meta.accepted_choices.is_empty()
|| !meta.choice_details.is_empty()
{
write_choices(
out,
meta.choices,
meta.accepted_choices,
meta.choice_aliases,
meta.choice_details,
(meta.ignore_case, meta.allow_unknown_choices),
inner,
)?;
}
indent(out, depth)?;
out.push_str("}\n");
Ok(())
}
fn write_help_hides(
out: &mut String,
hide_default_value: bool,
hide_env: bool,
hide_env_values: bool,
hide_possible_values: bool,
hide_short_help: bool,
hide_long_help: bool,
) -> core::fmt::Result {
for (name, hidden) in [
("hide_default_value", hide_default_value),
("hide_env", hide_env),
("hide_env_values", hide_env_values),
("hide_possible_values", hide_possible_values),
("hide_short_help", hide_short_help),
("hide_long_help", hide_long_help),
] {
if hidden {
write!(out, " {name}=#true")?;
}
}
Ok(())
}
fn write_arg(out: &mut String, meta: &ArgMeta<'_>, depth: usize) -> core::fmt::Result {
indent(out, depth)?;
let name = if meta.arg.name.is_empty() {
"arg"
} else {
meta.arg.name
};
write!(out, "arg {}", quoted(&arg_placeholder(name, meta)))?;
if let Some(help) = meta.help {
write!(out, " help={}", quoted(help))?;
}
if meta.hide {
out.push_str(" hide=#true");
}
write_help_hides(
out,
meta.hide_default_value,
meta.hide_env,
meta.hide_env_values,
meta.hide_possible_values,
meta.hide_short_help,
meta.hide_long_help,
)?;
if meta.conflicts.len() == 1 {
write!(out, " conflicts={}", quoted(meta.conflicts[0]))?;
}
if let Some(min) = meta.var_min {
write!(out, " var_min={min}")?;
}
if let Some(max) = meta.var_max {
write!(out, " var_max={max}")?;
}
if let Some(delimiter) = meta.delimiter {
write!(out, " delimiter={}", quoted(&delimiter.to_string()))?;
}
if meta.arg.allow_negative_numbers {
out.push_str(" allow_negative_numbers=#true");
}
if let Some(terminator) = meta.arg.value_terminator {
write!(
out,
" value_terminator={}",
quoted(::core::str::from_utf8(terminator).unwrap_or_default())
)?;
}
if meta.arg.double_dash != DoubleDash::Optional {
let mode = match meta.arg.double_dash {
DoubleDash::Required => "required",
DoubleDash::Preserve => "preserve",
DoubleDash::Automatic => "automatic",
DoubleDash::Optional => unreachable!("excluded by the branch above"),
};
write!(out, " double_dash={}", quoted(mode))?;
}
if let Some(heading) = meta.help_heading {
write!(out, " help_heading={}", quoted(heading))?;
}
if let Some(order) = meta.display_order {
write!(out, " display_order={order}")?;
}
if let Some(env) = meta.env {
write!(out, " env={}", quoted(env))?;
}
write_single_list(out, "env_fallback", meta.env_fallback)?;
write_single_list(out, "deprecated_env", meta.deprecated_env)?;
if let Some(validate) = meta.validate {
write!(out, " validate={}", quoted(validate))?;
}
if meta.validate.is_some() {
if let Some(error) = meta.validate_error {
write!(out, " validate_error={}", quoted(error))?;
}
}
write_single_default(out, meta.default)?;
write_single_list(out, "requires", meta.requires)?;
write_single_list(out, "required_if", meta.required_if)?;
write_single_list(out, "required_unless", meta.required_unless)?;
write_single_list(out, "required_unless_all", meta.required_unless_all)?;
let has_children = meta.long_help.is_some()
|| !meta.choices.is_empty()
|| !meta.accepted_choices.is_empty()
|| !meta.choice_details.is_empty()
|| meta.default.len() > 1
|| meta.conflicts.len() > 1
|| meta.requires.len() > 1
|| meta.required_if.len() > 1
|| !meta.required_if_eq.is_empty()
|| !meta.required_if_eq_all.is_empty()
|| meta.required_unless.len() > 1
|| meta.required_unless_all.len() > 1
|| meta.env_fallback.len() > 1
|| meta.deprecated_env.len() > 1;
if !has_children {
out.push('\n');
return Ok(());
}
out.push_str(" {\n");
let inner = depth + 1;
if let Some(long_help) = meta.long_help {
indent(out, inner)?;
writeln!(out, "long_help {}", quoted(long_help))?;
}
if meta.conflicts.len() > 1 {
indent(out, inner)?;
out.push_str("conflicts");
for conflict in meta.conflicts {
write!(out, " {}", quoted(conflict))?;
}
out.push('\n');
}
write_many_list(out, "requires", meta.requires, inner)?;
write_many_list(out, "required_if", meta.required_if, inner)?;
for condition in meta.required_if_eq {
indent(out, inner)?;
writeln!(
out,
"required_if_eq {} {}",
quoted(condition.selector),
quoted(condition.value)
)?;
}
if !meta.required_if_eq_all.is_empty() {
indent(out, inner)?;
out.push_str("required_if_eq_all");
for condition in meta.required_if_eq_all {
write!(
out,
" {} {}",
quoted(condition.selector),
quoted(condition.value)
)?;
}
out.push('\n');
}
write_many_list(out, "required_unless", meta.required_unless, inner)?;
write_many_list(out, "required_unless_all", meta.required_unless_all, inner)?;
write_many_list(out, "env_fallback", meta.env_fallback, inner)?;
write_many_list(out, "deprecated_env", meta.deprecated_env, inner)?;
write_many_defaults(out, meta.default, inner)?;
write_choices(
out,
meta.choices,
meta.accepted_choices,
meta.choice_aliases,
meta.choice_details,
(meta.ignore_case, meta.allow_unknown_choices),
inner,
)?;
indent(out, depth)?;
out.push_str("}\n");
Ok(())
}
fn write_single_list(out: &mut String, key: &str, values: &[&str]) -> core::fmt::Result {
if let [only] = values {
write!(out, " {key}={}", quoted(only))?;
}
Ok(())
}
fn write_many_list(
out: &mut String,
key: &str,
values: &[&str],
depth: usize,
) -> core::fmt::Result {
if values.len() < 2 {
return Ok(());
}
indent(out, depth)?;
write!(out, "{key}")?;
for value in values {
write!(out, " {}", quoted(value))?;
}
out.push('\n');
Ok(())
}
fn write_single_default(out: &mut String, defaults: &[&str]) -> core::fmt::Result {
if let [only] = defaults {
write!(out, " default={}", quoted(only))?;
}
Ok(())
}
fn write_many_defaults(out: &mut String, defaults: &[&str], depth: usize) -> core::fmt::Result {
if defaults.len() < 2 {
return Ok(());
}
indent(out, depth)?;
out.push_str("default {\n");
for value in defaults {
indent(out, depth + 1)?;
writeln!(out, "{}", quoted(value))?;
}
indent(out, depth)?;
out.push_str("}\n");
Ok(())
}
fn write_choices(
out: &mut String,
choices: &[&str],
accepted_choices: &[&str],
aliases: &[(&str, &str)],
details: &[ChoiceMeta<'_>],
policy: (bool, bool),
depth: usize,
) -> core::fmt::Result {
if choices.is_empty() && accepted_choices.is_empty() && details.is_empty() {
return Ok(());
}
indent(out, depth)?;
out.push_str("choices");
let (ignore_case, allow_unknown) = policy;
if ignore_case {
out.push_str(" ignore_case=#true");
}
if allow_unknown {
out.push_str(" strict=#false");
}
if !details.is_empty() {
out.push_str(" {\n");
let is_alias = |value: &str| {
aliases.iter().any(|(_, alias)| *alias == value)
|| details
.iter()
.flat_map(|choice| choice.aliases)
.any(|alias| alias.value == value)
};
let mut canonicals = std::vec::Vec::new();
for value in choices
.iter()
.chain(aliases.iter().map(|(canonical, _)| canonical))
.chain(accepted_choices.iter())
.chain(details.iter().map(|choice| &choice.value))
{
if !is_alias(value) && !canonicals.contains(value) {
canonicals.push(*value);
}
}
for value in canonicals {
let detail = details.iter().find(|choice| choice.value == value);
indent(out, depth + 1)?;
write!(out, "choice {}", quoted(value))?;
if let Some(help) = detail.and_then(|choice| choice.help) {
write!(out, " help={}", quoted(help))?;
}
if detail.is_some_and(|choice| choice.hide) || !choices.contains(&value) {
out.push_str(" hide=#true");
}
let fallback_aliases: std::vec::Vec<ChoiceAliasMeta<'_>> = aliases
.iter()
.filter_map(|(canonical, alias)| {
(*canonical == value).then_some(ChoiceAliasMeta {
value: alias,
hide: !choices.contains(alias),
})
})
.collect();
let choice_aliases = detail
.map(|choice| choice.aliases)
.unwrap_or(&fallback_aliases);
if choice_aliases.is_empty() {
out.push('\n');
continue;
}
out.push_str(" {\n");
for alias in choice_aliases {
indent(out, depth + 2)?;
write!(out, "alias {}", quoted(alias.value))?;
if alias.hide {
out.push_str(" hide=#true");
}
out.push('\n');
}
indent(out, depth + 1)?;
out.push_str("}\n");
}
indent(out, depth)?;
out.push_str("}\n");
return Ok(());
}
let has_hidden_accepted = accepted_choices
.iter()
.any(|value| !choices.contains(value));
if aliases.is_empty() && !has_hidden_accepted {
for choice in choices {
write!(out, " {}", quoted(choice))?;
}
out.push('\n');
return Ok(());
}
out.push_str(" {\n");
let is_alias = |value: &str| aliases.iter().any(|(_, alias)| *alias == value);
let mut canonicals = std::vec::Vec::new();
for value in choices
.iter()
.chain(aliases.iter().map(|(canonical, _)| canonical))
.chain(accepted_choices.iter())
{
if !is_alias(value) && !canonicals.contains(value) {
canonicals.push(*value);
}
}
for choice in canonicals {
indent(out, depth + 1)?;
write!(out, "choice {}", quoted(choice))?;
if !choices.contains(&choice) {
out.push_str(" hide=#true");
}
let choice_aliases: std::vec::Vec<&str> = aliases
.iter()
.filter_map(|(canonical, alias)| (*canonical == choice).then_some(*alias))
.collect();
if choice_aliases.is_empty() {
out.push('\n');
continue;
}
out.push_str(" {\n");
for alias in choice_aliases {
indent(out, depth + 2)?;
write!(out, "alias {}", quoted(alias))?;
if !choices.contains(&alias) {
out.push_str(" hide=#true");
}
out.push('\n');
}
indent(out, depth + 1)?;
out.push_str("}\n");
}
indent(out, depth)?;
out.push_str("}\n");
Ok(())
}
fn flag_forms(meta: &FlagMeta<'_>) -> String {
let flag = meta.flag;
let mut forms = String::new();
for short in flag.shorts {
if meta.hidden_shorts.contains(short) {
continue;
}
if !forms.is_empty() {
forms.push(' ');
}
forms.push('-');
forms.push(*short as char);
}
for long in flag.longs {
if meta.hidden_longs.contains(long) {
continue;
}
if !forms.is_empty() {
forms.push(' ');
}
forms.push_str("--");
forms.push_str(long);
}
if forms.is_empty() {
forms.push_str(flag.name);
forms.push(':');
}
forms
}
fn placeholder(name: &str, variadic: bool, optional: bool) -> String {
let ellipsis = if variadic { "..." } else { "" };
let (open, close) = if optional { ('[', ']') } else { ('<', '>') };
format!("{open}{name}{close}{ellipsis}")
}
fn exact_arity(min: Option<usize>, max: Option<usize>) -> Option<usize> {
match (min, max) {
(Some(min), Some(max)) if min == max => Some(min),
_ => None,
}
}
fn arg_placeholder(name: &str, meta: &ArgMeta<'_>) -> String {
if let Some(arity) =
exact_arity(meta.var_min, meta.var_max).filter(|n| *n > 1 && meta.value_names.len() <= 1)
{
let values = (0..arity)
.map(|_| placeholder(name, false, !meta.required))
.collect::<Vec<_>>()
.join(" ");
return values;
}
if meta.value_names.len() > 1 {
let (open, close) = if meta.required {
('<', '>')
} else {
('[', ']')
};
let values = meta
.value_names
.iter()
.map(|name| format!("{open}{name}{close}"))
.collect::<Vec<_>>()
.join(" ");
return if meta.arg.double_dash == DoubleDash::Required {
format!("-- {values}")
} else {
values
};
}
let ellipsis = if meta.arg.var { "..." } else { "" };
if meta.required {
format!("<{name}>{ellipsis}")
} else {
format!("[{name}]{ellipsis}")
}
}
fn indent(out: &mut String, depth: usize) -> core::fmt::Result {
for _ in 0..depth {
out.push_str(" ");
}
Ok(())
}
fn prop(out: &mut String, key: &str, value: &str) -> core::fmt::Result {
writeln!(out, "{key} {}", quoted(value))
}
fn quoted(value: &str) -> String {
if !value.is_empty() && is_plain_kdl_identifier(value) {
return value.to_owned();
}
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c.is_control() => {
let _ = write!(out, "\\u{{{:x}}}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
fn is_plain_kdl_identifier(value: &str) -> bool {
value.chars().all(|c| !is_disallowed_kdl_identifier_char(c))
&& !starts_like_kdl_number(value)
&& !matches!(value, "inf" | "-inf" | "nan" | "true" | "false" | "null")
}
fn starts_like_kdl_number(value: &str) -> bool {
let unsigned = value.strip_prefix(['-', '+']).unwrap_or(value).as_bytes();
unsigned.first().is_some_and(u8::is_ascii_digit)
|| (unsigned.first() == Some(&b'.') && unsigned.get(1).is_some_and(u8::is_ascii_digit))
}
fn is_disallowed_kdl_identifier_char(c: char) -> bool {
matches!(
c,
'\\' | '/' | '(' | ')' | '{' | '}' | '[' | ']' | ';' | '"' | '#' | '='
) || matches!(
c,
'\u{0000}'..='\u{0008}'
| '\u{000A}'..='\u{001F}'
| '\u{0085}'
| '\u{00A0}'
| '\u{1680}'
| '\u{2000}'..='\u{200A}'
| '\u{200E}'..='\u{200F}'
| '\u{2028}'..='\u{202F}'
| '\u{205F}'
| '\u{2066}'..='\u{2069}'
| '\u{3000}'
| '\u{FEFF}'
) || matches!(c, '\u{0009}' | '\u{0020}')
}
pub trait ValueEnum: Sized {
const CHOICES: &'static [&'static str];
const ACCEPTED_CHOICES: &'static [&'static str] = Self::CHOICES;
const ALIASES: &'static [(&'static str, &'static str)] = &[];
const DETAILS: &'static [ChoiceMeta<'static>] = &[];
const IGNORE_CASE: bool = false;
fn from_choice(value: &str) -> Option<Self>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChoiceMeta<'a> {
pub value: &'a str,
pub help: Option<&'a str>,
pub hide: bool,
pub aliases: &'a [ChoiceAliasMeta<'a>],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChoiceAliasMeta<'a> {
pub value: &'a str,
pub hide: bool,
}
pub fn choice_matches(choices: &[&str], value: &str, ignore_case: bool) -> bool {
choices
.iter()
.any(|choice| *choice == value || ignore_case && choice.eq_ignore_ascii_case(value))
}
pub trait ArgGroup: Sized {
const NAME: &'static str;
const FLAGS: &'static [&'static Flag<'static>];
const FLAG_METAS: &'static [FlagMeta<'static>];
const MEMBERS: &'static [&'static str];
type Partial: Default;
fn start() -> Self::Partial {
Self::Partial::default()
}
fn apply(partial: &mut Self::Partial, event: &crate::Event<'_, '_, '_>) -> bool;
fn any_given(partial: &Self::Partial) -> Option<&'static str>;
fn conflict(partial: &Self::Partial) -> Option<(&'static str, &'static str)>;
fn build(partial: &Self::Partial) -> Option<Self>;
fn argument_state(partial: &Self::Partial, selector: &str) -> Option<ArgumentState>;
fn standing_state(standing: &Self, selector: &str) -> Option<ArgumentState> {
let _ = (standing, selector);
None
}
fn argument_matches(partial: &Self::Partial, selector: &str, value: &[u8]) -> Option<bool>;
fn standing_matches(standing: &Self, selector: &str, value: &[u8]) -> Option<bool> {
let _ = (standing, selector, value);
None
}
fn displace(partial: &mut Self::Partial, selector: &str) -> bool;
fn event_matches(event: &crate::Event<'_, '_, '_>, selector: &str) -> bool;
}
#[derive(Debug, Clone, PartialEq)]
pub enum SettingGiven {
Bool(bool),
Int(i64),
Text(String),
List(Vec<String>),
NotText,
}
pub const fn concat_bindings<const N: usize>(
parts: &[&'static [(&'static str, &'static str)]],
) -> [(&'static str, &'static str); N] {
let mut joined = [("", ""); N];
let mut at = 0;
let mut part = 0;
while part < parts.len() {
let mut i = 0;
while i < parts[part].len() {
joined[at] = parts[part][i];
at += 1;
i += 1;
}
part += 1;
}
joined
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ArgumentState {
pub name: &'static str,
pub given: bool,
pub satisfied: bool,
}
pub trait CommandArgs: Sized {
type Partial;
const COMMAND: &'static Command<'static>;
const META: &'static CommandMeta<'static>;
fn start() -> Self::Partial;
fn apply(partial: &mut Self::Partial, event: &crate::Event<'_, '_, '_>) -> bool;
fn apply_mirrored_global(
partial: &mut Self::Partial,
event: &crate::Event<'_, '_, '_>,
) -> bool {
let _ = (partial, event);
false
}
const SETTINGS_BINDINGS: &'static [(&'static str, &'static str)] = &[];
fn settings_given(partial: &Self::Partial) -> Vec<(&'static str, SettingGiven)> {
let _ = partial;
Vec::new()
}
fn any_given(partial: &Self::Partial) -> Option<&'static str> {
let _ = partial;
None
}
fn exclusive_given(partial: &Self::Partial) -> Option<&'static str> {
let _ = partial;
None
}
fn deprecations(partial: &Self::Partial, out: &mut Vec<crate::warn::Warning<'static>>) {
let _ = (partial, out);
}
fn deprecations_for_view_path(
partial: &Self::Partial,
remaining_descendants: usize,
out: &mut Vec<crate::warn::Warning<'static>>,
) {
if remaining_descendants == 0 {
Self::deprecations(partial, out);
}
}
fn argument_state(partial: &Self::Partial, selector: &str) -> Option<ArgumentState> {
let _ = (partial, selector);
None
}
fn argument_matches(partial: &Self::Partial, selector: &str, value: &[u8]) -> Option<bool> {
let _ = (partial, selector, value);
None
}
fn displace(partial: &mut Self::Partial, selector: &str) -> bool {
let _ = (partial, selector);
false
}
fn event_matches(event: &crate::Event<'_, '_, '_>, selector: &str) -> bool {
let _ = (event, selector);
false
}
fn apply_defaults(partial: &mut Self::Partial) {
let _ = partial;
}
fn apply_defaults_for_view(
partial: &mut Self::Partial,
view: Option<&'static ViewMeta<'static>>,
) {
let _ = view;
Self::apply_defaults(partial);
}
fn apply_env(partial: &mut Self::Partial) {
let _ = partial;
}
fn apply_env_for_view(partial: &mut Self::Partial, view: Option<&'static ViewMeta<'static>>) {
let _ = view;
Self::apply_env(partial);
}
fn apply_env_for_view_path(partial: &mut Self::Partial, remaining_descendants: usize) {
if remaining_descendants == 0 {
Self::apply_env(partial);
}
}
fn check<'t, 'v>(partial: &mut Self::Partial) -> Result<(), crate::Error<'t, 'v>> {
let _ = partial;
Ok(())
}
fn check_with_args_override_self<'t, 'v>(
partial: &mut Self::Partial,
args_override_self: bool,
) -> Result<(), crate::Error<'t, 'v>> {
let _ = args_override_self;
Self::check(partial)
}
fn check_with_args_override_self_for_view<'t, 'v>(
partial: &mut Self::Partial,
args_override_self: bool,
view: Option<&'static ViewMeta<'static>>,
) -> Result<(), crate::Error<'t, 'v>> {
let _ = view;
Self::check_with_args_override_self(partial, args_override_self)
}
fn check_for_view_path<'t, 'v>(
partial: &mut Self::Partial,
remaining_descendants: usize,
) -> Result<(), crate::Error<'t, 'v>> {
if remaining_descendants == 0 {
Self::check(partial)
} else {
Ok(())
}
}
fn omit_own_for_view(partial: &mut Self::Partial) {
let _ = partial;
}
fn build<'t, 'v>(partial: Self::Partial) -> Result<Self, crate::Error<'t, 'v>>;
fn any_standing(standing: &Self) -> Option<&'static str> {
let _ = standing;
None
}
fn apply_defaults_update(partial: &mut Self::Partial, standing: &Self) {
let _ = standing;
Self::apply_defaults(partial);
}
fn apply_env_update(partial: &mut Self::Partial, standing: &Self) {
let _ = standing;
Self::apply_env(partial);
}
fn check_update<'t, 'v>(
partial: &mut Self::Partial,
standing: &Self,
) -> Result<(), crate::Error<'t, 'v>> {
let _ = standing;
Self::check(partial)
}
fn check_update_with_args_override_self<'t, 'v>(
partial: &mut Self::Partial,
args_override_self: bool,
standing: &Self,
) -> Result<(), crate::Error<'t, 'v>> {
let _ = standing;
Self::check_with_args_override_self(partial, args_override_self)
}
fn merge<'t, 'v>(
partial: Self::Partial,
standing: &mut Self,
) -> Result<(), crate::Error<'t, 'v>> {
*standing = Self::build(partial)?;
Ok(())
}
}
pub trait Omitted<T> {
fn omitted() -> T;
}
pub struct DefaultViewOmitter;
impl<T: Default> Omitted<T> for DefaultViewOmitter {
fn omitted() -> T {
T::default()
}
}
pub trait ViewCommandArgs<O>: CommandArgs {
fn build_for_view<'t, 'v>(partial: Self::Partial) -> Result<Self, crate::Error<'t, 'v>>;
}
pub const fn flag_selector_count(command: &Command<'_>, selector: &str) -> usize {
let selector = selector.as_bytes();
let mut count = 0;
let mut i = 0;
while i < command.flags.len() {
let flag = command.flags[i];
let mut matched = false;
if selector.len() == 2 && selector[0] == b'-' && selector[1] != b'-' {
let mut short = 0;
while short < flag.shorts.len() {
if flag.shorts[short] == selector[1] {
matched = true;
}
short += 1;
}
} else if selector.len() > 2 && selector[0] == b'-' && selector[1] == b'-' {
let mut long = 0;
while long < flag.longs.len() {
if long_selector_equal(selector, flag.longs[long].as_bytes()) {
matched = true;
}
long += 1;
}
if let Some(negate) = flag.negate {
if long_selector_equal(selector, negate.as_bytes()) {
matched = true;
}
}
}
if matched {
count += 1;
}
i += 1;
}
count
}
const fn long_selector_equal(selector: &[u8], name: &[u8]) -> bool {
if selector.len() != name.len() + 2 {
return false;
}
let mut i = 0;
while i < name.len() {
if selector[i + 2] != name[i] {
return false;
}
i += 1;
}
true
}
pub trait Subcommands: Sized {
type Partial: Default;
const COMMANDS: &'static [&'static Command<'static>];
const METAS: &'static [&'static CommandMeta<'static>];
const HAS_EXTERNAL: bool = false;
const EXTERNAL: Option<usize> = None;
const VARIANT_OF: &'static [usize] = &[];
fn apply(
partial: &mut Self::Partial,
selected: Option<usize>,
event: &crate::Event<'_, '_, '_>,
) -> bool;
fn begin(partial: &mut Self::Partial, selected: usize) {
let _ = (partial, selected);
}
const SETTINGS_BINDINGS: &'static [(&'static str, &'static str)] = &[];
fn settings_given(
partial: &Self::Partial,
selected: Option<usize>,
) -> Vec<(&'static str, SettingGiven)> {
let _ = (partial, selected);
Vec::new()
}
fn any_given(partial: &Self::Partial, selected: Option<usize>) -> Option<&'static str> {
let _ = (partial, selected);
None
}
fn exclusive_given(partial: &Self::Partial, selected: Option<usize>) -> Option<&'static str> {
let _ = (partial, selected);
None
}
fn deprecations(
partial: &Self::Partial,
selected: Option<usize>,
out: &mut Vec<crate::warn::Warning<'static>>,
) {
let _ = (partial, selected, out);
}
fn deprecations_for_view_path(
partial: &Self::Partial,
selected: Option<usize>,
remaining_commands: usize,
out: &mut Vec<crate::warn::Warning<'static>>,
) {
if remaining_commands <= 1 {
Self::deprecations(partial, selected, out);
}
}
fn apply_env(partial: &mut Self::Partial, selected: Option<usize>) {
let _ = (partial, selected);
}
fn apply_env_for_view_path(
partial: &mut Self::Partial,
selected: Option<usize>,
remaining_commands: usize,
) {
if remaining_commands <= 1 {
Self::apply_env(partial, selected);
}
}
fn check<'t, 'v>(
partial: &mut Self::Partial,
selected: usize,
) -> Result<(), crate::Error<'t, 'v>>;
fn check_for_view_path<'t, 'v>(
partial: &mut Self::Partial,
selected: usize,
remaining_commands: usize,
) -> Result<(), crate::Error<'t, 'v>> {
if remaining_commands <= 1 {
Self::check(partial, selected)
} else {
Ok(())
}
}
fn select<'t, 'v>(
partial: Self::Partial,
selected: usize,
) -> Result<Option<Self>, crate::Error<'t, 'v>>;
fn apply_env_update(partial: &mut Self::Partial, selected: Option<usize>, standing: &Self) {
let _ = standing;
Self::apply_env(partial, selected);
}
fn check_update<'t, 'v>(
partial: &mut Self::Partial,
selected: usize,
standing: &Self,
) -> Result<(), crate::Error<'t, 'v>> {
let _ = standing;
Self::check(partial, selected)
}
fn merge_into<'t, 'v>(
partial: Self::Partial,
selected: usize,
standing: &mut Self,
) -> Result<(), crate::Error<'t, 'v>> {
if let Some(built) = Self::select(partial, selected)? {
*standing = built;
}
Ok(())
}
}
pub trait ViewSubcommands<O>: Subcommands {
fn select_for_view<'t, 'v>(
partial: Self::Partial,
selected: usize,
) -> Result<Option<Self>, crate::Error<'t, 'v>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn composed_relationship_selectors_count_flag_spellings() {
static FIRST: Flag = Flag {
longs: &["first", "alias"],
shorts: b"f",
negate: Some("no-first"),
..Flag::BOOL
};
static SECOND: Flag = Flag {
longs: &["second"],
..Flag::BOOL
};
static COMMAND: Command = Command {
flags: &[&FIRST, &SECOND],
..Command::EMPTY
};
assert_eq!(flag_selector_count(&COMMAND, "--first"), 1);
assert_eq!(flag_selector_count(&COMMAND, "--alias"), 1);
assert_eq!(flag_selector_count(&COMMAND, "--no-first"), 1);
assert_eq!(flag_selector_count(&COMMAND, "-f"), 1);
assert_eq!(flag_selector_count(&COMMAND, "first"), 0);
assert_eq!(flag_selector_count(&COMMAND, "--missing"), 0);
}
#[test]
fn quoting_escapes_what_would_break_a_document() {
assert_eq!(quoted("plain"), "plain");
assert_eq!(quoted("true"), r#""true""#);
assert_eq!(quoted("12"), r#""12""#);
assert_eq!(quoted("-12"), r#""-12""#);
assert_eq!(quoted(".5"), r#"".5""#);
assert_eq!(quoted("-.5"), r#""-.5""#);
assert_eq!(quoted("+.5"), r#""+.5""#);
assert_eq!(quoted("with space"), r#""with space""#);
assert_eq!(quoted(r#"say "hi""#), r#""say \"hi\"""#);
assert_eq!(quoted("a\\b"), r#""a\\b""#);
assert_eq!(quoted("one\ntwo"), r#""one\ntwo""#);
}
fn test_completer(_: &CompleteCtx<'_>) -> Vec<Candidate<'static>> {
Vec::new()
}
#[test]
fn generated_completer_bridges_preserve_the_argv_vector() {
static ARG: Arg = Arg {
name: "ARG",
..Arg::REQUIRED
};
static ARG_META: ArgMeta = ArgMeta {
arg: &ARG,
complete: Some(test_completer),
..ArgMeta::EMPTY
};
static COMMAND: Command = Command {
args: &[&ARG],
..Command::EMPTY
};
static META: CommandMeta = CommandMeta {
cmd: &COMMAND,
args: &[ARG_META],
..CommandMeta::EMPTY
};
static SPEC: Spec = Spec {
name: "ex",
bin: Some("ex"),
root: &META,
..Spec::EMPTY
};
let kdl = SPEC.to_kdl();
assert!(
kdl.contains(
"__complete_word__ --candidates arg --line={{ words | shell_join | shell_quote }}"
),
"{kdl}"
);
assert!(!kdl.contains("replace(from="), "{kdl}");
}
#[test]
fn choice_emission_preserves_visible_and_hidden_aliases() {
let mut out = String::new();
write_choices(
&mut out,
&["shown", "short"],
&["shown", "secret", "short", "secret-short"],
&[("shown", "short"), ("shown", "secret-short")],
&[],
(false, false),
0,
)
.unwrap();
assert_eq!(
out,
"choices {\n choice shown {\n alias short\n alias secret-short hide=#true\n }\n choice secret hide=#true\n}\n"
);
out.clear();
write_choices(&mut out, &[], &["secret"], &[], &[], (false, false), 0).unwrap();
assert_eq!(
out, "choices {\n choice secret hide=#true\n}\n",
"an entirely hidden set must still be emitted"
);
out.clear();
write_choices(
&mut out,
&["plain", "shown", "short"],
&["plain", "shown", "short", "secret"],
&[("shown", "short")],
&[
ChoiceMeta {
value: "shown",
help: Some("Shown value"),
hide: false,
aliases: &[ChoiceAliasMeta {
value: "short",
hide: false,
}],
},
ChoiceMeta {
value: "secret",
help: None,
hide: true,
aliases: &[],
},
],
(false, false),
0,
)
.unwrap();
assert_eq!(
out,
"choices {\n choice plain\n choice shown help=\"Shown value\" {\n alias short\n }\n choice secret hide=#true\n}\n"
);
}
#[test]
fn an_argument_no_word_can_reach_is_caught_where_the_table_is_joined() {
static FILES: Arg = Arg {
name: "files",
..Arg::VAR
};
static AFTER: Arg = Arg {
name: "after",
..Arg::REQUIRED
};
static BROKEN: Command = Command {
name: "ex",
args: &[&FILES, &AFTER],
..Command::EMPTY
};
assert_eq!(unfillable_arg(&BROKEN), Some("after"));
static BOUNDED: Arg = Arg {
name: "files",
var_max: Some(2),
..Arg::VAR
};
static WITH_BOUND: Command = Command {
name: "ex",
args: &[&BOUNDED, &AFTER],
..Command::EMPTY
};
assert_eq!(unfillable_arg(&WITH_BOUND), None);
static PAST_SEPARATOR: Arg = Arg {
name: "args_last",
double_dash: DoubleDash::Required,
..Arg::VAR
};
static WITH_SEPARATOR: Command = Command {
name: "ex",
args: &[&FILES, &PAST_SEPARATOR],
..Command::EMPTY
};
assert_eq!(unfillable_arg(&WITH_SEPARATOR), None);
static AFTER_THE_SEPARATOR: Command = Command {
name: "ex",
args: &[&PAST_SEPARATOR, &AFTER],
..Command::EMPTY
};
assert_eq!(unfillable_arg(&AFTER_THE_SEPARATOR), Some("after"));
static KEEPS_SEPARATOR: Arg = Arg {
name: "kept",
double_dash: DoubleDash::Preserve,
..Arg::VAR
};
static SEPARATOR_KEPT: Command = Command {
name: "ex",
args: &[&KEEPS_SEPARATOR, &PAST_SEPARATOR],
..Command::EMPTY
};
assert_eq!(unfillable_arg(&SEPARATOR_KEPT), Some("args_last"));
static KEEPS_SEPARATOR_BOUNDED: Arg = Arg {
name: "kept",
double_dash: DoubleDash::Preserve,
var_max: Some(2),
..Arg::VAR
};
static BOUNDED_KEEPER: Command = Command {
name: "ex",
args: &[&KEEPS_SEPARATOR_BOUNDED, &PAST_SEPARATOR],
..Command::EMPTY
};
assert_eq!(unfillable_arg(&BOUNDED_KEEPER), None);
static NESTED: Command = Command {
name: "outer",
subcommands: &[&BROKEN],
..Command::EMPTY
};
assert_eq!(unfillable_arg(&NESTED), Some("after"));
}
#[test]
fn a_subcommand_writes_unknown_flags_only_where_it_differs() {
static STRICT_SUB: Command = Command {
name: "build",
unknown_flags: Some(UnknownFlags::Error),
..Command::EMPTY
};
static SILENT_SUB: Command = Command {
name: "test",
..Command::EMPTY
};
static LENIENT_SUB: Command = Command {
name: "exec",
unknown_flags: Some(UnknownFlags::Value),
..Command::EMPTY
};
static ROOT: Command = Command {
name: "ex",
subcommands: &[&STRICT_SUB, &SILENT_SUB, &LENIENT_SUB],
unknown_flags: Some(UnknownFlags::Error),
..Command::EMPTY
};
static STRICT_META: CommandMeta = CommandMeta {
cmd: &STRICT_SUB,
..CommandMeta::EMPTY
};
static SILENT_META: CommandMeta = CommandMeta {
cmd: &SILENT_SUB,
..CommandMeta::EMPTY
};
static LENIENT_META: CommandMeta = CommandMeta {
cmd: &LENIENT_SUB,
..CommandMeta::EMPTY
};
static ROOT_META: CommandMeta = CommandMeta {
cmd: &ROOT,
subcommands: &[&STRICT_META, &SILENT_META, &LENIENT_META],
..CommandMeta::EMPTY
};
let mut out = String::new();
let w = Writing {
bin: "ex",
overlays: &[],
sets: Flagsets::collect(&ROOT_META),
};
write_body(
&mut out,
&ROOT_META,
0,
UnknownFlags::Value,
&w,
&mut Vec::new(),
)
.unwrap();
let line = |name: &str| -> String {
out.lines()
.map(str::trim)
.find(|l| l.starts_with(&format!("cmd {name}")))
.unwrap_or_else(|| panic!("no `{name}` command was written:\n{out}"))
.to_string()
};
let build = line("build");
assert!(
!build.contains("unknown_flags"),
"a subcommand matching the enclosing command should not repeat it: {build}"
);
let test = line("test");
assert!(
!test.contains("unknown_flags"),
"a subcommand that declares nothing inherits, and writes nothing: {test}"
);
let exec = line("exec");
assert_eq!(
exec.matches("unknown_flags=value").count(),
1,
"a differing subcommand declares it exactly once: {exec}"
);
}
#[test]
fn a_spec_view_applies_identity_and_sparse_effects_without_mutating_the_base() {
static RM: Command = Command {
name: "rm",
key: 12,
..Command::EMPTY
};
static DIST_TAG: Command = Command {
name: "dist-tag",
subcommands: &[&RM],
..Command::EMPTY
};
static LIST: Command = Command {
name: "list",
key: 13,
..Command::EMPTY
};
static ROOT: Command = Command {
name: "ex",
subcommands: &[&DIST_TAG, &LIST],
..Command::EMPTY
};
static RM_META: CommandMeta = CommandMeta {
cmd: &RM,
..CommandMeta::EMPTY
};
static DIST_TAG_META: CommandMeta = CommandMeta {
cmd: &DIST_TAG,
subcommands: &[&RM_META],
..CommandMeta::EMPTY
};
static LIST_META: CommandMeta = CommandMeta {
cmd: &LIST,
..CommandMeta::EMPTY
};
static ROOT_META: CommandMeta = CommandMeta {
cmd: &ROOT,
subcommands: &[&DIST_TAG_META, &LIST_META],
..CommandMeta::EMPTY
};
static SPEC: Spec = Spec {
name: "ex",
bin: Some("ex"),
version: Some("1.0.0"),
long_version: Some("1.0.0\ncommit old"),
root: &ROOT_META,
..Spec::EMPTY
};
static OVERLAY: [CommandOverlay<'static>; 2] = [
CommandOverlay::effect("dist-tag rm", Effect::Destructive),
CommandOverlay::effect_for(13, Effect::Read),
];
let view = SPEC
.view()
.name("embedded")
.bin("embedded")
.version("2.0.0")
.overlay(&OVERLAY);
let effective = view.spec();
assert_eq!(effective.name, "embedded");
assert_eq!(effective.bin, Some("embedded"));
assert_eq!(effective.version, Some("2.0.0"));
assert_eq!(effective.long_version, None);
let kdl = view.to_kdl();
assert!(kdl.contains("name embedded"), "{kdl}");
assert!(kdl.contains("cmd rm effect=destructive"), "{kdl}");
assert!(kdl.contains("cmd list effect=read"), "{kdl}");
let base = SPEC.to_kdl();
assert!(base.contains("name ex"), "{base}");
assert!(base.contains("version \"1.0.0\""), "{base}");
assert!(
base.contains("long_version \"1.0.0\\ncommit old\""),
"{base}"
);
assert!(!base.contains("effect="), "{base}");
let without_version = SPEC.view().version("2.0.0").omit_version();
assert_eq!(without_version.spec().version, None);
assert!(!without_version.to_kdl().contains("version "));
assert_eq!(
SPEC.view().omit_version().version("3.0.0").spec().version,
Some("3.0.0")
);
let runtime_name = String::from("runtime");
let runtime_path = String::from("list");
let runtime_overlays = vec![CommandOverlay::effect(&runtime_path, Effect::Write)];
let runtime = SPEC
.view()
.name(&runtime_name)
.bin("embedded")
.version("2.0.0")
.overlay(&OVERLAY)
.overlay(&runtime_overlays)
.to_kdl();
assert!(runtime.contains("name runtime"), "{runtime}");
assert!(runtime.contains("cmd list effect=write"), "{runtime}");
assert!(runtime.contains("cmd rm effect=destructive"), "{runtime}");
}
#[test]
fn an_invalid_view_cannot_capture_the_host_program() {
static ROOT: Command = Command {
name: "host",
..Command::EMPTY
};
static ROOT_META: CommandMeta = CommandMeta {
cmd: &ROOT,
..CommandMeta::EMPTY
};
static VIEW: ViewMeta = ViewMeta {
id: "host",
name: "host view",
bin: "host.exe",
root: "run",
all_globals: false,
globals: &[],
};
static SPEC: Spec = Spec {
name: "host",
bin: Some("/usr/bin/host"),
views: &[VIEW],
root: &ROOT_META,
..Spec::EMPTY
};
assert!(view_for_program(&SPEC, std::ffi::OsStr::new("host")).is_none());
assert!(view_for_program(&SPEC, std::ffi::OsStr::new("/tmp/host.exe")).is_none());
}
#[test]
fn flag_forms_lists_shorts_then_longs() {
static F: Flag = Flag {
longs: &["jobs", "workers"],
shorts: b"jw",
..Flag::VALUE
};
assert_eq!(
flag_forms(&FlagMeta {
flag: &F,
hidden_shorts: b"w",
hidden_longs: &["workers"],
..FlagMeta::EMPTY
}),
"-j --jobs"
);
}
#[test]
fn placeholders_show_arity_and_optionality() {
static REQ: Arg = Arg {
name: "file",
..Arg::REQUIRED
};
static VAR: Arg = Arg {
name: "rest",
..Arg::VAR
};
let required = ArgMeta {
arg: &REQ,
..ArgMeta::EMPTY
};
let optional_var = ArgMeta {
arg: &VAR,
required: false,
..ArgMeta::EMPTY
};
assert_eq!(arg_placeholder("file", &required), "<file>");
assert_eq!(arg_placeholder("rest", &optional_var), "[rest]...");
assert_eq!(placeholder("n", false, false), "<n>");
assert_eq!(placeholder("pattern", true, false), "<pattern>...");
assert_eq!(placeholder("BUMP", false, true), "[BUMP]");
}
}
#[test]
#[should_panic(expected = "two flattened groups on one command have the same name")]
fn concatenating_group_metadata_rejects_duplicate_names() {
static LEFT: [GroupMeta; 1] = [GroupMeta {
name: "input",
members: &["--file", "--url"],
required: false,
multiple: false,
}];
static RIGHT: [GroupMeta; 1] = [GroupMeta {
name: "input",
members: &["--json", "--yaml"],
required: false,
multiple: false,
}];
let _ = concat_group_metas::<2>(&[&LEFT, &RIGHT]);
}