1 | //! xshell is a swiss-army knife for writing cross-platform "bash" scripts in |
2 | //! Rust. |
3 | //! |
4 | //! It doesn't use the shell directly, but rather re-implements parts of |
5 | //! scripting environment in Rust. The intended use-case is various bits of glue |
6 | //! code, which could be written in bash or python. The original motivation is |
7 | //! [`xtask`](https://github.com/matklad/cargo-xtask) development. |
8 | //! |
9 | //! Here's a quick example: |
10 | //! |
11 | //! ```no_run |
12 | //! use xshell::{Shell, cmd}; |
13 | //! |
14 | //! let sh = Shell::new()?; |
15 | //! let branch = "main" ; |
16 | //! let commit_hash = cmd!(sh, "git rev-parse {branch}" ).read()?; |
17 | //! # Ok::<(), xshell::Error>(()) |
18 | //! ``` |
19 | //! |
20 | //! **Goals:** |
21 | //! |
22 | //! * Ergonomics and DWIM ("do what I mean"): `cmd!` macro supports |
23 | //! interpolation, writing to a file automatically creates parent directories, |
24 | //! etc. |
25 | //! * Reliability: no [shell injection] by construction, good error messages |
26 | //! with file paths, non-zero exit status is an error, independence of the |
27 | //! host environment, etc. |
28 | //! * Frugality: fast compile times, few dependencies, low-tech API. |
29 | //! |
30 | //! # Guide |
31 | //! |
32 | //! For a short API overview, let's implement a script to clone a github |
33 | //! repository and publish it as a crates.io crate. The script will do the |
34 | //! following: |
35 | //! |
36 | //! 1. Clone the repository. |
37 | //! 2. `cd` into the repository's directory. |
38 | //! 3. Run the tests. |
39 | //! 4. Create a git tag using a version from `Cargo.toml`. |
40 | //! 5. Publish the crate with an optional `--dry-run`. |
41 | //! |
42 | //! Start with the following skeleton: |
43 | //! |
44 | //! ```no_run |
45 | //! use xshell::{cmd, Shell}; |
46 | //! |
47 | //! fn main() -> anyhow::Result<()> { |
48 | //! let sh = Shell::new()?; |
49 | //! |
50 | //! Ok(()) |
51 | //! } |
52 | //! ``` |
53 | //! |
54 | //! Only two imports are needed -- the [`Shell`] struct the and [`cmd!`] macro. |
55 | //! By convention, an instance of a [`Shell`] is stored in a variable named |
56 | //! `sh`. All the API is available as methods, so a short name helps here. For |
57 | //! "scripts", the [`anyhow`](https://docs.rs/anyhow) crate is a great choice |
58 | //! for an error-handling library. |
59 | //! |
60 | //! Next, clone the repository: |
61 | //! |
62 | //! ```no_run |
63 | //! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); |
64 | //! cmd!(sh, "git clone https://github.com/matklad/xshell.git" ).run()?; |
65 | //! # Ok::<(), xshell::Error>(()) |
66 | //! ``` |
67 | //! |
68 | //! The [`cmd!`] macro provides a convenient syntax for creating a command -- |
69 | //! the [`Cmd`] struct. The [`Cmd::run`] method runs the command as if you |
70 | //! typed it into the shell. The whole program outputs: |
71 | //! |
72 | //! ```console |
73 | //! $ git clone https://github.com/matklad/xshell.git |
74 | //! Cloning into 'xshell'... |
75 | //! remote: Enumerating objects: 676, done. |
76 | //! remote: Counting objects: 100% (220/220), done. |
77 | //! remote: Compressing objects: 100% (123/123), done. |
78 | //! remote: Total 676 (delta 106), reused 162 (delta 76), pack-reused 456 |
79 | //! Receiving objects: 100% (676/676), 136.80 KiB | 222.00 KiB/s, done. |
80 | //! Resolving deltas: 100% (327/327), done. |
81 | //! ``` |
82 | //! |
83 | //! Note that the command itself is echoed to stderr (the `$ git ...` bit in the |
84 | //! output). You can use [`Cmd::quiet`] to override this behavior: |
85 | //! |
86 | //! ```no_run |
87 | //! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); |
88 | //! cmd!(sh, "git clone https://github.com/matklad/xshell.git" ) |
89 | //! .quiet() |
90 | //! .run()?; |
91 | //! # Ok::<(), xshell::Error>(()) |
92 | //! ``` |
93 | //! |
94 | //! To make the code more general, let's use command interpolation to extract |
95 | //! the username and the repository: |
96 | //! |
97 | //! ```no_run |
98 | //! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); |
99 | //! let user = "matklad" ; |
100 | //! let repo = "xshell" ; |
101 | //! cmd!(sh, "git clone https://github.com/{user}/{repo}.git" ).run()?; |
102 | //! # Ok::<(), xshell::Error>(()) |
103 | //! ``` |
104 | //! |
105 | //! Note that the `cmd!` macro parses the command string at compile time, so you |
106 | //! don't have to worry about escaping the arguments. For example, the following |
107 | //! command "touches" a single file whose name is `contains a space`: |
108 | //! |
109 | //! ```no_run |
110 | //! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); |
111 | //! let file = "contains a space" ; |
112 | //! cmd!(sh, "touch {file}" ).run()?; |
113 | //! # Ok::<(), xshell::Error>(()) |
114 | //! ``` |
115 | //! |
116 | //! Next, `cd` into the folder you have just cloned: |
117 | //! |
118 | //! ```no_run |
119 | //! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); |
120 | //! # let repo = "xshell" ; |
121 | //! sh.change_dir(repo); |
122 | //! ``` |
123 | //! |
124 | //! Each instance of [`Shell`] has a current directory, which is independent of |
125 | //! the process-wide [`std::env::current_dir`]. The same applies to the |
126 | //! environment. |
127 | //! |
128 | //! Next, run the tests: |
129 | //! |
130 | //! ```no_run |
131 | //! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); |
132 | //! let test_args = ["-Zunstable-options" , "--report-time" ]; |
133 | //! cmd!(sh, "cargo test -- {test_args...}" ).run()?; |
134 | //! # Ok::<(), xshell::Error>(()) |
135 | //! ``` |
136 | //! |
137 | //! Note how the so-called splat syntax (`...`) is used to interpolate an |
138 | //! iterable of arguments. |
139 | //! |
140 | //! Next, read the Cargo.toml so that we can fetch crate' declared version: |
141 | //! |
142 | //! ```no_run |
143 | //! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); |
144 | //! let manifest = sh.read_file("Cargo.toml" )?; |
145 | //! # Ok::<(), xshell::Error>(()) |
146 | //! ``` |
147 | //! |
148 | //! [`Shell::read_file`] works like [`std::fs::read_to_string`], but paths are |
149 | //! relative to the current directory of the [`Shell`]. Unlike [`std::fs`], |
150 | //! error messages are much more useful. For example, if there isn't a |
151 | //! `Cargo.toml` in the repository, the error message is: |
152 | //! |
153 | //! ```text |
154 | //! Error: failed to read file `xshell/Cargo.toml`: no such file or directory (os error 2) |
155 | //! ``` |
156 | //! |
157 | //! `xshell` doesn't implement string processing utils like `grep`, `sed` or |
158 | //! `awk` -- there's no need to, built-in language features work fine, and it's |
159 | //! always possible to pull extra functionality from crates.io. |
160 | //! |
161 | //! To extract the `version` field from Cargo.toml, [`str::split_once`] is |
162 | //! enough: |
163 | //! |
164 | //! ```no_run |
165 | //! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); |
166 | //! let manifest = sh.read_file("Cargo.toml" )?; |
167 | //! let version = manifest |
168 | //! .split_once("version = \"" ) |
169 | //! .and_then(|it| it.1.split_once(' \"' )) |
170 | //! .map(|it| it.0) |
171 | //! .ok_or_else(|| anyhow::format_err!("can't find version field in the manifest" ))?; |
172 | //! |
173 | //! cmd!(sh, "git tag {version}" ).run()?; |
174 | //! # Ok::<(), anyhow::Error>(()) |
175 | //! ``` |
176 | //! |
177 | //! The splat (`...`) syntax works with any iterable, and in Rust options are |
178 | //! iterable. This means that `...` can be used to implement optional arguments. |
179 | //! For example, here's how to pass `--dry-run` when *not* running in CI: |
180 | //! |
181 | //! ```no_run |
182 | //! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); |
183 | //! let dry_run = if sh.var("CI" ).is_ok() { None } else { Some("--dry-run" ) }; |
184 | //! cmd!(sh, "cargo publish {dry_run...}" ).run()?; |
185 | //! # Ok::<(), xshell::Error>(()) |
186 | //! ``` |
187 | //! |
188 | //! Putting everything altogether, here's the whole script: |
189 | //! |
190 | //! ```no_run |
191 | //! use xshell::{cmd, Shell}; |
192 | //! |
193 | //! fn main() -> anyhow::Result<()> { |
194 | //! let sh = Shell::new()?; |
195 | //! |
196 | //! let user = "matklad" ; |
197 | //! let repo = "xshell" ; |
198 | //! cmd!(sh, "git clone https://github.com/{user}/{repo}.git" ).run()?; |
199 | //! sh.change_dir(repo); |
200 | //! |
201 | //! let test_args = ["-Zunstable-options" , "--report-time" ]; |
202 | //! cmd!(sh, "cargo test -- {test_args...}" ).run()?; |
203 | //! |
204 | //! let manifest = sh.read_file("Cargo.toml" )?; |
205 | //! let version = manifest |
206 | //! .split_once("version = \"" ) |
207 | //! .and_then(|it| it.1.split_once(' \"' )) |
208 | //! .map(|it| it.0) |
209 | //! .ok_or_else(|| anyhow::format_err!("can't find version field in the manifest" ))?; |
210 | //! |
211 | //! cmd!(sh, "git tag {version}" ).run()?; |
212 | //! |
213 | //! let dry_run = if sh.var("CI" ).is_ok() { None } else { Some("--dry-run" ) }; |
214 | //! cmd!(sh, "cargo publish {dry_run...}" ).run()?; |
215 | //! |
216 | //! Ok(()) |
217 | //! } |
218 | //! ``` |
219 | //! |
220 | //! `xshell` itself uses a similar script to automatically publish oneself to |
221 | //! crates.io when the version in Cargo.toml changes: |
222 | //! |
223 | //! <https://github.com/matklad/xshell/blob/master/examples/ci.rs> |
224 | //! |
225 | //! # Maintenance |
226 | //! |
227 | //! Minimum Supported Rust Version: 1.63.0. MSRV bump is not considered semver |
228 | //! breaking. MSRV is updated conservatively. |
229 | //! |
230 | //! The crate isn't comprehensive yet, but this is a goal. You are hereby |
231 | //! encouraged to submit PRs with missing functionality! |
232 | //! |
233 | //! # Related Crates |
234 | //! |
235 | //! [`duct`] is a crate for heavy-duty process herding, with support for |
236 | //! pipelines. |
237 | //! |
238 | //! Most of what this crate provides can be open-coded using |
239 | //! [`std::process::Command`] and [`std::fs`]. If you only need to spawn a |
240 | //! single process, using `std` is probably better (but don't forget to check |
241 | //! the exit status!). |
242 | //! |
243 | //! [`duct`]: https://github.com/oconnor663/duct.rs |
244 | //! [shell injection]: |
245 | //! https://en.wikipedia.org/wiki/Code_injection#Shell_injection |
246 | //! |
247 | //! The [`dax`](https://github.com/dsherret/dax) library for Deno shares the overall philosophy with |
248 | //! `xshell`, but is much more thorough and complete. If you don't need Rust, use `dax`. |
249 | //! |
250 | //! # Implementation Notes |
251 | //! |
252 | //! The design is heavily inspired by the Julia language: |
253 | //! |
254 | //! * [Shelling Out |
255 | //! Sucks](https://julialang.org/blog/2012/03/shelling-out-sucks/) |
256 | //! * [Put This In Your |
257 | //! Pipe](https://julialang.org/blog/2013/04/put-this-in-your-pipe/) |
258 | //! * [Running External |
259 | //! Programs](https://docs.julialang.org/en/v1/manual/running-external-programs/) |
260 | //! * [Filesystem](https://docs.julialang.org/en/v1/base/file/) |
261 | //! |
262 | //! Smaller influences are the [`duct`] crate and Ruby's |
263 | //! [`FileUtils`](https://ruby-doc.org/stdlib-2.4.1/libdoc/fileutils/rdoc/FileUtils.html) |
264 | //! module. |
265 | //! |
266 | //! The `cmd!` macro uses a simple proc-macro internally. It doesn't depend on |
267 | //! helper libraries, so the fixed-cost impact on compile times is moderate. |
268 | //! Compiling a trivial program with `cmd!("date +%Y-%m-%d")` takes one second. |
269 | //! Equivalent program using only `std::process::Command` compiles in 0.25 |
270 | //! seconds. |
271 | //! |
272 | //! To make IDEs infer correct types without expanding proc-macro, it is wrapped |
273 | //! into a declarative macro which supplies type hints. |
274 | |
275 | #![deny (missing_debug_implementations)] |
276 | #![deny (missing_docs)] |
277 | #![deny (rust_2018_idioms)] |
278 | |
279 | mod error; |
280 | |
281 | use std::{ |
282 | cell::RefCell, |
283 | collections::HashMap, |
284 | env::{self, current_dir, VarError}, |
285 | ffi::{OsStr, OsString}, |
286 | fmt, fs, |
287 | io::{self, ErrorKind, Write}, |
288 | mem, |
289 | path::{Path, PathBuf}, |
290 | process::{Command, ExitStatus, Output, Stdio}, |
291 | sync::atomic::{AtomicUsize, Ordering}, |
292 | }; |
293 | |
294 | pub use crate::error::{Error, Result}; |
295 | #[doc (hidden)] |
296 | pub use xshell_macros::__cmd; |
297 | |
298 | /// Constructs a [`Cmd`] from the given string. |
299 | /// |
300 | /// # Examples |
301 | /// |
302 | /// Basic: |
303 | /// |
304 | /// ```no_run |
305 | /// # use xshell::{cmd, Shell}; |
306 | /// let sh = Shell::new()?; |
307 | /// cmd!(sh, "echo hello world" ).run()?; |
308 | /// # Ok::<(), xshell::Error>(()) |
309 | /// ``` |
310 | /// |
311 | /// Interpolation: |
312 | /// |
313 | /// ``` |
314 | /// # use xshell::{cmd, Shell}; let sh = Shell::new()?; |
315 | /// let greeting = "hello world" ; |
316 | /// let c = cmd!(sh, "echo {greeting}" ); |
317 | /// assert_eq!(c.to_string(), r#"echo "hello world""# ); |
318 | /// |
319 | /// let c = cmd!(sh, "echo '{greeting}'" ); |
320 | /// assert_eq!(c.to_string(), r#"echo {greeting}"# ); |
321 | /// |
322 | /// let c = cmd!(sh, "echo {greeting}!" ); |
323 | /// assert_eq!(c.to_string(), r#"echo "hello world!""# ); |
324 | /// |
325 | /// let c = cmd!(sh, "echo 'spaces '{greeting}' around'" ); |
326 | /// assert_eq!(c.to_string(), r#"echo "spaces hello world around""# ); |
327 | /// |
328 | /// # Ok::<(), xshell::Error>(()) |
329 | /// ``` |
330 | /// |
331 | /// Splat interpolation: |
332 | /// |
333 | /// ``` |
334 | /// # use xshell::{cmd, Shell}; let sh = Shell::new()?; |
335 | /// let args = ["hello" , "world" ]; |
336 | /// let c = cmd!(sh, "echo {args...}" ); |
337 | /// assert_eq!(c.to_string(), r#"echo hello world"# ); |
338 | /// |
339 | /// let arg1: Option<&str> = Some("hello" ); |
340 | /// let arg2: Option<&str> = None; |
341 | /// let c = cmd!(sh, "echo {arg1...} {arg2...}" ); |
342 | /// assert_eq!(c.to_string(), r#"echo hello"# ); |
343 | /// # Ok::<(), xshell::Error>(()) |
344 | /// ``` |
345 | #[macro_export ] |
346 | macro_rules! cmd { |
347 | ($sh:expr, $cmd:literal) => {{ |
348 | #[cfg(any())] // Trick rust analyzer into highlighting interpolated bits |
349 | format_args!($cmd); |
350 | let f = |prog| $sh.cmd(prog); |
351 | let cmd: $crate::Cmd = $crate::__cmd!(f $cmd); |
352 | cmd |
353 | }}; |
354 | } |
355 | |
356 | /// A `Shell` is the main API entry point. |
357 | /// |
358 | /// Almost all of the crate's functionality is available as methods of the |
359 | /// `Shell` object. |
360 | /// |
361 | /// `Shell` is a stateful object. It maintains a logical working directory and |
362 | /// an environment map. They are independent from process's |
363 | /// [`std::env::current_dir`] and [`std::env::var`], and only affect paths and |
364 | /// commands passed to the [`Shell`]. |
365 | /// |
366 | /// |
367 | /// By convention, variable holding the shell is named `sh`. |
368 | /// |
369 | /// # Example |
370 | /// |
371 | /// ```no_run |
372 | /// use xshell::{cmd, Shell}; |
373 | /// |
374 | /// let sh = Shell::new()?; |
375 | /// let _d = sh.push_dir("./target" ); |
376 | /// let cwd = sh.current_dir(); |
377 | /// cmd!(sh, "echo current dir is {cwd}" ).run()?; |
378 | /// |
379 | /// let process_cwd = std::env::current_dir().unwrap(); |
380 | /// assert_eq!(cwd, process_cwd.join("./target" )); |
381 | /// # Ok::<(), xshell::Error>(()) |
382 | /// ``` |
383 | #[derive (Debug, Clone)] |
384 | pub struct Shell { |
385 | cwd: RefCell<PathBuf>, |
386 | env: RefCell<HashMap<OsString, OsString>>, |
387 | } |
388 | |
389 | impl std::panic::UnwindSafe for Shell {} |
390 | impl std::panic::RefUnwindSafe for Shell {} |
391 | |
392 | impl Shell { |
393 | /// Creates a new [`Shell`]. |
394 | /// |
395 | /// Fails if [`std::env::current_dir`] returns an error. |
396 | pub fn new() -> Result<Shell> { |
397 | let cwd = current_dir().map_err(|err| Error::new_current_dir(err, None))?; |
398 | let cwd = RefCell::new(cwd); |
399 | let env = RefCell::new(HashMap::new()); |
400 | Ok(Shell { cwd, env }) |
401 | } |
402 | |
403 | // region:env |
404 | /// Returns the working directory for this [`Shell`]. |
405 | /// |
406 | /// All relative paths are interpreted relative to this directory, rather |
407 | /// than [`std::env::current_dir`]. |
408 | #[doc (alias = "pwd" )] |
409 | pub fn current_dir(&self) -> PathBuf { |
410 | self.cwd.borrow().clone() |
411 | } |
412 | |
413 | /// Changes the working directory for this [`Shell`]. |
414 | /// |
415 | /// Note that this doesn't affect [`std::env::current_dir`]. |
416 | #[doc (alias = "pwd" )] |
417 | pub fn change_dir<P: AsRef<Path>>(&self, dir: P) { |
418 | self._change_dir(dir.as_ref()) |
419 | } |
420 | fn _change_dir(&self, dir: &Path) { |
421 | let dir = self.path(dir); |
422 | *self.cwd.borrow_mut() = dir; |
423 | } |
424 | |
425 | /// Temporary changes the working directory of this [`Shell`]. |
426 | /// |
427 | /// Returns a RAII guard which reverts the working directory to the old |
428 | /// value when dropped. |
429 | /// |
430 | /// Note that this doesn't affect [`std::env::current_dir`]. |
431 | #[doc (alias = "pushd" )] |
432 | pub fn push_dir<P: AsRef<Path>>(&self, path: P) -> PushDir<'_> { |
433 | self._push_dir(path.as_ref()) |
434 | } |
435 | fn _push_dir(&self, path: &Path) -> PushDir<'_> { |
436 | let path = self.path(path); |
437 | PushDir::new(self, path) |
438 | } |
439 | |
440 | /// Fetches the environmental variable `key` for this [`Shell`]. |
441 | /// |
442 | /// Returns an error if the variable is not set, or set to a non-utf8 value. |
443 | /// |
444 | /// Environment of the [`Shell`] affects all commands spawned via this |
445 | /// shell. |
446 | pub fn var<K: AsRef<OsStr>>(&self, key: K) -> Result<String> { |
447 | self._var(key.as_ref()) |
448 | } |
449 | fn _var(&self, key: &OsStr) -> Result<String> { |
450 | match self._var_os(key) { |
451 | Some(it) => it.into_string().map_err(VarError::NotUnicode), |
452 | None => Err(VarError::NotPresent), |
453 | } |
454 | .map_err(|err| Error::new_var(err, key.to_os_string())) |
455 | } |
456 | |
457 | /// Fetches the environmental variable `key` for this [`Shell`] as |
458 | /// [`OsString`] Returns [`None`] if the variable is not set. |
459 | /// |
460 | /// Environment of the [`Shell`] affects all commands spawned via this |
461 | /// shell. |
462 | pub fn var_os<K: AsRef<OsStr>>(&self, key: K) -> Option<OsString> { |
463 | self._var_os(key.as_ref()) |
464 | } |
465 | fn _var_os(&self, key: &OsStr) -> Option<OsString> { |
466 | self.env.borrow().get(key).cloned().or_else(|| env::var_os(key)) |
467 | } |
468 | |
469 | /// Sets the value of `key` environment variable for this [`Shell`] to |
470 | /// `val`. |
471 | /// |
472 | /// Note that this doesn't affect [`std::env::var`]. |
473 | pub fn set_var<K: AsRef<OsStr>, V: AsRef<OsStr>>(&self, key: K, val: V) { |
474 | self._set_var(key.as_ref(), val.as_ref()) |
475 | } |
476 | fn _set_var(&self, key: &OsStr, val: &OsStr) { |
477 | self.env.borrow_mut().insert(key.to_os_string(), val.to_os_string()); |
478 | } |
479 | |
480 | /// Temporary sets the value of `key` environment variable for this |
481 | /// [`Shell`] to `val`. |
482 | /// |
483 | /// Returns a RAII guard which restores the old environment when dropped. |
484 | /// |
485 | /// Note that this doesn't affect [`std::env::var`]. |
486 | pub fn push_env<K: AsRef<OsStr>, V: AsRef<OsStr>>(&self, key: K, val: V) -> PushEnv<'_> { |
487 | self._push_env(key.as_ref(), val.as_ref()) |
488 | } |
489 | fn _push_env(&self, key: &OsStr, val: &OsStr) -> PushEnv<'_> { |
490 | PushEnv::new(self, key.to_os_string(), val.to_os_string()) |
491 | } |
492 | // endregion:env |
493 | |
494 | // region:fs |
495 | /// Read the entire contents of a file into a string. |
496 | #[doc (alias = "cat" )] |
497 | pub fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<String> { |
498 | self._read_file(path.as_ref()) |
499 | } |
500 | fn _read_file(&self, path: &Path) -> Result<String> { |
501 | let path = self.path(path); |
502 | fs::read_to_string(&path).map_err(|err| Error::new_read_file(err, path)) |
503 | } |
504 | |
505 | /// Read the entire contents of a file into a vector of bytes. |
506 | pub fn read_binary_file<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> { |
507 | self._read_binary_file(path.as_ref()) |
508 | } |
509 | fn _read_binary_file(&self, path: &Path) -> Result<Vec<u8>> { |
510 | let path = self.path(path); |
511 | fs::read(&path).map_err(|err| Error::new_read_file(err, path)) |
512 | } |
513 | |
514 | /// Returns a sorted list of paths directly contained in the directory at |
515 | /// `path`. |
516 | #[doc (alias = "ls" )] |
517 | pub fn read_dir<P: AsRef<Path>>(&self, path: P) -> Result<Vec<PathBuf>> { |
518 | self._read_dir(path.as_ref()) |
519 | } |
520 | fn _read_dir(&self, path: &Path) -> Result<Vec<PathBuf>> { |
521 | let path = self.path(path); |
522 | let mut res = Vec::new(); |
523 | || -> _ { |
524 | for entry in fs::read_dir(&path)? { |
525 | let entry = entry?; |
526 | res.push(entry.path()) |
527 | } |
528 | Ok(()) |
529 | }() |
530 | .map_err(|err| Error::new_read_dir(err, path))?; |
531 | res.sort(); |
532 | Ok(res) |
533 | } |
534 | |
535 | /// Write a slice as the entire contents of a file. |
536 | /// |
537 | /// This function will create the file and all intermediate directories if |
538 | /// they don't exist. |
539 | // TODO: probably want to make this an atomic rename write? |
540 | pub fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(&self, path: P, contents: C) -> Result<()> { |
541 | self._write_file(path.as_ref(), contents.as_ref()) |
542 | } |
543 | fn _write_file(&self, path: &Path, contents: &[u8]) -> Result<()> { |
544 | let path = self.path(path); |
545 | if let Some(p) = path.parent() { |
546 | self.create_dir(p)?; |
547 | } |
548 | fs::write(&path, contents).map_err(|err| Error::new_write_file(err, path)) |
549 | } |
550 | |
551 | /// Copies `src` into `dst`. |
552 | /// |
553 | /// `src` must be a file, but `dst` need not be. If `dst` is an existing |
554 | /// directory, `src` will be copied into a file in the `dst` directory whose |
555 | /// name is same as that of `src`. |
556 | /// |
557 | /// Otherwise, `dst` is a file or does not exist, and `src` will be copied into |
558 | /// it. |
559 | #[doc (alias = "cp" )] |
560 | pub fn copy_file<S: AsRef<Path>, D: AsRef<Path>>(&self, src: S, dst: D) -> Result<()> { |
561 | self._copy_file(src.as_ref(), dst.as_ref()) |
562 | } |
563 | fn _copy_file(&self, src: &Path, dst: &Path) -> Result<()> { |
564 | let src = self.path(src); |
565 | let dst = self.path(dst); |
566 | let dst = dst.as_path(); |
567 | let mut _tmp; |
568 | let mut dst = dst; |
569 | if dst.is_dir() { |
570 | if let Some(file_name) = src.file_name() { |
571 | _tmp = dst.join(file_name); |
572 | dst = &_tmp; |
573 | } |
574 | } |
575 | std::fs::copy(&src, dst) |
576 | .map_err(|err| Error::new_copy_file(err, src.to_path_buf(), dst.to_path_buf()))?; |
577 | Ok(()) |
578 | } |
579 | |
580 | /// Hardlinks `src` to `dst`. |
581 | #[doc (alias = "ln" )] |
582 | pub fn hard_link<S: AsRef<Path>, D: AsRef<Path>>(&self, src: S, dst: D) -> Result<()> { |
583 | self._hard_link(src.as_ref(), dst.as_ref()) |
584 | } |
585 | fn _hard_link(&self, src: &Path, dst: &Path) -> Result<()> { |
586 | let src = self.path(src); |
587 | let dst = self.path(dst); |
588 | fs::hard_link(&src, &dst).map_err(|err| Error::new_hard_link(err, src, dst)) |
589 | } |
590 | |
591 | /// Creates the specified directory. |
592 | /// |
593 | /// All intermediate directories will also be created. |
594 | #[doc (alias("mkdir_p" , "mkdir" ))] |
595 | pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> { |
596 | self._create_dir(path.as_ref()) |
597 | } |
598 | fn _create_dir(&self, path: &Path) -> Result<PathBuf> { |
599 | let path = self.path(path); |
600 | match fs::create_dir_all(&path) { |
601 | Ok(()) => Ok(path), |
602 | Err(err) => Err(Error::new_create_dir(err, path)), |
603 | } |
604 | } |
605 | |
606 | /// Creates an empty named world-readable temporary directory. |
607 | /// |
608 | /// Returns a [`TempDir`] RAII guard with the path to the directory. When |
609 | /// dropped, the temporary directory and all of its contents will be |
610 | /// removed. |
611 | /// |
612 | /// Note that this is an **insecure method** -- any other process on the |
613 | /// system will be able to read the data. |
614 | #[doc (alias = "mktemp" )] |
615 | pub fn create_temp_dir(&self) -> Result<TempDir> { |
616 | let base = std::env::temp_dir(); |
617 | self.create_dir(&base)?; |
618 | |
619 | static CNT: AtomicUsize = AtomicUsize::new(0); |
620 | |
621 | let mut n_try = 0u32; |
622 | loop { |
623 | let cnt = CNT.fetch_add(1, Ordering::Relaxed); |
624 | let path = base.join(format!("xshell-tmp-dir- {}" , cnt)); |
625 | match fs::create_dir(&path) { |
626 | Ok(()) => return Ok(TempDir { path }), |
627 | Err(err) if n_try == 1024 => return Err(Error::new_create_dir(err, path)), |
628 | Err(_) => n_try += 1, |
629 | } |
630 | } |
631 | } |
632 | |
633 | /// Removes the file or directory at the given path. |
634 | #[doc (alias("rm_rf" , "rm" ))] |
635 | pub fn remove_path<P: AsRef<Path>>(&self, path: P) -> Result<()> { |
636 | self._remove_path(path.as_ref()) |
637 | } |
638 | fn _remove_path(&self, path: &Path) -> Result<(), Error> { |
639 | let path = self.path(path); |
640 | match path.metadata() { |
641 | Ok(meta) => if meta.is_dir() { remove_dir_all(&path) } else { fs::remove_file(&path) } |
642 | .map_err(|err| Error::new_remove_path(err, path)), |
643 | Err(err) if err.kind() == ErrorKind::NotFound => Ok(()), |
644 | Err(err) => Err(Error::new_remove_path(err, path)), |
645 | } |
646 | } |
647 | |
648 | /// Returns whether a file or directory exists at the given path. |
649 | #[doc (alias("stat" ))] |
650 | pub fn path_exists<P: AsRef<Path>>(&self, path: P) -> bool { |
651 | self.path(path.as_ref()).exists() |
652 | } |
653 | // endregion:fs |
654 | |
655 | /// Creates a new [`Cmd`] that executes the given `program`. |
656 | pub fn cmd<P: AsRef<Path>>(&self, program: P) -> Cmd<'_> { |
657 | // TODO: path lookup? |
658 | Cmd::new(self, program.as_ref()) |
659 | } |
660 | |
661 | fn path(&self, p: &Path) -> PathBuf { |
662 | let cd = self.cwd.borrow(); |
663 | cd.join(p) |
664 | } |
665 | } |
666 | |
667 | /// RAII guard returned from [`Shell::push_dir`]. |
668 | /// |
669 | /// Dropping `PushDir` restores the working directory of the [`Shell`] to the |
670 | /// old value. |
671 | #[derive (Debug)] |
672 | #[must_use ] |
673 | pub struct PushDir<'a> { |
674 | old_cwd: PathBuf, |
675 | shell: &'a Shell, |
676 | } |
677 | |
678 | impl<'a> PushDir<'a> { |
679 | fn new(shell: &'a Shell, path: PathBuf) -> PushDir<'a> { |
680 | PushDir { old_cwd: mem::replace(&mut *shell.cwd.borrow_mut(), src:path), shell } |
681 | } |
682 | } |
683 | |
684 | impl Drop for PushDir<'_> { |
685 | fn drop(&mut self) { |
686 | mem::swap(&mut *self.shell.cwd.borrow_mut(), &mut self.old_cwd) |
687 | } |
688 | } |
689 | |
690 | /// RAII guard returned from [`Shell::push_env`]. |
691 | /// |
692 | /// Dropping `PushEnv` restores the old value of the environmental variable. |
693 | #[derive (Debug)] |
694 | #[must_use ] |
695 | pub struct PushEnv<'a> { |
696 | key: OsString, |
697 | old_value: Option<OsString>, |
698 | shell: &'a Shell, |
699 | } |
700 | |
701 | impl<'a> PushEnv<'a> { |
702 | fn new(shell: &'a Shell, key: OsString, val: OsString) -> PushEnv<'a> { |
703 | let old_value: Option = shell.env.borrow_mut().insert(k:key.clone(), v:val); |
704 | PushEnv { shell, key, old_value } |
705 | } |
706 | } |
707 | |
708 | impl Drop for PushEnv<'_> { |
709 | fn drop(&mut self) { |
710 | let mut env: RefMut<'_, HashMap> = self.shell.env.borrow_mut(); |
711 | let key: OsString = mem::take(&mut self.key); |
712 | match self.old_value.take() { |
713 | Some(value: OsString) => { |
714 | env.insert(k:key, v:value); |
715 | } |
716 | None => { |
717 | env.remove(&key); |
718 | } |
719 | } |
720 | } |
721 | } |
722 | |
723 | /// A builder object for constructing a subprocess. |
724 | /// |
725 | /// A [`Cmd`] is usually created with the [`cmd!`] macro. The command exists |
726 | /// within a context of a [`Shell`] and uses its working directory and |
727 | /// environment. |
728 | /// |
729 | /// # Example |
730 | /// |
731 | /// ```no_run |
732 | /// use xshell::{Shell, cmd}; |
733 | /// |
734 | /// let sh = Shell::new()?; |
735 | /// |
736 | /// let branch = "main" ; |
737 | /// let cmd = cmd!(sh, "git switch {branch}" ).quiet().run()?; |
738 | /// # Ok::<(), xshell::Error>(()) |
739 | /// ``` |
740 | #[derive (Debug)] |
741 | #[must_use ] |
742 | pub struct Cmd<'a> { |
743 | shell: &'a Shell, |
744 | data: CmdData, |
745 | } |
746 | |
747 | #[derive (Debug, Default, Clone)] |
748 | struct CmdData { |
749 | prog: PathBuf, |
750 | args: Vec<OsString>, |
751 | env_changes: Vec<EnvChange>, |
752 | ignore_status: bool, |
753 | quiet: bool, |
754 | secret: bool, |
755 | stdin_contents: Option<Vec<u8>>, |
756 | ignore_stdout: bool, |
757 | ignore_stderr: bool, |
758 | } |
759 | |
760 | // We just store a list of functions to call on the `Command` — the alternative |
761 | // would require mirroring the logic that `std::process::Command` (or rather |
762 | // `sys_common::CommandEnvs`) uses, which is moderately complex, involves |
763 | // special-casing `PATH`, and plausibly could change. |
764 | #[derive (Debug, Clone)] |
765 | enum EnvChange { |
766 | Set(OsString, OsString), |
767 | Remove(OsString), |
768 | Clear, |
769 | } |
770 | |
771 | impl fmt::Display for Cmd<'_> { |
772 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
773 | fmt::Display::fmt(&self.data, f) |
774 | } |
775 | } |
776 | |
777 | impl fmt::Display for CmdData { |
778 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
779 | if self.secret { |
780 | return write!(f, "<secret>" ); |
781 | } |
782 | |
783 | write!(f, " {}" , self.prog.display())?; |
784 | for arg: &OsString in &self.args { |
785 | // TODO: this is potentially not copy-paste safe. |
786 | let arg: Cow<'_, str> = arg.to_string_lossy(); |
787 | if arg.chars().any(|it: char| it.is_ascii_whitespace()) { |
788 | write!(f, " \"{}\"" , arg.escape_default())? |
789 | } else { |
790 | write!(f, " {}" , arg)? |
791 | }; |
792 | } |
793 | Ok(()) |
794 | } |
795 | } |
796 | |
797 | impl From<Cmd<'_>> for Command { |
798 | fn from(cmd: Cmd<'_>) -> Command { |
799 | cmd.to_command() |
800 | } |
801 | } |
802 | |
803 | impl<'a> Cmd<'a> { |
804 | fn new(shell: &'a Shell, prog: &Path) -> Cmd<'a> { |
805 | let mut data = CmdData::default(); |
806 | data.prog = prog.to_path_buf(); |
807 | Cmd { shell, data } |
808 | } |
809 | |
810 | // region:builder |
811 | /// Adds an argument to this commands. |
812 | pub fn arg<P: AsRef<OsStr>>(mut self, arg: P) -> Cmd<'a> { |
813 | self._arg(arg.as_ref()); |
814 | self |
815 | } |
816 | fn _arg(&mut self, arg: &OsStr) { |
817 | self.data.args.push(arg.to_owned()) |
818 | } |
819 | |
820 | /// Adds all of the arguments to this command. |
821 | pub fn args<I>(mut self, args: I) -> Cmd<'a> |
822 | where |
823 | I: IntoIterator, |
824 | I::Item: AsRef<OsStr>, |
825 | { |
826 | args.into_iter().for_each(|it| self._arg(it.as_ref())); |
827 | self |
828 | } |
829 | |
830 | #[doc (hidden)] |
831 | pub fn __extend_arg<P: AsRef<OsStr>>(mut self, arg_fragment: P) -> Cmd<'a> { |
832 | self.___extend_arg(arg_fragment.as_ref()); |
833 | self |
834 | } |
835 | fn ___extend_arg(&mut self, arg_fragment: &OsStr) { |
836 | match self.data.args.last_mut() { |
837 | Some(last_arg) => last_arg.push(arg_fragment), |
838 | None => { |
839 | let mut prog = mem::take(&mut self.data.prog).into_os_string(); |
840 | prog.push(arg_fragment); |
841 | self.data.prog = prog.into(); |
842 | } |
843 | } |
844 | } |
845 | |
846 | /// Overrides the value of the environmental variable for this command. |
847 | pub fn env<K: AsRef<OsStr>, V: AsRef<OsStr>>(mut self, key: K, val: V) -> Cmd<'a> { |
848 | self._env_set(key.as_ref(), val.as_ref()); |
849 | self |
850 | } |
851 | |
852 | fn _env_set(&mut self, key: &OsStr, val: &OsStr) { |
853 | self.data.env_changes.push(EnvChange::Set(key.to_owned(), val.to_owned())); |
854 | } |
855 | |
856 | /// Overrides the values of specified environmental variables for this |
857 | /// command. |
858 | pub fn envs<I, K, V>(mut self, vars: I) -> Cmd<'a> |
859 | where |
860 | I: IntoIterator<Item = (K, V)>, |
861 | K: AsRef<OsStr>, |
862 | V: AsRef<OsStr>, |
863 | { |
864 | vars.into_iter().for_each(|(k, v)| self._env_set(k.as_ref(), v.as_ref())); |
865 | self |
866 | } |
867 | |
868 | /// Removes the environment variable from this command. |
869 | pub fn env_remove<K: AsRef<OsStr>>(mut self, key: K) -> Cmd<'a> { |
870 | self._env_remove(key.as_ref()); |
871 | self |
872 | } |
873 | fn _env_remove(&mut self, key: &OsStr) { |
874 | self.data.env_changes.push(EnvChange::Remove(key.to_owned())); |
875 | } |
876 | |
877 | /// Removes all of the environment variables from this command. |
878 | pub fn env_clear(mut self) -> Cmd<'a> { |
879 | self.data.env_changes.push(EnvChange::Clear); |
880 | self |
881 | } |
882 | |
883 | /// Don't return an error if command the command exits with non-zero status. |
884 | /// |
885 | /// By default, non-zero exit status is considered an error. |
886 | pub fn ignore_status(mut self) -> Cmd<'a> { |
887 | self.set_ignore_status(true); |
888 | self |
889 | } |
890 | /// Controls whether non-zero exit status is considered an error. |
891 | pub fn set_ignore_status(&mut self, yes: bool) { |
892 | self.data.ignore_status = yes; |
893 | } |
894 | |
895 | /// Don't echo the command itself to stderr. |
896 | /// |
897 | /// By default, the command itself will be printed to stderr when executed via [`Cmd::run`]. |
898 | pub fn quiet(mut self) -> Cmd<'a> { |
899 | self.set_quiet(true); |
900 | self |
901 | } |
902 | /// Controls whether the command itself is printed to stderr. |
903 | pub fn set_quiet(&mut self, yes: bool) { |
904 | self.data.quiet = yes; |
905 | } |
906 | |
907 | /// Marks the command as secret. |
908 | /// |
909 | /// If a command is secret, it echoes `<secret>` instead of the program and |
910 | /// its arguments, even in error messages. |
911 | pub fn secret(mut self) -> Cmd<'a> { |
912 | self.set_secret(true); |
913 | self |
914 | } |
915 | /// Controls whether the command is secret. |
916 | pub fn set_secret(&mut self, yes: bool) { |
917 | self.data.secret = yes; |
918 | } |
919 | |
920 | /// Pass the given slice to the standard input of the spawned process. |
921 | pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd<'a> { |
922 | self._stdin(stdin.as_ref()); |
923 | self |
924 | } |
925 | fn _stdin(&mut self, stdin: &[u8]) { |
926 | self.data.stdin_contents = Some(stdin.to_vec()); |
927 | } |
928 | |
929 | /// Ignores the standard output stream of the process. |
930 | /// |
931 | /// This is equivalent to redirecting stdout to `/dev/null`. By default, the |
932 | /// stdout is inherited or captured. |
933 | pub fn ignore_stdout(mut self) -> Cmd<'a> { |
934 | self.set_ignore_stdout(true); |
935 | self |
936 | } |
937 | /// Controls whether the standard output is ignored. |
938 | pub fn set_ignore_stdout(&mut self, yes: bool) { |
939 | self.data.ignore_stdout = yes; |
940 | } |
941 | |
942 | /// Ignores the standard output stream of the process. |
943 | /// |
944 | /// This is equivalent redirecting stderr to `/dev/null`. By default, the |
945 | /// stderr is inherited or captured. |
946 | pub fn ignore_stderr(mut self) -> Cmd<'a> { |
947 | self.set_ignore_stderr(true); |
948 | self |
949 | } |
950 | /// Controls whether the standard error is ignored. |
951 | pub fn set_ignore_stderr(&mut self, yes: bool) { |
952 | self.data.ignore_stderr = yes; |
953 | } |
954 | // endregion:builder |
955 | |
956 | // region:running |
957 | /// Runs the command. |
958 | /// |
959 | /// By default the command itself is echoed to stderr, its standard streams |
960 | /// are inherited, and non-zero return code is considered an error. These |
961 | /// behaviors can be overridden by using various builder methods of the [`Cmd`]. |
962 | pub fn run(&self) -> Result<()> { |
963 | if !self.data.quiet { |
964 | eprintln!("$ {}" , self); |
965 | } |
966 | self.output_impl(false, false).map(|_| ()) |
967 | } |
968 | |
969 | /// Run the command and return its stdout as a string. Any trailing newline or carriage return will be trimmed. |
970 | pub fn read(&self) -> Result<String> { |
971 | self.read_stream(false) |
972 | } |
973 | |
974 | /// Run the command and return its stderr as a string. Any trailing newline or carriage return will be trimmed. |
975 | pub fn read_stderr(&self) -> Result<String> { |
976 | self.read_stream(true) |
977 | } |
978 | |
979 | /// Run the command and return its output. |
980 | pub fn output(&self) -> Result<Output> { |
981 | self.output_impl(true, true) |
982 | } |
983 | // endregion:running |
984 | |
985 | fn read_stream(&self, read_stderr: bool) -> Result<String> { |
986 | let read_stdout = !read_stderr; |
987 | let output = self.output_impl(read_stdout, read_stderr)?; |
988 | self.check_status(output.status)?; |
989 | |
990 | let stream = if read_stderr { output.stderr } else { output.stdout }; |
991 | let mut stream = String::from_utf8(stream).map_err(|err| Error::new_cmd_utf8(self, err))?; |
992 | |
993 | if stream.ends_with(' \n' ) { |
994 | stream.pop(); |
995 | } |
996 | if stream.ends_with(' \r' ) { |
997 | stream.pop(); |
998 | } |
999 | |
1000 | Ok(stream) |
1001 | } |
1002 | |
1003 | fn output_impl(&self, read_stdout: bool, read_stderr: bool) -> Result<Output> { |
1004 | let mut child = { |
1005 | let mut command = self.to_command(); |
1006 | |
1007 | if !self.data.ignore_stdout { |
1008 | command.stdout(if read_stdout { Stdio::piped() } else { Stdio::inherit() }); |
1009 | } |
1010 | if !self.data.ignore_stderr { |
1011 | command.stderr(if read_stderr { Stdio::piped() } else { Stdio::inherit() }); |
1012 | } |
1013 | |
1014 | command.stdin(match &self.data.stdin_contents { |
1015 | Some(_) => Stdio::piped(), |
1016 | None => Stdio::null(), |
1017 | }); |
1018 | |
1019 | command.spawn().map_err(|err| { |
1020 | // Try to determine whether the command failed because the current |
1021 | // directory does not exist. Return an appropriate error in such a |
1022 | // case. |
1023 | if matches!(err.kind(), io::ErrorKind::NotFound) { |
1024 | let cwd = self.shell.cwd.borrow(); |
1025 | if let Err(err) = cwd.metadata() { |
1026 | return Error::new_current_dir(err, Some(cwd.clone())); |
1027 | } |
1028 | } |
1029 | Error::new_cmd_io(self, err) |
1030 | })? |
1031 | }; |
1032 | |
1033 | let mut io_thread = None; |
1034 | if let Some(stdin_contents) = self.data.stdin_contents.clone() { |
1035 | let mut stdin = child.stdin.take().unwrap(); |
1036 | io_thread = Some(std::thread::spawn(move || { |
1037 | stdin.write_all(&stdin_contents)?; |
1038 | stdin.flush() |
1039 | })); |
1040 | } |
1041 | let out_res = child.wait_with_output(); |
1042 | let err_res = io_thread.map(|it| it.join().unwrap()); |
1043 | let output = out_res.map_err(|err| Error::new_cmd_io(self, err))?; |
1044 | if let Some(err_res) = err_res { |
1045 | err_res.map_err(|err| Error::new_cmd_stdin(self, err))?; |
1046 | } |
1047 | self.check_status(output.status)?; |
1048 | Ok(output) |
1049 | } |
1050 | |
1051 | fn to_command(&self) -> Command { |
1052 | let mut res = Command::new(&self.data.prog); |
1053 | res.current_dir(self.shell.current_dir()); |
1054 | res.args(&self.data.args); |
1055 | |
1056 | for (key, val) in &*self.shell.env.borrow() { |
1057 | res.env(key, val); |
1058 | } |
1059 | for change in &self.data.env_changes { |
1060 | match change { |
1061 | EnvChange::Clear => res.env_clear(), |
1062 | EnvChange::Remove(key) => res.env_remove(key), |
1063 | EnvChange::Set(key, val) => res.env(key, val), |
1064 | }; |
1065 | } |
1066 | |
1067 | if self.data.ignore_stdout { |
1068 | res.stdout(Stdio::null()); |
1069 | } |
1070 | |
1071 | if self.data.ignore_stderr { |
1072 | res.stderr(Stdio::null()); |
1073 | } |
1074 | |
1075 | res |
1076 | } |
1077 | |
1078 | fn check_status(&self, status: ExitStatus) -> Result<()> { |
1079 | if status.success() || self.data.ignore_status { |
1080 | return Ok(()); |
1081 | } |
1082 | Err(Error::new_cmd_status(self, status)) |
1083 | } |
1084 | } |
1085 | |
1086 | /// A temporary directory. |
1087 | /// |
1088 | /// This is a RAII object which will remove the underlying temporary directory |
1089 | /// when dropped. |
1090 | #[derive (Debug)] |
1091 | #[must_use ] |
1092 | pub struct TempDir { |
1093 | path: PathBuf, |
1094 | } |
1095 | |
1096 | impl TempDir { |
1097 | /// Returns the path to the underlying temporary directory. |
1098 | pub fn path(&self) -> &Path { |
1099 | &self.path |
1100 | } |
1101 | } |
1102 | |
1103 | impl Drop for TempDir { |
1104 | fn drop(&mut self) { |
1105 | let _ = remove_dir_all(&self.path); |
1106 | } |
1107 | } |
1108 | |
1109 | #[cfg (not(windows))] |
1110 | fn remove_dir_all(path: &Path) -> io::Result<()> { |
1111 | std::fs::remove_dir_all(path) |
1112 | } |
1113 | |
1114 | #[cfg (windows)] |
1115 | fn remove_dir_all(path: &Path) -> io::Result<()> { |
1116 | for _ in 0..99 { |
1117 | if fs::remove_dir_all(path).is_ok() { |
1118 | return Ok(()); |
1119 | } |
1120 | std::thread::sleep(std::time::Duration::from_millis(10)) |
1121 | } |
1122 | fs::remove_dir_all(path) |
1123 | } |
1124 | |