Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add which function for finding executables in PATH #2440

Merged
merged 34 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6fb2400
Add which function for finding executables in PATH
0xzhzh Oct 25, 2024
f1980b5
Change version of which function to master branch
0xzhzh Nov 4, 2024
0c6f5e8
Use internal implementation of which()
0xzhzh Nov 4, 2024
389b2ae
Add tests for internal implementation of which()
0xzhzh Dec 1, 2024
2a535c0
Remove stray addition to justfile
0xzhzh Dec 1, 2024
34f2ea6
Remove dependency on either
0xzhzh Dec 13, 2024
740c0ae
Merge remote-tracking branch 'origin/master' into add-which
casey Dec 20, 2024
c89e182
Handle empty command string up front
0xzhzh Dec 28, 2024
aad3831
Resolve relative paths relative to working directory
0xzhzh Dec 28, 2024
5aa3c07
Clean up implementation of which()
0xzhzh Dec 30, 2024
e7e9d56
Rename which_exec -> which_function
0xzhzh Dec 30, 2024
6a98c39
Use single quotes to avoid r# strings
0xzhzh Dec 30, 2024
1f40ec3
Remove use of temptree! macro
0xzhzh Dec 30, 2024
d642b1d
Add some tests for relative paths
0xzhzh Dec 30, 2024
ea958df
Merge branch 'master' into add-which
0xzhzh Dec 30, 2024
f967dc6
Add lexiclean to which path
0xzhzh Jan 12, 2025
5eaf570
Make HELLO_SCRIPT constant
0xzhzh Jan 12, 2025
1a3ddf4
Merge remote-tracking branch 'origin/master' into add-which
casey Jan 16, 2025
775d769
Sort function list alphabetically
casey Jan 16, 2025
941a5b9
Avoid import
casey Jan 16, 2025
a7fe156
Remove greeting
0xzhzh Jan 17, 2025
0beadec
Add require
0xzhzh Jan 17, 2025
f5c5809
Make which() function unstable
0xzhzh Jan 17, 2025
2ac4966
Merge remote-tracking branch 'origin/master' into add-which
casey Jan 22, 2025
a8e8ea4
Format
0xzhzh Jan 22, 2025
87a315b
Fix tests + add unstable test
0xzhzh Jan 22, 2025
f4501dd
Add documentation
0xzhzh Jan 22, 2025
e63fb7d
Merge remote-tracking branch 'origin/master' into add-which
casey Jan 22, 2025
2e17660
Remove a few unnecessary uses of format!()
casey Jan 22, 2025
86753cb
Expected stdout defaults to empty string
casey Jan 22, 2025
42c4e5a
Change readme section
casey Jan 22, 2025
6ccf803
Adapt
casey Jan 22, 2025
1f9abb5
Fix Windows test
casey Jan 22, 2025
2f40780
Try to fix Windows tests
casey Jan 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dirs = "5.0.1"
dotenvy = "0.15"
edit-distance = "2.0.0"
heck = "0.5.0"
is_executable = "1.0.4"
lexiclean = "0.0.1"
libc = "0.2.0"
num_cpus = "1.15.0"
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1674,6 +1674,43 @@ set unstable
foo := env('FOO') || 'DEFAULT_VALUE'
```

#### Executables

- `require(name)`<sup>master</sup> — 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.

```just
bash := require("bash")

@test:
echo "bash: '{{bash}}'"
```

```console
$ just
bash: '/bin/bash'
```

- `which(name)`<sup>master</sup> — 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

bosh := require("bosh")

@test:
echo "bosh: '{{bosh}}'"
```

```console
$ just
bosh: ''
```

#### Invocation Information

- `is_dependency()` - Returns the string `true` if the current recipe is being
Expand Down
57 changes: 57 additions & 0 deletions src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ pub(crate) fn get(name: &str) -> Option<Function> {
"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),
Expand All @@ -111,6 +112,7 @@ pub(crate) fn get(name: &str) -> Option<Function> {
"uppercamelcase" => Unary(uppercamelcase),
"uppercase" => Unary(uppercase),
"uuid" => Nullary(uuid),
"which" => Unary(which),
"without_extension" => Unary(without_extension),
_ => return None,
};
Expand Down Expand Up @@ -511,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)
Expand Down Expand Up @@ -661,6 +672,52 @@ fn uuid(_context: Context) -> FunctionResult {
Ok(uuid::Uuid::new_v4().to_string())
}

fn which(context: Context, s: &str) -> FunctionResult {
0xzhzh marked this conversation as resolved.
Show resolved Hide resolved
casey marked this conversation as resolved.
Show resolved Hide resolved
let cmd = Path::new(s);

let candidates = match cmd.components().count() {
0 => return Err("empty command".into()),
1 => {
// cmd is a regular command
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))
.collect()
}
_ => {
// cmd contains a path separator, treat it as a path
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")`,
// or because there was a relative path in `PATH`. Resolve it to an absolute path,
// relative to the working directory of the just invocation.
candidate = context
.evaluator
.context
.working_directory()
.join(candidate);
}

candidate = candidate.lexiclean();

if is_executable::is_executable(&candidate) {
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 {
let parent = Utf8Path::new(path)
.parent()
Expand Down
5 changes: 5 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,11 @@ 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)?,
})
Expand Down
2 changes: 2 additions & 0 deletions src/unstable_feature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub(crate) enum UnstableFeature {
LogicalOperators,
ScriptAttribute,
ScriptInterpreterSetting,
WhichFunction,
}

impl Display for UnstableFeature {
Expand All @@ -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."),
}
}
}
1 change: 1 addition & 0 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ mod timestamps;
mod undefined_variables;
mod unexport;
mod unstable;
mod which_function;
#[cfg(windows)]
mod windows;
#[cfg(target_family = "windows")]
Expand Down
23 changes: 23 additions & 0 deletions tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,29 @@ impl Test {
self
}

pub(crate) fn make_executable(self, path: impl AsRef<Path>) -> 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()
);

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<Path>, content: impl AsRef<[u8]>) -> Self {
let path = path.as_ref();
self
Expand Down
Loading
Loading