use std::collections::HashMap;
use std::sync::OnceLock;
use crate::error::UsageErr;
use crate::sh::sh;
use crate::spec::builder::SpecCommandBuilder;
use crate::spec::context::ParsingContext;
use crate::spec::effect::{SpecCommandEffect, EFFECT_VALUES};
use crate::spec::flagset::SpecUse;
use crate::spec::group::SpecGroup;
use crate::spec::helpers::{string_entry, NodeHelper};
use crate::spec::is_false;
use crate::spec::mount::SpecMount;
use crate::spec::unknown_flags::UnknownFlags;
use crate::{Spec, SpecArg, SpecComplete, SpecFlag};
use indexmap::IndexMap;
use itertools::Itertools;
use kdl::{KdlDocument, KdlEntry, KdlNode};
use serde::Serialize;
#[derive(Debug, Serialize, Clone)]
pub struct SpecCommand {
pub full_cmd: Vec<String>,
pub usage: String,
pub subcommands: IndexMap<String, SpecCommand>,
pub args: Vec<SpecArg>,
pub flags: Vec<SpecFlag>,
#[serde(skip)]
pub(crate) uses: Vec<SpecUse>,
pub mounts: Vec<SpecMount>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub groups: Vec<SpecGroup>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated_warn_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated_remove_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub effect: Option<SpecCommandEffect>,
pub unknown_flags: Option<UnknownFlags>,
pub hide: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub help_heading: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_order: Option<usize>,
#[serde(skip)]
pub mounted: bool,
#[serde(skip)]
pub flags_from_mount: bool,
#[serde(skip_serializing_if = "is_false")]
pub subcommand_required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub subcommand_help_heading: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subcommand_value_name: Option<String>,
#[serde(skip_serializing_if = "is_false")]
pub next_line_help: bool,
#[serde(skip_serializing_if = "is_false")]
pub flatten_help: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub term_width: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_term_width: Option<usize>,
#[serde(skip_serializing_if = "is_false")]
pub external_subcommand: bool,
#[serde(skip_serializing_if = "is_false")]
pub arg_required_else_help: bool,
#[serde(skip_serializing_if = "is_false")]
pub disable_help_flag: bool,
#[serde(skip_serializing_if = "is_false")]
pub disable_help_subcommand: bool,
#[serde(skip_serializing_if = "is_false")]
pub disable_version_flag: bool,
#[serde(skip_serializing_if = "is_false")]
pub dont_delimit_trailing_values: bool,
pub args_override_self: bool,
#[serde(skip_serializing_if = "is_false")]
pub subcommand_negates_reqs: bool,
#[serde(skip_serializing_if = "is_false")]
pub args_conflicts_with_subcommands: bool,
#[serde(skip_serializing_if = "is_false")]
pub subcommand_precedence_over_arg: bool,
#[serde(skip_serializing_if = "is_false")]
pub allow_missing_positional: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub restart_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub help_long: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub help_md: Option<String>,
pub name: String,
pub aliases: Vec<String>,
pub hidden_aliases: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before_help: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before_help_long: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before_help_md: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after_help: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after_help_long: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after_help_md: Option<String>,
pub examples: Vec<SpecExample>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
pub complete: IndexMap<String, SpecComplete>,
#[serde(skip)]
pub(crate) subcommand_lookup: OnceLock<HashMap<String, String>>,
}
impl Default for SpecCommand {
fn default() -> Self {
Self {
full_cmd: vec![],
usage: "".to_string(),
subcommands: IndexMap::new(),
args: vec![],
flags: vec![],
uses: vec![],
mounts: vec![],
groups: vec![],
deprecated: None,
deprecated_warn_at: None,
deprecated_remove_at: None,
effect: None,
unknown_flags: None,
hide: false,
help_heading: None,
display_order: None,
mounted: false,
flags_from_mount: false,
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,
external_subcommand: false,
arg_required_else_help: false,
disable_help_flag: false,
disable_help_subcommand: false,
disable_version_flag: false,
dont_delimit_trailing_values: false,
args_override_self: true,
subcommand_negates_reqs: false,
args_conflicts_with_subcommands: false,
subcommand_precedence_over_arg: false,
allow_missing_positional: false,
restart_token: None,
help: None,
help_long: None,
help_md: None,
name: "".to_string(),
aliases: vec![],
hidden_aliases: vec![],
before_help: None,
before_help_long: None,
before_help_md: None,
after_help: None,
after_help_long: None,
after_help_md: None,
examples: vec![],
subcommand_lookup: OnceLock::new(),
complete: IndexMap::new(),
}
}
}
#[derive(Debug, Default, Serialize, Clone)]
#[non_exhaustive]
pub struct SpecExample {
pub code: String,
pub header: Option<String>,
pub help: Option<String>,
pub lang: String,
}
impl SpecExample {
pub fn new(code: impl Into<String>) -> Self {
Self {
code: code.into(),
..Default::default()
}
}
pub fn header(mut self, header: impl Into<String>) -> Self {
self.header = Some(header.into());
self
}
pub fn help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
pub fn lang(mut self, lang: impl Into<String>) -> Self {
self.lang = lang.into();
self
}
}
impl From<&SpecExample> for KdlNode {
fn from(example: &SpecExample) -> KdlNode {
let mut node = KdlNode::new("example");
node.push(string_entry(None, &example.code));
if let Some(header) = &example.header {
node.push(string_entry(Some("header"), header));
}
if let Some(help) = &example.help {
node.push(string_entry(Some("help"), help));
}
if !example.lang.is_empty() {
node.push(string_entry(Some("lang"), &example.lang));
}
node
}
}
impl SpecCommand {
pub fn builder() -> SpecCommandBuilder {
SpecCommandBuilder::new()
}
pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
node.ensure_arg_len(1..=1)?;
let mut cmd = Self {
name: node.arg(0)?.ensure_string()?.to_string(),
..Default::default()
};
for (k, v) in node.props() {
match k {
"help" => cmd.help = Some(v.ensure_string()?),
"long_help" => cmd.help_long = Some(v.ensure_string()?),
"help_long" => cmd.help_long = Some(v.ensure_string()?),
"help_md" => cmd.help_md = Some(v.ensure_string()?),
"before_help" => cmd.before_help = Some(v.ensure_string()?),
"before_long_help" => cmd.before_help_long = Some(v.ensure_string()?),
"before_help_long" => cmd.before_help_long = Some(v.ensure_string()?),
"before_help_md" => cmd.before_help_md = Some(v.ensure_string()?),
"after_help" => cmd.after_help = Some(v.ensure_string()?),
"after_long_help" => {
cmd.after_help_long = Some(v.ensure_string()?);
}
"after_help_long" => {
cmd.after_help_long = Some(v.ensure_string()?);
}
"after_help_md" => cmd.after_help_md = Some(v.ensure_string()?),
"subcommand_required" => cmd.subcommand_required = v.ensure_bool()?,
"subcommand_help_heading" => cmd.subcommand_help_heading = Some(v.ensure_string()?),
"subcommand_value_name" => cmd.subcommand_value_name = Some(v.ensure_string()?),
"next_line_help" => cmd.next_line_help = v.ensure_bool()?,
"flatten_help" => cmd.flatten_help = v.ensure_bool()?,
"term_width" => cmd.term_width = Some(v.ensure_usize()?),
"max_term_width" => cmd.max_term_width = Some(v.ensure_usize()?),
"external_subcommand" => cmd.external_subcommand = v.ensure_bool()?,
"arg_required_else_help" => cmd.arg_required_else_help = v.ensure_bool()?,
"disable_help_flag" => cmd.disable_help_flag = v.ensure_bool()?,
"disable_help_subcommand" => cmd.disable_help_subcommand = v.ensure_bool()?,
"disable_version_flag" => cmd.disable_version_flag = v.ensure_bool()?,
"dont_delimit_trailing_values" => {
cmd.dont_delimit_trailing_values = v.ensure_bool()?
}
"args_override_self" => cmd.args_override_self = v.ensure_bool()?,
"subcommand_negates_reqs" => cmd.subcommand_negates_reqs = v.ensure_bool()?,
"args_conflicts_with_subcommands" => {
cmd.args_conflicts_with_subcommands = v.ensure_bool()?
}
"subcommand_precedence_over_arg" => {
cmd.subcommand_precedence_over_arg = v.ensure_bool()?
}
"allow_missing_positional" => cmd.allow_missing_positional = v.ensure_bool()?,
"hide" => cmd.hide = v.ensure_bool()?,
"help_heading" => cmd.help_heading = Some(v.ensure_string()?),
"display_order" => cmd.display_order = Some(v.ensure_usize()?),
"unknown_flags" => {
let raw = v.ensure_string()?;
match raw.parse() {
Ok(mode) => cmd.unknown_flags = Some(mode),
Err(_) => bail_parse!(
ctx,
v.entry.span(),
"unsupported unknown_flags {raw}, expected one of: {}",
crate::spec::unknown_flags::UNKNOWN_FLAGS_VALUES
),
}
}
"effect" => {
let raw = v.ensure_string()?;
match raw.parse() {
Ok(effect) => cmd.effect = Some(effect),
Err(_) => bail_parse!(
ctx,
v.entry.span(),
"unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
),
}
}
"restart_token" => cmd.restart_token = Some(v.ensure_string()?),
"deprecated" => {
cmd.deprecated = match v.value.as_bool() {
Some(true) => Some("deprecated".to_string()),
Some(false) => None,
None => Some(v.ensure_string()?),
}
}
"deprecated_warn_at" => cmd.deprecated_warn_at = Some(v.ensure_string()?),
"deprecated_remove_at" => cmd.deprecated_remove_at = Some(v.ensure_string()?),
k => bail_parse!(ctx, v.entry.span(), "unsupported cmd prop {k}"),
}
}
for child in node.children() {
match child.name() {
"flag" => cmd.flags.push(SpecFlag::parse(ctx, &child)?),
"use" => {
let at = cmd.flags.len();
cmd.uses.push(SpecUse::parse(ctx, &child, at)?);
}
"arg" => {
let arg = SpecArg::parse(ctx, &child)?;
if arg.delimiter.is_some() && !arg.var {
bail_parse!(
ctx,
child.node.name().span(),
"argument <{}> has a delimiter and holds one value; add \
`var=#true` for the values it splits into",
arg.name
);
}
cmd.args.push(arg);
}
"mount" => cmd.mounts.push(SpecMount::parse(ctx, &child)?),
"group" => cmd.groups.push(SpecGroup::parse(ctx, &child)?),
"cmd" => {
let node = SpecCommand::parse(ctx, &child)?;
cmd.subcommands.insert(node.name.to_string(), node);
}
"alias" => {
let alias = child
.ensure_arg_len(1..)?
.args()
.map(|e| e.ensure_string())
.collect::<Result<Vec<_>, _>>()?;
let hide = child
.get("hide")
.map(|n| n.ensure_bool())
.unwrap_or(Ok(false))?;
if hide {
cmd.hidden_aliases.extend(alias);
} else {
cmd.aliases.extend(alias);
}
}
"example" => {
let code = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?;
let mut example = SpecExample::new(code.trim().to_string());
for (k, v) in child.props() {
match k {
"header" => example.header = Some(v.ensure_string()?),
"help" => example.help = Some(v.ensure_string()?),
"lang" => example.lang = v.ensure_string()?,
k => bail_parse!(ctx, v.entry.span(), "unsupported example key {k}"),
}
}
cmd.examples.push(example);
}
"help" => {
cmd.help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
}
"long_help" => {
cmd.help_long = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
}
"help_md" => {
cmd.help_md = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
}
"before_help" => {
cmd.before_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
}
"before_long_help" => {
cmd.before_help_long =
Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
}
"before_help_md" => {
cmd.before_help_md =
Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
}
"after_help" => {
cmd.after_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
}
"after_long_help" => {
cmd.after_help_long =
Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
}
"after_help_md" => {
cmd.after_help_md = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
}
"subcommand_required" => {
cmd.subcommand_required = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"help_heading" => {
cmd.help_heading = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
}
"subcommand_help_heading" => {
cmd.subcommand_help_heading =
Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
}
"subcommand_value_name" => {
cmd.subcommand_value_name =
Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
}
"next_line_help" => {
cmd.next_line_help = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"flatten_help" => {
cmd.flatten_help = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"term_width" => {
cmd.term_width = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_usize()?)
}
"max_term_width" => {
cmd.max_term_width = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_usize()?)
}
"external_subcommand" => {
cmd.external_subcommand = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"arg_required_else_help" => {
cmd.arg_required_else_help =
child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"disable_help_flag" => {
cmd.disable_help_flag = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"disable_help_subcommand" => {
cmd.disable_help_subcommand =
child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"disable_version_flag" => {
cmd.disable_version_flag = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"dont_delimit_trailing_values" => {
cmd.dont_delimit_trailing_values =
child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"args_override_self" => {
cmd.args_override_self = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"subcommand_negates_reqs" => {
cmd.subcommand_negates_reqs =
child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"args_conflicts_with_subcommands" => {
cmd.args_conflicts_with_subcommands =
child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"subcommand_precedence_over_arg" => {
cmd.subcommand_precedence_over_arg =
child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"allow_missing_positional" => {
cmd.allow_missing_positional =
child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"hide" => cmd.hide = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?,
"effect" => {
let arg = child.ensure_arg_len(1..=1)?.arg(0)?;
let raw = arg.ensure_string()?;
match raw.parse() {
Ok(effect) => cmd.effect = Some(effect),
Err(_) => bail_parse!(
ctx,
arg.entry.span(),
"unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
),
}
}
"restart_token" => {
cmd.restart_token = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
}
"deprecated" => {
cmd.deprecated = match child.arg(0)?.value.as_bool() {
Some(true) => Some("deprecated".to_string()),
Some(false) => None,
None => Some(child.arg(0)?.ensure_string()?),
}
}
"deprecated_warn_at" => {
cmd.deprecated_warn_at = Some(child.arg(0)?.ensure_string()?)
}
"deprecated_remove_at" => {
cmd.deprecated_remove_at = Some(child.arg(0)?.ensure_string()?)
}
"complete" => {
let complete = SpecComplete::parse(ctx, &child)?;
cmd.complete.insert(complete.name.clone(), complete);
}
k => bail_parse!(ctx, child.node.name().span(), "unsupported cmd key {k}"),
}
}
Ok(cmd)
}
pub(crate) fn is_empty(&self) -> bool {
self.args.is_empty()
&& self.flags.is_empty()
&& self.mounts.is_empty()
&& self.subcommands.is_empty()
}
pub fn usage(&self) -> String {
self.usage_with_subcommands(true)
}
#[cfg(feature = "docs")]
pub(crate) fn usage_without_subcommands(&self) -> String {
self.usage_with_subcommands(false)
}
fn usage_with_subcommands(&self, include_subcommands: bool) -> String {
let mut usage = self.full_cmd.join(" ");
let flags = self.flags.iter().filter(|f| !f.hide).collect_vec();
let args = self.args.iter().filter(|a| !a.hide).collect_vec();
if !flags.is_empty() {
if flags.len() <= 2 {
let inlines = flags
.iter()
.map(|f| {
if f.required {
format!("<{}>", f.usage())
} else {
format!("[{}]", f.usage())
}
})
.join(" ");
usage = format!("{usage} {inlines}").trim().to_string();
} else if flags.iter().any(|f| f.required) {
usage = format!("{usage} <FLAGS>");
} else {
usage = format!("{usage} [FLAGS]");
}
}
if !args.is_empty() {
if args.len() <= 2 {
let inlines = args.iter().map(|a| a.usage()).join(" ");
usage = format!("{usage} {inlines}").trim().to_string();
} else if args.iter().any(|a| a.required) {
usage = format!("{usage} <ARGS>…");
} else {
usage = format!("{usage} [ARGS]…");
}
}
if include_subcommands && !self.subcommands.is_empty() {
let name = self
.subcommand_value_name
.as_deref()
.unwrap_or("SUBCOMMAND");
usage = format!("{usage} <{name}>");
}
usage.trim().to_string()
}
pub(crate) fn merge(&mut self, other: Self) {
self.subcommand_lookup = OnceLock::new();
let Self {
name,
help,
help_long,
help_md,
before_help,
before_help_long,
before_help_md,
after_help,
after_help_long,
after_help_md,
args,
flags,
uses,
mounts,
groups,
aliases,
hidden_aliases,
examples,
hide,
help_heading,
display_order,
subcommand_required,
subcommand_help_heading,
subcommand_value_name,
next_line_help,
flatten_help,
term_width,
max_term_width,
external_subcommand,
arg_required_else_help,
disable_help_flag,
disable_help_subcommand,
disable_version_flag,
dont_delimit_trailing_values,
args_override_self,
subcommand_negates_reqs,
args_conflicts_with_subcommands,
subcommand_precedence_over_arg,
allow_missing_positional,
restart_token,
subcommands,
complete,
deprecated,
deprecated_warn_at,
deprecated_remove_at,
effect,
unknown_flags,
full_cmd: _,
usage: _,
mounted: _,
flags_from_mount: _,
subcommand_lookup: _,
} = other;
if !name.is_empty() {
self.name = name;
}
if help.is_some() {
self.help = help;
}
if help_long.is_some() {
self.help_long = help_long;
}
if help_md.is_some() {
self.help_md = help_md;
}
if before_help.is_some() {
self.before_help = before_help;
}
if before_help_long.is_some() {
self.before_help_long = before_help_long;
}
if before_help_md.is_some() {
self.before_help_md = before_help_md;
}
if after_help.is_some() {
self.after_help = after_help;
}
if after_help_long.is_some() {
self.after_help_long = after_help_long;
}
if after_help_md.is_some() {
self.after_help_md = after_help_md;
}
if !args.is_empty() {
self.args = args;
}
let flags_replaced = !flags.is_empty();
if flags_replaced {
self.flags = flags;
}
if flags_replaced || !uses.is_empty() {
self.uses = uses;
}
if !mounts.is_empty() {
self.mounts = mounts;
}
if flags_replaced || !groups.is_empty() {
self.groups = groups;
}
if !aliases.is_empty() {
self.aliases = aliases;
}
if !hidden_aliases.is_empty() {
self.hidden_aliases = hidden_aliases;
}
if !examples.is_empty() {
self.examples = examples;
}
self.hide = hide;
if help_heading.is_some() {
self.help_heading = help_heading;
}
if display_order.is_some() {
self.display_order = display_order;
}
self.subcommand_required = subcommand_required;
if subcommand_help_heading.is_some() {
self.subcommand_help_heading = subcommand_help_heading;
}
if subcommand_value_name.is_some() {
self.subcommand_value_name = subcommand_value_name;
}
self.next_line_help = next_line_help;
self.flatten_help = flatten_help;
if term_width.is_some() {
self.term_width = term_width;
}
if max_term_width.is_some() {
self.max_term_width = max_term_width;
}
self.external_subcommand = external_subcommand;
self.arg_required_else_help = arg_required_else_help;
self.disable_help_flag = disable_help_flag;
self.disable_help_subcommand = disable_help_subcommand;
self.disable_version_flag = disable_version_flag;
self.dont_delimit_trailing_values = dont_delimit_trailing_values;
self.args_override_self = args_override_self;
self.subcommand_negates_reqs = subcommand_negates_reqs;
self.args_conflicts_with_subcommands = args_conflicts_with_subcommands;
self.subcommand_precedence_over_arg = subcommand_precedence_over_arg;
self.allow_missing_positional = allow_missing_positional;
if effect.is_some() {
self.effect = effect;
}
if unknown_flags.is_some() {
self.unknown_flags = unknown_flags;
}
if deprecated.is_some() {
self.deprecated = deprecated;
}
if deprecated_warn_at.is_some() {
self.deprecated_warn_at = deprecated_warn_at;
}
if deprecated_remove_at.is_some() {
self.deprecated_remove_at = deprecated_remove_at;
}
if restart_token.is_some() {
self.restart_token = restart_token;
}
for (name, cmd) in subcommands {
self.subcommands.insert(name, cmd);
}
for (name, complete) in complete {
self.complete.insert(name, complete);
}
}
pub fn all_subcommands(&self) -> Vec<&SpecCommand> {
let mut cmds = vec![];
for cmd in self.subcommands.values() {
cmds.push(cmd);
cmds.extend(cmd.all_subcommands());
}
cmds
}
pub fn find_subcommand(&self, name: &str) -> Option<&SpecCommand> {
let sl = self.subcommand_lookup.get_or_init(|| {
let mut map = HashMap::new();
for name in self.subcommands.keys() {
map.insert(name.clone(), name.clone());
}
for (name, cmd) in &self.subcommands {
for alias in cmd.aliases.iter().chain(&cmd.hidden_aliases) {
map.entry(alias.clone()).or_insert_with(|| name.clone());
}
}
map
});
let name = sl.get(name)?;
self.subcommands.get(name)
}
pub(crate) fn mount(
&mut self,
global_flag_args: &[String],
injected: Option<&HashMap<String, String>>,
) -> Result<(), UsageErr> {
for mount in self.mounts.iter().cloned().collect_vec() {
let cmd = if global_flag_args.is_empty() {
mount.run.clone()
} else {
let mut tokens = shell_words::split(&mount.run)
.expect("mount command should be valid shell syntax");
if !tokens.is_empty() {
tokens.splice(1..1, global_flag_args.iter().cloned());
}
shell_words::join(tokens)
};
let output = match injected {
Some(outputs) => outputs
.get(&mount.run)
.cloned()
.ok_or_else(|| UsageErr::MissingMountOutput(mount.run.clone()))?,
None => sh(&cmd)?,
};
let mut spec: Spec = output.parse()?;
if let Some(outputs) = injected {
spec.resolve_mount_outputs_at_root(outputs, false)?;
}
for cmd in spec.cmd.subcommands.values_mut() {
cmd.mark_mounted();
}
self.flags_from_mount |= !spec.cmd.flags.is_empty();
self.merge(spec.cmd);
}
Ok(())
}
pub(crate) fn mark_mounted(&mut self) {
self.mounted = true;
for cmd in self.subcommands.values_mut() {
cmd.mark_mounted();
}
}
}
impl From<&SpecCommand> for KdlNode {
fn from(cmd: &SpecCommand) -> Self {
let SpecCommand {
name,
hide,
help_heading,
display_order,
subcommand_required,
subcommand_help_heading,
subcommand_value_name,
next_line_help,
flatten_help,
term_width,
max_term_width,
external_subcommand,
arg_required_else_help,
disable_help_flag,
disable_help_subcommand,
disable_version_flag,
dont_delimit_trailing_values,
args_override_self,
subcommand_negates_reqs,
args_conflicts_with_subcommands,
subcommand_precedence_over_arg,
allow_missing_positional,
restart_token,
unknown_flags,
aliases,
hidden_aliases,
help,
help_long,
help_md,
before_help,
before_help_long,
before_help_md,
after_help,
after_help_long,
after_help_md,
deprecated,
deprecated_warn_at,
deprecated_remove_at,
effect,
flags,
args,
mounts,
groups,
subcommands,
complete,
examples,
uses: _,
full_cmd: _,
usage: _,
mounted: _,
flags_from_mount: _,
subcommand_lookup: _,
} = cmd;
let mut node = Self::new("cmd");
node.entries_mut().push(name.clone().into());
if *hide {
node.entries_mut().push(KdlEntry::new_prop("hide", true));
}
if let Some(heading) = help_heading {
node.entries_mut()
.push(KdlEntry::new_prop("help_heading", heading.clone()));
}
if let Some(order) = display_order {
node.entries_mut()
.push(KdlEntry::new_prop("display_order", *order as i128));
}
if *subcommand_required {
node.entries_mut()
.push(KdlEntry::new_prop("subcommand_required", true));
}
if let Some(heading) = subcommand_help_heading {
node.push(KdlEntry::new_prop(
"subcommand_help_heading",
heading.clone(),
));
}
if let Some(name) = subcommand_value_name {
node.push(KdlEntry::new_prop("subcommand_value_name", name.clone()));
}
if *next_line_help {
node.push(KdlEntry::new_prop("next_line_help", true));
}
if *flatten_help {
node.push(KdlEntry::new_prop("flatten_help", true));
}
if let Some(width) = term_width {
node.push(KdlEntry::new_prop("term_width", *width as i128));
}
if let Some(width) = max_term_width {
node.push(KdlEntry::new_prop("max_term_width", *width as i128));
}
if *external_subcommand {
node.entries_mut()
.push(KdlEntry::new_prop("external_subcommand", true));
}
if *arg_required_else_help {
node.entries_mut()
.push(KdlEntry::new_prop("arg_required_else_help", true));
}
if *disable_help_flag {
node.push(KdlEntry::new_prop("disable_help_flag", true));
}
if *disable_help_subcommand {
node.push(KdlEntry::new_prop("disable_help_subcommand", true));
}
if *disable_version_flag {
node.push(KdlEntry::new_prop("disable_version_flag", true));
}
if *dont_delimit_trailing_values {
node.entries_mut()
.push(KdlEntry::new_prop("dont_delimit_trailing_values", true));
}
if !*args_override_self {
node.push(KdlEntry::new_prop("args_override_self", false));
}
if *subcommand_negates_reqs {
node.push(KdlEntry::new_prop("subcommand_negates_reqs", true));
}
if *args_conflicts_with_subcommands {
node.push(KdlEntry::new_prop("args_conflicts_with_subcommands", true));
}
if *subcommand_precedence_over_arg {
node.push(KdlEntry::new_prop("subcommand_precedence_over_arg", true));
}
if *allow_missing_positional {
node.push(KdlEntry::new_prop("allow_missing_positional", true));
}
if let Some(restart_token) = &restart_token {
node.entries_mut()
.push(KdlEntry::new_prop("restart_token", restart_token.clone()));
}
if !aliases.is_empty() {
let mut alias_node = KdlNode::new("alias");
for alias in aliases {
alias_node.entries_mut().push(alias.clone().into());
}
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
children.nodes_mut().push(alias_node);
}
if !hidden_aliases.is_empty() {
let mut alias_node = KdlNode::new("alias");
for alias in hidden_aliases {
alias_node.entries_mut().push(alias.clone().into());
}
alias_node
.entries_mut()
.push(KdlEntry::new_prop("hide", true));
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
children.nodes_mut().push(alias_node);
}
if let Some(help) = &help {
node.entries_mut().push(string_entry(Some("help"), help));
}
if let Some(help) = &help_long {
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
let mut node = KdlNode::new("long_help");
node.push(string_entry(None, help));
children.nodes_mut().push(node);
}
if let Some(help) = &help_md {
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
let mut node = KdlNode::new("help_md");
node.push(string_entry(None, help));
children.nodes_mut().push(node);
}
if let Some(help) = &before_help {
node.entries_mut()
.push(string_entry(Some("before_help"), help));
}
if let Some(help) = &before_help_long {
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
let mut node = KdlNode::new("before_long_help");
node.push(string_entry(None, help));
children.nodes_mut().push(node);
}
if let Some(help) = &before_help_md {
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
let mut node = KdlNode::new("before_help_md");
node.push(string_entry(None, help));
children.nodes_mut().push(node);
}
if let Some(help) = &after_help {
node.entries_mut()
.push(string_entry(Some("after_help"), help));
}
if let Some(help) = &after_help_long {
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
let mut node = KdlNode::new("after_long_help");
node.push(string_entry(None, help));
children.nodes_mut().push(node);
}
if let Some(help) = &after_help_md {
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
let mut node = KdlNode::new("after_help_md");
node.push(string_entry(None, help));
children.nodes_mut().push(node);
}
if let Some(deprecated) = &deprecated {
node.entries_mut()
.push(string_entry(Some("deprecated"), deprecated));
}
if let Some(at) = deprecated_warn_at {
node.push(string_entry(Some("deprecated_warn_at"), at));
}
if let Some(at) = deprecated_remove_at {
node.push(string_entry(Some("deprecated_remove_at"), at));
}
if let Some(effect) = effect {
node.entries_mut()
.push(string_entry(Some("effect"), effect.as_str()));
}
if let Some(unknown_flags) = unknown_flags {
node.entries_mut()
.push(string_entry(Some("unknown_flags"), unknown_flags.as_str()));
}
for flag in flags {
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
children.nodes_mut().push(flag.into());
}
for arg in args {
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
children.nodes_mut().push(arg.into());
}
for mount in mounts {
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
children.nodes_mut().push(mount.into());
}
for group in groups {
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
children.nodes_mut().push(group.into());
}
for cmd in subcommands.values() {
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
children.nodes_mut().push(cmd.into());
}
for example in examples {
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
children.nodes_mut().push(example.into());
}
for complete in complete.values() {
let children = node.children_mut().get_or_insert_with(KdlDocument::new);
children.nodes_mut().push(complete.into());
}
node
}
}
#[cfg(feature = "clap")]
impl From<&clap::Command> for SpecCommand {
fn from(cmd: &clap::Command) -> Self {
let mut spec = Self {
name: cmd.get_name().to_string(),
hide: cmd.is_hide_set(),
help: cmd.get_about().map(|s| s.to_string()),
help_long: cmd.get_long_about().map(|s| s.to_string()),
before_help: cmd.get_before_help().map(|s| s.to_string()),
before_help_long: cmd.get_before_long_help().map(|s| s.to_string()),
after_help: cmd.get_after_help().map(|s| s.to_string()),
after_help_long: cmd.get_after_long_help().map(|s| s.to_string()),
..Default::default()
};
spec.external_subcommand = cmd.is_allow_external_subcommands_set();
let forwards = cmd
.get_arguments()
.any(|arg| arg.is_allow_hyphen_values_set() || arg.is_trailing_var_arg_set());
spec.unknown_flags = Some(if forwards {
UnknownFlags::Value
} else {
UnknownFlags::Error
});
for alias in cmd.get_visible_aliases() {
spec.aliases.push(alias.to_string());
}
for alias in cmd.get_all_aliases() {
if spec.aliases.contains(&alias.to_string()) {
continue;
}
spec.hidden_aliases.push(alias.to_string());
}
for arg in cmd.get_arguments() {
let complete_type = crate::spec::arg::value_hint_type(arg.get_value_hint());
let conflicts: Vec<String> = cmd
.get_arg_conflicts_with(arg)
.iter()
.filter_map(|other| match (other.get_long(), other.get_short()) {
(Some(long), _) => Some(format!("--{long}")),
(None, Some(short)) => Some(format!("-{short}")),
(None, None) if other.is_positional() => Some(SpecArg::from(*other).name),
(None, None) => None,
})
.collect();
if arg.is_positional() {
let mut positional: SpecArg = arg.into();
positional.allow_negative_numbers |= cmd.is_allow_negative_numbers_set();
positional.conflicts = conflicts;
if let Some(type_) = complete_type {
let name = positional.name.to_lowercase();
spec.complete.insert(
name.clone(),
SpecComplete {
name,
type_: Some(type_.to_string()),
..Default::default()
},
);
}
spec.args.push(positional)
} else {
let mut flag: SpecFlag = arg.into();
if let Some(value) = &mut flag.arg {
value.allow_negative_numbers |= cmd.is_allow_negative_numbers_set();
}
flag.conflicts = conflicts;
if let (Some(type_), Some(value)) = (complete_type, flag.arg.as_ref()) {
let name = value.name.to_lowercase();
spec.complete.insert(
name.clone(),
SpecComplete {
name,
type_: Some(type_.to_string()),
..Default::default()
},
);
}
spec.flags.push(flag)
}
}
if spec
.args
.windows(2)
.all(|pair| pair[0].display_order <= pair[1].display_order)
{
for arg in &mut spec.args {
arg.display_order = None;
}
}
if spec
.flags
.windows(2)
.all(|pair| pair[0].display_order <= pair[1].display_order)
{
for flag in &mut spec.flags {
flag.display_order = None;
}
}
for group in cmd.get_groups() {
let members: Vec<String> = group
.get_args()
.filter_map(|id| cmd.get_arguments().find(|arg| arg.get_id() == id))
.filter_map(|arg| match (arg.get_long(), arg.get_short()) {
(Some(long), _) => Some(format!("--{long}")),
(None, Some(short)) => Some(format!("-{short}")),
(None, None) if arg.is_positional() => Some(SpecArg::from(arg).name),
(None, None) => None,
})
.collect();
if members.len() < 2 {
continue;
}
let required = group.is_required_set();
let multiple = group.clone().is_multiple();
if multiple && !required {
continue;
}
let mut spec_group = SpecGroup::new(group.get_id().as_str(), members);
spec_group.required = required;
spec_group.multiple = multiple;
spec.groups.push(spec_group);
}
spec.subcommand_required = cmd.is_subcommand_required_set();
spec.subcommand_help_heading = cmd.get_subcommand_help_heading().map(str::to_string);
spec.subcommand_value_name = cmd.get_subcommand_value_name().map(str::to_string);
spec.next_line_help = cmd.is_next_line_help_set();
spec.flatten_help = cmd.is_flatten_help_set();
spec.arg_required_else_help = cmd.is_arg_required_else_help_set();
spec.disable_help_flag = cmd.is_disable_help_flag_set();
spec.disable_help_subcommand =
cmd.get_subcommands().next().is_some() && cmd.is_disable_help_subcommand_set();
spec.disable_version_flag = (cmd.get_version().is_some()
|| cmd.get_long_version().is_some())
&& cmd.is_disable_version_flag_set();
spec.dont_delimit_trailing_values = cmd.is_dont_delimit_trailing_values_set();
spec.args_override_self = cmd.is_args_override_self();
spec.subcommand_negates_reqs = cmd.is_subcommand_negates_reqs_set();
spec.args_conflicts_with_subcommands = cmd.is_args_conflicts_with_subcommands_set();
spec.subcommand_precedence_over_arg = cmd.is_subcommand_precedence_over_arg_set();
spec.allow_missing_positional = cmd.is_allow_missing_positional_set();
for subcmd in cmd.get_subcommands() {
let mut scmd: SpecCommand = subcmd.into();
scmd.name = subcmd.get_name().to_string();
scmd.display_order = Some(subcmd.get_display_order());
spec.subcommands.insert(scmd.name.clone(), scmd);
}
if spec
.subcommands
.iter()
.all(|(_, subcommand)| subcommand.display_order == Some(999))
{
for (_, subcommand) in &mut spec.subcommands {
subcommand.display_order = None;
}
}
spec
}
}
#[cfg(feature = "clap")]
impl From<clap::Command> for Spec {
fn from(cmd: clap::Command) -> Self {
(&cmd).into()
}
}
#[cfg(test)]
mod tests {
use crate::spec::effect::SpecCommandEffect;
use crate::Spec;
use insta::assert_snapshot;
#[test]
fn test_effect_prop_and_child_node() {
let spec = Spec::parse(
&Default::default(),
r#"
bin "mise"
cmd "ls" effect="read"
cmd "use" effect="write"
cmd "uninstall" {
effect "destructive"
}
cmd "version"
"#,
)
.unwrap();
let cmds = &spec.cmd.subcommands;
assert_eq!(cmds["ls"].effect, Some(SpecCommandEffect::Read));
assert_eq!(cmds["use"].effect, Some(SpecCommandEffect::Write));
assert_eq!(
cmds["uninstall"].effect,
Some(SpecCommandEffect::Destructive)
);
assert_eq!(cmds["version"].effect, None);
}
#[test]
fn test_effect_is_not_inherited_by_subcommands() {
let spec = Spec::parse(
&Default::default(),
r#"
bin "git"
cmd "remote" effect="read" {
cmd "add" effect="write"
cmd "show"
}
"#,
)
.unwrap();
let remote = &spec.cmd.subcommands["remote"];
assert_eq!(remote.effect, Some(SpecCommandEffect::Read));
assert_eq!(
remote.subcommands["add"].effect,
Some(SpecCommandEffect::Write)
);
assert_eq!(remote.subcommands["show"].effect, None);
}
#[test]
fn test_effect_roundtrips_through_kdl() {
let spec = Spec::parse(
&Default::default(),
r#"
bin "mise"
cmd "ls" effect="read"
cmd "uninstall" effect="destructive"
"#,
)
.unwrap();
assert_snapshot!(spec, @r#"
name mise
bin mise
cmd ls effect=read
cmd uninstall effect=destructive
"#);
}
#[test]
fn test_effect_survives_merge() {
let cmd_with = |src: &str| {
Spec::parse(&Default::default(), src)
.unwrap()
.cmd
.subcommands["uninstall"]
.clone()
};
let declared = cmd_with(r#"cmd "uninstall" effect="destructive""#);
let silent = cmd_with(r#"cmd "uninstall" help="Remove a tool""#);
let contradicting = cmd_with(r#"cmd "uninstall" effect="write""#);
let mut cmd = declared.clone();
cmd.merge(silent);
assert_eq!(cmd.effect, Some(SpecCommandEffect::Destructive));
let mut cmd = declared;
cmd.merge(contradicting);
assert_eq!(cmd.effect, Some(SpecCommandEffect::Write));
}
#[test]
fn test_unknown_effect_is_an_error() {
let err = Spec::parse(
&Default::default(),
r#"
bin "mise"
cmd "ls" effect="readonly"
"#,
)
.unwrap_err();
assert!(
err.to_string().contains("Invalid usage config"),
"unexpected error: {err}"
);
}
}
#[cfg(test)]
mod merge_tests {
use crate::Spec;
fn uninstall(src: &str) -> crate::SpecCommand {
Spec::parse(&Default::default(), src)
.unwrap()
.cmd
.subcommands["uninstall"]
.clone()
}
#[test]
fn test_deprecated_survives_merge() {
let declared = uninstall(r#"cmd "uninstall" deprecated="use `remove`""#);
let silent = uninstall(r#"cmd "uninstall" help="Remove a tool""#);
let contradicting = uninstall(r#"cmd "uninstall" deprecated="gone in v3""#);
let mut cmd = declared.clone();
cmd.merge(silent);
assert_eq!(cmd.deprecated.as_deref(), Some("use `remove`"));
let mut cmd = declared;
cmd.merge(contradicting);
assert_eq!(cmd.deprecated.as_deref(), Some("gone in v3"));
}
}
#[cfg(test)]
mod roundtrip_tests {
use crate::Spec;
#[test]
fn test_spec_survives_a_kdl_roundtrip() {
let src = r#"
name "My CLI"
bin "mycli"
about "does things"
version "1.0.0"
author "nobody"
license "MIT"
flag "-v --verbose" help="Verbose logging" global=#true count=#true
arg "<dir>" help="Directory to use"
cmd "install" help="Install a package" subcommand_required=#false {
alias "i"
alias "add" hide=#true
long_help "The long help for install"
help_md "The **markdown** help for install"
before_help "before"
before_long_help "The long before-help for install"
before_help_md "The **markdown** before-help for install"
after_help "after"
after_long_help "The long after-help for install"
after_help_md "The **markdown** after-help for install"
arg "<pkg>" help="Package to install"
arg "[dest]" effect="write"
flag "-f --force" help="Overwrite"
flag "--purge" effect="destructive" overrides="-f" required_unless="--keep"
complete "pkg" run="mycli list --available" descriptions=#true
example "mycli install foo" header="Install foo" help="Installs foo" lang="sh"
example "mycli install bar"
cmd "from" help="Install from a source" {
arg "<src>"
}
}
cmd "wrapped" help="Wraps another CLI" {
mount run="mycli plugin usage-spec"
}
cmd "remove" help="Remove a package" deprecated="use `uninstall`" effect="destructive"
cmd "run" restart_token=":::" help="Run tasks"
cmd "exec" external_subcommand=#true help="Run an external command"
cmd "hidden" hide=#true
"#;
let original = Spec::parse(&Default::default(), src).unwrap();
let reparsed = Spec::parse(&Default::default(), &original.to_string()).unwrap();
let original = serde_json::to_value(&original).unwrap();
let reparsed = serde_json::to_value(&reparsed).unwrap();
pretty_assertions::assert_eq!(original, reparsed);
let install = &original["cmd"]["subcommands"]["install"];
let purge = install["flags"]
.as_array()
.unwrap()
.iter()
.find(|flag| flag["name"] == "purge")
.unwrap();
assert_eq!(purge["overrides"], serde_json::json!(["-f"]));
assert_eq!(purge["required_unless"], serde_json::json!(["--keep"]));
for key in [
"help_long",
"help_md",
"before_help",
"before_help_long",
"before_help_md",
"after_help",
"after_help_long",
"after_help_md",
"deprecated",
"effect",
"restart_token",
"examples",
"complete",
"mounts",
"aliases",
"hidden_aliases",
] {
let populated = match key {
"deprecated" | "effect" => {
original["cmd"]["subcommands"]["remove"].get(key).is_some()
}
"restart_token" => original["cmd"]["subcommands"]["run"].get(key).is_some(),
"mounts" => !original["cmd"]["subcommands"]["wrapped"]["mounts"]
.as_array()
.unwrap()
.is_empty(),
_ => match install.get(key) {
Some(serde_json::Value::Array(a)) => !a.is_empty(),
Some(serde_json::Value::Object(o)) => !o.is_empty(),
Some(_) => true,
None => false,
},
};
assert!(populated, "fixture does not exercise `{key}`");
}
assert!(
install["flags"]
.as_array()
.unwrap()
.iter()
.any(|f| f.get("effect").is_some()),
"fixture does not exercise a flag-level `effect`"
);
assert!(
install["args"]
.as_array()
.unwrap()
.iter()
.any(|a| a.get("effect").is_some()),
"fixture does not exercise an arg-level `effect`"
);
}
#[cfg(feature = "clap")]
#[test]
fn a_clap_command_says_what_clap_would_do_with_an_unknown_flag() {
use super::{SpecCommand, UnknownFlags};
let plain = clap::Command::new("build")
.arg(clap::Arg::new("target").required(false))
.arg(clap::Arg::new("force").long("force").num_args(0));
let spec: SpecCommand = (&plain).into();
assert_eq!(spec.unknown_flags, Some(UnknownFlags::Error));
let wrapper = clap::Command::new("run").arg(
clap::Arg::new("args")
.num_args(0..)
.allow_hyphen_values(true),
);
let spec: SpecCommand = (&wrapper).into();
assert_eq!(spec.unknown_flags, Some(UnknownFlags::Value));
let trailing = clap::Command::new("exec").arg(
clap::Arg::new("cmd")
.num_args(0..)
.trailing_var_arg(true)
.allow_hyphen_values(true),
);
let spec: SpecCommand = (&trailing).into();
assert_eq!(spec.unknown_flags, Some(UnknownFlags::Value));
let external = clap::Command::new("x").allow_external_subcommands(true);
let spec: SpecCommand = (&external).into();
assert_eq!(spec.unknown_flags, Some(UnknownFlags::Error));
assert!(spec.external_subcommand);
}
#[cfg(feature = "clap")]
#[test]
fn the_decision_survives_being_written_and_read_back() {
use super::SpecCommand;
let plain = clap::Command::new("build").arg(clap::Arg::new("target").required(false));
let spec: SpecCommand = (&plain).into();
let node: kdl::KdlNode = (&spec).into();
let kdl = node.to_string();
assert!(kdl.contains("unknown_flags=error"), "{kdl}");
}
#[cfg(feature = "clap")]
#[test]
fn the_clap_bridge_preserves_args_override_self() {
use super::SpecCommand;
let strict: SpecCommand = (&clap::Command::new("strict")).into();
assert!(!strict.args_override_self, "clap is strict by default");
let permissive: SpecCommand =
(&clap::Command::new("permissive").args_override_self(true)).into();
assert!(permissive.args_override_self);
let node: kdl::KdlNode = (&strict).into();
assert!(node.to_string().contains("args_override_self=#false"));
}
#[cfg(feature = "clap")]
#[test]
fn the_clap_bridge_preserves_subcommand_presentation() {
use super::SpecCommand;
let spec: SpecCommand = (&clap::Command::new("ex")
.subcommand(clap::Command::new("run"))
.subcommand_help_heading("Actions")
.subcommand_value_name("ACTION"))
.into();
assert_eq!(spec.subcommand_help_heading.as_deref(), Some("Actions"));
assert_eq!(spec.subcommand_value_name.as_deref(), Some("ACTION"));
let node: kdl::KdlNode = (&spec).into();
let kdl = node.to_string();
assert!(kdl.contains("subcommand_help_heading=Actions"), "{kdl}");
assert!(kdl.contains("subcommand_value_name=ACTION"), "{kdl}");
}
#[cfg(feature = "clap")]
#[test]
fn the_clap_bridge_preserves_subcommand_negates_requirements() {
use super::SpecCommand;
let spec: SpecCommand = (&clap::Command::new("ex").subcommand_negates_reqs(true)).into();
assert!(spec.subcommand_negates_reqs);
let node: kdl::KdlNode = (&spec).into();
assert!(node.to_string().contains("subcommand_negates_reqs=#true"));
}
#[cfg(feature = "clap")]
#[test]
fn the_clap_bridge_preserves_argument_subcommand_conflicts() {
use super::SpecCommand;
let spec: SpecCommand =
(&clap::Command::new("ex").args_conflicts_with_subcommands(true)).into();
assert!(spec.args_conflicts_with_subcommands);
let node: kdl::KdlNode = (&spec).into();
assert!(node
.to_string()
.contains("args_conflicts_with_subcommands=#true"));
}
#[cfg(feature = "clap")]
#[test]
fn the_clap_bridge_preserves_allow_missing_positional() {
use super::SpecCommand;
let spec: SpecCommand = (&clap::Command::new("ex").allow_missing_positional(true)).into();
assert!(spec.allow_missing_positional);
let node: kdl::KdlNode = (&spec).into();
assert!(node.to_string().contains("allow_missing_positional=#true"));
}
}