| 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 | |