Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion derive/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3846,6 +3846,7 @@ pub fn emit_args(cli: &Cli) -> TokenStream {
.effect
.clone()
.unwrap_or_else(|| quote!(::core::option::Option::None));
let hide = cli.hide;
quote! {
#[doc(hidden)]
#[allow(
Expand Down Expand Up @@ -3903,6 +3904,7 @@ pub fn emit_args(cli: &Cli) -> TokenStream {
deprecated_warn_at: #deprecated_warn_at,
deprecated_remove_at: #deprecated_remove_at,
hidden_aliases: &[#(#hidden_aliases),*],
hide: #hide,
restart_token: #restart_token,
subcommand_required: #subcommand_required,
subcommand_help_heading: #subcommand_help_heading,
Expand Down Expand Up @@ -4378,7 +4380,7 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream {
before_long_help: #before_long_help,
after_help: #after_help,
after_long_help: #after_long_help,
hide: #hide,
hide: #hide || <#ty as usage_argv::spec::CommandArgs>::META.hide,
help_heading: #help_heading,
display_order: #display_order,
hidden_aliases: &#hidden_name,
Expand Down
2 changes: 2 additions & 0 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@
//!
//! On the struct itself: `bin`, `version`, `long_version`, `author`, `license`, `repository`, `about`,
//! `long_about`, `before_help`, `after_help`,
//! clap-compatible `visible_alias(es)`, hidden `alias(es)`, and `hide` may stay on an
//! `Args` struct and are inherited by every subcommand variant that mounts it —
//! `verbatim_doc_comment` — preserve doc-comment line breaks and whitespace —
//! `default_subcommand`, `multicall` — argv[0]'s basename selects a subcommand —
//! `arg_required_else_help` — a selected command with no argv of its own shows short help —
Expand Down
36 changes: 31 additions & 5 deletions derive/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ pub struct Cli {
/// `Subcommands` variant may still add aliases for the particular route mounting it.
pub aliases: Vec<String>,
pub hidden_aliases: Vec<String>,
/// Whether a command using this argument struct is omitted from help and completions.
pub hide: bool,
/// Default casing for inferred flag and positional names.
rename_all: Option<CasingStyle>,
/// Default casing for environment names inferred by bare `env`.
Expand Down Expand Up @@ -630,6 +632,7 @@ impl Cli {
effect: None,
aliases: Vec::new(),
hidden_aliases: Vec::new(),
hide: false,
rename_all: None,
rename_all_env: CasingStyle::ScreamingSnake,
attr_span: input
Expand Down Expand Up @@ -683,6 +686,7 @@ impl Cli {
};

for attr in attrs(&input.attrs) {
let clap_attr = attr.path().is_ident("command");
for meta in nested(attr)? {
let path = meta.path().clone();
match ident_of(&path).as_str() {
Expand Down Expand Up @@ -739,8 +743,15 @@ impl Cli {
"settings" => cli.settings = flag_value(&meta)?,
"verbatim_doc_comment" => verbatim_doc_comment = flag_value(&meta)?,
"effect" => cli.effect = Some(effect_value(&meta)?),
"alias" => cli.aliases.extend(selectors(&meta)?),
"alias" | "aliases" if clap_attr => {
cli.hidden_aliases.extend(selectors(&meta)?);
}
Comment thread
cursor[bot] marked this conversation as resolved.
"alias" | "aliases" => cli.aliases.extend(selectors(&meta)?),
"visible_alias" | "visible_aliases" => {
cli.aliases.extend(selectors(&meta)?);
}
"alias_hidden" => cli.hidden_aliases.extend(selectors(&meta)?),
"hide" => cli.hide = flag_value(&meta)?,
"min_usage_version" => cli.min_usage_version = Some(string_value(&meta)?),
"usage" => cli.usage = Some(string_value(&meta)?),
"version" => {
Expand Down Expand Up @@ -804,8 +815,18 @@ impl Cli {
// in one CLI" against "mise prepares your development environment before
// each command runs." There is no comment that says both, so they can be
// declared.
"about" => cli.about_attr = Some(metadata_expr(&meta)?),
"long_about" => cli.long_about_attr = Some(metadata_expr(&meta)?),
"about" => {
cli.about_attr = Some(match &meta {
Meta::Path(_) => quote::quote!(env!("CARGO_PKG_DESCRIPTION")),
_ => metadata_expr(&meta)?,
});
}
"long_about" => {
cli.long_about_attr = Some(match &meta {
Meta::Path(_) => quote::quote!(env!("CARGO_PKG_DESCRIPTION")),
_ => metadata_expr(&meta)?,
});
}
"deprecated" => cli.deprecated = Some(string_value(&meta)?),
"deprecated_warn_at" => cli.deprecated_warn_at = Some(string_value(&meta)?),
"deprecated_remove_at" => cli.deprecated_remove_at = Some(string_value(&meta)?),
Expand Down Expand Up @@ -875,7 +896,7 @@ impl Cli {
path,
format!(
"unknown option `{other}` on a struct; usage::Cli takes \
`name`, `name_spec`, `bin`, `bin_spec`, `version`, `version_spec`, `long_version`, `long_version_spec`, `author`, `license`, `repository`, `usage`, `deprecated`, `deprecated_warn_at`, `deprecated_remove_at`, `verbatim_doc_comment`, `unknown_flags`, \
`name`, `name_spec`, `bin`, `bin_spec`, `version`, `version_spec`, `long_version`, `long_version_spec`, `author`, `license`, `repository`, `usage`, `alias`, `alias_hidden`, `visible_alias`, `hide`, `deprecated`, `deprecated_warn_at`, `deprecated_remove_at`, `verbatim_doc_comment`, `unknown_flags`, \
Comment thread
cursor[bot] marked this conversation as resolved.
`default_subcommand`, `multicall`, `no_binary_name`, `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`, \
`next_help_heading`, `subcommand_help_heading`, `next_line_help`, `flatten_help`, `term_width`, `max_term_width`, \
`subcommand_value_name`, `restart_token`, `mount` and \
Expand Down Expand Up @@ -4356,12 +4377,17 @@ impl Variant {
let mut after_long_help = None;

for attr in attrs(&variant.attrs) {
let clap_attr = attr.path().is_ident("command");
for meta in nested(attr)? {
let path = meta.path().clone();
match ident_of(&path).as_str() {
"name" => name = strip_dashes(&string_value(&meta)?),
// One as a value or several as a list, as the relationship options do.
"alias" => aliases.extend(selectors(&meta)?),
"alias" | "aliases" if clap_attr => {
hidden_aliases.extend(selectors(&meta)?);
}
"alias" | "aliases" => aliases.extend(selectors(&meta)?),
"visible_alias" | "visible_aliases" => aliases.extend(selectors(&meta)?),
"alias_hidden" => hidden_aliases.extend(selectors(&meta)?),
"hide" => hide = flag_value(&meta)?,
"help_heading" => help_heading = Some(string_value(&meta)?),
Expand Down
15 changes: 15 additions & 0 deletions lib/src/docs/markdown/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ use itertools::Itertools;

fn escape_md(value: &str, html_encode: bool) -> String {
let mut in_fenced_code_block = false;
// Help text is allowed to contain terminal styling. clap-era applications commonly build
// their examples with `color_print::cstr!`, which embeds SGR sequences even when color is
// disabled at runtime. Terminal styling has no meaning in generated Markdown, and leaving
// it here publishes literal escape bytes in docs and downstream static sites.
let value = xx::regex!(r"\x1b\[[0-?]*[ -/]*[@-~]").replace_all(value, "");

value
.lines()
Expand Down Expand Up @@ -249,4 +254,14 @@ mod tests {

assert_eq!(escape_md(input, false), input);
}

#[test]
fn strips_terminal_styling_from_generated_markdown() {
let input =
"\u{1b}[1m\u{1b}[4mExamples:\u{1b}[22m\u{1b}[24m\n\n \u{1b}[1mmise run\u{1b}[22m";
let expected = "Examples:\n\n mise run";

assert_eq!(escape_md(input, true), expected);
assert_eq!(escape_md(input, false), expected);
}
}
35 changes: 35 additions & 0 deletions usage-rs/tests/facade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,28 @@ struct PresentedSubcommands {
command: Option<ArgumentConflictCommand>,
}

#[derive(Args)]
#[command(
visible_alias = "go",
alias = "secret-run",
hide,
after_long_help = "More details."
)]
struct StructCommandMetadata;

#[derive(Subcommands)]
enum StructMetadataCommands {
Run(StructCommandMetadata),
}

#[derive(Cli)]
#[command(bin = "struct-metadata", about)]
#[allow(dead_code)]
struct StructMetadataCli {
#[command(subcommand)]
command: StructMetadataCommands,
}

#[derive(Cli)]
#[command(bin = "ordered")]
#[allow(dead_code)]
Expand Down Expand Up @@ -1424,6 +1446,19 @@ fn typed_subcommand_presentation_reaches_help_and_the_spec() {
assert!(page.contains("Actions:"), "{page}");
}

#[test]
fn clap_command_metadata_can_stay_on_the_args_struct() {
let spec = StructMetadataCli::spec();
let meta = spec.root.subcommands[0];
assert_eq!(meta.cmd.aliases, ["go", "secret-run"]);
assert_eq!(meta.hidden_aliases, ["secret-run"]);
assert!(meta.hide);
assert_eq!(meta.after_long_help, Some("More details."));

assert!(StructMetadataCli::parse_from(&[OsStr::new("go")]).is_ok());
assert!(StructMetadataCli::parse_from(&[OsStr::new("secret-run")]).is_ok());
}

#[test]
fn explicit_display_order_reaches_help_and_the_portable_spec() {
let spec = OrderedHelp::spec();
Expand Down
Loading