summaryrefslogtreecommitdiffstats
path: root/vendor/xshell
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
commit698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch)
tree173a775858bd501c378080a10dca74132f05bc50 /vendor/xshell
parentInitial commit. (diff)
downloadrustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.tar.xz
rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.zip
Adding upstream version 1.64.0+dfsg1.upstream/1.64.0+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/xshell')
-rw-r--r--vendor/xshell/.cargo-checksum.json1
-rw-r--r--vendor/xshell/CHANGELOG.md85
-rw-r--r--vendor/xshell/Cargo.lock23
-rw-r--r--vendor/xshell/Cargo.toml27
-rw-r--r--vendor/xshell/LICENSE-APACHE201
-rw-r--r--vendor/xshell/LICENSE-MIT23
-rw-r--r--vendor/xshell/README.md39
-rw-r--r--vendor/xshell/examples/ci.rs112
-rw-r--r--vendor/xshell/examples/clone_and_publish.rs28
-rw-r--r--vendor/xshell/src/error.rs177
-rw-r--r--vendor/xshell/src/lib.rs1112
-rw-r--r--vendor/xshell/tests/compile_time.rs41
-rw-r--r--vendor/xshell/tests/data/xecho.rs85
-rw-r--r--vendor/xshell/tests/it/compile_failures.rs128
-rw-r--r--vendor/xshell/tests/it/env.rs130
-rw-r--r--vendor/xshell/tests/it/main.rs462
-rw-r--r--vendor/xshell/tests/it/tidy.rs37
17 files changed, 2711 insertions, 0 deletions
diff --git a/vendor/xshell/.cargo-checksum.json b/vendor/xshell/.cargo-checksum.json
new file mode 100644
index 000000000..dd079af53
--- /dev/null
+++ b/vendor/xshell/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"CHANGELOG.md":"dd08a5079cd2e437eb1269f7c60be9c05ce97db407dbb95a631d64c0f1c478a1","Cargo.lock":"f8f5019959ed5fcd772c3cf199220be41ce1e7b2b9a35b153c52f9c8f3a35171","Cargo.toml":"04a9cad4d6a6e7c756c35d3ba776334f0c47b895e54f7006a27617430c84b25f","LICENSE-APACHE":"a9040321c3712d8fd0b09cf52b17445de04a23a10165049ae187cd39e5c86be5","LICENSE-MIT":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3","README.md":"a480b1b8b943c633c64ce9c215f8ecea24e58c7242fe4605d37e7a3be810ab9f","examples/ci.rs":"d5fbfc199469c08f3d459164c05a85c1a0a8f1bef625b347533ad7a43c1e97fb","examples/clone_and_publish.rs":"94568ef665e65527417bb5d50b0404bc60d6a72942d70260c8f3ce1a99820077","src/error.rs":"9222d0b21a889c9fbac1d285c6d43573be9c94f0ea5c02b4bd692bdc02753b49","src/lib.rs":"61d7c1dcd569e60188baba51521661c177bff5a27bcf5272bd83f06542c2304a","tests/compile_time.rs":"224f3476eff4070fbd62c1974e7e69996efd92263bc12f7ddbd59823d85484da","tests/data/xecho.rs":"7a82252daade541bc3843fffa617fc50bf2faf7eebc55e4442a0bc9bb59182fd","tests/it/compile_failures.rs":"c87a438583c9f4b4e45a7422df3ee7c6bd5e69150eba5468425c63aa70fa47d3","tests/it/env.rs":"e863965669378e603c36186e1c738914e3d2300cbe3b04288a9ed689edcf09fb","tests/it/main.rs":"1bb089455f92d6486bf80502ebbbd1f136248194b9c454949f21ed279ea58028","tests/it/tidy.rs":"f530cf51504d43716e849ac96fb64a3a6ef80bd3e56b6eb1bd7b5325dc2f2de9"},"package":"6d47097dc5c85234b1e41851b3422dd6d19b3befdd35b4ae5ce386724aeca981"} \ No newline at end of file
diff --git a/vendor/xshell/CHANGELOG.md b/vendor/xshell/CHANGELOG.md
new file mode 100644
index 000000000..cbef62a3e
--- /dev/null
+++ b/vendor/xshell/CHANGELOG.md
@@ -0,0 +1,85 @@
+# Changelog
+
+## 0.2.2
+
+- Add `Shell::path_exists`.
+
+## 0.2.1
+
+- `Shell::remove_path` returns `Ok` if the path does not exist (ie the function
+ is now idempotent).
+
+## 0.2.0
+
+A major release with significant changes to the API:
+
+- All global state is removed in favor of explicitly passing a `Shell` instance.
+- Some methods are renamed to better match Rust naming conventions.
+- New APIs for controlling working directory and environment.
+- MSRV is raised to 1.59.0.
+- Improved reliability across the board: the crate aims to become a dependable
+ 1.0 tool in the future (no ETA).
+- This is expected to be the last *large* API reshuffle.
+
+## 0.1.17
+
+- Allow panics to transparently pass through xshell calls.
+ This removes some internal lock poisoned errors.
+
+## 0.1.16
+
+- Add `xshell::hard_link`.
+
+## 0.1.15
+
+- Correctly handle multiple internal read guards.
+
+## 0.1.14
+
+- Correctly handle commands name starting with quote.
+
+## 0.1.13
+
+- Add `ignore_stdout`, `ignore_stderr` functions.
+
+## 0.1.12
+
+- Add `env`, `env_revome`, `env_clear` functions.
+
+## 0.1.11
+
+- `write_file` now creates the intervening directory path if it doesn't exit.
+
+## 0.1.10
+
+- `echo_cmd` output goes to stderr, not stdout.
+
+## 0.1.9
+
+- `mktemp_d` creates an (insecure, world readable) temporary directory.
+- Fix cp docs.
+
+## 0.1.8
+
+- Add option to not echo command at all.
+- Add option to censor command contents when echoing.
+- Add docs.
+
+## 0.1.7
+
+- `cp(foo, bar)` copies `foo` _into_ `bar`, if `bar` is an existing directory.
+- Tweak reading API.
+
+## 0.1.6
+
+- `.read()` chomps `\r\n` on Windows.
+- Prevent cwd/env races when using `.read()` or `.run()`.
+- Better spans in error messages.
+
+## 0.1.5
+
+- Improve proc-macro error messages.
+
+## 0.1.4
+
+- No changelog until this point :(
diff --git a/vendor/xshell/Cargo.lock b/vendor/xshell/Cargo.lock
new file mode 100644
index 000000000..1ea0d8ff2
--- /dev/null
+++ b/vendor/xshell/Cargo.lock
@@ -0,0 +1,23 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "anyhow"
+version = "1.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
+
+[[package]]
+name = "xshell"
+version = "0.2.2"
+dependencies = [
+ "anyhow",
+ "xshell-macros",
+]
+
+[[package]]
+name = "xshell-macros"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88301b56c26dd9bf5c43d858538f82d6f3f7764767defbc5d34e59459901c41a"
diff --git a/vendor/xshell/Cargo.toml b/vendor/xshell/Cargo.toml
new file mode 100644
index 000000000..f4fb7cff2
--- /dev/null
+++ b/vendor/xshell/Cargo.toml
@@ -0,0 +1,27 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+rust-version = "1.59"
+name = "xshell"
+version = "0.2.2"
+authors = ["Aleksey Kladov <aleksey.kladov@gmail.com>"]
+exclude = [".github/", "bors.toml", "rustfmt.toml", "cbench", "mock_bin/"]
+description = "Utilities for quick shell scripting in Rust"
+categories = ["development-tools::build-utils", "filesystem"]
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/matklad/xshell"
+resolver = "2"
+[dependencies.xshell-macros]
+version = "=0.2.2"
+[dev-dependencies.anyhow]
+version = "1.0.56"
diff --git a/vendor/xshell/LICENSE-APACHE b/vendor/xshell/LICENSE-APACHE
new file mode 100644
index 000000000..78173fa2e
--- /dev/null
+++ b/vendor/xshell/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/vendor/xshell/LICENSE-MIT b/vendor/xshell/LICENSE-MIT
new file mode 100644
index 000000000..31aa79387
--- /dev/null
+++ b/vendor/xshell/LICENSE-MIT
@@ -0,0 +1,23 @@
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/vendor/xshell/README.md b/vendor/xshell/README.md
new file mode 100644
index 000000000..36026cb10
--- /dev/null
+++ b/vendor/xshell/README.md
@@ -0,0 +1,39 @@
+# xshell: Making Rust a Better Bash
+
+`xshell` provides a set of cross-platform utilities for writing cross-platform
+and ergonomic "bash" scripts.
+
+## Example
+
+```rust
+//! Clones a git repository and publishes it to crates.io.
+use xshell::{cmd, Shell};
+
+fn main() -> anyhow::Result<()> {
+ let sh = Shell::new()?;
+
+ let user = "matklad";
+ let repo = "xshell";
+ cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?;
+ sh.change_dir(repo);
+
+ let test_args = ["-Zunstable-options", "--report-time"];
+ cmd!(sh, "cargo test -- {test_args...}").run()?;
+
+ let manifest = sh.read_file("Cargo.toml")?;
+ let version = manifest
+ .split_once("version = \"")
+ .and_then(|it| it.1.split_once('\"'))
+ .map(|it| it.0)
+ .ok_or_else(|| anyhow::format_err!("can't find version field in the manifest"))?;
+
+ cmd!(sh, "git tag {version}").run()?;
+
+ let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") };
+ cmd!(sh, "cargo publish {dry_run...}").run()?;
+
+ Ok(())
+}
+```
+
+See [the docs](https://docs.rs/xshell) for more.
diff --git a/vendor/xshell/examples/ci.rs b/vendor/xshell/examples/ci.rs
new file mode 100644
index 000000000..799b28824
--- /dev/null
+++ b/vendor/xshell/examples/ci.rs
@@ -0,0 +1,112 @@
+//! This CI script for `xshell`.
+//!
+//! It also serves as a real-world example, yay bootstrap!
+use std::{env, process, thread, time::Duration, time::Instant};
+
+use xshell::{cmd, Result, Shell};
+
+fn main() {
+ if let Err(err) = try_main() {
+ eprintln!("{}", err);
+ process::exit(1);
+ }
+}
+
+fn try_main() -> Result<()> {
+ let sh = Shell::new()?;
+ if env::args().nth(1).as_deref() == Some("publish") {
+ publish(&sh)
+ } else {
+ test(&sh)
+ }
+}
+
+fn test(sh: &Shell) -> Result<()> {
+ // Can't delete oneself on Windows.
+ if !cfg!(windows) {
+ sh.remove_path("./target")?;
+ }
+
+ {
+ let _s = Section::new("BUILD");
+ cmd!(sh, "cargo test --workspace --no-run").run()?;
+ }
+
+ {
+ let _s = Section::new("TEST");
+ cmd!(sh, "cargo test --workspace").run()?;
+ }
+ Ok(())
+}
+
+fn publish(sh: &Shell) -> Result<()> {
+ let _s = Section::new("PUBLISH");
+ let manifest = sh.read_file("./Cargo.toml")?;
+
+ let version = manifest
+ .lines()
+ .find_map(|line| {
+ let words = line.split_ascii_whitespace().collect::<Vec<_>>();
+ match words.as_slice() {
+ [n, "=", v, ..] if n.trim() == "version" => {
+ assert!(v.starts_with('"') && v.ends_with('"'));
+ return Some(&v[1..v.len() - 1]);
+ }
+ _ => None,
+ }
+ })
+ .unwrap();
+
+ let tag = format!("v{}", version);
+ let tags = cmd!(sh, "git tag --list").read()?;
+ let tag_exists = tags.split_ascii_whitespace().any(|it| it == &tag);
+
+ let current_branch = cmd!(sh, "git branch --show-current").read()?;
+
+ if current_branch == "master" && !tag_exists {
+ cmd!(sh, "git tag v{version}").run()?;
+
+ let token = sh.var("CRATES_IO_TOKEN").unwrap_or("DUMMY_TOKEN".to_string());
+ {
+ let _p = sh.push_dir("xshell-macros");
+ cmd!(sh, "cargo publish --token {token}").run()?;
+ for _ in 0..100 {
+ thread::sleep(Duration::from_secs(3));
+ let err_msg =
+ cmd!(sh, "cargo install xshell-macros --version {version} --bin non-existing")
+ .ignore_status()
+ .read_stderr()?;
+
+ let not_found = err_msg.contains("could not find ");
+ let tried_installing = err_msg.contains("Installing");
+ assert!(not_found ^ tried_installing);
+ if tried_installing {
+ break;
+ }
+ }
+ }
+ cmd!(sh, "cargo publish --token {token}").run()?;
+ cmd!(sh, "git push --tags").run()?;
+ }
+ Ok(())
+}
+
+struct Section {
+ name: &'static str,
+ start: Instant,
+}
+
+impl Section {
+ fn new(name: &'static str) -> Section {
+ println!("::group::{}", name);
+ let start = Instant::now();
+ Section { name, start }
+ }
+}
+
+impl Drop for Section {
+ fn drop(&mut self) {
+ println!("{}: {:.2?}", self.name, self.start.elapsed());
+ println!("::endgroup::");
+ }
+}
diff --git a/vendor/xshell/examples/clone_and_publish.rs b/vendor/xshell/examples/clone_and_publish.rs
new file mode 100644
index 000000000..e18d9e200
--- /dev/null
+++ b/vendor/xshell/examples/clone_and_publish.rs
@@ -0,0 +1,28 @@
+//! Clones a git repository and publishes it to crates.io.
+use xshell::{cmd, Shell};
+
+fn main() -> anyhow::Result<()> {
+ let sh = Shell::new()?;
+
+ let user = "matklad";
+ let repo = "xshell";
+ cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?;
+ sh.change_dir(repo);
+
+ let test_args = ["-Zunstable-options", "--report-time"];
+ cmd!(sh, "cargo test -- {test_args...}").run()?;
+
+ let manifest = sh.read_file("Cargo.toml")?;
+ let version = manifest
+ .split_once("version = \"")
+ .and_then(|it| it.1.split_once('\"'))
+ .map(|it| it.0)
+ .ok_or_else(|| anyhow::format_err!("can't find version field in the manifest"))?;
+
+ cmd!(sh, "git tag {version}").run()?;
+
+ let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") };
+ cmd!(sh, "cargo publish {dry_run...}").run()?;
+
+ Ok(())
+}
diff --git a/vendor/xshell/src/error.rs b/vendor/xshell/src/error.rs
new file mode 100644
index 000000000..3e0f5dc3f
--- /dev/null
+++ b/vendor/xshell/src/error.rs
@@ -0,0 +1,177 @@
+use std::{env, ffi::OsString, fmt, io, path::PathBuf, process::ExitStatus, string::FromUtf8Error};
+
+use crate::{Cmd, CmdData};
+
+/// `Result` from std, with the error type defaulting to xshell's [`Error`].
+pub type Result<T, E = Error> = std::result::Result<T, E>;
+
+/// An error returned by an `xshell` operation.
+pub struct Error {
+ kind: Box<ErrorKind>,
+}
+
+/// Note: this is intentionally not public.
+enum ErrorKind {
+ CurrentDir { err: io::Error },
+ Var { err: env::VarError, var: OsString },
+ ReadFile { err: io::Error, path: PathBuf },
+ ReadDir { err: io::Error, path: PathBuf },
+ WriteFile { err: io::Error, path: PathBuf },
+ CopyFile { err: io::Error, src: PathBuf, dst: PathBuf },
+ HardLink { err: io::Error, src: PathBuf, dst: PathBuf },
+ CreateDir { err: io::Error, path: PathBuf },
+ RemovePath { err: io::Error, path: PathBuf },
+ CmdStatus { cmd: CmdData, status: ExitStatus },
+ CmdIo { err: io::Error, cmd: CmdData },
+ CmdUtf8 { err: FromUtf8Error, cmd: CmdData },
+ CmdStdin { err: io::Error, cmd: CmdData },
+}
+
+impl From<ErrorKind> for Error {
+ fn from(kind: ErrorKind) -> Error {
+ let kind = Box::new(kind);
+ Error { kind }
+ }
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match &*self.kind {
+ ErrorKind::CurrentDir { err } => write!(f, "failed to get current directory: {err}"),
+ ErrorKind::Var { err, var } => {
+ let var = var.to_string_lossy();
+ write!(f, "failed to get environment variable `{var}`: {err}")
+ }
+ ErrorKind::ReadFile { err, path } => {
+ let path = path.display();
+ write!(f, "failed to read file `{path}`: {err}")
+ }
+ ErrorKind::ReadDir { err, path } => {
+ let path = path.display();
+ write!(f, "failed read directory `{path}`: {err}")
+ }
+ ErrorKind::WriteFile { err, path } => {
+ let path = path.display();
+ write!(f, "failed to write file `{path}`: {err}")
+ }
+ ErrorKind::CopyFile { err, src, dst } => {
+ let src = src.display();
+ let dst = dst.display();
+ write!(f, "failed to copy `{src}` to `{dst}`: {err}")
+ }
+ ErrorKind::HardLink { err, src, dst } => {
+ let src = src.display();
+ let dst = dst.display();
+ write!(f, "failed hard link `{src}` to `{dst}`: {err}")
+ }
+ ErrorKind::CreateDir { err, path } => {
+ let path = path.display();
+ write!(f, "failed to create directory `{path}`: {err}")
+ }
+ ErrorKind::RemovePath { err, path } => {
+ let path = path.display();
+ write!(f, "failed to remove path `{path}`: {err}")
+ }
+ ErrorKind::CmdStatus { cmd, status } => match status.code() {
+ Some(code) => write!(f, "command exited with non-zero code `{cmd}`: {code}"),
+ #[cfg(unix)]
+ None => {
+ use std::os::unix::process::ExitStatusExt;
+ match status.signal() {
+ Some(sig) => write!(f, "command was terminated by a signal `{cmd}`: {sig}"),
+ None => write!(f, "command was terminated by a signal `{cmd}`"),
+ }
+ }
+ #[cfg(not(unix))]
+ None => write!(f, "command was terminated by a signal `{cmd}`"),
+ },
+ ErrorKind::CmdIo { err, cmd } => {
+ if err.kind() == io::ErrorKind::NotFound {
+ let prog = cmd.prog.display();
+ write!(f, "command not found: `{prog}`")
+ } else {
+ write!(f, "io error when running command `{cmd}`: {err}")
+ }
+ }
+ ErrorKind::CmdUtf8 { err, cmd } => {
+ write!(f, "failed to decode output of command `{cmd}`: {err}")
+ }
+ ErrorKind::CmdStdin { err, cmd } => {
+ write!(f, "failed to write to stdin of command `{cmd}`: {err}")
+ }
+ }?;
+ Ok(())
+ }
+}
+
+impl fmt::Debug for Error {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Display::fmt(self, f)
+ }
+}
+impl std::error::Error for Error {}
+
+/// `pub(crate)` constructors, visible only in this crate.
+impl Error {
+ pub(crate) fn new_current_dir(err: io::Error) -> Error {
+ ErrorKind::CurrentDir { err }.into()
+ }
+
+ pub(crate) fn new_var(err: env::VarError, var: OsString) -> Error {
+ ErrorKind::Var { err, var }.into()
+ }
+
+ pub(crate) fn new_read_file(err: io::Error, path: PathBuf) -> Error {
+ ErrorKind::ReadFile { err, path }.into()
+ }
+
+ pub(crate) fn new_read_dir(err: io::Error, path: PathBuf) -> Error {
+ ErrorKind::ReadDir { err, path }.into()
+ }
+
+ pub(crate) fn new_write_file(err: io::Error, path: PathBuf) -> Error {
+ ErrorKind::WriteFile { err, path }.into()
+ }
+
+ pub(crate) fn new_copy_file(err: io::Error, src: PathBuf, dst: PathBuf) -> Error {
+ ErrorKind::CopyFile { err, src, dst }.into()
+ }
+
+ pub(crate) fn new_hard_link(err: io::Error, src: PathBuf, dst: PathBuf) -> Error {
+ ErrorKind::HardLink { err, src, dst }.into()
+ }
+
+ pub(crate) fn new_create_dir(err: io::Error, path: PathBuf) -> Error {
+ ErrorKind::CreateDir { err, path }.into()
+ }
+
+ pub(crate) fn new_remove_path(err: io::Error, path: PathBuf) -> Error {
+ ErrorKind::RemovePath { err, path }.into()
+ }
+
+ pub(crate) fn new_cmd_status(cmd: &Cmd<'_>, status: ExitStatus) -> Error {
+ let cmd = cmd.data.clone();
+ ErrorKind::CmdStatus { cmd, status }.into()
+ }
+
+ pub(crate) fn new_cmd_io(cmd: &Cmd<'_>, err: io::Error) -> Error {
+ let cmd = cmd.data.clone();
+ ErrorKind::CmdIo { err, cmd }.into()
+ }
+
+ pub(crate) fn new_cmd_utf8(cmd: &Cmd<'_>, err: FromUtf8Error) -> Error {
+ let cmd = cmd.data.clone();
+ ErrorKind::CmdUtf8 { err, cmd }.into()
+ }
+
+ pub(crate) fn new_cmd_stdin(cmd: &Cmd<'_>, err: io::Error) -> Error {
+ let cmd = cmd.data.clone();
+ ErrorKind::CmdStdin { err, cmd }.into()
+ }
+}
+
+#[test]
+fn error_send_sync() {
+ fn f<T: Send + Sync>() {}
+ f::<Error>();
+}
diff --git a/vendor/xshell/src/lib.rs b/vendor/xshell/src/lib.rs
new file mode 100644
index 000000000..6b980ba22
--- /dev/null
+++ b/vendor/xshell/src/lib.rs
@@ -0,0 +1,1112 @@
+//! xshell is a swiss-army knife for writing cross-platform "bash" scripts in
+//! Rust.
+//!
+//! It doesn't use the shell directly, but rather re-implements parts of
+//! scripting environment in Rust. The intended use-case is various bits of glue
+//! code, which could be written in bash or python. The original motivation is
+//! [`xtask`](https://github.com/matklad/cargo-xtask) development.
+//!
+//! Here's a quick example:
+//!
+//! ```no_run
+//! use xshell::{Shell, cmd};
+//!
+//! let sh = Shell::new()?;
+//! let branch = "main";
+//! let commit_hash = cmd!(sh, "git rev-parse {branch}").read()?;
+//! # Ok::<(), xshell::Error>(())
+//! ```
+//!
+//! **Goals:**
+//!
+//! * Ergonomics and DWIM ("do what I mean"): `cmd!` macro supports
+//! interpolation, writing to a file automatically creates parent directories,
+//! etc.
+//! * Reliability: no [shell injection] by construction, good error messages
+//! with file paths, non-zero exit status is an error, independence of the
+//! host environment, etc.
+//! * Frugality: fast compile times, few dependencies, low-tech API.
+//!
+//! # Guide
+//!
+//! For a short API overview, let's implement a script to clone a github
+//! repository and publish it as a crates.io crate. The script will do the
+//! following:
+//!
+//! 1. Clone the repository.
+//! 2. `cd` into the repository's directory.
+//! 3. Run the tests.
+//! 4. Create a git tag using a version from `Cargo.toml`.
+//! 5. Publish the crate with an optional `--dry-run`.
+//!
+//! Start with the following skeleton:
+//!
+//! ```no_run
+//! use xshell::{cmd, Shell};
+//!
+//! fn main() -> anyhow::Result<()> {
+//! let sh = Shell::new()?;
+//!
+//! Ok(())
+//! }
+//! ```
+//!
+//! Only two imports are needed -- the [`Shell`] struct the and [`cmd!`] macro.
+//! By convention, an instance of a [`Shell`] is stored in a variable named
+//! `sh`. All the API is available as methods, so a short name helps here. For
+//! "scripts", the [`anyhow`](https://docs.rs/anyhow) crate is a great choice
+//! for an error-handling library.
+//!
+//! Next, clone the repository:
+//!
+//! ```no_run
+//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
+//! cmd!(sh, "git clone https://github.com/matklad/xshell.git").run()?;
+//! # Ok::<(), xshell::Error>(())
+//! ```
+//!
+//! The [`cmd!`] macro provides a convenient syntax for creating a command --
+//! the [`Cmd`] struct. The [`Cmd::run`] method runs the command as if you
+//! typed it into the shell. The whole program outputs:
+//!
+//! ```console
+//! $ git clone https://github.com/matklad/xshell.git
+//! Cloning into 'xshell'...
+//! remote: Enumerating objects: 676, done.
+//! remote: Counting objects: 100% (220/220), done.
+//! remote: Compressing objects: 100% (123/123), done.
+//! remote: Total 676 (delta 106), reused 162 (delta 76), pack-reused 456
+//! Receiving objects: 100% (676/676), 136.80 KiB | 222.00 KiB/s, done.
+//! Resolving deltas: 100% (327/327), done.
+//! ```
+//!
+//! Note that the command itself is echoed to stderr (the `$ git ...` bit in the
+//! output). You can use [`Cmd::quiet`] to override this behavior:
+//!
+//! ```no_run
+//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
+//! cmd!(sh, "git clone https://github.com/matklad/xshell.git")
+//! .quiet()
+//! .run()?;
+//! # Ok::<(), xshell::Error>(())
+//! ```
+//!
+//! To make the code more general, let's use command interpolation to extract
+//! the username and the repository:
+//!
+//! ```no_run
+//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
+//! let user = "matklad";
+//! let repo = "xshell";
+//! cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?;
+//! # Ok::<(), xshell::Error>(())
+//! ```
+//!
+//! Note that the `cmd!` macro parses the command string at compile time, so you
+//! don't have to worry about escaping the arguments. For example, the following
+//! command "touches" a single file whose name is `contains a space`:
+//!
+//! ```no_run
+//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
+//! let file = "contains a space";
+//! cmd!(sh, "touch {file}").run()?;
+//! # Ok::<(), xshell::Error>(())
+//! ```
+//!
+//! Next, `cd` into the folder you have just cloned:
+//!
+//! ```no_run
+//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
+//! # let repo = "xshell";
+//! sh.change_dir(repo);
+//! ```
+//!
+//! Each instance of [`Shell`] has a current directory, which is independent of
+//! the process-wide [`std::env::current_dir`]. The same applies to the
+//! environment.
+//!
+//! Next, run the tests:
+//!
+//! ```no_run
+//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
+//! let test_args = ["-Zunstable-options", "--report-time"];
+//! cmd!(sh, "cargo test -- {test_args...}").run()?;
+//! # Ok::<(), xshell::Error>(())
+//! ```
+//!
+//! Note how the so-called splat syntax (`...`) is used to interpolate an
+//! iterable of arguments.
+//!
+//! Next, read the Cargo.toml so that we can fetch crate' declared version:
+//!
+//! ```no_run
+//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
+//! let manifest = sh.read_file("Cargo.toml")?;
+//! # Ok::<(), xshell::Error>(())
+//! ```
+//!
+//! [`Shell::read_file`] works like [`std::fs::read_to_string`], but paths are
+//! relative to the current directory of the [`Shell`]. Unlike [`std::fs`],
+//! error messages are much more useful. For example, if there isn't a
+//! `Cargo.toml` in the repository, the error message is:
+//!
+//! ```text
+//! Error: failed to read file `xshell/Cargo.toml`: no such file or directory (os error 2)
+//! ```
+//!
+//! `xshell` doesn't implement string processing utils like `grep`, `sed` or
+//! `awk` -- there's no need to, built-in language features work fine, and it's
+//! always possible to pull extra functionality from crates.io.
+//!
+//! To extract the `version` field from Cargo.toml, [`str::split_once`] is
+//! enough:
+//!
+//! ```no_run
+//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
+//! let manifest = sh.read_file("Cargo.toml")?;
+//! let version = manifest
+//! .split_once("version = \"")
+//! .and_then(|it| it.1.split_once('\"'))
+//! .map(|it| it.0)
+//! .ok_or_else(|| anyhow::format_err!("can't find version field in the manifest"))?;
+//!
+//! cmd!(sh, "git tag {version}").run()?;
+//! # Ok::<(), anyhow::Error>(())
+//! ```
+//!
+//! The splat (`...`) syntax works with any iterable, and in Rust options are
+//! iterable. This means that `...` can be used to implement optional arguments.
+//! For example, here's how to pass `--dry-run` when *not* running in CI:
+//!
+//! ```no_run
+//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
+//! let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") };
+//! cmd!(sh, "cargo publish {dry_run...}").run()?;
+//! # Ok::<(), xshell::Error>(())
+//! ```
+//!
+//! Putting everything altogether, here's the whole script:
+//!
+//! ```no_run
+//! use xshell::{cmd, Shell};
+//!
+//! fn main() -> anyhow::Result<()> {
+//! let sh = Shell::new()?;
+//!
+//! let user = "matklad";
+//! let repo = "xshell";
+//! cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?;
+//! sh.change_dir(repo);
+//!
+//! let test_args = ["-Zunstable-options", "--report-time"];
+//! cmd!(sh, "cargo test -- {test_args...}").run()?;
+//!
+//! let manifest = sh.read_file("Cargo.toml")?;
+//! let version = manifest
+//! .split_once("version = \"")
+//! .and_then(|it| it.1.split_once('\"'))
+//! .map(|it| it.0)
+//! .ok_or_else(|| anyhow::format_err!("can't find version field in the manifest"))?;
+//!
+//! cmd!(sh, "git tag {version}").run()?;
+//!
+//! let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") };
+//! cmd!(sh, "cargo publish {dry_run...}").run()?;
+//!
+//! Ok(())
+//! }
+//! ```
+//!
+//! `xshell` itself uses a similar script to automatically publish oneself to
+//! crates.io when the version in Cargo.toml changes:
+//!
+//! <https://github.com/matklad/xshell/blob/master/examples/ci.rs>
+//!
+//! # Maintenance
+//!
+//! Minimum Supported Rust Version: 1.59.0. MSRV bump is not considered semver
+//! breaking. MSRV is updated conservatively.
+//!
+//! The crate isn't comprehensive yet, but this is a goal. You are hereby
+//! encouraged to submit PRs with missing functionality!
+//!
+//! # Related Crates
+//!
+//! [`duct`] is a crate for heavy-duty process herding, with support for
+//! pipelines.
+//!
+//! Most of what this crate provides can be open-coded using
+//! [`std::process::Command`] and [`std::fs`]. If you only need to spawn a
+//! single process, using `std` is probably better (but don't forget to check
+//! the exit status!).
+//!
+//! [`duct`]: https://github.com/oconnor663/duct.rs
+//! [shell injection]:
+//! https://en.wikipedia.org/wiki/Code_injection#Shell_injection
+//!
+//! # Implementation Notes
+//!
+//! The design is heavily inspired by the Julia language:
+//!
+//! * [Shelling Out
+//! Sucks](https://julialang.org/blog/2012/03/shelling-out-sucks/)
+//! * [Put This In Your
+//! Pipe](https://julialang.org/blog/2013/04/put-this-in-your-pipe/)
+//! * [Running External
+//! Programs](https://docs.julialang.org/en/v1/manual/running-external-programs/)
+//! * [Filesystem](https://docs.julialang.org/en/v1/base/file/)
+//!
+//! Smaller influences are the [`duct`] crate and Ruby's
+//! [`FileUtils`](https://ruby-doc.org/stdlib-2.4.1/libdoc/fileutils/rdoc/FileUtils.html)
+//! module.
+//!
+//! The `cmd!` macro uses a simple proc-macro internally. It doesn't depend on
+//! helper libraries, so the fixed-cost impact on compile times is moderate.
+//! Compiling a trivial program with `cmd!("date +%Y-%m-%d")` takes one second.
+//! Equivalent program using only `std::process::Command` compiles in 0.25
+//! seconds.
+//!
+//! To make IDEs infer correct types without expanding proc-macro, it is wrapped
+//! into a declarative macro which supplies type hints.
+
+#![deny(missing_debug_implementations)]
+#![deny(missing_docs)]
+#![deny(rust_2018_idioms)]
+
+mod error;
+
+use std::{
+ cell::RefCell,
+ collections::HashMap,
+ env::{self, current_dir, VarError},
+ ffi::{OsStr, OsString},
+ fmt, fs,
+ io::{self, ErrorKind, Write},
+ mem,
+ path::{Path, PathBuf},
+ process::{Command, ExitStatus, Output, Stdio},
+ sync::atomic::{AtomicUsize, Ordering},
+};
+
+pub use crate::error::{Error, Result};
+#[doc(hidden)]
+pub use xshell_macros::__cmd;
+
+/// Constructs a [`Cmd`] from the given string.
+///
+/// # Examples
+///
+/// Basic:
+///
+/// ```no_run
+/// # use xshell::{cmd, Shell};
+/// let sh = Shell::new()?;
+/// cmd!(sh, "echo hello world").run()?;
+/// # Ok::<(), xshell::Error>(())
+/// ```
+///
+/// Interpolation:
+///
+/// ```
+/// # use xshell::{cmd, Shell}; let sh = Shell::new()?;
+/// let greeting = "hello world";
+/// let c = cmd!(sh, "echo {greeting}");
+/// assert_eq!(c.to_string(), r#"echo "hello world""#);
+///
+/// let c = cmd!(sh, "echo '{greeting}'");
+/// assert_eq!(c.to_string(), r#"echo {greeting}"#);
+///
+/// let c = cmd!(sh, "echo {greeting}!");
+/// assert_eq!(c.to_string(), r#"echo "hello world!""#);
+///
+/// let c = cmd!(sh, "echo 'spaces '{greeting}' around'");
+/// assert_eq!(c.to_string(), r#"echo "spaces hello world around""#);
+///
+/// # Ok::<(), xshell::Error>(())
+/// ```
+///
+/// Splat interpolation:
+///
+/// ```
+/// # use xshell::{cmd, Shell}; let sh = Shell::new()?;
+/// let args = ["hello", "world"];
+/// let c = cmd!(sh, "echo {args...}");
+/// assert_eq!(c.to_string(), r#"echo hello world"#);
+///
+/// let arg1: Option<&str> = Some("hello");
+/// let arg2: Option<&str> = None;
+/// let c = cmd!(sh, "echo {arg1...} {arg2...}");
+/// assert_eq!(c.to_string(), r#"echo hello"#);
+/// # Ok::<(), xshell::Error>(())
+/// ```
+#[macro_export]
+macro_rules! cmd {
+ ($sh:expr, $cmd:literal) => {{
+ #[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)]
+ format_args!($cmd);
+ let f = |prog| $sh.cmd(prog);
+ let cmd: $crate::Cmd = $crate::__cmd!(f $cmd);
+ cmd
+ }};
+}
+
+/// A `Shell` is the main API entry point.
+///
+/// Almost all of the crate's functionality is available as methods of the
+/// `Shell` object.
+///
+/// `Shell` is a stateful object. It maintains a logical working directory and
+/// an environment map. They are independent from process's
+/// [`std::env::current_dir`] and [`std::env::var`], and only affect paths and
+/// commands passed to the [`Shell`].
+///
+///
+/// By convention, variable holding the shell is named `sh`.
+///
+/// # Example
+///
+/// ```no_run
+/// use xshell::{cmd, Shell};
+///
+/// let sh = Shell::new()?;
+/// let _d = sh.push_dir("./target");
+/// let cwd = sh.current_dir();
+/// cmd!(sh, "echo current dir is {cwd}").run()?;
+///
+/// let process_cwd = std::env::current_dir().unwrap();
+/// assert_eq!(cwd, process_cwd.join("./target"));
+/// # Ok::<(), xshell::Error>(())
+/// ```
+#[derive(Debug)]
+pub struct Shell {
+ cwd: RefCell<PathBuf>,
+ env: RefCell<HashMap<OsString, OsString>>,
+}
+
+impl std::panic::UnwindSafe for Shell {}
+impl std::panic::RefUnwindSafe for Shell {}
+
+impl Shell {
+ /// Creates a new [`Shell`].
+ ///
+ /// Fails if [`std::env::current_dir`] returns an error.
+ pub fn new() -> Result<Shell> {
+ let cwd = current_dir().map_err(Error::new_current_dir)?;
+ let cwd = RefCell::new(cwd);
+ let env = RefCell::new(HashMap::new());
+ Ok(Shell { cwd, env })
+ }
+
+ // region:env
+ /// Returns the working directory for this [`Shell`].
+ ///
+ /// All relative paths are interpreted relative to this directory, rather
+ /// than [`std::env::current_dir`].
+ #[doc(alias = "pwd")]
+ pub fn current_dir(&self) -> PathBuf {
+ self.cwd.borrow().clone()
+ }
+
+ /// Changes the working directory for this [`Shell`].
+ ///
+ /// Note that this doesn't affect [`std::env::current_dir`].
+ #[doc(alias = "pwd")]
+ pub fn change_dir<P: AsRef<Path>>(&self, dir: P) {
+ self._change_dir(dir.as_ref())
+ }
+ fn _change_dir(&self, dir: &Path) {
+ let dir = self.path(dir);
+ *self.cwd.borrow_mut() = dir;
+ }
+
+ /// Temporary changes the working directory of this [`Shell`].
+ ///
+ /// Returns a RAII guard which reverts the working directory to the old
+ /// value when dropped.
+ ///
+ /// Note that this doesn't affect [`std::env::current_dir`].
+ #[doc(alias = "pushd")]
+ pub fn push_dir<P: AsRef<Path>>(&self, path: P) -> PushDir<'_> {
+ self._push_dir(path.as_ref())
+ }
+ fn _push_dir(&self, path: &Path) -> PushDir<'_> {
+ let path = self.path(path);
+ PushDir::new(self, path)
+ }
+
+ /// Fetches the environmental variable `key` for this [`Shell`].
+ ///
+ /// Returns an error if the variable is not set, or set to a non-utf8 value.
+ ///
+ /// Environment of the [`Shell`] affects all commands spawned via this
+ /// shell.
+ pub fn var<K: AsRef<OsStr>>(&self, key: K) -> Result<String> {
+ self._var(key.as_ref())
+ }
+ fn _var(&self, key: &OsStr) -> Result<String> {
+ match self._var_os(key) {
+ Some(it) => it.into_string().map_err(VarError::NotUnicode),
+ None => Err(VarError::NotPresent),
+ }
+ .map_err(|err| Error::new_var(err, key.to_os_string()))
+ }
+
+ /// Fetches the environmental variable `key` for this [`Shell`] as
+ /// [`OsString`] Returns [`None`] if the variable is not set.
+ ///
+ /// Environment of the [`Shell`] affects all commands spawned via this
+ /// shell.
+ pub fn var_os<K: AsRef<OsStr>>(&self, key: K) -> Option<OsString> {
+ self._var_os(key.as_ref())
+ }
+ fn _var_os(&self, key: &OsStr) -> Option<OsString> {
+ self.env.borrow().get(key).cloned().or_else(|| env::var_os(key))
+ }
+
+ /// Sets the value of `key` environment variable for this [`Shell`] to
+ /// `val`.
+ ///
+ /// Note that this doesn't affect [`std::env::var`].
+ pub fn set_var<K: AsRef<OsStr>, V: AsRef<OsStr>>(&self, key: K, val: V) {
+ self._set_var(key.as_ref(), val.as_ref())
+ }
+ fn _set_var(&self, key: &OsStr, val: &OsStr) {
+ self.env.borrow_mut().insert(key.to_os_string(), val.to_os_string());
+ }
+
+ /// Temporary sets the value of `key` environment variable for this
+ /// [`Shell`] to `val`.
+ ///
+ /// Returns a RAII guard which restores the old environment when dropped.
+ ///
+ /// Note that this doesn't affect [`std::env::var`].
+ pub fn push_env<K: AsRef<OsStr>, V: AsRef<OsStr>>(&self, key: K, val: V) -> PushEnv<'_> {
+ self._push_env(key.as_ref(), val.as_ref())
+ }
+ fn _push_env(&self, key: &OsStr, val: &OsStr) -> PushEnv<'_> {
+ PushEnv::new(self, key.to_os_string(), val.to_os_string())
+ }
+ // endregion:env
+
+ // region:fs
+ /// Read the entire contents of a file into a string.
+ #[doc(alias = "cat")]
+ pub fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<String> {
+ self._read_file(path.as_ref())
+ }
+ fn _read_file(&self, path: &Path) -> Result<String> {
+ let path = self.path(path);
+ fs::read_to_string(&path).map_err(|err| Error::new_read_file(err, path))
+ }
+
+ /// Read the entire contents of a file into a vector of bytes.
+ pub fn read_binary_file<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
+ self._read_binary_file(path.as_ref())
+ }
+ fn _read_binary_file(&self, path: &Path) -> Result<Vec<u8>> {
+ let path = self.path(path);
+ fs::read(&path).map_err(|err| Error::new_read_file(err, path))
+ }
+
+ /// Returns a sorted list of paths directly contained in the directory at
+ /// `path`.
+ #[doc(alias = "ls")]
+ pub fn read_dir<P: AsRef<Path>>(&self, path: P) -> Result<Vec<PathBuf>> {
+ self._read_dir(path.as_ref())
+ }
+ fn _read_dir(&self, path: &Path) -> Result<Vec<PathBuf>> {
+ let path = self.path(path);
+ let mut res = Vec::new();
+ || -> _ {
+ for entry in fs::read_dir(&path)? {
+ let entry = entry?;
+ res.push(entry.path())
+ }
+ Ok(())
+ }()
+ .map_err(|err| Error::new_read_dir(err, path))?;
+ res.sort();
+ Ok(res)
+ }
+
+ /// Write a slice as the entire contents of a file.
+ ///
+ /// This function will create the file and all intermediate directories if
+ /// they don't exist.
+ // TODO: probably want to make this an atomic rename write?
+ pub fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(&self, path: P, contents: C) -> Result<()> {
+ self._write_file(path.as_ref(), contents.as_ref())
+ }
+ fn _write_file(&self, path: &Path, contents: &[u8]) -> Result<()> {
+ let path = self.path(path);
+ if let Some(p) = path.parent() {
+ self.create_dir(p)?;
+ }
+ fs::write(&path, contents).map_err(|err| Error::new_write_file(err, path))
+ }
+
+ /// Copies `src` into `dst`.
+ ///
+ /// `src` must be a file, but `dst` need not be. If `dst` is an existing
+ /// directory, `src` will be copied into a file in the `dst` directory whose
+ /// name is same as that of `src`.
+ ///
+ /// Otherwise, `dst` is a file or does not exist, and `src` will be copied into
+ /// it.
+ #[doc(alias = "cp")]
+ pub fn copy_file<S: AsRef<Path>, D: AsRef<Path>>(&self, src: S, dst: D) -> Result<()> {
+ self._copy_file(src.as_ref(), dst.as_ref())
+ }
+ fn _copy_file(&self, src: &Path, dst: &Path) -> Result<()> {
+ let src = self.path(src);
+ let dst = self.path(dst);
+ let dst = dst.as_path();
+ let mut _tmp;
+ let mut dst = dst;
+ if dst.is_dir() {
+ if let Some(file_name) = src.file_name() {
+ _tmp = dst.join(file_name);
+ dst = &_tmp;
+ }
+ }
+ std::fs::copy(&src, dst)
+ .map_err(|err| Error::new_copy_file(err, src.to_path_buf(), dst.to_path_buf()))?;
+ Ok(())
+ }
+
+ /// Hardlinks `src` to `dst`.
+ #[doc(alias = "ln")]
+ pub fn hard_link<S: AsRef<Path>, D: AsRef<Path>>(&self, src: S, dst: D) -> Result<()> {
+ self._hard_link(src.as_ref(), dst.as_ref())
+ }
+ fn _hard_link(&self, src: &Path, dst: &Path) -> Result<()> {
+ let src = self.path(src);
+ let dst = self.path(dst);
+ fs::hard_link(&src, &dst).map_err(|err| Error::new_hard_link(err, src, dst))
+ }
+
+ /// Creates the specified directory.
+ ///
+ /// All intermediate directories will also be created.
+ #[doc(alias("mkdir_p", "mkdir"))]
+ pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
+ self._create_dir(path.as_ref())
+ }
+ fn _create_dir(&self, path: &Path) -> Result<PathBuf> {
+ let path = self.path(path);
+ match fs::create_dir_all(&path) {
+ Ok(()) => Ok(path),
+ Err(err) => Err(Error::new_create_dir(err, path)),
+ }
+ }
+
+ /// Creates an empty named world-readable temporary directory.
+ ///
+ /// Returns a [`TempDir`] RAII guard with the path to the directory. When
+ /// dropped, the temporary directory and all of its contents will be
+ /// removed.
+ ///
+ /// Note that this is an **insecure method** -- any other process on the
+ /// system will be able to read the data.
+ #[doc(alias = "mktemp")]
+ pub fn create_temp_dir(&self) -> Result<TempDir> {
+ let base = std::env::temp_dir();
+ self.create_dir(&base)?;
+
+ static CNT: AtomicUsize = AtomicUsize::new(0);
+
+ let mut n_try = 0u32;
+ loop {
+ let cnt = CNT.fetch_add(1, Ordering::Relaxed);
+ let path = base.join(format!("xshell-tmp-dir-{}", cnt));
+ match fs::create_dir(&path) {
+ Ok(()) => return Ok(TempDir { path }),
+ Err(err) if n_try == 1024 => return Err(Error::new_create_dir(err, path)),
+ Err(_) => n_try += 1,
+ }
+ }
+ }
+
+ /// Removes the file or directory at the given path.
+ #[doc(alias("rm_rf", "rm"))]
+ pub fn remove_path<P: AsRef<Path>>(&self, path: P) -> Result<()> {
+ self._remove_path(path.as_ref())
+ }
+ fn _remove_path(&self, path: &Path) -> Result<(), Error> {
+ let path = self.path(path);
+ match path.metadata() {
+ Ok(meta) => if meta.is_dir() { remove_dir_all(&path) } else { fs::remove_file(&path) }
+ .map_err(|err| Error::new_remove_path(err, path)),
+ Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
+ Err(err) => Err(Error::new_remove_path(err, path)),
+ }
+ }
+
+ /// Returns whether a file or directory exists at the given path.
+ #[doc(alias("stat"))]
+ pub fn path_exists<P: AsRef<Path>>(&self, path: P) -> bool {
+ self.path(path.as_ref()).exists()
+ }
+ // endregion:fs
+
+ /// Creates a new [`Cmd`] that executes the given `program`.
+ pub fn cmd<P: AsRef<Path>>(&self, program: P) -> Cmd<'_> {
+ // TODO: path lookup?
+ Cmd::new(self, program.as_ref())
+ }
+
+ fn path(&self, p: &Path) -> PathBuf {
+ let cd = self.cwd.borrow();
+ cd.join(p)
+ }
+}
+
+/// RAII guard returned from [`Shell::push_dir`].
+///
+/// Dropping `PushDir` restores the working directory of the [`Shell`] to the
+/// old value.
+#[derive(Debug)]
+#[must_use]
+pub struct PushDir<'a> {
+ old_cwd: PathBuf,
+ shell: &'a Shell,
+}
+
+impl<'a> PushDir<'a> {
+ fn new(shell: &'a Shell, path: PathBuf) -> PushDir<'a> {
+ PushDir { old_cwd: mem::replace(&mut *shell.cwd.borrow_mut(), path), shell }
+ }
+}
+
+impl Drop for PushDir<'_> {
+ fn drop(&mut self) {
+ mem::swap(&mut *self.shell.cwd.borrow_mut(), &mut self.old_cwd)
+ }
+}
+
+/// RAII guard returned from [`Shell::push_env`].
+///
+/// Dropping `PushEnv` restores the old value of the environmental variable.
+#[derive(Debug)]
+#[must_use]
+pub struct PushEnv<'a> {
+ key: OsString,
+ old_value: Option<OsString>,
+ shell: &'a Shell,
+}
+
+impl<'a> PushEnv<'a> {
+ fn new(shell: &'a Shell, key: OsString, val: OsString) -> PushEnv<'a> {
+ let old_value = shell.env.borrow_mut().insert(key.clone(), val);
+ PushEnv { shell, key, old_value }
+ }
+}
+
+impl Drop for PushEnv<'_> {
+ fn drop(&mut self) {
+ let mut env = self.shell.env.borrow_mut();
+ let key = mem::take(&mut self.key);
+ match self.old_value.take() {
+ Some(value) => {
+ env.insert(key, value);
+ }
+ None => {
+ env.remove(&key);
+ }
+ }
+ }
+}
+
+/// A builder object for constructing a subprocess.
+///
+/// A [`Cmd`] is usually created with the [`cmd!`] macro. The command exists
+/// within a context of a [`Shell`] and uses its working directory and
+/// environment.
+///
+/// # Example
+///
+/// ```no_run
+/// use xshell::{Shell, cmd};
+///
+/// let sh = Shell::new()?;
+///
+/// let branch = "main";
+/// let cmd = cmd!(sh, "git switch {branch}").quiet().run()?;
+/// # Ok::<(), xshell::Error>(())
+/// ```
+#[derive(Debug)]
+#[must_use]
+pub struct Cmd<'a> {
+ shell: &'a Shell,
+ data: CmdData,
+}
+
+#[derive(Debug, Default, Clone)]
+struct CmdData {
+ prog: PathBuf,
+ args: Vec<OsString>,
+ env_changes: Vec<EnvChange>,
+ ignore_status: bool,
+ quiet: bool,
+ secret: bool,
+ stdin_contents: Option<Vec<u8>>,
+ ignore_stdout: bool,
+ ignore_stderr: bool,
+}
+
+// We just store a list of functions to call on the `Command` — the alternative
+// would require mirroring the logic that `std::process::Command` (or rather
+// `sys_common::CommandEnvs`) uses, which is moderately complex, involves
+// special-casing `PATH`, and plausibly could change.
+#[derive(Debug, Clone)]
+enum EnvChange {
+ Set(OsString, OsString),
+ Remove(OsString),
+ Clear,
+}
+
+impl fmt::Display for Cmd<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Display::fmt(&self.data, f)
+ }
+}
+
+impl fmt::Display for CmdData {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.secret {
+ return write!(f, "<secret>");
+ }
+
+ write!(f, "{}", self.prog.display())?;
+ for arg in &self.args {
+ // TODO: this is potentially not copy-paste safe.
+ let arg = arg.to_string_lossy();
+ if arg.chars().any(|it| it.is_ascii_whitespace()) {
+ write!(f, " \"{}\"", arg.escape_default())?
+ } else {
+ write!(f, " {}", arg)?
+ };
+ }
+ Ok(())
+ }
+}
+
+impl From<Cmd<'_>> for Command {
+ fn from(cmd: Cmd<'_>) -> Command {
+ cmd.to_command()
+ }
+}
+
+impl<'a> Cmd<'a> {
+ fn new(shell: &'a Shell, prog: &Path) -> Cmd<'a> {
+ let mut data = CmdData::default();
+ data.prog = prog.to_path_buf();
+ Cmd { shell, data }
+ }
+
+ // region:builder
+ /// Adds an argument to this commands.
+ pub fn arg<P: AsRef<OsStr>>(mut self, arg: P) -> Cmd<'a> {
+ self._arg(arg.as_ref());
+ self
+ }
+ fn _arg(&mut self, arg: &OsStr) {
+ self.data.args.push(arg.to_owned())
+ }
+
+ /// Adds all of the arguments to this command.
+ pub fn args<I>(mut self, args: I) -> Cmd<'a>
+ where
+ I: IntoIterator,
+ I::Item: AsRef<OsStr>,
+ {
+ args.into_iter().for_each(|it| self._arg(it.as_ref()));
+ self
+ }
+
+ #[doc(hidden)]
+ pub fn __extend_arg<P: AsRef<OsStr>>(mut self, arg_fragment: P) -> Cmd<'a> {
+ self.___extend_arg(arg_fragment.as_ref());
+ self
+ }
+ fn ___extend_arg(&mut self, arg_fragment: &OsStr) {
+ match self.data.args.last_mut() {
+ Some(last_arg) => last_arg.push(arg_fragment),
+ None => {
+ let mut prog = mem::take(&mut self.data.prog).into_os_string();
+ prog.push(arg_fragment);
+ self.data.prog = prog.into();
+ }
+ }
+ }
+
+ /// Overrides the value of the environmental variable for this command.
+ pub fn env<K: AsRef<OsStr>, V: AsRef<OsStr>>(mut self, key: K, val: V) -> Cmd<'a> {
+ self._env_set(key.as_ref(), val.as_ref());
+ self
+ }
+
+ fn _env_set(&mut self, key: &OsStr, val: &OsStr) {
+ self.data.env_changes.push(EnvChange::Set(key.to_owned(), val.to_owned()));
+ }
+
+ /// Overrides the values of specified environmental variables for this
+ /// command.
+ pub fn envs<I, K, V>(mut self, vars: I) -> Cmd<'a>
+ where
+ I: IntoIterator<Item = (K, V)>,
+ K: AsRef<OsStr>,
+ V: AsRef<OsStr>,
+ {
+ vars.into_iter().for_each(|(k, v)| self._env_set(k.as_ref(), v.as_ref()));
+ self
+ }
+
+ /// Removes the environment variable from this command.
+ pub fn env_remove<K: AsRef<OsStr>>(mut self, key: K) -> Cmd<'a> {
+ self._env_remove(key.as_ref());
+ self
+ }
+ fn _env_remove(&mut self, key: &OsStr) {
+ self.data.env_changes.push(EnvChange::Remove(key.to_owned()));
+ }
+
+ /// Removes all of the environment variables from this command.
+ pub fn env_clear(mut self) -> Cmd<'a> {
+ self.data.env_changes.push(EnvChange::Clear);
+ self
+ }
+
+ /// Don't return an error if command the command exits with non-zero status.
+ ///
+ /// By default, non-zero exit status is considered an error.
+ pub fn ignore_status(mut self) -> Cmd<'a> {
+ self.set_ignore_status(true);
+ self
+ }
+ /// Controls whether non-zero exit status is considered an error.
+ pub fn set_ignore_status(&mut self, yes: bool) {
+ self.data.ignore_status = yes;
+ }
+
+ /// Don't echo the command itself to stderr.
+ ///
+ /// By default, the command itself will be printed to stderr when executed via [`Cmd::run`].
+ pub fn quiet(mut self) -> Cmd<'a> {
+ self.set_quiet(true);
+ self
+ }
+ /// Controls whether the command itself is printed to stderr.
+ pub fn set_quiet(&mut self, yes: bool) {
+ self.data.quiet = yes;
+ }
+
+ /// Marks the command as secret.
+ ///
+ /// If a command is secret, it echoes `<secret>` instead of the program and
+ /// its arguments, even in error messages.
+ pub fn secret(mut self) -> Cmd<'a> {
+ self.set_secret(true);
+ self
+ }
+ /// Controls whether the command is secret.
+ pub fn set_secret(&mut self, yes: bool) {
+ self.data.secret = yes;
+ }
+
+ /// Pass the given slice to the standard input of the spawned process.
+ pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd<'a> {
+ self._stdin(stdin.as_ref());
+ self
+ }
+ fn _stdin(&mut self, stdin: &[u8]) {
+ self.data.stdin_contents = Some(stdin.to_vec());
+ }
+
+ /// Ignores the standard output stream of the process.
+ ///
+ /// This is equivalent to redirecting stdout to `/dev/null`. By default, the
+ /// stdout is inherited or captured.
+ pub fn ignore_stdout(mut self) -> Cmd<'a> {
+ self.set_ignore_stdout(true);
+ self
+ }
+ /// Controls whether the standard output is ignored.
+ pub fn set_ignore_stdout(&mut self, yes: bool) {
+ self.data.ignore_stdout = yes;
+ }
+
+ /// Ignores the standard output stream of the process.
+ ///
+ /// This is equivalent redirecting stderr to `/dev/null`. By default, the
+ /// stderr is inherited or captured.
+ pub fn ignore_stderr(mut self) -> Cmd<'a> {
+ self.set_ignore_stderr(true);
+ self
+ }
+ /// Controls whether the standard error is ignored.
+ pub fn set_ignore_stderr(&mut self, yes: bool) {
+ self.data.ignore_stderr = yes;
+ }
+ // endregion:builder
+
+ // region:running
+ /// Runs the command.
+ ///
+ /// By default the command itself is echoed to stderr, its standard streams
+ /// are inherited, and non-zero return code is considered an error. These
+ /// behaviors can be overridden by using various builder methods of the [`Cmd`].
+ pub fn run(&self) -> Result<()> {
+ if !self.data.quiet {
+ eprintln!("$ {}", self);
+ }
+ let mut command = self.to_command();
+ let status = command.status().map_err(|err| Error::new_cmd_io(self, err))?;
+ self.check_status(status)?;
+ Ok(())
+ }
+
+ /// Run the command and return its stdout as a string.
+ pub fn read(&self) -> Result<String> {
+ self.read_stream(false)
+ }
+
+ /// Run the command and return its stderr as a string.
+ pub fn read_stderr(&self) -> Result<String> {
+ self.read_stream(true)
+ }
+
+ /// Run the command and return its output.
+ pub fn output(&self) -> Result<Output> {
+ self.output_impl(true, true)
+ }
+ // endregion:running
+
+ fn read_stream(&self, read_stderr: bool) -> Result<String> {
+ let read_stdout = !read_stderr;
+ let output = self.output_impl(read_stdout, read_stderr)?;
+ self.check_status(output.status)?;
+
+ let stream = if read_stderr { output.stderr } else { output.stdout };
+ let mut stream = String::from_utf8(stream).map_err(|err| Error::new_cmd_utf8(self, err))?;
+
+ if stream.ends_with('\n') {
+ stream.pop();
+ }
+ if stream.ends_with('\r') {
+ stream.pop();
+ }
+
+ Ok(stream)
+ }
+
+ fn output_impl(&self, read_stdout: bool, read_stderr: bool) -> Result<Output> {
+ let mut child = {
+ let mut command = self.to_command();
+
+ if !self.data.ignore_stdout {
+ command.stdout(if read_stdout { Stdio::piped() } else { Stdio::inherit() });
+ }
+ if !self.data.ignore_stderr {
+ command.stderr(if read_stderr { Stdio::piped() } else { Stdio::inherit() });
+ }
+
+ command.stdin(match &self.data.stdin_contents {
+ Some(_) => Stdio::piped(),
+ None => Stdio::null(),
+ });
+
+ command.spawn().map_err(|err| Error::new_cmd_io(self, err))?
+ };
+
+ let mut io_thread = None;
+ if let Some(stdin_contents) = self.data.stdin_contents.clone() {
+ let mut stdin = child.stdin.take().unwrap();
+ io_thread = Some(std::thread::spawn(move || {
+ stdin.write_all(&stdin_contents)?;
+ stdin.flush()
+ }));
+ }
+ let out_res = child.wait_with_output();
+ let err_res = io_thread.map(|it| it.join().unwrap());
+ let output = out_res.map_err(|err| Error::new_cmd_io(self, err))?;
+ if let Some(err_res) = err_res {
+ err_res.map_err(|err| Error::new_cmd_stdin(self, err))?;
+ }
+ self.check_status(output.status)?;
+ Ok(output)
+ }
+
+ fn to_command(&self) -> Command {
+ let mut res = Command::new(&self.data.prog);
+ res.current_dir(self.shell.current_dir());
+ res.args(&self.data.args);
+
+ for (key, val) in &*self.shell.env.borrow() {
+ res.env(key, val);
+ }
+ for change in &self.data.env_changes {
+ match change {
+ EnvChange::Clear => res.env_clear(),
+ EnvChange::Remove(key) => res.env_remove(key),
+ EnvChange::Set(key, val) => res.env(key, val),
+ };
+ }
+
+ if self.data.ignore_stdout {
+ res.stdout(Stdio::null());
+ }
+
+ if self.data.ignore_stderr {
+ res.stderr(Stdio::null());
+ }
+
+ res
+ }
+
+ fn check_status(&self, status: ExitStatus) -> Result<()> {
+ if status.success() || self.data.ignore_status {
+ return Ok(());
+ }
+ Err(Error::new_cmd_status(self, status))
+ }
+}
+
+/// A temporary directory.
+///
+/// This is a RAII object which will remove the underlying temporary directory
+/// when dropped.
+#[derive(Debug)]
+#[must_use]
+pub struct TempDir {
+ path: PathBuf,
+}
+
+impl TempDir {
+ /// Returns the path to the underlying temporary directory.
+ pub fn path(&self) -> &Path {
+ &self.path
+ }
+}
+
+impl Drop for TempDir {
+ fn drop(&mut self) {
+ let _ = remove_dir_all(&self.path);
+ }
+}
+
+#[cfg(not(windows))]
+fn remove_dir_all(path: &Path) -> io::Result<()> {
+ std::fs::remove_dir_all(path)
+}
+
+#[cfg(windows)]
+fn remove_dir_all(path: &Path) -> io::Result<()> {
+ for _ in 0..99 {
+ if fs::remove_dir_all(path).is_ok() {
+ return Ok(());
+ }
+ std::thread::sleep(std::time::Duration::from_millis(10))
+ }
+ fs::remove_dir_all(path)
+}
diff --git a/vendor/xshell/tests/compile_time.rs b/vendor/xshell/tests/compile_time.rs
new file mode 100644
index 000000000..ca35eefe8
--- /dev/null
+++ b/vendor/xshell/tests/compile_time.rs
@@ -0,0 +1,41 @@
+use std::time::{Duration, Instant};
+
+use xshell::{cmd, Shell};
+
+#[test]
+fn fixed_cost_compile_times() {
+ let sh = Shell::new().unwrap();
+
+ let _p = sh.push_dir("tests/data");
+ let baseline = compile_bench(&sh, "baseline");
+ let _ducted = compile_bench(&sh, "ducted");
+ let xshelled = compile_bench(&sh, "xshelled");
+ let ratio = (xshelled.as_millis() as f64) / (baseline.as_millis() as f64);
+ assert!(1.0 < ratio && ratio < 10.0);
+
+ fn compile_bench(sh: &Shell, name: &str) -> Duration {
+ let _p = sh.push_dir(name);
+ let cargo_build = cmd!(sh, "cargo build -q");
+ cargo_build.read().unwrap();
+
+ let n = 5;
+ let mut times = Vec::new();
+ for _ in 0..n {
+ sh.remove_path("./target").unwrap();
+ let start = Instant::now();
+ cargo_build.read().unwrap();
+ let elapsed = start.elapsed();
+ times.push(elapsed);
+ }
+
+ times.sort();
+ times.remove(0);
+ times.pop();
+ let total = times.iter().sum::<Duration>();
+ let average = total / (times.len() as u32);
+
+ eprintln!("compiling {name}: {average:?}");
+
+ total
+ }
+}
diff --git a/vendor/xshell/tests/data/xecho.rs b/vendor/xshell/tests/data/xecho.rs
new file mode 100644
index 000000000..ac86433c3
--- /dev/null
+++ b/vendor/xshell/tests/data/xecho.rs
@@ -0,0 +1,85 @@
+use std::io::{self, Write};
+
+fn main() {
+ if let Err(err) = try_main() {
+ eprintln!("{err}");
+ std::process::exit(1);
+ }
+}
+
+fn try_main() -> io::Result<()> {
+ let mut tee_stderr = false;
+ let mut echo_stdin = false;
+ let mut echo_env = false;
+ let mut fail = false;
+ let mut suicide = false;
+
+ let mut args = std::env::args().skip(1).peekable();
+ while let Some(arg) = args.peek() {
+ match arg.as_str() {
+ "-e" => tee_stderr = true,
+ "-i" => echo_stdin = true,
+ "-$" => echo_env = true,
+ "-f" => fail = true,
+ "-s" => suicide = true,
+ _ => break,
+ }
+ args.next();
+ }
+
+ let stdin = io::stdin();
+ let stdout = io::stdout();
+ let stderr = io::stderr();
+ let mut stdin = stdin.lock();
+ let mut stdout = stdout.lock();
+ let mut stderr = stderr.lock();
+ macro_rules! w {
+ ($($tt:tt)*) => {
+ write!(stdout, $($tt)*)?;
+ if tee_stderr {
+ write!(stderr, $($tt)*)?;
+ }
+ }
+ }
+
+ if echo_stdin {
+ io::copy(&mut stdin, &mut stdout)?;
+ } else if echo_env {
+ for key in args {
+ if let Some(v) = std::env::var_os(&key) {
+ w!("{}={}\n", key, v.to_string_lossy());
+ }
+ }
+ } else {
+ let mut space = "";
+ for arg in args {
+ w!("{}{}", space, arg);
+ space = " ";
+ }
+ w!("\n");
+ }
+
+ if fail {
+ return Err(io::ErrorKind::Other.into());
+ }
+ if suicide {
+ #[cfg(unix)]
+ unsafe {
+ let pid = signals::getpid();
+ if pid > 0 {
+ signals::kill(pid, 9);
+ }
+ }
+ }
+
+ Ok(())
+}
+
+#[cfg(unix)]
+mod signals {
+ use std::os::raw::c_int;
+ extern "C" {
+ pub fn kill(pid: c_int, sig: c_int) -> c_int;
+ pub fn getpid() -> c_int;
+ }
+}
diff --git a/vendor/xshell/tests/it/compile_failures.rs b/vendor/xshell/tests/it/compile_failures.rs
new file mode 100644
index 000000000..611af5acb
--- /dev/null
+++ b/vendor/xshell/tests/it/compile_failures.rs
@@ -0,0 +1,128 @@
+use xshell::{cmd, Shell};
+
+#[track_caller]
+fn check(code: &str, err_msg: &str) {
+ let sh = Shell::new().unwrap();
+ let xshell_dir = sh.current_dir();
+ let temp_dir = sh.create_temp_dir().unwrap();
+ sh.change_dir(temp_dir.path());
+
+ let manifest = format!(
+ r#"
+[package]
+name = "cftest"
+version = "0.0.0"
+edition = "2018"
+[workspace]
+
+[lib]
+path = "main.rs"
+
+[dependencies]
+xshell = {{ path = {xshell_dir:?} }}
+"#,
+ );
+
+ let snip = format!(
+ "
+use xshell::*;
+pub fn f() {{
+ let sh = Shell::new().unwrap();
+ {code};
+}}
+"
+ );
+
+ sh.write_file("Cargo.toml", manifest).unwrap();
+ sh.write_file("main.rs", snip).unwrap();
+
+ let stderr = cmd!(sh, "cargo build").ignore_status().read_stderr().unwrap();
+ assert!(
+ stderr.contains(err_msg),
+ "\n\nCompile fail fail!\n\nExpected:\n{}\n\nActual:\n{}\n",
+ err_msg,
+ stderr
+ );
+}
+
+#[test]
+fn not_a_string_literal() {
+ check("cmd!(sh, 92)", "expected a plain string literal");
+}
+
+#[test]
+fn not_raw_string_literal() {
+ check(r#"cmd!(sh, r"raw")"#, "expected a plain string literal");
+}
+
+#[test]
+fn interpolate_complex_expression() {
+ check(
+ r#"cmd!(sh, "{echo.as_str()}")"#,
+ "error: can only interpolate simple variables, got this expression instead: `echo.as_str()`",
+ );
+}
+
+#[test]
+fn interpolate_splat_concat_prefix() {
+ check(
+ r#"cmd!(sh, "echo a{args...}")"#,
+ "error: can't combine splat with concatenation, add spaces around `{args...}`",
+ );
+}
+
+#[test]
+fn interpolate_splat_concat_suffix() {
+ check(
+ r#"cmd!(sh, "echo {args...}b")"#,
+ "error: can't combine splat with concatenation, add spaces around `{args...}`",
+ );
+}
+
+#[test]
+fn interpolate_splat_concat_mixfix() {
+ check(
+ r#"cmd!(sh, "echo a{args...}b")"#,
+ "error: can't combine splat with concatenation, add spaces around `{args...}`",
+ );
+}
+
+#[test]
+fn empty_command() {
+ check(r#"cmd!(sh, "")"#, "error: command can't be empty");
+}
+
+#[test]
+fn spalt_program() {
+ check(r#"cmd!(sh, "{cmd...}")"#, "error: can't splat program name");
+}
+
+#[test]
+fn unclosed_quote() {
+ check(r#"cmd!(sh, "echo 'hello world")"#, "error: unclosed `'` in command");
+}
+
+#[test]
+fn unclosed_curly() {
+ check(r#"cmd!(sh, "echo {hello world")"#, "error: unclosed `{` in command");
+}
+
+#[test]
+fn interpolate_integer() {
+ check(
+ r#"
+ let x = 92;
+ cmd!(sh, "make -j {x}")"#,
+ r#"is not implemented"#,
+ );
+}
+
+#[test]
+fn splat_fn_pointer() {
+ check(
+ r#"
+ let dry_run: fn() -> Option<&'static str> = || None;
+ cmd!(sh, "make -j {dry_run...}")"#,
+ r#"is not implemented"#,
+ );
+}
diff --git a/vendor/xshell/tests/it/env.rs b/vendor/xshell/tests/it/env.rs
new file mode 100644
index 000000000..949869467
--- /dev/null
+++ b/vendor/xshell/tests/it/env.rs
@@ -0,0 +1,130 @@
+use std::collections::BTreeMap;
+
+use xshell::cmd;
+
+use crate::setup;
+
+#[test]
+fn test_env() {
+ let sh = setup();
+
+ let v1 = "xshell_test_123";
+ let v2 = "xshell_test_456";
+
+ assert_env(cmd!(sh, "xecho -$ {v1}").env(v1, "123"), &[(v1, Some("123"))]);
+
+ assert_env(
+ cmd!(sh, "xecho -$ {v1} {v2}").envs([(v1, "123"), (v2, "456")].iter().copied()),
+ &[(v1, Some("123")), (v2, Some("456"))],
+ );
+ assert_env(
+ cmd!(sh, "xecho -$ {v1} {v2}")
+ .envs([(v1, "123"), (v2, "456")].iter().copied())
+ .env_remove(v2),
+ &[(v1, Some("123")), (v2, None)],
+ );
+ assert_env(
+ cmd!(sh, "xecho -$ {v1} {v2}")
+ .envs([(v1, "123"), (v2, "456")].iter().copied())
+ .env_remove("nothing"),
+ &[(v1, Some("123")), (v2, Some("456"))],
+ );
+
+ let _g1 = sh.push_env(v1, "foobar");
+ let _g2 = sh.push_env(v2, "quark");
+
+ assert_env(cmd!(sh, "xecho -$ {v1} {v2}"), &[(v1, Some("foobar")), (v2, Some("quark"))]);
+
+ assert_env(
+ cmd!(sh, "xecho -$ {v1} {v2}").env(v1, "wombo"),
+ &[(v1, Some("wombo")), (v2, Some("quark"))],
+ );
+
+ assert_env(cmd!(sh, "xecho -$ {v1} {v2}").env_remove(v1), &[(v1, None), (v2, Some("quark"))]);
+ assert_env(
+ cmd!(sh, "xecho -$ {v1} {v2}").env_remove(v1).env(v1, "baz"),
+ &[(v1, Some("baz")), (v2, Some("quark"))],
+ );
+ assert_env(
+ cmd!(sh, "xecho -$ {v1} {v2}").env(v1, "baz").env_remove(v1),
+ &[(v1, None), (v2, Some("quark"))],
+ );
+}
+
+#[test]
+fn test_env_clear() {
+ let sh = setup();
+
+ let v1 = "xshell_test_123";
+ let v2 = "xshell_test_456";
+
+ let xecho = format!("./target/xecho{}", std::env::consts::EXE_SUFFIX);
+
+ assert_env(
+ cmd!(sh, "{xecho} -$ {v1} {v2}")
+ .envs([(v1, "123"), (v2, "456")].iter().copied())
+ .env_clear(),
+ &[(v1, None), (v2, None)],
+ );
+ assert_env(
+ cmd!(sh, "{xecho} -$ {v1} {v2}")
+ .envs([(v1, "123"), (v2, "456")].iter().copied())
+ .env_clear()
+ .env(v1, "789"),
+ &[(v1, Some("789")), (v2, None)],
+ );
+
+ let _g1 = sh.push_env(v1, "foobar");
+ let _g2 = sh.push_env(v2, "quark");
+
+ assert_env(cmd!(sh, "{xecho} -$ {v1} {v2}").env_clear(), &[(v1, None), (v2, None)]);
+ assert_env(
+ cmd!(sh, "{xecho} -$ {v1} {v2}").env_clear().env(v1, "baz"),
+ &[(v1, Some("baz")), (v2, None)],
+ );
+ assert_env(
+ cmd!(sh, "{xecho} -$ {v1} {v2}").env(v1, "baz").env_clear(),
+ &[(v1, None), (v2, None)],
+ );
+}
+
+#[track_caller]
+fn assert_env(xecho_env_cmd: xshell::Cmd, want_env: &[(&str, Option<&str>)]) {
+ let output = xecho_env_cmd.output().unwrap();
+ let env = String::from_utf8_lossy(&output.stdout)
+ .lines()
+ .filter(|line| !line.is_empty())
+ .map(|line| {
+ let (key, val) = line.split_once('=').unwrap_or_else(|| {
+ panic!("failed to parse line from `xecho -$` output: {:?}", line)
+ });
+ (key.to_owned(), val.to_owned())
+ })
+ .collect::<BTreeMap<_, _>>();
+ check_env(&env, want_env);
+}
+
+#[track_caller]
+fn check_env(env: &BTreeMap<String, String>, wanted_env: &[(&str, Option<&str>)]) {
+ let mut failed = false;
+ let mut seen = env.clone();
+ for &(k, val) in wanted_env {
+ match (seen.remove(k), val) {
+ (Some(env_v), Some(want_v)) if env_v == want_v => {}
+ (None, None) => {}
+ (have, want) => {
+ eprintln!("mismatch on env var {:?}: have `{:?}`, want `{:?}` ", k, have, want);
+ failed = true;
+ }
+ }
+ }
+ for (k, v) in seen {
+ eprintln!("Unexpected env key {:?} (value: {:?})", k, v);
+ failed = true;
+ }
+ assert!(
+ !failed,
+ "env didn't match (see stderr for cleaner output):\nsaw: {:?}\n\nwanted: {:?}",
+ env, wanted_env,
+ );
+}
diff --git a/vendor/xshell/tests/it/main.rs b/vendor/xshell/tests/it/main.rs
new file mode 100644
index 000000000..6ac8fe074
--- /dev/null
+++ b/vendor/xshell/tests/it/main.rs
@@ -0,0 +1,462 @@
+mod tidy;
+mod env;
+mod compile_failures;
+
+use std::{ffi::OsStr, path::Path};
+
+use xshell::{cmd, Shell};
+
+fn setup() -> Shell {
+ static ONCE: std::sync::Once = std::sync::Once::new();
+
+ let sh = Shell::new().unwrap();
+ let xecho_src = sh.current_dir().join("./tests/data/xecho.rs");
+ let target_dir = sh.current_dir().join("./target/");
+
+ ONCE.call_once(|| {
+ cmd!(sh, "rustc {xecho_src} --out-dir {target_dir}")
+ .quiet()
+ .run()
+ .unwrap_or_else(|err| panic!("failed to install binaries from mock_bin: {}", err))
+ });
+
+ sh.set_var("PATH", target_dir);
+ sh
+}
+
+#[test]
+fn smoke() {
+ let sh = setup();
+
+ let pwd = "lol";
+ let cmd = cmd!(sh, "xecho 'hello '{pwd}");
+ println!("{}", cmd);
+}
+
+#[test]
+fn into_command() {
+ let sh = setup();
+ let _: std::process::Command = cmd!(sh, "git branch").into();
+}
+
+#[test]
+fn multiline() {
+ let sh = setup();
+
+ let output = cmd!(
+ sh,
+ "
+ xecho hello
+ "
+ )
+ .read()
+ .unwrap();
+ assert_eq!(output, "hello");
+}
+
+#[test]
+fn interpolation() {
+ let sh = setup();
+
+ let hello = "hello";
+ let output = cmd!(sh, "xecho {hello}").read().unwrap();
+ assert_eq!(output, "hello");
+}
+
+#[test]
+fn program_interpolation() {
+ let sh = setup();
+
+ let echo = "xecho";
+ let output = cmd!(sh, "{echo} hello").read().unwrap();
+ assert_eq!(output, "hello");
+}
+
+#[test]
+fn interpolation_concatenation() {
+ let sh = setup();
+
+ let hello = "hello";
+ let world = "world";
+ let output = cmd!(sh, "xecho {hello}-{world}").read().unwrap();
+ assert_eq!(output, "hello-world");
+}
+
+#[test]
+fn program_concatenation() {
+ let sh = setup();
+
+ let ho = "ho";
+ let output = cmd!(sh, "xec{ho} hello").read().unwrap();
+ assert_eq!(output, "hello");
+}
+
+#[test]
+fn interpolation_move() {
+ let sh = setup();
+
+ let hello = "hello".to_string();
+ let output1 = cmd!(sh, "xecho {hello}").read().unwrap();
+ let output2 = cmd!(sh, "xecho {hello}").read().unwrap();
+ assert_eq!(output1, output2)
+}
+
+#[test]
+fn interpolation_spat() {
+ let sh = setup();
+
+ let a = &["hello", "world"];
+ let b: &[&OsStr] = &[];
+ let c = &["!".to_string()];
+ let output = cmd!(sh, "xecho {a...} {b...} {c...}").read().unwrap();
+ assert_eq!(output, "hello world !")
+}
+
+#[test]
+fn splat_option() {
+ let sh = setup();
+
+ let a: Option<&OsStr> = None;
+ let b = Some("hello");
+ let output = cmd!(sh, "xecho {a...} {b...}").read().unwrap();
+ assert_eq!(output, "hello")
+}
+
+#[test]
+fn splat_idiom() {
+ let sh = setup();
+
+ let check = if true { &["--", "--check"][..] } else { &[] };
+ let cmd = cmd!(sh, "cargo fmt {check...}");
+ assert_eq!(cmd.to_string(), "cargo fmt -- --check");
+
+ let dry_run = if true { Some("--dry-run") } else { None };
+ let cmd = cmd!(sh, "cargo publish {dry_run...}");
+ assert_eq!(cmd.to_string(), "cargo publish --dry-run");
+}
+
+#[test]
+fn exit_status() {
+ let sh = setup();
+
+ let err = cmd!(sh, "xecho -f").read().unwrap_err();
+ assert_eq!(err.to_string(), "command exited with non-zero code `xecho -f`: 1");
+}
+
+#[test]
+#[cfg_attr(not(unix), ignore)]
+fn exit_status_signal() {
+ let sh = setup();
+
+ let err = cmd!(sh, "xecho -s").read().unwrap_err();
+ assert_eq!(err.to_string(), "command was terminated by a signal `xecho -s`: 9");
+}
+
+#[test]
+fn ignore_status() {
+ let sh = setup();
+
+ let output = cmd!(sh, "xecho -f").ignore_status().read().unwrap();
+ assert_eq!(output, "");
+}
+
+#[test]
+fn ignore_status_no_such_command() {
+ let sh = setup();
+
+ let err = cmd!(sh, "xecho-f").ignore_status().read().unwrap_err();
+ assert_eq!(err.to_string(), "command not found: `xecho-f`");
+}
+
+#[test]
+#[cfg_attr(not(unix), ignore)]
+fn ignore_status_signal() {
+ let sh = setup();
+
+ let output = cmd!(sh, "xecho -s dead").ignore_status().read().unwrap();
+ assert_eq!(output, "dead");
+}
+
+#[test]
+fn read_stderr() {
+ let sh = setup();
+
+ let output = cmd!(sh, "xecho -f -e snafu").ignore_status().read_stderr().unwrap();
+ assert!(output.contains("snafu"));
+}
+
+#[test]
+fn unknown_command() {
+ let sh = setup();
+
+ let err = cmd!(sh, "nope no way").read().unwrap_err();
+ assert_eq!(err.to_string(), "command not found: `nope`");
+}
+
+#[test]
+fn args_with_spaces() {
+ let sh = setup();
+
+ let hello_world = "hello world";
+ let cmd = cmd!(sh, "xecho {hello_world} 'hello world' hello world");
+ assert_eq!(cmd.to_string(), r#"xecho "hello world" "hello world" hello world"#)
+}
+
+#[test]
+fn escape() {
+ let sh = setup();
+
+ let output = cmd!(sh, "xecho \\hello\\ '\\world\\'").read().unwrap();
+ assert_eq!(output, r#"\hello\ \world\"#)
+}
+
+#[test]
+fn stdin_redirection() {
+ let sh = setup();
+
+ let lines = "\
+foo
+baz
+bar
+";
+ let output = cmd!(sh, "xecho -i").stdin(lines).read().unwrap().replace("\r\n", "\n");
+ assert_eq!(
+ output,
+ "\
+foo
+baz
+bar"
+ )
+}
+
+#[test]
+fn no_deadlock() {
+ let sh = setup();
+
+ let mut data = "All the work and now paly made Jack a dull boy.\n".repeat(1 << 20);
+ data.pop();
+ let res = cmd!(sh, "xecho -i").stdin(&data).read().unwrap();
+ assert_eq!(data, res);
+}
+
+#[test]
+fn test_push_dir() {
+ let sh = setup();
+
+ let d1 = sh.current_dir();
+ {
+ let _p = sh.push_dir("xshell-macros");
+ let d2 = sh.current_dir();
+ assert_eq!(d2, d1.join("xshell-macros"));
+ {
+ let _p = sh.push_dir("src");
+ let d3 = sh.current_dir();
+ assert_eq!(d3, d1.join("xshell-macros/src"));
+ }
+ let d4 = sh.current_dir();
+ assert_eq!(d4, d1.join("xshell-macros"));
+ }
+ let d5 = sh.current_dir();
+ assert_eq!(d5, d1);
+}
+
+#[test]
+fn test_push_and_change_dir() {
+ let sh = setup();
+
+ let d1 = sh.current_dir();
+ {
+ let _p = sh.push_dir("xshell-macros");
+ let d2 = sh.current_dir();
+ assert_eq!(d2, d1.join("xshell-macros"));
+ sh.change_dir("src");
+ let d3 = sh.current_dir();
+ assert_eq!(d3, d1.join("xshell-macros/src"));
+ }
+ let d5 = sh.current_dir();
+ assert_eq!(d5, d1);
+}
+
+#[test]
+fn push_dir_parent_dir() {
+ let sh = setup();
+
+ let current = sh.current_dir();
+ let dirname = current.file_name().unwrap();
+ let _d = sh.push_dir("..");
+ let _d = sh.push_dir(dirname);
+ assert_eq!(sh.current_dir().canonicalize().unwrap(), current.canonicalize().unwrap());
+}
+
+const VAR: &str = "SPICA";
+
+#[test]
+fn test_push_env() {
+ let sh = setup();
+
+ let e1 = sh.var_os(VAR);
+ {
+ let _e = sh.push_env(VAR, "1");
+ let e2 = sh.var_os(VAR);
+ assert_eq!(e2, Some("1".into()));
+ {
+ let _e = sh.push_env(VAR, "2");
+ let e3 = sh.var_os(VAR);
+ assert_eq!(e3, Some("2".into()));
+ }
+ let e4 = sh.var_os(VAR);
+ assert_eq!(e4, e2);
+ }
+ let e5 = sh.var_os(VAR);
+ assert_eq!(e5, e1);
+}
+
+#[test]
+fn test_push_env_and_set_var() {
+ let sh = setup();
+
+ let e1 = sh.var_os(VAR);
+ {
+ let _e = sh.push_env(VAR, "1");
+ let e2 = sh.var_os(VAR);
+ assert_eq!(e2, Some("1".into()));
+ let _e = sh.set_var(VAR, "2");
+ let e3 = sh.var_os(VAR);
+ assert_eq!(e3, Some("2".into()));
+ }
+ let e5 = sh.var_os(VAR);
+ assert_eq!(e5, e1);
+}
+
+#[test]
+fn output_with_ignore() {
+ let sh = setup();
+
+ let output = cmd!(sh, "xecho -e 'hello world!'").ignore_stdout().output().unwrap();
+ assert_eq!(output.stderr, b"hello world!\n");
+ assert_eq!(output.stdout, b"");
+
+ let output = cmd!(sh, "xecho -e 'hello world!'").ignore_stderr().output().unwrap();
+ assert_eq!(output.stdout, b"hello world!\n");
+ assert_eq!(output.stderr, b"");
+
+ let output =
+ cmd!(sh, "xecho -e 'hello world!'").ignore_stdout().ignore_stderr().output().unwrap();
+ assert_eq!(output.stdout, b"");
+ assert_eq!(output.stderr, b"");
+}
+
+#[test]
+fn test_read_with_ignore() {
+ let sh = setup();
+
+ let stdout = cmd!(sh, "xecho -e 'hello world'").ignore_stdout().read().unwrap();
+ assert!(stdout.is_empty());
+
+ let stderr = cmd!(sh, "xecho -e 'hello world'").ignore_stderr().read_stderr().unwrap();
+ assert!(stderr.is_empty());
+
+ let stdout = cmd!(sh, "xecho -e 'hello world!'").ignore_stderr().read().unwrap();
+ assert_eq!(stdout, "hello world!");
+
+ let stderr = cmd!(sh, "xecho -e 'hello world!'").ignore_stdout().read_stderr().unwrap();
+ assert_eq!(stderr, "hello world!");
+}
+
+#[test]
+fn test_copy_file() {
+ let sh = setup();
+
+ let path;
+ {
+ let tempdir = sh.create_temp_dir().unwrap();
+ path = tempdir.path().to_path_buf();
+ let foo = tempdir.path().join("foo.txt");
+ let bar = tempdir.path().join("bar.txt");
+ let dir = tempdir.path().join("dir");
+ sh.write_file(&foo, "hello world").unwrap();
+ sh.create_dir(&dir).unwrap();
+
+ sh.copy_file(&foo, &bar).unwrap();
+ assert_eq!(sh.read_file(&bar).unwrap(), "hello world");
+
+ sh.copy_file(&foo, &dir).unwrap();
+ assert_eq!(sh.read_file(&dir.join("foo.txt")).unwrap(), "hello world");
+ assert!(path.exists());
+ }
+ assert!(!path.exists());
+}
+
+#[test]
+fn test_exists() {
+ let sh = setup();
+ let tmp = sh.create_temp_dir().unwrap();
+ let _d = sh.change_dir(tmp.path());
+ assert!(!sh.path_exists("foo.txt"));
+ sh.write_file("foo.txt", "foo").unwrap();
+ assert!(sh.path_exists("foo.txt"));
+ assert!(!sh.path_exists("bar"));
+ sh.create_dir("bar").unwrap();
+ assert!(sh.path_exists("bar"));
+ let _d = sh.change_dir("bar");
+ assert!(!sh.path_exists("quz.rs"));
+ sh.write_file("quz.rs", "fn main () {}").unwrap();
+ assert!(sh.path_exists("quz.rs"));
+ sh.remove_path("quz.rs").unwrap();
+ assert!(!sh.path_exists("quz.rs"));
+}
+
+#[test]
+fn write_makes_directory() {
+ let sh = setup();
+
+ let tempdir = sh.create_temp_dir().unwrap();
+ let folder = tempdir.path().join("some/nested/folder/structure");
+ sh.write_file(folder.join(".gitinclude"), "").unwrap();
+ assert!(folder.exists());
+}
+
+#[test]
+fn test_remove_path() {
+ let sh = setup();
+
+ let tempdir = sh.create_temp_dir().unwrap();
+ sh.change_dir(tempdir.path());
+ sh.write_file(Path::new("a/b/c.rs"), "fn main() {}").unwrap();
+ assert!(tempdir.path().join("a/b/c.rs").exists());
+ sh.remove_path("./a").unwrap();
+ assert!(!tempdir.path().join("a/b/c.rs").exists());
+ sh.remove_path("./a").unwrap();
+}
+
+#[test]
+fn recovers_from_panics() {
+ let sh = setup();
+
+ let tempdir = sh.create_temp_dir().unwrap();
+ let tempdir = tempdir.path().canonicalize().unwrap();
+
+ let orig = sh.current_dir();
+
+ std::panic::catch_unwind(|| {
+ let _p = sh.push_dir(&tempdir);
+ assert_eq!(sh.current_dir(), tempdir);
+ std::panic::resume_unwind(Box::new(()));
+ })
+ .unwrap_err();
+
+ assert_eq!(sh.current_dir(), orig);
+ {
+ let _p = sh.push_dir(&tempdir);
+ assert_eq!(sh.current_dir(), tempdir);
+ }
+}
+
+#[test]
+fn string_escapes() {
+ let sh = setup();
+
+ assert_eq!(cmd!(sh, "\"hello\"").to_string(), "\"hello\"");
+ assert_eq!(cmd!(sh, "\"\"\"asdf\"\"\"").to_string(), r##""""asdf""""##);
+ assert_eq!(cmd!(sh, "\\\\").to_string(), r#"\\"#);
+}
diff --git a/vendor/xshell/tests/it/tidy.rs b/vendor/xshell/tests/it/tidy.rs
new file mode 100644
index 000000000..2b55b2907
--- /dev/null
+++ b/vendor/xshell/tests/it/tidy.rs
@@ -0,0 +1,37 @@
+use xshell::{cmd, Shell};
+
+#[test]
+fn versions_match() {
+ let sh = Shell::new().unwrap();
+
+ let read_version = |path: &str| {
+ let text = sh.read_file(path).unwrap();
+ let vers = text.lines().find(|it| it.starts_with("version =")).unwrap();
+ let vers = vers.splitn(2, '#').next().unwrap();
+ vers.trim_start_matches("version =").trim().trim_matches('"').to_string()
+ };
+
+ let v1 = read_version("./Cargo.toml");
+ let v2 = read_version("./xshell-macros/Cargo.toml");
+ assert_eq!(v1, v2);
+
+ let cargo_toml = sh.read_file("./Cargo.toml").unwrap();
+ let dep = format!("xshell-macros = {{ version = \"={}\",", v1);
+ assert!(cargo_toml.contains(&dep));
+}
+
+#[test]
+fn formatting() {
+ let sh = Shell::new().unwrap();
+
+ cmd!(sh, "cargo fmt --all -- --check").run().unwrap()
+}
+
+#[test]
+fn current_version_in_changelog() {
+ let sh = Shell::new().unwrap();
+ let _p = sh.push_dir(env!("CARGO_MANIFEST_DIR"));
+ let changelog = sh.read_file("CHANGELOG.md").unwrap();
+ let current_version_header = format!("## {}", env!("CARGO_PKG_VERSION"));
+ assert_eq!(changelog.lines().filter(|&line| line == current_version_header).count(), 1);
+}