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 { Ok(target::arch().to_owned()) } +fn clean(_context: &FunctionContext, path: &str) -> Result { + Ok(Path::new(path).lexiclean().to_str().unwrap().to_owned()) +} + fn env_var(context: &FunctionContext, key: &str) -> Result { use std::env::VarError::*; 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: "