diff --git a/src/function.rs b/src/function.rs index d5b9f192bc..1646889113 100644 --- a/src/function.rs +++ b/src/function.rs @@ -513,13 +513,8 @@ 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 require(context: Context, name: &str) -> FunctionResult { + crate::which(context, name)?.ok_or_else(|| format!("could not find executable `{name}`")) } fn replace_regex(_context: Context, s: &str, regex: &str, replacement: &str) -> FunctionResult { @@ -672,50 +667,8 @@ fn uuid(_context: Context) -> FunctionResult { Ok(uuid::Uuid::new_v4().to_string()) } -fn which(context: Context, s: &str) -> FunctionResult { - 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 which(context: Context, name: &str) -> FunctionResult { + Ok(crate::which(context, name)?.unwrap_or_default()) } fn without_extension(_context: Context, path: &str) -> FunctionResult { diff --git a/src/lib.rs b/src/lib.rs index 30777e513d..6820a3c7be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,6 +97,7 @@ pub(crate) use { variables::Variables, verbosity::Verbosity, warning::Warning, + which::which, }, camino::Utf8Path, clap::ValueEnum, @@ -273,3 +274,4 @@ mod use_color; mod variables; mod verbosity; mod warning; +mod which; diff --git a/src/which.rs b/src/which.rs new file mode 100644 index 0000000000..1a1e4de0a7 --- /dev/null +++ b/src/which.rs @@ -0,0 +1,48 @@ +use super::*; + +pub(crate) fn which(context: function::Context, name: &str) -> Result, String> { + let name = Path::new(name); + + let candidates = match name.components().count() { + 0 => return Err("empty command".into()), + 1 => { + // cmd is a regular command + env::split_paths(&env::var_os("PATH").ok_or("`PATH` environment variable not set")?) + .map(|path| path.join(name)) + .collect() + } + _ => { + // cmd contains a path separator, treat it as a path + vec![name.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(|candidate| Some(candidate.into())) + .ok_or_else(|| { + format!( + "Executable path is not valid unicode: {}", + candidate.display() + ) + }); + } + } + + Ok(None) +} diff --git a/tests/which_function.rs b/tests/which_function.rs index 754075e4c2..31993fc428 100644 --- a/tests/which_function.rs +++ b/tests/which_function.rs @@ -231,6 +231,39 @@ fn is_unstable() { .make_executable("hello.exe") .env("PATH", path.to_str().unwrap()) .stderr_regex(r".*The `which\(\)` function is currently unstable\..*") - .status(1) + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn require_error() { + Test::new() + .justfile("p := require('asdfasdf')") + .args(["--evaluate", "p"]) + .stderr( + " + error: Call to function `require` failed: could not find executable `asdfasdf` + ——▶ justfile:1:6 + │ + 1 │ p := require('asdfasdf') + │ ^^^^^^^ + ", + ) + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn require_success() { + let tmp = tempdir(); + let path = PathBuf::from(tmp.path()); + + Test::with_tempdir(tmp) + .justfile("p := require('hello.exe')") + .args(["--evaluate", "p"]) + .write("hello.exe", HELLO_SCRIPT) + .make_executable("hello.exe") + .env("PATH", path.to_str().unwrap()) + .stdout(path.join("hello.exe").display().to_string()) .run(); }