Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,17 @@ $ just bar
/subdir
```

To apply the same behavior to every recipe in a module, use `set no-cd := true`.
This setting is module-local, so imported modules choose their own default, and
it can't appear alongside `set working-directory` in the same `justfile`.
Recipe-level attributes still take precedence: `[working-directory(...)]`
overrides both, and `[no-cd]` on a recipe overrides `set working-directory`.

Path resolution remains the same by default: backticks, functions like
`read()`, and shell() calls use the module's working directory even if `no-cd`
is set. To also resolve these relative to the invocation directory when
skipping `cd`, turn on `set no-cd-strict := true`.

You can override the working directory for all recipes with
`set working-directory := '…'`:

Expand Down Expand Up @@ -1032,6 +1043,8 @@ foo:
| `export` | boolean | `false` | Export all variables as environment variables. |
| `fallback` | boolean | `false` | Search `justfile` in parent directory if the first recipe on the command line is not found. |
| `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. |
| `no-cd` | boolean | `false` | Don't change directory before executing recipes and evaluating backticks, unless overridden by recipe attributes. |
| `no-cd-strict` | boolean | `false` | When `no-cd` is set, also resolve backticks, shell functions, and path helpers relative to the invocation directory instead of the module directory. |
| `positional-arguments` | boolean | `false` | Pass positional arguments. |
| `quiet` | boolean | `false` | Disable echoing recipe lines before executing. |
| `script-interpreter`<sup>1.33.0</sup> | `[COMMAND, ARGS…]` | `['sh', '-eu']` | Set command used to invoke recipes with empty `[script]` attribute. |
Expand Down
24 changes: 24 additions & 0 deletions src/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,30 @@ impl<'run, 'src> Analyzer<'run, 'src> {
}));
}

if let Some(keyword) = Keyword::from_lexeme(set.name.lexeme()) {
match keyword {
Keyword::NoCd => {
if let Some(conflict) = self.sets.get(Keyword::WorkingDirectory.lexeme()) {
return Err(set.name.error(NoCdAndWorkingDirectorySetting {
first: Keyword::WorkingDirectory,
first_line: conflict.name.line,
second: keyword,
}));
}
}
Keyword::WorkingDirectory => {
if let Some(conflict) = self.sets.get(Keyword::NoCd.lexeme()) {
return Err(set.name.error(NoCdAndWorkingDirectorySetting {
first: Keyword::NoCd,
first_line: conflict.name.line,
second: keyword,
}));
}
}
_ => {}
}
}

Ok(())
}

