Repository: casey/just
Pull request #883 (2021-06-25 06:34:12)

Add `clean(path)` function


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index ff59b765..0b1e92c1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -207,6 +207,7 @@ dependencies = [
  "env_logger",
  "executable-path",
  "lazy_static",
+ "lexiclean",
  "libc",
  "log",
  "pretty_assertions",
@@ -227,6 +228,12 @@ version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
+[[package]]
+name = "lexiclean"
+version = "0.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "441225017b106b9f902e97947a6d31e44ebcf274b91bdbfb51e5c477fcd468e5"
+
 [[package]]
 name = "libc"
 version = "0.2.97"
diff --git a/Cargo.toml b/Cargo.toml
index fa15e960..dc126400 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,7 @@ dotenv        = "0.15.0"
 edit-distance = "2.0.0"
 env_logger    = "0.8.0"
 lazy_static   = "1.0.0"
+lexiclean     = "0.0.1"
 libc          = "0.2.0"
 log           = "0.4.4"
 snafu         = "0.6.0"
diff --git a/README.adoc b/README.adoc
index 1b00187b..e82689ec 100644
--- a/README.adoc
+++ b/README.adoc
@@ -807,8 +807,7 @@ These functions can fail, for example if a path does not have an extension, whic
 ===== Infallible
 
 - `join(a, b)` - Join path `a` with path `b`. `join("foo/bar", "baz")` is `foo/bar/baz`.
-
-These functions always succeed.
+- `clean(path)` - Simplify `path` by removing extra path separators, intermediate `.` components, and `..` where possible. `clean("foo//bar")` is `foo/bar`, `clean("foo/..")` is `.`, `clean("foo/./bar")` is `foo/bar`.
 
 === Command Evaluation Using Backticks
 
diff --git a/src/common.rs b/src/common.rs
index fc6ea43c..c27d3f86 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -22,6 +22,7 @@ pub(crate) use std::{
 pub(crate) use camino::Utf8Path;
 pub(crate) use derivative::Derivative;
 pub(crate) use edit_distance::edit_distance;
+pub(crate) use lexiclean::Lexiclean;
 pub(crate) use libc::EXIT_FAILURE;
 pub(crate) use log::{info, warn};
 pub(crate) use snafu::{ResultExt, Snafu};
diff --git a/src/function.rs b/src/function.rs
index eabeb381..c0cc9db3 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -10,6 +10,7 @@ pub(crate) enum Function {
 lazy_static! {
   pub(crate) static ref TABLE: BTreeMap<&'static str, Function> = vec![
     ("arch", Nullary(arch)),
+    ("clean", Unary(clean)),
     ("env_var", Unary(env_var)),
     ("env_var_or_default", Binary(env_var_or_default)),
     ("extension", Unary(extension)),
@@ -43,6 +44,10 @@ fn arch(_context: &FunctionContext) -> Result<String, String> {
   Ok(target::arch().to_owned())
 }
 
+fn clean(_context: &FunctionContext, path: &str) -> Result<String, String> {
+  Ok(Path::new(path).lexiclean().to_str().unwrap().to_owned())
+}
+
 fn env_var(context: &FunctionContext, key: &str) -> Result<String, String> {
   use std::env::VarError::*;
 


Test patch:
diff --git a/tests/functions.rs b/tests/functions.rs
new file mode 100644
index 00000000..d4bcd27f
--- /dev/null
+++ b/tests/functions.rs
@@ -0,0 +1,264 @@
+use crate::common::*;
+
+test! {
+  name:     test_os_arch_functions_in_interpolation,
+  justfile: r#"
+foo:
+  echo {{arch()}} {{os()}} {{os_family()}}
+"#,
+  stdout:   format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
+  stderr:   format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
+}
+
+test! {
+  name:     test_os_arch_functions_in_expression,
+  justfile: r#"
+a := arch()
+o := os()
+f := os_family()
+
+foo:
+  echo {{a}} {{o}} {{f}}
+"#,
+  stdout:   format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
+  stderr:   format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
+}
+
+#[cfg(not(windows))]
+test! {
+  name:     env_var_functions,
+  justfile: r#"
+p := env_var('USER')
+b := env_var_or_default('ZADDY', 'HTAP')
+x := env_var_or_default('XYZ', 'ABC')
+
+foo:
+  /bin/echo '{{p}}' '{{b}}' '{{x}}'
+"#,
+  stdout:   format!("{} HTAP ABC\n", env::var("USER").unwrap()).as_str(),
+  stderr:   format!("/bin/echo '{}' 'HTAP' 'ABC'\n", env::var("USER").unwrap()).as_str(),
+}
+
+#[cfg(not(windows))]
+test! {
+  name: path_functions,
+  justfile: r#"
+we  := without_extension('/foo/bar/baz.hello')
+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}}' '{{jn}}'
+"#,
+  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))]
+test! {
+  name: path_functions2,
+  justfile: r#"
+we  := without_extension('/foo/bar/baz')
+fs  := file_stem('/foo/bar/baz.hello.ciao')
+fn  := file_name('/bar/baz.hello.ciao')
+dir := parent_directory('/foo/')
+ext := extension('/foo/bar/baz.hello.ciao')
+
+foo:
+  /bin/echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}'
+"#,
+  stdout:   "/foo/bar/baz baz.hello baz.hello.ciao / ciao\n",
+  stderr:   "/bin/echo '/foo/bar/baz' 'baz.hello' 'baz.hello.ciao' '/' 'ciao'\n",
+}
+
+#[cfg(not(windows))]
+test! {
+  name: broken_without_extension_function,
+  justfile: r#"
+we  := without_extension('')
+
+foo:
+  /bin/echo '{{we}}'
+"#,
+  stdout:   "",
+  stderr:   format!("{} {}\n{}\n{}\n{}\n",
+    "error: Call to function `without_extension` failed:",
+    "Could not extract parent from ``",
+    "  |",
+    "1 | we  := without_extension(\'\')",
+    "  |        ^^^^^^^^^^^^^^^^^").as_str(),
+  status:   EXIT_FAILURE,
+}
+
+#[cfg(not(windows))]
+test! {
+  name: broken_extension_function,
+  justfile: r#"
+we  := extension('')
+
+foo:
+  /bin/echo '{{we}}'
+"#,
+  stdout:   "",
+  stderr:   format!("{}\n{}\n{}\n{}\n",
+    "error: Call to function `extension` failed: Could not extract extension from ``",
+    "  |",
+    "1 | we  := extension(\'\')",
+    "  |        ^^^^^^^^^").as_str(),
+  status:   EXIT_FAILURE,
+}
+
+#[cfg(not(windows))]
+test! {
+  name: broken_extension_function2,
+  justfile: r#"
+we  := extension('foo')
+
+foo:
+  /bin/echo '{{we}}'
+"#,
+  stdout:   "",
+  stderr:   format!("{}\n{}\n{}\n{}\n",
+    "error: Call to function `extension` failed: Could not extract extension from `foo`",
+    "  |",
+    "1 | we  := extension(\'foo\')",
+    "  |        ^^^^^^^^^").as_str(),
+  status:   EXIT_FAILURE,
+}
+
+#[cfg(not(windows))]
+test! {
+  name: broken_file_stem_function,
+  justfile: r#"
+we  := file_stem('')
+
+foo:
+  /bin/echo '{{we}}'
+"#,
+  stdout:   "",
+  stderr:   format!("{}\n{}\n{}\n{}\n",
+    "error: Call to function `file_stem` failed: Could not extract file stem from ``",
+    "  |",
+    "1 | we  := file_stem(\'\')",
+    "  |        ^^^^^^^^^").as_str(),
+  status:   EXIT_FAILURE,
+}
+
+#[cfg(not(windows))]
+test! {
+  name: broken_file_name_function,
+  justfile: r#"
+we  := file_name('')
+
+foo:
+  /bin/echo '{{we}}'
+"#,
+  stdout:   "",
+  stderr:   format!("{}\n{}\n{}\n{}\n",
+    "error: Call to function `file_name` failed: Could not extract file name from ``",
+    "  |",
+    "1 | we  := file_name(\'\')",
+    "  |        ^^^^^^^^^").as_str(),
+  status:   EXIT_FAILURE,
+}
+
+#[cfg(not(windows))]
+test! {
+  name: broken_directory_function,
+  justfile: r#"
+we  := parent_directory('')
+
+foo:
+  /bin/echo '{{we}}'
+"#,
+  stdout:   "",
+  stderr:   format!("{} {}\n{}\n{}\n{}\n",
+    "error: Call to function `parent_directory` failed:",
+    "Could not extract parent directory from ``",
+    "  |",
+    "1 | we  := parent_directory(\'\')",
+    "  |        ^^^^^^^^^^^^^^^^").as_str(),
+  status:   EXIT_FAILURE,
+}
+
+#[cfg(not(windows))]
+test! {
+  name: broken_directory_function2,
+  justfile: r#"
+we  := parent_directory('/')
+
+foo:
+  /bin/echo '{{we}}'
+"#,
+  stdout:   "",
+  stderr:   format!("{} {}\n{}\n{}\n{}\n",
+    "error: Call to function `parent_directory` failed:",
+    "Could not extract parent directory from `/`",
+    "  |",
+    "1 | we  := parent_directory(\'/\')",
+    "  |        ^^^^^^^^^^^^^^^^").as_str(),
+  status:   EXIT_FAILURE,
+}
+
+#[cfg(windows)]
+test! {
+  name:     env_var_functions,
+  justfile: r#"
+p := env_var('USERNAME')
+b := env_var_or_default('ZADDY', 'HTAP')
+x := env_var_or_default('XYZ', 'ABC')
+
+foo:
+  /bin/echo '{{p}}' '{{b}}' '{{x}}'
+"#,
+  stdout:   format!("{} HTAP ABC\n", env::var("USERNAME").unwrap()).as_str(),
+  stderr:   format!("/bin/echo '{}' 'HTAP' 'ABC'\n", env::var("USERNAME").unwrap()).as_str(),
+}
+
+test! {
+  name:     env_var_failure,
+  justfile: "a:\n  echo {{env_var('ZADDY')}}",
+  args:     ("a"),
+  stdout:   "",
+  stderr:   "error: Call to function `env_var` failed: environment variable `ZADDY` not present
+  |
+2 |   echo {{env_var('ZADDY')}}
+  |          ^^^^^^^
+",
+  status:   EXIT_FAILURE,
+}
+
+test! {
+  name:     test_just_executable_function,
+  justfile: "
+    a:
+      @printf 'Executable path is: %s\\n' '{{ just_executable() }}'
+  ",
+  args:     ("a"),
+  stdout:   format!("Executable path is: {}\n", executable_path("just").to_str().unwrap()).as_str(),
+  stderr:   "",
+  status:   EXIT_SUCCESS,
+}
+
+test! {
+  name:     test_os_arch_functions_in_default,
+  justfile: r#"
+foo a=arch() o=os() f=os_family():
+  echo {{a}} {{o}} {{f}}
+"#,
+  stdout:   format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
+  stderr:   format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
+}
+
+test! {
+  name: clean,
+  justfile: "
+    foo:
+      echo {{ clean('a/../b') }}
+  ",
+  stdout: "b\n",
+  stderr: "echo b\n",
+}
diff --git a/tests/lib.rs b/tests/lib.rs
index 61b3f8da..237279bc 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -15,6 +15,7 @@ mod evaluate;
 mod examples;
 mod export;
 mod fmt;
