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')")