command_stream/
command_builder.rs1use crate::{CommandResult, Result, StdinOption};
3use std::ffi::OsString;
4use std::path::PathBuf;
5use std::process::Stdio;
6use tokio::io::AsyncWriteExt;
7
8#[derive(Debug, Clone, Default)]
9pub enum CommandStdio {
10 #[default]
11 Pipe,
12 Inherit,
13 Ignore,
14}
15
16impl CommandStdio {
17 fn native(&self) -> Stdio {
18 match self {
19 Self::Pipe => Stdio::piped(),
20 Self::Inherit => Stdio::inherit(),
21 Self::Ignore => Stdio::null(),
22 }
23 }
24}
25
26#[derive(Debug, Clone)]
28pub struct CommandBuilder {
29 pub file: OsString,
30 pub arguments: Vec<OsString>,
31 cwd: Option<PathBuf>,
32 env: Vec<(OsString, Option<OsString>)>,
35 clear_env: bool,
36 stdout: CommandStdio,
37 stderr: CommandStdio,
38 stdin: StdinOption,
39}
40
41impl CommandBuilder {
42 pub fn new(file: impl Into<OsString>) -> Self {
43 Self {
44 file: file.into(),
45 arguments: Vec::new(),
46 cwd: None,
47 env: Vec::new(),
48 clear_env: false,
49 stdout: CommandStdio::Pipe,
50 stderr: CommandStdio::Pipe,
51 stdin: StdinOption::Null,
52 }
53 }
54
55 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
56 self.arguments.push(arg.into());
57 self
58 }
59
60 pub fn args<I, S>(mut self, args: I) -> Self
61 where
62 I: IntoIterator<Item = S>,
63 S: Into<OsString>,
64 {
65 self.arguments.extend(args.into_iter().map(Into::into));
66 self
67 }
68
69 pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
70 self.cwd = Some(path.into());
71 self
72 }
73 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
74 self.env.push((key.into(), Some(value.into())));
75 self
76 }
77 pub fn envs<I, K, V>(mut self, variables: I) -> Self
78 where
79 I: IntoIterator<Item = (K, V)>,
80 K: Into<OsString>,
81 V: Into<OsString>,
82 {
83 for (key, value) in variables {
84 self = self.env(key, value);
85 }
86 self
87 }
88 pub fn env_remove(mut self, key: impl Into<OsString>) -> Self {
89 self.env.push((key.into(), None));
90 self
91 }
92 pub fn env_clear(mut self) -> Self {
93 self.env.clear();
94 self.clear_env = true;
95 self
96 }
97 pub fn stdout(mut self, mode: CommandStdio) -> Self {
98 self.stdout = mode;
99 self
100 }
101 pub fn stderr(mut self, mode: CommandStdio) -> Self {
102 self.stderr = mode;
103 self
104 }
105 pub fn stdin(mut self, input: StdinOption) -> Self {
106 self.stdin = input;
107 self
108 }
109
110 pub fn pipe(self, transform: impl FnOnce(Self) -> Self) -> Self {
112 transform(self)
113 }
114
115 fn native_command(&self) -> std::process::Command {
116 let mut command = std::process::Command::new(&self.file);
117 command
118 .args(&self.arguments)
119 .stdin(match &self.stdin {
120 StdinOption::Inherit => Stdio::inherit(),
121 StdinOption::Null => Stdio::null(),
122 _ => Stdio::piped(),
123 })
124 .stdout(self.stdout.native())
125 .stderr(self.stderr.native());
126 if self.clear_env {
127 command.env_clear();
128 }
129 for (key, value) in &self.env {
130 if let Some(value) = value {
131 command.env(key, value);
132 } else {
133 command.env_remove(key);
134 }
135 }
136 if let Some(cwd) = &self.cwd {
137 command.current_dir(cwd);
138 }
139 command
140 }
141
142 pub async fn run(&self) -> Result<CommandResult> {
143 let mut command = tokio::process::Command::from(self.native_command());
144 command.kill_on_drop(true);
145 let mut child = command.spawn()?;
146 let input = child.stdin.take();
147 let write = async {
148 if let (Some(mut input), StdinOption::Content(data)) = (input, &self.stdin) {
149 if let Err(error) = input.write_all(data.as_bytes()).await {
150 if error.kind() != std::io::ErrorKind::BrokenPipe {
151 return Err(error);
152 }
153 }
154 }
155 Ok::<(), std::io::Error>(())
156 };
157 let (output, written) = tokio::join!(child.wait_with_output(), write);
158 written?;
159 Ok(Self::result(output?))
160 }
161
162 pub fn run_sync(&self) -> Result<CommandResult> {
164 let mut child = self.native_command().spawn()?;
165 let input = child.stdin.take();
166 let output = std::thread::scope(|scope| {
167 let writer = scope.spawn(|| {
168 if let (Some(mut input), StdinOption::Content(data)) = (input, &self.stdin) {
169 use std::io::Write;
170 if let Err(error) = input.write_all(data.as_bytes()) {
171 if error.kind() != std::io::ErrorKind::BrokenPipe {
172 return Err(error);
173 }
174 }
175 }
176 Ok::<(), std::io::Error>(())
177 });
178 let output = child.wait_with_output();
179 writer
180 .join()
181 .map_err(|_| std::io::Error::other("command stdin writer panicked"))??;
182 output
183 })?;
184 Ok(Self::result(output))
185 }
186
187 fn result(output: std::process::Output) -> CommandResult {
188 let code = output.status.code().unwrap_or(1);
189 #[cfg(unix)]
190 let code = {
191 use std::os::unix::process::ExitStatusExt;
192 output.status.signal().map_or(code, |signal| 128 + signal)
193 };
194 CommandResult::new(
195 String::from_utf8_lossy(&output.stdout).into_owned(),
196 String::from_utf8_lossy(&output.stderr).into_owned(),
197 code,
198 )
199 }
200
201 pub async fn exit_code(&self) -> Result<i32> {
202 Ok(self.run().await?.code)
203 }
204
205 pub fn shell_string(&self) -> String {
207 std::iter::once(&self.file)
208 .chain(&self.arguments)
209 .map(|value| format!("'{}'", value.to_string_lossy().replace('\'', "'\\''")))
210 .collect::<Vec<_>>()
211 .join(" ")
212 }
213}
214
215pub fn fy(source: &str) -> String {
217 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"))
218}