cradle-0.2.2/.cargo_vcs_info.json0000644000000001360000000000100122670ustar { "git": { "sha1": "c357995bddf65ad978dfa91a0e9b0740e87c68e2" }, "path_in_vcs": "" }cradle-0.2.2/.gitignore000064400000000000000000000000250072674642500130740ustar 00000000000000/target/ /Cargo.lock cradle-0.2.2/Cargo.lock0000644000000150130000000000100102420ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ "winapi", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cradle" version = "0.2.2" dependencies = [ "bitflags", "executable-path", "gag", "lazy_static", "nix", "pretty_assertions", "rustversion", "tempfile", "unindent", ] [[package]] name = "ctor" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" dependencies = [ "quote", "syn", ] [[package]] name = "diff" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" [[package]] name = "executable-path" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ebc5a6d89e3c90b84e8f33c8737933dda8f1c106b5415900b38b9d433841478" [[package]] name = "fastrand" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ "instant", ] [[package]] name = "gag" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cc0b9f53275dc5fada808f1d2f82e3688a6c14d735633d1590b7be8eb2307b5" dependencies = [ "libc", "tempfile", ] [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" [[package]] name = "memoffset" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ "autocfg", ] [[package]] name = "nix" version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" dependencies = [ "bitflags", "cc", "cfg-if", "libc", "memoffset", ] [[package]] name = "output_vt100" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" dependencies = [ "winapi", ] [[package]] name = "pretty_assertions" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c89f989ac94207d048d92db058e4f6ec7342b0971fc58d1271ca148b799b3563" dependencies = [ "ansi_term", "ctor", "diff", "output_vt100", ] [[package]] name = "proc-macro2" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" dependencies = [ "unicode-xid", ] [[package]] name = "quote" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" dependencies = [ "bitflags", ] [[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ "winapi", ] [[package]] name = "rustversion" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "syn" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] [[package]] name = "tempfile" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if", "fastrand", "libc", "redox_syscall", "remove_dir_all", "winapi", ] [[package]] name = "unicode-xid" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" [[package]] name = "unindent" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514672a55d7380da379785a4d70ca8386c8883ff7eaae877be4d2081cebe73d8" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" cradle-0.2.2/Cargo.toml0000644000000036250000000000100102730ustar # 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 = "2018" name = "cradle" version = "0.2.2" authors = ["Sönke Hahn "] exclude = ["/.github"] description = "Execute child processes with ease" homepage = "https://github.com/soenkehahn/cradle" keywords = [ "child", "child-process", "command", "process", "shell", ] categories = [ "filesystem", "os", ] license = "CC0-1.0" repository = "https://github.com/soenkehahn/cradle" [[bin]] name = "context_integration_tests" path = "src/context_integration_tests.rs" required-features = ["test_executables"] [[bin]] name = "test_executables_helper" path = "src/test_executables/helper.rs" required-features = ["test_executables"] [[bin]] name = "test_executables_panic" path = "src/test_executables/panic.rs" required-features = ["test_executables"] [dependencies.rustversion] version = "1.0.4" [dev-dependencies.bitflags] version = "=1.2.1" [dev-dependencies.executable-path] version = "1.0.0" [dev-dependencies.lazy_static] version = "1.4.0" [dev-dependencies.pretty_assertions] version = "1.0.0" [dev-dependencies.tempfile] version = "3.2.0" [dev-dependencies.unindent] version = "0.1.7" [features] test_executables = [ "gag", "executable-path", "nix", ] [target."cfg(unix)".dependencies.executable-path] version = "1.0.0" optional = true [target."cfg(unix)".dependencies.gag] version = "0.1.10" optional = true [target."cfg(unix)".dependencies.nix] version = "0.22.2" optional = true cradle-0.2.2/Cargo.toml.orig000064400000000000000000000027600072674642500140030ustar 00000000000000[package] name = "cradle" version = "0.2.2" description = "Execute child processes with ease" authors = ["Sönke Hahn "] license = "CC0-1.0" edition = "2018" repository = "https://github.com/soenkehahn/cradle" homepage = "https://github.com/soenkehahn/cradle" keywords = ["child", "child-process", "command", "process", "shell"] categories = ["filesystem", "os"] exclude = ["/.github"] [workspace] members = [".", "memory-tests"] [dependencies] rustversion = "1.0.4" [dev-dependencies] executable-path = "1.0.0" lazy_static = "1.4.0" pretty_assertions = "1.0.0" tempfile = "3.2.0" unindent = "0.1.7" # cradle only indirectly depends on bitflags (for tests). # But newer bitflags versions don't compile with older compilers anymore. # So we put in this exact dependency to force a working bitflags version. bitflags = "=1.2.1" [[bin]] name = "context_integration_tests" path = "src/context_integration_tests.rs" required-features = ["test_executables"] [[bin]] name = "test_executables_helper" path = "src/test_executables/helper.rs" required-features = ["test_executables"] [[bin]] name = "test_executables_panic" path = "src/test_executables/panic.rs" required-features = ["test_executables"] [target.'cfg(unix)'.dependencies.gag] version = "0.1.10" optional = true [target.'cfg(unix)'.dependencies.executable-path] version = "1.0.0" optional = true [target.'cfg(unix)'.dependencies.nix] version = "0.22.2" optional = true [features] test_executables = ["gag", "executable-path", "nix"] cradle-0.2.2/LICENSE000064400000000000000000000156100072674642500121170ustar 00000000000000Creative Commons Legal Code CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. cradle-0.2.2/README.md000064400000000000000000000030210072674642500123620ustar 00000000000000[![ci status badge](https://github.com/soenkehahn/cradle/actions/workflows/ci.yaml/badge.svg)](https://github.com/soenkehahn/cradle/actions?query=branch%3Amain) [![crates.io](https://img.shields.io/crates/v/cradle.svg)](https://crates.io/crates/cradle) [![docs](https://docs.rs/cradle/badge.svg)](https://docs.rs/cradle) `cradle` is a library for executing child processes. It provides a more convenient interface than [std::process::Command](https://doc.rust-lang.org/std/process/struct.Command.html). Here's an example: ``` rust use cradle::prelude::*; fn main() { // output git version run!(%"git --version"); // output configured git user let (StdoutTrimmed(git_user), Status(status)) = run_output!(%"git config --get user.name"); if status.success() { eprintln!("git user: {}", git_user); } else { eprintln!("git user not configured"); } } ``` For comprehensive documentation, head over to [docs.rs/cradle](https://docs.rs/cradle/latest/cradle/). ## Design Goals `cradle` is meant to make it as easy as possible to run child processes, while making it hard to use incorrectly. As such it provides an interface that is concise and flexible, and tries to avoid surprising behavior. `cradle` does not try to emulate the syntax or functionality of `bash` or other shells, such as pipes (`|`), globs (`*`), or other string expansion. Instead, it aims to be a convenient wrapper around the operating system's interface for running child processes. ## MSRV The minimal supported rust version is `0.41`. cradle-0.2.2/README.php000064400000000000000000000023230072674642500125550ustar 00000000000000[![ci status badge](https://github.com/soenkehahn/cradle/actions/workflows/ci.yaml/badge.svg)](https://github.com/soenkehahn/cradle/actions?query=branch%3Amain) [![crates.io](https://img.shields.io/crates/v/cradle.svg)](https://crates.io/crates/cradle) [![docs](https://docs.rs/cradle/badge.svg)](https://docs.rs/cradle) `cradle` is a library for executing child processes. It provides a more convenient interface than [std::process::Command](https://doc.rust-lang.org/std/process/struct.Command.html). Here's an example: ``` rust ``` For comprehensive documentation, head over to [docs.rs/cradle](https://docs.rs/cradle/latest/cradle/). ## Design Goals `cradle` is meant to make it as easy as possible to run child processes, while making it hard to use incorrectly. As such it provides an interface that is concise and flexible, and tries to avoid surprising behavior. `cradle` does not try to emulate the syntax or functionality of `bash` or other shells, such as pipes (`|`), globs (`*`), or other string expansion. Instead, it aims to be a convenient wrapper around the operating system's interface for running child processes. ## MSRV The minimal supported rust version is `0.41`. cradle-0.2.2/examples/readme.rs000064400000000000000000000005470072674642500145360ustar 00000000000000use cradle::prelude::*; fn main() { // output git version run!(%"git --version"); // output configured git user let (StdoutTrimmed(git_user), Status(status)) = run_output!(%"git config --get user.name"); if status.success() { eprintln!("git user: {}", git_user); } else { eprintln!("git user not configured"); } } cradle-0.2.2/justfile000064400000000000000000000022520072674642500126600ustar 00000000000000ci: test build doc clippy fmt context-integration-tests run-examples forbidden-words render-readme-check build: cargo build --all-targets --all-features --workspace test +pattern="": cargo test {{ pattern }} test-lib-fast +pattern="": cargo test --lib {{ pattern }} context-integration-tests: cargo run --features "test_executables" --bin context_integration_tests doc +args="": cargo doc --workspace {{args}} clippy: cargo clippy --all-targets --all-features --workspace fmt: cargo fmt --all -- --check run-examples: cargo run --example readme render-readme: php README.php > README.md render-readme-check: #!/usr/bin/env bash diff <(php README.php) README.md forbidden-words: ! grep -rni \ 'dbg!\|fixme\|todo\|#\[ignore\]' \ src tests examples @echo No forbidden words found all-rustc-versions *args="ci": #!/usr/bin/env bash set -eu export RUSTFLAGS="--deny warnings" # install yq with: pip3 install yq versions=$(cat .github/workflows/ci.yaml \ | yq -r '.jobs.all.strategy.matrix.rust | sort | join(" ")' \ ;) for RUSTUP_TOOLCHAIN in $versions do export RUSTUP_TOOLCHAIN cargo version just {{ args }} done cradle-0.2.2/src/child_output.rs000064400000000000000000000076630072674642500147630ustar 00000000000000//! An internal module used for the outputs of child processes. use crate::{ collected_output::Waiter, config::Config, context::Context, error::Error, output::Output, }; use std::{ ffi::OsString, io::Write, process::{Command, ExitStatus, Stdio}, }; /// Internal type to capture all the outputs of a child process. /// Usually you don't have to use this type directly. /// /// See also the documentation for /// [Custom `Output` impls](crate::Output#custom-output-impls). #[derive(Clone, Debug)] pub struct ChildOutput { pub(crate) stdout: Option>, pub(crate) stderr: Option>, pub(crate) exit_status: ExitStatus, } impl ChildOutput { pub(crate) fn run_child_process_output( context: Context, mut config: Config, ) -> Result where Stdout: Write + Clone + Send + 'static, Stderr: Write + Clone + Send + 'static, T: Output, { ::configure(&mut config); let child_output = ChildOutput::run_child_process(context, &config)?; T::from_child_output(&config, &child_output) } fn run_child_process( mut context: Context, config: &Config, ) -> Result where Stdout: Write + Clone + Send + 'static, Stderr: Write + Clone + Send + 'static, { let (executable, arguments) = Self::parse_input(config.arguments.clone())?; if config.log_command { writeln!(context.stderr, "+ {}", config.full_command()) .map_err(|error| Error::command_io_error(config, error))?; } let mut command = Command::new(&executable); command.args(arguments); for (key, value) in &config.added_environment_variables { command.env(key, value); } command .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); if let Some(working_directory) = &config.working_directory { command.current_dir(working_directory); } let mut child = command.spawn().map_err(|source| { if source.kind() == std::io::ErrorKind::NotFound { Error::FileNotFound { executable, source } } else { Error::command_io_error(config, source) } })?; let waiter = Waiter::spawn_standard_stream_relaying( &context, config, child.stdin.take().expect("child process should have stdin"), child .stdout .take() .expect("child process should have stdout"), child .stderr .take() .expect("child process should have stderr"), ); let exit_status = child .wait() .map_err(|error| Error::command_io_error(config, error))?; let collected_output = waiter .join() .map_err(|error| Error::command_io_error(config, error))?; Self::check_exit_status(config, exit_status)?; Ok(Self { stdout: collected_output.stdout, stderr: collected_output.stderr, exit_status, }) } fn parse_input( input: Vec, ) -> Result<(OsString, impl Iterator), Error> { let mut words = input.into_iter(); { match words.next() { None => Err(Error::NoExecutableGiven), Some(command) => Ok((command, words)), } } } fn check_exit_status(config: &Config, exit_status: ExitStatus) -> Result<(), Error> { if config.error_on_non_zero_exit_code && !exit_status.success() { Err(Error::NonZeroExitCode { full_command: config.full_command(), exit_status, }) } else { Ok(()) } } } cradle-0.2.2/src/collected_output.rs000064400000000000000000000057600072674642500156320ustar 00000000000000use crate::{config::Config, context::Context}; use std::{ io::{self, Read, Write}, process::{ChildStderr, ChildStdin, ChildStdout}, thread::{self, JoinHandle}, }; #[derive(Debug)] pub(crate) struct Waiter { stdin: Option>>, stdout: JoinHandle>>>, stderr: JoinHandle>>>, } impl Waiter { fn spawn_standard_stream_handler( capture_stream: bool, mut source: impl Read + Send + 'static, mut relay_sink: impl Write + Send + 'static, ) -> JoinHandle>>> { thread::spawn(move || -> io::Result>> { let mut collected = if capture_stream { Some(Vec::new()) } else { None }; let buffer = &mut [0; 256]; loop { let length = source.read(buffer)?; if (length) == 0 { break; } if let Some(collected) = &mut collected { collected.extend(&buffer[..length]); } if !capture_stream { relay_sink.write_all(&buffer[..length])?; } } Ok(collected) }) } pub(crate) fn spawn_standard_stream_relaying( context: &Context, config: &Config, mut child_stdin: ChildStdin, child_stdout: ChildStdout, child_stderr: ChildStderr, ) -> Self where Stdout: Write + Send + Clone + 'static, Stderr: Write + Send + Clone + 'static, { let stdin_join_handle = config.stdin.clone().map(|config_stdin| { thread::spawn(move || -> io::Result<()> { child_stdin.write_all(&config_stdin)?; Ok(()) }) }); let stdout_join_handle = Self::spawn_standard_stream_handler( config.capture_stdout, child_stdout, context.stdout.clone(), ); let stderr_join_handle = Self::spawn_standard_stream_handler( config.capture_stderr, child_stderr, context.stderr.clone(), ); Waiter { stdin: stdin_join_handle, stdout: stdout_join_handle, stderr: stderr_join_handle, } } pub(crate) fn join(self) -> io::Result { if let Some(stdin) = self.stdin { stdin.join().expect("stdout relaying thread panicked")?; } Ok(CollectedOutput { stdout: self .stdout .join() .expect("stdout relaying thread panicked")?, stderr: self .stderr .join() .expect("stderr relaying thread panicked")?, }) } } #[derive(Debug)] pub(crate) struct CollectedOutput { pub(crate) stdout: Option>, pub(crate) stderr: Option>, } cradle-0.2.2/src/common_re_exports.rs.snippet000064400000000000000000000007470072674642500174770ustar 00000000000000// This file contains common re-exports included in both `src/lib.rs` and `src/prelude.rs`. // Putting these into a common file ensures that they stay in sync. // // Note that the macros defined in `src/macros.rs` are already exported from the root module. // So they can't be included here, since that would clash. pub use crate::{ error::Error, input::{CurrentDir, Env, Input, LogCommand, Split, Stdin}, output::{Output, Status, Stderr, StdoutTrimmed, StdoutUntrimmed}, }; cradle-0.2.2/src/config.rs000064400000000000000000000035220072674642500135130ustar 00000000000000//! An internal module used for configuring child processes. use std::{ffi::OsString, path::PathBuf, sync::Arc}; /// Used by `Input` implementations to configure how child processes are run. /// Usually you don't have to use this type directly. /// /// See also the documentation for /// [Custom `Input` impls](crate::Input#custom-input-impls) and /// [Custom `Output` impls](crate::Output#custom-output-impls). #[rustversion::attr(since(1.48), allow(clippy::rc_buffer))] #[derive(Debug, Clone)] pub struct Config { pub(crate) arguments: Vec, pub(crate) log_command: bool, pub(crate) working_directory: Option, pub(crate) added_environment_variables: Vec<(OsString, OsString)>, pub(crate) stdin: Option>>, pub(crate) capture_stdout: bool, pub(crate) capture_stderr: bool, pub(crate) error_on_non_zero_exit_code: bool, } impl Config { pub(crate) fn full_command(&self) -> String { let mut result = String::new(); for argument in self.arguments.iter() { let argument = argument.to_string_lossy(); if !result.is_empty() { result.push(' '); } let needs_quotes = argument.is_empty() || argument.contains(' '); if needs_quotes { result.push('\''); } result.push_str(&argument); if needs_quotes { result.push('\''); } } result } } impl Default for Config { fn default() -> Self { Config { arguments: Vec::new(), log_command: false, working_directory: None, added_environment_variables: Vec::new(), stdin: None, capture_stdout: false, capture_stderr: false, error_on_non_zero_exit_code: true, } } } cradle-0.2.2/src/context.rs000064400000000000000000000041520072674642500137320ustar 00000000000000//! An internal module used for testing cradle. use std::io::{self, Write}; #[derive(Clone, Debug)] pub(crate) struct Stdout; impl Write for Stdout { fn write(&mut self, buf: &[u8]) -> io::Result { io::stdout().write(buf) } fn flush(&mut self) -> io::Result<()> { io::stdout().flush() } } #[derive(Clone, Debug)] pub(crate) struct Stderr; impl Write for Stderr { fn write(&mut self, buf: &[u8]) -> io::Result { io::stderr().write(buf) } fn flush(&mut self) -> io::Result<()> { io::stderr().flush() } } #[doc(hidden)] #[derive(Clone, Debug)] pub(crate) struct Context { pub(crate) stdout: Stdout, pub(crate) stderr: Stderr, } impl Context { pub(crate) fn production() -> Self { Context { stdout: Stdout, stderr: Stderr, } } } #[cfg(test)] mod test { use super::*; use std::{ io::Cursor, sync::{Arc, Mutex}, }; #[derive(Clone, Debug)] pub(crate) struct TestOutput(Arc>>>); impl TestOutput { fn new() -> TestOutput { TestOutput(Arc::new(Mutex::new(Cursor::new(Vec::new())))) } } impl Write for TestOutput { fn write(&mut self, buf: &[u8]) -> io::Result { let mut lock = self.0.lock().unwrap(); lock.write(buf) } fn flush(&mut self) -> io::Result<()> { let mut lock = self.0.lock().unwrap(); lock.flush() } } impl Context { pub(crate) fn test() -> Self { Context { stdout: TestOutput::new(), stderr: TestOutput::new(), } } pub(crate) fn stdout(&self) -> String { let lock = self.stdout.0.lock().unwrap(); String::from_utf8(lock.clone().into_inner()).unwrap() } pub(crate) fn stderr(&self) -> String { let lock = self.stderr.0.lock().unwrap(); String::from_utf8(lock.clone().into_inner()).unwrap() } } } cradle-0.2.2/src/context_integration_tests.rs000064400000000000000000000023020072674642500175520ustar 00000000000000fn main() { #[cfg(unix)] { { use cradle::prelude::*; run!( LogCommand, %"cargo build --bin test_executables_helper --features test_executables", ); } use cradle::*; use executable_path::executable_path; use gag::BufferRedirect; use std::io::{self, Read}; fn with_gag(mk_buf: fn() -> io::Result, f: F) -> String where F: FnOnce(), { let mut buf = mk_buf().unwrap(); f(); let mut output = String::new(); buf.read_to_string(&mut output).unwrap(); output } { assert_eq!( with_gag(BufferRedirect::stdout, || run_output!(%"echo foo")), "foo\n" ); } { assert_eq!( with_gag(BufferRedirect::stderr, || run_output!( executable_path("test_executables_helper").to_str().unwrap(), "write to stderr" )), "foo\n" ); } eprintln!("context integration tests: SUCCESS") } } cradle-0.2.2/src/error.rs000064400000000000000000000222540072674642500134020ustar 00000000000000//! The [`Error`] type used in the return type of [`run_result!`]. use crate::config::Config; use std::{ffi::OsString, fmt::Display, io, process::ExitStatus, string::FromUtf8Error}; /// Error type returned when an error occurs while using [`run_result!`] /// or [`crate::input::Input::run_result`]. /// /// [`run!`], [`crate::input::Input::run`], [`run_output!`], /// and [`crate::input::Input::run_output`] will turn these errors /// into panics. #[derive(Debug)] pub enum Error { /// The [`Input`](crate::Input)s to a command must produce /// at least one argument: the executable to run. /// /// ``` /// use cradle::prelude::*; /// /// let result: Result<(), cradle::Error> = run_result!(()); /// match result { /// Err(Error::NoExecutableGiven) => {} /// _ => panic!(), /// } /// ``` NoExecutableGiven, /// A `file not found` error occurred while trying to spawn /// the child process: /// /// ``` /// use cradle::prelude::*; /// /// let result: Result<(), Error> = run_result!("does-not-exist"); /// match result { /// Err(Error::FileNotFound { .. }) => {} /// _ => panic!(), /// } /// ``` /// /// Note that this error doesn't necessarily mean that the executable file /// could not be found. /// A few other circumstances in which this can occur are: /// /// - a binary is dynamically linked against a library, /// but that library cannot be found, or /// - the executable starts with a /// [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)), /// but the interpreter specified in the shebang cannot be found. FileNotFound { executable: OsString, source: io::Error, }, /// An IO error during execution. A few circumstances in which this can occur are: /// /// - spawning the child process fails (for another reason than /// [`FileNotFound`](Error::FileNotFound)), /// - writing to `stdin` of the child process fails, /// - reading from `stdout` or `stderr` of the child process fails, /// - writing to the parent's `stdout` or `stderr` fails, /// - the given executable doesn't have the executable flag set. CommandIoError { message: String, source: io::Error }, /// The child process exited with a non-zero exit code. /// /// ``` /// use cradle::prelude::*; /// /// let result: Result<(), cradle::Error> = run_result!("false"); /// match result { /// Err(Error::NonZeroExitCode { .. }) => {} /// _ => panic!(), /// } /// ``` /// /// This error will be suppressed when [`Status`](crate::Status) is used. NonZeroExitCode { full_command: String, exit_status: ExitStatus, }, /// The child process's `stdout` is being captured, /// (e.g. with [`StdoutUntrimmed`](crate::StdoutUntrimmed)), /// but the process wrote bytes to its `stdout` that are not /// valid utf-8. InvalidUtf8ToStdout { full_command: String, source: FromUtf8Error, }, /// The child process's `stderr` is being captured, /// (with [`Stderr`](crate::Stderr)), /// but the process wrote bytes to its `stderr` that are not /// valid utf-8. InvalidUtf8ToStderr { full_command: String, source: FromUtf8Error, }, /// This error is raised when an internal invariant of `cradle` is broken, /// and likely indicates a bug. Internal { message: String, full_command: String, config: Config, }, } impl Error { pub(crate) fn command_io_error(config: &Config, source: io::Error) -> Error { Error::CommandIoError { message: format!("{}:\n {}", config.full_command(), source), source, } } pub(crate) fn internal(message: &str, config: &Config) -> Error { Error::Internal { message: message.to_string(), full_command: config.full_command(), config: config.clone(), } } } #[doc(hidden)] #[rustversion::attr(since(1.46), track_caller)] pub fn panic_on_error(result: Result) -> T { match result { Ok(t) => t, Err(error) => panic!("cradle error: {}", error), } } fn english_list(list: &[&str]) -> String { let mut result = String::new(); for (i, word) in list.iter().enumerate() { let is_first = i == 0; let is_last = i == list.len() - 1; if !is_first { result.push_str(if is_last { " and " } else { ", " }); } result.push('\''); result.push_str(word); result.push('\''); } result } fn executable_with_whitespace_note(executable: &str) -> Option { let words = executable.split_whitespace().collect::>(); if words.len() >= 2 { let intended_executable = words[0]; let intended_arguments = &words[1..]; let mut result = format!( "note: Given executable name '{}' contains whitespace.\n", executable ); result.push_str(&format!( " Did you mean to run '{}', with {} as {}?\n", intended_executable, english_list(intended_arguments), if intended_arguments.len() == 1 { "the argument" } else { "arguments" }, )); result.push_str(concat!( " Consider using Split: ", "https://docs.rs/cradle/latest/cradle/input/struct.Split.html" )); Some(result) } else { None } } impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use Error::*; match self { NoExecutableGiven => write!(f, "no arguments given"), FileNotFound { executable, .. } => { let executable = executable.to_string_lossy(); write!(f, "File not found error when executing '{}'", executable)?; if let Some(whitespace_note) = executable_with_whitespace_note(executable.as_ref()) { write!(f, "\n{}", whitespace_note)?; } Ok(()) } CommandIoError { message, .. } => write!(f, "{}", message), NonZeroExitCode { full_command, exit_status, } => { if let Some(exit_code) = exit_status.code() { write!( f, "{}:\n exited with exit code: {}", full_command, exit_code ) } else { write!(f, "{}:\n exited with {}", full_command, exit_status) } } InvalidUtf8ToStdout { full_command, .. } => { write!(f, "{}:\n invalid utf-8 written to stdout", full_command) } InvalidUtf8ToStderr { full_command, .. } => { write!(f, "{}:\n invalid utf-8 written to stderr", full_command) } Internal { .. } => { let snippets = vec![ "Congratulations, you've found a bug in cradle! :/", "Please, open an issue on https://github.com/soenkehahn/cradle/issues", "with the following information:", ]; writeln!(f, "{}\n{:#?}", snippets.join(" "), self) } } } } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { use Error::*; match self { FileNotFound { source, .. } | CommandIoError { source, .. } => Some(source), InvalidUtf8ToStdout { source, .. } | InvalidUtf8ToStderr { source, .. } => Some(source), NoExecutableGiven | NonZeroExitCode { .. } | Internal { .. } => None, } } } #[cfg(test)] mod tests { use super::*; use crate::prelude::*; use executable_path::executable_path; #[test] fn invalid_utf8_to_stdout_has_source() { let result: Result = run_result!( executable_path("cradle_test_helper").to_str().unwrap(), "invalid utf-8 stdout" ); assert!(std::error::Error::source(&result.unwrap_err()).is_some()); } #[test] fn invalid_utf8_to_stderr_has_source() { let result: Result = run_result!( executable_path("cradle_test_helper").to_str().unwrap(), "invalid utf-8 stderr" ); assert!(std::error::Error::source(&result.unwrap_err()).is_some()); } mod english_list { use super::*; use pretty_assertions::assert_eq; macro_rules! test { ($name:ident, $input:expr, $expected:expr) => { #[test] fn $name() { assert_eq!(english_list($input), $expected); } }; } test!(one, &["foo"], "'foo'"); test!(two, &["foo", "bar"], "'foo' and 'bar'"); test!(three, &["foo", "bar", "baz"], "'foo', 'bar' and 'baz'"); test!( four, &["foo", "bar", "baz", "boo"], "'foo', 'bar', 'baz' and 'boo'" ); } } cradle-0.2.2/src/input.rs000064400000000000000000000450530072674642500134120ustar 00000000000000//! The [`Input`] trait that defines all possible inputs to a child process. use crate::{ child_output::ChildOutput, config::Config, context::Context, error::{panic_on_error, Error}, output::Output, }; use std::{ ffi::{OsStr, OsString}, io::Write, path::{Path, PathBuf}, sync::Arc, }; /// All types that are possible arguments to [`run!`], [`run_output!`] or /// [`run_result!`] must implement this trait. /// This makes `cradle` very flexible. /// For example you can pass in an executable as a String, /// and a variable number of arguments as a [`Vec`]: /// /// ``` /// use cradle::prelude::*; /// /// let executable = "echo"; /// let arguments = vec!["foo", "bar"]; /// let StdoutUntrimmed(output) = run_output!(executable, arguments); /// assert_eq!(output, "foo bar\n"); /// ``` /// /// For more documentation on all possible input types, /// see the documentation for the individual impls of [`Input`]. /// Here's a non-exhaustive list of the most commonly used types to get you started: /// /// - [`String`] and [`&str`], /// - [`Split`] (and its shortcut `%`) to split commands by whitespace, /// - [`PathBuf`] and [`&Path`], /// - multiple sequence types, like [`vectors`], [`slices`] and (since version 1.51) [`arrays`], /// - [`CurrentDir`], /// - [`Env`] for setting environment variables, /// - [`Stdin`], and /// - [`LogCommand`]. /// /// [`String`]: trait.Input.html#impl-Input-for-String /// [`&str`]: trait.Input.html#impl-Input-for-%26str /// [`PathBuf`]: trait.Input.html#impl-Input-for-PathBuf /// [`&Path`]: trait.Input.html#impl-Input-for-%26Path /// [`vectors`]: trait.Input.html#impl-Input-for-Vec /// [`slices`]: trait.Input.html#impl-Input-for-%26[T] /// [`arrays`]: trait.Input.html#impl-Input-for-[T%3B%20N] /// /// ## Tuples /// /// `cradle` also implements [`Input`] for tuples of types that themselves implement [`Input`]. /// Instead of passing multiple arguments to [`run!`], they can be passed in a single tuple: /// /// ``` /// use cradle::prelude::*; /// /// let args = ("echo", "foo"); /// let StdoutTrimmed(output) = run_output!(args); /// assert_eq!(output, "foo"); /// ``` /// /// This can be used to group arguments: /// /// ``` /// use cradle::prelude::*; /// /// let to_hex_command = ("xxd", "-ps", "-u", LogCommand); /// let StdoutTrimmed(output) = run_output!(to_hex_command, Stdin(&[14, 15, 16])); /// assert_eq!(output, "0E0F10"); /// ``` /// /// Also, tuples make it possible to write wrappers around [`run!`] without requiring the use of macros: /// /// ``` /// use cradle::prelude::*; /// /// fn to_hex(input: I) -> String { /// let StdoutTrimmed(hex) = run_output!(%"xxd -ps -u", input); /// hex /// } /// /// // It works for slices: /// let hex = to_hex(Stdin(&[14, 15, 16])); /// assert_eq!(hex, "0E0F10"); /// /// // Vectors: /// let hex = to_hex(Stdin(vec![14, 15, 16])); /// assert_eq!(hex, "0E0F10"); /// /// // And multiple arguments using tuples: /// let hex = to_hex((Stdin(&[14, 15, 16]), Stdin(&[17, 18, 19]))); /// assert_eq!(hex, "0E0F10111213"); /// ``` /// /// ## Custom [`Input`] impls /// /// The provided `Input` implementations should be sufficient for most use cases, /// but custom `Input` implementations can be written to extend `cradle`. /// /// Here's an example of an `Environment` type, that wraps /// [`BTreeMap`](std::collections::BTreeMap) and adds all contained /// key-value pairs to the environment of the child process. /// /// ``` /// use cradle::prelude::*; /// use cradle::config::Config; /// use std::collections::BTreeMap; /// /// struct Environment(BTreeMap); /// /// impl Environment { /// fn new() -> Self { /// Environment(BTreeMap::new()) /// } /// /// fn add(mut self, key: &str, value: &str) -> Self { /// self.0.insert(key.to_owned(), value.to_owned()); /// self /// } /// } /// /// impl Input for Environment { /// fn configure(self, config: &mut Config) { /// for (key, value) in self.0.into_iter() { /// Env(key, value).configure(config) /// } /// } /// } /// /// let env_vars = Environment::new() /// .add("FOO", "foo") /// .add("BAR", "bar"); /// /// let StdoutUntrimmed(output) = run_output!("env", env_vars); /// assert!(output.contains("FOO=foo\n")); /// assert!(output.contains("BAR=bar\n")); /// ``` /// /// It is not recommended to override [`run`](Input::run), /// [`run_output`](Input::run_output) or [`run_result`](Input::run_result). /// /// Also note that all fields of the type [`Config`] are private. /// That means that when you're writing your own [`Input`] impls, /// you _have_ to implement the [`Input::configure`] method /// of your type in terms of the [`Input::configure`] methods /// of the various [`Input`] types that `cradle` provides -- /// as demonstrated in the code snippet above. /// [`Config`]'s fields are private to allow to add new features to `cradle` /// without introducing breaking API changes. pub trait Input: Sized { /// Configures the given [`Config`](crate::config::Config) for the [`Input`] `self`. /// Usually you won't have to write your own custom impls for [`Input`], /// nor call this function yourself. /// So you can safely ignore this method. /// /// See also [Custom `Input` impls](#custom-input-impls). fn configure(self, config: &mut Config); /// `input.run()` runs `input` as a child process. /// It's equivalent to `run!(input)`. /// /// ``` /// # let temp_dir = tempfile::TempDir::new().unwrap(); /// # std::env::set_current_dir(&temp_dir).unwrap(); /// use cradle::prelude::*; /// /// ("touch", "foo").run(); /// ``` #[rustversion::attr(since(1.46), track_caller)] fn run(self) { self.run_output() } /// `input.run()` runs `input` as a child process. /// It's equivalent to `run_output!(input)`. /// /// ``` /// use cradle::prelude::*; /// /// let StdoutTrimmed(output) = ("echo", "foo").run_output(); /// assert_eq!(output, "foo"); /// ``` #[rustversion::attr(since(1.46), track_caller)] fn run_output(self) -> O where O: Output, { panic_on_error(self.run_result()) } /// `input.run_result()` runs `input` as a child process. /// It's equivalent to `run_result!(input)`. /// /// ``` /// use cradle::prelude::*; /// /// # fn build() -> Result<(), Error> { /// // make sure build tools are installed /// run_result!(%"which make")?; /// run_result!(%"which gcc")?; /// run_result!(%"which ld")?; /// run_result!(%"make build")?; /// # Ok(()) /// # } /// ``` fn run_result(self) -> Result where O: Output, { let context = Context::production(); run_result_with_context(context, self) } } pub(crate) fn run_result_with_context( context: Context, input: I, ) -> Result where Stdout: Write + Clone + Send + 'static, Stderr: Write + Clone + Send + 'static, I: Input, O: Output, { let mut config = Config::default(); input.configure(&mut config); ChildOutput::run_child_process_output(context, config) } #[cfg(test)] pub(crate) fn run_result_with_context_unit( context: Context, input: I, ) -> Result<(), Error> where Stdout: Write + Clone + Send + 'static, Stderr: Write + Clone + Send + 'static, I: Input, { run_result_with_context(context, input) } /// Blanket implementation for `&_`. impl Input for &T where T: Input + Clone, { #[doc(hidden)] fn configure(self, config: &mut Config) { self.clone().configure(config); } } /// Arguments of type [`OsString`] are passed to the child process /// as arguments. /// /// ``` /// use cradle::prelude::*; /// /// run!("ls", std::env::var_os("HOME").unwrap()); /// ``` impl Input for OsString { #[doc(hidden)] fn configure(self, config: &mut Config) { config.arguments.push(self); } } /// Arguments of type [`&OsStr`] are passed to the child process /// as arguments. /// /// ``` /// use cradle::prelude::*; /// /// run!("echo", std::env::current_dir().unwrap().file_name().unwrap()); /// ``` /// /// [`&OsStr`]: std::ffi::OsStr impl Input for &OsStr { #[doc(hidden)] fn configure(self, config: &mut Config) { self.to_os_string().configure(config); } } /// Arguments of type [`&str`] are passed to the child process as arguments. /// This is especially useful because it allows you to use string literals: /// /// ``` /// use cradle::prelude::*; /// /// let StdoutTrimmed(output) = run_output!("echo", "foo"); /// assert_eq!(output, "foo"); /// ``` impl Input for &str { #[doc(hidden)] fn configure(self, config: &mut Config) { OsStr::new(self).configure(config); } } /// Arguments of type [`String`] are passed to the child process /// as arguments. Executables can also be passed as [`String`]s: /// /// ``` /// use cradle::prelude::*; /// /// let executable: String = "echo".to_string(); /// let argument: String = "foo".to_string(); /// let StdoutTrimmed(output) = run_output!(executable, argument); /// assert_eq!(output, "foo"); /// ``` impl Input for String { #[doc(hidden)] fn configure(self, config: &mut Config) { OsString::from(self).configure(config); } } /// Splits the contained string by whitespace (using [`split_whitespace`]) /// and uses the resulting words as separate arguments. /// /// ``` /// use cradle::prelude::*; /// /// let StdoutTrimmed(output) = run_output!(Split("echo foo")); /// assert_eq!(output, "foo"); /// ``` /// /// Since this is such a common case, `cradle` also provides a syntactic shortcut /// for [`Split`], the `%` symbol: /// /// ``` /// use cradle::prelude::*; /// /// let StdoutTrimmed(output) = run_output!(%"echo foo"); /// assert_eq!(output, "foo"); /// ``` /// /// [`split_whitespace`]: str::split_whitespace #[derive(Debug, PartialEq, Clone)] pub struct Split(pub &'static str); impl Input for crate::input::Split { #[doc(hidden)] fn configure(self, config: &mut Config) { for argument in self.0.split_whitespace() { argument.configure(config); } } } /// Allows to use [`split`] to split your argument into words: /// /// ``` /// use cradle::prelude::*; /// /// let StdoutTrimmed(output) = run_output!("echo foo".split(' ')); /// assert_eq!(output, "foo"); /// ``` /// /// Arguments to [`split`] must be of type [`char`]. /// /// [`split`]: str::split impl Input for std::str::Split<'static, char> { #[doc(hidden)] fn configure(self, config: &mut Config) { for word in self { word.configure(config); } } } /// Allows to use [`split_whitespace`] to split your argument into words: /// /// ``` /// use cradle::prelude::*; /// /// let StdoutTrimmed(output) = run_output!("echo foo".split_whitespace()); /// assert_eq!(output, "foo"); /// ``` /// /// [`split_whitespace`]: str::split_whitespace impl Input for std::str::SplitWhitespace<'static> { #[doc(hidden)] fn configure(self, config: &mut Config) { for word in self { word.configure(config); } } } /// Allows to use [`split_ascii_whitespace`] to split your argument into words: /// /// ``` /// use cradle::prelude::*; /// /// let StdoutTrimmed(output) = run_output!("echo foo".split_ascii_whitespace()); /// assert_eq!(output, "foo"); /// ``` /// /// [`split_ascii_whitespace`]: str::split_ascii_whitespace impl Input for std::str::SplitAsciiWhitespace<'static> { #[doc(hidden)] fn configure(self, config: &mut Config) { for word in self { word.configure(config); } } } impl Input for () { #[doc(hidden)] fn configure(self, _: &mut Config) {} } macro_rules! tuple_impl { ($($index:tt, $generics:ident,)+) => { impl<$($generics),+> Input for ($($generics,)+) where $($generics: Input,)+ { #[doc(hidden)] fn configure(self, config: &mut Config) { $(<$generics as Input>::configure(self.$index, config);)+ } } }; } tuple_impl!(0, A,); tuple_impl!(0, A, 1, B,); tuple_impl!(0, A, 1, B, 2, C,); tuple_impl!(0, A, 1, B, 2, C, 3, D,); tuple_impl!(0, A, 1, B, 2, C, 3, D, 4, E,); tuple_impl!(0, A, 1, B, 2, C, 3, D, 4, E, 5, F,); tuple_impl!(0, A, 1, B, 2, C, 3, D, 4, E, 5, F, 6, G,); tuple_impl!(0, A, 1, B, 2, C, 3, D, 4, E, 5, F, 6, G, 7, H,); tuple_impl!(0, A, 1, B, 2, C, 3, D, 4, E, 5, F, 6, G, 7, H, 8, I,); tuple_impl!(0, A, 1, B, 2, C, 3, D, 4, E, 5, F, 6, G, 7, H, 8, I, 9, J,); tuple_impl!(0, A, 1, B, 2, C, 3, D, 4, E, 5, F, 6, G, 7, H, 8, I, 9, J, 10, K,); tuple_impl!(0, A, 1, B, 2, C, 3, D, 4, E, 5, F, 6, G, 7, H, 8, I, 9, J, 10, K, 11, L,); tuple_impl!(0, A, 1, B, 2, C, 3, D, 4, E, 5, F, 6, G, 7, H, 8, I, 9, J, 10, K, 11, L, 12, M,); tuple_impl!( 0, A, 1, B, 2, C, 3, D, 4, E, 5, F, 6, G, 7, H, 8, I, 9, J, 10, K, 11, L, 12, M, 13, N, ); tuple_impl!( 0, A, 1, B, 2, C, 3, D, 4, E, 5, F, 6, G, 7, H, 8, I, 9, J, 10, K, 11, L, 12, M, 13, N, 14, O, ); tuple_impl!( 0, A, 1, B, 2, C, 3, D, 4, E, 5, F, 6, G, 7, H, 8, I, 9, J, 10, K, 11, L, 12, M, 13, N, 14, O, 15, P, ); /// All elements of the given [`Vec`] are used as arguments to the child process. /// Same as passing in the elements separately. /// /// ``` /// use cradle::prelude::*; /// /// let StdoutTrimmed(output) = run_output!(vec!["echo", "foo"]); /// assert_eq!(output, "foo"); /// ``` impl Input for Vec where T: Input, { #[doc(hidden)] fn configure(self, config: &mut Config) { for t in self.into_iter() { t.configure(config); } } } /// Similar to the implementation for [`Vec`]. /// All elements of the array will be used as arguments. /// /// ``` /// use cradle::prelude::*; /// /// let StdoutTrimmed(output) = run_output!(["echo", "foo"]); /// assert_eq!(output, "foo"); /// ``` /// /// Only works on rust version `1.51` and up. #[rustversion::since(1.51)] impl Input for [T; N] where T: Input, { #[doc(hidden)] fn configure(self, config: &mut Config) { #[rustversion::before(1.59)] fn array_to_iter(array: [T; N]) -> impl Iterator { std::array::IntoIter::new(array) } #[rustversion::since(1.59)] fn array_to_iter(array: [T; N]) -> impl Iterator { IntoIterator::into_iter(array) } for t in array_to_iter(self) { t.configure(config); } } } /// Similar to the implementation for [`Vec`]. /// All elements of the slice will be used as arguments. impl Input for &[T] where T: Input + Clone, { #[doc(hidden)] fn configure(self, config: &mut Config) { self.to_vec().configure(config); } } /// Passing in [`LogCommand`] as an argument to `cradle` will cause it /// to log the commands (including all arguments) to `stderr`. /// (This is similar `bash`'s `-x` option.) /// /// ``` /// use cradle::prelude::*; /// /// run!(LogCommand, %"echo foo"); /// // writes '+ echo foo' to stderr /// ``` #[derive(Debug, Clone, Copy)] pub struct LogCommand; impl Input for LogCommand { #[doc(hidden)] fn configure(self, config: &mut Config) { config.log_command = true; } } /// By default child processes inherit the current directory from their /// parent. You can override this with [`CurrentDir`]: /// /// ``` /// use cradle::prelude::*; /// /// # #[cfg(target_os = "linux")] /// # { /// let StdoutTrimmed(output) = run_output!("pwd", CurrentDir("/tmp")); /// assert_eq!(output, "/tmp"); /// # } /// ``` /// /// Paths that are relative to the parent's current directory are allowed. #[derive(Debug, Clone)] pub struct CurrentDir>(pub T); impl Input for CurrentDir where T: AsRef, { #[doc(hidden)] fn configure(self, config: &mut Config) { config.working_directory = Some(self.0.as_ref().to_owned()); } } /// Arguments of type [`PathBuf`] are passed to the child process /// as arguments. /// /// ``` /// use cradle::prelude::*; /// use std::path::PathBuf; /// /// let current_dir: PathBuf = std::env::current_dir().unwrap(); /// run!("ls", current_dir); /// ``` impl Input for PathBuf { #[doc(hidden)] fn configure(self, config: &mut Config) { self.into_os_string().configure(config); } } /// Arguments of type [`&Path`] are passed to the child process /// as arguments. /// /// ``` /// # let temp_dir = tempfile::TempDir::new().unwrap(); /// # std::env::set_current_dir(&temp_dir).unwrap(); /// use cradle::prelude::*; /// use std::path::Path; /// /// let file: &Path = Path::new("./foo"); /// run!("touch", file); /// ``` /// /// [`&Path`]: std::path::Path impl Input for &Path { #[doc(hidden)] fn configure(self, config: &mut Config) { self.as_os_str().to_os_string().configure(config); } } /// Writes the given byte slice to the child's standard input. /// /// ``` /// use cradle::prelude::*; /// /// # #[cfg(target_os = "linux")] /// # { /// let StdoutUntrimmed(output) = run_output!("sort", Stdin("foo\nbar\n")); /// assert_eq!(output, "bar\nfoo\n"); /// # } /// ``` /// /// If `Stdin` is used multiple times, all given bytes slices will be written /// to the child's standard input in order. #[derive(Debug, Clone)] pub struct Stdin>(pub T); impl Input for Stdin where T: AsRef<[u8]>, { #[doc(hidden)] fn configure(self, config: &mut Config) { match config.stdin.as_mut() { Some(arc) => { Arc::make_mut(arc).extend_from_slice(self.0.as_ref()); } None => { config.stdin = Some(Arc::new(self.0.as_ref().to_vec())); } } } } /// Adds an environment variable to the environment of the child process. /// /// ``` /// use cradle::prelude::*; /// /// let StdoutUntrimmed(output) = run_output!("env", Env("FOO", "bar")); /// assert!(output.contains("FOO=bar\n")); /// ``` /// /// Child processes inherit the environment of the parent process. /// [`Env`] only adds environment variables to that inherited environment. /// If the environment variable is also set in the parent process, /// it is overwritten by [`Env`]. #[derive(Debug, Clone)] pub struct Env(pub Key, pub Value) where Key: AsRef, Value: AsRef; impl Input for Env where Key: AsRef, Value: AsRef, { #[doc(hidden)] fn configure(self, config: &mut Config) { let Self(key, value) = self; config .added_environment_variables .push((key.as_ref().to_os_string(), value.as_ref().to_os_string())); } } cradle-0.2.2/src/lib.rs000064400000000000000000001371360072674642500130250ustar 00000000000000#![deny(missing_debug_implementations)] //! `cradle` provides the [`run!`] macro, that makes //! it easy to run child processes from rust programs. //! //! ``` //! # let temp_dir = tempfile::TempDir::new().unwrap(); //! # std::env::set_current_dir(&temp_dir).unwrap(); //! use cradle::prelude::*; //! use std::path::Path; //! //! run!(%"touch foo"); //! assert!(Path::new("foo").is_file()); //! ``` //! //! # Input //! //! You can pass in multiple arguments (of different types) to [`run!`] //! to specify arguments, as long as they implement the [`Input`] trait: //! //! ``` //! # let temp_dir = tempfile::TempDir::new().unwrap(); //! # std::env::set_current_dir(&temp_dir).unwrap(); //! use cradle::prelude::*; //! use std::path::Path; //! //! run!("mkdir", "-p", "foo/bar/baz"); //! assert!(Path::new("foo/bar/baz").is_dir()); //! ``` //! //! For all possible inputs to [`run!`], see the documentation of [`Input`]. //! //! # Output //! //! `cradle` also provides a [`run_output!`] macro. //! It allows to capture outputs of the child process. //! It uses return-type polymorphism, so you can control which outputs //! are captured by choosing the return type of [`run_output!`]. //! The only constraint is that the chosen return type has to implement [`Output`]. //! For example you can use e.g. [`StdoutTrimmed`] //! to collect what the child process writes to `stdout`, //! trimmed of leading and trailing whitespace: //! //! ``` //! use cradle::prelude::*; //! //! let StdoutTrimmed(output) = run_output!(%"echo foo"); //! assert_eq!(output, "foo"); //! ``` //! //! (By default, the child's `stdout` is written to the parent's `stdout`. //! Using `StdoutTrimmed` as the return type suppresses that.) //! //! If you don't want any result from [`run_output!`], you can use `()` //! as the return value: //! //! ``` //! # let temp_dir = tempfile::TempDir::new().unwrap(); //! # std::env::set_current_dir(&temp_dir).unwrap(); //! use cradle::prelude::*; //! //! let () = run_output!(%"touch foo"); //! ``` //! //! Since that's a very common case, `cradle` provides the [`run!`] shortcut, //! that we've already seen above. //! It behaves exactly like [`run_output!`] but always returns `()`: //! //! ``` //! # let temp_dir = tempfile::TempDir::new().unwrap(); //! # std::env::set_current_dir(&temp_dir).unwrap(); //! use cradle::prelude::*; //! //! run!(%"touch foo"); //! ``` //! //! See the implementations for [`output::Output`] for all the supported types. //! //! # Whitespace Splitting of Inputs //! //! `cradle` does *not* split given string arguments on whitespace by default. //! So for example this code fails: //! //! ``` should_panic //! use cradle::prelude::*; //! //! let StdoutTrimmed(_) = run_output!("echo foo"); //! ``` //! //! In this code `cradle` tries to run a process from an executable called //! `"echo foo"`, including the space in the file name of the executable. //! That fails, because an executable with that name doesn't exist. //! `cradle` provides a new-type wrapper [`Split`] to help with that: //! //! ``` //! use cradle::prelude::*; //! //! let StdoutTrimmed(output) = run_output!(Split("echo foo")); //! assert_eq!(output, "foo"); //! ``` //! //! Wrapping an argument of type `&str` in [`Split`] will cause `cradle` to first //! split it by whitespace and then use the resulting words as if they were passed //! into [`run_output!`] as separate arguments. //! //! And -- since this is such a common case -- `cradle` provides a syntactic shortcut //! for [`Split`], the `%` symbol: //! //! ``` //! use cradle::prelude::*; //! //! let StdoutTrimmed(output) = run_output!(%"echo foo"); //! assert_eq!(output, "foo"); //! ``` //! //! # Error Handling //! //! **tl;dr:** [`run!`] and [`run_output!`] will panic on errors, //! [`run_result!`] will not. //! //! ## Panicking //! //! By default [`run!`] and [`run_output!`] panic when something goes wrong, //! for example when the executable cannot be found or //! when a child process exits with a non-zero exit code. //! This is by design to allow `cradle` to be used in contexts //! where more complex error handling is not needed or desired, //! for example in scripts. //! //! ``` should_panic //! use cradle::prelude::*; //! //! // panics with "false:\n exited with exit code: 1" //! run!("false"); //! ``` //! //! For a full list of reasons why [`run!`] and [`run_output!`] may panic, //! see the documentation of `cradle`'s [`Error`] type. //! //! ## Preventing Panics //! //! You can also turn **all** panics into [`std::result::Result::Err`]s //! by using [`run_result!`]. This will return a value of type //! [`Result`], where //! `T` is any type that implements [`output::Output`]. //! Here's some examples: //! //! ``` //! use cradle::prelude::*; //! //! let result: Result<(), cradle::Error> = run_result!("false"); //! let error_message = format!("{}", result.unwrap_err()); //! assert_eq!( //! error_message, //! "false:\n exited with exit code: 1" //! ); //! //! let result = run_result!(%"echo foo"); //! let StdoutTrimmed(output) = result.unwrap(); //! assert_eq!(output, "foo".to_string()); //! ``` //! //! [`run_result!`] can also be combined with `?` to handle errors in an //! idiomatic way, for example: //! //! ``` //! use cradle::prelude::*; //! //! fn build() -> Result<(), Error> { //! run_result!(%"which make")?; //! run_result!(%"which gcc")?; //! run_result!(%"which ld")?; //! run_result!(%"make build")?; //! Ok(()) //! } //! ``` //! //! If you don't want to prevent **all** panics, //! but just panics caused by non-zero exit codes, //! you can use [`Status`]. //! //! # Alternative Interface: Methods on [`input::Input`] //! //! `cradle` also provides an alternative interface to execute commands //! through methods on the [`Input`] trait: //! [`.run()`](Input::run), [`.run_output()`](Input::run_output) //! and [`.run_result()`](Input::run_result). //! These methods can be invoked on all values whose types implement //! [`Input`]. //! When using these methods, it's especially useful that //! [`Input`] is implemented by tuples. //! They work analog to [`run!`], [`run_output!`] and [`run_result!`]. //! Here are some examples: //! //! ``` //! # let temp_dir = tempfile::TempDir::new().unwrap(); //! # std::env::set_current_dir(&temp_dir).unwrap(); //! use cradle::prelude::*; //! //! ("touch", "foo").run(); //! //! let StdoutTrimmed(output) = ("echo", "foo").run_output(); //! assert_eq!(output, "foo"); //! //! let result: Result<(), cradle::Error> = "false".run_result(); //! let error_message = format!("{}", result.unwrap_err()); //! assert_eq!( //! error_message, //! "false:\n exited with exit code: 1" //! ); //! ``` //! //! Note: The `%` shortcut for [`Split`] is not available in this notation. //! You can either use tuples, or [`Split`] explicitly: //! //! ``` //! use cradle::prelude::*; //! //! ("echo", "foo").run(); //! Split("echo foo").run(); //! ``` //! //! # Prior Art //! //! `cradle` is heavily inspired by [shake](https://shakebuild.com/), //! specifically by its //! [`cmd`](https://hackage.haskell.org/package/shake-0.19.4/docs/Development-Shake.html#v:cmd) //! function. pub mod child_output; mod collected_output; pub mod config; mod context; pub mod error; pub mod input; mod macros; pub mod output; pub mod prelude; include!("common_re_exports.rs.snippet"); #[cfg(test)] mod tests { use crate::{ context::Context, input::{run_result_with_context, run_result_with_context_unit}, prelude::*, }; use lazy_static::lazy_static; use std::{ collections::BTreeSet, env::{current_dir, set_current_dir}, ffi::{OsStr, OsString}, fs, io::Write, path::PathBuf, sync::{Arc, Mutex}, }; use tempfile::TempDir; fn in_temporary_directory(f: F) where F: FnOnce() + std::panic::UnwindSafe, { lazy_static! { static ref CURRENT_DIR_LOCK: Mutex<()> = Mutex::new(()); } let _lock = CURRENT_DIR_LOCK.lock(); let temp_dir = TempDir::new().unwrap(); let original_working_directory = current_dir().unwrap(); set_current_dir(&temp_dir).unwrap(); let result = std::panic::catch_unwind(|| { f(); }); set_current_dir(original_working_directory).unwrap(); result.unwrap(); } fn test_executable(name: &str) -> PathBuf { lazy_static! { static ref BUILT: Arc>> = Arc::new(Mutex::new(BTreeSet::new())); } let mut set = match BUILT.lock() { Ok(set) => set, Err(error) => { let _ = write!( std::io::stderr(), "test_executable: BUILT poisoned: {}", error ); let _ = std::io::stderr().flush(); std::process::exit(1) } }; if !set.contains(name) { set.insert(name.to_owned()); run!( LogCommand, CurrentDir(std::env::var("CARGO_MANIFEST_DIR").unwrap()), %"cargo build", ("--bin", name), %"--features test_executables", ); } executable_path::executable_path(name) } fn test_helper() -> PathBuf { test_executable("test_executables_helper") } #[test] fn allows_to_execute_a_command() { in_temporary_directory(|| { run!(%"touch foo"); assert!(PathBuf::from("foo").exists()); }) } mod errors { use super::*; mod panics_by_default { use super::*; #[test] #[should_panic(expected = "cradle error: false:\n exited with exit code: 1")] fn non_zero_exit_codes() { run!("false"); } #[test] #[should_panic(expected = "cradle error: false:\n exited with exit code: 1")] fn combine_panics_with_other_outputs() { let StdoutTrimmed(_) = run_output!("false"); } #[test] #[should_panic(expected = "cradle error: false foo bar:\n exited with exit code: 1")] fn includes_full_command_on_non_zero_exit_codes() { run!(%"false foo bar"); } #[test] #[should_panic(expected = "exited with exit code: 42")] fn other_exit_codes() { run!(test_helper(), "exit code 42"); } #[test] #[should_panic( expected = "cradle error: File not found error when executing 'does-not-exist'" )] fn executable_cannot_be_found() { run!("does-not-exist"); } #[test] #[cfg(unix)] #[should_panic(expected = "/file foo bar:\n Permission denied (os error 13)")] fn includes_full_command_on_io_errors() { let temp_dir = TempDir::new().unwrap(); let without_executable_bit = temp_dir.path().join("file"); fs::write(&without_executable_bit, "").unwrap(); run!(without_executable_bit, %"foo bar"); } #[rustversion::since(1.46)] #[test] fn includes_source_location_of_run_run_call() { let (Status(_), Stderr(stderr)) = run_output!(test_executable("test_executables_panic")); let expected = "src/test_executables/panic.rs:4:5"; assert!( stderr.contains(expected), "{:?}\n does not contain\n{:?}", stderr, expected ); } #[test] #[should_panic(expected = "cradle error: no arguments given")] fn no_executable() { let vector: Vec = Vec::new(); run!(vector); } #[test] #[should_panic(expected = "invalid utf-8 written to stdout")] fn invalid_utf8_stdout() { let StdoutTrimmed(_) = run_output!(test_helper(), "invalid utf-8 stdout"); } #[test] #[cfg(not(windows))] fn invalid_utf8_to_stdout_is_allowed_when_not_captured() { run!(test_helper(), "invalid utf-8 stdout"); } } mod result_types { use super::*; use pretty_assertions::assert_eq; #[test] fn non_zero_exit_codes() { let result: Result<(), Error> = run_result!("false"); assert_eq!( result.unwrap_err().to_string(), "false:\n exited with exit code: 1" ); } #[test] fn no_errors() { let result: Result<(), Error> = run_result!("true"); result.unwrap(); } #[test] fn combine_ok_with_other_outputs() { let StdoutTrimmed(output) = run_result!(%"echo foo").unwrap(); assert_eq!(output, "foo".to_string()); } #[test] fn combine_err_with_other_outputs() { let result: Result = run_result!("false"); assert_eq!( result.unwrap_err().to_string(), "false:\n exited with exit code: 1" ); } #[test] fn includes_full_command_on_non_zero_exit_codes() { let result: Result<(), Error> = run_result!(%"false foo bar"); assert_eq!( result.unwrap_err().to_string(), "false foo bar:\n exited with exit code: 1" ); } #[test] fn includes_full_command_on_io_errors() { in_temporary_directory(|| { fs::write("without-executable-bit", "").unwrap(); let result: Result<(), Error> = run_result!(%"./without-executable-bit foo bar"); assert_eq!( result.unwrap_err().to_string(), if cfg!(windows) { "./without-executable-bit foo bar:\n %1 is not a valid Win32 application. (os error 193)" } else { "./without-executable-bit foo bar:\n Permission denied (os error 13)" } ); }); } #[test] fn other_exit_codes() { let result: Result<(), Error> = run_result!(test_helper(), "exit code 42"); assert!(result .unwrap_err() .to_string() .contains("exited with exit code: 42")); } #[test] fn missing_executable_file_error_message() { let result: Result<(), Error> = run_result!("does-not-exist"); assert_eq!( result.unwrap_err().to_string(), "File not found error when executing 'does-not-exist'" ); } #[test] fn missing_executable_file_error_can_be_matched_against() { let result: Result<(), Error> = run_result!("does-not-exist"); match result { Err(Error::FileNotFound { executable, .. }) => { assert_eq!(executable, "does-not-exist"); } _ => panic!("should match Error::FileNotFound"), } } #[test] fn missing_executable_file_error_can_be_caused_by_relative_paths() { let result: Result<(), Error> = run_result!("./does-not-exist"); match result { Err(Error::FileNotFound { executable, .. }) => { assert_eq!(executable, "./does-not-exist"); } _ => panic!("should match Error::FileNotFound"), } } #[test] fn no_executable() { let vector: Vec = Vec::new(); let result: Result<(), Error> = run_result!(vector); assert_eq!(result.unwrap_err().to_string(), "no arguments given"); } #[test] fn invalid_utf8_stdout() { let test_helper = test_helper(); let result: Result = run_result!(&test_helper, "invalid utf-8 stdout"); assert_eq!( result.unwrap_err().to_string(), format!( "{} 'invalid utf-8 stdout':\n invalid utf-8 written to stdout", test_helper.display() ) ); } } mod whitespace_in_executable_note { use super::*; use pretty_assertions::assert_eq; use unindent::Unindent; #[test] fn missing_executable_file_with_whitespace_includes_note() { let result: Result<(), Error> = run_result!("does not exist"); let expected = " File not found error when executing 'does not exist' note: Given executable name 'does not exist' contains whitespace. Did you mean to run 'does', with 'not' and 'exist' as arguments? Consider using Split: https://docs.rs/cradle/latest/cradle/input/struct.Split.html " .unindent() .trim() .to_string(); assert_eq!(result.unwrap_err().to_string(), expected); } #[test] fn single_argument() { let result: Result<(), Error> = run_result!("foo bar"); let expected = " File not found error when executing 'foo bar' note: Given executable name 'foo bar' contains whitespace. Did you mean to run 'foo', with 'bar' as the argument? Consider using Split: https://docs.rs/cradle/latest/cradle/input/struct.Split.html " .unindent() .trim() .to_string(); assert_eq!(result.unwrap_err().to_string(), expected); } } } #[test] fn allows_to_retrieve_stdout() { let StdoutTrimmed(stdout) = run_output!(%"echo foo"); assert_eq!(stdout, "foo"); } #[test] fn command_and_argument_as_separate_ref_str() { let StdoutTrimmed(stdout) = run_output!("echo", "foo"); assert_eq!(stdout, "foo"); } #[test] fn multiple_arguments_as_ref_str() { let StdoutTrimmed(stdout) = run_output!("echo", "foo", "bar"); assert_eq!(stdout, "foo bar"); } #[test] fn arguments_can_be_given_as_references() { let reference: &LogCommand = &LogCommand; let executable: &String = &"echo".to_string(); let argument: &String = &"foo".to_string(); let StdoutTrimmed(stdout) = run_output!(reference, executable, argument); assert_eq!(stdout, "foo"); } mod sequences { use super::*; #[test] fn allows_to_pass_in_arguments_as_a_vec_of_ref_str() { let args: Vec<&str> = vec!["foo"]; let StdoutTrimmed(stdout) = run_output!("echo", args); assert_eq!(stdout, "foo"); } #[test] fn vector_of_non_strings() { let context = Context::test(); let log_commands: Vec = vec![LogCommand]; let StdoutTrimmed(stdout) = run_result_with_context(context.clone(), (log_commands, Split("echo foo"))) .unwrap(); assert_eq!(stdout, "foo"); assert_eq!(context.stderr(), "+ echo foo\n"); } #[rustversion::since(1.51)] #[test] fn arrays_as_arguments() { let args: [&str; 2] = ["echo", "foo"]; let StdoutTrimmed(stdout) = run_output!(args); assert_eq!(stdout, "foo"); } #[rustversion::since(1.51)] #[test] fn arrays_of_non_strings() { let context = Context::test(); let log_commands: [LogCommand; 1] = [LogCommand]; let StdoutTrimmed(stdout) = run_result_with_context(context.clone(), (log_commands, Split("echo foo"))) .unwrap(); assert_eq!(stdout, "foo"); assert_eq!(context.stderr(), "+ echo foo\n"); } #[rustversion::since(1.51)] #[test] fn elements_in_arrays_are_not_split_by_whitespace() { in_temporary_directory(|| { let args: [&str; 1] = ["foo bar"]; run!("touch", args); assert!(PathBuf::from("foo bar").exists()); }); } #[rustversion::since(1.51)] #[test] fn array_refs_as_arguments() { let args: &[&str; 2] = &["echo", "foo"]; let StdoutTrimmed(stdout) = run_output!(args); assert_eq!(stdout, "foo"); } #[rustversion::since(1.51)] #[test] fn elements_in_array_refs_are_not_split_by_whitespace() { in_temporary_directory(|| { let args: &[&str; 1] = &["foo bar"]; run!("touch", args); assert!(PathBuf::from("foo bar").exists()); }); } #[test] fn slices_as_arguments() { let args: &[&str] = &["echo", "foo"]; let StdoutTrimmed(stdout) = run_output!(args); assert_eq!(stdout, "foo"); } #[test] fn slices_of_non_strings() { let context = Context::test(); let log_commands: &[LogCommand] = &[LogCommand]; let StdoutTrimmed(stdout) = run_result_with_context(context.clone(), (log_commands, Split("echo foo"))) .unwrap(); assert_eq!(stdout, "foo"); assert_eq!(context.stderr(), "+ echo foo\n"); } #[test] fn elements_in_slices_are_not_split_by_whitespace() { in_temporary_directory(|| { let args: &[&str] = &["foo bar"]; run!("touch", args); assert!(PathBuf::from("foo bar").exists()); }); } #[test] fn vector_of_vectors() { let StdoutTrimmed(output) = run_output!(vec![vec!["echo"], vec!["foo", "bar"]]); assert_eq!(output, "foo bar"); } } mod strings { use super::*; #[test] fn works_for_string() { let command: String = "true".to_string(); run!(command); } #[test] fn multiple_strings() { let command: String = "echo".to_string(); let argument: String = "foo".to_string(); let StdoutTrimmed(output) = run_output!(command, argument); assert_eq!(output, "foo"); } #[test] fn mix_ref_str_and_string() { let argument: String = "foo".to_string(); let StdoutTrimmed(output) = run_output!("echo", argument); assert_eq!(output, "foo"); } #[test] fn does_not_split_strings_in_vectors() { in_temporary_directory(|| { let argument: Vec = vec!["filename with spaces".to_string()]; run!("touch", argument); assert!(PathBuf::from("filename with spaces").exists()); }); } } mod os_strings { use super::*; #[test] fn works_for_os_string() { run!(OsString::from("true")); } #[test] fn works_for_os_str() { run!(OsStr::new("true")); } } mod stdout { use super::*; use std::{thread, time::Duration}; #[test] fn relays_stdout_by_default() { let context = Context::test(); run_result_with_context_unit(context.clone(), Split("echo foo")).unwrap(); assert_eq!(context.stdout(), "foo\n"); } #[test] fn relays_stdout_for_non_zero_exit_codes() { let context = Context::test(); let _ = run_result_with_context_unit( context.clone(), (test_helper(), "output foo and exit with 42"), ); assert_eq!(context.stdout(), "foo\n"); } #[test] fn streams_stdout() { in_temporary_directory(|| { let context = Context::test(); let context_clone = context.clone(); let thread = thread::spawn(|| { run_result_with_context_unit( context_clone, (test_helper(), "stream chunk then wait for file"), ) .unwrap(); }); while (context.stdout()) != "foo\n" { thread::sleep(Duration::from_secs_f32(0.05)); } run!(%"touch file"); thread.join().unwrap(); }); } #[test] fn does_not_relay_stdout_when_collecting_into_string() { let context = Context::test(); let StdoutTrimmed(_) = run_result_with_context(context.clone(), Split("echo foo")).unwrap(); assert_eq!(context.stdout(), ""); } #[test] fn does_not_relay_stdout_when_collecting_into_result_of_string() { let context = Context::test(); let _: Result = run_result_with_context(context.clone(), Split("echo foo")); assert_eq!(context.stdout(), ""); } } mod stderr { use super::*; use pretty_assertions::assert_eq; use std::{thread, time::Duration}; #[test] fn relays_stderr_by_default() { let context = Context::test(); run_result_with_context_unit(context.clone(), (test_helper(), "write to stderr")) .unwrap(); assert_eq!(context.stderr(), "foo\n"); } #[test] fn relays_stderr_for_non_zero_exit_codes() { let context = Context::test(); let _: Result<(), Error> = run_result_with_context( context.clone(), (test_helper(), "write to stderr and exit with 42"), ); assert_eq!(context.stderr(), "foo\n"); } #[test] fn streams_stderr() { in_temporary_directory(|| { let context = Context::test(); let context_clone = context.clone(); let thread = thread::spawn(|| { run_result_with_context_unit( context_clone, (test_helper(), "stream chunk to stderr then wait for file"), ) .unwrap(); }); loop { let expected = "foo\n"; let stderr = context.stderr(); if stderr == expected { break; } assert!( stderr.len() <= expected.len(), "expected: {}, got: {}", expected, stderr ); thread::sleep(Duration::from_secs_f32(0.05)); } run!(%"touch file"); thread.join().unwrap(); }); } #[test] fn capture_stderr() { let Stderr(stderr) = run_output!(test_helper(), "write to stderr"); assert_eq!(stderr, "foo\n"); } #[test] fn assumes_stderr_is_utf_8() { let result: Result = run_result!(test_helper(), "invalid utf-8 stderr"); assert_eq!( result.unwrap_err().to_string(), format!( "{} 'invalid utf-8 stderr':\n invalid utf-8 written to stderr", test_helper().display(), ) ); } #[test] #[cfg(not(windows))] fn does_allow_invalid_utf_8_to_stderr_when_not_captured() { run!(test_helper(), "invalid utf-8 stderr"); } #[test] fn does_not_relay_stderr_when_catpuring() { let context = Context::test(); let Stderr(_) = run_result_with_context(context.clone(), (test_helper(), "write to stderr")) .unwrap(); assert_eq!(context.stderr(), ""); } } mod log_commands { use super::*; #[test] fn logs_simple_commands() { let context = Context::test(); run_result_with_context_unit(context.clone(), (LogCommand, "true")).unwrap(); assert_eq!(context.stderr(), "+ true\n"); } #[test] fn logs_commands_with_arguments() { let context = Context::test(); run_result_with_context_unit(context.clone(), (LogCommand, Split("echo foo"))).unwrap(); assert_eq!(context.stderr(), "+ echo foo\n"); } #[test] fn quotes_arguments_with_spaces() { let context = Context::test(); run_result_with_context_unit(context.clone(), (LogCommand, "echo", "foo bar")).unwrap(); assert_eq!(context.stderr(), "+ echo 'foo bar'\n"); } #[test] fn quotes_empty_arguments() { let context = Context::test(); run_result_with_context_unit(context.clone(), (LogCommand, "echo", "")).unwrap(); assert_eq!(context.stderr(), "+ echo ''\n"); } #[test] #[cfg(unix)] fn arguments_with_invalid_utf8_will_be_logged_with_lossy_conversion() { use std::{ffi::OsStr, os::unix::prelude::OsStrExt, path::Path}; let context = Context::test(); let argument_with_invalid_utf8: &OsStr = OsStrExt::from_bytes(&[102, 111, 111, 0x80, 98, 97, 114]); let argument_with_invalid_utf8: &Path = argument_with_invalid_utf8.as_ref(); run_result_with_context_unit( context.clone(), (LogCommand, "echo", argument_with_invalid_utf8), ) .unwrap(); assert_eq!(context.stderr(), "+ echo foo�bar\n"); } } mod exit_status { use super::*; #[test] fn zero() { let Status(exit_status) = run_output!("true"); assert!(exit_status.success()); } #[test] fn one() { let Status(exit_status) = run_output!("false"); assert!(!exit_status.success()); } #[test] fn forty_two() { let Status(exit_status) = run_output!(test_helper(), "exit code 42"); assert!(!exit_status.success()); assert_eq!(exit_status.code(), Some(42)); } #[test] fn failing_commands_return_oks_when_exit_status_is_captured() { let Status(exit_status) = run_result!("false").unwrap(); assert!(!exit_status.success()); } } mod bool_output { use super::*; #[test] fn success_exit_status_is_true() { assert!(run_output!("true")); } #[test] fn failure_exit_status_is_false() { assert!(!run_output!("false")); } #[test] #[should_panic] fn io_error_panics() { assert!(run_output!("/")); } } mod tuple_inputs { use super::*; use pretty_assertions::assert_eq; #[test] fn two_tuple() { let StdoutTrimmed(output) = run_output!(("echo", "foo")); assert_eq!(output, "foo"); } #[test] fn three_tuples() { let StdoutTrimmed(output) = run_output!(("echo", "foo", "bar")); assert_eq!(output, "foo bar"); } #[test] fn nested_tuples() { let StdoutTrimmed(output) = run_output!(("echo", ("foo", "bar"))); assert_eq!(output, "foo bar"); } #[test] fn unit_input() { let StdoutTrimmed(output) = run_output!(("echo", ())); assert_eq!(output, ""); } } mod tuple_outputs { use super::*; #[test] fn two_tuple_1() { let (StdoutTrimmed(output), Status(exit_status)) = run_output!(test_helper(), "output foo and exit with 42"); assert_eq!(output, "foo"); assert_eq!(exit_status.code(), Some(42)); } #[test] fn two_tuple_2() { let (Status(exit_status), StdoutTrimmed(output)) = run_output!(test_helper(), "output foo and exit with 42"); assert_eq!(output, "foo"); assert_eq!(exit_status.code(), Some(42)); } #[test] fn result_of_tuple() { let (StdoutTrimmed(output), Status(exit_status)) = run_result!(%"echo foo").unwrap(); assert_eq!(output, "foo"); assert!(exit_status.success()); } #[test] fn result_of_tuple_when_erroring() { let (StdoutTrimmed(output), Status(exit_status)) = run_result!("false").unwrap(); assert_eq!(output, ""); assert_eq!(exit_status.code(), Some(1)); } #[test] fn three_tuples() { let (Stderr(stderr), StdoutTrimmed(stdout), Status(exit_status)) = run_output!(%"echo foo"); assert_eq!(stderr, ""); assert_eq!(stdout, "foo"); assert_eq!(exit_status.code(), Some(0)); } #[test] fn capturing_stdout_on_errors() { let (StdoutTrimmed(output), Status(exit_status)) = run_output!(test_helper(), "output foo and exit with 42"); assert!(!exit_status.success()); assert_eq!(output, "foo"); } #[test] fn capturing_stderr_on_errors() { let (Stderr(output), Status(exit_status)) = run_output!(test_helper(), "write to stderr and exit with 42"); assert!(!exit_status.success()); assert_eq!(output, "foo\n"); } } mod current_dir { use super::*; use std::path::Path; #[test] fn sets_the_working_directory() { in_temporary_directory(|| { fs::create_dir("dir").unwrap(); fs::write("dir/file", "foo").unwrap(); fs::write("file", "wrong file").unwrap(); let StdoutUntrimmed(output) = run_output!(%"cat file", CurrentDir("dir")); assert_eq!(output, "foo"); }); } #[test] fn works_for_other_types() { in_temporary_directory(|| { fs::create_dir("dir").unwrap(); let dir: String = "dir".to_string(); run!("true", CurrentDir(dir)); let dir: PathBuf = PathBuf::from("dir"); run!("true", CurrentDir(dir)); let dir: &Path = Path::new("dir"); run!("true", CurrentDir(dir)); }); } } mod capturing_stdout { use super::*; mod trimmed { use super::*; #[test] fn trims_trailing_whitespace() { let StdoutTrimmed(output) = run_output!(%"echo foo"); assert_eq!(output, "foo"); } #[test] fn trims_leading_whitespace() { let StdoutTrimmed(output) = run_output!(%"echo -n", " foo"); assert_eq!(output, "foo"); } #[test] fn does_not_remove_whitespace_within_output() { let StdoutTrimmed(output) = run_output!(%"echo -n", "foo bar"); assert_eq!(output, "foo bar"); } #[test] fn does_not_modify_output_without_whitespace() { let StdoutTrimmed(output) = run_output!(%"echo -n", "foo"); assert_eq!(output, "foo"); } #[test] fn does_not_relay_stdout() { let context = Context::test(); let StdoutTrimmed(_) = run_result_with_context(context.clone(), Split("echo foo")).unwrap(); assert_eq!(context.stdout(), ""); } } mod untrimmed { use super::*; #[test] fn does_not_trim_trailing_newline() { let StdoutUntrimmed(output) = run_output!(%"echo foo"); assert_eq!(output, "foo\n"); } #[test] fn does_not_trim_leading_whitespace() { let StdoutUntrimmed(output) = run_output!(%"echo -n", " foo"); assert_eq!(output, " foo"); } #[test] fn does_not_relay_stdout() { let context = Context::test(); let StdoutUntrimmed(_) = run_result_with_context(context.clone(), Split("echo foo")).unwrap(); assert_eq!(context.stdout(), ""); } } } mod split { use super::*; #[test] fn splits_words_by_whitespace() { let StdoutTrimmed(output) = run_output!(Split("echo foo")); assert_eq!(output, "foo"); } #[test] fn skips_multiple_whitespace_characters() { let StdoutUntrimmed(output) = run_output!("echo", Split("foo bar")); assert_eq!(output, "foo bar\n"); } #[test] fn trims_leading_whitespace() { let StdoutTrimmed(output) = run_output!(Split(" echo foo")); assert_eq!(output, "foo"); } #[test] fn trims_trailing_whitespace() { let StdoutUntrimmed(output) = run_output!("echo", Split("foo ")); assert_eq!(output, "foo\n"); } mod percent_sign { use super::*; #[test] fn splits_words() { let StdoutUntrimmed(output) = run_output!(%"echo foo"); assert_eq!(output, "foo\n"); } #[test] fn works_for_later_arguments() { let StdoutUntrimmed(output) = run_output!("echo", %"foo\tbar"); assert_eq!(output, "foo bar\n"); } #[test] fn for_first_of_multiple_arguments() { let StdoutUntrimmed(output) = run_output!(%"echo foo", "bar"); assert_eq!(output, "foo bar\n"); } #[test] fn non_literals() { let command = "echo foo"; let StdoutUntrimmed(output) = run_output!(%command); assert_eq!(output, "foo\n"); } #[test] fn in_run() { run!(%"echo foo"); } #[test] fn in_run_result() { let StdoutTrimmed(_) = run_result!(%"echo foo").unwrap(); } } } mod splitting_with_library_functions { use super::*; #[test] fn allow_to_use_split() { let StdoutTrimmed(output) = run_output!("echo foo".split(' ')); assert_eq!(output, "foo"); } #[test] fn split_whitespace() { let StdoutTrimmed(output) = run_output!("echo foo".split_whitespace()); assert_eq!(output, "foo"); } #[test] fn split_ascii_whitespace() { let StdoutTrimmed(output) = run_output!("echo foo".split_ascii_whitespace()); assert_eq!(output, "foo"); } } mod paths { use super::*; use pretty_assertions::assert_eq; use std::path::Path; fn write_test_script() -> PathBuf { if cfg!(unix) { let file = PathBuf::from("./test-script"); let script = "#!/usr/bin/env bash\necho test-output\n"; fs::write(&file, script).unwrap(); run!(%"chmod +x test-script"); file } else { let file = PathBuf::from("./test-script.bat"); let script = "@echo test-output\n"; fs::write(&file, script).unwrap(); file } } #[test] fn ref_path_as_argument() { in_temporary_directory(|| { let file: &Path = Path::new("file"); fs::write(file, "test-contents").unwrap(); let StdoutUntrimmed(output) = run_output!("cat", file); assert_eq!(output, "test-contents"); }) } #[test] fn ref_path_as_executable() { in_temporary_directory(|| { let file: &Path = &write_test_script(); let StdoutTrimmed(output) = run_output!(file); assert_eq!(output, "test-output"); }) } #[test] fn path_buf_as_argument() { in_temporary_directory(|| { let file: PathBuf = PathBuf::from("file"); fs::write(&file, "test-contents").unwrap(); let StdoutUntrimmed(output) = run_output!("cat", file); assert_eq!(output, "test-contents"); }) } #[test] fn path_buf_as_executable() { in_temporary_directory(|| { let file: PathBuf = write_test_script(); let StdoutTrimmed(output) = run_output!(file); assert_eq!(output, "test-output"); }) } } mod stdin { use super::*; #[test] fn allows_to_pass_in_strings_as_stdin() { let StdoutUntrimmed(output) = run_output!(test_helper(), "reverse", Stdin("foo")); assert_eq!(output, "oof"); } #[test] fn allows_passing_in_u8_slices_as_stdin() { let StdoutUntrimmed(output) = run_output!(test_helper(), "reverse", Stdin(&[0, 1, 2])); assert_eq!(output, "\x02\x01\x00"); } #[test] #[cfg(unix)] fn stdin_is_closed_by_default() { let StdoutTrimmed(output) = run_output!(test_helper(), "wait until stdin is closed"); assert_eq!(output, "stdin is closed"); } #[test] fn writing_too_many_bytes_into_a_non_reading_child_may_error() { let big_string = String::from_utf8(vec![b'a'; 2_usize.pow(16) + 1]).unwrap(); let result: Result<(), crate::Error> = run_result!("true", Stdin(big_string)); let message = result.unwrap_err().to_string(); assert!(if cfg!(unix) { message == "true:\n Broken pipe (os error 32)" } else { [ "true:\n The pipe is being closed. (os error 232)", "true:\n The pipe has been ended. (os error 109)", ] .contains(&message.as_str()) }); } #[test] fn multiple_stdin_arguments_are_all_passed_into_the_child_process() { let StdoutUntrimmed(output) = run_output!(test_helper(), "reverse", Stdin("foo"), Stdin("bar")); assert_eq!(output, "raboof"); } #[test] fn works_for_owned_strings() { let argument: String = "foo".to_string(); let StdoutUntrimmed(output) = run_output!(test_helper(), "reverse", Stdin(argument)); assert_eq!(output, "oof"); } } mod invocation_syntax { use super::*; #[test] fn trailing_comma_is_accepted_after_normal_argument() { run!("echo", "foo",); let StdoutUntrimmed(_) = run_output!("echo", "foo",); let _result: Result<(), Error> = run_result!("echo", "foo",); } #[test] fn trailing_comma_is_accepted_after_split_argument() { run!("echo", %"foo",); let StdoutUntrimmed(_) = run_output!("echo", %"foo",); let _result: Result<(), Error> = run_result!("echo", %"foo",); } } mod environment_variables { use super::*; use pretty_assertions::assert_eq; use std::env; #[test] fn allows_to_add_variables() { let StdoutTrimmed(output) = run_output!( test_helper(), %"echo FOO", Env("FOO", "bar") ); assert_eq!(output, "bar"); } #[test] fn works_for_multiple_variables() { let StdoutUntrimmed(output) = run_output!( test_helper(), %"echo FOO BAR", Env("FOO", "a"), Env("BAR", "b") ); assert_eq!(output, "a\nb\n"); } fn find_unused_environment_variable() -> String { let mut i = 0; loop { let key = format!("CRADLE_TEST_VARIABLE_{}", i); if env::var_os(&key).is_none() { break key; } i += 1; } } #[test] fn child_processes_inherit_the_environment() { let unused_key = find_unused_environment_variable(); env::set_var(&unused_key, "foo"); let StdoutTrimmed(output) = run_output!(test_helper(), "echo", unused_key); assert_eq!(output, "foo"); } #[test] fn overwrites_existing_parent_variables() { let unused_key = find_unused_environment_variable(); env::set_var(&unused_key, "foo"); let StdoutTrimmed(output) = run_output!(test_helper(), "echo", &unused_key, Env(&unused_key, "bar")); assert_eq!(output, "bar"); } #[test] fn variables_are_overwritten_by_subsequent_variables_with_the_same_name() { let StdoutTrimmed(output) = run_output!( test_helper(), "echo", "FOO", Env("FOO", "a"), Env("FOO", "b"), ); assert_eq!(output, "b"); } #[test] fn variables_can_be_set_to_the_empty_string() { let StdoutUntrimmed(output) = run_output!(test_helper(), "echo", "FOO", Env("FOO", ""),); assert_eq!(output, "empty variable: FOO\n"); } } mod run_interface { use super::*; use std::path::Path; #[test] fn allows_to_run_commands_with_dot_run() { let StdoutTrimmed(output) = Split("echo foo").run_output(); assert_eq!(output, "foo"); } #[test] fn allows_to_bundle_arguments_up_in_tuples() { let StdoutTrimmed(output) = ("echo", "foo").run_output(); assert_eq!(output, "foo"); } #[test] fn works_for_different_output_types() { let Status(status) = "false".run_output(); assert!(!status.success()); } #[test] fn run() { in_temporary_directory(|| { ("touch", "foo").run(); assert!(Path::new("foo").exists()); }); } #[test] fn run_result() { let StdoutTrimmed(output) = ("echo", "foo").run_result().unwrap(); assert_eq!(output, "foo"); let result: Result<(), Error> = "does-not-exist".run_result(); match result { Err(Error::FileNotFound { .. }) => {} _ => panic!("should match Error::FileNotFound"), } } } } cradle-0.2.2/src/macros.rs000064400000000000000000000064710072674642500135400ustar 00000000000000/// Executes a child process without capturing any output. /// /// ``` /// # let temp_dir = tempfile::TempDir::new().unwrap(); /// # std::env::set_current_dir(&temp_dir).unwrap(); /// use cradle::prelude::*; /// /// run!(%"touch ./foo"); /// ``` /// /// If an error occurs, `run!` will panic. /// See [`crate::error::Error`] for possible errors. /// /// For capturing output from child processes, see [`crate::run_output!`]. #[macro_export] macro_rules! run { ($($args:tt)*) => {{ $crate::input::Input::run($crate::tuple_up!($($args)*)) }} } /// Execute child processes, and capture some output. /// For example you can capture what the child process writes to stdout: /// /// ``` /// use cradle::prelude::*; /// /// let StdoutUntrimmed(output) = run_output!(%"echo foo"); /// assert_eq!(output, "foo\n"); /// ``` /// /// [`run_output!`] uses return-type polymorphism. /// So by using a different return type, /// you can control what outputs of child processes you want to capture. /// Here's an example to capture an exit code: /// /// ``` /// use cradle::prelude::*; /// /// let Status(status) = run_output!("false"); /// assert_eq!(status.code(), Some(1)); /// ``` /// /// You can use any type that implements [`crate::output::Output`] as the return type. /// See the module documentation for more comprehensive documentation. #[macro_export] macro_rules! run_output { ($($args:tt)*) => {{ $crate::input::Input::run_output($crate::tuple_up!($($args)*)) }} } /// Like [`run_output!`], but fixes the return type to [`Result`], /// where `T` is any type that implements [`Output`](crate::Output). #[macro_export] macro_rules! run_result { ($($args:tt)*) => {{ $crate::input::Input::run_result($crate::tuple_up!($($args)*)) }} } #[doc(hidden)] #[macro_export] macro_rules! tuple_up { (% $last:expr $(,)?) => { $crate::input::Split($last) }; ($last:expr $(,)?) => { $last }; (% $head:expr, $($tail:tt)*) => { ($crate::input::Split($head), $crate::tuple_up!($($tail)*)) }; ($head:expr, $($tail:tt)*) => { ($head, $crate::tuple_up!($($tail)*)) }; } #[cfg(test)] mod tests { use crate::prelude::*; mod tuple_up { use super::*; #[test] #[allow(clippy::eq_op)] fn one_value() { assert_eq!(tuple_up!(1), 1); } #[test] fn two_values() { assert_eq!(tuple_up!(1, 2), (1, 2)); } #[test] fn three_values() { assert_eq!(tuple_up!(1, 2, 3), (1, (2, 3))); } #[test] fn nested_tuples() { assert_eq!(tuple_up!(1, (2, 3), 4), (1, ((2, 3), 4))); } #[test] fn percent_shortcut() { assert_eq!(tuple_up!(%"foo"), Split("foo")); } #[test] fn percent_shortcut_with_subsequent_values() { assert_eq!(tuple_up!(%"foo", "bar"), (Split("foo"), "bar")); } #[test] fn percent_shortcut_with_preceeding_values() { assert_eq!(tuple_up!("foo", %"bar"), ("foo", Split("bar"))); } #[test] fn percent_shortcut_with_multiple_values() { assert_eq!( tuple_up!(%"foo", "bar", %"baz"), (Split("foo"), ("bar", Split("baz"))) ); } } } cradle-0.2.2/src/output.rs000064400000000000000000000252600072674642500136110ustar 00000000000000//! The [`Output`] trait that defines all possible outputs of a child process. use crate::{child_output::ChildOutput, config::Config, error::Error}; use std::process::ExitStatus; /// All possible return types of [`run!`], [`run_output!`] or /// [`run_result!`] must implement this trait. /// This return-type polymorphism makes cradle very flexible. /// For example, if you want to capture what a command writes /// to `stdout` you can do that using [`StdoutUntrimmed`]: /// /// ``` /// use cradle::prelude::*; /// /// let StdoutUntrimmed(output) = run_output!(%"echo foo"); /// assert_eq!(output, "foo\n"); /// ``` /// /// But if instead you want to capture the command's [`ExitStatus`], /// you can use [`Status`]: /// /// ``` /// use cradle::prelude::*; /// /// let Status(exit_status) = run_output!("false"); /// assert_eq!(exit_status.code(), Some(1)); /// ``` /// /// For documentation on what all the possible return types do, /// see the documentation for the individual impls of [`Output`]. /// Here's a non-exhaustive list of the more commonly used return types to get you started: /// /// - [`()`]: In case you don't want to capture anything. See also [`run`]. /// - To capture output streams: /// - [`StdoutTrimmed`]: To capture `stdout`, trimmed of whitespace. /// - [`StdoutUntrimmed`]: To capture `stdout` untrimmed. /// - [`Stderr`]: To capture `stderr`. /// - [`Status`]: To capture the command's [`ExitStatus`]. /// /// Also, [`Output`] is implemented for tuples. /// You can use this to combine multiple return types that implement [`Output`]. /// The following code for example retrieves the command's [`ExitStatus`] /// _and_ what it writes to `stdout`: /// /// ``` /// use cradle::prelude::*; /// /// let (Status(exit_status), StdoutUntrimmed(stdout)) = run_output!(%"echo foo"); /// assert!(exit_status.success()); /// assert_eq!(stdout, "foo\n"); /// ``` /// /// [`()`]: trait.Output.html#impl-Output-for-() /// /// ## Custom [`Output`] impls /// /// It is possible, but not recommended, to write [`Output`] implementations for your /// own types. /// The API is inconvenient, under-documented, and easy to misuse, /// i.e. it is easily possible to provoke [`Internal`](Error::Internal) errors. /// /// See /// [Issue 184: Provide a better API for writing custom Output impls](https://github.com/soenkehahn/cradle/issues/184) /// for more details and discussion. pub trait Output: Sized { /// Configures the given [`Config`](crate::config::Config) for the [`Output`] type. /// This is an internal function that should be ignored. /// /// See also [Custom `Output` impls](crate::Output#custom-output-impls). fn configure(config: &mut Config); /// Converts [`ChildOutput`](crate::child_output::ChildOutput)s /// from running a child process into values of the [`Output`] type. /// This is an internal function that should be ignored. /// /// See also [Custom `Output` impls](crate::Output#custom-output-impls). fn from_child_output(config: &Config, result: &ChildOutput) -> Result; } /// Use this when you don't need any result from the child process. /// /// ``` /// # let temp_dir = tempfile::TempDir::new().unwrap(); /// # std::env::set_current_dir(&temp_dir).unwrap(); /// use cradle::prelude::*; /// /// let () = run_output!(%"touch ./foo"); /// ``` /// /// Since [`run_output!`] (and [`run_result`]) use return type polymorphism, /// you have to make sure the compiler can figure out which return type you want to use. /// In this example that happens through the `let () =`. /// So you can't just omit that. /// /// See also [`run!`] for a more convenient way to use `()` as the return type. impl Output for () { #[doc(hidden)] fn configure(_config: &mut Config) {} #[doc(hidden)] fn from_child_output(_config: &Config, _child_output: &ChildOutput) -> Result { Ok(()) } } macro_rules! tuple_impl { ($($generics:ident,)+) => { impl<$($generics),+> Output for ($($generics,)+) where $($generics: Output,)+ { #[doc(hidden)] fn configure(config: &mut Config) { $(<$generics as Output>::configure(config);)+ } #[doc(hidden)] fn from_child_output(config: &Config, child_output: &ChildOutput) -> Result { Ok(( $(<$generics as Output>::from_child_output(config, child_output)?,)+ )) } } }; } tuple_impl!(A,); tuple_impl!(A, B,); tuple_impl!(A, B, C,); tuple_impl!(A, B, C, D,); tuple_impl!(A, B, C, D, E,); tuple_impl!(A, B, C, D, E, F,); tuple_impl!(A, B, C, D, E, F, G,); tuple_impl!(A, B, C, D, E, F, G, H,); tuple_impl!(A, B, C, D, E, F, G, H, I,); tuple_impl!(A, B, C, D, E, F, G, H, I, J,); tuple_impl!(A, B, C, D, E, F, G, H, I, J, K,); tuple_impl!(A, B, C, D, E, F, G, H, I, J, K, L,); tuple_impl!(A, B, C, D, E, F, G, H, I, J, K, L, M,); tuple_impl!(A, B, C, D, E, F, G, H, I, J, K, L, M, N,); tuple_impl!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O,); tuple_impl!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P,); /// Returns what the child process writes to `stdout`, interpreted as utf-8, /// collected into a string, trimmed of leading and trailing whitespace. /// This also suppresses output of the child's `stdout` /// to the parent's `stdout`. (Which would be the default when not using [`StdoutTrimmed`] /// as the return value.) /// /// It's recommended to pattern-match to get to the inner [`String`]. /// This will make sure that the return type can be inferred. /// Here's an example: /// /// ``` /// use std::path::Path; /// use cradle::prelude::*; /// /// # #[cfg(unix)] /// # { /// let StdoutTrimmed(output) = run_output!(%"which ls"); /// assert!(Path::new(&output).exists()); /// # } /// ``` #[derive(Debug, PartialEq, Clone)] pub struct StdoutTrimmed(pub String); impl Output for StdoutTrimmed { #[doc(hidden)] fn configure(config: &mut Config) { StdoutUntrimmed::configure(config); } #[doc(hidden)] fn from_child_output(config: &Config, child_output: &ChildOutput) -> Result { let StdoutUntrimmed(stdout) = StdoutUntrimmed::from_child_output(config, child_output)?; Ok(StdoutTrimmed(stdout.trim().to_owned())) } } /// Same as [`StdoutTrimmed`], but does not trim whitespace from the output: /// /// ``` /// use cradle::prelude::*; /// /// let StdoutUntrimmed(output) = run_output!(%"echo foo"); /// assert_eq!(output, "foo\n"); /// ``` #[derive(Debug, PartialEq, Clone)] pub struct StdoutUntrimmed(pub String); impl Output for StdoutUntrimmed { #[doc(hidden)] fn configure(config: &mut Config) { config.capture_stdout = true; } #[doc(hidden)] fn from_child_output(config: &Config, child_output: &ChildOutput) -> Result { let stdout = child_output .stdout .clone() .ok_or_else(|| Error::internal("stdout not captured", config))?; Ok(StdoutUntrimmed(String::from_utf8(stdout).map_err( |source| Error::InvalidUtf8ToStdout { full_command: config.full_command(), source, }, )?)) } } /// [`Stderr`] allows to capture the `stderr` of a child process: /// /// ``` /// use cradle::prelude::*; /// /// // (`Status` is used here to suppress panics caused by `ls` /// // terminating with a non-zero exit code.) /// let (Stderr(stderr), Status(_)) = run_output!(%"ls does-not-exist"); /// assert!(stderr.contains("No such file or directory")); /// ``` /// /// This assumes that the output written to `stderr` is encoded /// as utf-8, and will error otherwise. /// /// By default, what is written to `stderr` by the child process /// is relayed to the parent's `stderr`. However, when [`Stderr`] /// is used, this is switched off. #[derive(Debug, Clone)] pub struct Stderr(pub String); impl Output for Stderr { #[doc(hidden)] fn configure(config: &mut Config) { config.capture_stderr = true; } #[doc(hidden)] fn from_child_output(config: &Config, child_output: &ChildOutput) -> Result { let stderr = child_output .stderr .clone() .ok_or_else(|| Error::internal("stderr not captured", config))?; Ok(Stderr(String::from_utf8(stderr).map_err(|source| { Error::InvalidUtf8ToStderr { full_command: config.full_command(), source, } })?)) } } /// Use [`Status`] as the return type for [`run_output!`] to retrieve the /// [`ExitStatus`] of the child process: /// /// ``` /// use cradle::prelude::*; /// /// let Status(exit_status) = run_output!(%"echo foo"); /// assert!(exit_status.success()); /// ``` /// /// Also, when using [`Status`], non-zero exit codes won't /// result in neither a panic (when used with [`run!`] or /// [`run_output!`]) nor an [`std::result::Result::Err`] /// (when used with [`run_result!`]): /// /// ``` /// use cradle::prelude::*; /// /// let Status(exit_status) = run_output!("false"); /// assert_eq!(exit_status.code(), Some(1)); /// let result: Result = run_result!("false"); /// assert!(result.is_ok()); /// assert_eq!(result.unwrap().0.code(), Some(1)); /// ``` /// /// Also see the /// [section about error handling](index.html#error-handling) in /// the module documentation. #[derive(Debug, Clone)] pub struct Status(pub ExitStatus); impl Output for Status { #[doc(hidden)] fn configure(config: &mut Config) { config.error_on_non_zero_exit_code = false; } #[doc(hidden)] fn from_child_output(_config: &Config, child_output: &ChildOutput) -> Result { Ok(Status(child_output.exit_status)) } } /// Using [`bool`] as the return type for [`run_output!`] will return `true` if /// the command returned successfully, and `false` otherwise: /// /// ``` /// use cradle::prelude::*; /// /// if !run_output!(%"which cargo") { /// panic!("Cargo is not installed!"); /// } /// ``` /// /// Also, when using [`bool`], non-zero exit codes will not result in a panic /// or [`std::result::Result::Err`]: /// /// ``` /// use cradle::prelude::*; /// /// let success: bool = run_output!("false"); /// assert!(!success); /// let result: Result = run_result!("false"); /// assert!(result.is_ok()); /// assert_eq!(result.unwrap(), false); /// ``` /// /// Also see the /// [section about error handling](index.html#error-handling) in /// the module documentation. impl Output for bool { #[doc(hidden)] fn configure(config: &mut Config) { config.error_on_non_zero_exit_code = false; } #[doc(hidden)] fn from_child_output(_config: &Config, child_output: &ChildOutput) -> Result { Ok(child_output.exit_status.success()) } } cradle-0.2.2/src/prelude.rs000064400000000000000000000005520072674642500137060ustar 00000000000000//! `cradle`'s `prelude` module. //! It re-exports the most commonly used items from cradle. //! We recommend importing cradle like this: //! `use cradle::prelude::*;` //! //! For documentation about how to use cradle, //! see the documentation in the [crate root](crate). include!("common_re_exports.rs.snippet"); pub use crate::{run, run_output, run_result}; cradle-0.2.2/src/test_executables/helper.rs000064400000000000000000000046200072674642500170700ustar 00000000000000use std::{ io::{self, Read, Write}, path::PathBuf, thread::sleep, time::Duration, }; fn main() { let mut args = std::env::args(); args.next().unwrap(); match args.next().unwrap().as_str() { "invalid utf-8 stdout" => io::stdout().write_all(&[0x80]).unwrap(), "invalid utf-8 stderr" => io::stderr().write_all(&[0x80]).unwrap(), "exit code 42" => std::process::exit(42), "stream chunk then wait for file" => { println!("foo"); io::stdout().flush().unwrap(); let file = PathBuf::from("./file"); while !file.exists() { sleep(Duration::from_secs_f32(0.1)); } } "output foo and exit with 42" => { println!("foo"); std::process::exit(42) } "write to stderr" => { eprintln!("foo"); } "write to stderr and exit with 42" => { eprintln!("foo"); std::process::exit(42) } "stream chunk to stderr then wait for file" => { eprintln!("foo"); let file = PathBuf::from("./file"); while !file.exists() { sleep(Duration::from_secs_f32(0.1)); } } "reverse" => { let mut input = Vec::new(); io::stdin().read_to_end(&mut input).unwrap(); input.reverse(); io::stdout().write_all(&input).unwrap(); io::stdout().flush().unwrap(); } "wait until stdin is closed" => { while !stdin_is_closed() {} println!("stdin is closed"); } "echo" => { for variable in args { match std::env::var(&variable).unwrap().as_str() { "" => println!("empty variable: {}", variable), value => println!("{}", value), } } } arg => panic!("cradle_test_helper: invalid arg: {}", arg), } } fn stdin_is_closed() -> bool { #[cfg(unix)] { use nix::poll::{poll, PollFd, PollFlags}; let mut poll_fds = [PollFd::new(0, PollFlags::all())]; poll(&mut poll_fds, 0).unwrap(); if let Some(events) = poll_fds[0].revents() { events.contains(PollFlags::POLLHUP) } else { false } } #[cfg(windows)] panic!("stdin_is_closed is not supported on windows") } cradle-0.2.2/src/test_executables/panic.rs000064400000000000000000000000610072674642500166760ustar 00000000000000use cradle::*; fn main() { run!("false"); } cradle-0.2.2/tests/integration.rs000064400000000000000000000174120072674642500151470ustar 00000000000000#[cfg(unix)] const WHICH: &str = "which"; #[cfg(windows)] const WHICH: &str = "where"; #[test] fn runs_child_processes() { use cradle::prelude::*; use tempfile::TempDir; let temp_dir = TempDir::new().unwrap(); run!(CurrentDir(temp_dir.path()), %"touch foo"); assert!(temp_dir.path().join("foo").is_file()); } #[test] #[should_panic(expected = "false:\n exited with exit code: 1")] fn panics_on_non_zero_exit_codes() { use cradle::prelude::*; run!("false"); } #[test] fn capturing_stdout() { use cradle::prelude::*; let StdoutTrimmed(output) = run_output!(%"echo foo"); assert_eq!(output, "foo"); } #[test] fn result_succeeding() { use cradle::prelude::*; fn test() -> Result<(), Error> { // make sure 'ls' is installed run_result!(WHICH, "ls")?; Ok(()) } test().unwrap(); } #[test] fn result_failing() { use cradle::prelude::*; fn test() -> Result<(), Error> { run_result!(WHICH, "does-not-exist")?; Ok(()) } assert_eq!( test().unwrap_err().to_string(), if cfg!(unix) { "which does-not-exist:\n exited with exit code: 1" } else { "where does-not-exist:\n exited with exit code: 1" } ); } #[test] fn trimmed_stdout() { use cradle::prelude::*; use std::path::PathBuf; { let StdoutTrimmed(ls_path) = run_output!(WHICH, "ls"); assert!( PathBuf::from(&ls_path).exists(), "{:?} does not exist", &ls_path ); }; } #[test] fn trimmed_stdout_and_results() { use cradle::prelude::*; use std::path::PathBuf; fn test() -> Result<(), Error> { let StdoutTrimmed(ls_path) = run_result!(WHICH, "ls")?; assert!( PathBuf::from(&ls_path).exists(), "{:?} does not exist", &ls_path ); Ok(()) } test().unwrap(); } #[test] fn box_dyn_errors_succeeding() { use cradle::prelude::*; type MyResult = Result>; fn test() -> MyResult<()> { run_result!(WHICH, "ls")?; Ok(()) } test().unwrap(); } #[test] fn box_dyn_errors_failing() { use cradle::prelude::*; type MyResult = Result>; fn test() -> MyResult<()> { run_result!(WHICH, "does-not-exist")?; Ok(()) } assert_eq!( test().unwrap_err().to_string(), if cfg!(unix) { "which does-not-exist:\n exited with exit code: 1" } else { "where does-not-exist:\n exited with exit code: 1" } ); } #[test] fn user_supplied_errors_succeeding() { use cradle::prelude::*; #[derive(Debug)] enum Error { Cradle(cradle::Error), } impl From for Error { fn from(error: cradle::Error) -> Self { Error::Cradle(error) } } fn test() -> Result<(), Error> { run_result!(WHICH, "ls")?; Ok(()) } test().unwrap(); } #[test] fn user_supplied_errors_failing() { use cradle::prelude::*; use std::fmt::Display; #[derive(Debug)] enum Error { Cradle(cradle::Error), } impl From for Error { fn from(error: cradle::Error) -> Self { Error::Cradle(error) } } impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::Cradle(error) => write!(f, "cradle error: {}", error), } } } fn test() -> Result<(), Error> { run_result!(WHICH, "does-not-exist")?; Ok(()) } assert_eq!( test().unwrap_err().to_string(), if cfg!(unix) { "cradle error: which does-not-exist:\n exited with exit code: 1" } else { "cradle error: where does-not-exist:\n exited with exit code: 1" } ); } mod run_interface { use super::*; #[test] fn result_succeeding() { use cradle::prelude::*; fn test() -> Result<(), Error> { // make sure 'ls' is installed (WHICH, "ls").run_result()?; Ok(()) } test().unwrap(); } #[test] fn result_failing() { use cradle::prelude::*; fn test() -> Result<(), Error> { (WHICH, "does-not-exist").run_result()?; Ok(()) } assert_eq!( test().unwrap_err().to_string(), if cfg!(unix) { "which does-not-exist:\n exited with exit code: 1" } else { "where does-not-exist:\n exited with exit code: 1" } ); } #[test] fn box_dyn_errors_succeeding() { use cradle::prelude::*; type MyResult = Result>; fn test() -> MyResult<()> { (WHICH, "ls").run_result()?; Ok(()) } test().unwrap(); } #[test] fn box_dyn_errors_failing() { use cradle::prelude::*; type MyResult = Result>; fn test() -> MyResult<()> { (WHICH, "does-not-exist").run_result()?; Ok(()) } assert_eq!( test().unwrap_err().to_string(), if cfg!(unix) { "which does-not-exist:\n exited with exit code: 1" } else { "where does-not-exist:\n exited with exit code: 1" } ); } #[test] fn user_supplied_errors_succeeding() { use cradle::prelude::*; #[derive(Debug)] enum Error { Cradle(cradle::Error), } impl From for Error { fn from(error: cradle::Error) -> Self { Error::Cradle(error) } } fn test() -> Result<(), Error> { (WHICH, "ls").run_result()?; Ok(()) } test().unwrap(); } #[test] fn user_supplied_errors_failing() { use cradle::prelude::*; use std::fmt::Display; #[derive(Debug)] enum Error { Cradle(cradle::Error), } impl From for Error { fn from(error: cradle::Error) -> Self { Error::Cradle(error) } } impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::Cradle(error) => write!(f, "cradle error: {}", error), } } } fn test() -> Result<(), Error> { (WHICH, "does-not-exist").run_result()?; Ok(()) } assert_eq!( test().unwrap_err().to_string(), if cfg!(unix) { "cradle error: which does-not-exist:\n exited with exit code: 1" } else { "cradle error: where does-not-exist:\n exited with exit code: 1" } ); } } #[test] fn root_re_exports_everything_from_the_prelude() { // This does not import everything that we want to be re-exported from the root module, // but items that are hopefully good test proxies for everything. // run macros #[allow(unused_imports)] use cradle::{run, run_output, run_result}; // items from input #[allow(unused_imports)] use cradle::{CurrentDir, Input}; // items from output #[allow(unused_imports)] use cradle::{Output, Status}; // the cradle error type #[allow(unused_imports)] use cradle::Error; } #[cfg(target_os = "linux")] #[test] fn memory_test() { use cradle::prelude::*; run!(%"cargo build -p memory-tests --release"); run!(%"cargo run -p memory-tests --bin run"); }