Expand Down
11 changes: 11 additions & 0 deletions src/compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,17 @@ impl Display for CompileError<'_> {
f,
"Recipe `{recipe}` has both `[no-cd]` and `[working-directory]` attributes"
),
NoCdAndWorkingDirectorySetting {
first,
first_line,
second,
} => write!(
f,
"Setting `{}` first set on line {} is incompatible with setting `{}`",
first.lexeme(),
first_line.ordinal(),
second.lexeme()
),
OptionNameContainsEqualSign { parameter } => {
write!(
f,
Expand Down
5 changes: 5 additions & 0 deletions src/compile_error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ pub(crate) enum CompileErrorKind<'src> {
OptionNameEmpty {
parameter: String,
},
NoCdAndWorkingDirectorySetting {
first: Keyword,
first_line: usize,
second: Keyword,
},
ParameterFollowsVariadicParameter {
parameter: &'src str,
},
Expand Down
8 changes: 7 additions & 1 deletion src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ impl<'src, 'run> Evaluator<'src, 'run> {
Setting::IgnoreComments(value) => {
settings.ignore_comments = value;
}
Setting::NoCd(value) => {
settings.no_cd = value;
}
Setting::NoCdStrict(value) => {
settings.no_cd_strict = value;
}
Setting::NoExitMessage(value) => {
settings.no_exit_message = value;
}
Expand Down Expand Up @@ -445,7 +451,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
cmd
.arg(command)
.args(args)
.current_dir(context.working_directory())
.current_dir(context.path_working_directory())
.export(
&context.module.settings,
context.dotenv,
Expand Down
19 changes: 16 additions & 3 deletions src/execution_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,28 @@ impl<'src: 'run, 'run> ExecutionContext<'src, 'run> {

pub(crate) fn working_directory(&self) -> PathBuf {
let base = if self.module.is_submodule() {
&self.module.working_directory
self
.module
.source
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| self.search.working_directory.clone())
} else {
&self.search.working_directory
self.search.working_directory.clone()
};

if let Some(setting) = &self.module.settings.working_directory {
base.join(setting)
} else {
base.into()
base
}
}

pub(crate) fn path_working_directory(&self) -> PathBuf {
if self.module.settings.no_cd && self.module.settings.no_cd_strict {
self.config.invocation_directory.clone()
} else {
self.working_directory()
}
}
}
14 changes: 8 additions & 6 deletions src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ impl Function {
fn absolute_path(context: Context, path: &str) -> FunctionResult {
let abs_path_unchecked = context
.execution_context
.working_directory()
.path_working_directory()
.join(path)
.lexiclean();
match abs_path_unchecked.to_str() {
Expand Down Expand Up @@ -162,7 +162,7 @@ fn blake3(_context: Context, s: &str) -> FunctionResult {
}

fn blake3_file(context: Context, path: &str) -> FunctionResult {
let path = context.execution_context.working_directory().join(path);
let path = context.execution_context.path_working_directory().join(path);
let mut hasher = blake3::Hasher::new();
hasher
.update_mmap_rayon(&path)
Expand All @@ -171,7 +171,9 @@ fn blake3_file(context: Context, path: &str) -> FunctionResult {
}

fn canonicalize(context: Context, path: &str) -> FunctionResult {
let canonical = std::fs::canonicalize(context.execution_context.working_directory().join(path))
let canonical = std::fs::canonicalize(
context.execution_context.path_working_directory().join(path),
)
.map_err(|err| format!("I/O error canonicalizing path: {err}"))?;

canonical.to_str().map(str::to_string).ok_or_else(|| {
Expand Down Expand Up @@ -486,7 +488,7 @@ fn path_exists(context: Context, path: &str) -> FunctionResult {
Ok(
context
.execution_context
.working_directory()
.path_working_directory()
.join(path)
.exists()
.to_string(),
Expand All @@ -498,7 +500,7 @@ fn quote(_context: Context, s: &str) -> FunctionResult {
}

fn read(context: Context, filename: &str) -> FunctionResult {
fs::read_to_string(context.execution_context.working_directory().join(filename))
fs::read_to_string(context.execution_context.path_working_directory().join(filename))
.map_err(|err| format!("I/O error reading `{filename}`: {err}"))
}

Expand Down Expand Up @@ -529,7 +531,7 @@ fn sha256(_context: Context, s: &str) -> FunctionResult {

fn sha256_file(context: Context, path: &str) -> FunctionResult {
use sha2::{Digest, Sha256};
let path = context.execution_context.working_directory().join(path);
let path = context.execution_context.path_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()))?;
Expand Down
2 changes: 2 additions & 0 deletions src/keyword.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub(crate) enum Keyword {
IgnoreComments,
Import,
Mod,
NoCd,
NoCdStrict,
NoExitMessage,
PositionalArguments,
Quiet,
Expand Down
2 changes: 2 additions & 0 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ impl<'src> Node<'src> for Set<'src> {
| Setting::DotenvRequired(value)
| Setting::Export(value)
| Setting::Fallback(value)
| Setting::NoCd(value)
| Setting::NoCdStrict(value)
| Setting::NoExitMessage(value)
| Setting::PositionalArguments(value)
| Setting::Quiet(value)
Expand Down
14 changes: 14 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,8 @@ impl<'run, 'src> Parser<'run, 'src> {
Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)),
Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)),
Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),
Keyword::NoCd => Some(Setting::NoCd(self.parse_set_bool()?)),
Keyword::NoCdStrict => Some(Setting::NoCdStrict(self.parse_set_bool()?)),
Keyword::NoExitMessage => Some(Setting::NoExitMessage(self.parse_set_bool()?)),
Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),
Keyword::Quiet => Some(Setting::Quiet(self.parse_set_bool()?)),
Expand Down Expand Up @@ -2540,6 +2542,18 @@ mod tests {
tree: (justfile (set quiet false)),
}

test! {
name: set_no_cd,
text: "set no-cd := true",
tree: (justfile (set no_cd true)),
}

test! {
name: set_no_cd_strict,
text: "set no-cd-strict := true",
tree: (justfile (set no_cd_strict true)),
}

test! {
name: set_positional_arguments_false,
text: "set positional-arguments := false",
Expand Down
31 changes: 21 additions & 10 deletions src/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,19 @@ impl<'src, D> Recipe<'src, D> {
.contains(AttributeDiscriminant::PositionalArguments)
}

pub(crate) fn change_directory(&self) -> bool {
!self.attributes.contains(AttributeDiscriminant::NoCd)
pub(crate) fn change_directory(&self, settings: &Settings) -> bool {
if self
.attributes
.contains(AttributeDiscriminant::WorkingDirectory)
{
return true;
}

if self.attributes.contains(AttributeDiscriminant::NoCd) {
return false;
}

!settings.no_cd
}

pub(crate) fn enabled(&self) -> bool {
Expand Down Expand Up @@ -181,20 +192,20 @@ impl<'src, D> Recipe<'src, D> {
}
}

fn working_directory<'a>(&'a self, context: &'a ExecutionContext) -> Option<PathBuf> {
if !self.change_directory() {
return None;
}

let working_directory = context.working_directory();
pub(crate) fn working_directory<'a>(&'a self, context: &'a ExecutionContext) -> Option<PathBuf> {
let module_working_directory = context.working_directory();

for attribute in &self.attributes {
if let Attribute::WorkingDirectory(dir) = attribute {
return Some(working_directory.join(&dir.cooked));
return Some(module_working_directory.join(&dir.cooked));
}
}

Some(working_directory)
if !self.change_directory(&context.module.settings) {
return None;
}

Some(module_working_directory)
}

fn no_quiet(&self) -> bool {
Expand Down
4 changes: 4 additions & 0 deletions src/setting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub(crate) enum Setting<'src> {
Export(bool),
Fallback(bool),
IgnoreComments(bool),
NoCd(bool),
NoCdStrict(bool),
NoExitMessage(bool),
PositionalArguments(bool),
Quiet(bool),
Expand Down Expand Up @@ -59,6 +61,8 @@ impl Display for Setting<'_> {
| Self::Export(value)
| Self::Fallback(value)
| Self::IgnoreComments(value)
| Self::NoCd(value)
| Self::NoCdStrict(value)
| Self::NoExitMessage(value)
| Self::PositionalArguments(value)
| Self::Quiet(value)
Expand Down
2 changes: 2 additions & 0 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub(crate) struct Settings {
pub(crate) export: bool,
pub(crate) fallback: bool,
pub(crate) ignore_comments: bool,
pub(crate) no_cd: bool,
pub(crate) no_cd_strict: bool,
pub(crate) no_exit_message: bool,
pub(crate) positional_arguments: bool,
pub(crate) quiet: bool,
Expand Down
2 changes: 1 addition & 1 deletion src/which.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub(crate) fn which(context: function::Context, name: &str) -> Result<Option<Str
// relative to the working directory of the just invocation.
candidate = context
.execution_context
.working_directory()
.path_working_directory()
.join(candidate);
}

Expand Down
2 changes: 2 additions & 0 deletions tests/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ struct Settings<'a> {
export: bool,
fallback: bool,
ignore_comments: bool,
no_cd: bool,
no_cd_strict: bool,
no_exit_message: bool,
positional_arguments: bool,
quiet: bool,
Expand Down
22 changes: 22 additions & 0 deletions tests/modules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,28 @@ foo:
.run();
}

#[test]
fn modules_do_not_inherit_no_cd_setting() {
Test::new()
.write(
"foo.just",
"bar:
@cat data.txt
",
)
.write("data.txt", "MODULE\n")
.justfile(
"
set no-cd := true

mod foo
",
)
.args(["foo", "bar"])
.stdout("MODULE\n")
.run();
}

#[test]
fn modules_conflict_with_recipes() {
Test::new()
Expand Down
Loading