expand_path requires env vars
diff --git a/src/main.rs b/src/main.rs
index a7582b3..2a4b320 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -171,8 +171,8 @@ impl FromLuaMulti for Symlink {
fn from_lua_multi(args: LuaMultiValue, lua: &Lua) -> LuaResult<Self> {
let (target, source) = <(String, String)>::from_lua_multi(args, lua)?;
Ok(Self {
- target: Utils::expand_path(&target),
- source: Utils::expand_path(&source),
+ target: Utils::expand_path(&target).map_err(LuaError::external)?,
+ source: Utils::expand_path(&source).map_err(LuaError::external)?,
})
}
}
@@ -229,7 +229,7 @@ impl FromLuaMulti for Directory {
fn from_lua_multi(args: LuaMultiValue, lua: &Lua) -> LuaResult<Self> {
let path = String::from_lua_multi(args, lua)?;
Ok(Self {
- path: Utils::expand_path(&path),
+ path: Utils::expand_path(&path).map_err(LuaError::external)?,
})
}
}
diff --git a/src/utils.rs b/src/utils.rs
index 617b33d..1598633 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -16,34 +16,47 @@ impl Utils {
s.trim_end_matches('\n').trim_end_matches('\r').to_string()
}
- pub fn dirname(path: &str) -> Option<String> {
- let expanded_path = Self::expand_path(path);
- let parent = Path::new(&expanded_path).parent()?;
- parent.to_str().map(|s| s.to_string())
+ pub fn dirname(path: &str) -> Result<Option<String>> {
+ let expanded_path = Self::expand_path(path)?;
+ let parent = Path::new(&expanded_path).parent();
+ Ok(parent.and_then(|p| p.to_str().map(|s| s.to_string())))
}
- pub fn expand_path(path: &str) -> String {
+ pub fn expand_path(path: &str) -> Result<String> {
static ENV_VAR_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)").unwrap());
let mut expanded = path.to_string();
// Expand tilde
- if expanded.starts_with('~')
- && let Ok(home) = env::var("HOME")
- {
+ if expanded.starts_with('~') {
+ let home = env::var("HOME").map_err(|_| eyre!("HOME environment variable not set"))?;
expanded = expanded.replacen('~', &home, 1);
}
- // Expand environment variables using regex
+ // Expand environment variables
+ let mut missing = Vec::new();
expanded = ENV_VAR_REGEX
.replace_all(&expanded, |caps: ®ex::Captures| {
let var_name = caps.get(1).unwrap().as_str();
- env::var(var_name).unwrap_or_else(|_| caps.get(0).unwrap().as_str().to_string())
+ match env::var(var_name) {
+ Ok(value) => value,
+ Err(_) => {
+ missing.push(var_name.to_string());
+ caps.get(0).unwrap().as_str().to_string()
+ }
+ }
})
.to_string();
- expanded
+ if !missing.is_empty() {
+ return Err(eyre!(
+ "Environment variables not found: {}",
+ missing.join(", ")
+ ));
+ }
+
+ Ok(expanded)
}
pub fn sh(cmd: &str, args: &[String]) -> Result<(String, i32)> {
@@ -73,11 +86,15 @@ impl IntoLua for Utils {
utils_table.set(
"expand_path",
- lua.create_function(|_lua, path: String| Ok(Utils::expand_path(&path)))?,
+ lua.create_function(|_lua, path: String| {
+ Utils::expand_path(&path).map_err(LuaError::external)
+ })?,
)?;
utils_table.set(
"dirname",
- lua.create_function(|_lua, path: String| Ok(Utils::dirname(&path)))?,
+ lua.create_function(|_lua, path: String| {
+ Utils::dirname(&path).map_err(LuaError::external)
+ })?,
)?;
utils_table.set(
"chomp",
@@ -109,3 +126,123 @@ impl IntoLua for Utils {
Ok(LuaValue::Table(utils_table))
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::env;
+
+ #[test]
+ fn test_expand_path_no_variables() {
+ let result = Utils::expand_path("/path/to/file");
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), "/path/to/file");
+ }
+
+ #[test]
+ fn test_expand_path_with_tilde() {
+ let home = env::var("HOME").unwrap();
+ let result = Utils::expand_path("~/file");
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), format!("{}/file", home));
+ }
+
+ #[test]
+ fn test_expand_path_with_multiple_env_vars() {
+ unsafe {
+ env::set_var("TEST_VAR1", "value1");
+ env::set_var("TEST_VAR2", "value2");
+ }
+ let result = Utils::expand_path("/$TEST_VAR1/path/$TEST_VAR2/file");
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), "/value1/path/value2/file");
+ unsafe {
+ env::remove_var("TEST_VAR1");
+ env::remove_var("TEST_VAR2");
+ }
+ }
+
+ #[test]
+ fn test_expand_path_with_multiple_missing_env_vars() {
+ let result = Utils::expand_path("/$MISSING1/path/$MISSING2/file");
+ assert!(result.is_err());
+ let error = result.unwrap_err();
+ let error_str = error.to_string();
+ assert!(error_str.contains("Environment variables not found:"));
+ assert!(error_str.contains("MISSING1"));
+ assert!(error_str.contains("MISSING2"));
+ }
+
+ #[test]
+ fn test_expand_path_mixed_existing_and_missing() {
+ unsafe {
+ env::set_var("EXISTING_VAR", "exists");
+ }
+ let result = Utils::expand_path("/$EXISTING_VAR/path/$MISSING_VAR/file");
+ assert!(result.is_err());
+ let error = result.unwrap_err();
+ assert!(
+ error
+ .to_string()
+ .contains("Environment variables not found: MISSING_VAR")
+ );
+ unsafe {
+ env::remove_var("EXISTING_VAR");
+ }
+ }
+
+ #[test]
+ fn test_expand_path_tilde_and_env_var() {
+ let home = env::var("HOME").unwrap();
+ unsafe {
+ env::set_var("TEST_VAR", "test");
+ }
+ let result = Utils::expand_path("~/$TEST_VAR/file");
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), format!("{}/test/file", home));
+ unsafe {
+ env::remove_var("TEST_VAR");
+ }
+ }
+
+ #[test]
+ fn test_sh_through_lua() {
+ let lua = mlua::Lua::new();
+ let utils = Utils {};
+ let utils_table = utils.into_lua(&lua).unwrap();
+
+ lua.globals().set("utils", utils_table).unwrap();
+
+ // Test successful command
+ let result: (Option<String>, i32) = lua
+ .load(r#"return utils.sh("echo", "hello", "world")"#)
+ .eval()
+ .unwrap();
+
+ assert_eq!(result.0.unwrap().trim(), "hello world");
+ assert_eq!(result.1, 0);
+
+ // Test command with non-zero exit
+ let result: (Option<String>, i32) = lua.load(r#"return utils.sh("false")"#).eval().unwrap();
+
+ assert_eq!(result.1, 1);
+ }
+
+ #[test]
+ fn test_sh_through_lua_invalid_arguments() {
+ let lua = mlua::Lua::new();
+ let utils = Utils {};
+ let utils_table = utils.into_lua(&lua).unwrap();
+
+ lua.globals().set("utils", utils_table).unwrap();
+
+ // Test with no arguments - should fail
+ let result = lua
+ .load(r#"return utils.sh()"#)
+ .eval::<(Option<String>, i32)>();
+ assert!(result.is_err());
+
+ let error = result.unwrap_err();
+ assert!(error.to_string().contains("sh requires at least a command"));
+ }
+}