From 6fb24008599cb5388f601a5afc7496d70d9e8d8c Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Thu, 24 Oct 2024 23:42:28 -0700 Subject: [PATCH 01/29] Add which function for finding executables in PATH Closes #2109 (but with a function name that is shorter and more familiar) --- Cargo.toml | 1 + README.md | 16 ++++++++++++++++ src/function.rs | 11 +++++++++++ tests/lib.rs | 1 + tests/which_exec.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+) create mode 100644 tests/which_exec.rs diff --git a/Cargo.toml b/Cargo.toml index f74367d02f..8a516d9935 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ tempfile = "3.0.0" typed-arena = "2.0.1" unicode-width = "0.2.0" uuid = { version = "1.0.0", features = ["v4"] } +which = "6.0.0" [dev-dependencies] executable-path = "1.0.0" diff --git a/README.md b/README.md index 535b2bc402..82b94272ce 100644 --- a/README.md +++ b/README.md @@ -1597,6 +1597,22 @@ $ just name `key`, returning `default` if it is not present. - `env(key)`1.15.0 — Alias for `env_var(key)`. - `env(key, default)`1.15.0 — Alias for `env_var_or_default(key, default)`. +- `which(exe)`1.37.0 — Retrieves the full path of `exe` according + to the `PATH`. Returns an empty string if no executable named `exe` exists. + +```just +bash := which("bash") +nexist := which("does-not-exist") + +@test: + echo "bash: '{{bash}}'" + echo "nexist: '{{nexist}}'" +``` + +```console +bash: '/bin/bash' +nexist: '' +``` #### Invocation Information diff --git a/src/function.rs b/src/function.rs index a714a8d0fd..ff9c93b6a6 100644 --- a/src/function.rs +++ b/src/function.rs @@ -110,6 +110,7 @@ pub(crate) fn get(name: &str) -> Option { "uppercase" => Unary(uppercase), "uuid" => Nullary(uuid), "without_extension" => Unary(without_extension), + "which" => Unary(which_exec), _ => return None, }; Some(function) @@ -667,6 +668,16 @@ fn uuid(_context: Context) -> FunctionResult { Ok(uuid::Uuid::new_v4().to_string()) } +fn which_exec(_context: Context, s: &str) -> FunctionResult { + let path = which::which(s).unwrap_or_default(); + path.to_str().map(str::to_string).ok_or_else(|| { + format!( + "unable to convert which executable path to string: {}", + path.display() + ) + }) +} + fn without_extension(_context: Context, path: &str) -> FunctionResult { let parent = Utf8Path::new(path) .parent() diff --git a/tests/lib.rs b/tests/lib.rs index ec71c665da..0552851f69 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -110,6 +110,7 @@ mod timestamps; mod undefined_variables; mod unexport; mod unstable; +mod which_exec; #[cfg(windows)] mod windows; #[cfg(target_family = "windows")] diff --git a/tests/which_exec.rs b/tests/which_exec.rs new file mode 100644 index 0000000000..1b685e96a8 --- /dev/null +++ b/tests/which_exec.rs @@ -0,0 +1,42 @@ +use super::*; + +fn make_path() -> TempDir { + let tmp = temptree! { + "hello.exe": "#!/usr/bin/env bash\necho hello\n", + }; + + #[cfg(not(windows))] + { + let exe = tmp.path().join("hello.exe"); + let perms = std::os::unix::fs::PermissionsExt::from_mode(0o755); + fs::set_permissions(exe, perms).unwrap(); + } + + tmp +} + +#[test] +fn finds_executable() { + let tmp = make_path(); + let mut path = env::current_dir().unwrap(); + path.push("bin"); + Test::new() + .justfile(r#"p := which("hello.exe")"#) + .env("PATH", tmp.path().to_str().unwrap()) + .args(["--evaluate", "p"]) + .stdout(format!("{}", tmp.path().join("hello.exe").display())) + .run(); +} + +#[test] +fn prints_empty_string_for_missing_executable() { + let tmp = make_path(); + let mut path = env::current_dir().unwrap(); + path.push("bin"); + Test::new() + .justfile(r#"p := which("goodbye.exe")"#) + .env("PATH", tmp.path().to_str().unwrap()) + .args(["--evaluate", "p"]) + .stdout("") + .run(); +} From f1980b5150f7720aac4139daa1cbeaa86cb0b616 Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Sun, 3 Nov 2024 18:53:11 -0800 Subject: [PATCH 02/29] Change version of which function to master branch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82b94272ce..5b165b02e8 100644 --- a/README.md +++ b/README.md @@ -1597,7 +1597,7 @@ $ just name `key`, returning `default` if it is not present. - `env(key)`1.15.0 — Alias for `env_var(key)`. - `env(key, default)`1.15.0 — Alias for `env_var_or_default(key, default)`. -- `which(exe)`1.37.0 — Retrieves the full path of `exe` according +- `which(exe)`master — Retrieves the full path of `exe` according to the `PATH`. Returns an empty string if no executable named `exe` exists. ```just From 0c6f5e8376eb3c3ed2024637d2dd05fff8298638 Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Sun, 3 Nov 2024 23:28:42 -0800 Subject: [PATCH 03/29] Use internal implementation of which() --- Cargo.lock | 11 +++++++++ Cargo.toml | 3 ++- justfile | 2 ++ src/function.rs | 64 ++++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80a7276d9d..1ba1463bd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,6 +498,15 @@ dependencies = [ "cc", ] +[[package]] +name = "is_executable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2" +dependencies = [ + "winapi", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -535,8 +544,10 @@ dependencies = [ "dirs", "dotenvy", "edit-distance", + "either", "executable-path", "heck", + "is_executable", "lexiclean", "libc", "num_cpus", diff --git a/Cargo.toml b/Cargo.toml index 8a516d9935..bda81c5ec1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,9 @@ derivative = "2.0.0" dirs = "5.0.1" dotenvy = "0.15" edit-distance = "2.0.0" +either = "1.13.0" heck = "0.5.0" +is_executable = "1.0.4" lexiclean = "0.0.1" libc = "0.2.0" num_cpus = "1.15.0" @@ -51,7 +53,6 @@ tempfile = "3.0.0" typed-arena = "2.0.1" unicode-width = "0.2.0" uuid = { version = "1.0.0", features = ["v4"] } -which = "6.0.0" [dev-dependencies] executable-path = "1.0.0" diff --git a/justfile b/justfile index d0ff455c67..cf65778280 100755 --- a/justfile +++ b/justfile @@ -6,6 +6,8 @@ alias t := test log := "warn" +bingus := which("bash") + export JUST_LOG := log [group: 'dev'] diff --git a/src/function.rs b/src/function.rs index ff9c93b6a6..25c328b9c5 100644 --- a/src/function.rs +++ b/src/function.rs @@ -1,5 +1,6 @@ use { super::*, + either::Either, heck::{ ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase, @@ -110,7 +111,7 @@ pub(crate) fn get(name: &str) -> Option { "uppercase" => Unary(uppercase), "uuid" => Nullary(uuid), "without_extension" => Unary(without_extension), - "which" => Unary(which_exec), + "which" => Unary(which), _ => return None, }; Some(function) @@ -668,14 +669,59 @@ fn uuid(_context: Context) -> FunctionResult { Ok(uuid::Uuid::new_v4().to_string()) } -fn which_exec(_context: Context, s: &str) -> FunctionResult { - let path = which::which(s).unwrap_or_default(); - path.to_str().map(str::to_string).ok_or_else(|| { - format!( - "unable to convert which executable path to string: {}", - path.display() - ) - }) +fn which(context: Context, s: &str) -> FunctionResult { + use is_executable::IsExecutable; + + let cmd = PathBuf::from(s); + + let path_var; + let candidates = match cmd.components().count() { + 0 => Err("empty command string".to_string())?, + 1 => { + // cmd is a regular command + path_var = env::var_os("PATH").ok_or("Environment variable `PATH` is not set")?; + Either::Left(env::split_paths(&path_var).map(|path| path.join(cmd.clone()))) + } + _ => { + // cmd contains a path separator, treat it as a path + Either::Right(iter::once(cmd)) + } + }; + + for mut candidate in candidates.into_iter() { + if candidate.is_relative() { + // This candidate is a relative path, either because the user invoked `which("./rel/path")`, + // or because there was a relative path in `PATH`. Resolve it to an absolute path. + let cwd = context + .evaluator + .context + .search + .justfile + .parent() + .ok_or_else(|| { + format!( + "Could not resolve absolute path from `{}` relative to the justfile directory. Justfile `{}` had no parent.", + candidate.display(), + context.evaluator.context.search.justfile.display() + ) + })?; + let mut cwd = PathBuf::from(cwd); + cwd.push(candidate); + candidate = cwd; + } + + if candidate.is_executable() { + return candidate.to_str().map(str::to_string).ok_or_else(|| { + format!( + "Executable path is not valid unicode: {}", + candidate.display() + ) + }); + } + } + + // No viable candidates; return an empty string + Ok(String::new()) } fn without_extension(_context: Context, path: &str) -> FunctionResult { From 389b2ae4af5910ff2fceefedab8ee668db4115f1 Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Sun, 1 Dec 2024 17:47:48 -0500 Subject: [PATCH 04/29] Add tests for internal implementation of which() --- tests/which_exec.rs | 162 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 145 insertions(+), 17 deletions(-) diff --git a/tests/which_exec.rs b/tests/which_exec.rs index 1b685e96a8..edc09d723d 100644 --- a/tests/which_exec.rs +++ b/tests/which_exec.rs @@ -1,25 +1,46 @@ use super::*; -fn make_path() -> TempDir { - let tmp = temptree! { - "hello.exe": "#!/usr/bin/env bash\necho hello\n", - }; +trait TempDirExt { + fn executable(self, file: impl AsRef) -> Self; +} - #[cfg(not(windows))] - { - let exe = tmp.path().join("hello.exe"); - let perms = std::os::unix::fs::PermissionsExt::from_mode(0o755); - fs::set_permissions(exe, perms).unwrap(); - } +impl TempDirExt for TempDir { + fn executable(self, file: impl AsRef) -> Self { + let file = self.path().join(file.as_ref()); + + // Make sure it exists first, as a sanity check. + assert!( + file.exists(), + "executable file does not exist: {}", + file.display() + ); + + // Windows uses file extensions to determine whether a file is executable. + // Other systems don't care. To keep these tests cross-platform, just make + // sure all executables end with ".exe" suffix. + assert!( + file.extension() == Some("exe".as_ref()), + "executable file does not end with .exe: {}", + file.display() + ); + + #[cfg(not(windows))] + { + let perms = std::os::unix::fs::PermissionsExt::from_mode(0o755); + fs::set_permissions(file, perms).unwrap(); + } - tmp + self + } } #[test] fn finds_executable() { - let tmp = make_path(); - let mut path = env::current_dir().unwrap(); - path.push("bin"); + let tmp = temptree! { + "hello.exe": "#!/usr/bin/env bash\necho hello\n", + } + .executable("hello.exe"); + Test::new() .justfile(r#"p := which("hello.exe")"#) .env("PATH", tmp.path().to_str().unwrap()) @@ -30,9 +51,11 @@ fn finds_executable() { #[test] fn prints_empty_string_for_missing_executable() { - let tmp = make_path(); - let mut path = env::current_dir().unwrap(); - path.push("bin"); + let tmp = temptree! { + "hello.exe": "#!/usr/bin/env bash\necho hello\n", + } + .executable("hello.exe"); + Test::new() .justfile(r#"p := which("goodbye.exe")"#) .env("PATH", tmp.path().to_str().unwrap()) @@ -40,3 +63,108 @@ fn prints_empty_string_for_missing_executable() { .stdout("") .run(); } + +#[test] +fn skips_non_executable_files() { + let tmp = temptree! { + "hello.exe": "#!/usr/bin/env bash\necho hello\n", + "hi": "just some regular file", + } + .executable("hello.exe"); + + Test::new() + .justfile(r#"p := which("hi")"#) + .env("PATH", tmp.path().to_str().unwrap()) + .args(["--evaluate", "p"]) + .stdout("") + .run(); +} + +#[test] +fn supports_multiple_paths() { + let tmp1 = temptree! { + "hello1.exe": "#!/usr/bin/env bash\necho hello\n", + } + .executable("hello1.exe"); + + let tmp2 = temptree! { + "hello2.exe": "#!/usr/bin/env bash\necho hello\n", + } + .executable("hello2.exe"); + + let path = + env::join_paths([tmp1.path().to_str().unwrap(), tmp2.path().to_str().unwrap()]).unwrap(); + + Test::new() + .justfile(r#"p := which("hello1.exe")"#) + .env("PATH", path.to_str().unwrap()) + .args(["--evaluate", "p"]) + .stdout(format!("{}", tmp1.path().join("hello1.exe").display())) + .run(); + + Test::new() + .justfile(r#"p := which("hello2.exe")"#) + .env("PATH", path.to_str().unwrap()) + .args(["--evaluate", "p"]) + .stdout(format!("{}", tmp2.path().join("hello2.exe").display())) + .run(); +} + +#[test] +fn supports_shadowed_executables() { + let tmp1 = temptree! { + "shadowed.exe": "#!/usr/bin/env bash\necho hello\n", + } + .executable("shadowed.exe"); + + let tmp2 = temptree! { + "shadowed.exe": "#!/usr/bin/env bash\necho hello\n", + } + .executable("shadowed.exe"); + + // which should never resolve to this directory, no matter where or how many + // times it appears in PATH, because the "shadowed" file is not executable. + let dummy = if cfg!(windows) { + temptree! { + "shadowed": "#!/usr/bin/env bash\necho hello\n", + } + } else { + temptree! { + "shadowed.exe": "#!/usr/bin/env bash\necho hello\n", + } + }; + + // This PATH should give priority to tmp1/shadowed.exe + let tmp1_path = env::join_paths([ + dummy.path().to_str().unwrap(), + tmp1.path().to_str().unwrap(), + dummy.path().to_str().unwrap(), + tmp2.path().to_str().unwrap(), + dummy.path().to_str().unwrap(), + ]) + .unwrap(); + + // This PATH should give priority to tmp2/shadowed.exe + let tmp2_path = env::join_paths([ + dummy.path().to_str().unwrap(), + tmp2.path().to_str().unwrap(), + dummy.path().to_str().unwrap(), + tmp1.path().to_str().unwrap(), + dummy.path().to_str().unwrap(), + ]) + .unwrap(); + + Test::new() + .justfile(r#"p := which("shadowed.exe")"#) + .env("PATH", tmp1_path.to_str().unwrap()) + .args(["--evaluate", "p"]) + .stdout(format!("{}", tmp1.path().join("shadowed.exe").display())) + .run(); + + Test::new() + .justfile(r#"p := which("shadowed.exe")"#) + .env("PATH", tmp2_path.to_str().unwrap()) + .args(["--evaluate", "p"]) + .stdout(format!("{}", tmp2.path().join("shadowed.exe").display())) + .run(); +} From 2a535c0fbe70712c4dcdd52e27a6aa5966656074 Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Sun, 1 Dec 2024 18:00:27 -0500 Subject: [PATCH 05/29] Remove stray addition to justfile --- justfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/justfile b/justfile index cf65778280..d0ff455c67 100755 --- a/justfile +++ b/justfile @@ -6,8 +6,6 @@ alias t := test log := "warn" -bingus := which("bash") - export JUST_LOG := log [group: 'dev'] From 34f2ea607d4758349844a8e8635e4f90e97cd171 Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:14:21 -0800 Subject: [PATCH 06/29] Remove dependency on either --- Cargo.lock | 1 - Cargo.toml | 1 - src/function.rs | 7 +++---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ba1463bd6..8958927e23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -544,7 +544,6 @@ dependencies = [ "dirs", "dotenvy", "edit-distance", - "either", "executable-path", "heck", "is_executable", diff --git a/Cargo.toml b/Cargo.toml index bda81c5ec1..8c5be83add 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,6 @@ derivative = "2.0.0" dirs = "5.0.1" dotenvy = "0.15" edit-distance = "2.0.0" -either = "1.13.0" heck = "0.5.0" is_executable = "1.0.4" lexiclean = "0.0.1" diff --git a/src/function.rs b/src/function.rs index 25c328b9c5..795a5297fd 100644 --- a/src/function.rs +++ b/src/function.rs @@ -1,6 +1,5 @@ use { super::*, - either::Either, heck::{ ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase, @@ -680,15 +679,15 @@ fn which(context: Context, s: &str) -> FunctionResult { 1 => { // cmd is a regular command path_var = env::var_os("PATH").ok_or("Environment variable `PATH` is not set")?; - Either::Left(env::split_paths(&path_var).map(|path| path.join(cmd.clone()))) + env::split_paths(&path_var).map(|path| path.join(cmd.clone())).collect() } _ => { // cmd contains a path separator, treat it as a path - Either::Right(iter::once(cmd)) + vec![cmd] } }; - for mut candidate in candidates.into_iter() { + for mut candidate in candidates { if candidate.is_relative() { // This candidate is a relative path, either because the user invoked `which("./rel/path")`, // or because there was a relative path in `PATH`. Resolve it to an absolute path. From c89e18252257ea72a1afb2fe56037e844bacfd6d Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Sat, 28 Dec 2024 09:14:08 +0800 Subject: [PATCH 07/29] Handle empty command string up front --- src/function.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/function.rs b/src/function.rs index 51bd1bc733..5a81471a76 100644 --- a/src/function.rs +++ b/src/function.rs @@ -665,15 +665,21 @@ fn uuid(_context: Context) -> FunctionResult { fn which(context: Context, s: &str) -> FunctionResult { use is_executable::IsExecutable; + if s.is_empty() { + return Err("empty command".into()); + } + let cmd = PathBuf::from(s); let path_var; let candidates = match cmd.components().count() { - 0 => Err("empty command string".to_string())?, + 0 => unreachable!("empty command string"), 1 => { // cmd is a regular command path_var = env::var_os("PATH").ok_or("Environment variable `PATH` is not set")?; - env::split_paths(&path_var).map(|path| path.join(cmd.clone())).collect() + env::split_paths(&path_var) + .map(|path| path.join(cmd.clone())) + .collect() } _ => { // cmd contains a path separator, treat it as a path From aad38310aee8135d63f08dfe57aeb31d00f3ebaf Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Sat, 28 Dec 2024 09:16:37 +0800 Subject: [PATCH 08/29] Resolve relative paths relative to working directory --- src/function.rs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/function.rs b/src/function.rs index 5a81471a76..64188a06e8 100644 --- a/src/function.rs +++ b/src/function.rs @@ -690,21 +690,9 @@ fn which(context: Context, s: &str) -> FunctionResult { for mut candidate in candidates { if candidate.is_relative() { // This candidate is a relative path, either because the user invoked `which("./rel/path")`, - // or because there was a relative path in `PATH`. Resolve it to an absolute path. - let cwd = context - .evaluator - .context - .search - .justfile - .parent() - .ok_or_else(|| { - format!( - "Could not resolve absolute path from `{}` relative to the justfile directory. Justfile `{}` had no parent.", - candidate.display(), - context.evaluator.context.search.justfile.display() - ) - })?; - let mut cwd = PathBuf::from(cwd); + // or because there was a relative path in `PATH`. Resolve it to an absolute path, + // relative to the working directory of the just invocation. + let mut cwd = context.evaluator.context.working_directory(); cwd.push(candidate); candidate = cwd; } From 5aa3c07510569b30cae3e0c38be3545562a243fc Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Mon, 30 Dec 2024 18:37:03 +0800 Subject: [PATCH 09/29] Clean up implementation of which() - use Path::new(s) instead of PathBuf::from(s) - revert early handling of empty path - use PathBuf::join() instead of PathBuf::push() --- src/function.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/function.rs b/src/function.rs index 64188a06e8..04ffc2a385 100644 --- a/src/function.rs +++ b/src/function.rs @@ -665,36 +665,33 @@ fn uuid(_context: Context) -> FunctionResult { fn which(context: Context, s: &str) -> FunctionResult { use is_executable::IsExecutable; - if s.is_empty() { - return Err("empty command".into()); - } - - let cmd = PathBuf::from(s); + let cmd = Path::new(s); - let path_var; let candidates = match cmd.components().count() { - 0 => unreachable!("empty command string"), + 0 => return Err("empty command".into()), 1 => { // cmd is a regular command - path_var = env::var_os("PATH").ok_or("Environment variable `PATH` is not set")?; + let path_var = env::var_os("PATH").ok_or("Environment variable `PATH` is not set")?; env::split_paths(&path_var) - .map(|path| path.join(cmd.clone())) + .map(|path| path.join(cmd)) .collect() } _ => { // cmd contains a path separator, treat it as a path - vec![cmd] + vec![cmd.into()] } }; for mut candidate in candidates { if candidate.is_relative() { - // This candidate is a relative path, either because the user invoked `which("./rel/path")`, + // This candidate is a relative path, either because the user invoked `which("rel/path")`, // or because there was a relative path in `PATH`. Resolve it to an absolute path, // relative to the working directory of the just invocation. - let mut cwd = context.evaluator.context.working_directory(); - cwd.push(candidate); - candidate = cwd; + candidate = context + .evaluator + .context + .working_directory() + .join(candidate); } if candidate.is_executable() { From e7e9d565c8a509a639bfff80b29fc3de83251332 Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Mon, 30 Dec 2024 18:43:56 +0800 Subject: [PATCH 10/29] Rename which_exec -> which_function --- tests/lib.rs | 2 +- tests/{which_exec.rs => which_function.rs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/{which_exec.rs => which_function.rs} (100%) diff --git a/tests/lib.rs b/tests/lib.rs index fb2849719f..04133b9f7b 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -119,7 +119,7 @@ mod timestamps; mod undefined_variables; mod unexport; mod unstable; -mod which_exec; +mod which_function; #[cfg(windows)] mod windows; #[cfg(target_family = "windows")] diff --git a/tests/which_exec.rs b/tests/which_function.rs similarity index 100% rename from tests/which_exec.rs rename to tests/which_function.rs From 6a98c39457e70085aa770ae5b2426c8a1fbeda92 Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Mon, 30 Dec 2024 18:46:39 +0800 Subject: [PATCH 11/29] Use single quotes to avoid r# strings --- tests/which_function.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/which_function.rs b/tests/which_function.rs index edc09d723d..5a44d59aef 100644 --- a/tests/which_function.rs +++ b/tests/which_function.rs @@ -42,7 +42,7 @@ fn finds_executable() { .executable("hello.exe"); Test::new() - .justfile(r#"p := which("hello.exe")"#) + .justfile("p := which('hello.exe')") .env("PATH", tmp.path().to_str().unwrap()) .args(["--evaluate", "p"]) .stdout(format!("{}", tmp.path().join("hello.exe").display())) @@ -57,7 +57,7 @@ fn prints_empty_string_for_missing_executable() { .executable("hello.exe"); Test::new() - .justfile(r#"p := which("goodbye.exe")"#) + .justfile("p := which('goodbye.exe')") .env("PATH", tmp.path().to_str().unwrap()) .args(["--evaluate", "p"]) .stdout("") @@ -73,7 +73,7 @@ fn skips_non_executable_files() { .executable("hello.exe"); Test::new() - .justfile(r#"p := which("hi")"#) + .justfile("p := which('hi')") .env("PATH", tmp.path().to_str().unwrap()) .args(["--evaluate", "p"]) .stdout("") @@ -96,14 +96,14 @@ fn supports_multiple_paths() { env::join_paths([tmp1.path().to_str().unwrap(), tmp2.path().to_str().unwrap()]).unwrap(); Test::new() - .justfile(r#"p := which("hello1.exe")"#) + .justfile("p := which('hello1.exe')") .env("PATH", path.to_str().unwrap()) .args(["--evaluate", "p"]) .stdout(format!("{}", tmp1.path().join("hello1.exe").display())) .run(); Test::new() - .justfile(r#"p := which("hello2.exe")"#) + .justfile("p := which('hello2.exe')") .env("PATH", path.to_str().unwrap()) .args(["--evaluate", "p"]) .stdout(format!("{}", tmp2.path().join("hello2.exe").display())) @@ -155,14 +155,14 @@ fn supports_shadowed_executables() { .unwrap(); Test::new() - .justfile(r#"p := which("shadowed.exe")"#) + .justfile("p := which('shadowed.exe')") .env("PATH", tmp1_path.to_str().unwrap()) .args(["--evaluate", "p"]) .stdout(format!("{}", tmp1.path().join("shadowed.exe").display())) .run(); Test::new() - .justfile(r#"p := which("shadowed.exe")"#) + .justfile("p := which('shadowed.exe')") .env("PATH", tmp2_path.to_str().unwrap()) .args(["--evaluate", "p"]) .stdout(format!("{}", tmp2.path().join("shadowed.exe").display())) From 1f40ec3e88b726be9edcfd8055b235a343570259 Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Mon, 30 Dec 2024 20:58:36 +0800 Subject: [PATCH 12/29] Remove use of temptree! macro --- tests/test.rs | 25 +++++ tests/which_function.rs | 217 ++++++++++++++++++---------------------- 2 files changed, 120 insertions(+), 122 deletions(-) diff --git a/tests/test.rs b/tests/test.rs index e02b5a85b6..c56eebaa1a 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -205,6 +205,31 @@ impl Test { self } + pub(crate) fn make_executable(self, path: impl AsRef) -> Self { + let file = self.tempdir.path().join(path); + + // Make sure it exists first, as a sanity check. + assert!(file.exists(), "file does not exist: {}", file.display()); + + // Windows uses file extensions to determine whether a file is executable. + // Other systems don't care. To keep these tests cross-platform, just make + // sure all executables end with ".exe" suffix. + assert!( + file.extension() == Some("exe".as_ref()), + "executable file does not end with .exe: {}", + file.display() + ); + + println!("hi: {}", file.display()); + + if !cfg!(windows) { + let perms = std::os::unix::fs::PermissionsExt::from_mode(0o755); + fs::set_permissions(file, perms).unwrap(); + } + + self + } + pub(crate) fn expect_file(mut self, path: impl AsRef, content: impl AsRef<[u8]>) -> Self { let path = path.as_ref(); self diff --git a/tests/which_function.rs b/tests/which_function.rs index 5a44d59aef..8a76e3402b 100644 --- a/tests/which_function.rs +++ b/tests/which_function.rs @@ -1,170 +1,143 @@ use super::*; -trait TempDirExt { - fn executable(self, file: impl AsRef) -> Self; -} - -impl TempDirExt for TempDir { - fn executable(self, file: impl AsRef) -> Self { - let file = self.path().join(file.as_ref()); - - // Make sure it exists first, as a sanity check. - assert!( - file.exists(), - "executable file does not exist: {}", - file.display() - ); - - // Windows uses file extensions to determine whether a file is executable. - // Other systems don't care. To keep these tests cross-platform, just make - // sure all executables end with ".exe" suffix. - assert!( - file.extension() == Some("exe".as_ref()), - "executable file does not end with .exe: {}", - file.display() - ); - - #[cfg(not(windows))] - { - let perms = std::os::unix::fs::PermissionsExt::from_mode(0o755); - fs::set_permissions(file, perms).unwrap(); - } - - self - } -} - #[test] fn finds_executable() { - let tmp = temptree! { - "hello.exe": "#!/usr/bin/env bash\necho hello\n", - } - .executable("hello.exe"); + let tmp = tempdir(); + let path = PathBuf::from(tmp.path()); - Test::new() + Test::with_tempdir(tmp) .justfile("p := which('hello.exe')") - .env("PATH", tmp.path().to_str().unwrap()) .args(["--evaluate", "p"]) - .stdout(format!("{}", tmp.path().join("hello.exe").display())) + .write("hello.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("hello.exe") + .env("PATH", path.to_str().unwrap()) + .stdout(format!("{}", path.join("hello.exe").display())) .run(); } #[test] fn prints_empty_string_for_missing_executable() { - let tmp = temptree! { - "hello.exe": "#!/usr/bin/env bash\necho hello\n", - } - .executable("hello.exe"); + let tmp = tempdir(); + let path = PathBuf::from(tmp.path()); - Test::new() + Test::with_tempdir(tmp) .justfile("p := which('goodbye.exe')") - .env("PATH", tmp.path().to_str().unwrap()) .args(["--evaluate", "p"]) + .write("hello.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("hello.exe") + .env("PATH", path.to_str().unwrap()) .stdout("") .run(); } #[test] fn skips_non_executable_files() { - let tmp = temptree! { - "hello.exe": "#!/usr/bin/env bash\necho hello\n", - "hi": "just some regular file", - } - .executable("hello.exe"); + let tmp = tempdir(); + let path = PathBuf::from(tmp.path()); - Test::new() + Test::with_tempdir(tmp) .justfile("p := which('hi')") - .env("PATH", tmp.path().to_str().unwrap()) .args(["--evaluate", "p"]) + .write("hello.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("hello.exe") + .write("hi", "just some regular file") + .env("PATH", path.to_str().unwrap()) .stdout("") .run(); } #[test] fn supports_multiple_paths() { - let tmp1 = temptree! { - "hello1.exe": "#!/usr/bin/env bash\necho hello\n", - } - .executable("hello1.exe"); - - let tmp2 = temptree! { - "hello2.exe": "#!/usr/bin/env bash\necho hello\n", - } - .executable("hello2.exe"); - - let path = - env::join_paths([tmp1.path().to_str().unwrap(), tmp2.path().to_str().unwrap()]).unwrap(); - - Test::new() - .justfile("p := which('hello1.exe')") - .env("PATH", path.to_str().unwrap()) - .args(["--evaluate", "p"]) - .stdout(format!("{}", tmp1.path().join("hello1.exe").display())) - .run(); + let tmp = tempdir(); + let path = PathBuf::from(tmp.path()); + let path_var = env::join_paths([ + path.join("subdir1").to_str().unwrap(), + path.join("subdir2").to_str().unwrap(), + ]) + .unwrap(); - Test::new() - .justfile("p := which('hello2.exe')") - .env("PATH", path.to_str().unwrap()) + Test::with_tempdir(tmp) + .justfile("p := which('hello1.exe') + '+' + which('hello2.exe')") .args(["--evaluate", "p"]) - .stdout(format!("{}", tmp2.path().join("hello2.exe").display())) + .write("subdir1/hello1.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("subdir1/hello1.exe") + .write("subdir2/hello2.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("subdir2/hello2.exe") + .env("PATH", path_var.to_str().unwrap()) + .stdout(format!( + "{}+{}", + path.join("subdir1").join("hello1.exe").display(), + path.join("subdir2").join("hello2.exe").display(), + )) .run(); } #[test] fn supports_shadowed_executables() { - let tmp1 = temptree! { - "shadowed.exe": "#!/usr/bin/env bash\necho hello\n", - } - .executable("shadowed.exe"); - - let tmp2 = temptree! { - "shadowed.exe": "#!/usr/bin/env bash\necho hello\n", + enum Variation { + Dir1Dir2, // PATH=/tmp/.../dir1:/tmp/.../dir2 + Dir2Dir1, // PATH=/tmp/.../dir2:/tmp/.../dir1 } - .executable("shadowed.exe"); - // which should never resolve to this directory, no matter where or how many - // times it appears in PATH, because the "shadowed" file is not executable. - let dummy = if cfg!(windows) { - temptree! { - "shadowed": "#!/usr/bin/env bash\necho hello\n", - } - } else { - temptree! { - "shadowed.exe": "#!/usr/bin/env bash\necho hello\n", + for variation in [Variation::Dir1Dir2, Variation::Dir2Dir1] { + let tmp = tempdir(); + let path = PathBuf::from(tmp.path()); + + let path_var = match variation { + Variation::Dir1Dir2 => env::join_paths([ + path.join("dir1").to_str().unwrap(), + path.join("dir2").to_str().unwrap(), + ]), + Variation::Dir2Dir1 => env::join_paths([ + path.join("dir2").to_str().unwrap(), + path.join("dir1").to_str().unwrap(), + ]), } - }; - - // This PATH should give priority to tmp1/shadowed.exe - let tmp1_path = env::join_paths([ - dummy.path().to_str().unwrap(), - tmp1.path().to_str().unwrap(), - dummy.path().to_str().unwrap(), - tmp2.path().to_str().unwrap(), - dummy.path().to_str().unwrap(), - ]) - .unwrap(); + .unwrap(); + + let stdout = match variation { + Variation::Dir1Dir2 => format!("{}", path.join("dir1").join("shadowed.exe").display()), + Variation::Dir2Dir1 => format!("{}", path.join("dir2").join("shadowed.exe").display()), + }; + + Test::with_tempdir(tmp) + .justfile("p := which('shadowed.exe')") + .args(["--evaluate", "p"]) + .write("dir1/shadowed.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("dir1/shadowed.exe") + .write("dir2/shadowed.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("dir2/shadowed.exe") + .env("PATH", path_var.to_str().unwrap()) + .stdout(stdout) + .run(); + } +} - // This PATH should give priority to tmp2/shadowed.exe - let tmp2_path = env::join_paths([ - dummy.path().to_str().unwrap(), - tmp2.path().to_str().unwrap(), - dummy.path().to_str().unwrap(), - tmp1.path().to_str().unwrap(), - dummy.path().to_str().unwrap(), +#[test] +fn ignores_nonexecutable_candidates() { + let tmp = tempdir(); + let path = PathBuf::from(tmp.path()); + + let path_var = env::join_paths([ + path.join("dummy").to_str().unwrap(), + path.join("subdir").to_str().unwrap(), + path.join("dummy").to_str().unwrap(), ]) .unwrap(); - Test::new() - .justfile("p := which('shadowed.exe')") - .env("PATH", tmp1_path.to_str().unwrap()) - .args(["--evaluate", "p"]) - .stdout(format!("{}", tmp1.path().join("shadowed.exe").display())) - .run(); + let dummy_exe = if cfg!(windows) { + "dummy/foo" + } else { + "dummy/foo.exe" + }; - Test::new() - .justfile("p := which('shadowed.exe')") - .env("PATH", tmp2_path.to_str().unwrap()) + Test::with_tempdir(tmp) + .justfile("p := which('foo.exe')") .args(["--evaluate", "p"]) - .stdout(format!("{}", tmp2.path().join("shadowed.exe").display())) + .write("subdir/foo.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("subdir/foo.exe") + .write(dummy_exe, "#!/usr/bin/env bash\necho hello\n") + .env("PATH", path_var.to_str().unwrap()) + .stdout(format!("{}", path.join("subdir").join("foo.exe").display())) .run(); } From d642b1ddc434606ed6eee6e75303d10b44ab2fc9 Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:27:41 +0800 Subject: [PATCH 13/29] Add some tests for relative paths --- tests/which_function.rs | 56 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/which_function.rs b/tests/which_function.rs index 8a76e3402b..bcba1fd4cc 100644 --- a/tests/which_function.rs +++ b/tests/which_function.rs @@ -141,3 +141,59 @@ fn ignores_nonexecutable_candidates() { .stdout(format!("{}", path.join("subdir").join("foo.exe").display())) .run(); } + +#[test] +fn handles_absolute_path() { + let tmp = tempdir(); + let path = PathBuf::from(tmp.path()); + let abspath = path.join("subdir").join("foo.exe"); + + Test::with_tempdir(tmp) + .justfile(format!("p := which('{}')", abspath.display())) + .write("subdir/foo.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("subdir/foo.exe") + .write("pathdir/foo.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("pathdir/foo.exe") + .env("PATH", path.join("pathdir").to_str().unwrap()) + .args(["--evaluate", "p"]) + .stdout(format!("{}", abspath.display())) + .run(); +} + +#[test] +fn handles_dotslash() { + let tmp = tempdir(); + let path = tmp.path().canonicalize().unwrap(); + // canonicalize() is necessary here to account for the justfile prepending + // the canonicalized working directory to './foo.exe'. + + Test::with_tempdir(tmp) + .justfile("p := which('./foo.exe')") + .args(["--evaluate", "p"]) + .write("foo.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("foo.exe") + .write("pathdir/foo.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("pathdir/foo.exe") + .env("PATH", path.join("pathdir").to_str().unwrap()) + .stdout(format!("{}", path.join(".").join("foo.exe").display())) + .run(); +} + +#[test] +fn handles_dir_slash() { + let tmp = tempdir(); + let path = tmp.path().canonicalize().unwrap(); + // canonicalize() is necessary here to account for the justfile prepending + // the canonicalized working directory to 'subdir/foo.exe'. + + Test::with_tempdir(tmp) + .justfile("p := which('subdir/foo.exe')") + .args(["--evaluate", "p"]) + .write("subdir/foo.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("subdir/foo.exe") + .write("pathdir/foo.exe", "#!/usr/bin/env bash\necho hello\n") + .make_executable("pathdir/foo.exe") + .env("PATH", path.join("pathdir").to_str().unwrap()) + .stdout(format!("{}", path.join("subdir").join("foo.exe").display())) + .run(); +} From f967dc677d63cebe99f4fd94c145e540ff6033ec Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:31:20 -0800 Subject: [PATCH 14/29] Add lexiclean to which path --- src/function.rs | 2 ++ tests/which_function.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/function.rs b/src/function.rs index 04ffc2a385..664a69d9e3 100644 --- a/src/function.rs +++ b/src/function.rs @@ -694,6 +694,8 @@ fn which(context: Context, s: &str) -> FunctionResult { .join(candidate); } + candidate = candidate.lexiclean(); + if candidate.is_executable() { return candidate.to_str().map(str::to_string).ok_or_else(|| { format!( diff --git a/tests/which_function.rs b/tests/which_function.rs index bcba1fd4cc..8f98c7fb87 100644 --- a/tests/which_function.rs +++ b/tests/which_function.rs @@ -175,7 +175,7 @@ fn handles_dotslash() { .write("pathdir/foo.exe", "#!/usr/bin/env bash\necho hello\n") .make_executable("pathdir/foo.exe") .env("PATH", path.join("pathdir").to_str().unwrap()) - .stdout(format!("{}", path.join(".").join("foo.exe").display())) + .stdout(format!("{}", path.join("foo.exe").display())) .run(); } From 5eaf570344d09983c647d90a8bd189603530a71d Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:36:40 -0800 Subject: [PATCH 15/29] Make HELLO_SCRIPT constant --- tests/which_function.rs | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/tests/which_function.rs b/tests/which_function.rs index 8f98c7fb87..85bd0c8962 100644 --- a/tests/which_function.rs +++ b/tests/which_function.rs @@ -1,5 +1,9 @@ use super::*; +const HELLO_SCRIPT: &str = "#!/usr/bin/env bash +echo hello +"; + #[test] fn finds_executable() { let tmp = tempdir(); @@ -8,7 +12,7 @@ fn finds_executable() { Test::with_tempdir(tmp) .justfile("p := which('hello.exe')") .args(["--evaluate", "p"]) - .write("hello.exe", "#!/usr/bin/env bash\necho hello\n") + .write("hello.exe", HELLO_SCRIPT) .make_executable("hello.exe") .env("PATH", path.to_str().unwrap()) .stdout(format!("{}", path.join("hello.exe").display())) @@ -23,7 +27,7 @@ fn prints_empty_string_for_missing_executable() { Test::with_tempdir(tmp) .justfile("p := which('goodbye.exe')") .args(["--evaluate", "p"]) - .write("hello.exe", "#!/usr/bin/env bash\necho hello\n") + .write("hello.exe", HELLO_SCRIPT) .make_executable("hello.exe") .env("PATH", path.to_str().unwrap()) .stdout("") @@ -38,7 +42,7 @@ fn skips_non_executable_files() { Test::with_tempdir(tmp) .justfile("p := which('hi')") .args(["--evaluate", "p"]) - .write("hello.exe", "#!/usr/bin/env bash\necho hello\n") + .write("hello.exe", HELLO_SCRIPT) .make_executable("hello.exe") .write("hi", "just some regular file") .env("PATH", path.to_str().unwrap()) @@ -59,9 +63,9 @@ fn supports_multiple_paths() { Test::with_tempdir(tmp) .justfile("p := which('hello1.exe') + '+' + which('hello2.exe')") .args(["--evaluate", "p"]) - .write("subdir1/hello1.exe", "#!/usr/bin/env bash\necho hello\n") + .write("subdir1/hello1.exe", HELLO_SCRIPT) .make_executable("subdir1/hello1.exe") - .write("subdir2/hello2.exe", "#!/usr/bin/env bash\necho hello\n") + .write("subdir2/hello2.exe", HELLO_SCRIPT) .make_executable("subdir2/hello2.exe") .env("PATH", path_var.to_str().unwrap()) .stdout(format!( @@ -103,9 +107,9 @@ fn supports_shadowed_executables() { Test::with_tempdir(tmp) .justfile("p := which('shadowed.exe')") .args(["--evaluate", "p"]) - .write("dir1/shadowed.exe", "#!/usr/bin/env bash\necho hello\n") + .write("dir1/shadowed.exe", HELLO_SCRIPT) .make_executable("dir1/shadowed.exe") - .write("dir2/shadowed.exe", "#!/usr/bin/env bash\necho hello\n") + .write("dir2/shadowed.exe", HELLO_SCRIPT) .make_executable("dir2/shadowed.exe") .env("PATH", path_var.to_str().unwrap()) .stdout(stdout) @@ -134,9 +138,9 @@ fn ignores_nonexecutable_candidates() { Test::with_tempdir(tmp) .justfile("p := which('foo.exe')") .args(["--evaluate", "p"]) - .write("subdir/foo.exe", "#!/usr/bin/env bash\necho hello\n") + .write("subdir/foo.exe", HELLO_SCRIPT) .make_executable("subdir/foo.exe") - .write(dummy_exe, "#!/usr/bin/env bash\necho hello\n") + .write(dummy_exe, HELLO_SCRIPT) .env("PATH", path_var.to_str().unwrap()) .stdout(format!("{}", path.join("subdir").join("foo.exe").display())) .run(); @@ -150,9 +154,9 @@ fn handles_absolute_path() { Test::with_tempdir(tmp) .justfile(format!("p := which('{}')", abspath.display())) - .write("subdir/foo.exe", "#!/usr/bin/env bash\necho hello\n") + .write("subdir/foo.exe", HELLO_SCRIPT) .make_executable("subdir/foo.exe") - .write("pathdir/foo.exe", "#!/usr/bin/env bash\necho hello\n") + .write("pathdir/foo.exe", HELLO_SCRIPT) .make_executable("pathdir/foo.exe") .env("PATH", path.join("pathdir").to_str().unwrap()) .args(["--evaluate", "p"]) @@ -170,9 +174,9 @@ fn handles_dotslash() { Test::with_tempdir(tmp) .justfile("p := which('./foo.exe')") .args(["--evaluate", "p"]) - .write("foo.exe", "#!/usr/bin/env bash\necho hello\n") + .write("foo.exe", HELLO_SCRIPT) .make_executable("foo.exe") - .write("pathdir/foo.exe", "#!/usr/bin/env bash\necho hello\n") + .write("pathdir/foo.exe", HELLO_SCRIPT) .make_executable("pathdir/foo.exe") .env("PATH", path.join("pathdir").to_str().unwrap()) .stdout(format!("{}", path.join("foo.exe").display())) @@ -189,9 +193,9 @@ fn handles_dir_slash() { Test::with_tempdir(tmp) .justfile("p := which('subdir/foo.exe')") .args(["--evaluate", "p"]) - .write("subdir/foo.exe", "#!/usr/bin/env bash\necho hello\n") + .write("subdir/foo.exe", HELLO_SCRIPT) .make_executable("subdir/foo.exe") - .write("pathdir/foo.exe", "#!/usr/bin/env bash\necho hello\n") + .write("pathdir/foo.exe", HELLO_SCRIPT) .make_executable("pathdir/foo.exe") .env("PATH", path.join("pathdir").to_str().unwrap()) .stdout(format!("{}", path.join("subdir").join("foo.exe").display())) From 775d769fac05a58e4e60a57ce88150e46eed5f3a Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 16 Jan 2025 15:16:28 -0800 Subject: [PATCH 16/29] Sort function list alphabetically --- src/function.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/function.rs b/src/function.rs index 664a69d9e3..3a6c598357 100644 --- a/src/function.rs +++ b/src/function.rs @@ -111,8 +111,8 @@ pub(crate) fn get(name: &str) -> Option { "uppercamelcase" => Unary(uppercamelcase), "uppercase" => Unary(uppercase), "uuid" => Nullary(uuid), - "without_extension" => Unary(without_extension), "which" => Unary(which), + "without_extension" => Unary(without_extension), _ => return None, }; Some(function) From 941a5b92e8ce720e366bd3bedf641909dab09405 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 16 Jan 2025 15:24:29 -0800 Subject: [PATCH 17/29] Avoid import --- src/function.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/function.rs b/src/function.rs index 3a6c598357..07c8a12083 100644 --- a/src/function.rs +++ b/src/function.rs @@ -663,8 +663,6 @@ fn uuid(_context: Context) -> FunctionResult { } fn which(context: Context, s: &str) -> FunctionResult { - use is_executable::IsExecutable; - let cmd = Path::new(s); let candidates = match cmd.components().count() { @@ -696,7 +694,7 @@ fn which(context: Context, s: &str) -> FunctionResult { candidate = candidate.lexiclean(); - if candidate.is_executable() { + if is_executable::is_executable(&candidate) { return candidate.to_str().map(str::to_string).ok_or_else(|| { format!( "Executable path is not valid unicode: {}", From a7fe1569ad161c660f3b9ebfd50ba3406e33881c Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Thu, 16 Jan 2025 22:37:13 -0800 Subject: [PATCH 18/29] Remove greeting --- tests/test.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test.rs b/tests/test.rs index c56eebaa1a..39690241b7 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -220,8 +220,6 @@ impl Test { file.display() ); - println!("hi: {}", file.display()); - if !cfg!(windows) { let perms = std::os::unix::fs::PermissionsExt::from_mode(0o755); fs::set_permissions(file, perms).unwrap(); From 0beadec1850f85850ee1006dbd55b934eb4ef4d2 Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Thu, 16 Jan 2025 22:50:23 -0800 Subject: [PATCH 19/29] Add require --- src/function.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/function.rs b/src/function.rs index 07c8a12083..d5b9f192bc 100644 --- a/src/function.rs +++ b/src/function.rs @@ -90,6 +90,7 @@ pub(crate) fn get(name: &str) -> Option { "read" => Unary(read), "replace" => Ternary(replace), "replace_regex" => Ternary(replace_regex), + "require" => Unary(require), "semver_matches" => Binary(semver_matches), "sha256" => Unary(sha256), "sha256_file" => Unary(sha256_file), @@ -512,6 +513,15 @@ fn replace(_context: Context, s: &str, from: &str, to: &str) -> FunctionResult { Ok(s.replace(from, to)) } +fn require(context: Context, s: &str) -> FunctionResult { + let p = which(context, s)?; + if p.is_empty() { + Err(format!("could not find required executable: `{s}`")) + } else { + Ok(p) + } +} + fn replace_regex(_context: Context, s: &str, regex: &str, replacement: &str) -> FunctionResult { Ok( Regex::new(regex) From f5c58092ee41aaed9ba1f105c969449e59e5e41f Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Fri, 17 Jan 2025 13:24:48 -0800 Subject: [PATCH 20/29] Make which() function unstable --- src/parser.rs | 3 +++ src/unstable_feature.rs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/parser.rs b/src/parser.rs index 348032a065..b2ea48ecc5 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -698,6 +698,9 @@ impl<'run, 'src> Parser<'run, 'src> { if self.next_is(ParenL) { let arguments = self.parse_sequence()?; + if name.lexeme() == "which" { + self.unstable_features.insert(UnstableFeature::WhichFunction); + } Ok(Expression::Call { thunk: Thunk::resolve(name, arguments)?, }) diff --git a/src/unstable_feature.rs b/src/unstable_feature.rs index 70e26fabcf..daa2270b1e 100644 --- a/src/unstable_feature.rs +++ b/src/unstable_feature.rs @@ -6,6 +6,7 @@ pub(crate) enum UnstableFeature { LogicalOperators, ScriptAttribute, ScriptInterpreterSetting, + WhichFunction, } impl Display for UnstableFeature { @@ -20,6 +21,7 @@ impl Display for UnstableFeature { Self::ScriptInterpreterSetting => { write!(f, "The `script-interpreter` setting is currently unstable.") } + Self::WhichFunction => write!(f, "The `which()` function is currently unstable."), } } } From a8e8ea43b90a5ea3b0c48c7ff405a1abbaf77286 Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Tue, 21 Jan 2025 23:12:20 -0800 Subject: [PATCH 21/29] Format --- src/parser.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/parser.rs b/src/parser.rs index 588f46ab46..9305a42db5 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -699,7 +699,9 @@ impl<'run, 'src> Parser<'run, 'src> { if self.next_is(ParenL) { let arguments = self.parse_sequence()?; if name.lexeme() == "which" { - self.unstable_features.insert(UnstableFeature::WhichFunction); + self + .unstable_features + .insert(UnstableFeature::WhichFunction); } Ok(Expression::Call { thunk: Thunk::resolve(name, arguments)?, From 87a315b391b78a9e694ec4c50b8120fcaad8330e Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Tue, 21 Jan 2025 23:19:35 -0800 Subject: [PATCH 22/29] Fix tests + add unstable test --- tests/which_function.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/which_function.rs b/tests/which_function.rs index 85bd0c8962..51478948da 100644 --- a/tests/which_function.rs +++ b/tests/which_function.rs @@ -15,6 +15,7 @@ fn finds_executable() { .write("hello.exe", HELLO_SCRIPT) .make_executable("hello.exe") .env("PATH", path.to_str().unwrap()) + .env("JUST_UNSTABLE", "1") .stdout(format!("{}", path.join("hello.exe").display())) .run(); } @@ -30,6 +31,7 @@ fn prints_empty_string_for_missing_executable() { .write("hello.exe", HELLO_SCRIPT) .make_executable("hello.exe") .env("PATH", path.to_str().unwrap()) + .env("JUST_UNSTABLE", "1") .stdout("") .run(); } @@ -46,6 +48,7 @@ fn skips_non_executable_files() { .make_executable("hello.exe") .write("hi", "just some regular file") .env("PATH", path.to_str().unwrap()) + .env("JUST_UNSTABLE", "1") .stdout("") .run(); } @@ -68,6 +71,7 @@ fn supports_multiple_paths() { .write("subdir2/hello2.exe", HELLO_SCRIPT) .make_executable("subdir2/hello2.exe") .env("PATH", path_var.to_str().unwrap()) + .env("JUST_UNSTABLE", "1") .stdout(format!( "{}+{}", path.join("subdir1").join("hello1.exe").display(), @@ -112,6 +116,7 @@ fn supports_shadowed_executables() { .write("dir2/shadowed.exe", HELLO_SCRIPT) .make_executable("dir2/shadowed.exe") .env("PATH", path_var.to_str().unwrap()) + .env("JUST_UNSTABLE", "1") .stdout(stdout) .run(); } @@ -142,6 +147,7 @@ fn ignores_nonexecutable_candidates() { .make_executable("subdir/foo.exe") .write(dummy_exe, HELLO_SCRIPT) .env("PATH", path_var.to_str().unwrap()) + .env("JUST_UNSTABLE", "1") .stdout(format!("{}", path.join("subdir").join("foo.exe").display())) .run(); } @@ -159,6 +165,7 @@ fn handles_absolute_path() { .write("pathdir/foo.exe", HELLO_SCRIPT) .make_executable("pathdir/foo.exe") .env("PATH", path.join("pathdir").to_str().unwrap()) + .env("JUST_UNSTABLE", "1") .args(["--evaluate", "p"]) .stdout(format!("{}", abspath.display())) .run(); @@ -179,6 +186,7 @@ fn handles_dotslash() { .write("pathdir/foo.exe", HELLO_SCRIPT) .make_executable("pathdir/foo.exe") .env("PATH", path.join("pathdir").to_str().unwrap()) + .env("JUST_UNSTABLE", "1") .stdout(format!("{}", path.join("foo.exe").display())) .run(); } @@ -198,6 +206,23 @@ fn handles_dir_slash() { .write("pathdir/foo.exe", HELLO_SCRIPT) .make_executable("pathdir/foo.exe") .env("PATH", path.join("pathdir").to_str().unwrap()) + .env("JUST_UNSTABLE", "1") .stdout(format!("{}", path.join("subdir").join("foo.exe").display())) .run(); } + +#[test] +fn is_unstable() { + let tmp = tempdir(); + let path = PathBuf::from(tmp.path()); + + Test::with_tempdir(tmp) + .justfile("p := which('hello.exe')") + .args(["--evaluate", "p"]) + .write("hello.exe", HELLO_SCRIPT) + .make_executable("hello.exe") + .env("PATH", path.to_str().unwrap()) + .stderr_regex(r".*The `which\(\)` function is currently unstable\..*") + .status(1) + .run(); +} From f4501dd7d1f8023488946ad499527054aef56ea8 Mon Sep 17 00:00:00 2001 From: 0xzhzh <178814591+0xzhzh@users.noreply.github.com> Date: Tue, 21 Jan 2025 23:27:46 -0800 Subject: [PATCH 23/29] Add documentation --- README.md | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f3a02e7b44..b383d46588 100644 --- a/README.md +++ b/README.md @@ -1674,20 +1674,39 @@ set unstable foo := env('FOO') || 'DEFAULT_VALUE' ``` -- `which(exe)`master — Retrieves the full path of `exe` according - to the `PATH`. Returns an empty string if no executable named `exe` exists. +- `require(exe)`master — Retrieves the full path of `exe` according + to the `PATH`. Throws an error if no executable named `exe` exists. ```just -bash := which("bash") -nexist := which("does-not-exist") +bash := require("bash") @test: echo "bash: '{{bash}}'" - echo "nexist: '{{nexist}}'" ``` ```console +$ just bash: '/bin/bash' +``` + +- `which(exe)`master — Like `require()`, retrieves the full path of + `exe` according to the `PATH`. However, unlike `require()`, `which()` + returns an empty string if no executable named `exe` exists, instead of + throwing an error. + + This feature is currently unstable. + +```just +set unstable + +nexist := require("nexist") + +@test: + echo "nexist: '{{nexist}}'" +``` + +```console +$ just nexist: '' ``` From 2e17660e3fd7c23256b74c0e6a439e965b75ed04 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 22 Jan 2025 10:31:46 -0800 Subject: [PATCH 24/29] Remove a few unnecessary uses of format!() --- tests/which_function.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/which_function.rs b/tests/which_function.rs index 51478948da..122b99cafd 100644 --- a/tests/which_function.rs +++ b/tests/which_function.rs @@ -16,7 +16,7 @@ fn finds_executable() { .make_executable("hello.exe") .env("PATH", path.to_str().unwrap()) .env("JUST_UNSTABLE", "1") - .stdout(format!("{}", path.join("hello.exe").display())) + .stdout(path.join("hello.exe").display().to_string()) .run(); } @@ -148,7 +148,7 @@ fn ignores_nonexecutable_candidates() { .write(dummy_exe, HELLO_SCRIPT) .env("PATH", path_var.to_str().unwrap()) .env("JUST_UNSTABLE", "1") - .stdout(format!("{}", path.join("subdir").join("foo.exe").display())) + .stdout(path.join("subdir").join("foo.exe").display().to_string()) .run(); } @@ -167,7 +167,7 @@ fn handles_absolute_path() { .env("PATH", path.join("pathdir").to_str().unwrap()) .env("JUST_UNSTABLE", "1") .args(["--evaluate", "p"]) - .stdout(format!("{}", abspath.display())) + .stdout(abspath.display().to_string()) .run(); } @@ -187,7 +187,7 @@ fn handles_dotslash() { .make_executable("pathdir/foo.exe") .env("PATH", path.join("pathdir").to_str().unwrap()) .env("JUST_UNSTABLE", "1") - .stdout(format!("{}", path.join("foo.exe").display())) + .stdout(path.join("foo.exe").display().to_string()) .run(); } @@ -207,7 +207,7 @@ fn handles_dir_slash() { .make_executable("pathdir/foo.exe") .env("PATH", path.join("pathdir").to_str().unwrap()) .env("JUST_UNSTABLE", "1") - .stdout(format!("{}", path.join("subdir").join("foo.exe").display())) + .stdout(path.join("subdir").join("foo.exe").display().to_string()) .run(); } From 86753cbe6f04d174c49d9b645c4c3a92ea328da2 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 22 Jan 2025 10:32:56 -0800 Subject: [PATCH 25/29] Expected stdout defaults to empty string --- tests/which_function.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/which_function.rs b/tests/which_function.rs index 122b99cafd..5f4938858a 100644 --- a/tests/which_function.rs +++ b/tests/which_function.rs @@ -32,7 +32,6 @@ fn prints_empty_string_for_missing_executable() { .make_executable("hello.exe") .env("PATH", path.to_str().unwrap()) .env("JUST_UNSTABLE", "1") - .stdout("") .run(); } @@ -49,7 +48,6 @@ fn skips_non_executable_files() { .write("hi", "just some regular file") .env("PATH", path.to_str().unwrap()) .env("JUST_UNSTABLE", "1") - .stdout("") .run(); } From 42c4e5ac0d7b8e6e9bb079d92b7b0700dbb542bd Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 22 Jan 2025 10:42:31 -0800 Subject: [PATCH 26/29] Change readme section --- README.md | 53 +++++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index b383d46588..be44441768 100644 --- a/README.md +++ b/README.md @@ -1674,41 +1674,42 @@ set unstable foo := env('FOO') || 'DEFAULT_VALUE' ``` -- `require(exe)`master — Retrieves the full path of `exe` according - to the `PATH`. Throws an error if no executable named `exe` exists. +#### Executables -```just -bash := require("bash") +- `require(name)`master — Search directories in the `PATH` + environment variable for the executable `name` and return its full path, or + halt with an error if no executable with `name` exists. -@test: - echo "bash: '{{bash}}'" -``` + ```just + bash := require("bash") -```console -$ just -bash: '/bin/bash' -``` + @test: + echo "bash: '{{bash}}'" + ``` -- `which(exe)`master — Like `require()`, retrieves the full path of - `exe` according to the `PATH`. However, unlike `require()`, `which()` - returns an empty string if no executable named `exe` exists, instead of - throwing an error. + ```console + $ just + bash: '/bin/bash' + ``` - This feature is currently unstable. +- `which(s)`master — Search directories in the `PATH` environment + variable for the executable `name` and return its full path, or the empty + string if no executable with `name` exists. Currently unstable. -```just -set unstable -nexist := require("nexist") + ```just + set unstable -@test: - echo "nexist: '{{nexist}}'" -``` + bosh := require("bosh") -```console -$ just -nexist: '' -``` + @test: + echo "bosh: '{{bosh}}'" + ``` + + ```console + $ just + bosh: '' + ``` #### Invocation Information From 6ccf803f2ee06e72b7a8cbdc174054094fb52fff Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 22 Jan 2025 10:43:21 -0800 Subject: [PATCH 27/29] Adapt --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index be44441768..d8f7d42519 100644 --- a/README.md +++ b/README.md @@ -1692,7 +1692,7 @@ foo := env('FOO') || 'DEFAULT_VALUE' bash: '/bin/bash' ``` -- `which(s)`master — Search directories in the `PATH` environment +- `which(name)`master — Search directories in the `PATH` environment variable for the executable `name` and return its full path, or the empty string if no executable with `name` exists. Currently unstable. From 1f9abb5b3bb080df8dae542e8011a009ace7ab36 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 22 Jan 2025 10:48:05 -0800 Subject: [PATCH 28/29] Fix Windows test --- tests/test.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test.rs b/tests/test.rs index 39690241b7..78c4e6e5df 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -220,7 +220,8 @@ impl Test { file.display() ); - if !cfg!(windows) { + #[cfg(unix)] + { let perms = std::os::unix::fs::PermissionsExt::from_mode(0o755); fs::set_permissions(file, perms).unwrap(); } From 2f4078053fc857da94288e627a475dc89fb866de Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 22 Jan 2025 10:56:20 -0800 Subject: [PATCH 29/29] Try to fix Windows tests --- tests/which_function.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/which_function.rs b/tests/which_function.rs index 5f4938858a..754075e4c2 100644 --- a/tests/which_function.rs +++ b/tests/which_function.rs @@ -172,9 +172,14 @@ fn handles_absolute_path() { #[test] fn handles_dotslash() { let tmp = tempdir(); - let path = tmp.path().canonicalize().unwrap(); - // canonicalize() is necessary here to account for the justfile prepending - // the canonicalized working directory to './foo.exe'. + + let path = if cfg!(windows) { + tmp.path().into() + } else { + // canonicalize() is necessary here to account for the justfile prepending + // the canonicalized working directory to 'subdir/foo.exe'. + tmp.path().canonicalize().unwrap() + }; Test::with_tempdir(tmp) .justfile("p := which('./foo.exe')") @@ -192,9 +197,14 @@ fn handles_dotslash() { #[test] fn handles_dir_slash() { let tmp = tempdir(); - let path = tmp.path().canonicalize().unwrap(); - // canonicalize() is necessary here to account for the justfile prepending - // the canonicalized working directory to 'subdir/foo.exe'. + + let path = if cfg!(windows) { + tmp.path().into() + } else { + // canonicalize() is necessary here to account for the justfile prepending + // the canonicalized working directory to 'subdir/foo.exe'. + tmp.path().canonicalize().unwrap() + }; Test::with_tempdir(tmp) .justfile("p := which('subdir/foo.exe')")