Skip to main content

prettyping_rs/
cli.rs

1use std::ffi::OsString;
2
3use clap::error::ErrorKind;
4use clap::{ArgAction, CommandFactory, Parser};
5
6use crate::config::{self, Config, ConfigInput, DEFAULT_LAST};
7
8const CLI_AFTER_HELP: &str = "Compatibility notes:\n  - Removed legacy flags: --awkbin, --pingbin (hard error).\n  - Unsupported legacy flags: -f, -R, -q, -a (hard error).\n  - Legacy -v is accepted and ignored for compatibility.";
9
10const CLI_HELP_TEXT: &str = "Usage: prettyping [prettyping parameters] <host>\n\nThis is a Rust port of prettyping. It prints each ping response as a compact\ncharacter graph, with optional color and stats lines.\n\nprettyping parameters:\n  --[no]color        Enable/disable color output. (default: enabled)\n  --[no]multicolor   Enable/disable multi-color output. Has no effect if\n                       color is disabled. (default: enabled)\n  --[no]unicode      Enable/disable unicode characters. (default: enabled)\n  --[no]legend       Enable/disable the latency legend. (default: enabled)\n  --[no]globalstats  Enable/disable the global statistics line. (default: enabled)\n  --[no]recentstats  Enable/disable the last n statistics line. (default: enabled)\n  --[no]terminal     Force the output designed for a terminal. (default: auto)\n  --last <n>         Use the last n pings at the statistics line. (default: 60)\n  --columns <n>      Override auto-detection of terminal dimensions.\n  --lines <n>        Override auto-detection of terminal dimensions.\n  --rttmin <n>       Minimum RTT represented in the graph. (default: auto)\n  --rttmax <n>       Maximum RTT represented in the graph. (default: auto)\n\nping parameters handled by prettyping:\n  -4, --ipv4         Use IPv4 only.\n  -6, --ipv6         Use IPv6 only.\n  -c, --count <n>    Stop after sending n probes.\n  -i, --interval <s> Interval between probes in seconds.\n  -W, --timeout <s>  Per-probe timeout in seconds.\n  -s, --size <n>     Payload size in bytes.\n  -t, --ttl <n>      IP time-to-live (hop limit).\n\n";
11
12#[derive(Debug, Parser)]
13#[command(
14    name = "prettyping",
15    about = "Rust port of prettyping",
16    after_help = CLI_AFTER_HELP,
17    disable_version_flag = true
18)]
19struct RawCliArgs {
20    /// Destination host or IP
21    #[arg(value_name = "HOST")]
22    host: String,
23
24    // Kept prettyping flags
25    #[arg(long = "color", action = ArgAction::SetTrue)]
26    color: bool,
27    #[arg(long = "nocolor", action = ArgAction::SetTrue)]
28    nocolor: bool,
29    #[arg(long = "multicolor", action = ArgAction::SetTrue)]
30    multicolor: bool,
31    #[arg(long = "nomulticolor", action = ArgAction::SetTrue)]
32    nomulticolor: bool,
33    #[arg(long = "unicode", action = ArgAction::SetTrue)]
34    unicode: bool,
35    #[arg(long = "nounicode", action = ArgAction::SetTrue)]
36    nounicode: bool,
37    #[arg(long = "legend", action = ArgAction::SetTrue)]
38    legend: bool,
39    #[arg(long = "nolegend", action = ArgAction::SetTrue)]
40    nolegend: bool,
41    #[arg(long = "globalstats", action = ArgAction::SetTrue)]
42    globalstats: bool,
43    #[arg(long = "noglobalstats", action = ArgAction::SetTrue)]
44    noglobalstats: bool,
45    #[arg(long = "recentstats", action = ArgAction::SetTrue)]
46    recentstats: bool,
47    #[arg(long = "norecentstats", action = ArgAction::SetTrue)]
48    norecentstats: bool,
49    #[arg(long = "terminal", action = ArgAction::SetTrue)]
50    terminal: bool,
51    #[arg(long = "noterminal", action = ArgAction::SetTrue)]
52    noterminal: bool,
53    #[arg(long = "last", default_value_t = DEFAULT_LAST)]
54    last: u32,
55    #[arg(long = "columns")]
56    columns: Option<u16>,
57    #[arg(long = "lines")]
58    lines: Option<u16>,
59    #[arg(long = "rttmin")]
60    rttmin: Option<u32>,
61    #[arg(long = "rttmax")]
62    rttmax: Option<u32>,
63
64    // Native ping flags (explicitly handled in Rust)
65    #[arg(short = '4', long = "ipv4", action = ArgAction::SetTrue)]
66    ipv4: bool,
67    #[arg(short = '6', long = "ipv6", action = ArgAction::SetTrue)]
68    ipv6: bool,
69    #[arg(short = 'c', long = "count")]
70    count: Option<u32>,
71    #[arg(short = 'i', long = "interval")]
72    interval_secs: Option<f64>,
73    #[arg(short = 'W', long = "timeout")]
74    timeout_secs: Option<f64>,
75    #[arg(short = 's', long = "size")]
76    packet_size: Option<u32>,
77    #[arg(short = 't', long = "ttl")]
78    ttl: Option<u16>,
79
80    // Removed legacy passthrough flags - parsed only to emit explicit errors.
81    #[arg(long = "awkbin", value_name = "EXEC", hide = true)]
82    removed_awkbin: Option<String>,
83    #[arg(long = "pingbin", value_name = "EXEC", hide = true)]
84    removed_pingbin: Option<String>,
85
86    // Unsupported legacy flags - parsed only to emit explicit errors.
87    #[arg(short = 'a', action = ArgAction::SetTrue, hide = true)]
88    legacy_audible: bool,
89    #[arg(short = 'f', action = ArgAction::SetTrue, hide = true)]
90    legacy_flood: bool,
91    #[arg(short = 'R', action = ArgAction::SetTrue, hide = true)]
92    legacy_record_route: bool,
93    #[arg(short = 'q', action = ArgAction::SetTrue, hide = true)]
94    legacy_quiet: bool,
95
96    // Accepted legacy compatibility no-op.
97    #[arg(short = 'v', action = ArgAction::SetTrue, hide = true)]
98    legacy_verbose_ignored: bool,
99}
100
101pub fn parse_config_from_args<I, T>(args: I) -> Result<Config, clap::Error>
102where
103    I: IntoIterator<Item = T>,
104    T: Into<OsString>,
105{
106    let normalized = normalize_legacy_long_spellings(args);
107    if wants_help(&normalized) {
108        return Err(help_error());
109    }
110    let color = parse_toggle_override(&normalized, "--color", "--nocolor", true);
111    let multicolor = parse_toggle_override(&normalized, "--multicolor", "--nomulticolor", true);
112    let unicode = parse_toggle_override(&normalized, "--unicode", "--nounicode", true);
113    let legend = parse_toggle_override(&normalized, "--legend", "--nolegend", true);
114    let globalstats = parse_toggle_override(&normalized, "--globalstats", "--noglobalstats", true);
115    let recentstats = parse_toggle_override(&normalized, "--recentstats", "--norecentstats", true);
116    let terminal_override = parse_terminal_override(&normalized);
117
118    let raw = RawCliArgs::try_parse_from(normalized)?;
119
120    if raw.removed_awkbin.is_some() {
121        return Err(usage_error(
122            "--awkbin was removed in prettyping (pure Rust engine has no awk subprocess)",
123        ));
124    }
125    if raw.removed_pingbin.is_some() {
126        return Err(usage_error(
127            "--pingbin was removed in prettyping (pure Rust engine has no ping subprocess)",
128        ));
129    }
130
131    if raw.legacy_audible {
132        return Err(usage_error("unsupported legacy flag: -a"));
133    }
134    if raw.legacy_flood {
135        return Err(usage_error("unsupported legacy flag: -f"));
136    }
137    if raw.legacy_record_route {
138        return Err(usage_error("unsupported legacy flag: -R"));
139    }
140    if raw.legacy_quiet {
141        return Err(usage_error("unsupported legacy flag: -q"));
142    }
143
144    let _ = raw.legacy_verbose_ignored;
145
146    let input = ConfigInput {
147        host: raw.host,
148        color,
149        multicolor,
150        unicode,
151        legend,
152        globalstats,
153        recentstats,
154        terminal: terminal_override,
155        last: raw.last,
156        columns: raw.columns,
157        lines: raw.lines,
158        rttmin: raw.rttmin,
159        rttmax: raw.rttmax,
160        force_ipv4: raw.ipv4,
161        force_ipv6: raw.ipv6,
162        count: raw.count,
163        interval_secs: raw.interval_secs,
164        timeout_secs: raw.timeout_secs,
165        packet_size: raw.packet_size,
166        ttl: raw.ttl,
167    };
168
169    config::normalize(input).map_err(|err| usage_error(err.to_string()))
170}
171
172pub fn parse_config_from_env() -> Result<Config, clap::Error> {
173    parse_config_from_args(std::env::args_os())
174}
175
176fn usage_error(message: impl Into<String>) -> clap::Error {
177    let mut cmd = RawCliArgs::command();
178    cmd.error(ErrorKind::ValueValidation, message.into())
179}
180
181fn wants_help(args: &[OsString]) -> bool {
182    args.iter()
183        .any(|arg| arg == "--help" || arg == "-h" || arg == "-help" || arg == "--h")
184}
185
186fn help_error() -> clap::Error {
187    let mut out = String::new();
188    out.push_str(CLI_HELP_TEXT);
189    out.push_str(CLI_AFTER_HELP);
190    clap::Error::raw(ErrorKind::DisplayHelp, out)
191}
192
193fn parse_terminal_override(args: &[OsString]) -> Option<bool> {
194    parse_toggle_override_optional(args, "--terminal", "--noterminal")
195}
196
197fn parse_toggle_override(
198    args: &[OsString],
199    enabled_flag: &str,
200    disabled_flag: &str,
201    default: bool,
202) -> bool {
203    parse_toggle_override_optional(args, enabled_flag, disabled_flag).unwrap_or(default)
204}
205
206fn parse_toggle_override_optional(
207    args: &[OsString],
208    enabled_flag: &str,
209    disabled_flag: &str,
210) -> Option<bool> {
211    args.iter().skip(1).fold(None, |current, arg| {
212        if arg == enabled_flag {
213            Some(true)
214        } else if arg == disabled_flag {
215            Some(false)
216        } else {
217            current
218        }
219    })
220}
221
222fn normalize_legacy_long_spellings<I, T>(args: I) -> Vec<OsString>
223where
224    I: IntoIterator<Item = T>,
225    T: Into<OsString>,
226{
227    args.into_iter()
228        .map(Into::into)
229        .map(|arg| {
230            arg.to_str()
231                .and_then(rewrite_legacy_single_dash_flag)
232                .map(OsString::from)
233                .unwrap_or(arg)
234        })
235        .collect()
236}
237
238fn rewrite_legacy_single_dash_flag(flag: &str) -> Option<&'static str> {
239    match flag {
240        "-help" => Some("--help"),
241        "-color" => Some("--color"),
242        "-nocolor" => Some("--nocolor"),
243        "-multicolor" => Some("--multicolor"),
244        "-nomulticolor" => Some("--nomulticolor"),
245        "-unicode" => Some("--unicode"),
246        "-nounicode" => Some("--nounicode"),
247        "-legend" => Some("--legend"),
248        "-nolegend" => Some("--nolegend"),
249        "-globalstats" => Some("--globalstats"),
250        "-noglobalstats" => Some("--noglobalstats"),
251        "-recentstats" => Some("--recentstats"),
252        "-norecentstats" => Some("--norecentstats"),
253        "-terminal" => Some("--terminal"),
254        "-noterminal" => Some("--noterminal"),
255        "-last" => Some("--last"),
256        "-columns" => Some("--columns"),
257        "-lines" => Some("--lines"),
258        "-rttmin" => Some("--rttmin"),
259        "-rttmax" => Some("--rttmax"),
260        "-awkbin" => Some("--awkbin"),
261        "-pingbin" => Some("--pingbin"),
262        _ => None,
263    }
264}