rust_args_parser/
help.rs

1use crate::util::strip_ansi_len;
2use crate::{CmdSpec, Env};
3use core::fmt::Write;
4
5#[cfg(feature = "color")]
6mod ansi {
7    pub const RESET: &str = "\x1b[0m";
8    pub const BOLD: &str = "\x1b[1m";
9    pub const TITLE: &str = "\x1b[4;37m"; // section titles
10    pub const OPT_LABEL: &str = "\x1b[0;94m"; // option labels
11    pub const POS_LABEL: &str = "\x1b[0;93m"; // positional labels
12    pub const METAVAR: &str = "\x1b[0;96m"; // metavars
13    pub const COMMAND: &str = "\x1b[0;95m"; // command names
14    pub const BRIGHT_WHITE: &str = "\x1b[0;97m";
15}
16
17#[cfg(feature = "color")]
18fn paint_title(s: &str) -> String {
19    format!("{}{}{}{}:", ansi::BOLD, ansi::TITLE, s, ansi::RESET)
20}
21#[cfg(not(feature = "color"))]
22fn paint_title(s: &str) -> String {
23    s.to_string()
24}
25
26#[cfg(feature = "color")]
27fn paint_section(s: &str) -> String {
28    format!("{}{}{}:", ansi::TITLE, s, ansi::RESET)
29}
30#[cfg(not(feature = "color"))]
31fn paint_section(s: &str) -> String {
32    s.to_string()
33}
34
35#[cfg(feature = "color")]
36fn paint_option(s: &str) -> String {
37    format!("{}{}{}", ansi::OPT_LABEL, s, ansi::RESET)
38}
39#[cfg(not(feature = "color"))]
40fn paint_option(s: &str) -> String {
41    s.to_string()
42}
43
44#[cfg(feature = "color")]
45fn paint_positional(s: &str) -> String {
46    format!("{}{}{}", ansi::POS_LABEL, s, ansi::RESET)
47}
48#[cfg(not(feature = "color"))]
49fn paint_positional(s: &str) -> String {
50    s.to_string()
51}
52
53#[cfg(feature = "color")]
54fn paint_metavar(s: &str) -> String {
55    format!("{}{}{}", ansi::METAVAR, s, ansi::RESET)
56}
57#[cfg(not(feature = "color"))]
58fn paint_metavar(s: &str) -> String {
59    s.to_string()
60}
61
62#[cfg(feature = "color")]
63fn paint_command(s: &str) -> String {
64    format!("{}{}{}", ansi::COMMAND, s, ansi::RESET)
65}
66#[cfg(not(feature = "color"))]
67fn paint_command(s: &str) -> String {
68    s.to_string()
69}
70
71#[must_use]
72pub fn render_help<Ctx: ?Sized>(env: &Env, cmd: &CmdSpec<'_, Ctx>) -> String {
73    render_help_with_path(env, &[], cmd)
74}
75
76fn print_usage<Ctx: ?Sized>(out_buf: &mut String, path: &[&str], cmd: &CmdSpec<'_, Ctx>) {
77    use crate::spec::PosCardinality;
78    let mut out = String::new();
79    let is_root = path.len() <= 1;
80    let _ = writeln!(out, "{}", paint_title("Usage"));
81    let bin_name = (*path.first().unwrap_or(&"")).to_string();
82    #[cfg(feature = "color")]
83    let _ = write!(out, "  {}{}{}{}", ansi::BRIGHT_WHITE, ansi::BOLD, bin_name, ansi::RESET);
84    #[cfg(not(feature = "color"))]
85    let _ = write!(out, "  {}", bin_name);
86    for command in path.iter().skip(1) {
87        let _ = write!(out, " {}", paint_command(command));
88    }
89    if !cmd.get_opts().is_empty() || is_root {
90        #[cfg(feature = "color")]
91        let _ = write!(out, " {}[options]{}", ansi::OPT_LABEL, ansi::RESET);
92        #[cfg(not(feature = "color"))]
93        let _ = write!(out, " [options]");
94    }
95    if !cmd.get_subcommands().is_empty() {
96        #[cfg(feature = "color")]
97        let _ = write!(out, " {}<command>{}", ansi::COMMAND, ansi::RESET);
98        #[cfg(not(feature = "color"))]
99        let _ = write!(out, " <command>");
100    }
101    for p in cmd.get_positionals() {
102        let name = p.get_name();
103        let (req, ellip) = match p.get_cardinality() {
104            PosCardinality::One { .. } => (p.is_required(), false),
105            PosCardinality::Many => (p.is_required(), true),
106            PosCardinality::Range { min, max } => (min > 0, max > 1),
107        };
108        let token = if ellip { format!("{name}...") } else { name.to_string() };
109        if req {
110            #[cfg(feature = "color")]
111            let _ = write!(out, " {}<{token}>{}", ansi::POS_LABEL, ansi::RESET);
112            #[cfg(not(feature = "color"))]
113            let _ = write!(out, " <{token}>");
114        } else {
115            #[cfg(feature = "color")]
116            let _ = write!(out, " {}[{token}]{}", ansi::POS_LABEL, ansi::RESET);
117            #[cfg(not(feature = "color"))]
118            let _ = write!(out, " [{token}]");
119        }
120    }
121    let _ = writeln!(out_buf, "{out}\n");
122}
123
124/// Render help with **strict column alignment** based on the *longest* label in the section.
125#[allow(clippy::too_many_lines)]
126#[must_use]
127pub fn render_help_with_path<Ctx: ?Sized>(
128    env: &Env,
129    path: &[&str],
130    cmd: &CmdSpec<'_, Ctx>,
131) -> String {
132    let mut out = String::new();
133    if let Some(h) = cmd.get_help() {
134        let _ = writeln!(out, "{h}\n");
135    }
136
137    print_usage(&mut out, path, cmd);
138    let mut rows: Vec<(Vec<String>, Option<&str>, String)> = Vec::new();
139    let is_root = path.len() <= 1;
140
141    if env.auto_help {
142        rows.push((
143            vec!["-h".into(), "--help".into()],
144            None,
145            String::from("Show this help and exit"),
146        ));
147    }
148
149    if is_root {
150        if env.version.is_some() {
151            rows.push((
152                vec!["-V".into(), "--version".into()],
153                None,
154                String::from("Show version and exit"),
155            ));
156        }
157        if env.author.is_some() {
158            rows.push((
159                vec!["-A".into(), "--author".into()],
160                None,
161                String::from("Show author and exit"),
162            ));
163        }
164    }
165
166    // User‑defined options
167    for o in cmd.get_opts() {
168        let mut lab = vec![];
169        let mut meta: Option<&str> = None;
170        if let Some(s) = o.get_short() {
171            lab.push(format!("-{s}"));
172        }
173        if let Some(l) = o.get_long() {
174            lab.push(format!("--{l}"));
175        }
176        if let Some(mv) = o.get_metavar() {
177            meta = Some(mv);
178        }
179        let mut desc: Vec<String> = vec![];
180        if let Some(h) = o.get_help() {
181            desc.push(h.to_string());
182        }
183        if let Some(env) = o.get_env() {
184            desc.push(format!("Env: {env}"));
185        }
186        if let Some(d) = o.get_default() {
187            desc.push(format!("Default: {d:?}"));
188        }
189        let desc = desc.join("; ");
190        rows.push((lab, meta, desc));
191    }
192
193    if !rows.is_empty() {
194        let _ = writeln!(out, "{}", paint_section("Options"));
195        let max_raw = rows
196            .iter()
197            .map(|(opts, pos, _)| opts.join(", ").len() + pos.map_or(0, |s| s.len() + 1))
198            .max()
199            .unwrap_or(0);
200        let desc_col = 2 + max_raw + 2; // "  " + label + "  "
201        for (lab, pos, desc) in rows {
202            let mut painted =
203                lab.into_iter().map(|s| paint_option(&s)).collect::<Vec<String>>().join(", ");
204            if let Some(pos) = pos {
205                painted.push_str(format!(" {}", paint_metavar(pos)).as_str());
206            }
207            let raw = strip_ansi_len(&painted);
208            let pad = max_raw + (painted.len() - raw);
209            let _ = write!(out, "  {painted:pad$}  ");
210            wrap_after(&mut out, &desc, desc_col, env.wrap_cols);
211        }
212    }
213    // Arguments
214    if !cmd.get_positionals().is_empty() {
215        let _ = writeln!(out, "\n{}", paint_section("Arguments"));
216        let mut prow_labels: Vec<(String, usize, String)> = Vec::new();
217        let mut max_raw = 0usize;
218        for p in cmd.get_positionals() {
219            let lab = paint_positional(p.get_name());
220            let raw = strip_ansi_len(&lab);
221            max_raw = max_raw.max(raw);
222            prow_labels.push((lab, raw, p.get_help().unwrap_or("").to_string()));
223        }
224        let desc_col = 2 + max_raw + 2;
225        for (lab, raw, desc) in prow_labels {
226            let pad = max_raw + (lab.len() - raw);
227            let _ = write!(out, "  {lab:pad$}  ");
228            wrap_after(&mut out, &desc, desc_col, env.wrap_cols);
229        }
230    }
231    // Commands
232    if !cmd.get_subcommands().is_empty() {
233        let _ = writeln!(out, "\n{}", paint_section("Commands"));
234        let mut crow_labels: Vec<(String, usize, String)> = Vec::new();
235        let mut max_raw = 0usize;
236        for sc in cmd.get_subcommands() {
237            let name = sc.get_name();
238            let mut lab = vec![paint_command(name)];
239            for alias in sc.get_aliases() {
240                lab.push(paint_command(alias));
241            }
242            let lab = lab.join(", ");
243            let raw = strip_ansi_len(&lab);
244            max_raw = max_raw.max(raw);
245            crow_labels.push((lab, raw, sc.get_help().unwrap_or("").to_string()));
246        }
247        let desc_col = 2 + max_raw + 2;
248        for (lab, raw, desc) in crow_labels {
249            let pad = max_raw + (lab.len() - raw);
250            let _ = write!(out, "  {lab:pad$}  ");
251            wrap_after(&mut out, &desc, desc_col, env.wrap_cols);
252        }
253    }
254    out
255}
256
257/// Wrap `text` after the already‑printed label. Subsequent lines start at `start_col`.
258fn wrap_after(out: &mut String, text: &str, start_col: usize, wrap: usize) {
259    if text.is_empty() {
260        let _ = writeln!(out);
261        return;
262    }
263    if wrap == 0 {
264        let _ = writeln!(out, "{text}");
265        return;
266    }
267    let mut col = start_col;
268    let mut first = true;
269    for word in text.split_whitespace() {
270        let wlen = word.len();
271        let add = usize::from(!first);
272        if col + add + wlen > wrap && col > start_col {
273            let _ = writeln!(out);
274            let _ = write!(out, "{}", " ".repeat(start_col));
275            col = start_col;
276            first = true;
277        }
278        if !first {
279            let _ = write!(out, " ");
280            col += 1;
281        }
282        let _ = write!(out, "{word}");
283        col += wlen;
284        first = false;
285    }
286    let _ = writeln!(out);
287}