diff --git a/README.md b/README.md
index a992d62..1f5d6ad 100644
--- a/README.md
+++ b/README.md
@@ -379,11 +379,11 @@ There will never be a `just` 2.0. Any desirable backwards-incompatible changes
will be opt-in on a per-`justfile` basis, so users may migrate at their
leisure.
-Features that aren't yet ready for stabilization are gated behind the
-`--unstable` flag. Features enabled by `--unstable` may change in backwards
-incompatible ways at any time. Unstable features can also be enabled by setting
-the environment variable `JUST_UNSTABLE` to any value other than `false`, `0`,
-or the empty string.
+Features that aren't yet ready for stabilization are marked as unstable and may
+be changed or removed at any time. Using unstable features produces an error by
+default, which can be suppressed with by passing the `--unstable` flag,
+`set unstable`, or setting the environment variable `JUST_UNSTABLE`, to any
+value other than `false`, `0`, or the empty string.
Editor Support
--------------
@@ -820,6 +820,7 @@ foo:
| `positional-arguments` | boolean | `false` | Pass positional arguments. |
| `shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. |
| `tempdir` | string | - | Create temporary directories in `tempdir` instead of the system default temporary directory. |
+| `unstable`master | boolean | `false` | Enable unstable features. |
| `windows-powershell` | boolean | `false` | Use PowerShell on Windows as default shell. (Deprecated. Use `windows-shell` instead. |
| `windows-shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. |
@@ -3154,8 +3155,8 @@ Missing source files for optional imports do not produce an error.
### Modules1.19.0
A `justfile` can declare modules using `mod` statements. `mod` statements are
-currently unstable, so you'll need to use the `--unstable` flag, or set the
-`JUST_UNSTABLE` environment variable to use them.
+currently unstable, so you'll need to use the `--unstable` flag,
+`set unstable`, or set the `JUST_UNSTABLE` environment variable to use them.
If you have the following `justfile`:
diff --git a/src/analyzer.rs b/src/analyzer.rs
index 928d9fd..b0f447e 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -37,6 +37,8 @@ impl<'src> Analyzer<'src> {
let mut warnings = Vec::new();
+ let mut unstable = BTreeSet::new();
+
let mut modules: Table = Table::new();
let mut unexports: HashSet = HashSet::new();
@@ -92,6 +94,8 @@ impl<'src> Analyzer<'src> {
doc,
..
} => {
+ unstable.insert(Unstable::Modules);
+
if let Some(absolute) = absolute {
define(*name, "module", false)?;
modules.insert(Self::analyze(
@@ -194,6 +198,7 @@ impl<'src> Analyzer<'src> {
settings,
source: root.into(),
unexports,
+ unstable,
warnings,
})
}
diff --git a/src/argument_parser.rs b/src/argument_parser.rs
index 85daccc..091ffd8 100644
--- a/src/argument_parser.rs
+++ b/src/argument_parser.rs
@@ -252,7 +252,7 @@ mod tests {
fs::write(&path, "mod foo").unwrap();
fs::create_dir(tempdir.path().join("foo")).unwrap();
fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap();
- let compilation = Compiler::compile(true, &loader, &path).unwrap();
+ let compilation = Compiler::compile(&loader, &path).unwrap();
assert_eq!(
ArgumentParser::parse_arguments(&compilation.justfile, &["foo", "bar"]).unwrap(),
@@ -271,7 +271,7 @@ mod tests {
fs::write(&path, "mod foo").unwrap();
fs::create_dir(tempdir.path().join("foo")).unwrap();
fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap();
- let compilation = Compiler::compile(true, &loader, &path).unwrap();
+ let compilation = Compiler::compile(&loader, &path).unwrap();
assert_matches!(
ArgumentParser::parse_arguments(&compilation.justfile, &["foo", "zzz"]).unwrap_err(),
@@ -289,7 +289,7 @@ mod tests {
tempdir.write("foo.just", "bar:");
let loader = Loader::new();
- let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
+ let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
assert_matches!(
ArgumentParser::parse_arguments(&compilation.justfile, &["foo::zzz"]).unwrap_err(),
@@ -307,7 +307,7 @@ mod tests {
tempdir.write("foo.just", "bar:");
let loader = Loader::new();
- let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
+ let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
assert_matches!(
ArgumentParser::parse_arguments(&compilation.justfile, &["foo::bar::baz"]).unwrap_err(),
@@ -323,7 +323,7 @@ mod tests {
tempdir.write("justfile", "");
let loader = Loader::new();
- let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
+ let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
assert_matches!(
ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
@@ -337,7 +337,7 @@ mod tests {
tempdir.write("justfile", "foo bar:");
let loader = Loader::new();
- let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
+ let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
assert_matches!(
ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
@@ -355,7 +355,7 @@ mod tests {
tempdir.write("foo.just", "bar:");
let loader = Loader::new();
- let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
+ let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
assert_matches!(
ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
diff --git a/src/compiler.rs b/src/compiler.rs
index 31ad362..a977be5 100644
--- a/src/compiler.rs
+++ b/src/compiler.rs
@@ -4,14 +4,13 @@ pub(crate) struct Compiler;
impl Compiler {
pub(crate) fn compile<'src>(
- unstable: bool,
loader: &'src Loader,
root: &Path,
) -> RunResult<'src, Compilation<'src>> {
let mut asts = HashMap::::new();
+ let mut loaded = Vec::new();
let mut paths = HashMap::::new();
let mut srcs = HashMap::::new();
- let mut loaded = Vec::new();
let mut stack = Vec::new();
stack.push(Source::root(root));
@@ -42,12 +41,6 @@ impl Compiler {
relative,
..
} => {
- if !unstable {
- return Err(Error::Unstable {
- message: "Modules are currently unstable.".into(),
- });
- }
-
let parent = current.path.parent().unwrap();
let import = if let Some(relative) = relative {
@@ -112,9 +105,9 @@ impl Compiler {
Ok(Compilation {
asts,
- srcs,
justfile,
root: root.into(),
+ srcs,
})
}
@@ -225,7 +218,7 @@ recipe_b: recipe_c
let loader = Loader::new();
let justfile_a_path = tmp.path().join("justfile");
- let compilation = Compiler::compile(false, &loader, &justfile_a_path).unwrap();
+ let compilation = Compiler::compile(&loader, &justfile_a_path).unwrap();
assert_eq!(compilation.root_src(), justfile_a);
}
@@ -242,7 +235,7 @@ recipe_b: recipe_c
let loader = Loader::new();
let justfile_a_path = tmp.path().join("justfile");
- let loader_output = Compiler::compile(false, &loader, &justfile_a_path).unwrap_err();
+ let loader_output = Compiler::compile(&loader, &justfile_a_path).unwrap_err();
assert_matches!(loader_output, Error::CircularImport { current, import }
if current == tmp.path().join("subdir").join("b").lexiclean() &&
diff --git a/src/error.rs b/src/error.rs
index ca2acb1..d2e8704 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -460,7 +460,7 @@ impl<'src> ColorDisplay for Error<'src> {
}
}
Unstable { message } => {
- write!(f, "{message} Invoke `just` with the `--unstable` flag to enable unstable features.")?;
+ write!(f, "{message} Invoke `just` with `--unstable`, set the `JUST_UNSTABLE` environment variable, or add `set unstable` to your `justfile` to enable unstable features.")?;
}
WriteJustfile { justfile, io_error } => {
let justfile = justfile.display();
diff --git a/src/justfile.rs b/src/justfile.rs
index 1950392..c5be95d 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -27,6 +27,8 @@ pub(crate) struct Justfile<'src> {
pub(crate) source: PathBuf,
pub(crate) unexports: HashSet,
pub(crate) warnings: Vec,
+ #[serde(skip)]
+ pub(crate) unstable: BTreeSet,
}
impl<'src> Justfile<'src> {
@@ -225,6 +227,22 @@ impl<'src> Justfile<'src> {
Ok(())
}
+ pub(crate) fn check_unstable(&self, config: &Config) -> RunResult<'src> {
+ if !config.unstable && !self.settings.unstable {
+ if let Some(unstable) = self.unstable.iter().next() {
+ return Err(Error::Unstable {
+ message: unstable.message(),
+ });
+ }
+ }
+
+ for module in self.modules.values() {
+ module.check_unstable(config)?;
+ }
+
+ Ok(())
+ }
+
pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias<'src>> {
self.aliases.get(name)
}
diff --git a/src/keyword.rs b/src/keyword.rs
index d57032a..7f8b5b3 100644
--- a/src/keyword.rs
+++ b/src/keyword.rs
@@ -26,6 +26,7 @@ pub(crate) enum Keyword {
Tempdir,
True,
Unexport,
+ Unstable,
WindowsPowershell,
WindowsShell,
X,
diff --git a/src/lib.rs b/src/lib.rs
index 50e54e9..127881e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -42,8 +42,8 @@ pub(crate) use {
shell::Shell, show_whitespace::ShowWhitespace, source::Source, string_kind::StringKind,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
- unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
- verbosity::Verbosity, warning::Warning,
+ unresolved_recipe::UnresolvedRecipe, unstable::Unstable, use_color::UseColor,
+ variables::Variables, verbosity::Verbosity, warning::Warning,
},
camino::Utf8Path,
clap::ValueEnum,
@@ -204,6 +204,7 @@ mod token_kind;
mod unindent;
mod unresolved_dependency;
mod unresolved_recipe;
+mod unstable;
mod use_color;
mod variables;
mod verbosity;
diff --git a/src/node.rs b/src/node.rs
index 2fd73e3..1de9bdc 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -294,6 +294,7 @@ impl<'src> Node<'src> for Set<'src> {
| Setting::Fallback(value)
| Setting::PositionalArguments(value)
| Setting::Quiet(value)
+ | Setting::Unstable(value)
| Setting::WindowsPowerShell(value)
| Setting::IgnoreComments(value) => {
set.push_mut(value.to_string());
diff --git a/src/parser.rs b/src/parser.rs
index 90389a2..6a18d5f 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -936,6 +936,7 @@ impl<'run, 'src> Parser<'run, 'src> {
Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),
Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),
Keyword::Quiet => Some(Setting::Quiet(self.parse_set_bool()?)),
+ Keyword::Unstable => Some(Setting::Unstable(self.parse_set_bool()?)),
Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)),
_ => None,
};
diff --git a/src/setting.rs b/src/setting.rs
index e6cb515..f19dab4 100644
--- a/src/setting.rs
+++ b/src/setting.rs
@@ -15,6 +15,7 @@ pub(crate) enum Setting<'src> {
Quiet(bool),
Shell(Shell<'src>),
Tempdir(String),
+ Unstable(bool),
WindowsPowerShell(bool),
WindowsShell(Shell<'src>),
}
@@ -31,6 +32,7 @@ impl<'src> Display for Setting<'src> {
| Self::IgnoreComments(value)
| Self::PositionalArguments(value)
| Self::Quiet(value)
+ | Self::Unstable(value)
| Self::WindowsPowerShell(value) => write!(f, "{value}"),
Self::Shell(shell) | Self::WindowsShell(shell) => write!(f, "{shell}"),
Self::DotenvFilename(value) | Self::DotenvPath(value) | Self::Tempdir(value) => {
diff --git a/src/settings.rs b/src/settings.rs
index ae6c4b1..338f2cc 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -20,6 +20,7 @@ pub(crate) struct Settings<'src> {
pub(crate) quiet: bool,
pub(crate) shell: Option>,
pub(crate) tempdir: Option,
+ pub(crate) unstable: bool,
pub(crate) windows_powershell: bool,
pub(crate) windows_shell: Option>,
}
@@ -66,6 +67,9 @@ impl<'src> Settings<'src> {
Setting::Shell(shell) => {
settings.shell = Some(shell);
}
+ Setting::Unstable(unstable) => {
+ settings.unstable = unstable;
+ }
Setting::WindowsPowerShell(windows_powershell) => {
settings.windows_powershell = windows_powershell;
}
diff --git a/src/subcommand.rs b/src/subcommand.rs
index 65c80a8..6c405b7 100644
--- a/src/subcommand.rs
+++ b/src/subcommand.rs
@@ -190,7 +190,9 @@ impl Subcommand {
loader: &'src Loader,
search: &Search,
) -> RunResult<'src, Compilation<'src>> {
- let compilation = Compiler::compile(config.unstable, loader, &search.justfile)?;
+ let compilation = Compiler::compile(loader, &search.justfile)?;
+
+ compilation.justfile.check_unstable(config)?;
if config.verbosity.loud() {
for warning in &compilation.justfile.warnings {
diff --git a/src/summary.rs b/src/summary.rs
index 65a28ba..ee3a8d1 100644
--- a/src/summary.rs
+++ b/src/summary.rs
@@ -28,7 +28,7 @@ mod full {
pub fn summary(path: &Path) -> io::Result> {
let loader = Loader::new();
- match Compiler::compile(false, &loader, path) {
+ match Compiler::compile(&loader, path) {
Ok(compilation) => Ok(Ok(Summary::new(&compilation.justfile))),
Err(error) => Ok(Err(if let Error::Compile { compile_error } = error {
compile_error.to_string()
diff --git a/src/unstable.rs b/src/unstable.rs
new file mode 100644
index 0000000..6647fb4
--- /dev/null
+++ b/src/unstable.rs
@@ -0,0 +1,12 @@
+#[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd)]
+pub(crate) enum Unstable {
+ Modules,
+}
+
+impl Unstable {
+ pub(crate) fn message(self) -> String {
+ match self {
+ Self::Modules => "Modules are currently unstable.".into(),
+ }
+ }
+}
diff --git a/tests/fmt.rs b/tests/fmt.rs
index ba050fc..29a12ba 100644
--- a/tests/fmt.rs
+++ b/tests/fmt.rs
@@ -4,10 +4,7 @@ test! {
name: unstable_not_passed,
justfile: "",
args: ("--fmt"),
- stderr: "
- error: The `--fmt` command is currently unstable. \
- Invoke `just` with the `--unstable` flag to enable unstable features.
- ",
+ stderr_regex: "error: The `--fmt` command is currently unstable..*",
status: EXIT_FAILURE,
}
diff --git a/tests/json.rs b/tests/json.rs
index 9b827ca..a88bdef 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -56,7 +56,9 @@ fn alias() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"ignore_comments": false,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -98,6 +100,7 @@ fn assignment() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -153,6 +156,7 @@ fn body() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -220,6 +224,7 @@ fn dependencies() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -325,6 +330,7 @@ fn dependency_argument() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -392,6 +398,7 @@ fn duplicate_recipes() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -437,6 +444,7 @@ fn duplicate_variables() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -485,6 +493,7 @@ fn doc_comment() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -519,6 +528,7 @@ fn empty_justfile() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -674,6 +684,7 @@ fn parameters() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -762,6 +773,7 @@ fn priors() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -810,6 +822,7 @@ fn private() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -858,6 +871,7 @@ fn quiet() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -921,6 +935,7 @@ fn settings() {
"command": "a",
},
"tempdir": null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -972,6 +987,7 @@ fn shebang() {
"quiet": false,
"shell": null,
"tempdir": null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -1020,6 +1036,7 @@ fn simple() {
"quiet": false,
"shell": null,
"tempdir": null,
+ "unstable": false,
"windows_powershell": false,
"windows_shell": null,
},
@@ -1070,6 +1087,7 @@ fn attribute() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"ignore_comments": false,
"windows_powershell": false,
"windows_shell": null,
@@ -1136,6 +1154,7 @@ fn module() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"ignore_comments": false,
"windows_powershell": false,
"windows_shell": null,
@@ -1158,6 +1177,7 @@ fn module() {
"quiet": false,
"shell": null,
"tempdir" : null,
+ "unstable": false,
"ignore_comments": false,
"windows_powershell": false,
"windows_shell": null,
diff --git a/tests/modules.rs b/tests/modules.rs
index 3692858..1783942 100644
--- a/tests/modules.rs
+++ b/tests/modules.rs
@@ -8,10 +8,8 @@ fn modules_are_unstable() {
mod foo
",
)
- .stderr(
- "error: Modules are currently unstable. \
- Invoke `just` with the `--unstable` flag to enable unstable features.\n",
- )
+ .write("foo.just", "")
+ .stderr_regex("error: Modules are currently unstable..*")
.status(EXIT_FAILURE)
.run();
}
diff --git a/tests/test.rs b/tests/test.rs
index 2935035..288f6bd 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -150,7 +150,7 @@ impl Test {
}
pub(crate) fn stderr_regex(mut self, stderr_regex: impl AsRef) -> Self {
- self.stderr_regex = Some(Regex::new(&format!("^{}$", stderr_regex.as_ref())).unwrap());
+ self.stderr_regex = Some(Regex::new(&format!("^(?s){}$", stderr_regex.as_ref())).unwrap());
self
}
diff --git a/tests/unstable.rs b/tests/unstable.rs
index 23a5fa6..ad196cf 100644
--- a/tests/unstable.rs
+++ b/tests/unstable.rs
@@ -26,12 +26,12 @@ default:
"#;
for val in ["0", "", "false"] {
Test::new()
- .justfile(justfile)
- .args(["--fmt"])
- .env("JUST_UNSTABLE", val)
- .status(EXIT_FAILURE)
- .stderr("error: The `--fmt` command is currently unstable. Invoke `just` with the `--unstable` flag to enable unstable features.\n")
- .run();
+ .justfile(justfile)
+ .args(["--fmt"])
+ .env("JUST_UNSTABLE", val)
+ .status(EXIT_FAILURE)
+ .stderr_regex("error: The `--fmt` command is currently unstable.*")
+ .run();
}
}
@@ -45,6 +45,40 @@ default:
.justfile(justfile)
.args(["--fmt"])
.status(EXIT_FAILURE)
- .stderr("error: The `--fmt` command is currently unstable. Invoke `just` with the `--unstable` flag to enable unstable features.\n")
+ .stderr_regex("error: The `--fmt` command is currently unstable.*")
+ .run();
+}
+
+#[test]
+fn set_unstable_with_setting() {
+ Test::new()
+ .justfile(
+ "
+ set unstable
+
+ mod foo
+ ",
+ )
+ .write("foo.just", "@bar:\n echo BAR")
+ .args(["foo", "bar"])
+ .stdout("BAR\n")
+ .run();
+}
+
+#[test]
+fn unstable_setting_does_not_affect_submodules() {
+ Test::new()
+ .justfile(
+ "
+ set unstable
+
+ mod foo
+ ",
+ )
+ .write("foo.just", "mod bar")
+ .write("bar.just", "baz:\n echo hello")
+ .args(["foo", "bar"])
+ .stderr_regex("error: Modules are currently unstable.*")
+ .status(EXIT_FAILURE)
.run();
}