Implement regular expression match conditionals (#970)
This commit is contained in:
parent
09af9bb5e5
commit
0db4589efe
64
Cargo.toml
64
Cargo.toml
@ -1,37 +1,38 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "just"
|
name = "just"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
description = "🤖 Just a command runner"
|
description = "🤖 Just a command runner"
|
||||||
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
|
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
|
||||||
license = "CC0-1.0"
|
license = "CC0-1.0"
|
||||||
homepage = "https://github.com/casey/just"
|
homepage = "https://github.com/casey/just"
|
||||||
repository = "https://github.com/casey/just"
|
repository = "https://github.com/casey/just"
|
||||||
readme = "crates-io-readme.md"
|
readme = "crates-io-readme.md"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
autotests = false
|
autotests = false
|
||||||
categories = ["command-line-utilities", "development-tools"]
|
categories = ["command-line-utilities", "development-tools"]
|
||||||
keywords = ["command-line", "task", "runner", "development", "utility"]
|
keywords = ["command-line", "task", "runner", "development", "utility"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "bin/ref-type"]
|
members = [".", "bin/ref-type"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ansi_term = "0.12.0"
|
ansi_term = "0.12.0"
|
||||||
atty = "0.2.0"
|
atty = "0.2.0"
|
||||||
camino = "1.0.4"
|
camino = "1.0.4"
|
||||||
derivative = "2.0.0"
|
derivative = "2.0.0"
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
edit-distance = "2.0.0"
|
edit-distance = "2.0.0"
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.9.0"
|
||||||
lazy_static = "1.0.0"
|
lazy_static = "1.0.0"
|
||||||
lexiclean = "0.0.1"
|
lexiclean = "0.0.1"
|
||||||
libc = "0.2.0"
|
libc = "0.2.0"
|
||||||
log = "0.4.4"
|
log = "0.4.4"
|
||||||
snafu = "0.6.0"
|
regex = "1.5.4"
|
||||||
strum_macros = "0.21.1"
|
snafu = "0.6.0"
|
||||||
target = "2.0.0"
|
strum_macros = "0.21.1"
|
||||||
tempfile = "3.0.0"
|
target = "2.0.0"
|
||||||
typed-arena = "2.0.1"
|
tempfile = "3.0.0"
|
||||||
|
typed-arena = "2.0.1"
|
||||||
unicode-width = "0.1.0"
|
unicode-width = "0.1.0"
|
||||||
|
|
||||||
[dependencies.clap]
|
[dependencies.clap]
|
||||||
@ -47,13 +48,12 @@ version = "0.21.0"
|
|||||||
features = ["derive"]
|
features = ["derive"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
cradle = "0.0.22"
|
cradle = "0.0.22"
|
||||||
executable-path = "1.0.0"
|
executable-path = "1.0.0"
|
||||||
pretty_assertions = "0.7.0"
|
pretty_assertions = "0.7.0"
|
||||||
regex = "1.5.4"
|
temptree = "0.2.0"
|
||||||
temptree = "0.2.0"
|
which = "4.0.0"
|
||||||
which = "4.0.0"
|
yaml-rust = "0.4.5"
|
||||||
yaml-rust = "0.4.5"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# No features are active by default.
|
# No features are active by default.
|
||||||
|
16
README.adoc
16
README.adoc
@ -884,6 +884,22 @@ $ just bar
|
|||||||
xyz
|
xyz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
And match against regular expressions:
|
||||||
|
|
||||||
|
```make
|
||||||
|
foo := if "hello" =~ 'hel+o' { "match" } else { "mismatch" }
|
||||||
|
|
||||||
|
bar:
|
||||||
|
@echo {{foo}}
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ just bar
|
||||||
|
match
|
||||||
|
```
|
||||||
|
|
||||||
|
Regular expressions are provided by the https://github.com/rust-lang/regex[regex crate], whose syntax is documented on https://docs.rs/regex/1.5.4/regex/#syntax[docs.rs]. Since regular expressions commonly use backslash escape sequences, consider using single-quoted string literals, which will pass slashes to the regex parser unmolested.
|
||||||
|
|
||||||
Conditional expressions short-circuit, which means they only evaluate one of
|
Conditional expressions short-circuit, which means they only evaluate one of
|
||||||
their branches. This can be used to make sure that backtick expressions don't
|
their branches. This can be used to make sure that backtick expressions don't
|
||||||
run when they shouldn't.
|
run when they shouldn't.
|
||||||
|
@ -25,6 +25,7 @@ pub(crate) use edit_distance::edit_distance;
|
|||||||
pub(crate) use lexiclean::Lexiclean;
|
pub(crate) use lexiclean::Lexiclean;
|
||||||
pub(crate) use libc::EXIT_FAILURE;
|
pub(crate) use libc::EXIT_FAILURE;
|
||||||
pub(crate) use log::{info, warn};
|
pub(crate) use log::{info, warn};
|
||||||
|
pub(crate) use regex::Regex;
|
||||||
pub(crate) use snafu::{ResultExt, Snafu};
|
pub(crate) use snafu::{ResultExt, Snafu};
|
||||||
pub(crate) use strum::{Display, EnumString, IntoStaticStr};
|
pub(crate) use strum::{Display, EnumString, IntoStaticStr};
|
||||||
pub(crate) use typed_arena::Arena;
|
pub(crate) use typed_arena::Arena;
|
||||||
@ -46,19 +47,20 @@ pub(crate) use crate::{
|
|||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
alias::Alias, analyzer::Analyzer, assignment::Assignment,
|
alias::Alias, analyzer::Analyzer, assignment::Assignment,
|
||||||
assignment_resolver::AssignmentResolver, ast::Ast, binding::Binding, color::Color,
|
assignment_resolver::AssignmentResolver, ast::Ast, binding::Binding, color::Color,
|
||||||
compile_error::CompileError, compile_error_kind::CompileErrorKind, config::Config,
|
compile_error::CompileError, compile_error_kind::CompileErrorKind,
|
||||||
config_error::ConfigError, count::Count, delimiter::Delimiter, dependency::Dependency,
|
conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError,
|
||||||
enclosure::Enclosure, error::Error, evaluator::Evaluator, expression::Expression,
|
count::Count, delimiter::Delimiter, dependency::Dependency, enclosure::Enclosure, error::Error,
|
||||||
fragment::Fragment, function::Function, function_context::FunctionContext,
|
evaluator::Evaluator, expression::Expression, fragment::Fragment, function::Function,
|
||||||
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
|
function_context::FunctionContext, interrupt_guard::InterruptGuard,
|
||||||
justfile::Justfile, keyword::Keyword, lexer::Lexer, line::Line, list::List, loader::Loader,
|
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, keyword::Keyword,
|
||||||
name::Name, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
|
lexer::Lexer, line::Line, list::List, loader::Loader, name::Name, output_error::OutputError,
|
||||||
parser::Parser, platform::Platform, position::Position, positional::Positional, recipe::Recipe,
|
parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform,
|
||||||
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, scope::Scope, search::Search,
|
position::Position, positional::Positional, recipe::Recipe, recipe_context::RecipeContext,
|
||||||
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
|
recipe_resolver::RecipeResolver, scope::Scope, search::Search, search_config::SearchConfig,
|
||||||
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, string_kind::StringKind,
|
search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang,
|
||||||
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
|
show_whitespace::ShowWhitespace, string_kind::StringKind, string_literal::StringLiteral,
|
||||||
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
|
subcommand::Subcommand, suggestion::Suggestion, table::Table, thunk::Thunk, token::Token,
|
||||||
|
token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
|
||||||
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
|
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
|
||||||
verbosity::Verbosity, warning::Warning,
|
verbosity::Verbosity, warning::Warning,
|
||||||
};
|
};
|
||||||
|
22
src/conditional_operator.rs
Normal file
22
src/conditional_operator.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
/// A conditional expression operator.
|
||||||
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
|
pub(crate) enum ConditionalOperator {
|
||||||
|
/// `==`
|
||||||
|
Equality,
|
||||||
|
/// `!=`
|
||||||
|
Inequality,
|
||||||
|
/// `=~`
|
||||||
|
RegexMatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ConditionalOperator {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Equality => write!(f, "=="),
|
||||||
|
Self::Inequality => write!(f, "!="),
|
||||||
|
Self::RegexMatch => write!(f, "=~"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -95,6 +95,9 @@ pub(crate) enum Error<'src> {
|
|||||||
},
|
},
|
||||||
NoChoosableRecipes,
|
NoChoosableRecipes,
|
||||||
NoRecipes,
|
NoRecipes,
|
||||||
|
RegexCompile {
|
||||||
|
source: regex::Error,
|
||||||
|
},
|
||||||
Search {
|
Search {
|
||||||
search_error: SearchError,
|
search_error: SearchError,
|
||||||
},
|
},
|
||||||
@ -507,6 +510,9 @@ impl<'src> ColorDisplay for Error<'src> {
|
|||||||
NoRecipes => {
|
NoRecipes => {
|
||||||
write!(f, "Justfile contains no recipes.")?;
|
write!(f, "Justfile contains no recipes.")?;
|
||||||
}
|
}
|
||||||
|
RegexCompile { source } => {
|
||||||
|
write!(f, "{}", source)?;
|
||||||
|
}
|
||||||
Search { search_error } => Display::fmt(search_error, f)?,
|
Search { search_error } => Display::fmt(search_error, f)?,
|
||||||
Shebang {
|
Shebang {
|
||||||
recipe,
|
recipe,
|
||||||
|
@ -139,11 +139,17 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
rhs,
|
rhs,
|
||||||
then,
|
then,
|
||||||
otherwise,
|
otherwise,
|
||||||
inverted,
|
operator,
|
||||||
} => {
|
} => {
|
||||||
let lhs = self.evaluate_expression(lhs)?;
|
let lhs_value = self.evaluate_expression(lhs)?;
|
||||||
let rhs = self.evaluate_expression(rhs)?;
|
let rhs_value = self.evaluate_expression(rhs)?;
|
||||||
let condition = if *inverted { lhs != rhs } else { lhs == rhs };
|
let condition = match operator {
|
||||||
|
ConditionalOperator::Equality => lhs_value == rhs_value,
|
||||||
|
ConditionalOperator::Inequality => lhs_value != rhs_value,
|
||||||
|
ConditionalOperator::RegexMatch => Regex::new(&rhs_value)
|
||||||
|
.map_err(|source| Error::RegexCompile { source })?
|
||||||
|
.is_match(&lhs_value),
|
||||||
|
};
|
||||||
if condition {
|
if condition {
|
||||||
self.evaluate_expression(then)
|
self.evaluate_expression(then)
|
||||||
} else {
|
} else {
|
||||||
|
@ -26,7 +26,7 @@ pub(crate) enum Expression<'src> {
|
|||||||
rhs: Box<Expression<'src>>,
|
rhs: Box<Expression<'src>>,
|
||||||
then: Box<Expression<'src>>,
|
then: Box<Expression<'src>>,
|
||||||
otherwise: Box<Expression<'src>>,
|
otherwise: Box<Expression<'src>>,
|
||||||
inverted: bool,
|
operator: ConditionalOperator,
|
||||||
},
|
},
|
||||||
/// `(contents)`
|
/// `(contents)`
|
||||||
Group { contents: Box<Expression<'src>> },
|
Group { contents: Box<Expression<'src>> },
|
||||||
@ -52,15 +52,11 @@ impl<'src> Display for Expression<'src> {
|
|||||||
rhs,
|
rhs,
|
||||||
then,
|
then,
|
||||||
otherwise,
|
otherwise,
|
||||||
inverted,
|
operator,
|
||||||
} => write!(
|
} => write!(
|
||||||
f,
|
f,
|
||||||
"if {} {} {} {{ {} }} else {{ {} }}",
|
"if {} {} {} {{ {} }} else {{ {} }}",
|
||||||
lhs,
|
lhs, operator, rhs, then, otherwise
|
||||||
if *inverted { "!=" } else { "==" },
|
|
||||||
rhs,
|
|
||||||
then,
|
|
||||||
otherwise
|
|
||||||
),
|
),
|
||||||
Expression::StringLiteral { string_literal } => write!(f, "{}", string_literal),
|
Expression::StringLiteral { string_literal } => write!(f, "{}", string_literal),
|
||||||
Expression::Variable { name } => write!(f, "{}", name.lexeme()),
|
Expression::Variable { name } => write!(f, "{}", name.lexeme()),
|
||||||
|
58
src/lexer.rs
58
src/lexer.rs
@ -475,25 +475,25 @@ impl<'src> Lexer<'src> {
|
|||||||
/// Lex token beginning with `start` outside of a recipe body
|
/// Lex token beginning with `start` outside of a recipe body
|
||||||
fn lex_normal(&mut self, start: char) -> CompileResult<'src, ()> {
|
fn lex_normal(&mut self, start: char) -> CompileResult<'src, ()> {
|
||||||
match start {
|
match start {
|
||||||
'&' => self.lex_digraph('&', '&', AmpersandAmpersand),
|
' ' | '\t' => self.lex_whitespace(),
|
||||||
'!' => self.lex_digraph('!', '=', BangEquals),
|
'!' => self.lex_digraph('!', '=', BangEquals),
|
||||||
'*' => self.lex_single(Asterisk),
|
'#' => self.lex_comment(),
|
||||||
'$' => self.lex_single(Dollar),
|
'$' => self.lex_single(Dollar),
|
||||||
'@' => self.lex_single(At),
|
'&' => self.lex_digraph('&', '&', AmpersandAmpersand),
|
||||||
'[' => self.lex_delimiter(BracketL),
|
|
||||||
']' => self.lex_delimiter(BracketR),
|
|
||||||
'=' => self.lex_choice('=', EqualsEquals, Equals),
|
|
||||||
',' => self.lex_single(Comma),
|
|
||||||
':' => self.lex_colon(),
|
|
||||||
'(' => self.lex_delimiter(ParenL),
|
'(' => self.lex_delimiter(ParenL),
|
||||||
')' => self.lex_delimiter(ParenR),
|
')' => self.lex_delimiter(ParenR),
|
||||||
|
'*' => self.lex_single(Asterisk),
|
||||||
|
'+' => self.lex_single(Plus),
|
||||||
|
',' => self.lex_single(Comma),
|
||||||
|
':' => self.lex_colon(),
|
||||||
|
'=' => self.lex_choices('=', &[('=', EqualsEquals), ('~', EqualsTilde)], Equals),
|
||||||
|
'@' => self.lex_single(At),
|
||||||
|
'[' => self.lex_delimiter(BracketL),
|
||||||
|
'\n' | '\r' => self.lex_eol(),
|
||||||
|
']' => self.lex_delimiter(BracketR),
|
||||||
|
'`' | '"' | '\'' => self.lex_string(),
|
||||||
'{' => self.lex_delimiter(BraceL),
|
'{' => self.lex_delimiter(BraceL),
|
||||||
'}' => self.lex_delimiter(BraceR),
|
'}' => self.lex_delimiter(BraceR),
|
||||||
'+' => self.lex_single(Plus),
|
|
||||||
'#' => self.lex_comment(),
|
|
||||||
' ' | '\t' => self.lex_whitespace(),
|
|
||||||
'`' | '"' | '\'' => self.lex_string(),
|
|
||||||
'\n' | '\r' => self.lex_eol(),
|
|
||||||
_ if Self::is_identifier_start(start) => self.lex_identifier(),
|
_ if Self::is_identifier_start(start) => self.lex_identifier(),
|
||||||
_ => {
|
_ => {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
@ -610,20 +610,23 @@ impl<'src> Lexer<'src> {
|
|||||||
/// Lex a double-character token of kind `then` if the second character of
|
/// Lex a double-character token of kind `then` if the second character of
|
||||||
/// that token would be `second`, otherwise lex a single-character token of
|
/// that token would be `second`, otherwise lex a single-character token of
|
||||||
/// kind `otherwise`
|
/// kind `otherwise`
|
||||||
fn lex_choice(
|
fn lex_choices(
|
||||||
&mut self,
|
&mut self,
|
||||||
second: char,
|
first: char,
|
||||||
then: TokenKind,
|
choices: &[(char, TokenKind)],
|
||||||
otherwise: TokenKind,
|
otherwise: TokenKind,
|
||||||
) -> CompileResult<'src, ()> {
|
) -> CompileResult<'src, ()> {
|
||||||
self.advance()?;
|
self.presume(first)?;
|
||||||
|
|
||||||
if self.accepted(second)? {
|
for (second, then) in choices {
|
||||||
self.token(then);
|
if self.accepted(*second)? {
|
||||||
} else {
|
self.token(*then);
|
||||||
self.token(otherwise);
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.token(otherwise);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -930,6 +933,7 @@ mod tests {
|
|||||||
Eol => "\n",
|
Eol => "\n",
|
||||||
Equals => "=",
|
Equals => "=",
|
||||||
EqualsEquals => "==",
|
EqualsEquals => "==",
|
||||||
|
EqualsTilde => "=~",
|
||||||
Indent => " ",
|
Indent => " ",
|
||||||
InterpolationEnd => "}}",
|
InterpolationEnd => "}}",
|
||||||
InterpolationStart => "{{",
|
InterpolationStart => "{{",
|
||||||
@ -2054,7 +2058,7 @@ mod tests {
|
|||||||
|
|
||||||
error! {
|
error! {
|
||||||
name: tokenize_unknown,
|
name: tokenize_unknown,
|
||||||
input: "~",
|
input: "%",
|
||||||
offset: 0,
|
offset: 0,
|
||||||
line: 0,
|
line: 0,
|
||||||
column: 0,
|
column: 0,
|
||||||
@ -2113,16 +2117,6 @@ mod tests {
|
|||||||
kind: UnpairedCarriageReturn,
|
kind: UnpairedCarriageReturn,
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
|
||||||
name: unknown_start_of_token_tilde,
|
|
||||||
input: "~",
|
|
||||||
offset: 0,
|
|
||||||
line: 0,
|
|
||||||
column: 0,
|
|
||||||
width: 1,
|
|
||||||
kind: UnknownStartOfToken,
|
|
||||||
}
|
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
name: invalid_name_start_dash,
|
name: invalid_name_start_dash,
|
||||||
input: "-foo",
|
input: "-foo",
|
||||||
|
@ -43,6 +43,7 @@ mod compile_error;
|
|||||||
mod compile_error_kind;
|
mod compile_error_kind;
|
||||||
mod compiler;
|
mod compiler;
|
||||||
mod completions;
|
mod completions;
|
||||||
|
mod conditional_operator;
|
||||||
mod config;
|
mod config;
|
||||||
mod config_error;
|
mod config_error;
|
||||||
mod count;
|
mod count;
|
||||||
|
@ -58,15 +58,11 @@ impl<'src> Node<'src> for Expression<'src> {
|
|||||||
rhs,
|
rhs,
|
||||||
then,
|
then,
|
||||||
otherwise,
|
otherwise,
|
||||||
inverted,
|
operator,
|
||||||
} => {
|
} => {
|
||||||
let mut tree = Tree::atom(Keyword::If.lexeme());
|
let mut tree = Tree::atom(Keyword::If.lexeme());
|
||||||
tree.push_mut(lhs.tree());
|
tree.push_mut(lhs.tree());
|
||||||
if *inverted {
|
tree.push_mut(operator.to_string());
|
||||||
tree.push_mut("!=");
|
|
||||||
} else {
|
|
||||||
tree.push_mut("==");
|
|
||||||
}
|
|
||||||
tree.push_mut(rhs.tree());
|
tree.push_mut(rhs.tree());
|
||||||
tree.push_mut(then.tree());
|
tree.push_mut(then.tree());
|
||||||
tree.push_mut(otherwise.tree());
|
tree.push_mut(otherwise.tree());
|
||||||
|
@ -419,11 +419,14 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> {
|
fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> {
|
||||||
let lhs = self.parse_expression()?;
|
let lhs = self.parse_expression()?;
|
||||||
|
|
||||||
let inverted = self.accepted(BangEquals)?;
|
let operator = if self.accepted(BangEquals)? {
|
||||||
|
ConditionalOperator::Inequality
|
||||||
if !inverted {
|
} else if self.accepted(EqualsTilde)? {
|
||||||
|
ConditionalOperator::RegexMatch
|
||||||
|
} else {
|
||||||
self.expect(EqualsEquals)?;
|
self.expect(EqualsEquals)?;
|
||||||
}
|
ConditionalOperator::Equality
|
||||||
|
};
|
||||||
|
|
||||||
let rhs = self.parse_expression()?;
|
let rhs = self.parse_expression()?;
|
||||||
|
|
||||||
@ -449,7 +452,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
rhs: Box::new(rhs),
|
rhs: Box::new(rhs),
|
||||||
then: Box::new(then),
|
then: Box::new(then),
|
||||||
otherwise: Box::new(otherwise),
|
otherwise: Box::new(otherwise),
|
||||||
inverted,
|
operator,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,9 +18,9 @@ use crate::compiler::Compiler;
|
|||||||
|
|
||||||
mod full {
|
mod full {
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
assignment::Assignment, dependency::Dependency, expression::Expression, fragment::Fragment,
|
assignment::Assignment, conditional_operator::ConditionalOperator, dependency::Dependency,
|
||||||
justfile::Justfile, line::Line, parameter::Parameter, parameter_kind::ParameterKind,
|
expression::Expression, fragment::Fragment, justfile::Justfile, line::Line,
|
||||||
recipe::Recipe, thunk::Thunk,
|
parameter::Parameter, parameter_kind::ParameterKind, recipe::Recipe, thunk::Thunk,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +198,7 @@ pub enum Expression {
|
|||||||
rhs: Box<Expression>,
|
rhs: Box<Expression>,
|
||||||
then: Box<Expression>,
|
then: Box<Expression>,
|
||||||
otherwise: Box<Expression>,
|
otherwise: Box<Expression>,
|
||||||
inverted: bool,
|
operator: ConditionalOperator,
|
||||||
},
|
},
|
||||||
String {
|
String {
|
||||||
text: String,
|
text: String,
|
||||||
@ -245,16 +245,16 @@ impl Expression {
|
|||||||
},
|
},
|
||||||
Conditional {
|
Conditional {
|
||||||
lhs,
|
lhs,
|
||||||
rhs,
|
operator,
|
||||||
inverted,
|
|
||||||
then,
|
|
||||||
otherwise,
|
otherwise,
|
||||||
|
rhs,
|
||||||
|
then,
|
||||||
} => Expression::Conditional {
|
} => Expression::Conditional {
|
||||||
lhs: Box::new(Expression::new(lhs)),
|
lhs: Box::new(Expression::new(lhs)),
|
||||||
|
operator: ConditionalOperator::new(*operator),
|
||||||
|
otherwise: Box::new(Expression::new(otherwise)),
|
||||||
rhs: Box::new(Expression::new(rhs)),
|
rhs: Box::new(Expression::new(rhs)),
|
||||||
then: Box::new(Expression::new(then)),
|
then: Box::new(Expression::new(then)),
|
||||||
otherwise: Box::new(Expression::new(otherwise)),
|
|
||||||
inverted: *inverted,
|
|
||||||
},
|
},
|
||||||
StringLiteral { string_literal } => Expression::String {
|
StringLiteral { string_literal } => Expression::String {
|
||||||
text: string_literal.cooked.clone(),
|
text: string_literal.cooked.clone(),
|
||||||
@ -267,6 +267,23 @@ impl Expression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
||||||
|
pub enum ConditionalOperator {
|
||||||
|
Equality,
|
||||||
|
Inequality,
|
||||||
|
RegexMatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConditionalOperator {
|
||||||
|
fn new(operator: full::ConditionalOperator) -> Self {
|
||||||
|
match operator {
|
||||||
|
full::ConditionalOperator::Equality => Self::Equality,
|
||||||
|
full::ConditionalOperator::Inequality => Self::Inequality,
|
||||||
|
full::ConditionalOperator::RegexMatch => Self::RegexMatch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
||||||
pub struct Dependency {
|
pub struct Dependency {
|
||||||
pub recipe: String,
|
pub recipe: String,
|
||||||
@ -274,8 +291,8 @@ pub struct Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Dependency {
|
impl Dependency {
|
||||||
fn new(dependency: &full::Dependency) -> Dependency {
|
fn new(dependency: &full::Dependency) -> Self {
|
||||||
Dependency {
|
Self {
|
||||||
recipe: dependency.recipe.name().to_owned(),
|
recipe: dependency.recipe.name().to_owned(),
|
||||||
arguments: dependency.arguments.iter().map(Expression::new).collect(),
|
arguments: dependency.arguments.iter().map(Expression::new).collect(),
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ pub(crate) enum TokenKind {
|
|||||||
Eol,
|
Eol,
|
||||||
Equals,
|
Equals,
|
||||||
EqualsEquals,
|
EqualsEquals,
|
||||||
|
EqualsTilde,
|
||||||
Identifier,
|
Identifier,
|
||||||
Indent,
|
Indent,
|
||||||
InterpolationEnd,
|
InterpolationEnd,
|
||||||
@ -69,6 +70,7 @@ impl Display for TokenKind {
|
|||||||
Plus => "'+'",
|
Plus => "'+'",
|
||||||
StringToken => "string",
|
StringToken => "string",
|
||||||
Text => "command text",
|
Text => "command text",
|
||||||
|
EqualsTilde => "'=~'",
|
||||||
Unspecified => "unspecified",
|
Unspecified => "unspecified",
|
||||||
Whitespace => "whitespace",
|
Whitespace => "whitespace",
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ test! {
|
|||||||
",
|
",
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "
|
stderr: "
|
||||||
error: Expected '!=', '==', or '+', but found identifier
|
error: Expected '!=', '==', '=~', or '+', but found identifier
|
||||||
|
|
|
|
||||||
1 | a := if '' a '' { '' } else { b }
|
1 | a := if '' a '' { '' } else { b }
|
||||||
| ^
|
| ^
|
||||||
|
@ -25,6 +25,7 @@ mod misc;
|
|||||||
mod positional_arguments;
|
mod positional_arguments;
|
||||||
mod quiet;
|
mod quiet;
|
||||||
mod readme;
|
mod readme;
|
||||||
|
mod regexes;
|
||||||
mod search;
|
mod search;
|
||||||
mod shebang;
|
mod shebang;
|
||||||
mod shell;
|
mod shell;
|
||||||
|
66
tests/regexes.rs
Normal file
66
tests/regexes.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_succeeds_evaluates_to_first_branch() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
foo := if 'abbbc' =~ 'ab+c' {
|
||||||
|
'yes'
|
||||||
|
} else {
|
||||||
|
'no'
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
echo {{ foo }}
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.stderr("echo yes\n")
|
||||||
|
.stdout("yes\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_fails_evaluates_to_second_branch() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
foo := if 'abbbc' =~ 'ab{4}c' {
|
||||||
|
'yes'
|
||||||
|
} else {
|
||||||
|
'no'
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
echo {{ foo }}
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.stderr("echo no\n")
|
||||||
|
.stdout("no\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_regex_fails_at_runtime() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
default:
|
||||||
|
echo before
|
||||||
|
echo {{ if '' =~ '(' { 'a' } else { 'b' } }}
|
||||||
|
echo after
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.stderr(
|
||||||
|
"
|
||||||
|
echo before
|
||||||
|
error: regex parse error:
|
||||||
|
(
|
||||||
|
^
|
||||||
|
error: unclosed group
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.stdout("before\n")
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.run();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user