1 //! xshell makes it easy to write cross-platform "bash" scripts in Rust. 2 //! 3 //! It provides a `cmd!` macro for running subprocesses, as well as a number of 4 //! basic file manipulation utilities. 5 //! 6 //! ``` 7 //! # if cfg!(windows) { return Ok(()); } 8 //! use xshell::{cmd, read_file}; 9 //! 10 //! let name = "Julia"; 11 //! let output = cmd!("echo hello {name}!").read()?; 12 //! assert_eq!(output, "hello Julia!"); 13 //! 14 //! let err = read_file("feeling-lucky.txt").unwrap_err(); 15 //! assert_eq!( 16 //! err.to_string(), 17 //! "`feeling-lucky.txt`: no such file or directory (os error 2)", 18 //! ); 19 //! # Ok::<(), xshell::Error>(()) 20 //! ``` 21 //! 22 //! The intended use-case is various bits of glue code, which could be written 23 //! in bash or python. The original motivation is 24 //! [`xtask`](https://github.com/matklad/cargo-xtask) development. 25 //! 26 //! **Goals**: fast compile times, ergonomics, clear error messages.<br> 27 //! **Non goals**: completeness, robustness / misuse resistance. 28 //! 29 //! For "heavy-duty" code, consider using [`duct`] or [`std::process::Command`] 30 //! instead. 31 //! 32 //! # API Overview 33 //! 34 //! For a real-world example, see this crate's own CI script: 35 //! 36 //! [https://github.com/matklad/xshell/blob/master/examples/ci.rs](https://github.com/matklad/xshell/blob/master/examples/ci.rs) 37 //! 38 //! ## `cmd!` Macro 39 //! 40 //! Read output of the process into `String`. The final newline will be 41 //! stripped. 42 //! 43 //! ``` 44 //! # use xshell::cmd; 45 //! let output = cmd!("date +%Y-%m-%d").read()?; 46 //! assert!(output.chars().all(|c| "01234567890-".contains(c))); 47 //! # Ok::<(), xshell::Error>(()) 48 //! ``` 49 //! 50 //! If the exist status is non-zero, an error is returned. 51 //! 52 //! ``` 53 //! # use xshell::cmd; 54 //! let err = cmd!("false").read().unwrap_err(); 55 //! assert!(err.to_string().starts_with("command `false` failed")); 56 //! ``` 57 //! 58 //! <hr> 59 //! 60 //! Run the process, inheriting stdout and stderr. The command is echoed to 61 //! stdout. 62 //! 63 //! ``` 64 //! # use xshell::cmd; 65 //! cmd!("echo hello!").run()?; 66 //! # Ok::<(), xshell::Error>(()) 67 //! ``` 68 //! 69 //! Output 70 //! 71 //! ```text 72 //! $ echo hello! 73 //! hello! 74 //! ``` 75 //! 76 //! <hr> 77 //! 78 //! Interpolation is supported via `{name}` syntax. Use `{name...}` to 79 //! interpolate sequence of values. 80 //! 81 //! ``` 82 //! # use xshell::cmd; 83 //! let greeting = "Guten Tag"; 84 //! let people = &["Spica", "Boarst", "Georgina"]; 85 //! assert_eq!( 86 //! cmd!("echo {greeting} {people...}").to_string(), 87 //! r#"echo "Guten Tag" Spica Boarst Georgina"# 88 //! ); 89 //! ``` 90 //! 91 //! Note that the argument with a space is handled correctly. This is because 92 //! `cmd!` macro parses the string template at compile time. The macro hands the 93 //! interpolated values to the underlying `std::process::Command` as is and is 94 //! not vulnerable to [shell 95 //! injection](https://en.wikipedia.org/wiki/Code_injection#Shell_injection). 96 //! 97 //! Single quotes in literal arguments are supported: 98 //! 99 //! ``` 100 //! # use xshell::cmd; 101 //! assert_eq!( 102 //! cmd!("echo 'hello world'").to_string(), 103 //! r#"echo "hello world""#, 104 //! ) 105 //! ``` 106 //! Splat syntax is used for optional arguments idiom. 107 //! 108 //! ``` 109 //! # use xshell::cmd; 110 //! let check = if true { &["--", "--check"] } else { &[][..] }; 111 //! assert_eq!( 112 //! cmd!("cargo fmt {check...}").to_string(), 113 //! "cargo fmt -- --check" 114 //! ); 115 //! 116 //! let dry_run = if true { Some("--dry-run") } else { None }; 117 //! assert_eq!( 118 //! cmd!("git push {dry_run...}").to_string(), 119 //! "git push --dry-run" 120 //! ); 121 //! ``` 122 //! 123 //! <hr> 124 //! 125 //! xshell does not provide API for creating command pipelines. If you need 126 //! pipelines, consider using [`duct`] instead. Alternatively, you can convert 127 //! `xshell::Cmd` into [`std::process::Command`]: 128 //! 129 //! ``` 130 //! # use xshell::cmd; 131 //! let command: std::process::Command = cmd!("echo 'hello world'").into(); 132 //! ``` 133 //! 134 //! ## Manipulating the Environment 135 //! 136 //! Instead of `cd` and `export`, xshell uses RAII based `pushd` and `pushenv` 137 //! 138 //! ``` 139 //! use xshell::{cwd, pushd, pushenv}; 140 //! 141 //! let initial_dir = cwd()?; 142 //! { 143 //! let _p = pushd("src")?; 144 //! assert_eq!( 145 //! cwd()?, 146 //! initial_dir.join("src"), 147 //! ); 148 //! } 149 //! assert_eq!(cwd()?, initial_dir); 150 //! 151 //! assert!(std::env::var("MY_VAR").is_err()); 152 //! let _e = pushenv("MY_VAR", "92"); 153 //! assert_eq!( 154 //! std::env::var("MY_VAR").as_deref(), 155 //! Ok("92") 156 //! ); 157 //! # Ok::<(), xshell::Error>(()) 158 //! ``` 159 //! 160 //! ## Working with Files 161 //! 162 //! xshell provides the following utilities, which are mostly re-exports from 163 //! `std::fs` module with paths added to error messages: `rm_rf`, `read_file`, 164 //! `write_file`, `mkdir_p`, `cp`, `read_dir`, `cwd`. 165 //! 166 //! # Maintenance 167 //! 168 //! Minimum Supported Rust Version: 1.47.0. MSRV bump is not considered semver 169 //! breaking. MSRV is updated conservatively. 170 //! 171 //! The crate isn't comprehensive. Additional functionality is added on 172 //! as-needed bases, as long as it doesn't compromise compile times. 173 //! Function-level docs are an especially welcome addition :-) 174 //! 175 //! # Implementation details 176 //! 177 //! The design is heavily inspired by the Julia language: 178 //! 179 //! * [Shelling Out Sucks](https://julialang.org/blog/2012/03/shelling-out-sucks/) 180 //! * [Put This In Your Pipe](https://julialang.org/blog/2013/04/put-this-in-your-pipe/) 181 //! * [Running External Programs](https://docs.julialang.org/en/v1/manual/running-external-programs/) 182 //! * [Filesystem](https://docs.julialang.org/en/v1/base/file/) 183 //! 184 //! Smaller influences are the [`duct`] crate and Ruby's 185 //! [`FileUtils`](https://ruby-doc.org/stdlib-2.4.1/libdoc/fileutils/rdoc/FileUtils.html) 186 //! module. 187 //! 188 //! The `cmd!` macro uses a simple proc-macro internally. It doesn't depend on 189 //! helper libraries, so the fixed-cost impact on compile times is moderate. 190 //! Compiling a trivial program with `cmd!("date +%Y-%m-%d")` takes one second. 191 //! Equivalent program using only `std::process::Command` compiles in 0.25 192 //! seconds. 193 //! 194 //! To make IDEs infer correct types without expanding proc-macro, it is wrapped 195 //! into a declarative macro which supplies type hints. 196 //! 197 //! Environment manipulation mutates global state and might have surprising 198 //! interactions with threads. Internally, everything is protected by a global 199 //! shell lock, so all functions in this crate are thread safe. However, 200 //! functions outside of xshell's control might experience race conditions: 201 //! 202 //! ``` 203 //! use std::{thread, fs}; 204 //! 205 //! use xshell::{pushd, read_file}; 206 //! 207 //! let t1 = thread::spawn(|| { 208 //! let _p = pushd("./src"); 209 //! }); 210 //! 211 //! // This is guaranteed to work: t2 will block while t1 is in `pushd`. 212 //! let t2 = thread::spawn(|| { 213 //! let res = read_file("./src/lib.rs"); 214 //! assert!(res.is_ok()); 215 //! }); 216 //! 217 //! // This is a race: t3 might observe difference cwds depending on timing. 218 //! let t3 = thread::spawn(|| { 219 //! let res = fs::read_to_string("./src/lib.rs"); 220 //! assert!(res.is_ok() || res.is_err()); 221 //! }); 222 //! # t1.join().unwrap(); t2.join().unwrap(); t3.join().unwrap(); 223 //! ``` 224 //! 225 //! # Naming 226 //! 227 //! xshell is an ex-shell, for those who grew tired of bash.<br> 228 //! xshell is an x-platform shell, for those who don't want to run `build.sh` on windows.<br> 229 //! xshell is built for [`xtask`](https://github.com/matklad/cargo-xtask).<br> 230 //! xshell uses x-traordinary level of [trickery](https://github.com/matklad/xshell/blob/843df7cd5b7d69fc9d2b884dc0852598335718fe/src/lib.rs#L233-L234), 231 //! just like `xtask` [does](https://matklad.github.io/2018/01/03/make-your-own-make.html). 232 //! 233 //! [`duct`]: https://github.com/oconnor663/duct.rs 234 //! [`std::process::Command`]: https://doc.rust-lang.org/stable/std/process/struct.Command.html 235 236 #![deny(missing_debug_implementations)] 237 #![deny(missing_docs)] 238 #![deny(rust_2018_idioms)] 239 240 mod env; 241 mod gsl; 242 mod error; 243 mod fs; 244 245 use std::{ 246 ffi::{OsStr, OsString}, 247 fmt, io, 248 io::Write, 249 path::Path, 250 process::Output, 251 process::Stdio, 252 }; 253 254 use error::CmdErrorKind; 255 #[doc(hidden)] 256 pub use xshell_macros::__cmd; 257 258 pub use crate::{ 259 env::{pushd, pushenv, Pushd, Pushenv}, 260 error::{Error, Result}, 261 fs::{cp, cwd, hard_link, mkdir_p, mktemp_d, read_dir, read_file, rm_rf, write_file, TempDir}, 262 }; 263 264 /// Constructs a [`Cmd`] from the given string. 265 #[macro_export] 266 macro_rules! cmd { 267 ($cmd:tt) => {{ 268 #[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)] 269 format_args!($cmd); 270 use $crate::Cmd as __CMD; 271 let cmd: $crate::Cmd = $crate::__cmd!(__CMD $cmd); 272 cmd 273 }}; 274 } 275 276 /// A command. 277 #[must_use] 278 #[derive(Debug)] 279 pub struct Cmd { 280 args: Vec<OsString>, 281 stdin_contents: Option<Vec<u8>>, 282 ignore_status: bool, 283 echo_cmd: bool, 284 secret: bool, 285 env_changes: Vec<EnvChange>, 286 ignore_stdout: bool, 287 ignore_stderr: bool, 288 } 289 290 impl fmt::Display for Cmd { fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result291 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 292 if !self.secret { 293 let mut space = ""; 294 for arg in &self.args { 295 write!(f, "{}", space)?; 296 space = " "; 297 298 let arg = arg.to_string_lossy(); 299 if arg.chars().any(|it| it.is_ascii_whitespace()) { 300 write!(f, "\"{}\"", arg.escape_default())? 301 } else { 302 write!(f, "{}", arg)? 303 }; 304 } 305 } else { 306 write!(f, "<secret>")?; 307 } 308 Ok(()) 309 } 310 } 311 312 impl From<Cmd> for std::process::Command { from(cmd: Cmd) -> Self313 fn from(cmd: Cmd) -> Self { 314 cmd.command() 315 } 316 } 317 318 impl Cmd { 319 /// Creates a new `Cmd` that executes the given `program`. new(program: impl AsRef<Path>) -> Cmd320 pub fn new(program: impl AsRef<Path>) -> Cmd { 321 Cmd::_new(program.as_ref()) 322 } _new(program: &Path) -> Cmd323 fn _new(program: &Path) -> Cmd { 324 Cmd { 325 args: vec![program.as_os_str().to_owned()], 326 stdin_contents: None, 327 ignore_status: false, 328 echo_cmd: true, 329 secret: false, 330 env_changes: vec![], 331 ignore_stdout: false, 332 ignore_stderr: false, 333 } 334 } 335 336 /// Pushes an argument onto this `Cmd`. arg(mut self, arg: impl AsRef<OsStr>) -> Cmd337 pub fn arg(mut self, arg: impl AsRef<OsStr>) -> Cmd { 338 self._arg(arg.as_ref()); 339 self 340 } 341 342 /// Pushes the arguments onto this `Cmd`. args<I>(mut self, args: I) -> Cmd where I: IntoIterator, I::Item: AsRef<OsStr>,343 pub fn args<I>(mut self, args: I) -> Cmd 344 where 345 I: IntoIterator, 346 I::Item: AsRef<OsStr>, 347 { 348 args.into_iter().for_each(|it| self._arg(it.as_ref())); 349 self 350 } 351 _arg(&mut self, arg: &OsStr)352 fn _arg(&mut self, arg: &OsStr) { 353 self.args.push(arg.to_owned()) 354 } 355 356 /// Equivalent to [`std::process::Command::env`]. env<K, V>(mut self, key: K, val: V) -> Cmd where K: AsRef<OsStr>, V: AsRef<OsStr>,357 pub fn env<K, V>(mut self, key: K, val: V) -> Cmd 358 where 359 K: AsRef<OsStr>, 360 V: AsRef<OsStr>, 361 { 362 self._env_set(key.as_ref(), val.as_ref()); 363 self 364 } 365 _env_set(&mut self, key: &OsStr, val: &OsStr)366 fn _env_set(&mut self, key: &OsStr, val: &OsStr) { 367 self.env_changes.push(EnvChange::Set(key.to_owned(), val.to_owned())); 368 } 369 370 /// Equivalent to [`std::process::Command::envs`]. 371 /// 372 /// Note: This does not replace the child process's environment, unless you 373 /// call [`Cmd::env_clear`] first. envs<I, K, V>(mut self, vars: I) -> Cmd where I: IntoIterator<Item = (K, V)>, K: AsRef<OsStr>, V: AsRef<OsStr>,374 pub fn envs<I, K, V>(mut self, vars: I) -> Cmd 375 where 376 I: IntoIterator<Item = (K, V)>, 377 K: AsRef<OsStr>, 378 V: AsRef<OsStr>, 379 { 380 vars.into_iter().for_each(|(k, v)| self._env_set(k.as_ref(), v.as_ref())); 381 self 382 } 383 384 /// Equivalent to [`std::process::Command::env_remove`]. env_remove<K>(mut self, key: K) -> Cmd where K: AsRef<OsStr>,385 pub fn env_remove<K>(mut self, key: K) -> Cmd 386 where 387 K: AsRef<OsStr>, 388 { 389 self._env_remove(key.as_ref()); 390 self 391 } 392 _env_remove(&mut self, key: &OsStr)393 fn _env_remove(&mut self, key: &OsStr) { 394 self.env_changes.push(EnvChange::Remove(key.to_owned())); 395 } 396 397 /// Equivalent to [`std::process::Command::env_clear`]. 398 /// 399 /// Note that on Windows some environmental variables are required for 400 /// process spawning. See https://github.com/rust-lang/rust/issues/31259. env_clear(mut self) -> Cmd401 pub fn env_clear(mut self) -> Cmd { 402 self.env_changes.push(EnvChange::Clear); 403 self 404 } 405 406 /// Returns a `Cmd` that will ignore the stdout stream. This is equivalent of 407 /// attaching stdout to `/dev/null`. ignore_stdout(mut self) -> Cmd408 pub fn ignore_stdout(mut self) -> Cmd { 409 self.ignore_stdout = true; 410 self 411 } 412 413 /// Returns a `Cmd` that will ignore the stderr stream. This is equivalent of 414 /// attaching stderr to `/dev/null`. ignore_stderr(mut self) -> Cmd415 pub fn ignore_stderr(mut self) -> Cmd { 416 self.ignore_stderr = true; 417 self 418 } 419 420 #[doc(hidden)] __extend_arg(mut self, arg: impl AsRef<OsStr>) -> Cmd421 pub fn __extend_arg(mut self, arg: impl AsRef<OsStr>) -> Cmd { 422 self.___extend_arg(arg.as_ref()); 423 self 424 } ___extend_arg(&mut self, arg: &OsStr)425 fn ___extend_arg(&mut self, arg: &OsStr) { 426 self.args.last_mut().unwrap().push(arg) 427 } 428 429 /// Returns a `Cmd` that ignores its exit status. ignore_status(mut self) -> Cmd430 pub fn ignore_status(mut self) -> Cmd { 431 self.ignore_status = true; 432 self 433 } 434 435 /// Returns a `Cmd` with the given stdin. stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd436 pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd { 437 self._stdin(stdin.as_ref()); 438 self 439 } _stdin(&mut self, stdin: &[u8])440 fn _stdin(&mut self, stdin: &[u8]) { 441 self.stdin_contents = Some(stdin.to_vec()); 442 } 443 444 /// Returns a `Cmd` that echoes itself (or not) as specified. echo_cmd(mut self, echo: bool) -> Cmd445 pub fn echo_cmd(mut self, echo: bool) -> Cmd { 446 self.echo_cmd = echo; 447 self 448 } 449 450 /// Returns a `Cmd` that is secret (or not) as specified. 451 /// 452 /// If a command is secret, it echoes `<secret>` instead of the program and 453 /// its arguments. secret(mut self, secret: bool) -> Cmd454 pub fn secret(mut self, secret: bool) -> Cmd { 455 self.secret = secret; 456 self 457 } 458 459 /// Returns the stdout from running the command. read(self) -> Result<String>460 pub fn read(self) -> Result<String> { 461 self.read_stream(false) 462 } 463 464 /// Returns the stderr from running the command. read_stderr(self) -> Result<String>465 pub fn read_stderr(self) -> Result<String> { 466 self.read_stream(true) 467 } 468 469 /// Returns a [`std::process::Output`] from running the command. output(self) -> Result<Output>470 pub fn output(self) -> Result<Output> { 471 match self.output_impl(true, true) { 472 Ok(output) if output.status.success() || self.ignore_status => Ok(output), 473 Ok(output) => Err(CmdErrorKind::NonZeroStatus(output.status).err(self)), 474 Err(io_err) => Err(CmdErrorKind::Io(io_err).err(self)), 475 } 476 } 477 478 /// Runs the command. run(self) -> Result<()>479 pub fn run(self) -> Result<()> { 480 let _guard = gsl::read(); 481 if self.echo_cmd { 482 eprintln!("$ {}", self); 483 } 484 match self.command().status() { 485 Ok(status) if status.success() || self.ignore_status => Ok(()), 486 Ok(status) => Err(CmdErrorKind::NonZeroStatus(status).err(self)), 487 Err(io_err) => Err(CmdErrorKind::Io(io_err).err(self)), 488 } 489 } 490 read_stream(self, read_stderr: bool) -> Result<String>491 fn read_stream(self, read_stderr: bool) -> Result<String> { 492 let read_stdout = !read_stderr; 493 match self.output_impl(read_stdout, read_stderr) { 494 Ok(output) if output.status.success() || self.ignore_status => { 495 let stream = if read_stderr { output.stderr } else { output.stdout }; 496 let mut stream = String::from_utf8(stream) 497 .map_err(|utf8_err| CmdErrorKind::NonUtf8Output(utf8_err).err(self))?; 498 499 if stream.ends_with('\n') { 500 stream.pop(); 501 } 502 if stream.ends_with('\r') { 503 stream.pop(); 504 } 505 506 Ok(stream) 507 } 508 Ok(output) => Err(CmdErrorKind::NonZeroStatus(output.status).err(self)), 509 Err(io_err) => Err(CmdErrorKind::Io(io_err).err(self)), 510 } 511 } 512 output_impl(&self, read_stdout: bool, read_stderr: bool) -> io::Result<Output>513 fn output_impl(&self, read_stdout: bool, read_stderr: bool) -> io::Result<Output> { 514 let mut child = { 515 let _guard = gsl::read(); 516 let mut command = self.command(); 517 518 command.stdin(match &self.stdin_contents { 519 Some(_) => Stdio::piped(), 520 None => Stdio::null(), 521 }); 522 523 if !self.ignore_stdout { 524 command.stdout(if read_stdout { Stdio::piped() } else { Stdio::inherit() }); 525 } 526 527 if !self.ignore_stderr { 528 command.stderr(if read_stderr { Stdio::piped() } else { Stdio::inherit() }); 529 } 530 531 command.spawn()? 532 }; 533 534 if let Some(stdin_contents) = &self.stdin_contents { 535 let mut stdin = child.stdin.take().unwrap(); 536 stdin.write_all(stdin_contents)?; 537 stdin.flush()?; 538 } 539 child.wait_with_output() 540 } 541 command(&self) -> std::process::Command542 fn command(&self) -> std::process::Command { 543 let mut res = std::process::Command::new(&self.args[0]); 544 res.args(&self.args[1..]); 545 self.apply_env(&mut res); 546 if self.ignore_stdout { 547 res.stdout(Stdio::null()); 548 } 549 if self.ignore_stderr { 550 res.stderr(Stdio::null()); 551 } 552 res 553 } 554 apply_env(&self, cmd: &mut std::process::Command)555 fn apply_env(&self, cmd: &mut std::process::Command) { 556 for change in &self.env_changes { 557 match change { 558 EnvChange::Clear => cmd.env_clear(), 559 EnvChange::Remove(key) => cmd.env_remove(key), 560 EnvChange::Set(key, val) => cmd.env(key, val), 561 }; 562 } 563 } 564 } 565 566 // We just store a list of functions to call on the `Command` — the alternative 567 // would require mirroring the logic that `std::process::Command` (or rather 568 // `sys_common::CommandEnvs`) uses, which is moderately complex, involves 569 // special-casing `PATH`, and plausbly could change. 570 #[derive(Debug)] 571 enum EnvChange { 572 Set(OsString, OsString), 573 Remove(OsString), 574 Clear, 575 } 576