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 stderr = String::from_utf8_lossy(&output.stderr);
97 Self::Error(stderr.to_string())
98 }
99 }
100 Err(e) => {
101 let msg = format!("{} failed: {}", cmd.name, e);
102 Self::IO(msg)
103 }
104 }
105 }
106}
107
108#[derive(Debug)]
109pub struct Spawned {
110 pub name: &'static str,
111 pub child: Result<Child, io::Error>,
112}
113
114impl Spawned {
115 pub fn wait(self) -> Cmd {
116 match self.child {
117 Ok(child) => child.wait_with_output().into_cmd(self.name),
118 Err(e) => Cmd {
119 name: self.name,
120 result: Err(e),
121 },
122 }
123 }
124}
125
126trait SpawnedExt {
127 fn into_spawned(self, name: &'static str) -> Spawned;
128}
129
130impl SpawnedExt for Result<Child, io::Error> {
131 fn into_spawned(self, name: &'static str) -> Spawned {
132 Spawned { name, child: self }
133 }
134}
135
136impl From<Vec<Spawned>> for Exit<()> {
137 fn from(spawns: Vec<Spawned>) -> Self {
138 spawns
139 .into_iter()
140 .map(|spawn| spawn.wait())
141 .map(Exit::from)
142 .collect()
143 }
144}
145
146impl From<Spawned> for Exit<()> {
147 fn from(spawn: Spawned) -> Self {
148 spawn.wait().into()
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use std::process::Command;
155
156 use super::*;
157
158 #[test]
159 fn exit_from_404() {
160 let splat: Cmd = Command::new("splat").output().into_cmd("splat");
161 assert_eq!(splat.name, "splat");
162 assert!(
163 matches!(splat.result, Result::Err(ref e) if matches!(e.kind(), io::ErrorKind::NotFound))
164 );
165 let exit: Exit<()> = Exit::from(splat);
166 let Exit::IO(ref msg) = exit else {
167 panic!("not an IO2")
168 };
169 eprintln!("{}", msg);
170 assert!(msg.starts_with("splat failed: "));
171 }
172
173 #[test]
174 fn collect_exit() {
175 let exits = [
176 Exit::Ok(()),
177 Exit::IO("one".to_string()),
178 Exit::Error("two".to_string()),
179 Exit::Error("three".to_string()),
180 ];
181 let exit: Exit<()> = exits.into_iter().collect();
182 let expected = "one\ntwo\nthree\n";
183 dbg!(&exit);
184 assert!(matches!(exit, Exit::Error(s) if s == expected));
185 }
186}