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.59.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 | //! # Implementation Notes |
248 | //! |
249 | //! The design is heavily inspired by the Julia language: |
250 | //! |
251 | //! * [Shelling Out |
252 | //! Sucks](https://julialang.org/blog/2012/03/shelling-out-sucks/) |
253 | //! * [Put This In Your |
254 | //! Pipe](https://julialang.org/blog/2013/04/put-this-in-your-pipe/) |
255 | //! * [Running External |
256 | //! Programs](https://docs.julialang.org/en/v1/manual/running-external-programs/) |
257 | //! * [Filesystem](https://docs.julialang.org/en/v1/base/file/) |
258 | //! |
259 | //! Smaller influences are the [`duct`] crate and Ruby's |
260 | //! [`FileUtils`](https://ruby-doc.org/stdlib-2.4.1/libdoc/fileutils/rdoc/FileUtils.html) |
261 | //! module. |
262 | //! |
263 | //! The `cmd!` macro uses a simple proc-macro internally. It doesn't depend on |
264 | //! helper libraries, so the fixed-cost impact on compile times is moderate. |
265 | //! Compiling a trivial program with `cmd!("date +%Y-%m-%d")` takes one second. |
266 | //! Equivalent program using only `std::process::Command` compiles in 0.25 |
267 | //! seconds. |
268 | //! |
269 | //! To make IDEs infer correct types without expanding proc-macro, it is wrapped |
270 | //! into a declarative macro which supplies type hints. |
271 | |
272 | #![deny (missing_debug_implementations)] |
273 | #![deny (missing_docs)] |
274 | #![deny (rust_2018_idioms)] |
275 | |
276 | mod error; |
277 | |
278 | use std::{ |
279 | cell::RefCell, |
280 | collections::HashMap, |
281 | env::{self, current_dir, VarError}, |
282 | ffi::{OsStr, OsString}, |
283 | fmt, fs, |
284 | io::{self, ErrorKind, Write}, |
285 | mem, |
286 | path::{Path, PathBuf}, |
287 | process::{Command, ExitStatus, Output, Stdio}, |
288 | sync::atomic::{AtomicUsize, Ordering}, |
289 | }; |
290 | |
291 | pub use crate::error::{Error, Result}; |
292 | #[doc (hidden)] |
293 | pub use xshell_macros::__cmd; |
294 | |
295 | /// Constructs a [`Cmd`] from the given string. |
296 | /// |
297 | /// # Examples |
298 | /// |
299 | /// Basic: |
300 | /// |
301 | /// ```no_run |
302 | /// # use xshell::{cmd, Shell}; |
303 | /// let sh = Shell::new()?; |
304 | /// cmd!(sh, "echo hello world" ).run()?; |
305 | /// # Ok::<(), xshell::Error>(()) |
306 | /// ``` |
307 | /// |
308 | /// Interpolation: |
309 | /// |
310 | /// ``` |
311 | /// # use xshell::{cmd, Shell}; let sh = Shell::new()?; |
312 | /// let greeting = "hello world" ; |
313 | /// let c = cmd!(sh, "echo {greeting}" ); |
314 | /// assert_eq!(c.to_string(), r#"echo "hello world""# ); |
315 | /// |
316 | /// let c = cmd!(sh, "echo '{greeting}'" ); |
317 | /// assert_eq!(c.to_string(), r#"echo {greeting}"# ); |
318 | /// |
319 | /// let c = cmd!(sh, "echo {greeting}!" ); |
320 | /// assert_eq!(c.to_string(), r#"echo "hello world!""# ); |
321 | /// |
322 | /// let c = cmd!(sh, "echo 'spaces '{greeting}' around'" ); |
323 | /// assert_eq!(c.to_string(), r#"echo "spaces hello world around""# ); |
324 | /// |
325 | /// # Ok::<(), xshell::Error>(()) |
326 | /// ``` |
327 | /// |
328 | /// Splat interpolation: |
329 | /// |
330 | /// ``` |
331 | /// # use xshell::{cmd, Shell}; let sh = Shell::new()?; |
332 | /// let args = ["hello" , "world" ]; |
333 | /// let c = cmd!(sh, "echo {args...}" ); |
334 | /// assert_eq!(c.to_string(), r#"echo hello world"# ); |
335 | /// |
336 | /// let arg1: Option<&str> = Some("hello" ); |
337 | /// let arg2: Option<&str> = None; |
338 | /// let c = cmd!(sh, "echo {arg1...} {arg2...}" ); |
339 | /// assert_eq!(c.to_string(), r#"echo hello"# ); |
340 | /// # Ok::<(), xshell::Error>(()) |
341 | /// ``` |
342 | #[macro_export ] |
343 | macro_rules! cmd { |
344 | ($sh:expr, $cmd:literal) => {{ |
345 | #[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)] |
346 | format_args!($cmd); |
347 | let f = |prog| $sh.cmd(prog); |
348 | let cmd: $crate::Cmd = $crate::__cmd!(f $cmd); |
349 | cmd |
350 | }}; |
351 | } |
352 | |
353 | /// A `Shell` is the main API entry point. |
354 | /// |
355 | /// Almost all of the crate's functionality is available as methods of the |
356 | /// `Shell` object. |
357 | /// |
358 | /// `Shell` is a stateful object. It maintains a logical working directory and |
359 | /// an environment map. They are independent from process's |
360 | /// [`std::env::current_dir`] and [`std::env::var`], and only affect paths and |
361 | /// commands passed to the [`Shell`]. |
362 | /// |
363 | /// |
364 | /// By convention, variable holding the shell is named `sh`. |
365 | /// |
366 | /// # Example |
367 | /// |
368 | /// ```no_run |
369 | /// use xshell::{cmd, Shell}; |
370 | /// |
371 | /// let sh = Shell::new()?; |
372 | /// let _d = sh.push_dir("./target" ); |
373 | /// let cwd = sh.current_dir(); |
374 | /// cmd!(sh, "echo current dir is {cwd}" ).run()?; |
375 | /// |
376 | /// let process_cwd = std::env::current_dir().unwrap(); |
377 | /// assert_eq!(cwd, process_cwd.join("./target" )); |
378 | /// # Ok::<(), xshell::Error>(()) |
379 | /// ``` |
380 | #[derive (Debug)] |
381 | pub struct Shell { |
382 | cwd: RefCell<PathBuf>, |
383 | env: RefCell<HashMap<OsString, OsString>>, |
384 | } |
385 | |
386 | impl std::panic::UnwindSafe for Shell {} |
387 | impl std::panic::RefUnwindSafe for Shell {} |
388 | |
389 | impl Shell { |
390 | /// Creates a new [`Shell`]. |
391 | /// |
392 | /// Fails if [`std::env::current_dir`] returns an error. |
393 | pub fn new() -> Result<Shell> { |
394 | let cwd = current_dir().map_err(|err| Error::new_current_dir(err, None))?; |
395 | let cwd = RefCell::new(cwd); |
396 | let env = RefCell::new(HashMap::new()); |
397 | Ok(Shell { cwd, env }) |
398 | } |
399 | |
400 | // region:env |
401 | /// Returns the working directory for this [`Shell`]. |
402 | /// |
403 | /// All relative paths are interpreted relative to this directory, rather |
404 | /// than [`std::env::current_dir`]. |
405 | #[doc (alias = "pwd" )] |
406 | pub fn current_dir(&self) -> PathBuf { |
407 | self.cwd.borrow().clone() |
408 | } |
409 | |
410 | /// Changes the working directory for this [`Shell`]. |
411 | /// |
412 | /// Note that this doesn't affect [`std::env::current_dir`]. |
413 | #[doc (alias = "pwd" )] |
414 | pub fn change_dir<P: AsRef<Path>>(&self, dir: P) { |
415 | self._change_dir(dir.as_ref()) |
416 | } |
417 | fn _change_dir(&self, dir: &Path) { |
418 | let dir = self.path(dir); |
419 | *self.cwd.borrow_mut() = dir; |
420 | } |
421 | |
422 | /// Temporary changes the working directory of this [`Shell`]. |
423 | /// |
424 | /// Returns a RAII guard which reverts the working directory to the old |
425 | /// value when dropped. |
426 | /// |
427 | /// Note that this doesn't affect [`std::env::current_dir`]. |
428 | #[doc (alias = "pushd" )] |
429 | pub fn push_dir<P: AsRef<Path>>(&self, path: P) -> PushDir<'_> { |
430 | self._push_dir(path.as_ref()) |
431 | } |
432 | fn _push_dir(&self, path: &Path) -> PushDir<'_> { |
433 | let path = self.path(path); |
434 | PushDir::new(self, path) |
435 | } |
436 | |
437 | /// Fetches the environmental variable `key` for this [`Shell`]. |
438 | /// |
439 | /// Returns an error if the variable is not set, or set to a non-utf8 value. |
440 | /// |
441 | /// Environment of the [`Shell`] affects all commands spawned via this |
442 | /// shell. |
443 | pub fn var<K: AsRef<OsStr>>(&self, key: K) -> Result<String> { |
444 | self._var(key.as_ref()) |
445 | } |
446 | fn _var(&self, key: &OsStr) -> Result<String> { |
447 | match self._var_os(key) { |
448 | Some(it) => it.into_string().map_err(VarError::NotUnicode), |
449 | None => Err(VarError::NotPresent), |
450 | } |
451 | .map_err(|err| Error::new_var(err, key.to_os_string())) |
452 | } |
453 | |
454 | /// Fetches the environmental variable `key` for this [`Shell`] as |
455 | /// [`OsString`] Returns [`None`] if the variable is not set. |
456 | /// |
457 | /// Environment of the [`Shell`] affects all commands spawned via this |
458 | /// shell. |
459 | pub fn var_os<K: AsRef<OsStr>>(&self, key: K) -> Option<OsString> { |
460 | self._var_os(key.as_ref()) |
461 | } |
462 | fn _var_os(&self, key: &OsStr) -> Option<OsString> { |
463 | self.env.borrow().get(key).cloned().or_else(|| env::var_os(key)) |
464 | } |
465 | |
466 | /// Sets the value of `key` environment variable for this [`Shell`] to |
467 | /// `val`. |
468 | /// |
469 | /// Note that this doesn't affect [`std::env::var`]. |
470 | pub fn set_var<K: AsRef<OsStr>, V: AsRef<OsStr>>(&self, key: K, val: V) { |
471 | self._set_var(key.as_ref(), val.as_ref()) |
472 | } |
473 | fn _set_var(&self, key: &OsStr, val: &OsStr) { |
474 | self.env.borrow_mut().insert(key.to_os_string(), val.to_os_string()); |
475 | } |
476 | |
477 | /// Temporary sets the value of `key` environment variable for this |
478 | /// [`Shell`] to `val`. |
479 | /// |
480 | /// Returns a RAII guard which restores the old environment when dropped. |
481 | /// |
482 | /// Note that this doesn't affect [`std::env::var`]. |
483 | pub fn push_env<K: AsRef<OsStr>, V: AsRef<OsStr>>(&self, key: K, val: V) -> PushEnv<'_> { |
484 | self._push_env(key.as_ref(), val.as_ref()) |
485 | } |
486 | fn _push_env(&self, key: &OsStr, val: &OsStr) -> PushEnv<'_> { |
487 | PushEnv::new(self, key.to_os_string(), val.to_os_string()) |
488 | } |
489 | // endregion:env |
490 | |
491 | // region:fs |
492 | /// Read the entire contents of a file into a string. |
493 | #[doc (alias = "cat" )] |
494 | pub fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<String> { |
495 | self._read_file(path.as_ref()) |
496 | } |
497 | fn _read_file(&self, path: &Path) -> Result<String> { |
498 | let path = self.path(path); |
499 | fs::read_to_string(&path).map_err(|err| Error::new_read_file(err, path)) |
500 | } |
501 | |
502 | /// Read the entire contents of a file into a vector of bytes. |
503 | pub fn read_binary_file<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> { |
504 | self._read_binary_file(path.as_ref()) |
505 | } |
506 | fn _read_binary_file(&self, path: &Path) -> Result<Vec<u8>> { |
507 | let path = self.path(path); |
508 | fs::read(&path).map_err(|err| Error::new_read_file(err, path)) |
509 | } |
510 | |
511 | /// Returns a sorted list of paths directly contained in the directory at |
512 | /// `path`. |
513 | #[doc (alias = "ls" )] |
514 | pub fn read_dir<P: AsRef<Path>>(&self, path: P) -> Result<Vec<PathBuf>> { |
515 | self._read_dir(path.as_ref()) |
516 | } |
517 | fn _read_dir(&self, path: &Path) -> Result<Vec<PathBuf>> { |
518 | let path = self.path(path); |
519 | let mut res = Vec::new(); |
520 | || -> _ { |
521 | for entry in fs::read_dir(&path)? { |
522 | let entry = entry?; |
523 | res.push(entry.path()) |
524 | } |
525 | Ok(()) |
526 | }() |
527 | .map_err(|err| Error::new_read_dir(err, path))?; |
528 | res.sort(); |
529 | Ok(res) |
530 | } |
531 | |
532 | /// Write a slice as the entire contents of a file. |
533 | /// |
534 | /// This function will create the file and all intermediate directories if |
535 | /// they don't exist. |
536 | // TODO: probably want to make this an atomic rename write? |
537 | pub fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(&self, path: P, contents: C) -> Result<()> { |
538 | self._write_file(path.as_ref(), contents.as_ref()) |
539 | } |
540 | fn _write_file(&self, path: &Path, contents: &[u8]) -> Result<()> { |
541 | let path = self.path(path); |
542 | if let Some(p) = path.parent() { |
543 | self.create_dir(p)?; |
544 | } |
545 | fs::write(&path, contents).map_err(|err| Error::new_write_file(err, path)) |
546 | } |
547 | |
548 | /// Copies `src` into `dst`. |
549 | /// |
550 | /// `src` must be a file, but `dst` need not be. If `dst` is an existing |
551 | /// directory, `src` will be copied into a file in the `dst` directory whose |
552 | /// name is same as that of `src`. |
553 | /// |
554 | /// Otherwise, `dst` is a file or does not exist, and `src` will be copied into |
555 | /// it. |
556 | #[doc (alias = "cp" )] |
557 | pub fn copy_file<S: AsRef<Path>, D: AsRef<Path>>(&self, src: S, dst: D) -> Result<()> { |
558 | self._copy_file(src.as_ref(), dst.as_ref()) |
559 | } |
560 | fn _copy_file(&self, src: &Path, dst: &Path) -> Result<()> { |
561 | let src = self.path(src); |
562 | let dst = self.path(dst); |
563 | let dst = dst.as_path(); |
564 | let mut _tmp; |
565 | let mut dst = dst; |
566 | if dst.is_dir() { |
567 | if let Some(file_name) = src.file_name() { |
568 | _tmp = dst.join(file_name); |
569 | dst = &_tmp; |
570 | } |
571 | } |
572 | std::fs::copy(&src, dst) |
573 | .map_err(|err| Error::new_copy_file(err, src.to_path_buf(), dst.to_path_buf()))?; |
574 | Ok(()) |
575 | } |
576 | |
577 | /// Hardlinks `src` to `dst`. |
578 | #[doc (alias = "ln" )] |
579 | pub fn hard_link<S: AsRef<Path>, D: AsRef<Path>>(&self, src: S, dst: D) -> Result<()> { |
580 | self._hard_link(src.as_ref(), dst.as_ref()) |
581 | } |
582 | fn _hard_link(&self, src: &Path, dst: &Path) -> Result<()> { |
583 | let src = self.path(src); |
584 | let dst = self.path(dst); |
585 | fs::hard_link(&src, &dst).map_err(|err| Error::new_hard_link(err, src, dst)) |
586 | } |
587 | |
588 | /// Creates the specified directory. |
589 | /// |
590 | /// All intermediate directories will also be created. |
591 | #[doc (alias("mkdir_p" , "mkdir" ))] |
592 | pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> { |
593 | self._create_dir(path.as_ref()) |
594 | } |
595 | fn _create_dir(&self, path: &Path) -> Result<PathBuf> { |
596 | let path = self.path(path); |
597 | match fs::create_dir_all(&path) { |
598 | Ok(()) => Ok(path), |
599 | Err(err) => Err(Error::new_create_dir(err, path)), |
600 | } |
601 | } |
602 | |
603 | /// Creates an empty named world-readable temporary directory. |
604 | /// |
605 | /// Returns a [`TempDir`] RAII guard with the path to the directory. When |
606 | /// dropped, the temporary directory and all of its contents will be |
607 | /// removed. |
608 | /// |
609 | /// Note that this is an **insecure method** -- any other process on the |
610 | /// system will be able to read the data. |
611 | #[doc (alias = "mktemp" )] |
612 | pub fn create_temp_dir(&self) -> Result<TempDir> { |
613 | let base = std::env::temp_dir(); |
614 | self.create_dir(&base)?; |
615 | |
616 | static CNT: AtomicUsize = AtomicUsize::new(0); |
617 | |
618 | let mut n_try = 0u32; |
619 | loop { |
620 | let cnt = CNT.fetch_add(1, Ordering::Relaxed); |
621 | let path = base.join(format!("xshell-tmp-dir- {}" , cnt)); |
622 | match fs::create_dir(&path) { |
623 | Ok(()) => return Ok(TempDir { path }), |
624 | Err(err) if n_try == 1024 => return Err(Error::new_create_dir(err, path)), |
625 | Err(_) => n_try += 1, |
626 | } |
627 | } |
628 | } |
629 | |
630 | /// Removes the file or directory at the given path. |
631 | #[doc (alias("rm_rf" , "rm" ))] |
632 | pub fn remove_path<P: AsRef<Path>>(&self, path: P) -> Result<()> { |
633 | self._remove_path(path.as_ref()) |
634 | } |
635 | fn _remove_path(&self, path: &Path) -> Result<(), Error> { |
636 | let path = self.path(path); |
637 | match path.metadata() { |
638 | Ok(meta) => if meta.is_dir() { remove_dir_all(&path) } else { fs::remove_file(&path) } |
639 | .map_err(|err| Error::new_remove_path(err, path)), |
640 | Err(err) if err.kind() == ErrorKind::NotFound => Ok(()), |
641 | Err(err) => Err(Error::new_remove_path(err, path)), |
642 | } |
643 | } |
644 | |
645 | /// Returns whether a file or directory exists at the given path. |
646 | #[doc (alias("stat" ))] |
647 | pub fn path_exists<P: AsRef<Path>>(&self, path: P) -> bool { |
648 | self.path(path.as_ref()).exists() |
649 | } |
650 | // endregion:fs |
651 | |
652 | /// Creates a new [`Cmd`] that executes the given `program`. |
653 | pub fn cmd<P: AsRef<Path>>(&self, program: P) -> Cmd<'_> { |
654 | // TODO: path lookup? |
655 | Cmd::new(self, program.as_ref()) |
656 | } |
657 | |
658 | fn path(&self, p: &Path) -> PathBuf { |
659 | let cd = self.cwd.borrow(); |
660 | cd.join(p) |
661 | } |
662 | } |
663 | |
664 | /// RAII guard returned from [`Shell::push_dir`]. |
665 | /// |
666 | /// Dropping `PushDir` restores the working directory of the [`Shell`] to the |
667 | /// old value. |
668 | #[derive (Debug)] |
669 | #[must_use ] |
670 | pub struct PushDir<'a> { |
671 | old_cwd: PathBuf, |
672 | shell: &'a Shell, |
673 | } |
674 | |
675 | impl<'a> PushDir<'a> { |
676 | fn new(shell: &'a Shell, path: PathBuf) -> PushDir<'a> { |
677 | PushDir { old_cwd: mem::replace(&mut *shell.cwd.borrow_mut(), src:path), shell } |
678 | } |
679 | } |
680 | |
681 | impl Drop for PushDir<'_> { |
682 | fn drop(&mut self) { |
683 | mem::swap(&mut *self.shell.cwd.borrow_mut(), &mut self.old_cwd) |
684 | } |
685 | } |
686 | |
687 | /// RAII guard returned from [`Shell::push_env`]. |
688 | /// |
689 | /// Dropping `PushEnv` restores the old value of the environmental variable. |
690 | #[derive (Debug)] |
691 | #[must_use ] |
692 | pub struct PushEnv<'a> { |
693 | key: OsString, |
694 | old_value: Option<OsString>, |
695 | shell: &'a Shell, |
696 | } |
697 | |
698 | impl<'a> PushEnv<'a> { |
699 | fn new(shell: &'a Shell, key: OsString, val: OsString) -> PushEnv<'a> { |
700 | let old_value: Option = shell.env.borrow_mut().insert(k:key.clone(), v:val); |
701 | PushEnv { shell, key, old_value } |
702 | } |
703 | } |
704 | |
705 | impl Drop for PushEnv<'_> { |
706 | fn drop(&mut self) { |
707 | let mut env: RefMut<'_, HashMap> = self.shell.env.borrow_mut(); |
708 | let key: OsString = mem::take(&mut self.key); |
709 | match self.old_value.take() { |
710 | Some(value: OsString) => { |
711 | env.insert(k:key, v:value); |
712 | } |
713 | None => { |
714 | env.remove(&key); |
715 | } |
716 | } |
717 | } |
718 | } |
719 | |
720 | /// A builder object for constructing a subprocess. |
721 | /// |
722 | /// A [`Cmd`] is usually created with the [`cmd!`] macro. The command exists |
723 | /// within a context of a [`Shell`] and uses its working directory and |
724 | /// environment. |
725 | /// |
726 | /// # Example |
727 | /// |
728 | /// ```no_run |
729 | /// use xshell::{Shell, cmd}; |
730 | /// |
731 | /// let sh = Shell::new()?; |
732 | /// |
733 | /// let branch = "main" ; |
734 | /// let cmd = cmd!(sh, "git switch {branch}" ).quiet().run()?; |
735 | /// # Ok::<(), xshell::Error>(()) |
736 | /// ``` |
737 | #[derive (Debug)] |
738 | #[must_use ] |
739 | pub struct Cmd<'a> { |
740 | shell: &'a Shell, |
741 | data: CmdData, |
742 | } |
743 | |
744 | #[derive (Debug, Default, Clone)] |
745 | struct CmdData { |
746 | prog: PathBuf, |
747 | args: Vec<OsString>, |
748 | env_changes: Vec<EnvChange>, |
749 | ignore_status: bool, |
750 | quiet: bool, |
751 | secret: bool, |
752 | stdin_contents: Option<Vec<u8>>, |
753 | ignore_stdout: bool, |
754 | ignore_stderr: bool, |
755 | } |
756 | |
757 | // We just store a list of functions to call on the `Command` — the alternative |
758 | // would require mirroring the logic that `std::process::Command` (or rather |
759 | // `sys_common::CommandEnvs`) uses, which is moderately complex, involves |
760 | // special-casing `PATH`, and plausibly could change. |
761 | #[derive (Debug, Clone)] |
762 | enum EnvChange { |
763 | Set(OsString, OsString), |
764 | Remove(OsString), |
765 | Clear, |
766 | } |
767 | |
768 | impl fmt::Display for Cmd<'_> { |
769 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
770 | fmt::Display::fmt(&self.data, f) |
771 | } |
772 | } |
773 | |
774 | impl fmt::Display for CmdData { |
775 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
776 | if self.secret { |
777 | return write!(f, "<secret>" ); |
778 | } |
779 | |
780 | write!(f, " {}" , self.prog.display())?; |
781 | for arg: &OsString in &self.args { |
782 | // TODO: this is potentially not copy-paste safe. |
783 | let arg: Cow<'_, str> = arg.to_string_lossy(); |
784 | if arg.chars().any(|it: char| it.is_ascii_whitespace()) { |
785 | write!(f, " \"{}\"" , arg.escape_default())? |
786 | } else { |
787 | write!(f, " {}" , arg)? |
788 | }; |
789 | } |
790 | Ok(()) |
791 | } |
792 | } |
793 | |
794 | impl From<Cmd<'_>> for Command { |
795 | fn from(cmd: Cmd<'_>) -> Command { |
796 | cmd.to_command() |
797 | } |
798 | } |
799 | |
800 | impl<'a> Cmd<'a> { |
801 | fn new(shell: &'a Shell, prog: &Path) -> Cmd<'a> { |
802 | let mut data = CmdData::default(); |
803 | data.prog = prog.to_path_buf(); |
804 | Cmd { shell, data } |
805 | } |
806 | |
807 | // region:builder |
808 | /// Adds an argument to this commands. |
809 | pub fn arg<P: AsRef<OsStr>>(mut self, arg: P) -> Cmd<'a> { |
810 | self._arg(arg.as_ref()); |
811 | self |
812 | } |
813 | fn _arg(&mut self, arg: &OsStr) { |
814 | self.data.args.push(arg.to_owned()) |
815 | } |
816 | |
817 | /// Adds all of the arguments to this command. |
818 | pub fn args<I>(mut self, args: I) -> Cmd<'a> |
819 | where |
820 | I: IntoIterator, |
821 | I::Item: AsRef<OsStr>, |
822 | { |
823 | args.into_iter().for_each(|it| self._arg(it.as_ref())); |
824 | self |
825 | } |
826 | |
827 | #[doc (hidden)] |
828 | pub fn __extend_arg<P: AsRef<OsStr>>(mut self, arg_fragment: P) -> Cmd<'a> { |
829 | self.___extend_arg(arg_fragment.as_ref()); |
830 | self |
831 | } |
832 | fn ___extend_arg(&mut self, arg_fragment: &OsStr) { |
833 | match self.data.args.last_mut() { |
834 | Some(last_arg) => last_arg.push(arg_fragment), |
835 | None => { |
836 | let mut prog = mem::take(&mut self.data.prog).into_os_string(); |
837 | prog.push(arg_fragment); |
838 | self.data.prog = prog.into(); |
839 | } |
840 | } |
841 | } |
842 | |
843 | /// Overrides the value of the environmental variable for this command. |
844 | pub fn env<K: AsRef<OsStr>, V: AsRef<OsStr>>(mut self, key: K, val: V) -> Cmd<'a> { |
845 | self._env_set(key.as_ref(), val.as_ref()); |
846 | self |
847 | } |
848 | |
849 | fn _env_set(&mut self, key: &OsStr, val: &OsStr) { |
850 | self.data.env_changes.push(EnvChange::Set(key.to_owned(), val.to_owned())); |
851 | } |
852 | |
853 | /// Overrides the values of specified environmental variables for this |
854 | /// command. |
855 | pub fn envs<I, K, V>(mut self, vars: I) -> Cmd<'a> |
856 | where |
857 | I: IntoIterator<Item = (K, V)>, |
858 | K: AsRef<OsStr>, |
859 | V: AsRef<OsStr>, |
860 | { |
861 | vars.into_iter().for_each(|(k, v)| self._env_set(k.as_ref(), v.as_ref())); |
862 | self |
863 | } |
864 | |
865 | /// Removes the environment variable from this command. |
866 | pub fn env_remove<K: AsRef<OsStr>>(mut self, key: K) -> Cmd<'a> { |
867 | self._env_remove(key.as_ref()); |
868 | self |
869 | } |
870 | fn _env_remove(&mut self, key: &OsStr) { |
871 | self.data.env_changes.push(EnvChange::Remove(key.to_owned())); |
872 | } |
873 | |
874 | /// Removes all of the environment variables from this command. |
875 | pub fn env_clear(mut self) -> Cmd<'a> { |
876 | self.data.env_changes.push(EnvChange::Clear); |
877 | self |
878 | } |
879 | |
880 | /// Don't return an error if command the command exits with non-zero status. |
881 | /// |
882 | /// By default, non-zero exit status is considered an error. |
883 | pub fn ignore_status(mut self) -> Cmd<'a> { |
884 | self.set_ignore_status(true); |
885 | self |
886 | } |
887 | /// Controls whether non-zero exit status is considered an error. |
888 | pub fn set_ignore_status(&mut self, yes: bool) { |
889 | self.data.ignore_status = yes; |
890 | } |
891 | |
892 | /// Don't echo the command itself to stderr. |
893 | /// |
894 | /// By default, the command itself will be printed to stderr when executed via [`Cmd::run`]. |
895 | pub fn quiet(mut self) -> Cmd<'a> { |
896 | self.set_quiet(true); |
897 | self |
898 | } |
899 | /// Controls whether the command itself is printed to stderr. |
900 | pub fn set_quiet(&mut self, yes: bool) { |
901 | self.data.quiet = yes; |
902 | } |
903 | |
904 | /// Marks the command as secret. |
905 | /// |
906 | /// If a command is secret, it echoes `<secret>` instead of the program and |
907 | /// its arguments, even in error messages. |
908 | pub fn secret(mut self) -> Cmd<'a> { |
909 | self.set_secret(true); |
910 | self |
911 | } |
912 | /// Controls whether the command is secret. |
913 | pub fn set_secret(&mut self, yes: bool) { |
914 | self.data.secret = yes; |
915 | } |
916 | |
917 | /// Pass the given slice to the standard input of the spawned process. |
918 | pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd<'a> { |
919 | self._stdin(stdin.as_ref()); |
920 | self |
921 | } |
922 | fn _stdin(&mut self, stdin: &[u8]) { |
923 | self.data.stdin_contents = Some(stdin.to_vec()); |
924 | } |
925 | |
926 | /// Ignores the standard output stream of the process. |
927 | /// |
928 | /// This is equivalent to redirecting stdout to `/dev/null`. By default, the |
929 | /// stdout is inherited or captured. |
930 | pub fn ignore_stdout(mut self) -> Cmd<'a> { |
931 | self.set_ignore_stdout(true); |
932 | self |
933 | } |
934 | /// Controls whether the standard output is ignored. |
935 | pub fn set_ignore_stdout(&mut self, yes: bool) { |
936 | self.data.ignore_stdout = yes; |
937 | } |
938 | |
939 | /// Ignores the standard output stream of the process. |
940 | /// |
941 | /// This is equivalent redirecting stderr to `/dev/null`. By default, the |
942 | /// stderr is inherited or captured. |
943 | pub fn ignore_stderr(mut self) -> Cmd<'a> { |
944 | self.set_ignore_stderr(true); |
945 | self |
946 | } |
947 | /// Controls whether the standard error is ignored. |
948 | pub fn set_ignore_stderr(&mut self, yes: bool) { |
949 | self.data.ignore_stderr = yes; |
950 | } |
951 | // endregion:builder |
952 | |
953 | // region:running |
954 | /// Runs the command. |
955 | /// |
956 | /// By default the command itself is echoed to stderr, its standard streams |
957 | /// are inherited, and non-zero return code is considered an error. These |
958 | /// behaviors can be overridden by using various builder methods of the [`Cmd`]. |
959 | pub fn run(&self) -> Result<()> { |
960 | if !self.data.quiet { |
961 | eprintln!("$ {}" , self); |
962 | } |
963 | self.output_impl(false, false).map(|_| ()) |
964 | } |
965 | |
966 | /// Run the command and return its stdout as a string. |
967 | pub fn read(&self) -> Result<String> { |
968 | self.read_stream(false) |
969 | } |
970 | |
971 | /// Run the command and return its stderr as a string. |
972 | pub fn read_stderr(&self) -> Result<String> { |
973 | self.read_stream(true) |
974 | } |
975 | |
976 | /// Run the command and return its output. |
977 | pub fn output(&self) -> Result<Output> { |
978 | self.output_impl(true, true) |
979 | } |
980 | // endregion:running |
981 | |
982 | fn read_stream(&self, read_stderr: bool) -> Result<String> { |
983 | let read_stdout = !read_stderr; |
984 | let output = self.output_impl(read_stdout, read_stderr)?; |
985 | self.check_status(output.status)?; |
986 | |
987 | let stream = if read_stderr { output.stderr } else { output.stdout }; |
988 | let mut stream = String::from_utf8(stream).map_err(|err| Error::new_cmd_utf8(self, err))?; |
989 | |
990 | if stream.ends_with(' \n' ) { |
991 | stream.pop(); |
992 | } |
993 | if stream.ends_with(' \r' ) { |
994 | stream.pop(); |
995 | } |
996 | |
997 | Ok(stream) |
998 | } |
999 | |
1000 | fn output_impl(&self, read_stdout: bool, read_stderr: bool) -> Result<Output> { |
1001 | let mut child = { |
1002 | let mut command = self.to_command(); |
1003 | |
1004 | if !self.data.ignore_stdout { |
1005 | command.stdout(if read_stdout { Stdio::piped() } else { Stdio::inherit() }); |
1006 | } |
1007 | if !self.data.ignore_stderr { |
1008 | command.stderr(if read_stderr { Stdio::piped() } else { Stdio::inherit() }); |
1009 | } |
1010 | |
1011 | command.stdin(match &self.data.stdin_contents { |
1012 | Some(_) => Stdio::piped(), |
1013 | None => Stdio::null(), |
1014 | }); |
1015 | |
1016 | command.spawn().map_err(|err| { |
1017 | // Try to determine whether the command failed because the current |
1018 | // directory does not exist. Return an appropriate error in such a |
1019 | // case. |
1020 | if matches!(err.kind(), io::ErrorKind::NotFound) { |
1021 | let cwd = self.shell.cwd.borrow(); |
1022 | if let Err(err) = cwd.metadata() { |
1023 | return Error::new_current_dir(err, Some(cwd.clone())); |
1024 | } |
1025 | } |
1026 | Error::new_cmd_io(self, err) |
1027 | })? |
1028 | }; |
1029 | |
1030 | let mut io_thread = None; |
1031 | if let Some(stdin_contents) = self.data.stdin_contents.clone() { |
1032 | let mut stdin = child.stdin.take().unwrap(); |
1033 | io_thread = Some(std::thread::spawn(move || { |
1034 | stdin.write_all(&stdin_contents)?; |
1035 | stdin.flush() |
1036 | })); |
1037 | } |
1038 | let out_res = child.wait_with_output(); |
1039 | let err_res = io_thread.map(|it| it.join().unwrap()); |
1040 | let output = out_res.map_err(|err| Error::new_cmd_io(self, err))?; |
1041 | if let Some(err_res) = err_res { |
1042 | err_res.map_err(|err| Error::new_cmd_stdin(self, err))?; |
1043 | } |
1044 | self.check_status(output.status)?; |
1045 | Ok(output) |
1046 | } |
1047 | |
1048 | fn to_command(&self) -> Command { |
1049 | let mut res = Command::new(&self.data.prog); |
1050 | res.current_dir(self.shell.current_dir()); |
1051 | res.args(&self.data.args); |
1052 | |
1053 | for (key, val) in &*self.shell.env.borrow() { |
1054 | res.env(key, val); |
1055 | } |
1056 | for change in &self.data.env_changes { |
1057 | match change { |
1058 | EnvChange::Clear => res.env_clear(), |
1059 | EnvChange::Remove(key) => res.env_remove(key), |
1060 | EnvChange::Set(key, val) => res.env(key, val), |
1061 | }; |
1062 | } |
1063 | |
1064 | if self.data.ignore_stdout { |
1065 | res.stdout(Stdio::null()); |
1066 | } |
1067 | |
1068 | if self.data.ignore_stderr { |
1069 | res.stderr(Stdio::null()); |
1070 | } |
1071 | |
1072 | res |
1073 | } |
1074 | |
1075 | fn check_status(&self, status: ExitStatus) -> Result<()> { |
1076 | if status.success() || self.data.ignore_status { |
1077 | return Ok(()); |
1078 | } |
1079 | Err(Error::new_cmd_status(self, status)) |
1080 | } |
1081 | } |
1082 | |
1083 | /// A temporary directory. |
1084 | /// |
1085 | /// This is a RAII object which will remove the underlying temporary directory |
1086 | /// when dropped. |
1087 | #[derive (Debug)] |
1088 | #[must_use ] |
1089 | pub struct TempDir { |
1090 | path: PathBuf, |
1091 | } |
1092 | |
1093 | impl TempDir { |
1094 | /// Returns the path to the underlying temporary directory. |
1095 | pub fn path(&self) -> &Path { |
1096 | &self.path |
1097 | } |
1098 | } |
1099 | |
1100 | impl Drop for TempDir { |
1101 | fn drop(&mut self) { |
1102 | let _ = remove_dir_all(&self.path); |
1103 | } |
1104 | } |
1105 | |
1106 | #[cfg (not(windows))] |
1107 | fn remove_dir_all(path: &Path) -> io::Result<()> { |
1108 | std::fs::remove_dir_all(path) |
1109 | } |
1110 | |
1111 | #[cfg (windows)] |
1112 | fn remove_dir_all(path: &Path) -> io::Result<()> { |
1113 | for _ in 0..99 { |
1114 | if fs::remove_dir_all(path).is_ok() { |
1115 | return Ok(()); |
1116 | } |
1117 | std::thread::sleep(std::time::Duration::from_millis(10)) |
1118 | } |
1119 | fs::remove_dir_all(path) |
1120 | } |
1121 | |