summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCasey Rodarmor <casey@rodarmor.com>2023-11-25 00:36:18 -0800
committerCasey Rodarmor <casey@rodarmor.com>2023-11-26 02:09:15 -0800
commitda32c6e3dc5a3e9e447aee747e13c37c50f52f80 (patch)
tree9fd9e30575cf7e4e9788a17a8915f8a53c30f6f8
parent92bae080aba135c2fc969e53c24b9aeaf98ffe9e (diff)
Add recipe opts
-rwxr-xr-xjustfile4
-rw-r--r--src/evaluator.rs5
-rw-r--r--src/justfile.rs29
-rw-r--r--src/lexer.rs12
-rw-r--r--src/lib.rs7
-rw-r--r--src/opt.rs34
-rw-r--r--src/parser.rs39
-rw-r--r--src/recipe.rs6
-rw-r--r--src/recipe_resolver.rs14
-rw-r--r--src/token_kind.rs2
-rw-r--r--src/unresolved_recipe.rs7
-rw-r--r--tests/error_messages.rs4
-rw-r--r--tests/lib.rs1
-rw-r--r--tests/misc.rs8
-rw-r--r--tests/slash_operator.rs2
15 files changed, 138 insertions, 36 deletions
diff --git a/justfile b/justfile
index 37b442bc..0944c491 100755
--- a/justfile
+++ b/justfile
@@ -182,8 +182,8 @@ build-book:
mdbook build book/en
mdbook build book/zh
-convert-integration-test test:
- cargo expand --test integration {{test}} | \
+convert-integration-test TEST:
+ cargo expand --test integration {{ TEST }} | \
sed \
-E \
-e 's/#\[cfg\(test\)\]/#\[test\]/' \
diff --git a/src/evaluator.rs b/src/evaluator.rs
index ac70f6b2..a7ef67fd 100644
--- a/src/evaluator.rs
+++ b/src/evaluator.rs
@@ -255,6 +255,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
config: &'run Config,
dotenv: &'run BTreeMap<String, String>,
parameters: &[Parameter<'src>],
+ opts: BTreeMap<Name<'src>, &str>,
arguments: &[&str],
scope: &'run Scope<'src, 'run>,
settings: &'run Settings,
@@ -271,6 +272,10 @@ impl<'src, 'run> Evaluator<'src, 'run> {
let mut scope = scope.child();
+ for (name, value) in opts {
+ scope.bind(false, name, value.into());
+ }
+
let mut positional = Vec::new();
let mut rest = arguments;
diff --git a/src/justfile.rs b/src/justfile.rs
index 45b327d5..d75deb46 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -215,8 +215,25 @@ impl<'src> Justfile<'src> {
while let Some((argument, mut tail)) = rest.split_first() {
if let Some(recipe) = self.get_recipe(argument) {
+ let mut opts = BTreeMap::new();
+
+ for opt in &recipe.opts {
+ if let Some(arg) = tail.first() {
+ if opt.accepts(arg) {
+ if let Some(value) = tail.get(1) {
+ opts.insert(opt.variable, *value);
+ } else {
+ panic!("opt with no value");
+ }
+ tail = &tail[2..];
+ continue;
+ }
+ }
+ panic!("opt not found");
+ }
+
if recipe.parameters.is_empty() {
- grouped.push((recipe, &[][..]));
+ grouped.push((recipe, opts, &[][..]));
} else {
let argument_range = recipe.argument_range();
let argument_count = cmp::min(tail.len(), recipe.max_arguments());
@@ -229,7 +246,7 @@ impl<'src> Justfile<'src> {
max: recipe.max_arguments(),
});
}
- grouped.push((recipe, &tail[0..argument_count]));
+ grouped.push((recipe, opts, &tail[0..argument_count]));
tail = &tail[argument_count..];
}
} else {
@@ -258,8 +275,8 @@ impl<'src> Justfile<'src> {
};
let mut ran = BTreeSet::new();
- for (recipe, arguments) in grouped {
- Self::run_recipe(&context, recipe, arguments, &dotenv, search, &mut ran)?;
+ for (recipe, opts, arguments) in grouped {
+ Self::run_recipe(&context, recipe, opts, arguments, &dotenv, search, &mut ran)?;
}
Ok(())
@@ -280,6 +297,7 @@ impl<'src> Justfile<'src> {
fn run_recipe(
context: &RecipeContext<'src, '_>,
recipe: &Recipe<'src>,
+ opts: BTreeMap<Name<'src>, &str>,
arguments: &[&str],
dotenv: &BTreeMap<String, String>,
search: &Search,
@@ -304,6 +322,7 @@ impl<'src> Justfile<'src> {
context.config,
dotenv,
&recipe.parameters,
+ opts,
arguments,
&context.scope,
context.settings,
@@ -324,6 +343,7 @@ impl<'src> Justfile<'src> {
Self::run_recipe(
context,
recipe,
+ BTreeMap::new(),
&arguments.iter().map(String::as_ref).collect::<Vec<&str>>(),
dotenv,
search,
@@ -346,6 +366,7 @@ impl<'src> Justfile<'src> {
Self::run_recipe(
context,
recipe,
+ BTreeMap::new(),
&evaluated.iter().map(String::as_ref).collect::<Vec<&str>>(),
dotenv,
search,
diff --git a/src/lexer.rs b/src/lexer.rs
index 3a9a429c..b2eb0ef7 100644
--- a/src/lexer.rs
+++ b/src/lexer.rs
@@ -282,11 +282,7 @@ impl<'src> Lexer<'src> {
/// True if `c` can be a continuation character of an identifier
fn is_identifier_continue(c: char) -> bool {
- if Self::is_identifier_start(c) {
- return true;
- }
-
- matches!(c, '0'..='9' | '-')
+ Self::is_identifier_start(c) | matches!(c, '0'..='9' | '-')
}
/// Consume the text and produce a series of tokens
@@ -490,6 +486,7 @@ impl<'src> Lexer<'src> {
'*' => self.lex_single(Asterisk),
'+' => self.lex_single(Plus),
',' => self.lex_single(Comma),
+ '-' => self.lex_digraph('-', '-', DashDash),
'/' => self.lex_single(Slash),
':' => self.lex_colon(),
'\\' => self.lex_escape(),
@@ -990,6 +987,7 @@ mod tests {
Colon => ":",
ColonEquals => ":=",
Comma => ",",
+ DashDash => "--",
Dollar => "$",
Eol => "\n",
Equals => "=",
@@ -2205,8 +2203,8 @@ mod tests {
}
error! {
- name: invalid_name_start_dash,
- input: "-foo",
+ name: invalid_name_start_caret,
+ input: "^foo",
offset: 0,
line: 0,
column: 0,
diff --git a/src/lib.rs b/src/lib.rs
index fa88edee..255cda9d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -21,9 +21,9 @@ pub(crate) use {
fragment::Fragment, function::Function, function_context::FunctionContext,
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List,
- load_dotenv::load_dotenv, loader::Loader, name::Name, ordinal::Ordinal, output::output,
- output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind, parser::Parser,
- platform::Platform, platform_interface::PlatformInterface, position::Position,
+ load_dotenv::load_dotenv, loader::Loader, name::Name, opt::Opt, ordinal::Ordinal,
+ output::output, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
+ parser::Parser, platform::Platform, platform_interface::PlatformInterface, position::Position,
positional::Positional, range_ext::RangeExt, recipe::Recipe, recipe_context::RecipeContext,
recipe_resolver::RecipeResolver, scope::Scope, search::Search, search_config::SearchConfig,
search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang,
@@ -145,6 +145,7 @@ mod list;
mod load_dotenv;
mod loader;
mod name;
+mod opt;
mod ordinal;
mod output;
mod output_error;
diff --git a/src/opt.rs b/src/opt.rs
new file mode 100644
index 00000000..fabcb1ea
--- /dev/null
+++ b/src/opt.rs
@@ -0,0 +1,34 @@
+use super::*;
+
+#[derive(PartialEq, Debug, Clone, Serialize)]
+pub(crate) struct Opt<'src> {
+ pub(crate) default: Option<Expression<'src>>,
+ pub(crate) key: Name<'src>,
+ pub(crate) variable: Name<'src>,
+}
+
+impl<'src> Opt<'src> {
+ pub(crate) fn accepts(&self, arg: &str) -> bool {
+ arg
+ .strip_prefix("--")
+ .map(|key| key == self.key.lexeme())
+ .unwrap_or_default()
+ }
+}
+
+impl<'src> ColorDisplay for Opt<'src> {
+ fn fmt(&self, f: &mut Formatter, color: Color) -> Result<(), fmt::Error> {
+ write!(
+ f,
+ "--{} {}",
+ color.annotation().paint(self.key.lexeme()),
+ color.parameter().paint(self.variable.lexeme())
+ )?;
+
+ if let Some(ref default) = self.default {
+ write!(f, "={}", color.string().paint(&default.to_string()))?;
+ }
+
+ Ok(())
+ }
+}
diff --git a/src/parser.rs b/src/parser.rs
index a8a04d80..f607a130 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -627,9 +627,16 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
let name = self.parse_name()?;
let mut positional = Vec::new();
+ let mut opts = Vec::new();
- while self.next_is(Identifier) || self.next_is(Dollar) {
- positional.push(self.parse_parameter(ParameterKind::Singular)?);
+ loop {
+ if self.next_is(Identifier) || self.next_is(Dollar) {
+ positional.push(self.parse_parameter(ParameterKind::Singular)?);
+ } else if self.next_is(DashDash) {
+ opts.push(self.parse_opt()?);
+ } else {
+ break;
+ }
}
let kind = if self.accepted(Plus)? {
@@ -687,11 +694,12 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
private: name.lexeme().starts_with('_'),
shebang: body.first().map_or(false, Line::is_shebang),
attributes,
- priors,
body,
dependencies,
doc,
name,
+ opts,
+ priors,
quiet,
})
}
@@ -716,6 +724,27 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
})
}
+ /// Parse a recipe option --foo foo
+ fn parse_opt(&mut self) -> CompileResult<'src, Opt<'src>> {
+ self.presume(DashDash)?;
+
+ let key = self.parse_name()?;
+
+ let variable = self.parse_name()?;
+
+ let default = if self.accepted(Equals)? {
+ Some(self.parse_value()?)
+ } else {
+ None
+ };
+
+ Ok(Opt {
+ default,
+ key,
+ variable,
+ })
+ }
+
/// Parse the body of a recipe
fn parse_body(&mut self) -> CompileResult<'src, Vec<Line<'src>>> {
let mut lines = Vec::new();
@@ -2014,7 +2043,7 @@ mod tests {
column: 5,
width: 1,
kind: UnexpectedToken{
- expected: vec![Asterisk, Colon, Dollar, Equals, Identifier, Plus],
+ expected: vec![Asterisk, Colon, DashDash, Dollar, Equals, Identifier, Plus],
found: Eol
},
}
@@ -2149,7 +2178,7 @@ mod tests {
column: 8,
width: 0,
kind: UnexpectedToken {
- expected: vec![Asterisk, Colon, Dollar, Equals, Identifier, Plus],
+ expected: vec![Asterisk, Colon, DashDash, Dollar, Equals, Identifier, Plus],
found: Eof
},
}
diff --git a/src/recipe.rs b/src/recipe.rs
index 4ea8bc2b..0117a674 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -32,6 +32,8 @@ pub(crate) struct Recipe<'src, D = Dependency<'src>> {
pub(crate) private: bool,
pub(crate) quiet: bool,
pub(crate) shebang: bool,
+ #[serde(skip)]
+ pub(crate) opts: Vec<Opt<'src>>,
}
impl<'src, D> Recipe<'src, D> {
@@ -406,6 +408,10 @@ impl<'src, D: Display> ColorDisplay for Recipe<'src, D> {
write!(f, "{}", self.name)?;
}
+ for opt in &self.opts {
+ write!(f, " {}", opt.color_display(color))?;
+ }
+
for parameter in &self.parameters {
write!(f, " {}", parameter.color_display(color))?;
}
diff --git a/src/recipe_resolver.rs b/src/recipe_resolver.rs
index 2715acc2..701bdb05 100644
--- a/src/recipe_resolver.rs
+++ b/src/recipe_resolver.rs
@@ -25,7 +25,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
for parameter in &recipe.parameters {
if let Some(expression) = &parameter.default {
for variable in expression.variables() {
- resolver.resolve_variable(&variable, &[])?;
+ resolver.resolve_variable(&variable, &Vec::new(), &[])?;
}
}
}
@@ -33,7 +33,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
for dependency in &recipe.dependencies {
for argument in &dependency.arguments {
for variable in argument.variables() {
- resolver.resolve_variable(&variable, &recipe.parameters)?;
+ resolver.resolve_variable(&variable, &recipe.opts, &recipe.parameters)?;
}
}
}
@@ -42,7 +42,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
for fragment in &line.fragments {
if let Fragment::Interpolation { expression, .. } = fragment {
for variable in expression.variables() {
- resolver.resolve_variable(&variable, &recipe.parameters)?;
+ resolver.resolve_variable(&variable, &recipe.opts, &recipe.parameters)?;
}
}
}
@@ -55,11 +55,15 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
fn resolve_variable(
&self,
variable: &Token<'src>,
+ opts: &Vec<Opt<'src>>,
parameters: &[Parameter],
) -> CompileResult<'src, ()> {
let name = variable.lexeme();
- let undefined =
- !self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name);
+ let undefined = !self.assignments.contains_key(name)
+ && !opts.iter().any(|opt| opt.variable.lexeme() == name)
+ && !parameters
+ .iter()
+ .any(|parameter| parameter.name.lexeme() == name);
if undefined {
return Err(variable.error(UndefinedVariable { variable: name }));
diff --git a/src/token_kind.rs b/src/token_kind.rs
index 87c70d5f..6d87db10 100644
--- a/src/token_kind.rs
+++ b/src/token_kind.rs
@@ -17,6 +17,7 @@ pub(crate) enum TokenKind {
ColonEquals,
Comma,
Comment,
+ DashDash,
Dedent,
Dollar,
Eof,
@@ -60,6 +61,7 @@ impl Display for TokenKind {
ColonEquals => "':='",
Comma => "','",
Comment => "comment",
+ DashDash => "'--'",
Dedent => "dedent",
Dollar => "'$'",
Eof => "end of file",
diff --git a/src/unresolved_recipe.rs b/src/unresolved_recipe.rs
index 0a49443d..4e37b338 100644
--- a/src/unresolved_recipe.rs
+++ b/src/unresolved_recipe.rs
@@ -45,16 +45,17 @@ impl<'src> UnresolvedRecipe<'src> {
.collect();
Ok(Recipe {
+ attributes: self.attributes,
body: self.body,
+ dependencies,
doc: self.doc,
name: self.name,
+ opts: self.opts,
parameters: self.parameters,
+ priors: self.priors,
private: self.private,
quiet: self.quiet,
shebang: self.shebang,
- priors: self.priors,
- attributes: self.attributes,
- dependencies,
})
}
}
diff --git a/tests/error_messages.rs b/tests/error_messages.rs
index 6e860273..fecce367 100644
--- a/tests/error_messages.rs
+++ b/tests/error_messages.rs
@@ -62,7 +62,7 @@ fn file_path_is_indented_if_justfile_is_long() {
.status(EXIT_FAILURE)
.stderr(
"
-error: Expected '*', ':', '$', identifier, or '+', but found end of file
+error: Expected '*', ':', '--', '$', identifier, or '+', but found end of file
--> justfile:20:4
|
20 | foo
@@ -81,7 +81,7 @@ fn file_paths_are_relative() {
.status(EXIT_FAILURE)
.stderr(format!(
"
-error: Expected '*', ':', '$', identifier, or '+', but found end of file
+error: Expected '*', ':', '--', '$', identifier, or '+', but found end of file
--> foo{}bar.just:1:4
|
1 | baz
diff --git a/tests/lib.rs b/tests/lib.rs
index 42153f51..4c41a0e6 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -67,6 +67,7 @@ mod multibyte_char;
mod newline_escape;
mod no_cd;
mod no_exit_message;
+mod opts;
mod os_attributes;
mod parser;
mod positional_arguments;
diff --git a/tests/misc.rs b/tests/misc.rs
index 8733507c..2a7dca6c 100644
--- a/tests/misc.rs
+++ b/tests/misc.rs
@@ -1319,7 +1319,7 @@ test! {
justfile: "foo 'bar'",
args: ("foo"),
stdout: "",
- stderr: "error: Expected '*', ':', '$', identifier, or '+', but found string
+ stderr: "error: Expected '*', ':', '--', '$', identifier, or '+', but found string
--> justfile:1:5
|
1 | foo 'bar'
@@ -1563,8 +1563,8 @@ test! {
name: list_colors,
justfile: "
# comment
-a B C +D='hello':
- echo {{B}} {{C}} {{D}}
+a --b C D E +F='hello':
+ echo {{C}} {{D}} {{E}} {{F}}
",
args: ("--color", "always", "--list"),
stdout: "
@@ -1925,7 +1925,7 @@ test! {
echo {{foo}}
",
stderr: "
- error: Expected '*', ':', '$', identifier, or '+', but found '='
+ error: Expected '*', ':', '--', '$', identifier, or '+', but found '='
--> justfile:1:5
|
1 | foo = 'bar'
diff --git a/tests/slash_operator.rs b/tests/slash_operator.rs
index 7991d1c7..db3e4769 100644
--- a/tests/slash_operator.rs
+++ b/tests/slash_operator.rs
@@ -69,7 +69,7 @@ fn default_un_parenthesized() {
)
.stderr(
"
- error: Expected '*', ':', '$', identifier, or '+', but found '/'
+ error: Expected '*', ':', '--', '$', identifier, or '+', but found '/'
--> justfile:1:11
|
1 | foo x='a' / 'b':