From f039701f71d09ad70d6005ef2b2856818af0cba4 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 28 Dec 2023 18:08:02 -0800 Subject: [PATCH] Expand tilde in import and module paths (#1792) --- Cargo.lock | 70 ++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + README.md | 17 ++++++++++-- src/compiler.rs | 18 +++++++++++-- src/error.rs | 4 +++ tests/imports.rs | 17 ++++++++++++ tests/modules.rs | 19 +++++++++++++ 7 files changed, 142 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65bb40e..d521757 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,6 +156,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -326,6 +347,7 @@ dependencies = [ "cradle", "ctrlc", "derivative", + "dirs", "dotenvy", "edit-distance", "env_logger", @@ -372,6 +394,17 @@ version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -423,6 +456,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "pretty_assertions" version = "1.4.0" @@ -505,6 +544,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "ref-type" version = "0.0.0" @@ -785,6 +835,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.43", +] + [[package]] name = "typed-arena" version = "2.0.2" diff --git a/Cargo.toml b/Cargo.toml index f0414c5..cdf211c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ camino = "1.0.4" clap = { version = "2.33.0", features = ["wrap_help"] } ctrlc = { version = "3.1.1", features = ["termination"] } derivative = "2.0.0" +dirs = "5.0.1" dotenvy = "0.15" edit-distance = "2.0.0" env_logger = "0.10.0" diff --git a/README.md b/README.md index f0f5515..5ba69f0 100644 --- a/README.md +++ b/README.md @@ -2358,7 +2358,8 @@ A ``` The `import` path can be absolute or relative to the location of the justfile -containing it. +containing it. A leading `~/` in the import path is replaced with the current +users home directory. Justfiles are insensitive to order, so included files can reference variables and recipes defined after the `import` statement. @@ -2406,7 +2407,19 @@ If a module is named `foo`, just will search for the module file in `foo.just`, `foo/mod.just`, `foo/justfile`, and `foo/.justfile`. In the latter two cases, the module file may have any capitalization. -Environment files are loaded for the root justfile. +Module statements may be of the form: + +```mf +mod foo 'PATH' +``` + +Which loads the module's source file from `PATH`, instead of from the usual +locations. A leading `~/` in `PATH` is replaced with the current user's home +directory. + +Environment files are only loaded for the root justfile, and loaded environment +variables are available in submodules. Settings in submodules that affect +enviroment file loading are ignored. Recipes in submodules without the `[no-cd]` attribute run with the working directory set to the directory containing the submodule source file. diff --git a/src/compiler.rs b/src/compiler.rs index 6680a84..bc836ed 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -41,7 +41,7 @@ impl Compiler { let parent = current.parent().unwrap(); let import = if let Some(path) = path { - parent.join(&path.cooked) + parent.join(Self::expand_tilde(&path.cooked)?) } else { Self::find_module_file(parent, *name)? }; @@ -53,7 +53,11 @@ impl Compiler { stack.push((import, depth + 1)); } Item::Import { relative, absolute } => { - let import = current.parent().unwrap().join(&relative.cooked).lexiclean(); + let import = current + .parent() + .unwrap() + .join(Self::expand_tilde(&relative.cooked)?) + .lexiclean(); if srcs.contains_key(&import) { return Err(Error::CircularImport { current, import }); } @@ -117,6 +121,16 @@ impl Compiler { } } + fn expand_tilde(path: &str) -> RunResult<'static, PathBuf> { + Ok(if let Some(path) = path.strip_prefix("~/") { + dirs::home_dir() + .ok_or(Error::Homedir)? + .join(path.trim_start_matches('/')) + } else { + PathBuf::from(path) + }) + } + #[cfg(test)] pub(crate) fn test_compile(src: &str) -> CompileResult { let tokens = Lexer::test_lex(src)?; diff --git a/src/error.rs b/src/error.rs index a445e2e..3a0da1b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -95,6 +95,7 @@ pub(crate) enum Error<'src> { GetConfirmation { io_error: io::Error, }, + Homedir, InitExists { justfile: PathBuf, }, @@ -347,6 +348,9 @@ impl<'src> ColorDisplay for Error<'src> { GetConfirmation { io_error } => { write!(f, "Failed to read confirmation from stdin: {io_error}")?; } + Homedir => { + write!(f, "Failed to get homedir")?; + } InitExists { justfile } => { write!(f, "Justfile `{}` already exists", justfile.display())?; } diff --git a/tests/imports.rs b/tests/imports.rs index b1cb395..22a04b7 100644 --- a/tests/imports.rs +++ b/tests/imports.rs @@ -152,3 +152,20 @@ fn recipes_in_import_are_overridden_by_recipes_in_parent() { .stdout("ROOT\n") .run(); } + +#[cfg(not(windows))] +#[test] +fn import_paths_beginning_with_tilde_are_expanded_to_homdir() { + Test::new() + .write("foobar/mod.just", "foo:\n @echo FOOBAR") + .justfile( + " + import '~/mod.just' + ", + ) + .test_round_trip(false) + .arg("foo") + .stdout("FOOBAR\n") + .env("HOME", "foobar") + .run(); +} diff --git a/tests/modules.rs b/tests/modules.rs index ea4796a..8c2e772 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -529,3 +529,22 @@ fn submodule_shebang_recipes_run_in_submodule_directory() { .stdout("BAR") .run(); } + +#[cfg(not(windows))] +#[test] +fn module_paths_beginning_with_tilde_are_expanded_to_homdir() { + Test::new() + .write("foobar/mod.just", "foo:\n @echo FOOBAR") + .justfile( + " + mod foo '~/mod.just' + ", + ) + .test_round_trip(false) + .arg("--unstable") + .arg("foo") + .arg("foo") + .stdout("FOOBAR\n") + .env("HOME", "foobar") + .run(); +}