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: for<'enc> Utf8Encoding<'enc>> {
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 for<'enc> T: Utf8Encoding<'enc>,
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 fn cmd(&self, program: impl AsRef<Utf8Path<T>>) -> Cmd<'_, T> {
88 Cmd {
89 shell: self,
90 prog: program.as_ref().to_owned(),
91 args: Vec::new(),
92 env_changes: Vec::new(),
93 ignore_status: false,
94 stdin_contents: Vec::new(),
95 ignore_stdout: false,
96 ignore_stderr: false,
97 }
98 }
99}
100
101pub struct Cmd<'a, T: for<'enc> Utf8Encoding<'enc>> {
103 shell: &'a Shell<'a, T>,
104 prog: Utf8PathBuf<T>,
105 args: Vec<String>,
106 env_changes: Vec<EnvChange>,
107 ignore_status: bool,
108 stdin_contents: Vec<u8>,
109 ignore_stdout: bool,
110 ignore_stderr: bool,
111}
112
113enum EnvChange {
114 Set(String, String),
115 Remove(String),
116 Clear,
117}
118
119impl<'a, T: for<'enc> Utf8Encoding<'enc>> Cmd<'a, T> {
120 pub fn arg<P: AsRef<str>>(mut self, arg: P) -> Self {
122 self.args.push(arg.as_ref().to_owned());
123 self
124 }
125
126 pub fn args<I>(mut self, args: I) -> Self
128 where
129 I: IntoIterator,
130 I::Item: AsRef<str>,
131 {
132 for it in args.into_iter() {
133 self = self.arg(it.as_ref());
134 }
135 self
136 }
137
138 #[doc(hidden)]
140 pub fn __extend_arg(mut self, arg_fragment: impl AsRef<str>) -> Self {
141 match self.args.last_mut() {
142 Some(last_arg) => last_arg.push_str(arg_fragment.as_ref()),
143 None => {
144 let mut prog = std::mem::take(&mut self.prog).into_string();
145 prog.push_str(arg_fragment.as_ref());
146 self.prog = prog.into();
147 }
148 }
149 self
150 }
151
152 pub fn env(mut self, key: impl AsRef<str>, val: impl AsRef<str>) -> Self {
154 self.env_changes.push(EnvChange::Set(
155 key.as_ref().to_owned(),
156 val.as_ref().to_owned(),
157 ));
158 self
159 }
160
161 pub fn envs<I, K, V>(mut self, vars: I) -> Self
163 where
164 I: IntoIterator<Item = (K, V)>,
165 K: AsRef<str>,
166 V: AsRef<str>,
167 {
168 for (k, v) in vars.into_iter() {
169 self = self.env(k.as_ref(), v.as_ref());
170 }
171 self
172 }
173
174 pub fn env_remove(mut self, key: impl AsRef<str>) -> Self {
176 self.env_changes
177 .push(EnvChange::Remove(key.as_ref().to_owned()));
178 self
179 }
180
181 pub fn env_clear(mut self) -> Self {
183 self.env_changes.push(EnvChange::Clear);
184 self
185 }
186
187 pub fn ignore_status(mut self) -> Self {
191 self.ignore_status = true;
192 self
193 }
194
195 pub fn ignore_stdout(mut self) -> Self {
199 self.ignore_stdout = true;
200 self
201 }
202
203 pub fn ignore_stderr(mut self) -> Self {
207 self.ignore_stderr = true;
208 self
209 }
210
211 pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Self {
213 self.stdin_contents = stdin.as_ref().to_vec();
214 self
215 }
216
217 pub async fn run(&self) -> anyhow::Result<()> {
223 self.read_output().await?;
224 Ok(())
225 }
226
227 pub async fn read(&self) -> anyhow::Result<String> {
233 self.read_stream(false).await
234 }
235
236 pub async fn read_stderr(&self) -> anyhow::Result<String> {
242 self.read_stream(true).await
243 }
244
245 pub async fn output(&self) -> anyhow::Result<Output> {
250 self.read_output().await
251 }
252
253 fn command(&self) -> Command<'a> {
254 let mut command = self.shell.client.command(&self.prog);
255 command.args(&self.args);
256 command.current_dir(&self.shell.cwd);
257 for (name, value) in &self.shell.env {
258 command.env(name, value);
259 }
260 for change in &self.env_changes {
261 match change {
262 EnvChange::Set(name, value) => {
263 command.env(name, value);
264 }
265 EnvChange::Remove(name) => {
266 command.env_remove(name);
267 }
268 EnvChange::Clear => {
269 command.env_clear();
270 }
271 }
272 }
273 if self.ignore_stdout {
274 command.stdout(Stdio::null());
275 }
276 if self.ignore_stderr {
277 command.stderr(Stdio::null());
278 }
279 command
280 }
281
282 async fn read_stream(&self, read_stderr: bool) -> anyhow::Result<String> {
283 let output = self.read_output().await?;
284 let stream = if read_stderr {
285 output.stderr
286 } else {
287 output.stdout
288 };
289 let mut stream = String::from_utf8(stream).context("stream is not utf-8")?;
290 if stream.ends_with('\n') {
291 stream.pop();
292 }
293 if stream.ends_with('\r') {
294 stream.pop();
295 }
296 Ok(stream)
297 }
298
299 async fn read_output(&self) -> anyhow::Result<Output> {
300 let mut command = self.command();
301 if !self.ignore_stdout {
302 command.stdout(Stdio::piped());
303 }
304 if !self.ignore_stderr {
305 command.stderr(Stdio::piped());
306 }
307 if !self.stdin_contents.is_empty() {
308 command.stdin(Stdio::piped());
309 }
310 let mut child = command.spawn().await.context("failed to spawn child")?;
311
312 let stdin = child.stdin.take();
314 let copy_stdin = async move {
315 if let Some(mut stdin) = stdin {
316 stdin.write_all(&self.stdin_contents).await?;
317 }
318 anyhow::Ok(())
319 };
320
321 let wait = child.wait_with_output();
322
323 let (copy_r, wait_r) = (copy_stdin, wait).join().await;
324 let output = wait_r.context("failed to wait for child")?;
325 copy_r.context("failed to write stdin")?;
326
327 let out = String::from_utf8_lossy(&output.stdout);
328 tracing::info!(?out, "command stdout");
329
330 let err = String::from_utf8_lossy(&output.stderr);
331 tracing::info!(?err, "command stderr");
332
333 if !self.ignore_status && !output.status.success() {
334 anyhow::bail!("command failed: {}", output.status);
335 }
336
337 Ok(output)
338 }
339}
340
341#[macro_export]
352macro_rules! cmd {
353 ($sh:expr, $cmd:literal) => {{
354 let f = |prog| $sh.cmd(prog);
355 let cmd: $crate::shell::Cmd<'_, _> = $crate::shell::__cmd!(f $cmd);
356 cmd
357 }};
358}