xshell/lib.rs
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
279mod error;
280
281use 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
294pub use crate::error::{Error, Result};
295#[doc(hidden)]
296pub 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]
346macro_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)]
384pub struct Shell {
385 cwd: RefCell<PathBuf>,
386 env: RefCell<HashMap<OsString, OsString>>,
387}
388
389impl std::panic::UnwindSafe for Shell {}
390impl std::panic::RefUnwindSafe for Shell {}
391
392impl 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]
673pub struct PushDir<'a> {
674 old_cwd: PathBuf,
675 shell: &'a Shell,
676}
677
678impl<'a> PushDir<'a> {
679 fn new(shell: &'a Shell, path: PathBuf) -> PushDir<'a> {
680 PushDir { old_cwd: mem::replace(&mut *shell.cwd.borrow_mut(), path), shell }
681 }
682}
683
684impl 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]
695pub struct PushEnv<'a> {
696 key: OsString,
697 old_value: Option<OsString>,
698 shell: &'a Shell,
699}
700
701impl<'a> PushEnv<'a> {
702 fn new(shell: &'a Shell, key: OsString, val: OsString) -> PushEnv<'a> {
703 let old_value = shell.env.borrow_mut().insert(key.clone(), val);
704 PushEnv { shell, key, old_value }
705 }
706}
707
708impl Drop for PushEnv<'_> {
709 fn drop(&mut self) {
710 let mut env = self.shell.env.borrow_mut();
711 let key = mem::take(&mut self.key);
712 match self.old_value.take() {
713 Some(value) => {
714 env.insert(key, 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]
742pub struct Cmd<'a> {
743 shell: &'a Shell,
744 data: CmdData,
745}
746
747#[derive(Debug, Default, Clone)]
748struct 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)]
765enum EnvChange {
766 Set(OsString, OsString),
767 Remove(OsString),
768 Clear,
769}
770
771impl fmt::Display for Cmd<'_> {
772 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
773 fmt::Display::fmt(&self.data, f)
774 }
775}
776
777impl 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 in &self.args {
785 // TODO: this is potentially not copy-paste safe.
786 let arg = arg.to_string_lossy();
787 if arg.chars().any(|it| it.is_ascii_whitespace()) {
788 write!(f, " \"{}\"", arg.escape_default())?
789 } else {
790 write!(f, " {}", arg)?
791 };
792 }
793 Ok(())
794 }
795}
796
797impl From<Cmd<'_>> for Command {
798 fn from(cmd: Cmd<'_>) -> Command {
799 cmd.to_command()
800 }
801}
802
803impl<'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]
1092pub struct TempDir {
1093 path: PathBuf,
1094}
1095
1096impl TempDir {
1097 /// Returns the path to the underlying temporary directory.
1098 pub fn path(&self) -> &Path {
1099 &self.path
1100 }
1101}
1102
1103impl Drop for TempDir {
1104 fn drop(&mut self) {
1105 let _ = remove_dir_all(&self.path);
1106 }
1107}
1108
1109#[cfg(not(windows))]
1110fn remove_dir_all(path: &Path) -> io::Result<()> {
1111 std::fs::remove_dir_all(path)
1112}
1113
1114#[cfg(windows)]
1115fn 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}