Repository: casey/just
Pull request #1792 (2023-12-29 02:02:27)

Expand tilde


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 65bb40e7..d521757b 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 f0414c54..cdf211cd 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 f0f55154..5ba69f07 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 6680a845..bc836ed6 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<Justfile> {
     let tokens = Lexer::test_lex(src)?;
diff --git a/src/error.rs b/src/error.rs
index a445e2e0..3a0da1b4 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())?;
       }


Test patch:
diff --git a/tests/imports.rs b/tests/imports.rs
index b1cb395c..22a04b71 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 ea4796a4..8c2e7723 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();
+}