1#[doc(hidden)]
11pub use xshell_macros::__cmd;
12
13use crate::PipetteClient;
14use crate::process::Command;
15use crate::process::Output;
16use crate::process::Stdio;
17use anyhow::Context;
18use futures::AsyncWriteExt;
19use futures_concurrency::future::Join;
20use std::collections::HashMap;
21use typed_path::Utf8Encoding;
22use typed_path::Utf8Path;
23use typed_path::Utf8PathBuf;
24use typed_path::Utf8UnixEncoding;
25use typed_path::Utf8WindowsEncoding;
26
27pub struct Shell<'a, T: Utf8Encoding> {
31 client: &'a PipetteClient,
32 cwd: Utf8PathBuf<T>,
33 env: HashMap<String, String>,
34}
35
36pub type WindowsShell<'a> = Shell<'a, Utf8WindowsEncoding>;
38
39pub type UnixShell<'a> = Shell<'a, Utf8UnixEncoding>;
41
42impl<'a> UnixShell<'a> {
43 pub(crate) fn new(client: &'a PipetteClient) -> Self {
44 Self {
45 client,
46 cwd: Utf8PathBuf::from("/"),
47 env: HashMap::new(),
48 }
49 }
50}
51
52impl<'a> WindowsShell<'a> {
53 pub(crate) fn new(client: &'a PipetteClient) -> Self {
54 Self {
55 client,
56 cwd: Utf8PathBuf::from("C:/"),
57 env: HashMap::new(),
58 }
59 }
60}
61
62impl<T> Shell<'_, T>
63where
64 T: Utf8Encoding,
65{
66 fn path(&self, path: impl AsRef<Utf8Path<T>>) -> Utf8PathBuf<T> {
67 self.cwd.join(path)
68 }
69
70 pub fn change_dir(&mut self, path: impl AsRef<Utf8Path<T>>) {
74 self.cwd = self.path(path);
75 }
76
77 pub async fn read_file(&self, path: impl AsRef<Utf8Path<T>>) -> anyhow::Result<String> {
79 let path = self.path(path);
80 let v = self.client.read_file(path.as_str()).await?;
81 String::from_utf8(v).with_context(|| format!("file '{}' is not valid utf-8", path.as_str()))
82 }
83
84 pub async fn read_file_raw(&self, path: impl AsRef<Utf8Path<T>>) -> anyhow::Result<Vec<u8>> {
86 let path = self.path(path);
87 let v = self.client.read_file(path.as_str()).await?;
88 Ok(v)
89 }
90
91 pub fn cmd(&self, program: impl AsRef<Utf8Path<T>>) -> Cmd<'_, T> {
95 Cmd {
96 shell: self,
97 prog: program.as_ref().to_owned(),
98 args: Vec::new(),
99 env_changes: Vec::new(),
100 ignore_status: false,
101 stdin_contents: Vec::new(),
102 ignore_stdout: false,
103 ignore_stderr: false,
104 }
105 }
106}
107
108pub struct Cmd<'a, T: Utf8Encoding> {
110 shell: &'a Shell<'a, T>,
111 prog: Utf8PathBuf<T>,
112 args: Vec<String>,
113 env_changes: Vec<EnvChange>,
114 ignore_status: bool,
115 stdin_contents: Vec<u8>,
116 ignore_stdout: bool,
117 ignore_stderr: bool,
118}
119
120enum EnvChange {
121 Set(String, String),
122 Remove(String),
123 Clear,
124}
125
126impl<'a, T: Utf8Encoding> Cmd<'a, T> {
127 pub fn arg<P: AsRef<str>>(mut self, arg: P) -> Self {
129 self.args.push(arg.as_ref().to_owned());
130 self
131 }
132
133 pub fn args<I>(mut self, args: I) -> Self
135 where
136 I: IntoIterator,
137 I::Item: AsRef<str>,
138 {
139 for it in args.into_iter() {
140 self = self.arg(it.as_ref());
141 }
142 self
143 }
144
145 #[doc(hidden)]
147 pub fn __extend_arg(mut self, arg_fragment: impl AsRef<str>) -> Self {
148 match self.args.last_mut() {
149 Some(last_arg) => last_arg.push_str(arg_fragment.as_ref()),
150 None => {
151 let mut prog = std::mem::take(&mut self.prog).into_string();
152 prog.push_str(arg_fragment.as_ref());
153 self.prog = prog.into();
154 }
155 }
156 self
157 }
158
159 pub fn env(mut self, key: impl AsRef<str>, val: impl AsRef<str>) -> Self {
161 self.env_changes.push(EnvChange::Set(
162 key.as_ref().to_owned(),
163 val.as_ref().to_owned(),
164 ));
165 self
166 }
167
168 pub fn envs<I, K, V>(mut self, vars: I) -> Self
170 where
171 I: IntoIterator<Item = (K, V)>,
172 K: AsRef<str>,
173 V: AsRef<str>,
174 {
175 for (k, v) in vars.into_iter() {
176 self = self.env(k.as_ref(), v.as_ref());
177 }
178 self
179 }
180
181 pub fn env_remove(mut self, key: impl AsRef<str>) -> Self {
183 self.env_changes
184 .push(EnvChange::Remove(key.as_ref().to_owned()));
185 self
186 }
187
188 pub fn env_clear(mut self) -> Self {
190 self.env_changes.push(EnvChange::Clear);
191 self
192 }
193
194 pub fn ignore_status(mut self) -> Self {
198 self.ignore_status = true;
199 self
200 }
201
202 pub fn ignore_stdout(mut self) -> Self {
206 self.ignore_stdout = true;
207 self
208 }
209
210 pub fn ignore_stderr(mut self) -> Self {
214 self.ignore_stderr = true;
215 self
216 }
217
218 pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Self {
220 self.stdin_contents = stdin.as_ref().to_vec();
221 self
222 }
223
224 pub async fn run(&self) -> anyhow::Result<()> {
230 self.read_output().await?;
231 Ok(())
232 }
233
234 pub async fn read(&self) -> anyhow::Result<String> {
240 self.read_stream(false).await
241 }
242
243 pub async fn read_stderr(&self) -> anyhow::Result<String> {
249 self.read_stream(true).await
250 }
251
252 pub async fn output(&self) -> anyhow::Result<Output> {
257 self.read_output().await
258 }
259
260 fn command(&self) -> Command<'a> {
261 let mut command = self.shell.client.command(&self.prog);
262 command.args(&self.args);
263 command.current_dir(&self.shell.cwd);
264 for (name, value) in &self.shell.env {
265 command.env(name, value);
266 }
267 for change in &self.env_changes {
268 match change {
269 EnvChange::Set(name, value) => {
270 command.env(name, value);
271 }
272 EnvChange::Remove(name) => {
273 command.env_remove(name);
274 }
275 EnvChange::Clear => {
276 command.env_clear();
277 }
278 }
279 }
280 if self.ignore_stdout {
281 command.stdout(Stdio::null());
282 }
283 if self.ignore_stderr {
284 command.stderr(Stdio::null());
285 }
286 command
287 }
288
289 async fn read_stream(&self, read_stderr: bool) -> anyhow::Result<String> {
290 let output = self.read_output().await?;
291 let stream = if read_stderr {
292 output.stderr
293 } else {
294 output.stdout
295 };
296 let mut stream = String::from_utf8(stream).context("stream is not utf-8")?;
297 if stream.ends_with('\n') {
298 stream.pop();
299 }
300 if stream.ends_with('\r') {
301 stream.pop();
302 }
303 Ok(stream)
304 }
305
306 async fn read_output(&self) -> anyhow::Result<Output> {
307 let mut command = self.command();
308 if !self.ignore_stdout {
309 command.stdout(Stdio::piped());
310 }
311 if !self.ignore_stderr {
312 command.stderr(Stdio::piped());
313 }
314 if !self.stdin_contents.is_empty() {
315 command.stdin(Stdio::piped());
316 }
317 let mut child = command.spawn().await.context("failed to spawn child")?;
318
319 let stdin = child.stdin.take();
321 let copy_stdin = async move {
322 if let Some(mut stdin) = stdin {
323 stdin.write_all(&self.stdin_contents).await?;
324 }
325 anyhow::Ok(())
326 };
327
328 let wait = child.wait_with_output();
329
330 let (copy_r, wait_r) = (copy_stdin, wait).join().await;
331 let output = wait_r.context("failed to wait for child")?;
332 copy_r.context("failed to write stdin")?;
333
334 let out = String::from_utf8_lossy(&output.stdout);
335 tracing::info!(?out, "command stdout");
336
337 let err = String::from_utf8_lossy(&output.stderr);
338 tracing::info!(?err, "command stderr");
339
340 if !self.ignore_status && !output.status.success() {
341 anyhow::bail!("command failed: {}", output.status);
342 }
343
344 Ok(output)
345 }
346}
347
348#[macro_export]
359macro_rules! cmd {
360 ($sh:expr, $cmd:literal) => {{
361 let f = |prog| $sh.cmd(prog);
362 let cmd: $crate::shell::Cmd<'_, _> = $crate::shell::__cmd!(f $cmd);
363 cmd
364 }};
365}