11use crate :: args:: Args ;
22use clap:: { Arg , ArgAction , Command , CommandFactory } ;
3- use std:: { borrow:: Cow , fmt:: Write } ;
3+ use std:: { borrow:: Cow , collections:: BTreeMap , fmt:: Write } ;
4+
5+ /// A map from argument ID to the set of argument IDs it conflicts with (bidirectional).
6+ type ConflictMap = BTreeMap < String , Vec < String > > ;
47
58/// Renders the man page for `pdu` as a string in roff format.
69pub fn render_man_page ( ) -> String {
710 let mut command = Args :: command ( ) ;
811 command. build ( ) ;
12+ let conflict_map = build_conflict_map ( & command) ;
913 let mut out = String :: new ( ) ;
1014 render_title ( & mut out, & command) ;
1115 render_name_section ( & mut out, & command) ;
1216 render_synopsis_section ( & mut out, & command) ;
1317 render_description_section ( & mut out, & command) ;
14- render_options_section ( & mut out, & command) ;
18+ render_options_section ( & mut out, & command, & conflict_map ) ;
1519 render_examples_section ( & mut out, & command) ;
1620 render_version_section ( & mut out, & command) ;
1721 out
1822}
1923
24+ /// Builds a bidirectional conflict map from clap's one-directional conflict declarations.
25+ fn build_conflict_map ( command : & Command ) -> ConflictMap {
26+ let mut map = ConflictMap :: new ( ) ;
27+ for arg in command. get_arguments ( ) {
28+ let arg_id = arg. get_id ( ) . to_string ( ) ;
29+ for conflict in command. get_arg_conflicts_with ( arg) {
30+ let conflict_id = conflict. get_id ( ) . to_string ( ) ;
31+ map. entry ( arg_id. clone ( ) )
32+ . or_default ( )
33+ . push ( conflict_id. clone ( ) ) ;
34+ map. entry ( conflict_id) . or_default ( ) . push ( arg_id. clone ( ) ) ;
35+ }
36+ }
37+ // Deduplicate each entry
38+ for conflicts in map. values_mut ( ) {
39+ conflicts. sort ( ) ;
40+ conflicts. dedup ( ) ;
41+ }
42+ map
43+ }
44+
45+ /// Resolves an argument ID to its `--long` flag name for display.
46+ fn resolve_flag_name ( command : & Command , arg_id : & str ) -> Option < String > {
47+ command
48+ . get_arguments ( )
49+ . find ( |arg| arg. get_id ( ) . as_str ( ) == arg_id)
50+ . and_then ( |arg| arg. get_long ( ) )
51+ . map ( |long| format ! ( "\\ fB\\ -\\ -{}\\ fR" , roff_escape( long) ) )
52+ }
53+
2054/// Escapes a string for roff by replacing hyphens with `\-`.
2155fn roff_escape ( text : & str ) -> String {
2256 text. replace ( '-' , r"\-" )
@@ -128,17 +162,17 @@ fn render_paragraph_text(out: &mut String, text: &str) {
128162 }
129163}
130164
131- fn render_options_section ( out : & mut String , command : & Command ) {
165+ fn render_options_section ( out : & mut String , command : & Command , conflict_map : & ConflictMap ) {
132166 out. push_str ( ".SH OPTIONS\n " ) ;
133167 for arg in command. get_arguments ( ) {
134168 if arg. is_hide_set ( ) {
135169 continue ;
136170 }
137- render_option_entry ( out, command, arg) ;
171+ render_option_entry ( out, command, arg, conflict_map ) ;
138172 }
139173}
140174
141- fn render_option_entry ( out : & mut String , command : & Command , arg : & Arg ) {
175+ fn render_option_entry ( out : & mut String , command : & Command , arg : & Arg , conflict_map : & ConflictMap ) {
142176 out. push_str ( ".TP\n " ) ;
143177 if arg. is_positional ( ) {
144178 render_option_header_positional ( out, arg) ;
@@ -152,7 +186,7 @@ fn render_option_entry(out: &mut String, command: &Command, arg: &Arg) {
152186 . unwrap_or_default ( ) ;
153187 writeln ! ( out, "{}" , roff_escape( & help) ) . unwrap ( ) ;
154188 render_possible_values ( out, arg) ;
155- render_conflicts ( out, command, arg) ;
189+ render_conflicts ( out, command, arg, conflict_map ) ;
156190}
157191
158192fn render_option_header_positional ( out : & mut String , arg : & Arg ) {
@@ -254,23 +288,25 @@ fn render_possible_values(out: &mut String, arg: &Arg) {
254288 out. push_str ( ".RE\n " ) ;
255289}
256290
257- fn render_conflicts ( out : & mut String , command : & Command , arg : & Arg ) {
258- let conflicts = command. get_arg_conflicts_with ( arg) ;
259- if conflicts. is_empty ( ) {
260- return ;
261- }
262- let conflict_names: Vec < _ > = conflicts
291+ fn render_conflicts ( out : & mut String , command : & Command , arg : & Arg , conflict_map : & ConflictMap ) {
292+ let arg_id = arg. get_id ( ) . as_str ( ) ;
293+ let conflict_ids = match conflict_map. get ( arg_id) {
294+ Some ( ids) if !ids. is_empty ( ) => ids,
295+ _ => return ,
296+ } ;
297+ let conflict_names: Vec < _ > = conflict_ids
263298 . iter ( )
264- . filter_map ( |conflict_arg| {
265- conflict_arg
266- . get_long ( )
267- . map ( |long| format ! ( "\\ fB\\ -\\ -{}\\ fR" , roff_escape( long) ) )
268- } )
299+ . filter_map ( |conflict_id| resolve_flag_name ( command, conflict_id) )
269300 . collect ( ) ;
270301 if conflict_names. is_empty ( ) {
271302 return ;
272303 }
273- writeln ! ( out, ".br\n Conflicts with {}." , conflict_names. join( ", " ) ) . unwrap ( ) ;
304+ writeln ! (
305+ out,
306+ ".PP\n Cannot be used with {}." ,
307+ conflict_names. join( ", " )
308+ )
309+ . unwrap ( ) ;
274310}
275311
276312fn render_examples_section ( out : & mut String , command : & Command ) {
0 commit comments