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 #[arg(value_name = "HOST")]
22 host: String,
23
24 #[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 #[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 #[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 #[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 #[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}