diff --git a/README.md b/README.md index cbfaa26..1b5adc4 100644 --- a/README.md +++ b/README.md @@ -1420,6 +1420,18 @@ script: ./{{justfile_directory()}}/scripts/some_script ``` +#### Source and Source Directory + +- `source()`master - Retrieves the path of the current source file. + +- `source_directory()`master - Retrieves the path of the parent directory of the + current source file. + +`source()` and `source_directory()` behave the same as `justfile()` and +`justfile_directory()` in the root `justfile`, but will return the path and +directory, respectively, of the current `import` or `mod` source file when +called from within an import or submodule. + #### Just Executable - `just_executable()` - Absolute path to the `just` executable. diff --git a/src/evaluator.rs b/src/evaluator.rs index 0ccafb9..2794b46 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -68,25 +68,13 @@ impl<'src, 'run> Evaluator<'src, 'run> { Expression::Call { thunk } => { use Thunk::*; - match thunk { - Nullary { name, function, .. } => function(self).map_err(|message| Error::FunctionCall { - function: *name, - message, - }), - Unary { - name, - function, - arg, - .. - } => { + let result = match thunk { + Nullary { function, .. } => function(function::Context::new(self, thunk.name())), + Unary { function, arg, .. } => { let arg = self.evaluate_expression(arg)?; - function(self, &arg).map_err(|message| Error::FunctionCall { - function: *name, - message, - }) + function(function::Context::new(self, thunk.name()), &arg) } UnaryOpt { - name, function, args: (a, b), .. @@ -97,13 +85,9 @@ impl<'src, 'run> Evaluator<'src, 'run> { None => None, }; - function(self, &a, b.as_deref()).map_err(|message| Error::FunctionCall { - function: *name, - message, - }) + function(function::Context::new(self, thunk.name()), &a, b.as_deref()) } UnaryPlus { - name, function, args: (a, rest), .. @@ -113,26 +97,22 @@ impl<'src, 'run> Evaluator<'src, 'run> { for arg in rest { rest_evaluated.push(self.evaluate_expression(arg)?); } - function(self, &a, &rest_evaluated).map_err(|message| Error::FunctionCall { - function: *name, - message, - }) + function( + function::Context::new(self, thunk.name()), + &a, + &rest_evaluated, + ) } Binary { - name, function, args: [a, b], .. } => { let a = self.evaluate_expression(a)?; let b = self.evaluate_expression(b)?; - function(self, &a, &b).map_err(|message| Error::FunctionCall { - function: *name, - message, - }) + function(function::Context::new(self, thunk.name()), &a, &b) } BinaryPlus { - name, function, args: ([a, b], rest), .. @@ -143,13 +123,14 @@ impl<'src, 'run> Evaluator<'src, 'run> { for arg in rest { rest_evaluated.push(self.evaluate_expression(arg)?); } - function(self, &a, &b, &rest_evaluated).map_err(|message| Error::FunctionCall { - function: *name, - message, - }) + function( + function::Context::new(self, thunk.name()), + &a, + &b, + &rest_evaluated, + ) } Ternary { - name, function, args: [a, b, c], .. @@ -157,12 +138,14 @@ impl<'src, 'run> Evaluator<'src, 'run> { let a = self.evaluate_expression(a)?; let b = self.evaluate_expression(b)?; let c = self.evaluate_expression(c)?; - function(self, &a, &b, &c).map_err(|message| Error::FunctionCall { - function: *name, - message, - }) + function(function::Context::new(self, thunk.name()), &a, &b, &c) } - } + }; + + result.map_err(|message| Error::FunctionCall { + function: thunk.name(), + message, + }) } Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()), Expression::Backtick { contents, token } => { diff --git a/src/function.rs b/src/function.rs index 0321508..52fcf00 100644 --- a/src/function.rs +++ b/src/function.rs @@ -11,13 +11,24 @@ use { }; pub(crate) enum Function { - Nullary(fn(&Evaluator) -> Result), - Unary(fn(&Evaluator, &str) -> Result), - UnaryOpt(fn(&Evaluator, &str, Option<&str>) -> Result), - UnaryPlus(fn(&Evaluator, &str, &[String]) -> Result), - Binary(fn(&Evaluator, &str, &str) -> Result), - BinaryPlus(fn(&Evaluator, &str, &str, &[String]) -> Result), - Ternary(fn(&Evaluator, &str, &str, &str) -> Result), + Nullary(fn(Context) -> Result), + Unary(fn(Context, &str) -> Result), + UnaryOpt(fn(Context, &str, Option<&str>) -> Result), + UnaryPlus(fn(Context, &str, &[String]) -> Result), + Binary(fn(Context, &str, &str) -> Result), + BinaryPlus(fn(Context, &str, &str, &[String]) -> Result), + Ternary(fn(Context, &str, &str, &str) -> Result), +} + +pub(crate) struct Context<'src: 'run, 'run> { + pub(crate) evaluator: &'run Evaluator<'src, 'run>, + pub(crate) name: Name<'src>, +} + +impl<'src: 'run, 'run> Context<'src, 'run> { + pub(crate) fn new(evaluator: &'run Evaluator<'src, 'run>, name: Name<'src>) -> Self { + Self { evaluator, name } + } } pub(crate) fn get(name: &str) -> Option { @@ -72,6 +83,8 @@ pub(crate) fn get(name: &str) -> Option { "shoutykebabcase" => Unary(shoutykebabcase), "shoutysnakecase" => Unary(shoutysnakecase), "snakecase" => Unary(snakecase), + "source_directory" => Nullary(source_directory), + "source_file" => Nullary(source_file), "titlecase" => Unary(titlecase), "trim" => Unary(trim), "trim_end" => Unary(trim_end), @@ -103,18 +116,23 @@ impl Function { } } -fn absolute_path(evaluator: &Evaluator, path: &str) -> Result { - let abs_path_unchecked = evaluator.search.working_directory.join(path).lexiclean(); +fn absolute_path(context: Context, path: &str) -> Result { + let abs_path_unchecked = context + .evaluator + .search + .working_directory + .join(path) + .lexiclean(); match abs_path_unchecked.to_str() { Some(absolute_path) => Ok(absolute_path.to_owned()), None => Err(format!( "Working directory is not valid unicode: {}", - evaluator.search.working_directory.display() + context.evaluator.search.working_directory.display() )), } } -fn append(_evaluator: &Evaluator, suffix: &str, s: &str) -> Result { +fn append(_context: Context, suffix: &str, s: &str) -> Result { Ok( s.split_whitespace() .map(|s| format!("{s}{suffix}")) @@ -123,16 +141,16 @@ fn append(_evaluator: &Evaluator, suffix: &str, s: &str) -> Result Result { +fn arch(_context: Context) -> Result { Ok(target::arch().to_owned()) } -fn blake3(_evaluator: &Evaluator, s: &str) -> Result { +fn blake3(_context: Context, s: &str) -> Result { Ok(blake3::hash(s.as_bytes()).to_string()) } -fn blake3_file(evaluator: &Evaluator, path: &str) -> Result { - let path = evaluator.search.working_directory.join(path); +fn blake3_file(context: Context, path: &str) -> Result { + let path = context.evaluator.search.working_directory.join(path); let mut hasher = blake3::Hasher::new(); hasher .update_mmap_rayon(&path) @@ -140,7 +158,7 @@ fn blake3_file(evaluator: &Evaluator, path: &str) -> Result { Ok(hasher.finalize().to_string()) } -fn canonicalize(_evaluator: &Evaluator, path: &str) -> Result { +fn canonicalize(_context: Context, path: &str) -> Result { let canonical = std::fs::canonicalize(path).map_err(|err| format!("I/O error canonicalizing path: {err}"))?; @@ -152,7 +170,7 @@ fn canonicalize(_evaluator: &Evaluator, path: &str) -> Result { }) } -fn capitalize(_evaluator: &Evaluator, s: &str) -> Result { +fn capitalize(_context: Context, s: &str) -> Result { let mut capitalized = String::new(); for (i, c) in s.chars().enumerate() { if i == 0 { @@ -164,7 +182,7 @@ fn capitalize(_evaluator: &Evaluator, s: &str) -> Result { Ok(capitalized) } -fn choose(_evaluator: &Evaluator, n: &str, alphabet: &str) -> Result { +fn choose(_context: Context, n: &str, alphabet: &str) -> Result { if alphabet.is_empty() { return Err("empty alphabet".into()); } @@ -188,7 +206,7 @@ fn choose(_evaluator: &Evaluator, n: &str, alphabet: &str) -> Result Result { +fn clean(_context: Context, path: &str) -> Result { Ok(Path::new(path).lexiclean().to_str().unwrap().to_owned()) } @@ -208,7 +226,7 @@ fn dir(name: &'static str, f: fn() -> Option) -> Result } } -fn encode_uri_component(_evaluator: &Evaluator, s: &str) -> Result { +fn encode_uri_component(_context: Context, s: &str) -> Result { static PERCENT_ENCODE: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC .remove(b'-') .remove(b'_') @@ -222,10 +240,10 @@ fn encode_uri_component(_evaluator: &Evaluator, s: &str) -> Result Result { +fn env_var(context: Context, key: &str) -> Result { use std::env::VarError::*; - if let Some(value) = evaluator.dotenv.get(key) { + if let Some(value) = context.evaluator.dotenv.get(key) { return Ok(value.clone()); } @@ -238,10 +256,10 @@ fn env_var(evaluator: &Evaluator, key: &str) -> Result { } } -fn env_var_or_default(evaluator: &Evaluator, key: &str, default: &str) -> Result { +fn env_var_or_default(context: Context, key: &str, default: &str) -> Result { use std::env::VarError::*; - if let Some(value) = evaluator.dotenv.get(key) { + if let Some(value) = context.evaluator.dotenv.get(key) { return Ok(value.clone()); } @@ -254,48 +272,49 @@ fn env_var_or_default(evaluator: &Evaluator, key: &str, default: &str) -> Result } } -fn env(evaluator: &Evaluator, key: &str, default: Option<&str>) -> Result { +fn env(context: Context, key: &str, default: Option<&str>) -> Result { match default { - Some(val) => env_var_or_default(evaluator, key, val), - None => env_var(evaluator, key), + Some(val) => env_var_or_default(context, key, val), + None => env_var(context, key), } } -fn error(_evaluator: &Evaluator, message: &str) -> Result { +fn error(_context: Context, message: &str) -> Result { Err(message.to_owned()) } -fn extension(_evaluator: &Evaluator, path: &str) -> Result { +fn extension(_context: Context, path: &str) -> Result { Utf8Path::new(path) .extension() .map(str::to_owned) .ok_or_else(|| format!("Could not extract extension from `{path}`")) } -fn file_name(_evaluator: &Evaluator, path: &str) -> Result { +fn file_name(_context: Context, path: &str) -> Result { Utf8Path::new(path) .file_name() .map(str::to_owned) .ok_or_else(|| format!("Could not extract file name from `{path}`")) } -fn file_stem(_evaluator: &Evaluator, path: &str) -> Result { +fn file_stem(_context: Context, path: &str) -> Result { Utf8Path::new(path) .file_stem() .map(str::to_owned) .ok_or_else(|| format!("Could not extract file stem from `{path}`")) } -fn invocation_directory(evaluator: &Evaluator) -> Result { +fn invocation_directory(context: Context) -> Result { Platform::convert_native_path( - &evaluator.search.working_directory, - &evaluator.config.invocation_directory, + &context.evaluator.search.working_directory, + &context.evaluator.config.invocation_directory, ) .map_err(|e| format!("Error getting shell path: {e}")) } -fn invocation_directory_native(evaluator: &Evaluator) -> Result { - evaluator +fn invocation_directory_native(context: Context) -> Result { + context + .evaluator .config .invocation_directory .to_str() @@ -303,12 +322,12 @@ fn invocation_directory_native(evaluator: &Evaluator) -> Result .ok_or_else(|| { format!( "Invocation directory is not valid unicode: {}", - evaluator.config.invocation_directory.display() + context.evaluator.config.invocation_directory.display() ) }) } -fn prepend(_evaluator: &Evaluator, prefix: &str, s: &str) -> Result { +fn prepend(_context: Context, prefix: &str, s: &str) -> Result { Ok( s.split_whitespace() .map(|s| format!("{prefix}{s}")) @@ -317,7 +336,7 @@ fn prepend(_evaluator: &Evaluator, prefix: &str, s: &str) -> Result Result { +fn join(_context: Context, base: &str, with: &str, and: &[String]) -> Result { let mut result = Utf8Path::new(base).join(with); for arg in and { result.push(arg); @@ -325,7 +344,7 @@ fn join(_evaluator: &Evaluator, base: &str, with: &str, and: &[String]) -> Resul Ok(result.to_string()) } -fn just_executable(_evaluator: &Evaluator) -> Result { +fn just_executable(_context: Context) -> Result { let exe_path = env::current_exe().map_err(|e| format!("Error getting current executable: {e}"))?; @@ -337,12 +356,13 @@ fn just_executable(_evaluator: &Evaluator) -> Result { }) } -fn just_pid(_evaluator: &Evaluator) -> Result { +fn just_pid(_context: Context) -> Result { Ok(std::process::id().to_string()) } -fn justfile(evaluator: &Evaluator) -> Result { - evaluator +fn justfile(context: Context) -> Result { + context + .evaluator .search .justfile .to_str() @@ -350,16 +370,16 @@ fn justfile(evaluator: &Evaluator) -> Result { .ok_or_else(|| { format!( "Justfile path is not valid unicode: {}", - evaluator.search.justfile.display() + context.evaluator.search.justfile.display() ) }) } -fn justfile_directory(evaluator: &Evaluator) -> Result { - let justfile_directory = evaluator.search.justfile.parent().ok_or_else(|| { +fn justfile_directory(context: Context) -> Result { + let justfile_directory = context.evaluator.search.justfile.parent().ok_or_else(|| { format!( "Could not resolve justfile directory. Justfile `{}` had no parent.", - evaluator.search.justfile.display() + context.evaluator.search.justfile.display() ) })?; @@ -374,41 +394,42 @@ fn justfile_directory(evaluator: &Evaluator) -> Result { }) } -fn kebabcase(_evaluator: &Evaluator, s: &str) -> Result { +fn kebabcase(_context: Context, s: &str) -> Result { Ok(s.to_kebab_case()) } -fn lowercamelcase(_evaluator: &Evaluator, s: &str) -> Result { +fn lowercamelcase(_context: Context, s: &str) -> Result { Ok(s.to_lower_camel_case()) } -fn lowercase(_evaluator: &Evaluator, s: &str) -> Result { +fn lowercase(_context: Context, s: &str) -> Result { Ok(s.to_lowercase()) } -fn num_cpus(_evaluator: &Evaluator) -> Result { +fn num_cpus(_context: Context) -> Result { let num = num_cpus::get(); Ok(num.to_string()) } -fn os(_evaluator: &Evaluator) -> Result { +fn os(_context: Context) -> Result { Ok(target::os().to_owned()) } -fn os_family(_evaluator: &Evaluator) -> Result { +fn os_family(_context: Context) -> Result { Ok(target::family().to_owned()) } -fn parent_directory(_evaluator: &Evaluator, path: &str) -> Result { +fn parent_directory(_context: Context, path: &str) -> Result { Utf8Path::new(path) .parent() .map(Utf8Path::to_string) .ok_or_else(|| format!("Could not extract parent directory from `{path}`")) } -fn path_exists(evaluator: &Evaluator, path: &str) -> Result { +fn path_exists(context: Context, path: &str) -> Result { Ok( - evaluator + context + .evaluator .search .working_directory .join(path) @@ -417,16 +438,16 @@ fn path_exists(evaluator: &Evaluator, path: &str) -> Result { ) } -fn quote(_evaluator: &Evaluator, s: &str) -> Result { +fn quote(_context: Context, s: &str) -> Result { Ok(format!("'{}'", s.replace('\'', "'\\''"))) } -fn replace(_evaluator: &Evaluator, s: &str, from: &str, to: &str) -> Result { +fn replace(_context: Context, s: &str, from: &str, to: &str) -> Result { Ok(s.replace(from, to)) } fn replace_regex( - _evaluator: &Evaluator, + _context: Context, s: &str, regex: &str, replacement: &str, @@ -439,7 +460,7 @@ fn replace_regex( ) } -fn sha256(_evaluator: &Evaluator, s: &str) -> Result { +fn sha256(_context: Context, s: &str) -> Result { use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); hasher.update(s); @@ -447,9 +468,9 @@ fn sha256(_evaluator: &Evaluator, s: &str) -> Result { Ok(format!("{hash:x}")) } -fn sha256_file(evaluator: &Evaluator, path: &str) -> Result { +fn sha256_file(context: Context, path: &str) -> Result { use sha2::{Digest, Sha256}; - let path = evaluator.search.working_directory.join(path); + let path = context.evaluator.search.working_directory.join(path); let mut hasher = Sha256::new(); let mut file = fs::File::open(&path).map_err(|err| format!("Failed to open `{}`: {err}", path.display()))?; @@ -459,73 +480,112 @@ fn sha256_file(evaluator: &Evaluator, path: &str) -> Result { Ok(format!("{hash:x}")) } -fn shell(evaluator: &Evaluator, command: &str, args: &[String]) -> Result { +fn shell(context: Context, command: &str, args: &[String]) -> Result { let args = iter::once(command) .chain(args.iter().map(String::as_str)) .collect::>(); - evaluator + context + .evaluator .run_command(command, &args) .map_err(|output_error| output_error.to_string()) } -fn shoutykebabcase(_evaluator: &Evaluator, s: &str) -> Result { +fn shoutykebabcase(_context: Context, s: &str) -> Result { Ok(s.to_shouty_kebab_case()) } -fn shoutysnakecase(_evaluator: &Evaluator, s: &str) -> Result { +fn shoutysnakecase(_context: Context, s: &str) -> Result { Ok(s.to_shouty_snake_case()) } -fn snakecase(_evaluator: &Evaluator, s: &str) -> Result { +fn snakecase(_context: Context, s: &str) -> Result { Ok(s.to_snake_case()) } -fn titlecase(_evaluator: &Evaluator, s: &str) -> Result { +fn source_directory(context: Context) -> Result { + context + .evaluator + .search + .justfile + .parent() + .unwrap() + .join(context.name.token.path) + .parent() + .unwrap() + .to_str() + .map(str::to_owned) + .ok_or_else(|| { + format!( + "Source file path not valid unicode: {}", + context.name.token.path.display(), + ) + }) +} + +fn source_file(context: Context) -> Result { + context + .evaluator + .search + .justfile + .parent() + .unwrap() + .join(context.name.token.path) + .to_str() + .map(str::to_owned) + .ok_or_else(|| { + format!( + "Source file path not valid unicode: {}", + context.name.token.path.display(), + ) + }) +} + +fn titlecase(_context: Context, s: &str) -> Result { Ok(s.to_title_case()) } -fn trim(_evaluator: &Evaluator, s: &str) -> Result { +fn trim(_context: Context, s: &str) -> Result { Ok(s.trim().to_owned()) } -fn trim_end(_evaluator: &Evaluator, s: &str) -> Result { +fn trim_end(_context: Context, s: &str) -> Result { Ok(s.trim_end().to_owned()) } -fn trim_end_match(_evaluator: &Evaluator, s: &str, pat: &str) -> Result { +fn trim_end_match(_context: Context, s: &str, pat: &str) -> Result { Ok(s.strip_suffix(pat).unwrap_or(s).to_owned()) } -fn trim_end_matches(_evaluator: &Evaluator, s: &str, pat: &str) -> Result { +fn trim_end_matches(_context: Context, s: &str, pat: &str) -> Result { Ok(s.trim_end_matches(pat).to_owned()) } -fn trim_start(_evaluator: &Evaluator, s: &str) -> Result { +fn trim_start(_context: Context, s: &str) -> Result { Ok(s.trim_start().to_owned()) } -fn trim_start_match(_evaluator: &Evaluator, s: &str, pat: &str) -> Result { +fn trim_start_match(_context: Context, s: &str, pat: &str) -> Result { Ok(s.strip_prefix(pat).unwrap_or(s).to_owned()) } -fn trim_start_matches(_evaluator: &Evaluator, s: &str, pat: &str) -> Result { +fn trim_start_matches(_context: Context, s: &str, pat: &str) -> Result { Ok(s.trim_start_matches(pat).to_owned()) } -fn uppercamelcase(_evaluator: &Evaluator, s: &str) -> Result { +fn uppercamelcase(_context: Context, s: &str) -> Result { Ok(s.to_upper_camel_case()) } -fn uppercase(_evaluator: &Evaluator, s: &str) -> Result { +fn uppercase(_context: Context, s: &str) -> Result { Ok(s.to_uppercase()) } -fn uuid(_evaluator: &Evaluator) -> Result { +fn uuid(_context: Context) -> Result { Ok(uuid::Uuid::new_v4().to_string()) } -fn without_extension(_evaluator: &Evaluator, path: &str) -> Result { +fn without_extension(_context: Context, path: &str) -> Result { let parent = Utf8Path::new(path) .parent() .ok_or_else(|| format!("Could not extract parent from `{path}`"))?; @@ -539,11 +599,7 @@ fn without_extension(_evaluator: &Evaluator, path: &str) -> Result=0.1.0") -fn semver_matches( - _evaluator: &Evaluator, - version: &str, - requirement: &str, -) -> Result { +fn semver_matches(_context: Context, version: &str, requirement: &str) -> Result { Ok( requirement .parse::() diff --git a/src/thunk.rs b/src/thunk.rs index 87d66a3..2ab203a 100644 --- a/src/thunk.rs +++ b/src/thunk.rs @@ -6,48 +6,48 @@ pub(crate) enum Thunk<'src> { Nullary { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] - function: fn(&Evaluator) -> Result, + function: fn(function::Context) -> Result, }, Unary { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] - function: fn(&Evaluator, &str) -> Result, + function: fn(function::Context, &str) -> Result, arg: Box>, }, UnaryOpt { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] - function: fn(&Evaluator, &str, Option<&str>) -> Result, + function: fn(function::Context, &str, Option<&str>) -> Result, args: (Box>, Box>>), }, UnaryPlus { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] - function: fn(&Evaluator, &str, &[String]) -> Result, + function: fn(function::Context, &str, &[String]) -> Result, args: (Box>, Vec>), }, Binary { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] - function: fn(&Evaluator, &str, &str) -> Result, + function: fn(function::Context, &str, &str) -> Result, args: [Box>; 2], }, BinaryPlus { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] - function: fn(&Evaluator, &str, &str, &[String]) -> Result, + function: fn(function::Context, &str, &str, &[String]) -> Result, args: ([Box>; 2], Vec>), }, Ternary { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] - function: fn(&Evaluator, &str, &str, &str) -> Result, + function: fn(function::Context, &str, &str, &str) -> Result, args: [Box>; 3], }, } impl<'src> Thunk<'src> { - fn name(&self) -> &Name<'src> { + pub(crate) fn name(&self) -> Name<'src> { match self { Self::Nullary { name, .. } | Self::Unary { name, .. } @@ -55,7 +55,7 @@ impl<'src> Thunk<'src> { | Self::UnaryPlus { name, .. } | Self::Binary { name, .. } | Self::BinaryPlus { name, .. } - | Self::Ternary { name, .. } => name, + | Self::Ternary { name, .. } => *name, } } @@ -190,7 +190,7 @@ impl<'src> Serialize for Thunk<'src> { { let mut seq = serializer.serialize_seq(None)?; seq.serialize_element("call")?; - seq.serialize_element(self.name())?; + seq.serialize_element(&self.name())?; match self { Self::Nullary { .. } => {} Self::Unary { arg, .. } => seq.serialize_element(&arg)?, diff --git a/tests/functions.rs b/tests/functions.rs index 4631f55..514f598 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -853,3 +853,54 @@ fn encode_uri_component() { .stdout("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!%22%23%24%25%26'()*%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~%20%09%0D%0A%F0%9F%8C%90") .run(); } + +#[test] +fn source_file() { + Test::new() + .args(["--evaluate", "x"]) + .justfile("x := source_file()") + .stdout_regex(r".*[/\\]justfile") + .run(); + + Test::new() + .args(["--evaluate", "x"]) + .test_round_trip(false) + .justfile( + " + import 'foo.just' + ", + ) + .write("foo.just", "x := source_file()") + .stdout_regex(r".*[/\\]foo.just") + .run(); + + Test::new() + .args(["--unstable", "foo", "bar"]) + .test_round_trip(false) + .justfile( + " + mod foo + ", + ) + .write("foo.just", "x := source_file()\nbar:\n @echo '{{x}}'") + .stdout_regex(r".*[/\\]foo.just\n") + .run(); +} + +#[test] +fn source_directory() { + Test::new() + .args(["--unstable", "foo", "bar"]) + .test_round_trip(false) + .justfile( + " + mod foo + ", + ) + .write( + "foo/mod.just", + "x := source_directory()\nbar:\n @echo '{{x}}'", + ) + .stdout_regex(r".*[/\\]foo\n") + .run(); +}