Skip to content

Commit 02128f7

Browse files
Eric Gumbathomas-zahner
authored andcommitted
fix(clap_mangen): Take into consideration display_order
In #3362 we have an issue where when we configure an arg via .display_order(int), and then generate a manpage, the synposis and options will render the order the args were provided to the App rather than the order they were configured e.g Command::new(name) arg(Arg::new("few").short('b').display_order(2)) arg(Arg::new("bar").short('a').display_order(1)) will show ... SYNOPSIS <name> [-b] [-a] ... ... OPTIONS -b -a instead of ... SYNOPSIS <name> [-a] [-b] ... ... OPTIONS -a -b and so on. This fix adds sorting in the synopsis and options functions responsible for generating the corresponding synopsis and options sections of the manpage.
1 parent c9a39a5 commit 02128f7

File tree

6 files changed

+281
-18
lines changed

6 files changed

+281
-18
lines changed

clap_mangen/src/render.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ pub(crate) fn synopsis(roff: &mut Roff, cmd: &clap::Command) {
3333
let name = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name());
3434
let mut line = vec![bold(name), roman(" ")];
3535

36-
for opt in cmd.get_arguments().filter(|i| !i.is_hide_set()) {
36+
let mut opts: Vec<_> = cmd.get_arguments().filter(|i| !i.is_hide_set()).collect();
37+
38+
opts.sort_by_key(|opt| option_sort_key(opt));
39+
40+
for opt in opts {
3741
let (lhs, rhs) = option_markers(opt);
3842
match (opt.get_short(), opt.get_long()) {
3943
(Some(short), Some(long)) => {
@@ -89,7 +93,10 @@ pub(crate) fn synopsis(roff: &mut Roff, cmd: &clap::Command) {
8993
}
9094

9195
pub(crate) fn options(roff: &mut Roff, items: &[&Arg]) {
92-
for opt in items.iter().filter(|a| !a.is_positional()) {
96+
let mut sorted_items = items.to_vec();
97+
sorted_items.sort_by_key(|opt| option_sort_key(opt));
98+
99+
for opt in sorted_items.iter().filter(|a| !a.is_positional()) {
93100
let mut header = match (opt.get_short(), opt.get_long()) {
94101
(Some(short), Some(long)) => {
95102
vec![short_option(short), roman(", "), long_option(long)]
@@ -217,7 +224,10 @@ fn possible_options(roff: &mut Roff, arg: &Arg, arg_help_written: bool) {
217224
}
218225

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

223233
let name = format!(
@@ -378,3 +388,23 @@ fn format_possible_values(possibles: &Vec<&clap::builder::PossibleValue>) -> (Ve
378388
}
379389
(lines, with_help)
380390
}
391+
392+
fn subcommand_sort_key(command: &clap::Command) -> (usize, &str) {
393+
(command.get_display_order(), command.get_name())
394+
}
395+
396+
/// Note that this function is duplicated from `clap::builder`
397+
fn option_sort_key(arg: &Arg) -> (usize, String) {
398+
let key = if let Some(x) = arg.get_short() {
399+
let mut s = x.to_ascii_lowercase().to_string();
400+
s.push(if x.is_ascii_lowercase() { '0' } else { '1' });
401+
s
402+
} else if let Some(x) = arg.get_long() {
403+
x.to_string()
404+
} else {
405+
let mut s = '{'.to_string();
406+
s.push_str(arg.get_id().as_str());
407+
s
408+
};
409+
(arg.get_display_order(), key)
410+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
.ie \n(.g .ds Aq \(aq
2+
.el .ds Aq '
3+
.TH my-app 1 "my-app "
4+
.SH NAME
5+
my\-app
6+
.SH SYNOPSIS
7+
\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]
8+
.SH DESCRIPTION
9+
.SH OPTIONS
10+
.TP
11+
\fB\-O\fR, \fB\-\-first\fR
12+
Should be 1st
13+
.TP
14+
\fB\-P\fR, \fB\-\-second\fR
15+
Should be 2nd
16+
.TP
17+
\fB\-Q\fR, \fB\-\-third\fR
18+
Should be 3rd
19+
.TP
20+
\fB\-\-fourth\fR
21+
Should be 4th
22+
.TP
23+
\fB\-h\fR, \fB\-\-help\fR
24+
Print help
25+
.TP
26+
[\fI1st\fR]
27+
1st
28+
.TP
29+
[\fI2nd\fR]
30+
2nd
31+
.TP
32+
[\fI3rd\fR]
33+
3rd
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.ie \n(.g .ds Aq \(aq
2+
.el .ds Aq '
3+
.TH my-app 1 "my-app 1"
4+
.SH NAME
5+
my\-app
6+
.SH SYNOPSIS
7+
\fBmy\-app\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIsubcommands\fR]
8+
.SH DESCRIPTION
9+
.SH OPTIONS
10+
.TP
11+
\fB\-h\fR, \fB\-\-help\fR
12+
Print help
13+
.TP
14+
\fB\-V\fR, \fB\-\-version\fR
15+
Print version
16+
.SH SUBCOMMANDS
17+
.TP
18+
my\-app\-a1(1)
19+
blah a1
20+
.TP
21+
my\-app\-b1(1)
22+
blah b1
23+
.TP
24+
my\-app\-help(1)
25+
Print this message or the help of the given subcommand(s)
26+
.SH VERSION
27+
v1
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.ie \n(.g .ds Aq \(aq
2+
.el .ds Aq '
3+
.TH my-app 1 "my-app 1"
4+
.SH NAME
5+
my\-app
6+
.SH SYNOPSIS
7+
\fBmy\-app\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIsubcommands\fR]
8+
.SH DESCRIPTION
9+
.SH OPTIONS
10+
.TP
11+
\fB\-h\fR, \fB\-\-help\fR
12+
Print help
13+
.TP
14+
\fB\-V\fR, \fB\-\-version\fR
15+
Print version
16+
.SH SUBCOMMANDS
17+
.TP
18+
my\-app\-b1(1)
19+
blah b1
20+
.TP
21+
my\-app\-a1(1)
22+
blah a1
23+
.TP
24+
my\-app\-help(1)
25+
Print this message or the help of the given subcommand(s)
26+
.SH VERSION
27+
v1

clap_mangen/tests/testsuite/common.rs

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,36 @@ pub(crate) fn env_value_command(name: &'static str) -> clap::Command {
273273
)
274274
}
275275

276+
pub(crate) fn mangen_output(cmd: &clap::Command) -> String {
277+
let mut buf = vec![];
278+
clap_mangen::Man::new(cmd.clone()).render(&mut buf).unwrap();
279+
280+
let s = match std::str::from_utf8(&buf) {
281+
Ok(s) => s,
282+
Err(e) => panic!("Failed to convert output to UTF-8: {e}"),
283+
};
284+
285+
s.to_string()
286+
}
287+
288+
// Go through the output and assert that the keywords
289+
// appear in the expected order
290+
pub(crate) fn is_correct_ordering(keywords: &[&'static str], text: &str) -> bool {
291+
let mut s = text;
292+
for keyword in keywords {
293+
if !s.contains(keyword) {
294+
return false;
295+
}
296+
// everytime we find a match,
297+
// we only look at the rest of the string
298+
s = match s.split(keyword).last() {
299+
Some(rest) => rest,
300+
None => return false,
301+
};
302+
}
303+
true
304+
}
305+
276306
pub(crate) fn assert_matches(expected: impl IntoData, cmd: clap::Command) {
277307
let mut buf = vec![];
278308
clap_mangen::Man::new(cmd).render(&mut buf).unwrap();
@@ -324,6 +354,40 @@ pub(crate) fn value_name_without_arg(name: &'static str) -> clap::Command {
324354
)
325355
}
326356

357+
pub(crate) fn configured_display_order_args(name: &'static str) -> clap::Command {
358+
clap::Command::new(name)
359+
.arg(clap::Arg::new("1st").help("1st"))
360+
.arg(clap::Arg::new("2nd").help("2nd"))
361+
.arg(clap::Arg::new("3rd").help("3rd").last(true))
362+
.arg(
363+
clap::Arg::new("c")
364+
.long("third")
365+
.short('Q')
366+
.display_order(3)
367+
.help("Should be 3rd"),
368+
)
369+
.arg(
370+
clap::Arg::new("d")
371+
.long("fourth")
372+
.display_order(4)
373+
.help("Should be 4th"),
374+
)
375+
.arg(
376+
clap::Arg::new("a")
377+
.long("first")
378+
.short('O')
379+
.display_order(1)
380+
.help("Should be 1st"),
381+
)
382+
.arg(
383+
clap::Arg::new("b")
384+
.long("second")
385+
.short('P')
386+
.display_order(2)
387+
.help("Should be 2nd"),
388+
)
389+
}
390+
327391
pub(crate) fn help_headings(name: &'static str) -> clap::Command {
328392
clap::Command::new(name)
329393
.arg(
@@ -356,8 +420,7 @@ pub(crate) fn help_headings(name: &'static str) -> clap::Command {
356420
}
357421

358422
pub(crate) fn value_with_required_equals(name: &'static str) -> clap::Command {
359-
clap::Command::new(name)
360-
.arg(
423+
clap::Command::new(name).arg(
361424
clap::Arg::new("config")
362425
.long("config")
363426
.value_name("FILE")
@@ -367,8 +430,7 @@ pub(crate) fn value_with_required_equals(name: &'static str) -> clap::Command {
367430
}
368431

369432
pub(crate) fn optional_value_with_required_equals(name: &'static str) -> clap::Command {
370-
clap::Command::new(name)
371-
.arg(
433+
clap::Command::new(name).arg(
372434
clap::Arg::new("config")
373435
.long("config")
374436
.value_name("FILE")
@@ -379,8 +441,7 @@ pub(crate) fn optional_value_with_required_equals(name: &'static str) -> clap::C
379441
}
380442

381443
pub(crate) fn optional_value(name: &'static str) -> clap::Command {
382-
clap::Command::new(name)
383-
.arg(
444+
clap::Command::new(name).arg(
384445
clap::Arg::new("config")
385446
.long("config")
386447
.value_name("FILE")
@@ -390,8 +451,7 @@ pub(crate) fn optional_value(name: &'static str) -> clap::Command {
390451
}
391452

392453
pub(crate) fn multiple_optional_values(name: &'static str) -> clap::Command {
393-
clap::Command::new(name)
394-
.arg(
454+
clap::Command::new(name).arg(
395455
clap::Arg::new("config")
396456
.long("config")
397457
.value_names(["FILE1", "FILE2"])
@@ -401,13 +461,45 @@ pub(crate) fn multiple_optional_values(name: &'static str) -> clap::Command {
401461
}
402462

403463
pub(crate) fn variadic_values(name: &'static str) -> clap::Command {
404-
clap::Command::new(name)
405-
.arg(
464+
clap::Command::new(name).arg(
406465
clap::Arg::new("config")
407466
.long("config")
408467
.value_names(["FILE1", "FILE2"])
409468
.require_equals(false)
410469
.num_args(3)
411470
.help("Optional config file"),
412471
)
413-
}
472+
}
473+
474+
pub(crate) fn configured_subcmd_order(name: &'static str) -> clap::Command {
475+
clap::Command::new(name)
476+
.version("1")
477+
.next_display_order(None)
478+
.subcommands(vec![
479+
clap::Command::new("b1").about("blah b1").arg(
480+
clap::Arg::new("test")
481+
.short('t')
482+
.action(clap::ArgAction::SetTrue),
483+
),
484+
clap::Command::new("a1").about("blah a1").arg(
485+
clap::Arg::new("roster")
486+
.short('r')
487+
.action(clap::ArgAction::SetTrue),
488+
),
489+
])
490+
}
491+
492+
pub(crate) fn default_subcmd_order(name: &'static str) -> clap::Command {
493+
clap::Command::new(name).version("1").subcommands(vec![
494+
clap::Command::new("b1").about("blah b1").arg(
495+
clap::Arg::new("test")
496+
.short('t')
497+
.action(clap::ArgAction::SetTrue),
498+
),
499+
clap::Command::new("a1").about("blah a1").arg(
500+
clap::Arg::new("roster")
501+
.short('r')
502+
.action(clap::ArgAction::SetTrue),
503+
),
504+
])
505+
}

clap_mangen/tests/testsuite/roff.rs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,7 @@ fn optional_value_with_required_equals() {
137137
fn optional_value() {
138138
let name = "my-app";
139139
let cmd = common::optional_value(name);
140-
common::assert_matches(
141-
snapbox::file!["../snapshots/optional_value.bash.roff"],
142-
cmd,
143-
);
140+
common::assert_matches(snapbox::file!["../snapshots/optional_value.bash.roff"], cmd);
144141
}
145142

146143
#[test]
@@ -162,3 +159,60 @@ fn variadic_values() {
162159
cmd,
163160
);
164161
}
162+
163+
#[test]
164+
fn configured_display_order_args() {
165+
let name = "my-app";
166+
let cmd = common::configured_display_order_args(name);
167+
168+
let s = common::mangen_output(&cmd);
169+
170+
let ordered_keywords = ["first", "second", "third", "fourth", "1st", "2nd", "3rd"];
171+
let default_ordered_keywords = ["third", "fourth", "first", "second", "1st", "2nd", "3rd"];
172+
173+
assert!(common::is_correct_ordering(&ordered_keywords, &s));
174+
assert!(!common::is_correct_ordering(&default_ordered_keywords, &s));
175+
176+
common::assert_matches(
177+
snapbox::file!["../snapshots/configured_display_order_args.roff"],
178+
cmd,
179+
);
180+
}
181+
182+
#[test]
183+
fn configured_subcmd_order() {
184+
let name = "my-app";
185+
let cmd = common::configured_subcmd_order(name);
186+
187+
let s = common::mangen_output(&cmd);
188+
189+
let ordered_keywords = ["a1", "b1"];
190+
let default_ordered_keywords = ["b1", "a1"];
191+
192+
assert!(common::is_correct_ordering(&ordered_keywords, &s));
193+
assert!(!common::is_correct_ordering(&default_ordered_keywords, &s));
194+
195+
common::assert_matches(
196+
snapbox::file!["../snapshots/configured_subcmd_order.roff"],
197+
cmd,
198+
);
199+
}
200+
201+
#[test]
202+
fn default_subcmd_order() {
203+
let name = "my-app";
204+
let cmd = common::default_subcmd_order(name);
205+
206+
let s = common::mangen_output(&cmd);
207+
208+
let ordered_keywords = ["a1", "b1"];
209+
let default_ordered_keywords = ["b1", "a1"];
210+
211+
assert!(!common::is_correct_ordering(&ordered_keywords, &s));
212+
assert!(common::is_correct_ordering(&default_ordered_keywords, &s));
213+
214+
common::assert_matches(
215+
snapbox::file!["../snapshots/default_subcmd_order.roff"],
216+
cmd,
217+
);
218+
}

0 commit comments

Comments
 (0)