Skip to content
Open
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
46 changes: 46 additions & 0 deletions clap_builder/src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,49 @@ pub(crate) use self::str::Inner as StrInner;
pub(crate) use action::CountType;
pub(crate) use arg_settings::{ArgFlags, ArgSettings};
pub(crate) use command::AppExt;

/// Strategies for ordering arguments
pub enum ArgOrder {
/// Preserve argument order and skip sorting
Preserve,
/// Sort arguments by short flags, then by long flags and finally by name
LongShortName,
}

impl ArgOrder {
/// Sort arguments as specified by `ArgOrder`
pub fn sort(self, mut args: Vec<&Arg>) -> Vec<&Arg> {
let comparison = match self {
ArgOrder::Preserve => None,
ArgOrder::LongShortName => Some(option_sort_key),
};

if let Some(c) = comparison {
args.sort_by_key(|e| c(e));
}
args
}
}

fn option_sort_key(arg: &Arg) -> (usize, String) {
// Formatting key like this to ensure that:
// 1. Argument has long flags are printed just after short flags.
// 2. For two args both have short flags like `-c` and `-C`, the
// `-C` arg is printed just after the `-c` arg
// 3. For args without short or long flag, print them at last(sorted
// by arg name).
// Example order: -a, -b, -B, -s, --select-file, --select-folder, -x

let key = if let Some(x) = arg.get_short() {
let mut s = x.to_ascii_lowercase().to_string();
s.push(if x.is_ascii_lowercase() { '0' } else { '1' });
s
} else if let Some(x) = arg.get_long() {
x.to_string()
} else {
let mut s = '{'.to_string();
s.push_str(arg.get_id().as_str());
s
};
(arg.get_display_order(), key)
}
52 changes: 9 additions & 43 deletions clap_builder/src/output/help_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::builder::PossibleValue;
use crate::builder::Str;
use crate::builder::StyledStr;
use crate::builder::Styles;
use crate::builder::{Arg, Command};
use crate::builder::{Arg, ArgOrder, Command};
use crate::output::display_width;
use crate::output::wrap;
use crate::output::Usage;
Expand Down Expand Up @@ -220,14 +220,14 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> {
self.write_args(
&self.cmd.get_non_positionals().collect::<Vec<_>>(),
"options",
option_sort_key,
ArgOrder::LongShortName,
);
}
"positionals" => {
self.write_args(
&self.cmd.get_positionals().collect::<Vec<_>>(),
"positionals",
positional_sort_key,
ArgOrder::Preserve,
);
}
"subcommands" => {
Expand Down Expand Up @@ -422,7 +422,7 @@ impl HelpTemplate<'_, '_> {
// Write positional args if any
let help_heading = "Arguments";
let _ = write!(self.writer, "{header}{help_heading}:{header:#}\n",);
self.write_args(&pos, "Arguments", positional_sort_key);
self.write_args(&pos, "Arguments", ArgOrder::Preserve);
}

if !non_pos.is_empty() {
Expand All @@ -432,7 +432,7 @@ impl HelpTemplate<'_, '_> {
first = false;
let help_heading = "Options";
let _ = write!(self.writer, "{header}{help_heading}:{header:#}\n",);
self.write_args(&non_pos, "Options", option_sort_key);
self.write_args(&non_pos, "Options", ArgOrder::LongShortName);
}
if !custom_headings.is_empty() {
for heading in custom_headings {
Expand All @@ -454,7 +454,7 @@ impl HelpTemplate<'_, '_> {
}
first = false;
let _ = write!(self.writer, "{header}{heading}:{header:#}\n",);
self.write_args(&args, heading, option_sort_key);
self.write_args(&args, heading, ArgOrder::LongShortName);
}
}
}
Expand All @@ -466,11 +466,10 @@ impl HelpTemplate<'_, '_> {
}

/// Sorts arguments by length and display order and write their help to the wrapped stream.
fn write_args(&mut self, args: &[&Arg], _category: &str, sort_key: ArgSortKey) {
fn write_args(&mut self, args: &[&Arg], _category: &str, order: ArgOrder) {
debug!("HelpTemplate::write_args {_category}");
// The shortest an arg can legally be is 2 (i.e. '-x')
let mut longest = 2;
let mut ord_v = BTreeMap::new();

// Determine the longest
for &arg in args.iter().filter(|arg| {
Expand All @@ -493,14 +492,10 @@ impl HelpTemplate<'_, '_> {
longest
);
}

let key = (sort_key)(arg);
ord_v.insert(key, arg);
}

let next_line_help = self.will_args_wrap(args, longest);

for (i, (_, arg)) in ord_v.iter().enumerate() {
for (i, arg) in order.sort(args.to_vec()).iter().enumerate() {
if i != 0 {
self.writer.push_str("\n");
if next_line_help && self.use_long {
Expand Down Expand Up @@ -914,7 +909,7 @@ impl HelpTemplate<'_, '_> {
term_w: self.term_w,
use_long: self.use_long,
};
sub_help.write_args(&args, heading, option_sort_key);
sub_help.write_args(&args, heading, ArgOrder::LongShortName);
if subcommand.is_flatten_help_set() {
sub_help.write_flat_subcommands(subcommand, first);
}
Expand Down Expand Up @@ -1058,35 +1053,6 @@ impl HelpTemplate<'_, '_> {

const NEXT_LINE_INDENT: &str = " ";

type ArgSortKey = fn(arg: &Arg) -> (usize, String);

fn positional_sort_key(arg: &Arg) -> (usize, String) {
(arg.get_index().unwrap_or(0), String::new())
}

fn option_sort_key(arg: &Arg) -> (usize, String) {
// Formatting key like this to ensure that:
// 1. Argument has long flags are printed just after short flags.
// 2. For two args both have short flags like `-c` and `-C`, the
// `-C` arg is printed just after the `-c` arg
// 3. For args without short or long flag, print them at last(sorted
// by arg name).
// Example order: -a, -b, -B, -s, --select-file, --select-folder, -x

let key = if let Some(x) = arg.get_short() {
let mut s = x.to_ascii_lowercase().to_string();
s.push(if x.is_ascii_lowercase() { '0' } else { '1' });
s
} else if let Some(x) = arg.get_long() {
x.to_string()
} else {
let mut s = '{'.to_string();
s.push_str(arg.get_id().as_str());
s
};
(arg.get_display_order(), key)
}

pub(crate) fn dimensions() -> (Option<usize>, Option<usize>) {
#[cfg(not(feature = "wrap_help"))]
return (None, None);
Expand Down
20 changes: 16 additions & 4 deletions clap_mangen/src/render.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clap::{Arg, ArgAction};
use clap::{builder::ArgOrder, Arg, ArgAction};
use roff::{bold, italic, roman, Inline, Roff};

pub(crate) fn subcommand_heading(cmd: &clap::Command) -> &str {
Expand Down Expand Up @@ -33,7 +33,10 @@ pub(crate) fn synopsis(roff: &mut Roff, cmd: &clap::Command) {
let name = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name());
let mut line = vec![bold(name), roman(" ")];

for opt in cmd.get_arguments().filter(|i| !i.is_hide_set()) {
let opts: Vec<_> =
ArgOrder::LongShortName.sort(cmd.get_arguments().filter(|i| !i.is_hide_set()).collect());

for opt in opts {
let (lhs, rhs) = option_markers(opt);
match (opt.get_short(), opt.get_long()) {
(Some(short), Some(long)) => {
Expand Down Expand Up @@ -89,7 +92,9 @@ pub(crate) fn synopsis(roff: &mut Roff, cmd: &clap::Command) {
}

pub(crate) fn options(roff: &mut Roff, items: &[&Arg]) {
for opt in items.iter().filter(|a| !a.is_positional()) {
let sorted_items: Vec<_> = ArgOrder::LongShortName.sort(items.to_vec());

for opt in sorted_items.iter().filter(|a| !a.is_positional()) {
let mut header = match (opt.get_short(), opt.get_long()) {
(Some(short), Some(long)) => {
vec![short_option(short), roman(", "), long_option(long)]
Expand Down Expand Up @@ -199,7 +204,10 @@ fn possible_options(roff: &mut Roff, arg: &Arg, arg_help_written: bool) {
}

pub(crate) fn subcommands(roff: &mut Roff, cmd: &clap::Command, section: &str) {
for sub in cmd.get_subcommands().filter(|s| !s.is_hide_set()) {
let mut sorted_subcommands: Vec<_> =
cmd.get_subcommands().filter(|s| !s.is_hide_set()).collect();
sorted_subcommands.sort_by_key(|c| subcommand_sort_key(c));
for sub in sorted_subcommands {
roff.control("TP", []);

let name = format!(
Expand Down Expand Up @@ -335,3 +343,7 @@ fn format_possible_values(possibles: &Vec<&clap::builder::PossibleValue>) -> (Ve
}
(lines, with_help)
}

fn subcommand_sort_key(command: &clap::Command) -> (usize, &str) {
(command.get_display_order(), command.get_name())
}
33 changes: 33 additions & 0 deletions clap_mangen/tests/snapshots/configured_display_order_args.roff
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH my-app 1 "my-app "
.SH NAME
my\-app
.SH SYNOPSIS
\fBmy\-app\fR [\fB\-O\fR|\fB\-\-first\fR] [\fB\-P\fR|\fB\-\-second\fR] [\fB\-Q\fR|\fB\-\-third\fR] [\fB\-\-fourth\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fI1st\fR] [\fI2nd\fR] [\fI3rd\fR]
.SH DESCRIPTION
.SH OPTIONS
.TP
\fB\-O\fR, \fB\-\-first\fR
Should be 1st
.TP
\fB\-P\fR, \fB\-\-second\fR
Should be 2nd
.TP
\fB\-Q\fR, \fB\-\-third\fR
Should be 3rd
.TP
\fB\-\-fourth\fR
Should be 4th
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help
.TP
[\fI1st\fR]
1st
.TP
[\fI2nd\fR]
2nd
.TP
[\fI3rd\fR]
3rd
27 changes: 27 additions & 0 deletions clap_mangen/tests/snapshots/configured_subcmd_order.roff
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH my-app 1 "my-app 1"
.SH NAME
my\-app
.SH SYNOPSIS
\fBmy\-app\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIsubcommands\fR]
.SH DESCRIPTION
.SH OPTIONS
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version
.SH SUBCOMMANDS
.TP
my\-app\-a1(1)
blah a1
.TP
my\-app\-b1(1)
blah b1
.TP
my\-app\-help(1)
Print this message or the help of the given subcommand(s)
.SH VERSION
v1
27 changes: 27 additions & 0 deletions clap_mangen/tests/snapshots/default_subcmd_order.roff
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH my-app 1 "my-app 1"
.SH NAME
my\-app
.SH SYNOPSIS
\fBmy\-app\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIsubcommands\fR]
.SH DESCRIPTION
.SH OPTIONS
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version
.SH SUBCOMMANDS
.TP
my\-app\-b1(1)
blah b1
.TP
my\-app\-a1(1)
blah a1
.TP
my\-app\-help(1)
Print this message or the help of the given subcommand(s)
.SH VERSION
v1
Loading