diff --git a/GRAMMAR.md b/GRAMMAR.md index c32cb6a..98b0fbd 100644 --- a/GRAMMAR.md +++ b/GRAMMAR.md @@ -62,6 +62,7 @@ export : 'export' assignment setting : 'set' 'dotenv-load' boolean? | 'set' 'ignore-comments' boolean? | 'set' 'export' boolean? + | 'set' 'fallback' boolean? | 'set' 'positional-arguments' boolean? | 'set' 'allow-duplicate-recipes' boolean? | 'set' 'windows-powershell' boolean? diff --git a/README.md b/README.md index 5032bdc..97c9220 100644 --- a/README.md +++ b/README.md @@ -646,6 +646,7 @@ foo: | `allow-duplicate-recipes` | boolean | False | Allow recipes appearing later in a `justfile` to override earlier recipes with the same name. | | `dotenv-load` | boolean | False | Load a `.env` file, if present. | | `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 `#`. | | `positional-arguments` | boolean | False | Pass positional arguments. | | `shell` | `[COMMAND, ARGSā€¦]` | - | Set the command used to invoke recipes and evaluate backticks. | @@ -2063,8 +2064,10 @@ The `--dump` command can be used with `--dump-format json` to print a JSON repre ### Falling back to parent `justfile`s -If a recipe is not found, `just` will look for `justfile`s in the parent -directory and up, until it reaches the root directory. +If a recipe is not found in a `justfile` and the `fallback` setting is set, +`just` will look for `justfile`s in the parent directory and up, until it +reaches the root directory. `just` will stop after it reaches a `justfile` in +which the `fallback` setting is `false` or unset. This feature is currently unstable, and so must be enabled with the `--unstable` flag. @@ -2072,6 +2075,7 @@ This feature is currently unstable, and so must be enabled with the As an example, suppose the current directory contains this `justfile`: ```make +set fallback foo: echo foo ``` diff --git a/src/analyzer.rs b/src/analyzer.rs index bbd4434..22fe923 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -53,6 +53,9 @@ impl<'src> Analyzer<'src> { Setting::Export(export) => { settings.export = export; } + Setting::Fallback(fallback) => { + settings.fallback = fallback; + } Setting::IgnoreComments(ignore_comments) => { settings.ignore_comments = ignore_comments; } diff --git a/src/keyword.rs b/src/keyword.rs index 439a154..11b0f3f 100644 --- a/src/keyword.rs +++ b/src/keyword.rs @@ -8,6 +8,7 @@ pub(crate) enum Keyword { DotenvLoad, Else, Export, + Fallback, False, If, IgnoreComments, diff --git a/src/node.rs b/src/node.rs index 65c1c63..8ce634c 100644 --- a/src/node.rs +++ b/src/node.rs @@ -227,6 +227,7 @@ impl<'src> Node<'src> for Set<'src> { Setting::AllowDuplicateRecipes(value) | Setting::DotenvLoad(value) | Setting::Export(value) + | Setting::Fallback(value) | Setting::PositionalArguments(value) | Setting::WindowsPowerShell(value) | Setting::IgnoreComments(value) => { diff --git a/src/parser.rs b/src/parser.rs index 4e76082..44c06c0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -761,8 +761,9 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { Some(Setting::AllowDuplicateRecipes(self.parse_set_bool()?)) } Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)), - Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)), 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::PositionalArguments => Some(Setting::PositionalArguments(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 a30abdc..74c23e4 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -4,8 +4,9 @@ use super::*; pub(crate) enum Setting<'src> { AllowDuplicateRecipes(bool), DotenvLoad(bool), - IgnoreComments(bool), Export(bool), + Fallback(bool), + IgnoreComments(bool), PositionalArguments(bool), Shell(Shell<'src>), WindowsPowerShell(bool), @@ -17,8 +18,9 @@ impl<'src> Display for Setting<'src> { match self { Setting::AllowDuplicateRecipes(value) | Setting::DotenvLoad(value) - | Setting::IgnoreComments(value) | Setting::Export(value) + | Setting::Fallback(value) + | Setting::IgnoreComments(value) | Setting::PositionalArguments(value) | Setting::WindowsPowerShell(value) => write!(f, "{}", value), Setting::Shell(shell) | Setting::WindowsShell(shell) => write!(f, "{}", shell), diff --git a/src/settings.rs b/src/settings.rs index 6e27305..dbb732c 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -10,6 +10,7 @@ pub(crate) struct Settings<'src> { pub(crate) allow_duplicate_recipes: bool, pub(crate) dotenv_load: Option, pub(crate) export: bool, + pub(crate) fallback: bool, pub(crate) ignore_comments: bool, pub(crate) positional_arguments: bool, pub(crate) shell: Option>, diff --git a/src/subcommand.rs b/src/subcommand.rs index 90f1bf3..ef99058 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -135,7 +135,7 @@ impl Subcommand { }; match Self::run_inner(config, loader, arguments, overrides, &search) { - Err(err @ Error::UnknownRecipes { .. }) => { + Err((err @ Error::UnknownRecipes { .. }, true)) => { match search.justfile.parent().unwrap().parent() { Some(parent) => { unknown_recipes_errors.get_or_insert(err); @@ -144,7 +144,7 @@ impl Subcommand { None => return Err(err), } } - result => return result, + result => return result.map_err(|(err, _fallback)| err), } } } else { @@ -155,6 +155,7 @@ impl Subcommand { overrides, &Search::find(&config.search_config, &config.invocation_directory)?, ) + .map_err(|(err, _fallback)| err) } } @@ -164,9 +165,12 @@ impl Subcommand { arguments: &[String], overrides: &BTreeMap, search: &Search, - ) -> Result<(), Error<'src>> { - let (_src, _ast, justfile) = Self::compile(config, loader, search)?; - justfile.run(config, search, overrides, arguments) + ) -> Result<(), (Error<'src>, bool)> { + let (_src, _ast, justfile) = + Self::compile(config, loader, search).map_err(|err| (err, false))?; + justfile + .run(config, search, overrides, arguments) + .map_err(|err| (err, justfile.settings.fallback)) } fn compile<'src>( diff --git a/tests/fall_back_to_parent.rs b/tests/fall_back_to_parent.rs index a5d03cf..4daa2a5 100644 --- a/tests/fall_back_to_parent.rs +++ b/tests/fall_back_to_parent.rs @@ -6,6 +6,40 @@ fn runs_recipe_in_parent_if_not_found_in_current() { .tree(tree! { bar: { justfile: " + set fallback + + baz: + echo subdir + " + } + }) + .justfile( + " + foo: + echo root + ", + ) + .args(&["--unstable", "foo"]) + .current_dir("bar") + .stderr(format!( + " + Trying ..{}justfile + echo root + ", + MAIN_SEPARATOR + )) + .stdout("root\n") + .run(); +} + +#[test] +fn setting_accepts_value() { + Test::new() + .tree(tree! { + bar: { + justfile: " + set fallback := true + baz: echo subdir " @@ -36,6 +70,8 @@ fn print_error_from_parent_if_recipe_not_found_in_current() { .tree(tree! { bar: { justfile: " + set fallback + baz: echo subdir " @@ -64,6 +100,8 @@ fn requires_unstable() { .tree(tree! { bar: { justfile: " + set fallback + baz: echo subdir " @@ -83,7 +121,7 @@ fn requires_unstable() { } #[test] -fn works_with_provided_search_directory() { +fn requires_setting() { Test::new() .tree(tree! { bar: { @@ -99,6 +137,34 @@ fn works_with_provided_search_directory() { echo root ", ) + .args(&["--unstable", "foo"]) + .current_dir("bar") + .status(EXIT_FAILURE) + .stderr("error: Justfile does not contain recipe `foo`.\n") + .run(); +} + +#[test] +fn works_with_provided_search_directory() { + Test::new() + .tree(tree! { + bar: { + justfile: " + set fallback + + baz: + echo subdir + " + } + }) + .justfile( + " + set fallback + + foo: + echo root + ", + ) .args(&["--unstable", "./foo"]) .stdout("root\n") .stderr(format!( @@ -118,6 +184,8 @@ fn doesnt_work_with_justfile() { .tree(tree! { bar: { justfile: " + set fallback + baz: echo subdir " @@ -125,6 +193,8 @@ fn doesnt_work_with_justfile() { }) .justfile( " + set fallback + foo: echo root ", @@ -142,6 +212,8 @@ fn doesnt_work_with_justfile_and_working_directory() { .tree(tree! { bar: { justfile: " + set fallback + baz: echo subdir " @@ -149,6 +221,8 @@ fn doesnt_work_with_justfile_and_working_directory() { }) .justfile( " + set fallback + foo: echo root ", @@ -173,6 +247,8 @@ fn prints_correct_error_message_when_recipe_not_found() { .tree(tree! { bar: { justfile: " + set fallback + bar: echo subdir " @@ -196,3 +272,82 @@ fn prints_correct_error_message_when_recipe_not_found() { )) .run(); } + +#[test] +fn multiple_levels_of_fallback_work() { + Test::new() + .tree(tree! { + a: { + b: { + justfile: " + set fallback + + foo: + echo subdir + " + }, + justfile: " + set fallback + + bar: + echo subdir + " + } + }) + .justfile( + " + baz: + echo root + ", + ) + .args(&["--unstable", "baz"]) + .current_dir("a/b") + .stdout("root\n") + .stderr(format!( + " + Trying ..{}justfile + Trying ..{}..{}justfile + echo root + ", + MAIN_SEPARATOR, MAIN_SEPARATOR, MAIN_SEPARATOR + )) + .run(); +} + +#[test] +fn stop_fallback_when_fallback_is_false() { + Test::new() + .tree(tree! { + a: { + b: { + justfile: " + set fallback + foo: + echo subdir + " + }, + justfile: " + bar: + echo subdir + " + } + }) + .justfile( + " + baz: + echo root + ", + ) + .args(&["--unstable", "baz"]) + .current_dir("a/b") + .stderr(format!( + " + Trying ..{}justfile + error: Justfile does not contain recipe `baz`. + Did you mean `bar`? + ", + MAIN_SEPARATOR + )) + .status(EXIT_FAILURE) + .run(); +} diff --git a/tests/json.rs b/tests/json.rs index 8742b0b..d70ea6e 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -42,6 +42,7 @@ fn alias() { "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, + "fallback": false, "positional_arguments": false, "shell": null, "ignore_comments": false, @@ -72,6 +73,7 @@ fn assignment() { "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, + "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, @@ -115,6 +117,7 @@ fn body() { "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, + "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, @@ -168,6 +171,7 @@ fn dependencies() { "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, + "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, @@ -258,6 +262,7 @@ fn dependency_argument() { "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, + "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, @@ -312,6 +317,7 @@ fn duplicate_recipes() { "allow_duplicate_recipes": true, "dotenv_load": null, "export": false, + "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, @@ -348,6 +354,7 @@ fn doc_comment() { "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, + "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, @@ -372,6 +379,7 @@ fn empty_justfile() { "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, + "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, @@ -505,6 +513,7 @@ fn parameters() { "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, + "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, @@ -577,6 +586,7 @@ fn priors() { "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, + "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, @@ -613,6 +623,7 @@ fn private() { "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, + "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, @@ -649,6 +660,7 @@ fn quiet() { "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, + "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, @@ -676,6 +688,7 @@ fn settings() { " set dotenv-load set export + set fallback set positional-arguments set ignore-comments set shell := ['a', 'b', 'c'] @@ -704,6 +717,7 @@ fn settings() { "allow_duplicate_recipes": false, "dotenv_load": true, "export": true, + "fallback": true, "ignore_comments": true, "positional_arguments": true, "shell": { @@ -746,6 +760,7 @@ fn shebang() { "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, + "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, @@ -782,6 +797,7 @@ fn simple() { "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, + "fallback": false, "positional_arguments": false, "shell": null, "ignore_comments": false,