diff --git a/Cargo.lock b/Cargo.lock index 80a7276d..1ba1463b 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 8a516d99..bda81c5e 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 d0ff455c..cf657782 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 ff9c93b6..25c328b9 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 {