Repository: casey/just
Pull request #882 (2021-06-24 22:50:57)

Add `join` function for joining paths


Patch:
diff --git a/README.adoc b/README.adoc
index 17a7ef00..1b00187b 100644
--- a/README.adoc
+++ b/README.adoc
@@ -794,14 +794,22 @@ Starting server with database localhost:6379 on port 1337...
 
 ==== Path Manipulation
 
+===== Fallible
+
+- `extension(path)` - Extension of `path`. `extension("/foo/bar.txt")` is `txt`.
 - `file_name(path)` - File name of `path` with any leading directory components removed. `file_name("/foo/bar.txt")` is `bar.txt`.
-- `parent_directory(path)` - Parent directory of `path`. `parent_directory("/foo/bar.txt")` is `/foo`.
 - `file_stem(path)` - File name of `path` without extension. `file_stem("/foo/bar.txt")` is `bar`.
+- `parent_directory(path)` - Parent directory of `path`. `parent_directory("/foo/bar.txt")` is `/foo`.
 - `without_extension(path)` - `path` without extension. `without_extension("/foo/bar.txt")` is `/foo/bar`.
-- `extension(path)` - Extension of `path`. `extension("/foo/bar.txt")` is `txt`.
 
 These functions can fail, for example if a path does not have an extension, which will halt execution.
 
+===== Infallible
+
+- `join(a, b)` - Join path `a` with path `b`. `join("foo/bar", "baz")` is `foo/bar/baz`.
+
+These functions always succeed.
+
 === Command Evaluation Using Backticks
 
 Backticks can be used to store the result of commands:
diff --git a/src/function.rs b/src/function.rs
index 36127313..eabeb381 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -10,19 +10,20 @@ pub(crate) enum Function {
 lazy_static! {
   pub(crate) static ref TABLE: BTreeMap<&'static str, Function> = vec![
     ("arch", Nullary(arch)),
-    ("os", Nullary(os)),
-    ("os_family", Nullary(os_family)),
-    ("justfile_directory", Nullary(justfile_directory)),
-    ("justfile", Nullary(justfile)),
-    ("invocation_directory", Nullary(invocation_directory)),
     ("env_var", Unary(env_var)),
     ("env_var_or_default", Binary(env_var_or_default)),
-    ("just_executable", Nullary(just_executable)),
+    ("extension", Unary(extension)),
     ("file_name", Unary(file_name)),
-    ("parent_directory", Unary(parent_directory)),
     ("file_stem", Unary(file_stem)),
+    ("invocation_directory", Nullary(invocation_directory)),
+    ("join", Binary(join)),
+    ("just_executable", Nullary(just_executable)),
+    ("justfile", Nullary(justfile)),
+    ("justfile_directory", Nullary(justfile_directory)),
+    ("os", Nullary(os)),
+    ("os_family", Nullary(os_family)),
+    ("parent_directory", Unary(parent_directory)),
     ("without_extension", Unary(without_extension)),
-    ("extension", Unary(extension))
   ]
   .into_iter()
   .collect();
@@ -42,55 +43,6 @@ fn arch(_context: &FunctionContext) -> Result<String, String> {
   Ok(target::arch().to_owned())
 }
 
-fn os(_context: &FunctionContext) -> Result<String, String> {
-  Ok(target::os().to_owned())
-}
-
-fn os_family(_context: &FunctionContext) -> Result<String, String> {
-  Ok(target::os_family().to_owned())
-}
-
-fn invocation_directory(context: &FunctionContext) -> Result<String, String> {
-  Platform::convert_native_path(
-    &context.search.working_directory,
-    context.invocation_directory,
-  )
-  .map_err(|e| format!("Error getting shell path: {}", e))
-}
-
-fn justfile(context: &FunctionContext) -> Result<String, String> {
-  context
-    .search
-    .justfile
-    .to_str()
-    .map(str::to_owned)
-    .ok_or_else(|| {
-      format!(
-        "Justfile path is not valid unicode: {}",
-        context.search.justfile.to_string_lossy()
-      )
-    })
-}
-
-fn justfile_directory(context: &FunctionContext) -> Result<String, String> {
-  let justfile_directory = context.search.justfile.parent().ok_or_else(|| {
-    format!(
-      "Could not resolve justfile directory. Justfile `{}` had no parent.",
-      context.search.justfile.display()
-    )
-  })?;
-
-  justfile_directory
-    .to_str()
-    .map(str::to_owned)
-    .ok_or_else(|| {
-      format!(
-        "Justfile directory is not valid unicode: {}",
-        justfile_directory.to_string_lossy()
-      )
-    })
-}
-
 fn env_var(context: &FunctionContext, key: &str) -> Result<String, String> {
   use std::env::VarError::*;
 
@@ -129,6 +81,39 @@ fn env_var_or_default(
   }
 }
 
+fn extension(_context: &FunctionContext, path: &str) -> Result<String, String> {
+  Utf8Path::new(path)
+    .extension()
+    .map(str::to_owned)
+    .ok_or_else(|| format!("Could not extract extension from `{}`", path))
+}
+
+fn file_name(_context: &FunctionContext, path: &str) -> Result<String, String> {
+  Utf8Path::new(path)
+    .file_name()
+    .map(str::to_owned)
+    .ok_or_else(|| format!("Could not extract file name from `{}`", path))
+}
+
+fn file_stem(_context: &FunctionContext, path: &str) -> Result<String, String> {
+  Utf8Path::new(path)
+    .file_stem()
+    .map(str::to_owned)
+    .ok_or_else(|| format!("Could not extract file stem from `{}`", path))
+}
+
+fn invocation_directory(context: &FunctionContext) -> Result<String, String> {
+  Platform::convert_native_path(
+    &context.search.working_directory,
+    context.invocation_directory,
+  )
+  .map_err(|e| format!("Error getting shell path: {}", e))
+}
+
+fn join(_context: &FunctionContext, base: &str, with: &str) -> Result<String, String> {
+  Ok(Utf8Path::new(base).join(with).to_string())
+}
+
 fn just_executable(_context: &FunctionContext) -> Result<String, String> {
   let exe_path =
     std::env::current_exe().map_err(|e| format!("Error getting current executable: {}", e))?;
@@ -141,11 +126,45 @@ fn just_executable(_context: &FunctionContext) -> Result<String, String> {
   })
 }
 
