Skip to content

Commit 6423262

Browse files
committed
add setting no-cd / no-cd-strict
no-cd-strict provides more strict application of the no-cd directiive for path helpers
1 parent 6fa7358 commit 6423262

18 files changed

Lines changed: 252 additions & 21 deletions

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,17 @@ $ just bar
941941
/subdir
942942
```
943943

944+
To apply the same behavior to every recipe in a module, use `set no-cd := true`.
945+
This setting is module-local, so imported modules choose their own default, and
946+
it can't appear alongside `set working-directory` in the same `justfile`.
947+
Recipe-level attributes still take precedence: `[working-directory(...)]`
948+
overrides both, and `[no-cd]` on a recipe overrides `set working-directory`.
949+
950+
Path resolution remains the same by default: backticks, functions like
951+
`read()`, and shell() calls use the module's working directory even if `no-cd`
952+
is set. To also resolve these relative to the invocation directory when
953+
skipping `cd`, turn on `set no-cd-strict := true`.
954+
944955
You can override the working directory for all recipes with
945956
`set working-directory := '…'`:
946957

@@ -1032,6 +1043,8 @@ foo:
10321043
| `export` | boolean | `false` | Export all variables as environment variables. |
10331044
| `fallback` | boolean | `false` | Search `justfile` in parent directory if the first recipe on the command line is not found. |
10341045
| `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. |
1046+
| `no-cd` | boolean | `false` | Don't change directory before executing recipes and evaluating backticks, unless overridden by recipe attributes. |
1047+
| `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. |
10351048
| `positional-arguments` | boolean | `false` | Pass positional arguments. |
10361049
| `quiet` | boolean | `false` | Disable echoing recipe lines before executing. |
10371050
| `script-interpreter`<sup>1.33.0</sup> | `[COMMAND, ARGS…]` | `['sh', '-eu']` | Set command used to invoke recipes with empty `[script]` attribute. |

src/analyzer.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,30 @@ impl<'run, 'src> Analyzer<'run, 'src> {
333333
}));
334334
}
335335

336+
if let Some(keyword) = Keyword::from_lexeme(set.name.lexeme()) {
337+
match keyword {
338+
Keyword::NoCd => {
339+
if let Some(conflict) = self.sets.get(Keyword::WorkingDirectory.lexeme()) {
340+
return Err(set.name.error(NoCdAndWorkingDirectorySetting {
341+
first: Keyword::WorkingDirectory,
342+
first_line: conflict.name.line,
343+
second: keyword,
344+
}));
345+
}
346+
}
347+
Keyword::WorkingDirectory => {
348+
if let Some(conflict) = self.sets.get(Keyword::NoCd.lexeme()) {
349+
return Err(set.name.error(NoCdAndWorkingDirectorySetting {
350+
first: Keyword::NoCd,
351+
first_line: conflict.name.line,
352+
second: keyword,
353+
}));
354+
}
355+
}
356+
_ => {}
357+
}
358+
}
359+
336360
Ok(())
337361
}
338362

src/compile_error.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,17 @@ impl Display for CompileError<'_> {
246246
f,
247247
"Recipe `{recipe}` has both `[no-cd]` and `[working-directory]` attributes"
248248
),
249+
NoCdAndWorkingDirectorySetting {
250+
first,
251+
first_line,
252+
second,
253+
} => write!(
254+
f,
255+
"Setting `{}` first set on line {} is incompatible with setting `{}`",
256+
first.lexeme(),
257+
first_line.ordinal(),
258+
second.lexeme()
259+
),
249260
OptionNameContainsEqualSign { parameter } => {
250261
write!(
251262
f,

src/compile_error_kind.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ pub(crate) enum CompileErrorKind<'src> {
109109
OptionNameEmpty {
110110
parameter: String,
111111
},
112+
NoCdAndWorkingDirectorySetting {
113+
first: Keyword,
114+
first_line: usize,
115+
second: Keyword,
116+
},
112117
ParameterFollowsVariadicParameter {
113118
parameter: &'src str,
114119
},

src/evaluator.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ impl<'src, 'run> Evaluator<'src, 'run> {
104104
Setting::IgnoreComments(value) => {
105105
settings.ignore_comments = value;
106106
}
107+
Setting::NoCd(value) => {
108+
settings.no_cd = value;
109+
}
110+
Setting::NoCdStrict(value) => {
111+
settings.no_cd_strict = value;
112+
}
107113
Setting::NoExitMessage(value) => {
108114
settings.no_exit_message = value;
109115
}
@@ -445,7 +451,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
445451
cmd
446452
.arg(command)
447453
.args(args)
448-
.current_dir(context.working_directory())
454+
.current_dir(context.path_working_directory())
449455
.export(
450456
&context.module.settings,
451457
context.dotenv,

src/execution_context.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,28 @@ impl<'src: 'run, 'run> ExecutionContext<'src, 'run> {
4141

4242
pub(crate) fn working_directory(&self) -> PathBuf {
4343
let base = if self.module.is_submodule() {
44-
&self.module.working_directory
44+
self
45+
.module
46+
.source
47+
.parent()
48+
.map(Path::to_path_buf)
49+
.unwrap_or_else(|| self.search.working_directory.clone())
4550
} else {
46-
&self.search.working_directory
51+
self.search.working_directory.clone()
4752
};
4853

4954
if let Some(setting) = &self.module.settings.working_directory {
5055
base.join(setting)
5156
} else {
52-
base.into()
57+
base
58+
}
59+
}
60+
61+
pub(crate) fn path_working_directory(&self) -> PathBuf {
62+
if self.module.settings.no_cd && self.module.settings.no_cd_strict {
63+
self.config.invocation_directory.clone()
64+
} else {
65+
self.working_directory()
5366
}
5467
}
5568
}

src/function.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ impl Function {
132132
fn absolute_path(context: Context, path: &str) -> FunctionResult {
133133
let abs_path_unchecked = context
134134
.execution_context
135-
.working_directory()
135+
.path_working_directory()
136136
.join(path)
137137
.lexiclean();
138138
match abs_path_unchecked.to_str() {
@@ -162,7 +162,7 @@ fn blake3(_context: Context, s: &str) -> FunctionResult {
162162
}
163163

164164
fn blake3_file(context: Context, path: &str) -> FunctionResult {
165-
let path = context.execution_context.working_directory().join(path);
165+
let path = context.execution_context.path_working_directory().join(path);
166166
let mut hasher = blake3::Hasher::new();
167167
hasher
168168
.update_mmap_rayon(&path)
@@ -171,7 +171,9 @@ fn blake3_file(context: Context, path: &str) -> FunctionResult {
171171
}
172172

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

177179
canonical.to_str().map(str::to_string).ok_or_else(|| {
@@ -486,7 +488,7 @@ fn path_exists(context: Context, path: &str) -> FunctionResult {
486488
Ok(
487489
context
488490
.execution_context
489-
.working_directory()
491+
.path_working_directory()
490492
.join(path)
491493
.exists()
492494
.to_string(),
@@ -498,7 +500,7 @@ fn quote(_context: Context, s: &str) -> FunctionResult {
498500
}
499501

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

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

530532
fn sha256_file(context: Context, path: &str) -> FunctionResult {
531533
use sha2::{Digest, Sha256};
532-
let path = context.execution_context.working_directory().join(path);
534+
let path = context.execution_context.path_working_directory().join(path);
533535
let mut hasher = Sha256::new();
534536
let mut file =
535537
fs::File::open(&path).map_err(|err| format!("Failed to open `{}`: {err}", path.display()))?;

src/keyword.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pub(crate) enum Keyword {
2121
IgnoreComments,
2222
Import,
2323
Mod,
24+
NoCd,
25+
NoCdStrict,
2426
NoExitMessage,
2527
PositionalArguments,
2628
Quiet,

src/node.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ impl<'src> Node<'src> for Set<'src> {
317317
| Setting::DotenvRequired(value)
318318
| Setting::Export(value)
319319
| Setting::Fallback(value)
320+
| Setting::NoCd(value)
321+
| Setting::NoCdStrict(value)
320322
| Setting::NoExitMessage(value)
321323
| Setting::PositionalArguments(value)
322324
| Setting::Quiet(value)

src/parser.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,6 +1336,8 @@ impl<'run, 'src> Parser<'run, 'src> {
13361336
Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)),
13371337
Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)),
13381338
Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),
1339+
Keyword::NoCd => Some(Setting::NoCd(self.parse_set_bool()?)),
1340+
Keyword::NoCdStrict => Some(Setting::NoCdStrict(self.parse_set_bool()?)),
13391341
Keyword::NoExitMessage => Some(Setting::NoExitMessage(self.parse_set_bool()?)),
13401342
Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),
13411343
Keyword::Quiet => Some(Setting::Quiet(self.parse_set_bool()?)),
@@ -2540,6 +2542,18 @@ mod tests {
25402542
tree: (justfile (set quiet false)),
25412543
}
25422544

2545+
test! {
2546+
name: set_no_cd,
2547+
text: "set no-cd := true",
2548+
tree: (justfile (set no_cd true)),
2549+
}
2550+
2551+
test! {
2552+
name: set_no_cd_strict,
2553+
text: "set no-cd-strict := true",
2554+
tree: (justfile (set no_cd_strict true)),
2555+
}
2556+
25432557
test! {
25442558
name: set_positional_arguments_false,
25452559
text: "set positional-arguments := false",

0 commit comments

Comments
 (0)