1#![feature(never_type)]
2#![feature(try_trait_v2)]
3#![feature(try_trait_v2_residual)]
4
5use std::{
6 fmt::Debug,
7 io,
8 process::{Child, Output, Termination as _T},
9};
10
11use exit_safely::Termination;
12use try_v2::{Try, Try_ConvertResult};
13
14pub mod commands;
15
16#[derive(Debug, Termination, Try, Try_ConvertResult, PartialEq, PartialOrd, Eq, Ord)]
17#[repr(u8)]
18#[must_use]
19pub enum Exit<T: _T> {
20 Ok(T) = 0,
21 Error(String) = 1,
22 InvocationError(String) = 2,
23 IO(String) = 3,
24}
25
26impl Exit<()> {
27 fn message(&self) -> &str {
28 match self {
29 Exit::Ok(_) => "",
30 Exit::Error(m) => m,
31 Exit::InvocationError(m) => m,
32 Exit::IO(m) => m,
33 }
34 }
35
36 fn replace_message(self, msg: String) -> Option<Self> {
37 match self {
38 Exit::Ok(_) => None,
39 Exit::Error(_) => Some(Exit::Error(msg)),
40 Exit::InvocationError(_) => Some(Exit::InvocationError(msg)),
41 Exit::IO(_) => Some(Exit::IO(msg)),
42 }
43 }
44}
45
46impl FromIterator<Exit<()>> for Exit<()> {
47 fn from_iter<I: IntoIterator<Item = Exit<()>>>(iter: I) -> Self {
48 let mut msg = String::new();
49 iter.into_iter()
50 .filter_map(|e| {
51 if let Exit::Ok(_) = e {
52 None
53 } else {
54 msg.push_str(e.message());
55 msg.push('\n');
56 Some(e)
57 }
58 })
59 .min()
60 .and_then(|e| e.replace_message(msg))
61 .unwrap_or(Exit::Ok(()))
62 }
63}
64
65impl<T: _T> From<clap::Error> for Exit<T> {
66 fn from(e: clap::Error) -> Self {
67 Self::InvocationError(e.to_string())
68 }
69}
70
71#[derive(Debug)]
72pub struct Cmd {
73 pub name: &'static str,
74 pub result: Result<Output, io::Error>,
75}
76
77trait CmdExt {
78 fn into_cmd(self, name: &'static str) -> Cmd;
79}
80
81impl CmdExt for Result<Output, io::Error> {
82 fn into_cmd(self, name: &'static str) -> Cmd {
83 Cmd { name, result: self }
84 }
85}
86
87impl From<Cmd> for Exit<()> {
89 fn from(cmd: Cmd) -> Self {
90 match cmd.result {
91 Ok(output) => {
92 if output.status.success() {
93 println!("{}: OK", cmd.name);
94 Self::Ok(())
95 } else {
96 let stdout = String::from_utf8_lossy(&output.stdout);
97 let stderr = String::from_utf8_lossy(&output.stderr);
98 Self::Error(format!(
99 "====== {} exited with {} ======\n-- stdout: --\n{}\n\n-- stderr: --\n{}",
100 cmd.name, output.status, stdout, stderr
101 ))
102 }
103 }
104 Err(e) => {
105 let msg = format!("{} failed: {}", cmd.name, e);
106 Self::IO(msg)
107 }
108 }
109 }
110}
111
112#[derive(Debug)]
113pub struct Spawned {
114 pub name: &'static str,
115 pub child: Result<Child, io::Error>,
116}
117
118impl Spawned {
119 pub fn wait(self) -> Cmd {
120 match self.child {
121 Ok(child) => child.wait_with_output().into_cmd(self.name),
122 Err(e) => Cmd {
123 name: self.name,
124 result: Err(e),
125 },
126 }
127 }
128}
129
130trait SpawnedExt {
131 fn into_spawned(self, name: &'static str) -> Spawned;
132}
133
134impl SpawnedExt for Result<Child, io::Error> {
135 fn into_spawned(self, name: &'static str) -> Spawned {
136 Spawned { name, child: self }
137 }
138}
139
140impl From<Vec<Spawned>> for Exit<()> {
141 fn from(spawns: Vec<Spawned>) -> Self {
142 spawns
143 .into_iter()
144 .map(|spawn| spawn.wait())
145 .map(Exit::from)
146 .collect()
147 }
148}
149
150impl From<Spawned> for Exit<()> {
151 fn from(spawn: Spawned) -> Self {
152 spawn.wait().into()
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use std::process::Command;
159
160 use super::*;
161
162 #[test]
163 fn exit_from_404() {
164 let splat: Cmd = Command::new("splat").output().into_cmd("splat");
165 assert_eq!(splat.name, "splat");
166 assert!(
167 matches!(splat.result, Result::Err(ref e) if matches!(e.kind(), io::ErrorKind::NotFound))
168 );
169 let exit: Exit<()> = Exit::from(splat);
170 let Exit::IO(ref msg) = exit else {
171 panic!("not an IO2")
172 };
173 eprintln!("{}", msg);
174 assert!(msg.starts_with("splat failed: "));
175 }
176
177 #[test]
178 fn collect_exit() {
179 let exits = [
180 Exit::Ok(()),
181 Exit::IO("one".to_string()),
182 Exit::Error("two".to_string()),
183 Exit::Error("three".to_string()),
184 ];
185 let exit: Exit<()> = exits.into_iter().collect();
186 let expected = "one\ntwo\nthree\n";
187 dbg!(&exit);
188 assert!(matches!(exit, Exit::Error(s) if s == expected));
189 }
190}