1use crate::{StringError, bait::ResultExt, bog::BogOkExt, ebog};
4use cfg_if::cfg_if;
5use log::{debug, trace};
6use std::{
7 env,
8 ffi::{OsStr, OsString},
9 path::Path,
10 process::{Child, ChildStdout, Command, Stdio, exit},
11 sync::LazyLock,
12};
13
14#[easy_ext::ext(ChildExt)]
15impl Child {
16 pub fn wait_for_code(&mut self) -> i32 {
17 if let Some(status) = self.wait()._elog() {
18 status.code().unwrap_or(1)
19 } else {
20 1
21 }
22 }
23}
24
25#[easy_ext::ext(CommandExt)]
26impl Command {
27 pub fn from_script(script: &str) -> Self {
31 let (shell, arg) = &*SHELL;
32
33 let mut ret = Command::new(shell);
34
35 ret.arg(arg).arg(script).arg(""); ret
38 }
39
40 pub fn with_arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
41 self.arg(arg);
42 self
43 }
44
45 pub fn with_args<I, S>(mut self, args: I) -> Self
46 where
47 I: IntoIterator<Item = S>,
48 S: AsRef<OsStr>,
49 {
50 self.args(args);
51 self
52 }
53
54 pub fn display(&self) -> String {
57 std::iter::once(self.get_program())
58 .chain(self.get_args())
59 .map(|s| s.to_string_lossy())
60 .collect::<Vec<_>>()
61 .join(" ")
62 }
63
64 pub fn detach(&mut self) -> &mut Self {
67 cfg_if! {
68 if #[cfg(unix)] {
69 use std::os::unix::process::CommandExt;
70
71 unsafe {
72 self.pre_exec(|| {
73 match libc::fork() {
89 -1 => (),
90 0 => (),
91 _ => libc::_exit(0),
92 }
93
94 if libc::setsid() == -1 {
95 }
97 Ok(())
98 });
99 }
100 } else if #[cfg(windows)] {
101 use std::os::windows::process::CommandExt;
102
103 const DETACHED_PROCESS: u32 = 0x00000008;
104 const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
105
106 self.creation_flags(
107 DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
109 );
110 } else {
111 log::info!("Failed to detach: unsupported platform")
112 }
113 }
114
115 self
116 }
117
118 pub fn spawn_detached(&mut self) -> Option<Child> {
122 let ep = format!("Failed to spawn: {}", self.display());
123 debug!("Spawning detached: {self:?}");
124
125 self.stdin(Stdio::null())
126 .stdout(Stdio::null())
127 .stderr(Stdio::null())
128 .detach();
129
130 self.spawn().prefix(&ep)._ebog()
131 }
132
133 pub fn spawn_piped(&mut self) -> Result<ChildStdout, StringError> {
136 trace!("Spawning piped: {self:?}");
137
138 match self
139 .stdin(Stdio::null())
140 .stdout(Stdio::piped())
141 .stderr(Stdio::null())
142 .spawn()
143 .prefix(&format!("Failed to spawn: {}", self.display()))?
144 .stdout
145 .take()
146 {
147 Some(s) => Ok(s),
148 None => Err(format!("No stdout for {}.", self.display()).into()), }
150 }
151
152 pub fn read_to_string(&mut self) -> Result<String, StringError> {
155 trace!("Collecting output: {self:?}");
156
157 let output = self
158 .stdin(Stdio::null())
159 .stdout(Stdio::piped())
160 .stderr(Stdio::piped())
161 .output()
162 .prefix(&format!("Failed to spawn: {}", self.display()))?;
163
164 if !output.status.success() {
165 let stderr = String::from_utf8_lossy(&output.stderr);
166
167 let code = output
168 .status
169 .code()
170 .map_or("None".to_string(), |c| c.to_string());
171
172 return Err(format!(
173 "{} exited with code {}: {}",
174 self.display(),
175 code,
176 stderr.trim()
177 )
178 .into());
179 }
180
181 let stdout = String::from_utf8(output.stdout)
182 .map_err(|_| format!("{} produced non-UTF8 stdout", self.display()))?;
183
184 Ok(stdout)
185 }
186
187 pub fn success(&mut self) -> bool {
189 self.stdout(Stdio::null())
190 .stderr(Stdio::null())
191 .status()
192 .map(|status| status.success())
193 .unwrap_or(false)
194 }
195
196 pub fn _exec(&mut self) -> ! {
200 debug!("Becoming: {self:?}");
201
202 #[cfg(not(windows))]
203 {
204 use std::os::unix::process::CommandExt;
206 let err = self.exec();
207
208 ebog!("Could not exec {}: {err}", self.display());
209 exit(1)
210 }
211
212 #[cfg(windows)]
213 {
214 match self.status() {
215 Ok(status) => exit(
216 status
217 .code()
218 .unwrap_or(if status.success() { 0 } else { 1 }),
219 ),
220 Err(err) => {
221 ebog!("Could not exec {}: {err}", self.display());
222 exit(1)
223 }
224 }
225 }
226 }
227
228 pub fn _spawn(&mut self) -> Option<Child> {
230 trace!("Spawning: {self:?}");
231 self.spawn()
232 .prefix(&format!("Could not spawn: {}", self.display()))
233 ._elog()
234 }
235}
236
237pub fn format_sh_command(inputs: &[impl AsRef<OsStr>], double: bool) -> OsString {
241 let mut cmd = OsString::new();
242 let mut first = true;
243
244 for arg in inputs {
245 if !first {
246 cmd.push(" ");
247 }
248 first = false;
249
250 let os = arg.as_ref();
251
252 match os.to_str() {
253 Some(s) => {
254 if double {
255 let escaped = s
256 .replace("\\", "\\\\")
257 .replace('\"', "\\\"")
258 .replace("$", "\\$");
259 cmd.push("\"");
260 cmd.push(escaped);
261 cmd.push("\"");
262 } else {
263 let escaped = s.replace('\'', "'\\''");
264 cmd.push("'");
265 cmd.push(escaped);
266 cmd.push("'");
267 }
268 }
269 None => {
270 cmd.push(os);
272 }
273 }
274 }
275
276 cmd
277}
278
279pub fn display_sh_prog_and_args(prog: impl AsRef<OsStr>, args: &[impl AsRef<OsStr>]) -> String {
280 format_sh_command(
281 &{
282 let mut i = vec![prog.as_ref()];
283 i.extend(args.iter().map(|x| x.as_ref()));
284 i
285 },
286 false,
287 )
288 .to_string_lossy()
289 .to_string()
290}
291
292pub static SHELL: LazyLock<(String, String)> = LazyLock::new(|| {
294 #[cfg(windows)]
295 {
296 let path = env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string());
297
298 let lower = path.to_lowercase();
299 let flag = if lower.contains("powershell") || lower.contains("pwsh") {
300 "-Command".to_string()
301 } else {
302 "/C".to_string()
303 };
304 (path, flag)
305 }
306 #[cfg(unix)]
307 {
308 let path = env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
309 let flag = "-c".to_string();
310 log::debug!("SHELL: {}, {}", path, flag);
311 (path, flag)
312 }
313});
314
315pub fn current_shell() -> String {
318 #[cfg(unix)]
319 {
320 if let Ok(shell) = env::var("SHELL") {
321 if let Some(name) = Path::new(&shell).file_name().and_then(|n| n.to_str()) {
322 return name.to_ascii_lowercase();
323 }
324 }
325 }
326
327 #[cfg(windows)]
328 {
329 if let Ok(ps) = env::var("PSModulePath") {
331 if !ps.is_empty() {
332 return "pwsh".into();
333 }
334 }
335
336 if let Ok(comspec) = env::var("COMSPEC") {
337 if let Some(name) = Path::new(&comspec).file_name().and_then(|n| n.to_str()) {
338 return name.to_ascii_lowercase();
339 }
340 }
341 }
342
343 String::new()
344}
345
346pub fn tty_or_inherit() -> Stdio {
347 if let Ok(mut tty) = std::fs::File::open("/dev/tty") {
348 let _ = std::io::Write::flush(&mut tty); Stdio::from(tty)
350 } else {
351 log::error!("Failed to open /dev/tty");
352 Stdio::inherit()
353 }
354}
355
356use std::{cell::RefCell, collections::HashMap};
357thread_local! {
358 static HAS_CACHE: RefCell<HashMap<String, bool>> = RefCell::new(HashMap::new());
359}
360
361pub fn has(name: &str) -> bool {
362 HAS_CACHE.with(|cache| {
363 let mut cache = cache.borrow_mut();
364 if let Some(&found) = cache.get(name) {
365 found
366 } else {
367 let found = which::which(name).is_ok();
368 cache.insert(name.to_owned(), found);
369 found
370 }
371 })
372}
373
374pub type EnvVars = Vec<(String, String)>;
376
377#[macro_export]
378macro_rules! env_vars {
379 ($( $name:expr => $value:expr ),* $(,)?) => {
380 Vec::<(String, String)>::from([
381 $( ($name.into(), $value.into()) ),*
382 ]
383 )
384 };
385}