Skip to content

Commit

Permalink
Add which function for finding executables in PATH
Browse files Browse the repository at this point in the history
Closes casey#2109 (but with a function name that is shorter and more familiar)
  • Loading branch information
0xzhzh committed Oct 25, 2024
1 parent 4f31853 commit 6fb2400
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1597,6 +1597,22 @@ $ just
name `key`, returning `default` if it is not present.
- `env(key)`<sup>1.15.0</sup> — Alias for `env_var(key)`.
- `env(key, default)`<sup>1.15.0</sup> — Alias for `env_var_or_default(key, default)`.
- `which(exe)`<sup>1.37.0</sup> — 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

Expand Down
11 changes: 11 additions & 0 deletions src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pub(crate) fn get(name: &str) -> Option<Function> {
"uppercase" => Unary(uppercase),
"uuid" => Nullary(uuid),
"without_extension" => Unary(without_extension),
"which" => Unary(which_exec),
_ => return None,
};
Some(function)
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ mod timestamps;
mod undefined_variables;
mod unexport;
mod unstable;
mod which_exec;
#[cfg(windows)]
mod windows;
#[cfg(target_family = "windows")]
Expand Down
42 changes: 42 additions & 0 deletions tests/which_exec.rs
Original file line number Diff line number Diff line change
@@ -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();
}

0 comments on commit 6fb2400

Please sign in to comment.