-fn file_name(_context: &FunctionContext, path: &str) -> Result<String, String> {
-  Utf8Path::new(path)
-    .file_name()
+fn justfile(context: &FunctionContext) -> Result<String, String> {
+  context
+    .search
+    .justfile
+    .to_str()
     .map(str::to_owned)
-    .ok_or_else(|| format!("Could not extract file name from `{}`", path))
+    .ok_or_else(|| {
+      format!(
+        "Justfile path is not valid unicode: {}",
+        context.search.justfile.to_string_lossy()
+      )
+    })
+}
+
+fn justfile_directory(context: &FunctionContext) -> Result<String, String> {
+  let justfile_directory = context.search.justfile.parent().ok_or_else(|| {
+    format!(
+      "Could not resolve justfile directory. Justfile `{}` had no parent.",
+      context.search.justfile.display()
+    )
+  })?;
+
+  justfile_directory
+    .to_str()
+    .map(str::to_owned)
+    .ok_or_else(|| {
+      format!(
+        "Justfile directory is not valid unicode: {}",
+        justfile_directory.to_string_lossy()
+      )
+    })
+}
+
+fn os(_context: &FunctionContext) -> Result<String, String> {
+  Ok(target::os().to_owned())
+}
+
+fn os_family(_context: &FunctionContext) -> Result<String, String> {
+  Ok(target::os_family().to_owned())
 }
 
 fn parent_directory(_context: &FunctionContext, path: &str) -> Result<String, String> {
@@ -155,13 +174,6 @@ fn parent_directory(_context: &FunctionContext, path: &str) -> Result<String, St
     .ok_or_else(|| format!("Could not extract parent directory from `{}`", path))
 }
 
-fn file_stem(_context: &FunctionContext, path: &str) -> Result<String, String> {
-  Utf8Path::new(path)
-    .file_stem()
-    .map(str::to_owned)
-    .ok_or_else(|| format!("Could not extract file stem from `{}`", path))
-}
-
 fn without_extension(_context: &FunctionContext, path: &str) -> Result<String, String> {
   let parent = Utf8Path::new(path)
     .parent()
@@ -173,10 +185,3 @@ fn without_extension(_context: &FunctionContext, path: &str) -> Result<String, S
 
   Ok(parent.join(file_stem).to_string())
 }
-
-fn extension(_context: &FunctionContext, path: &str) -> Result<String, String> {
-  Utf8Path::new(path)
-    .extension()
-    .map(str::to_owned)
-    .ok_or_else(|| format!("Could not extract extension from `{}`", path))
-}


Test patch:
diff --git a/tests/misc.rs b/tests/misc.rs
index d6389051..ac933179 100644
--- a/tests/misc.rs
+++ b/tests/misc.rs
@@ -1179,12 +1179,13 @@ fs  := file_stem('/foo/bar/baz.hello')
 fn  := file_name('/foo/bar/baz.hello')
 dir := parent_directory('/foo/bar/baz.hello')
 ext := extension('/foo/bar/baz.hello')
+jn  := join('a', 'b')
 
 foo:
-  /bin/echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}'
+  /bin/echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}' '{{jn}}'
 "#,
-  stdout:   "/foo/bar/baz baz baz.hello /foo/bar hello\n",
-  stderr:   "/bin/echo '/foo/bar/baz' 'baz' 'baz.hello' '/foo/bar' 'hello'\n",
+  stdout:   "/foo/bar/baz baz baz.hello /foo/bar hello a/b\n",
+  stderr:   "/bin/echo '/foo/bar/baz' 'baz' 'baz.hello' '/foo/bar' 'hello' 'a/b'\n",
 }
 
 #[cfg(not(windows))]