+mod functions;
 mod init;
 mod interrupts;
 mod invocation_directory;
diff --git a/tests/misc.rs b/tests/misc.rs
index ac933179..76f47262 100644
--- a/tests/misc.rs
+++ b/tests/misc.rs
@@ -1131,249 +1131,6 @@ foo:
   stderr:   "echo abc\n",
 }
 
-test! {
-  name:     test_os_arch_functions_in_interpolation,
-  justfile: r#"
-foo:
-  echo {{arch()}} {{os()}} {{os_family()}}
-"#,
-  stdout:   format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
-  stderr:   format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
-}
-
-test! {
-  name:     test_os_arch_functions_in_expression,
-  justfile: r#"
-a := arch()
-o := os()
-f := os_family()
-
-foo:
-  echo {{a}} {{o}} {{f}}
-"#,
-  stdout:   format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
-  stderr:   format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
-}
-
-#[cfg(not(windows))]
-test! {
-  name:     env_var_functions,
-  justfile: r#"
-p := env_var('USER')
-b := env_var_or_default('ZADDY', 'HTAP')
-x := env_var_or_default('XYZ', 'ABC')
-
-foo:
-  /bin/echo '{{p}}' '{{b}}' '{{x}}'
-"#,
-  stdout:   format!("{} HTAP ABC\n", env::var("USER").unwrap()).as_str(),
-  stderr:   format!("/bin/echo '{}' 'HTAP' 'ABC'\n", env::var("USER").unwrap()).as_str(),
-}
-
-#[cfg(not(windows))]
-test! {
-  name: path_functions,
-  justfile: r#"
-we  := without_extension('/foo/bar/baz.hello')
-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}}' '{{jn}}'
-"#,
-  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))]
-test! {
-  name: path_functions2,
-  justfile: r#"
-we  := without_extension('/foo/bar/baz')
-fs  := file_stem('/foo/bar/baz.hello.ciao')
-fn  := file_name('/bar/baz.hello.ciao')
-dir := parent_directory('/foo/')
-ext := extension('/foo/bar/baz.hello.ciao')
-
-foo:
-  /bin/echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}'
-"#,
-  stdout:   "/foo/bar/baz baz.hello baz.hello.ciao / ciao\n",
-  stderr:   "/bin/echo '/foo/bar/baz' 'baz.hello' 'baz.hello.ciao' '/' 'ciao'\n",
-}
-
-#[cfg(not(windows))]
-test! {
-  name: broken_without_extension_function,
-  justfile: r#"
-we  := without_extension('')
-
-foo:
-  /bin/echo '{{we}}'
-"#,
-  stdout:   "",
-  stderr:   format!("{} {}\n{}\n{}\n{}\n",
-    "error: Call to function `without_extension` failed:",
-    "Could not extract parent from ``",
-    "  |",
-    "1 | we  := without_extension(\'\')",
-    "  |        ^^^^^^^^^^^^^^^^^").as_str(),
-  status:   EXIT_FAILURE,
-}
-
-#[cfg(not(windows))]
-test! {
-  name: broken_extension_function,
-  justfile: r#"
-we  := extension('')
-
-foo:
-  /bin/echo '{{we}}'
-"#,
-  stdout:   "",
-  stderr:   format!("{}\n{}\n{}\n{}\n",
-    "error: Call to function `extension` failed: Could not extract extension from ``",
-    "  |",
-    "1 | we  := extension(\'\')",
-    "  |        ^^^^^^^^^").as_str(),
-  status:   EXIT_FAILURE,
-}
-
-#[cfg(not(windows))]
-test! {
-  name: broken_extension_function2,
-  justfile: r#"
-we  := extension('foo')
-
-foo:
-  /bin/echo '{{we}}'
-"#,
-  stdout:   "",
-  stderr:   format!("{}\n{}\n{}\n{}\n",
-    "error: Call to function `extension` failed: Could not extract extension from `foo`",
-    "  |",
-    "1 | we  := extension(\'foo\')",
-    "  |        ^^^^^^^^^").as_str(),
-  status:   EXIT_FAILURE,
-}
-
-#[cfg(not(windows))]
-test! {
-  name: broken_file_stem_function,
-  justfile: r#"
-we  := file_stem('')
-
-foo:
-  /bin/echo '{{we}}'
-"#,
-  stdout:   "",
-  stderr:   format!("{}\n{}\n{}\n{}\n",
-    "error: Call to function `file_stem` failed: Could not extract file stem from ``",
-    "  |",
-    "1 | we  := file_stem(\'\')",
-    "  |        ^^^^^^^^^").as_str(),
-  status:   EXIT_FAILURE,
-}
-
-#[cfg(not(windows))]
-test! {
-  name: broken_file_name_function,
-  justfile: r#"
-we  := file_name('')
-
-foo:
-  /bin/echo '{{we}}'
-"#,
-  stdout:   "",
-  stderr:   format!("{}\n{}\n{}\n{}\n",
-    "error: Call to function `file_name` failed: Could not extract file name from ``",
-    "  |",
-    "1 | we  := file_name(\'\')",
-    "  |        ^^^^^^^^^").as_str(),
-  status:   EXIT_FAILURE,
-}
-
-#[cfg(not(windows))]
-test! {
-  name: broken_directory_function,
-  justfile: r#"
-we  := parent_directory('')
-
-foo:
-  /bin/echo '{{we}}'
-"#,
-  stdout:   "",
-  stderr:   format!("{} {}\n{}\n{}\n{}\n",
-    "error: Call to function `parent_directory` failed:",
-    "Could not extract parent directory from ``",
-    "  |",
-    "1 | we  := parent_directory(\'\')",
-    "  |        ^^^^^^^^^^^^^^^^").as_str(),
-  status:   EXIT_FAILURE,
-}
-
-#[cfg(not(windows))]
-test! {
-  name: broken_directory_function2,
-  justfile: r#"
-we  := parent_directory('/')
-
-foo:
-  /bin/echo '{{we}}'
-"#,
-  stdout:   "",
-  stderr:   format!("{} {}\n{}\n{}\n{}\n",
-    "error: Call to function `parent_directory` failed:",
-    "Could not extract parent directory from `/`",
-    "  |",
-    "1 | we  := parent_directory(\'/\')",
-    "  |        ^^^^^^^^^^^^^^^^").as_str(),
-  status:   EXIT_FAILURE,
-}
-
-#[cfg(windows)]
-test! {
-  name:     env_var_functions,
-  justfile: r#"
-p := env_var('USERNAME')
-b := env_var_or_default('ZADDY', 'HTAP')
-x := env_var_or_default('XYZ', 'ABC')
-
-foo:
-  /bin/echo '{{p}}' '{{b}}' '{{x}}'
-"#,
-  stdout:   format!("{} HTAP ABC\n", env::var("USERNAME").unwrap()).as_str(),
-  stderr:   format!("/bin/echo '{}' 'HTAP' 'ABC'\n", env::var("USERNAME").unwrap()).as_str(),
-}
-
-test! {
-  name:     env_var_failure,
-  justfile: "a:\n  echo {{env_var('ZADDY')}}",
-  args:     ("a"),
-  stdout:   "",
-  stderr:   "error: Call to function `env_var` failed: environment variable `ZADDY` not present
-  |
-2 |   echo {{env_var('ZADDY')}}
-  |          ^^^^^^^
-",
-  status:   EXIT_FAILURE,
-}
-
-test! {
-  name:     test_just_executable_function,
-  justfile: "
-    a:
-      @printf 'Executable path is: %s\\n' '{{ just_executable() }}'
-  ",
-  args:     ("a"),
-  stdout:   format!("Executable path is: {}\n", executable_path("just").to_str().unwrap()).as_str(),
-  stderr:   "",
-  status:   EXIT_SUCCESS,
-}
-
 test! {
   name: infallable_command,
   justfile: r#"
@@ -2026,16 +1783,6 @@ foo x=y:
    stderr:   "echo foo\n",
 }
 
-test! {
-  name:     test_os_arch_functions_in_default,
-  justfile: r#"
-foo a=arch() o=os() f=os_family():
-  echo {{a}} {{o}} {{f}}
-"#,
-  stdout:   format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
-  stderr:   format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
-}
-
 test! {
   name:     unterminated_interpolation_eol,
   justfile: "