diff --git a/GRAMMAR.md b/GRAMMAR.md
index c05091c918..93c8c1855a 100644
--- a/GRAMMAR.md
+++ b/GRAMMAR.md
@@ -84,6 +84,7 @@ setting : 'allow-duplicate-recipes' boolean?
| 'fallback' boolean?
| 'guards' boolean?
| 'ignore-comments' boolean?
+ | 'indentation' ':=' string
| 'lazy' boolean?
| 'lists' boolean?
| 'minimum-version' ':=' string
diff --git a/README.md b/README.md
index a40e6a3b4e..f773bebec6 100644
--- a/README.md
+++ b/README.md
@@ -4502,6 +4502,14 @@ stdout.
Note that formatting is not covered by any backwards compatibility guarantee
and is subject to change from time to time.
+Recipe bodies are indented with four spaces by default. This can be changed
+with the `--indentation` command-line option, the `JUST_INDENTATION`
+environment variable, or the `indentation` setting:
+
+```just
+set indentation := " "
+```
+
Invoking `just --fmt --check` runs `--fmt` in check mode. Instead of
overwriting the `justfile`, `just` will exit with an exit code of 0 if it is
formatted correctly, and will exit with 1 and print a diff if it is not.
@@ -4794,6 +4802,7 @@ foo:
| `fallback` | boolean | `false` | Search for `justfile` in parent directory if the first recipe on the command line is not found. |
| `guards`1.47.0 | boolean | `false` | Enable the `?` guard sigil on recipe lines. See [sigils](#sigils). |
| `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. |
+| `indentation`master | string | - | Set recipe body indentation used when formatting with `--fmt` or `--dump`. |
| `lazy`1.47.0 | boolean | `false` | Don't evaluate unused variables. |
| `lists`1.53.0 | boolean | `false` | Values may be lists of strings instead of strings. Currently unstable. |
| `minimum-version`1.55.0 | string | - | Error if `just` is older than `minimum-version`. Accepts a string of the form `MAJOR.MINOR.PATCH`, e.g., `"1.55.0"`. |
diff --git a/src/arguments.rs b/src/arguments.rs
index 8d4bf78616..d1700d2851 100644
--- a/src/arguments.rs
+++ b/src/arguments.rs
@@ -186,12 +186,11 @@ pub struct Arguments {
)]
pub(crate) highlight: bool,
#[arg(
- default_value = " ",
env = "JUST_INDENTATION",
help = "Indent recipes bodies with ",
long
)]
- pub(crate) indentation: Indentation,
+ pub(crate) indentation: Option,
#[arg(
add = ArgValueCompleter::new(PathCompleter::file()),
env = "JUST_JUSTFILE",
diff --git a/src/ast.rs b/src/ast.rs
index eb63468168..e2a8180c51 100644
--- a/src/ast.rs
+++ b/src/ast.rs
@@ -13,6 +13,20 @@ pub(crate) struct Ast<'src> {
pub(crate) working_directory: PathBuf,
}
+impl Ast<'_> {
+ pub(crate) fn indentation(&self) -> Option {
+ self.items.iter().find_map(|item| {
+ if let Item::Setting(set) = item
+ && let Setting::Indentation(_, indentation) = set.value
+ {
+ Some(indentation)
+ } else {
+ None
+ }
+ })
+ }
+}
+
impl ColorDisplay for Ast<'_> {
fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
let mut newlines = 0;
diff --git a/src/color.rs b/src/color.rs
index 631b9dc714..b810758616 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -175,7 +175,14 @@ impl Color {
}
}
- pub(crate) fn use_color(self, use_color: UseColor) -> Self {
+ pub(crate) fn with_use_color(self, use_color: UseColor) -> Self {
Self { use_color, ..self }
}
+
+ pub(crate) fn with_indentation(self, indentation: Indentation) -> Self {
+ Self {
+ indentation,
+ ..self
+ }
+ }
}
diff --git a/src/compile_error.rs b/src/compile_error.rs
index a12796814e..8cfa103c87 100644
--- a/src/compile_error.rs
+++ b/src/compile_error.rs
@@ -278,6 +278,9 @@ impl Display for CompileError<'_> {
"shell recipe `{recipe}` has script recipe attribute `{}`",
attribute.name(),
),
+ InvalidIndentation { message } => {
+ write!(f, "{message}")
+ }
InvalidMinimumVersion { source, version } => {
write!(
f,
@@ -305,12 +308,6 @@ impl Display for CompileError<'_> {
f,
"justfile requires just {minimum} or later, but using {current}",
),
- MinimumVersionExpression => {
- write!(
- f,
- "`minimum-version` setting must be a plain string literal"
- )
- }
MismatchedClosingDelimiter {
open,
open_line,
@@ -372,6 +369,9 @@ impl Display for CompileError<'_> {
f,
"recipe `{recipe}` has both `[script]` and `[shell]` attributes"
),
+ SettingExpression { setting } => {
+ write!(f, "`{setting}` setting must be a plain string literal")
+ }
ShellExpansion { err } => write!(f, "shell expansion failed: {err}"),
ShortOptionWithMultipleCharacters { parameter } => {
write!(
diff --git a/src/compile_error_kind.rs b/src/compile_error_kind.rs
index 041312e2b7..fcb7a300a6 100644
--- a/src/compile_error_kind.rs
+++ b/src/compile_error_kind.rs
@@ -139,6 +139,9 @@ pub(crate) enum CompileErrorKind<'src> {
InvalidEscapeSequence {
character: char,
},
+ InvalidIndentation {
+ message: &'static str,
+ },
InvalidMinimumVersion {
source: &'static str,
version: String,
@@ -158,7 +161,6 @@ pub(crate) enum CompileErrorKind<'src> {
current: Version,
minimum: Version,
},
- MinimumVersionExpression,
MismatchedClosingDelimiter {
close: Delimiter,
open: Delimiter,
@@ -192,6 +194,9 @@ pub(crate) enum CompileErrorKind<'src> {
ScriptAndShellAttribute {
recipe: &'src str,
},
+ SettingExpression {
+ setting: Keyword,
+ },
ShellExpansion {
err: shellexpand::LookupError,
},
diff --git a/src/config.rs b/src/config.rs
index f82fea8633..771e5dce23 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -18,6 +18,7 @@ pub(crate) struct Config {
pub(crate) explain: bool,
pub(crate) groups: Vec,
pub(crate) highlight: bool,
+ pub(crate) indentation: Option,
pub(crate) invocation_directory: PathBuf,
pub(crate) justfile_names: Option>,
pub(crate) list_heading: String,
@@ -63,6 +64,7 @@ impl Config {
explain: false,
groups: Vec::new(),
highlight: true,
+ indentation: None,
invocation_directory: env::current_dir().context(config_error::CurrentDir)?,
justfile_names: None,
list_heading: Arguments::DEFAULT_LIST_HEADING.into(),
@@ -315,7 +317,7 @@ impl Config {
}
let unstable = arguments.unstable || subcommand == Subcommand::Summary;
- let color = Color::new(arguments.indentation, arguments.color);
+ let color = Color::new(arguments.indentation.unwrap_or_default(), arguments.color);
let invocation_directory = env::current_dir().context(config_error::CurrentDir)?;
@@ -338,6 +340,7 @@ impl Config {
explain: arguments.explain,
groups: arguments.group,
highlight: !arguments.no_highlight,
+ indentation: arguments.indentation,
invocation_directory,
justfile_names: arguments.justfile_names,
list_heading: arguments.list_heading,
diff --git a/src/evaluator.rs b/src/evaluator.rs
index 73c69c2fb8..343c641687 100644
--- a/src/evaluator.rs
+++ b/src/evaluator.rs
@@ -107,6 +107,9 @@ impl<'src, 'run> Evaluator<'src, 'run> {
Setting::IgnoreComments(value) => {
settings.ignore_comments = value;
}
+ Setting::Indentation(_, indentation) => {
+ settings.indentation = Some(indentation);
+ }
Setting::Lazy(value) => {
settings.lazy = value;
}
diff --git a/src/indentation.rs b/src/indentation.rs
index fda2e3481e..53edd01245 100644
--- a/src/indentation.rs
+++ b/src/indentation.rs
@@ -24,6 +24,15 @@ impl Display for Indentation {
}
}
+impl Serialize for Indentation {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: Serializer,
+ {
+ serializer.collect_str(self)
+ }
+}
+
impl FromStr for Indentation {
type Err = &'static str;
diff --git a/src/keyword.rs b/src/keyword.rs
index 038d4a2d30..5404eef880 100644
--- a/src/keyword.rs
+++ b/src/keyword.rs
@@ -25,6 +25,7 @@ pub(crate) enum Keyword {
If,
IgnoreComments,
Import,
+ Indentation,
Lazy,
Lists,
MinimumVersion,
diff --git a/src/node.rs b/src/node.rs
index 540854d75e..00e18a5e56 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -318,11 +318,13 @@ impl<'src> Node<'src> for Set<'src> {
Setting::DotenvCommand(value)
| Setting::DotenvFilename(value)
| Setting::DotenvPath(value)
- | Setting::MinimumVersion(value)
| Setting::Tempdir(value)
| Setting::WorkingDirectory(value) => {
set.push_mut(value.tree());
}
+ Setting::Indentation(value, _) | Setting::MinimumVersion(value) => {
+ set.push_mut(Tree::string(&value.cooked));
+ }
Setting::ScriptInterpreter(Interpreter { command, arguments })
| Setting::Shell(Interpreter { command, arguments })
| Setting::WindowsShell(Interpreter { command, arguments }) => {
diff --git a/src/parser.rs b/src/parser.rs
index eb69576071..50c5938be8 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -1614,15 +1614,37 @@ impl<'run, 'src> Parser<'run, 'src> {
Keyword::DotenvCommand => Some(Setting::DotenvCommand(self.parse_expression()?)),
Keyword::DotenvFilename => Some(Setting::DotenvFilename(self.parse_expression()?)),
Keyword::DotenvPath => Some(Setting::DotenvPath(self.parse_expression()?)),
+ Keyword::Indentation => {
+ let expression = self.parse_expression()?;
+
+ let Expression::StringLiteral { string_literal } = expression else {
+ return Err(name.error(CompileErrorKind::SettingExpression { setting: keyword }));
+ };
+
+ if string_literal.expand || string_literal.kind.indented || string_literal.part.is_some() {
+ return Err(name.error(CompileErrorKind::SettingExpression { setting: keyword }));
+ }
+
+ let indentation = string_literal
+ .cooked
+ .parse::()
+ .map_err(|message| {
+ string_literal
+ .token
+ .error(CompileErrorKind::InvalidIndentation { message })
+ })?;
+
+ Some(Setting::Indentation(string_literal, indentation))
+ }
Keyword::MinimumVersion => {
let expression = self.parse_expression()?;
- let Expression::StringLiteral { string_literal } = &expression else {
- return Err(name.error(CompileErrorKind::MinimumVersionExpression));
+ let Expression::StringLiteral { string_literal } = expression else {
+ return Err(name.error(CompileErrorKind::SettingExpression { setting: keyword }));
};
if string_literal.expand || string_literal.kind.indented || string_literal.part.is_some() {
- return Err(name.error(CompileErrorKind::MinimumVersionExpression));
+ return Err(name.error(CompileErrorKind::SettingExpression { setting: keyword }));
}
let minimum = string_literal.cooked.parse::().map_err(|source| {
@@ -1643,7 +1665,7 @@ impl<'run, 'src> Parser<'run, 'src> {
);
}
- Some(Setting::MinimumVersion(expression))
+ Some(Setting::MinimumVersion(string_literal))
}
Keyword::ScriptInterpreter => Some(Setting::ScriptInterpreter(self.parse_interpreter()?)),
Keyword::Shell => Some(Setting::Shell(self.parse_interpreter()?)),
diff --git a/src/setting.rs b/src/setting.rs
index 65aa666a94..7966a02507 100644
--- a/src/setting.rs
+++ b/src/setting.rs
@@ -16,9 +16,10 @@ pub(crate) enum Setting<'src> {
Fallback(bool),
Guards(bool),
IgnoreComments(bool),
+ Indentation(StringLiteral<'src>, Indentation),
Lazy(bool),
Lists(bool),
- MinimumVersion(Expression<'src>),
+ MinimumVersion(StringLiteral<'src>),
NoCd(bool),
NoExitMessage(bool),
PositionalArguments(bool),
@@ -57,9 +58,9 @@ impl<'src> Setting<'src> {
Self::DotenvCommand(_value)
| Self::DotenvFilename(_value)
| Self::DotenvPath(_value)
- | Self::MinimumVersion(_value)
| Self::Tempdir(_value)
| Self::WorkingDirectory(_value) => false,
+ Self::Indentation(..) | Self::MinimumVersion(_) => false,
Self::ScriptInterpreter(_value) | Self::Shell(_value) | Self::WindowsShell(_value) => false,
}
}
@@ -131,11 +132,11 @@ impl Display for Setting<'_> {
Self::DotenvCommand(value)
| Self::DotenvFilename(value)
| Self::DotenvPath(value)
- | Self::MinimumVersion(value)
| Self::Tempdir(value)
| Self::WorkingDirectory(value) => {
write!(f, "{value}")
}
+ Self::Indentation(value, _) | Self::MinimumVersion(value) => write!(f, "{value}"),
Self::ScriptInterpreter(value) | Self::Shell(value) | Self::WindowsShell(value) => {
write!(f, "[{value}]")
}
diff --git a/src/settings.rs b/src/settings.rs
index 6337e75a15..60a36cedb8 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -34,6 +34,7 @@ pub(crate) struct Settings {
pub(crate) fallback: bool,
pub(crate) guards: bool,
pub(crate) ignore_comments: bool,
+ pub(crate) indentation: Option,
pub(crate) lazy: bool,
pub(crate) lists: bool,
pub(crate) no_cd: bool,
diff --git a/src/subcommand.rs b/src/subcommand.rs
index 91d7263bb9..5b898b50de 100644
--- a/src/subcommand.rs
+++ b/src/subcommand.rs
@@ -431,12 +431,22 @@ impl Subcommand {
.map_err(|source| Error::DumpJson { source })?;
println!();
}
- DumpFormat::Just => print!(
- "{}",
- compilation
- .root_ast()
- .color_display(config.color.use_color(UseColor::Never))
- ),
+ DumpFormat::Just => {
+ print!(
+ "{}",
+ compilation.root_ast().color_display(
+ config
+ .color
+ .with_use_color(UseColor::Never)
+ .with_indentation(
+ config
+ .indentation
+ .or(compilation.justfile.settings.indentation)
+ .unwrap_or_default()
+ )
+ )
+ );
+ }
}
Ok(())
}
@@ -476,7 +486,12 @@ impl Subcommand {
)?;
let formatted = ast
- .color_display(config.color.use_color(UseColor::Never))
+ .color_display(
+ config
+ .color
+ .with_use_color(UseColor::Never)
+ .with_indentation(config.indentation.or(ast.indentation()).unwrap_or_default()),
+ )
.to_string();
if config.check {
@@ -516,7 +531,7 @@ impl Subcommand {
})?;
if config.verbosity.loud() {
- eprintln!("Wrote justfile to `{}`", search.justfile.display());
+ eprintln!("wrote justfile to `{}`", search.justfile.display());
}
}
@@ -541,7 +556,7 @@ impl Subcommand {
}
if config.verbosity.loud() {
- eprintln!("Wrote justfile to `{}`", search.justfile.display());
+ eprintln!("wrote justfile to `{}`", search.justfile.display());
}
Ok(())
diff --git a/tests/ceiling.rs b/tests/ceiling.rs
index 5e6999b113..aec731ed60 100644
--- a/tests/ceiling.rs
+++ b/tests/ceiling.rs
@@ -76,9 +76,9 @@ fn justfile_init_search_stops_at_ceiling_dir() {
.current_dir("foo/bar")
.args(["--init", "--ceiling", ceiling.to_str().unwrap()])
.stderr_regex(if cfg!(windows) {
- r"Wrote justfile to `.*\\foo\\bar\\justfile`\n"
+ r"wrote justfile to `.*\\foo\\bar\\justfile`\n"
} else {
- "Wrote justfile to `.*/foo/bar/justfile`\n"
+ "wrote justfile to `.*/foo/bar/justfile`\n"
})
.success();
diff --git a/tests/format.rs b/tests/format.rs
index 7e0f1f864c..412f20aa96 100644
--- a/tests/format.rs
+++ b/tests/format.rs
@@ -1577,6 +1577,159 @@ fn indentation_env() {
.success();
}
+#[test]
+fn indentation_setting_dump() {
+ Test::new()
+ .arg("--dump")
+ .justfile(
+ "
+ set indentation := ' '
+
+ foo:
+ echo bar
+ ",
+ )
+ .stdout(
+ "
+ set indentation := ' '
+
+ foo:
+ echo bar
+ ",
+ )
+ .success();
+}
+
+#[test]
+fn indentation_setting_format() {
+ let output = Test::new()
+ .arg("--fmt")
+ .justfile(
+ "
+ set indentation := ' '
+
+ foo:
+ echo bar
+ ",
+ )
+ .stderr_regex("wrote justfile to `.*justfile`\n")
+ .success();
+
+ assert_eq!(
+ fs::read_to_string(output.tempdir.path().join("justfile")).unwrap(),
+ "set indentation := ' '
+
+foo:
+ echo bar
+",
+ );
+}
+
+#[test]
+fn indentation_flag_overrides_setting_dump() {
+ Test::new()
+ .args(["--dump", "--indentation", " "])
+ .justfile(
+ "
+ set indentation := ' '
+
+ foo:
+ echo bar
+ ",
+ )
+ .stdout(
+ "
+ set indentation := ' '
+
+ foo:
+ echo bar
+ ",
+ )
+ .success();
+}
+
+#[test]
+fn indentation_flag_overrides_setting_format() {
+ let output = Test::new()
+ .args(["--fmt", "--indentation", " "])
+ .justfile(
+ "
+ set indentation := ' '
+
+ foo:
+ echo bar
+ ",
+ )
+ .stderr_regex("wrote justfile to `.*justfile`\n")
+ .success();
+
+ assert_eq!(
+ fs::read_to_string(output.tempdir.path().join("justfile")).unwrap(),
+ "set indentation := ' '
+
+foo:
+ echo bar
+",
+ );
+}
+
+#[test]
+fn indentation_setting_must_be_string_literal() {
+ Test::new()
+ .justfile("set indentation := (' ' + ' ')")
+ .stderr(
+ "
+ error: `indentation` setting must be a plain string literal
+ ——▶ justfile:1:5
+ │
+ 1 │ set indentation := (' ' + ' ')
+ │ ^^^^^^^^^^^
+ ",
+ )
+ .failure();
+}
+
+#[test]
+fn indentation_setting_invalid_values() {
+ #[track_caller]
+ fn case(justfile: &str, stderr: &str) {
+ Test::new().justfile(justfile).stderr(stderr).failure();
+ }
+
+ case(
+ "set indentation := ''",
+ "
+ error: indentation must not be empty
+ ——▶ justfile:1:20
+ │
+ 1 │ set indentation := ''
+ │ ^^
+ ",
+ );
+
+ case(
+ "set indentation := 'x'",
+ "
+ error: indentation must be spaces or tabs
+ ——▶ justfile:1:20
+ │
+ 1 │ set indentation := 'x'
+ │ ^^^
+ ",
+ );
+
+ case(
+ "set indentation := \" \\t\"",
+ "
+ error: indentation may not be mixed
+ ——▶ justfile:1:20
+ │
+ 1 │ set indentation := \" \\t\"
+ │ ^^^^^
+ ",
+ );
+}
+
#[test]
fn multi_line_comments_before_recipes_are_not_broken_up() {
assert_dump(
diff --git a/tests/init.rs b/tests/init.rs
index d2e8f4ad87..b6b39a21b0 100644
--- a/tests/init.rs
+++ b/tests/init.rs
@@ -22,7 +22,7 @@ fn current_dir() {
fn exists() {
Test::new()
.arg("--init")
- .stderr_regex("Wrote justfile to `.*`\n")
+ .stderr_regex("wrote justfile to `.*`\n")
.success()
.test()
.arg("--init")
@@ -55,7 +55,7 @@ fn invocation_directory() {
let justfile_path = test.justfile_path();
let _tmp = test
- .stderr_regex("Wrote justfile to `.*`\n")
+ .stderr_regex("wrote justfile to `.*`\n")
.arg("--init")
.success();
@@ -170,7 +170,7 @@ fn justfile_name_from_invocation_directory() {
Test::new()
.create_dir(".git")
.args(["--init", "--justfile-name", "foo"])
- .stderr_regex("Wrote justfile to `.*`\n")
+ .stderr_regex("wrote justfile to `.*`\n")
.expect_file("foo", INIT_JUSTFILE)
.success();
}
@@ -180,7 +180,7 @@ fn justfile_name_from_search_directory() {
Test::new()
.create_dir("sub/.git")
.args(["--init", "--justfile-name", "foo", "sub/"])
- .stderr_regex("Wrote justfile to `.*`\n")
+ .stderr_regex("wrote justfile to `.*`\n")
.expect_file("sub/foo", INIT_JUSTFILE)
.success();
}
@@ -189,7 +189,7 @@ fn justfile_name_from_search_directory() {
fn fmt_compatibility() {
Test::new()
.arg("--init")
- .stderr_regex("Wrote justfile to `.*`\n")
+ .stderr_regex("wrote justfile to `.*`\n")
.success()
.test()
.arg("--unstable")
diff --git a/tests/json.rs b/tests/json.rs
index b9c43d5cd7..106366aa81 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -107,6 +107,7 @@ struct Settings<'a> {
fallback: bool,
guards: bool,
ignore_comments: bool,
+ indentation: Option<&'a str>,
lazy: bool,
lists: bool,
no_cd: bool,
@@ -702,6 +703,7 @@ fn settings() {
set export
set fallback
set ignore-comments
+ set indentation := \" \"
set positional-arguments
set quiet
set shell := ['a', 'b', 'c']
@@ -731,6 +733,7 @@ fn settings() {
export: true,
fallback: true,
ignore_comments: true,
+ indentation: Some(" "),
positional_arguments: true,
quiet: true,
shell: Some(Interpreter {
diff --git a/tests/unstable.rs b/tests/unstable.rs
index e39953824f..f40292b924 100644
--- a/tests/unstable.rs
+++ b/tests/unstable.rs
@@ -7,7 +7,7 @@ fn set_unstable_true_with_env_var() {
.justfile("# hello")
.args(["--fmt"])
.env("JUST_UNSTABLE", val)
- .stderr_regex("Wrote justfile to `.*`\n")
+ .stderr_regex("wrote justfile to `.*`\n")
.success();
}
}
@@ -38,7 +38,7 @@ fn set_unstable_with_setting() {
Test::new()
.justfile("set unstable")
.arg("--fmt")
- .stderr_regex("Wrote justfile to .*")
+ .stderr_regex("wrote justfile to .*")
.success();
}