command_stream/
command_builder.rs1use crate::{CommandResult, Result, StdinOption};
3use std::collections::HashMap;
4use std::ffi::OsString;
5use std::path::PathBuf;
6use std::process::Stdio;
7use tokio::io::AsyncWriteExt;
8
9#[derive(Debug, Clone, Default)]
10pub enum CommandStdio {
11 #[default]
12 Pipe,
13 Inherit,
14 Ignore,
15}
16
17impl CommandStdio {
18 fn native(&self) -> Stdio {
19 match self {
20 Self::Pipe => Stdio::piped(),
21 Self::Inherit => Stdio::inherit(),
22 Self::Ignore => Stdio::null(),
23 }
24 }
25}
26
27#[derive(Debug, Clone)]
29pub struct CommandBuilder {
30 pub file: OsString,
31 pub arguments: Vec<OsString>,
32 cwd: Option<PathBuf>,
33 env: HashMap<OsString, OsString>,
34 stdout: CommandStdio,
35 stderr: CommandStdio,
36 stdin: StdinOption,
37}
38
39impl CommandBuilder {
40 pub fn new(file: impl Into<OsString>) -> Self {
41 Self {
42 file: file.into(),
43 arguments: Vec::new(),
44 cwd: None,
45 env: HashMap::new(),
46 stdout: CommandStdio::Pipe,
47 stderr: CommandStdio::Pipe,
48 stdin: StdinOption::Null,
49 }
50 }
51
52 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
53 self.arguments.push(arg.into());
54 self
55 }
56
57 pub fn args<I, S>(mut self, args: I) -> Self
58 where
59 I: IntoIterator<Item = S>,
60 S: Into<OsString>,
61 {
62 self.arguments.extend(args.into_iter().map(Into::into));
63 self
64 }
65
66 pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
67 self.cwd = Some(path.into());
68 self
69 }
70 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
71 self.env.insert(key.into(), value.into());
72 self
73 }
74 pub fn stdout(mut self, mode: CommandStdio) -> Self {
75 self.stdout = mode;
76 self
77 }
78 pub fn stderr(mut self, mode: CommandStdio) -> Self {
79 self.stderr = mode;
80 self
81 }
82 pub fn stdin(mut self, input: StdinOption) -> Self {
83 self.stdin = input;
84 self
85 }
86
87 pub async fn run(&self) -> Result<CommandResult> {
88 let mut command = tokio::process::Command::new(&self.file);
89 command
90 .args(&self.arguments)
91 .envs(&self.env)
92 .stdin(match &self.stdin {
93 StdinOption::Inherit => Stdio::inherit(),
94 StdinOption::Null => Stdio::null(),
95 _ => Stdio::piped(),
96 })
97 .stdout(self.stdout.native())
98 .stderr(self.stderr.native())
99 .kill_on_drop(true);
100 if let Some(cwd) = &self.cwd {
101 command.current_dir(cwd);
102 }
103 let mut child = command.spawn()?;
104 let input = child.stdin.take();
105 let write = async {
106 if let (Some(mut input), StdinOption::Content(data)) = (input, &self.stdin) {
107 if let Err(error) = input.write_all(data.as_bytes()).await {
108 if error.kind() != std::io::ErrorKind::BrokenPipe {
109 return Err(error);
110 }
111 }
112 }
113 Ok::<(), std::io::Error>(())
114 };
115 let (output, written) = tokio::join!(child.wait_with_output(), write);
116 written?;
117 let output = output?;
118 let code = output.status.code().unwrap_or(1);
119 #[cfg(unix)]
120 let code = {
121 use std::os::unix::process::ExitStatusExt;
122 output.status.signal().map_or(code, |signal| 128 + signal)
123 };
124 Ok(CommandResult::new(
125 String::from_utf8_lossy(&output.stdout).into_owned(),
126 String::from_utf8_lossy(&output.stderr).into_owned(),
127 code,
128 ))
129 }
130
131 pub async fn exit_code(&self) -> Result<i32> {
132 Ok(self.run().await?.code)
133 }
134
135 pub fn shell_string(&self) -> String {
137 std::iter::once(&self.file)
138 .chain(&self.arguments)
139 .map(|value| format!("'{}'", value.to_string_lossy().replace('\'', "'\\''")))
140 .collect::<Vec<_>>()
141 .join(" ")
142 }
143}
144
145pub fn fy(source: &str) -> String {
147 format!("#!/usr/bin/env node\nimport {{ command }} from 'command-stream';\nconst source = {};\nconst result = await command('sh', '-c', source, 'translated.sh', ...process.argv.slice(2)).with({{ mirror: true, capture: false, stdin: 'inherit' }}).run();\nprocess.exitCode = result.code;\n", serde_json::to_string(source).expect("string serialization"))
148}