diff --git a/README.md b/README.md index 8e11de1..aec78e6 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ fn main() -> anyhow::Result<()> { let user = "matklad"; let repo = "xshell"; cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?; - sh.change_dir(repo); + sh.set_current_dir(repo); let test_args = ["-Zunstable-options", "--report-time"]; cmd!(sh, "cargo test -- {test_args...}").run()?; @@ -29,7 +29,7 @@ fn main() -> anyhow::Result<()> { cmd!(sh, "git tag {version}").run()?; - let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") }; + let dry_run = if sh.env_var("CI").is_ok() { None } else { Some("--dry-run") }; cmd!(sh, "cargo publish {dry_run...}").run()?; Ok(()) diff --git a/examples/ci.rs b/examples/ci.rs index 8166447..41c9f6f 100644 --- a/examples/ci.rs +++ b/examples/ci.rs @@ -57,7 +57,7 @@ fn publish(sh: &Shell) -> Result<()> { if current_branch == "master" && !tag_exists { // Could also just use `CARGO_REGISTRY_TOKEN` environmental variable. - let token = sh.var("CRATES_IO_TOKEN").unwrap_or("DUMMY_TOKEN".to_string()); + let token = sh.env_var("CRATES_IO_TOKEN").unwrap_or("DUMMY_TOKEN".to_string()); cmd!(sh, "git tag v{version}").run()?; cmd!(sh, "cargo publish --token {token} --package xshell-macros").run()?; cmd!(sh, "cargo publish --token {token} --package xshell").run()?; diff --git a/examples/clone_and_publish.rs b/examples/clone_and_publish.rs index 360529c..7e971db 100644 --- a/examples/clone_and_publish.rs +++ b/examples/clone_and_publish.rs @@ -7,7 +7,7 @@ fn main() -> anyhow::Result<()> { let user = "matklad"; let repo = "xshell"; cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?; - sh.change_dir(repo); + sh.set_current_dir(repo); let test_args = ["-Zunstable-options", "--report-time"]; cmd!(sh, "cargo test -- {test_args...}").run()?; @@ -21,7 +21,7 @@ fn main() -> anyhow::Result<()> { cmd!(sh, "git tag {version}").run()?; - let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") }; + let dry_run = if sh.env_var("CI").is_ok() { None } else { Some("--dry-run") }; cmd!(sh, "cargo publish {dry_run...}").run()?; Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 2f8706e..0975da8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,7 +118,7 @@ //! ```no_run //! # use xshell::{Shell, cmd}; let mut sh = Shell::new().unwrap(); //! # let repo = "xshell"; -//! sh.change_dir(repo); +//! sh.set_current_dir(repo); //! ``` //! //! Each instance of [`Shell`] has a current directory, which is independent of @@ -180,7 +180,7 @@ //! //! ```no_run //! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); -//! let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") }; +//! let dry_run = if sh.env_var("CI").is_ok() { None } else { Some("--dry-run") }; //! cmd!(sh, "cargo publish {dry_run...}").run()?; //! # Ok::<(), xshell::Error>(()) //! ``` @@ -196,7 +196,7 @@ //! let user = "matklad"; //! let repo = "xshell"; //! cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?; -//! sh.change_dir(repo); +//! sh.set_current_dir(repo); //! //! let test_args = ["-Zunstable-options", "--report-time"]; //! cmd!(sh, "cargo test -- {test_args...}").run()?; @@ -210,7 +210,7 @@ //! //! cmd!(sh, "git tag {version}").run()?; //! -//! let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") }; +//! let dry_run = if sh.env_var("CI").is_ok() { None } else { Some("--dry-run") }; //! cmd!(sh, "cargo publish {dry_run...}").run()?; //! //! Ok(()) @@ -374,7 +374,7 @@ macro_rules! cmd { /// use xshell::{cmd, Shell}; /// /// let sh = Shell::new()?; -/// let sh = sh.push_dir("./target"); +/// let sh = sh.with_current_dir("./target"); /// let cwd = sh.current_dir(); /// cmd!(sh, "echo current dir is {cwd}").run()?; /// @@ -385,7 +385,7 @@ macro_rules! cmd { #[derive(Debug, Clone)] pub struct Shell { cwd: Arc, - env: HashMap, Arc>, + env: Arc, Arc>>, } impl std::panic::UnwindSafe for Shell {} @@ -397,7 +397,7 @@ impl Shell { /// Fails if [`std::env::current_dir`] returns an error. pub fn new() -> Result { let cwd = current_dir().map_err(|err| Error::new_current_dir(err, None))?; - Ok(Shell { cwd: cwd.into(), env: HashMap::new() }) + Ok(Shell { cwd: cwd.into(), env: Default::default() }) } // region:env @@ -413,11 +413,11 @@ impl Shell { /// Changes the working directory for this [`Shell`]. /// /// Note that this doesn't affect [`std::env::current_dir`]. - #[doc(alias = "pwd")] - pub fn change_dir(&mut self, dir: impl AsRef) { - self._change_dir(dir.as_ref().as_ref()) + #[doc(alias = "cd")] + pub fn set_current_dir(&mut self, dir: impl AsRef) { + self._set_current_dir(dir.as_ref().as_ref()) } - fn _change_dir(&mut self, dir: &OsStr) { + fn _set_current_dir(&mut self, dir: &OsStr) { self.cwd = self.cwd.join(dir).into(); } @@ -426,10 +426,10 @@ impl Shell { /// Note that this doesn't affect [`std::env::current_dir`]. #[doc(alias = "pushd")] #[must_use] - pub fn push_dir(&self, path: impl AsRef) -> Self { - self._push_dir(path.as_ref()) + pub fn with_current_dir(&self, path: impl AsRef) -> Self { + self._with_current_dir(path.as_ref()) } - fn _push_dir(&self, path: &Path) -> Self { + fn _with_current_dir(&self, path: &Path) -> Self { Self { cwd: self.cwd.join(path).into(), env: self.env.clone() } } @@ -439,11 +439,11 @@ impl Shell { /// /// Environment of the [`Shell`] affects all commands spawned via this /// shell. - pub fn var(&self, key: impl AsRef) -> Result { - self._var(key.as_ref()) + pub fn env_var(&self, key: impl AsRef) -> Result { + self._env_var(key.as_ref()) } - fn _var(&self, key: &OsStr) -> Result { - match self._var_os(key) { + fn _env_var(&self, key: &OsStr) -> Result { + match self._env_var_os(key) { Some(it) => match it.to_str() { Some(it) => Ok(it.to_string()), None => Err(VarError::NotUnicode(key.into())), @@ -458,22 +458,45 @@ impl Shell { /// /// Environment of the [`Shell`] affects all commands spawned via this /// shell. - pub fn var_os(&self, key: impl AsRef) -> Option> { - self._var_os(key.as_ref()) + pub fn env_var_os(&self, key: impl AsRef) -> Option> { + self._env_var_os(key.as_ref()) } - fn _var_os(&self, key: &OsStr) -> Option> { + fn _env_var_os(&self, key: &OsStr) -> Option> { self.env.get(key).cloned().or_else(|| env::var_os(key).map(Into::into)) } + /// Fetches the whole environment as a `(Key, Value)` iterator for this [`Shell`]. + /// + /// Returns an error if any of the variables are not utf8. + /// + /// Environment of the [`Shell`] affects all commands spawned via this + /// shell. + pub fn env_vars(&self) -> Result> { + if let Some((key, _)) = + self.env_vars_os().find(|(a, b)| a.to_str().or(b.to_str()).is_none()) + { + return Err(Error::new_var(VarError::NotUnicode(key.into()), key.into())); + } + Ok(self.env_vars_os().map(|(k, v)| (k.to_str().unwrap(), v.to_str().unwrap()))) + } + + /// Fetches the whole environment as a `(Key, Value)` iterator for this [`Shell`]. + /// + /// Environment of the [`Shell`] affects all commands spawned via this + /// shell. + pub fn env_vars_os(&self) -> impl Iterator { + self.env.iter().map(|(k, v)| (k.as_ref(), v.as_ref())) + } + /// Sets the value of `key` environment variable for this [`Shell`] to /// `val`. /// /// Note that this doesn't affect [`std::env::var`]. - pub fn set_var(&mut self, key: impl AsRef, val: impl AsRef) { - self._set_var(key.as_ref(), val.as_ref()) + pub fn set_env_var(&mut self, key: impl AsRef, val: impl AsRef) { + self._set_env_var(key.as_ref(), val.as_ref()) } - fn _set_var(&mut self, key: &OsStr, val: &OsStr) { - self.env.insert(key.into(), val.into()); + fn _set_env_var(&mut self, key: &OsStr, val: &OsStr) { + Arc::make_mut(&mut self.env).insert(key.into(), val.into()); } // endregion:env @@ -670,10 +693,9 @@ impl Shell { #[derive(Debug, Clone)] #[must_use] pub struct Cmd { - cwd: Arc, + sh: Shell, prog: PathBuf, args: Vec, - envs: HashMap, Arc>, ignore_status: bool, quiet: bool, secret: bool, @@ -709,11 +731,10 @@ impl From for Command { } impl Cmd { - fn new(shell: &Shell, prog: impl AsRef) -> Self { + fn new(sh: &Shell, prog: impl AsRef) -> Self { Cmd { - cwd: shell.cwd.clone(), + sh: sh.clone(), prog: prog.as_ref().into(), - envs: shell.env.clone(), args: Vec::new(), ignore_status: false, quiet: false, @@ -767,7 +788,7 @@ impl Cmd { } fn _env_set(&mut self, key: &OsStr, val: &OsStr) { - self.envs.insert(key.into(), val.into()); + Arc::make_mut(&mut self.sh.env).insert(key.into(), val.into()); } /// Overrides the values of specified environmental variables for this @@ -778,7 +799,8 @@ impl Cmd { K: AsRef, V: AsRef, { - self.envs.extend(vars.into_iter().map(|(k, v)| (k.as_ref().into(), v.as_ref().into()))); + Arc::make_mut(&mut self.sh.env) + .extend(vars.into_iter().map(|(k, v)| (k.as_ref().into(), v.as_ref().into()))); self } @@ -788,12 +810,12 @@ impl Cmd { self } fn _env_remove(&mut self, key: &OsStr) { - self.envs.remove(key); + Arc::make_mut(&mut self.sh.env).remove(key); } /// Removes all of the environment variables from this command. pub fn env_clear(mut self) -> Self { - self.envs.clear(); + Arc::make_mut(&mut self.sh.env).clear(); self } @@ -938,8 +960,8 @@ impl Cmd { // directory does not exist. Return an appropriate error in such a // case. if matches!(err.kind(), io::ErrorKind::NotFound) { - if let Err(err) = self.cwd.metadata() { - return Error::new_current_dir(err, Some(self.cwd.clone())); + if let Err(err) = self.sh.cwd.metadata() { + return Error::new_current_dir(err, Some(self.sh.cwd.clone())); } } Error::new_cmd_io(self, err) @@ -966,10 +988,10 @@ impl Cmd { fn to_command(&self) -> Command { let mut res = Command::new(&self.prog); - res.current_dir(&self.cwd); + res.current_dir(&self.sh.cwd); res.args(&self.args); - for (key, val) in &self.envs { + for (key, val) in &*self.sh.env { res.env(key, val); } diff --git a/tests/compile_time.rs b/tests/compile_time.rs index 17ec1b9..a90eefc 100644 --- a/tests/compile_time.rs +++ b/tests/compile_time.rs @@ -6,7 +6,7 @@ use xshell::{cmd, Shell}; fn fixed_cost_compile_times() { let mut sh = Shell::new().unwrap(); - let _p = sh.change_dir("tests/data"); + let _p = sh.set_current_dir("tests/data"); let baseline = compile_bench(&mut sh, "baseline"); let _ducted = compile_bench(&sh, "ducted"); let xshelled = compile_bench(&mut sh, "xshelled"); @@ -14,7 +14,7 @@ fn fixed_cost_compile_times() { assert!(1.0 < ratio && ratio < 10.0); fn compile_bench(sh: &Shell, name: &str) -> Duration { - let sh = sh.push_dir(name); + let sh = sh.with_current_dir(name); let cargo_build = cmd!(sh, "cargo build -q"); cargo_build.read().unwrap(); diff --git a/tests/it/compile_failures.rs b/tests/it/compile_failures.rs index 1199a35..fd9c8f7 100644 --- a/tests/it/compile_failures.rs +++ b/tests/it/compile_failures.rs @@ -5,7 +5,7 @@ fn check(code: &str, err_msg: &str) { let mut sh = Shell::new().unwrap(); let xshell_dir = sh.current_dir().to_owned(); let temp_dir = sh.create_temp_dir().unwrap(); - sh.change_dir(temp_dir.path()); + sh.set_current_dir(temp_dir.path()); let manifest = format!( r#" diff --git a/tests/it/env.rs b/tests/it/env.rs index 2c73333..9d5d2bb 100644 --- a/tests/it/env.rs +++ b/tests/it/env.rs @@ -34,8 +34,8 @@ fn test_env() { ); } - let _g1 = sh.set_var(v1, "foobar"); - let _g2 = sh.set_var(v2, "quark"); + let _g1 = sh.set_env_var(v1, "foobar"); + let _g2 = sh.set_env_var(v2, "quark"); assert_env(cmd!(sh, "xecho -$ {v1} {v2}"), &[(v1, Some("foobar")), (v2, Some("quark"))]); assert_env(cmd!(cloned_sh, "xecho -$ {v1} {v2}"), &[(v1, None), (v2, None)]); @@ -79,8 +79,8 @@ fn test_env_clear() { &[(v1, Some("789")), (v2, None)], ); - let _g1 = sh.set_var(v1, "foobar"); - let _g2 = sh.set_var(v2, "quark"); + let _g1 = sh.set_env_var(v1, "foobar"); + let _g2 = sh.set_env_var(v2, "quark"); assert_env(cmd!(sh, "{xecho} -$ {v1} {v2}").env_clear(), &[(v1, None), (v2, None)]); assert_env( diff --git a/tests/it/main.rs b/tests/it/main.rs index 5b9f3d5..1c108f1 100644 --- a/tests/it/main.rs +++ b/tests/it/main.rs @@ -20,7 +20,7 @@ fn setup() -> Shell { .unwrap_or_else(|err| panic!("failed to install binaries from mock_bin: {}", err)) }); - sh.set_var("PATH", target_dir); + sh.set_env_var("PATH", target_dir); sh } @@ -245,11 +245,11 @@ fn test_push_dir() { let d1 = sh.current_dir(); { - let sh = sh.push_dir("xshell-macros"); + let sh = sh.with_current_dir("xshell-macros"); let d2 = sh.current_dir(); assert_eq!(d2, d1.join("xshell-macros")); { - let sh = sh.push_dir("src"); + let sh = sh.with_current_dir("src"); let d3 = sh.current_dir(); assert_eq!(d3, d1.join("xshell-macros/src")); } @@ -266,10 +266,10 @@ fn test_push_and_change_dir() { let d1 = sh.current_dir(); { - let mut sh = sh.push_dir("xshell-macros"); + let mut sh = sh.with_current_dir("xshell-macros"); let d2 = sh.current_dir(); assert_eq!(d2, d1.join("xshell-macros")); - sh.change_dir("src"); + sh.set_current_dir("src"); let d3 = sh.current_dir(); assert_eq!(d3, d1.join("xshell-macros/src")); } @@ -283,8 +283,8 @@ fn push_dir_parent_dir() { let current = sh.current_dir(); let dirname = current.file_name().unwrap(); - let sh = sh.push_dir(".."); - let sh = sh.push_dir(dirname); + let sh = sh.with_current_dir(".."); + let sh = sh.with_current_dir(dirname); assert_eq!(sh.current_dir().canonicalize().unwrap(), current.canonicalize().unwrap()); } @@ -294,40 +294,40 @@ const VAR: &str = "SPICA"; fn test_subshells_env() { let sh = setup(); - let e1 = sh.var_os(VAR); + let e1 = sh.env_var_os(VAR); { let mut sh = sh.clone(); - sh.set_var(VAR, "1"); - let e2 = sh.var_os(VAR); + sh.set_env_var(VAR, "1"); + let e2 = sh.env_var_os(VAR); assert_eq!(e2.as_deref(), Some("1".as_ref())); { let mut sh = sh.clone(); - let _e = sh.set_var(VAR, "2"); - let e3 = sh.var_os(VAR); + let _e = sh.set_env_var(VAR, "2"); + let e3 = sh.env_var_os(VAR); assert_eq!(e3.as_deref(), Some("2".as_ref())); } - let e4 = sh.var_os(VAR); + let e4 = sh.env_var_os(VAR); assert_eq!(e4, e2); } - let e5 = sh.var_os(VAR); + let e5 = sh.env_var_os(VAR); assert_eq!(e5, e1); } #[test] -fn test_push_env_and_set_var() { +fn test_push_env_and_set_env_var() { let sh = setup(); - let e1 = sh.var_os(VAR); + let e1 = sh.env_var_os(VAR); { let mut sh = sh.clone(); - sh.set_var(VAR, "1"); - let e2 = sh.var_os(VAR); + sh.set_env_var(VAR, "1"); + let e2 = sh.env_var_os(VAR); assert_eq!(e2.as_deref(), Some("1".as_ref())); - sh.set_var(VAR, "2"); - let e3 = sh.var_os(VAR); + sh.set_env_var(VAR, "2"); + let e3 = sh.env_var_os(VAR); assert_eq!(e3.as_deref(), Some("2".as_ref())); } - let e5 = sh.var_os(VAR); + let e5 = sh.env_var_os(VAR); assert_eq!(e5, e1); } @@ -394,14 +394,14 @@ fn test_copy_file() { fn test_exists() { let mut sh = setup(); let tmp = sh.create_temp_dir().unwrap(); - let _d = sh.change_dir(tmp.path()); + let _d = sh.set_current_dir(tmp.path()); assert!(!sh.path_exists("foo.txt")); sh.write_file("foo.txt", "foo").unwrap(); assert!(sh.path_exists("foo.txt")); assert!(!sh.path_exists("bar")); sh.create_dir("bar").unwrap(); assert!(sh.path_exists("bar")); - let _d = sh.change_dir("bar"); + let _d = sh.set_current_dir("bar"); assert!(!sh.path_exists("quz.rs")); sh.write_file("quz.rs", "fn main () {}").unwrap(); assert!(sh.path_exists("quz.rs")); @@ -424,7 +424,7 @@ fn test_remove_path() { let mut sh = setup(); let tempdir = sh.create_temp_dir().unwrap(); - sh.change_dir(tempdir.path()); + sh.set_current_dir(tempdir.path()); sh.write_file(Path::new("a/b/c.rs"), "fn main() {}").unwrap(); assert!(tempdir.path().join("a/b/c.rs").exists()); sh.remove_path("./a").unwrap(); @@ -442,7 +442,7 @@ fn recovers_from_panics() { let orig = sh.current_dir(); std::panic::catch_unwind(|| { - let sh = sh.push_dir(&tempdir); + let sh = sh.with_current_dir(&tempdir); assert_eq!(sh.current_dir(), tempdir); std::panic::resume_unwind(Box::new(())); }) @@ -450,7 +450,7 @@ fn recovers_from_panics() { assert_eq!(sh.current_dir(), orig); { - let sh = sh.push_dir(&tempdir); + let sh = sh.with_current_dir(&tempdir); assert_eq!(sh.current_dir(), tempdir); } } @@ -467,7 +467,7 @@ fn string_escapes() { #[test] fn nonexistent_current_directory() { let mut sh = setup(); - sh.change_dir("nonexistent"); + sh.set_current_dir("nonexistent"); let err = cmd!(sh, "ls").run().unwrap_err(); let message = err.to_string(); if cfg!(unix) { diff --git a/tests/it/tidy.rs b/tests/it/tidy.rs index b73d464..6bcacf5 100644 --- a/tests/it/tidy.rs +++ b/tests/it/tidy.rs @@ -28,7 +28,7 @@ fn formatting() { #[test] fn current_version_in_changelog() { let mut sh = Shell::new().unwrap(); - sh.change_dir(env!("CARGO_MANIFEST_DIR")); + sh.set_current_dir(env!("CARGO_MANIFEST_DIR")); let changelog = sh.read_file("CHANGELOG.md").unwrap(); let current_version_header = format!("## {}", env!("CARGO_PKG_VERSION")); assert_eq!(changelog.lines().filter(|&line| line == current_version_header).count(), 1);