Skip to content

Commit f540d51

Browse files
committed
add setting no-cd
1 parent efa727c commit f540d51

18 files changed

Lines changed: 254 additions & 50 deletions

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,12 @@ $ just bar
935935
/subdir
936936
```
937937

938+
To apply the same behavior to every recipe in a module, use `set no-cd := true`.
939+
This setting is module-local, so imported modules choose their own default, and
940+
it can't appear alongside `set working-directory` in the same `justfile`.
941+
Recipe-level attributes still take precedence: `[working-directory(...)]`
942+
overrides both, and `[no-cd]` on a recipe overrides `set working-directory`.
943+
938944
You can override the working directory for all recipes with
939945
`set working-directory := '…'`:
940946

@@ -1026,6 +1032,7 @@ foo:
10261032
| `export` | boolean | `false` | Export all variables as environment variables. |
10271033
| `fallback` | boolean | `false` | Search `justfile` in parent directory if the first recipe on the command line is not found. |
10281034
| `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. |
1035+
| `no-cd` | boolean | `false` | Don't change directory before executing recipes and evaluating backticks, unless overridden by recipe attributes. |
10291036
| `positional-arguments` | boolean | `false` | Pass positional arguments. |
10301037
| `quiet` | boolean | `false` | Disable echoing recipe lines before executing. |
10311038
| `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
@@ -311,6 +311,30 @@ impl<'run, 'src> Analyzer<'run, 'src> {
311311
}));
312312
}
313313

314+
if let Some(keyword) = Keyword::from_lexeme(set.name.lexeme()) {
315+
match keyword {
316+
Keyword::NoCd => {
317+
if let Some(conflict) = self.sets.get(Keyword::WorkingDirectory.lexeme()) {
318+
return Err(set.name.error(NoCdAndWorkingDirectorySetting {
319+
first: Keyword::WorkingDirectory,
320+
first_line: conflict.name.line,
321+
second: keyword,
322+
}));
323+
}
324+
}
325+
Keyword::WorkingDirectory => {
326+
if let Some(conflict) = self.sets.get(Keyword::NoCd.lexeme()) {
327+
return Err(set.name.error(NoCdAndWorkingDirectorySetting {
328+
first: Keyword::NoCd,
329+
first_line: conflict.name.line,
330+
second: keyword,
331+
}));
332+
}
333+
}
334+
_ => {}
335+
}
336+
}
337+
314338
Ok(())
315339
}
316340

src/compile_error.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,17 @@ impl Display for CompileError<'_> {
212212
f,
213213
"Recipe `{recipe}` has both `[no-cd]` and `[working-directory]` attributes"
214214
),
215+
NoCdAndWorkingDirectorySetting {
216+
first,
217+
first_line,
218+
second,
219+
} => write!(
220+
f,
221+
"Setting `{}` first set on line {} is incompatible with setting `{}`",
222+
first.lexeme(),
223+
first_line.ordinal(),
224+
second.lexeme()
225+
),
215226
ParameterFollowsVariadicParameter { parameter } => {
216227
write!(f, "Parameter `{parameter}` follows variadic parameter")
217228
}

src/compile_error_kind.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ pub(crate) enum CompileErrorKind<'src> {
9090
NoCdAndWorkingDirectoryAttribute {
9191
recipe: &'src str,
9292
},
93+
NoCdAndWorkingDirectorySetting {
94+
first: Keyword,
95+
first_line: usize,
96+
second: Keyword,
97+
},
9398
ParameterFollowsVariadicParameter {
9499
parameter: &'src str,
95100
},

src/evaluator.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
1-
use super::*;
1+
use {super::*, std::path::Path};
22

33
pub(crate) struct Evaluator<'src: 'run, 'run> {
44
pub(crate) assignments: Option<&'run Table<'src, Assignment<'src>>>,
55
pub(crate) context: ExecutionContext<'src, 'run>,
66
pub(crate) is_dependency: bool,
77
pub(crate) scope: Scope<'src, 'run>,
8+
pub(crate) working_directory: Option<PathBuf>,
89
}
910

1011
impl<'src, 'run> Evaluator<'src, 'run> {
12+
pub(crate) fn working_directory(&self) -> Option<&Path> {
13+
self.working_directory.as_deref()
14+
}
15+
16+
pub(crate) fn working_directory_or_invocation(&self) -> PathBuf {
17+
self
18+
.working_directory
19+
.clone()
20+
.unwrap_or_else(|| self.context.config.invocation_directory.clone())
21+
}
22+
1123
pub(crate) fn evaluate_assignments(
1224
config: &'run Config,
1325
dotenv: &'run BTreeMap<String, String>,
@@ -55,6 +67,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
5567
assignments: Some(&module.assignments),
5668
scope,
5769
is_dependency: false,
70+
working_directory: context.module_default_working_directory(),
5871
};
5972

6073
for assignment in module.assignments.values() {
@@ -273,10 +286,13 @@ impl<'src, 'run> Evaluator<'src, 'run> {
273286
.settings
274287
.shell_command(self.context.config);
275288

289+
cmd.arg(command).args(args);
290+
291+
if let Some(working_directory) = self.working_directory() {
292+
cmd.current_dir(working_directory);
293+
}
294+
276295
cmd
277-
.arg(command)
278-
.args(args)
279-
.current_dir(self.context.working_directory())
280296
.export(
281297
&self.context.module.settings,
282298
self.context.dotenv,
@@ -325,8 +341,9 @@ impl<'src, 'run> Evaluator<'src, 'run> {
325341
arguments: &[String],
326342
parameters: &[Parameter<'src>],
327343
scope: &'run Scope<'src, 'run>,
344+
working_directory: Option<PathBuf>,
328345
) -> RunResult<'src, (Scope<'src, 'run>, Vec<String>)> {
329-
let mut evaluator = Self::new(context, is_dependency, scope);
346+
let mut evaluator = Self::new(context, is_dependency, scope, working_directory);
330347

331348
let mut positional = Vec::new();
332349

@@ -374,12 +391,14 @@ impl<'src, 'run> Evaluator<'src, 'run> {
374391
context: &ExecutionContext<'src, 'run>,
375392
is_dependency: bool,
376393
scope: &'run Scope<'src, 'run>,
394+
working_directory: Option<PathBuf>,
377395
) -> Self {
378396
Self {
379397
assignments: None,
380398
context: *context,
381399
is_dependency,
382400
scope: scope.child(),
401+
working_directory: working_directory.or_else(|| context.module_default_working_directory()),
383402
}
384403
}
385404
}

src/execution_context.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,34 @@ impl<'src: 'run, 'run> ExecutionContext<'src, 'run> {
3939
})
4040
}
4141

42-
pub(crate) fn working_directory(&self) -> PathBuf {
43-
let base = if self.module.is_submodule() {
44-
&self.module.working_directory
42+
pub(crate) fn module_base_directory(&self) -> PathBuf {
43+
if self.module.is_submodule() {
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
47-
};
51+
self.search.working_directory.clone()
52+
}
53+
}
54+
55+
pub(crate) fn module_working_directory(&self) -> PathBuf {
56+
let base = self.module_base_directory();
4857

4958
if let Some(setting) = &self.module.settings.working_directory {
5059
base.join(setting)
5160
} else {
52-
base.into()
61+
base
62+
}
63+
}
64+
65+
pub(crate) fn module_default_working_directory(&self) -> Option<PathBuf> {
66+
if self.module.settings.no_cd {
67+
None
68+
} else {
69+
Some(self.module_working_directory())
5370
}
5471
}
5572
}

src/function.rs

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -134,17 +134,13 @@ impl Function {
134134
}
135135

136136
fn absolute_path(context: Context, path: &str) -> FunctionResult {
137-
let abs_path_unchecked = context
138-
.evaluator
139-
.context
140-
.working_directory()
141-
.join(path)
142-
.lexiclean();
137+
let working_directory = context.evaluator.working_directory_or_invocation();
138+
let abs_path_unchecked = working_directory.join(path).lexiclean();
143139
match abs_path_unchecked.to_str() {
144140
Some(absolute_path) => Ok(absolute_path.to_owned()),
145141
None => Err(format!(
146142
"Working directory is not valid unicode: {}",
147-
context.evaluator.context.search.working_directory.display()
143+
working_directory.display()
148144
)),
149145
}
150146
}
@@ -167,7 +163,10 @@ fn blake3(_context: Context, s: &str) -> FunctionResult {
167163
}
168164

169165
fn blake3_file(context: Context, path: &str) -> FunctionResult {
170-
let path = context.evaluator.context.working_directory().join(path);
166+
let path = context
167+
.evaluator
168+
.working_directory_or_invocation()
169+
.join(path);
171170
let mut hasher = blake3::Hasher::new();
172171
hasher
173172
.update_mmap_rayon(&path)
@@ -176,8 +175,13 @@ fn blake3_file(context: Context, path: &str) -> FunctionResult {
176175
}
177176

178177
fn canonicalize(context: Context, path: &str) -> FunctionResult {
179-
let canonical = std::fs::canonicalize(context.evaluator.context.working_directory().join(path))
180-
.map_err(|err| format!("I/O error canonicalizing path: {err}"))?;
178+
let canonical = std::fs::canonicalize(
179+
context
180+
.evaluator
181+
.working_directory_or_invocation()
182+
.join(path),
183+
)
184+
.map_err(|err| format!("I/O error canonicalizing path: {err}"))?;
181185

182186
canonical.to_str().map(str::to_string).ok_or_else(|| {
183187
format!(
@@ -495,8 +499,7 @@ fn path_exists(context: Context, path: &str) -> FunctionResult {
495499
Ok(
496500
context
497501
.evaluator
498-
.context
499-
.working_directory()
502+
.working_directory_or_invocation()
500503
.join(path)
501504
.exists()
502505
.to_string(),
@@ -508,8 +511,13 @@ fn quote(_context: Context, s: &str) -> FunctionResult {
508511
}
509512

510513
fn read(context: Context, filename: &str) -> FunctionResult {
511-
fs::read_to_string(context.evaluator.context.working_directory().join(filename))
512-
.map_err(|err| format!("I/O error reading `{filename}`: {err}"))
514+
fs::read_to_string(
515+
context
516+
.evaluator
517+
.working_directory_or_invocation()
518+
.join(filename),
519+
)
520+
.map_err(|err| format!("I/O error reading `{filename}`: {err}"))
513521
}
514522

515523
fn replace(_context: Context, s: &str, from: &str, to: &str) -> FunctionResult {
@@ -539,7 +547,10 @@ fn sha256(_context: Context, s: &str) -> FunctionResult {
539547

540548
fn sha256_file(context: Context, path: &str) -> FunctionResult {
541549
use sha2::{Digest, Sha256};
542-
let path = context.evaluator.context.working_directory().join(path);
550+
let path = context
551+
.evaluator
552+
.working_directory_or_invocation()
553+
.join(path);
543554
let mut hasher = Sha256::new();
544555
let mut file =
545556
fs::File::open(&path).map_err(|err| format!("Failed to open `{}`: {err}", path.display()))?;

src/justfile.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,17 +343,20 @@ impl<'src> Justfile<'src> {
343343
search,
344344
};
345345

346+
let recipe_working_directory = recipe.working_directory(&context);
347+
346348
let (outer, positional) = Evaluator::evaluate_parameters(
347349
&context,
348350
is_dependency,
349351
arguments,
350352
&recipe.parameters,
351353
scope,
354+
recipe_working_directory.clone(),
352355
)?;
353356

354357
let scope = outer.child();
355358

356-
let mut evaluator = Evaluator::new(&context, true, &scope);
359+
let mut evaluator = Evaluator::new(&context, true, &scope, recipe_working_directory.clone());
357360

358361
Self::run_dependencies(
359362
config,

src/keyword.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub(crate) enum Keyword {
2020
IgnoreComments,
2121
Import,
2222
Mod,
23+
NoCd,
2324
NoExitMessage,
2425
PositionalArguments,
2526
Quiet,

src/node.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ impl<'src> Node<'src> for Set<'src> {
307307
| Setting::DotenvRequired(value)
308308
| Setting::Export(value)
309309
| Setting::Fallback(value)
310+
| Setting::NoCd(value)
310311
| Setting::NoExitMessage(value)
311312
| Setting::PositionalArguments(value)
312313
| Setting::Quiet(value)

0 commit comments

Comments
 (0)