1use crate::args::Args;
2use clap::{Arg, ArgAction, Command, CommandFactory};
3use itertools::Itertools;
4use std::{collections::BTreeMap, fmt::Write};
5
6type ConflictMap = BTreeMap<String, Vec<String>>;
8
9pub fn render_man_page() -> String {
11 let mut command = Args::command();
12 command.build();
13 let conflict_map = build_conflict_map(&command);
14 let mut out = String::new();
15 render_title(&mut out, &command);
16 render_name_section(&mut out, &command);
17 render_synopsis_section(&mut out, &command);
18 render_description_section(&mut out, &command);
19 render_options_section(&mut out, &command, &conflict_map);
20 render_examples_section(&mut out, &command);
21 render_version_section(&mut out, &command);
22 out
23}
24
25fn build_conflict_map(command: &Command) -> ConflictMap {
30 let mut map = ConflictMap::new();
31 for arg in command.get_arguments() {
32 if arg.is_hide_set() {
33 continue;
34 }
35 let arg_id = arg.get_id().to_string();
36 for conflict in command.get_arg_conflicts_with(arg) {
37 if conflict.is_hide_set() {
38 continue;
39 }
40 let conflict_id = conflict.get_id().to_string();
41 map.entry(arg_id.clone())
42 .or_default()
43 .push(conflict_id.clone());
44 map.entry(conflict_id).or_default().push(arg_id.clone());
45 }
46 }
47 for conflicts in map.values_mut() {
48 conflicts.sort();
49 conflicts.dedup();
50 }
51 map
52}
53
54fn resolve_flag_name(command: &Command, arg_id: &str) -> Option<String> {
56 command
57 .get_arguments()
58 .find(|arg| arg.get_id().as_str() == arg_id)
59 .and_then(|arg| arg.get_long())
60 .map(|long| format!("\\fB\\-\\-{}\\fR", roff_escape(long)))
61}
62
63fn roff_escape(text: &str) -> String {
65 text.replace('-', r"\-")
66}
67
68fn render_title(out: &mut String, command: &Command) {
69 let name = command.get_name();
70 let version = command.get_version().unwrap_or_default();
71 writeln!(out, ".TH {name} 1 \"{name} {version}\"").unwrap();
72}
73
74fn render_name_section(out: &mut String, command: &Command) {
75 let name = command.get_name();
76 let about = command
77 .get_about()
78 .map(ToString::to_string)
79 .unwrap_or_default();
80 writeln!(out, ".SH NAME").unwrap();
81 writeln!(out, "{name} \\- {}", roff_escape(&about)).unwrap();
82}
83
84fn render_synopsis_section(out: &mut String, command: &Command) {
85 out.push_str(".SH SYNOPSIS\n");
86 write!(out, "\\fB{}\\fR", command.get_name()).unwrap();
87 let options = command
88 .get_arguments()
89 .filter(|arg| !arg.is_positional())
90 .filter(|arg| !arg.is_hide_set());
91 for arg in options {
92 out.push(' ');
93 render_synopsis_option(out, arg);
94 }
95 let positionals = command
96 .get_arguments()
97 .filter(|arg| arg.is_positional())
98 .filter(|arg| !arg.is_hide_set());
99 for arg in positionals {
100 out.push(' ');
101 render_synopsis_positional(out, arg);
102 }
103 out.push('\n');
104}
105
106fn render_synopsis_option(out: &mut String, arg: &Arg) {
107 out.push('[');
108 if let Some(short) = arg.get_short() {
109 write!(out, "\\fB\\-{}\\fR", roff_escape(&short.to_string())).unwrap();
110 if arg.get_long().is_some() {
111 out.push('|');
112 }
113 }
114 if let Some(long) = arg.get_long() {
115 write!(out, "\\fB\\-\\-{}\\fR", roff_escape(long)).unwrap();
116 }
117 if arg.get_action().takes_values()
118 && let Some(value_names) = arg.get_value_names()
119 {
120 for name in value_names {
121 write!(out, " \\fI{}\\fR", roff_escape(name)).unwrap();
122 }
123 }
124 out.push(']');
125}
126
127fn is_multiple(arg: &Arg) -> bool {
128 arg.get_num_args()
129 .map(|range| range.max_values() > 1)
130 .unwrap_or(false)
131}
132
133fn render_synopsis_positional(out: &mut String, arg: &Arg) {
134 let name = arg
135 .get_value_names()
136 .and_then(|names| names.first())
137 .map(|name| name.as_str())
138 .unwrap_or_else(|| arg.get_id().as_str());
139 let ellipsis = if is_multiple(arg) { "..." } else { "" };
140 if arg.is_required_set() {
141 write!(out, "\\fI{}\\fR{ellipsis}", roff_escape(name)).unwrap();
142 } else {
143 write!(out, "[\\fI{}\\fR]{ellipsis}", roff_escape(name)).unwrap();
144 }
145}
146
147fn render_description_section(out: &mut String, command: &Command) {
148 out.push_str(".SH DESCRIPTION\n");
149 let text = command
150 .get_long_about()
151 .or_else(|| command.get_about())
152 .map(ToString::to_string)
153 .unwrap_or_default();
154 render_paragraph_text(out, &text);
155}
156
157fn render_paragraph_text(out: &mut String, text: &str) {
162 let mut need_paragraph = false;
163 let mut first = true;
164 for line in text.lines() {
165 if line.is_empty() {
166 need_paragraph = true;
167 continue;
168 }
169 if need_paragraph && !first {
170 out.push_str(".PP\n");
171 } else if !first {
172 out.push_str(".br\n");
173 }
174 need_paragraph = false;
175 first = false;
176 writeln!(out, "{}", roff_escape(line)).unwrap();
177 }
178}
179
180fn render_options_section(out: &mut String, command: &Command, conflict_map: &ConflictMap) {
181 out.push_str(".SH OPTIONS\n");
182 for arg in command.get_arguments() {
183 if arg.is_hide_set() {
184 continue;
185 }
186 render_option_entry(out, command, arg, conflict_map);
187 }
188}
189
190fn render_option_entry(out: &mut String, command: &Command, arg: &Arg, conflict_map: &ConflictMap) {
191 out.push_str(".TP\n");
192 if arg.is_positional() {
193 render_option_header_positional(out, arg);
194 } else {
195 render_option_header_flag(out, arg);
196 }
197 let help = arg
198 .get_long_help()
199 .or_else(|| arg.get_help())
200 .map(ToString::to_string)
201 .unwrap_or_default();
202 writeln!(out, "{}", roff_escape(&help)).unwrap();
203 render_possible_values(out, arg);
204 render_conflicts(out, command, arg, conflict_map);
205}
206
207fn render_option_header_positional(out: &mut String, arg: &Arg) {
208 let name = arg
209 .get_value_names()
210 .and_then(|names| names.first())
211 .map(|name| name.as_str())
212 .unwrap_or_else(|| arg.get_id().as_str());
213 let ellipsis = if is_multiple(arg) { "..." } else { "" };
214 if arg.is_required_set() {
215 writeln!(out, "\\fI{name}\\fR{ellipsis}").unwrap();
216 } else {
217 writeln!(out, "[\\fI{name}\\fR]{ellipsis}").unwrap();
218 }
219}
220
221fn render_option_header_flag(out: &mut String, arg: &Arg) {
222 let short = arg
223 .get_short()
224 .map(|short| roff_escape(&short.to_string()))
225 .map(|short| format!("\\fB\\-{short}\\fR"));
226 let long = arg
227 .get_long()
228 .map(roff_escape)
229 .map(|long| format!("\\fB\\-\\-{long}\\fR"));
230 let aliases = arg
231 .get_visible_aliases()
232 .into_iter()
233 .flatten()
234 .map(roff_escape)
235 .map(|alias| format!("\\fB\\-\\-{alias}\\fR"));
236 let header = short.into_iter().chain(long).chain(aliases).join(", ");
237 if arg.get_action().takes_values() {
238 let value_str = render_value_hint(arg);
239 writeln!(out, "{header} {value_str}").unwrap();
240 } else {
241 writeln!(out, "{header}").unwrap();
242 }
243}
244
245fn render_value_hint(arg: &Arg) -> String {
246 let value_part = arg
247 .get_value_names()
248 .map(<[_]>::iter)
249 .map(|names| names.map(|name| name.as_str()))
250 .map(Vec::from_iter)
251 .unwrap_or_else(|| vec![arg.get_id().as_str()])
252 .into_iter()
253 .map(roff_escape)
254 .map(|name| format!("\\fI<{name}>\\fR"))
255 .join(" ");
256 let defaults = arg
257 .get_default_values()
258 .iter()
259 .map(|value| value.to_string_lossy())
260 .map(|value| roff_escape(&value))
261 .join(", ");
262 let hide_defaults = defaults.is_empty()
263 || arg.is_hide_default_value_set()
264 || matches!(arg.get_action(), ArgAction::SetTrue);
265 if hide_defaults {
266 value_part
267 } else {
268 format!("{value_part} [default: {defaults}]")
269 }
270}
271
272fn render_possible_values(out: &mut String, arg: &Arg) {
273 if arg.is_hide_possible_values_set() {
274 return;
275 }
276 if matches!(
277 arg.get_action(),
278 ArgAction::SetTrue | ArgAction::SetFalse | ArgAction::Count
279 ) {
280 return;
281 }
282 let possible_values: Vec<_> = arg
283 .get_possible_values()
284 .into_iter()
285 .filter(|value| !value.is_hide_set())
286 .collect();
287 if possible_values.is_empty() {
288 return;
289 }
290 let flag = arg
291 .get_long()
292 .map(roff_escape)
293 .map(|long| format!("\\-\\-{long}"))
294 .unwrap_or_default();
295 out.push_str(".RS\n");
296 for value in &possible_values {
297 let name = roff_escape(value.get_name());
298 let help = value
299 .get_help()
300 .map(|help| format!("\n{}", roff_escape(&help.to_string())))
301 .unwrap_or_default();
302 writeln!(out, ".TP\n\\fB{flag} {name}\\fR{help}").unwrap();
303 }
304 out.push_str(".RE\n");
305}
306
307fn render_conflicts(out: &mut String, command: &Command, arg: &Arg, conflict_map: &ConflictMap) {
308 let arg_id = arg.get_id().as_str();
309 let conflicts = conflict_map
310 .get(arg_id)
311 .into_iter()
312 .flatten()
313 .filter_map(|id| resolve_flag_name(command, id))
314 .join(", ");
315 if !conflicts.is_empty() {
316 writeln!(out, ".RS\n.PP\nCannot be used with {conflicts}.\n.RE").unwrap();
317 }
318}
319
320fn render_examples_section(out: &mut String, command: &Command) {
321 let text = match command.get_after_long_help() {
322 Some(text) => text.to_string(),
323 None => return,
324 };
325 let mut lines = text.lines();
326 let mut has_examples = false;
327 for line in lines.by_ref() {
328 if line.trim() == "Examples:" {
329 has_examples = true;
330 break;
331 }
332 }
333 if !has_examples {
334 return;
335 }
336 out.push_str(".SH EXAMPLES\n");
337 for line in lines {
338 let line = line.trim();
339 if line.is_empty() {
340 continue;
341 }
342 if let Some(example_command) = line.strip_prefix("$ ") {
343 writeln!(out, ".nf\n\\fB$ {}\\fR\n.fi", roff_escape(example_command)).unwrap();
344 } else {
345 writeln!(out, ".TP\n{}", roff_escape(line)).unwrap();
346 }
347 }
348}
349
350fn render_version_section(out: &mut String, command: &Command) {
351 if let Some(version) = command.get_version() {
352 writeln!(out, ".SH VERSION\nv{version}").unwrap();
353 }
354}