1use std::ffi::OsString;
27use std::io::Write;
28use std::process::ExitCode;
29
30use crate::{aggregate, fanout, spawner};
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum UnknownFlag {
35 Short(char),
37 Long(String),
39}
40
41pub fn format_unknown_flag(flag: &UnknownFlag) -> String {
43 match flag {
44 UnknownFlag::Short(c) => format!("rusty-pee: invalid option -- '{c}'"),
45 UnknownFlag::Long(name) => format!("rusty-pee: unknown option -- '{name}'"),
46 }
47}
48
49pub fn format_spawn_failure(cmd: &str) -> String {
54 format!("pee: Can not open pipe to '{cmd}'")
55}
56
57pub fn run(argv: &[OsString]) -> ExitCode {
59 let parsed = match parse_argv(argv) {
60 Ok(p) => p,
61 Err(unk) => {
62 let msg = format_unknown_flag(&unk);
63 let _ = writeln!(std::io::stderr().lock(), "{msg}");
64 return ExitCode::from(2);
65 }
66 };
67
68 if parsed.commands.is_empty() {
70 use std::io::Read;
71 let stdin = std::io::stdin();
72 let mut handle = stdin.lock();
73 let mut buf = vec![0u8; crate::BUFSIZ];
74 loop {
75 match handle.read(&mut buf) {
76 Ok(0) => break,
77 Ok(_) => {}
78 Err(_) => return ExitCode::from(1),
79 }
80 }
81 return ExitCode::SUCCESS;
82 }
83
84 let mut children = Vec::with_capacity(parsed.commands.len());
87 for cmd in &parsed.commands {
88 match spawner::spawn_one(cmd) {
89 Ok(c) => children.push(c),
90 Err(_) => {
91 let _ = writeln!(std::io::stderr().lock(), "{}", format_spawn_failure(cmd));
92 for mut c in children.into_iter() {
94 let _ = c.kill();
95 let _ = c.wait();
96 }
97 return ExitCode::from(1);
98 }
99 }
100 }
101
102 let stdin = std::io::stdin();
104 let statuses = match fanout::run(stdin.lock(), children) {
105 Ok(s) => s,
106 Err(_) => return ExitCode::from(1),
107 };
108
109 let codes: Vec<i32> = statuses.iter().map(|s| s.code().unwrap_or(1)).collect();
111 let aggregated = aggregate::strict_or(&codes);
112 let byte = if (0..=255).contains(&aggregated) {
113 aggregated as u8
114 } else {
115 1u8
116 };
117 ExitCode::from(byte)
118}
119
120#[derive(Debug, Default)]
122struct StrictArgs {
123 commands: Vec<String>,
124}
125
126fn parse_argv(argv: &[OsString]) -> Result<StrictArgs, UnknownFlag> {
133 let mut out = StrictArgs::default();
134 let mut iter = argv.iter().skip(1);
135 let mut first_positional = true;
136
137 while let Some(arg) = iter.next() {
138 let s = arg.to_string_lossy();
139
140 if s == "--strict" || s == "--no-strict" {
142 continue;
143 }
144
145 if s == "--" {
147 for rest in iter.by_ref() {
148 out.commands.push(rest.to_string_lossy().into_owned());
149 }
150 break;
151 }
152
153 if first_positional && s == "completions" {
155 return Err(UnknownFlag::Long(String::from("completions")));
156 }
157
158 if let Some(rest) = s.strip_prefix("--") {
160 let flag_name = rest.split('=').next().unwrap_or(rest).to_string();
162 return Err(UnknownFlag::Long(flag_name));
163 }
164
165 if let Some(rest) = s.strip_prefix('-') {
167 if !rest.is_empty() {
168 let first = rest.chars().next().expect("non-empty after strip_prefix");
169 return Err(UnknownFlag::Short(first));
170 }
171 }
172
173 first_positional = false;
175 out.commands.push(s.into_owned());
176 }
177
178 Ok(out)
179}
180
181pub fn pre_scan_strict_flag(argv: &[OsString]) -> Option<bool> {
183 let mut chosen: Option<bool> = None;
184 for arg in argv.iter().skip(1) {
185 let s = arg.to_string_lossy();
186 if s == "--strict" {
187 chosen = Some(true);
188 } else if s == "--no-strict" {
189 chosen = Some(false);
190 } else if s == "--" {
191 break;
192 }
193 }
194 chosen
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 fn argv(parts: &[&str]) -> Vec<OsString> {
202 parts.iter().map(|s| OsString::from(*s)).collect()
203 }
204
205 #[test]
206 fn parse_no_flags_yields_no_commands() {
207 let r = parse_argv(&argv(&["pee"])).unwrap();
208 assert!(r.commands.is_empty());
209 }
210
211 #[test]
212 fn parse_two_commands() {
213 let r = parse_argv(&argv(&["pee", "wc -l", "grep foo"])).unwrap();
214 assert_eq!(r.commands, vec!["wc -l", "grep foo"]);
215 }
216
217 #[test]
218 fn parse_rejects_help() {
219 let err = parse_argv(&argv(&["pee", "--help"])).unwrap_err();
220 assert_eq!(err, UnknownFlag::Long(String::from("help")));
221 }
222
223 #[test]
224 fn parse_rejects_version() {
225 let err = parse_argv(&argv(&["pee", "--version"])).unwrap_err();
226 assert_eq!(err, UnknownFlag::Long(String::from("version")));
227 }
228
229 #[test]
230 fn parse_rejects_capture() {
231 let err = parse_argv(&argv(&["pee", "--capture"])).unwrap_err();
232 assert_eq!(err, UnknownFlag::Long(String::from("capture")));
233 }
234
235 #[test]
236 fn parse_rejects_completions_subcommand() {
237 let err = parse_argv(&argv(&["pee", "completions", "bash"])).unwrap_err();
238 assert_eq!(err, UnknownFlag::Long(String::from("completions")));
239 }
240
241 #[test]
242 fn parse_completions_after_other_positional_treated_as_command() {
243 let r = parse_argv(&argv(&["pee", "wc -l", "completions"])).unwrap();
246 assert_eq!(r.commands, vec!["wc -l", "completions"]);
247 }
248
249 #[test]
250 fn parse_rejects_unknown_long_flag() {
251 let err = parse_argv(&argv(&["pee", "--foo"])).unwrap_err();
252 assert_eq!(err, UnknownFlag::Long(String::from("foo")));
253 }
254
255 #[test]
256 fn parse_rejects_unknown_short_flag() {
257 let err = parse_argv(&argv(&["pee", "-x"])).unwrap_err();
258 assert_eq!(err, UnknownFlag::Short('x'));
259 }
260
261 #[test]
262 fn parse_first_unknown_wins() {
263 let err = parse_argv(&argv(&["pee", "-x", "--foo"])).unwrap_err();
264 assert_eq!(err, UnknownFlag::Short('x'));
265 let err = parse_argv(&argv(&["pee", "--foo", "-x"])).unwrap_err();
266 assert_eq!(err, UnknownFlag::Long(String::from("foo")));
267 }
268
269 #[test]
270 fn parse_double_dash_treats_rest_as_commands() {
271 let r = parse_argv(&argv(&["pee", "--", "--help", "-x"])).unwrap();
272 assert_eq!(
273 r.commands,
274 vec!["--help", "-x"],
275 "after `--` everything is a command"
276 );
277 }
278
279 #[test]
280 fn pre_scan_detects_strict() {
281 assert_eq!(
282 pre_scan_strict_flag(&argv(&["rusty-pee", "--strict"])),
283 Some(true)
284 );
285 }
286
287 #[test]
288 fn pre_scan_stops_at_double_dash() {
289 assert_eq!(
290 pre_scan_strict_flag(&argv(&["rusty-pee", "--", "--strict"])),
291 None
292 );
293 }
294
295 #[test]
296 fn format_unknown_short_matches_spec() {
297 assert_eq!(
298 format_unknown_flag(&UnknownFlag::Short('x')),
299 "rusty-pee: invalid option -- 'x'"
300 );
301 }
302
303 #[test]
304 fn format_unknown_long_matches_spec() {
305 assert_eq!(
306 format_unknown_flag(&UnknownFlag::Long(String::from("foo"))),
307 "rusty-pee: unknown option -- 'foo'"
308 );
309 }
310
311 #[test]
312 fn format_spawn_failure_uses_two_word_can_not() {
313 assert_eq!(
315 format_spawn_failure("nonexistent"),
316 "pee: Can not open pipe to 'nonexistent'"
317 );
318 }
319}