Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement regular expression match conditionals #970

Merged
merged 3 commits into from
Sep 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 32 additions & 32 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,37 +1,38 @@
[package]
name = "just"
version = "0.10.1"
name = "just"
version = "0.10.1"
description = "🤖 Just a command runner"
authors = ["Casey Rodarmor <[email protected]>"]
license = "CC0-1.0"
homepage = "https://github.com/casey/just"
repository = "https://github.com/casey/just"
readme = "crates-io-readme.md"
edition = "2018"
autotests = false
categories = ["command-line-utilities", "development-tools"]
keywords = ["command-line", "task", "runner", "development", "utility"]
authors = ["Casey Rodarmor <[email protected]>"]
license = "CC0-1.0"
homepage = "https://github.com/casey/just"
repository = "https://github.com/casey/just"
readme = "crates-io-readme.md"
edition = "2018"
autotests = false
categories = ["command-line-utilities", "development-tools"]
keywords = ["command-line", "task", "runner", "development", "utility"]

[workspace]
members = [".", "bin/ref-type"]

[dependencies]
ansi_term = "0.12.0"
atty = "0.2.0"
camino = "1.0.4"
derivative = "2.0.0"
dotenv = "0.15.0"
ansi_term = "0.12.0"
atty = "0.2.0"
camino = "1.0.4"
derivative = "2.0.0"
dotenv = "0.15.0"
edit-distance = "2.0.0"
env_logger = "0.9.0"
lazy_static = "1.0.0"
lexiclean = "0.0.1"
libc = "0.2.0"
log = "0.4.4"
snafu = "0.6.0"
strum_macros = "0.21.1"
target = "2.0.0"
tempfile = "3.0.0"
typed-arena = "2.0.1"
env_logger = "0.9.0"
lazy_static = "1.0.0"
lexiclean = "0.0.1"
libc = "0.2.0"
log = "0.4.4"
regex = "1.5.4"
snafu = "0.6.0"
strum_macros = "0.21.1"
target = "2.0.0"
tempfile = "3.0.0"
typed-arena = "2.0.1"
unicode-width = "0.1.0"

[dependencies.clap]
Expand All @@ -47,13 +48,12 @@ version = "0.21.0"
features = ["derive"]

[dev-dependencies]
cradle = "0.0.22"
executable-path = "1.0.0"
cradle = "0.0.22"
executable-path = "1.0.0"
pretty_assertions = "0.7.0"
regex = "1.5.4"
temptree = "0.2.0"
which = "4.0.0"
yaml-rust = "0.4.5"
temptree = "0.2.0"
which = "4.0.0"
yaml-rust = "0.4.5"

[features]
# No features are active by default.
Expand Down
16 changes: 16 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,22 @@ $ just bar
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
their branches. This can be used to make sure that backtick expressions don't
run when they shouldn't.
Expand Down
28 changes: 15 additions & 13 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub(crate) use edit_distance::edit_distance;
pub(crate) use lexiclean::Lexiclean;
pub(crate) use libc::EXIT_FAILURE;
pub(crate) use log::{info, warn};
pub(crate) use regex::Regex;
pub(crate) use snafu::{ResultExt, Snafu};
pub(crate) use strum::{Display, EnumString, IntoStaticStr};
pub(crate) use typed_arena::Arena;
Expand All @@ -46,19 +47,20 @@ pub(crate) use crate::{
pub(crate) use crate::{
alias::Alias, analyzer::Analyzer, assignment::Assignment,
assignment_resolver::AssignmentResolver, ast::Ast, binding::Binding, color::Color,
compile_error::CompileError, compile_error_kind::CompileErrorKind, config::Config,
config_error::ConfigError, count::Count, delimiter::Delimiter, dependency::Dependency,
enclosure::Enclosure, error::Error, evaluator::Evaluator, expression::Expression,
fragment::Fragment, function::Function, function_context::FunctionContext,
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
justfile::Justfile, keyword::Keyword, lexer::Lexer, line::Line, list::List, loader::Loader,
name::Name, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
parser::Parser, platform::Platform, position::Position, positional::Positional, 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, show_whitespace::ShowWhitespace, string_kind::StringKind,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
compile_error::CompileError, compile_error_kind::CompileErrorKind,
conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError,
count::Count, delimiter::Delimiter, dependency::Dependency, enclosure::Enclosure, error::Error,
evaluator::Evaluator, expression::Expression, fragment::Fragment, function::Function,
function_context::FunctionContext, interrupt_guard::InterruptGuard,
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, keyword::Keyword,
lexer::Lexer, line::Line, list::List, loader::Loader, name::Name, output_error::OutputError,
parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform,
position::Position, positional::Positional, 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,
show_whitespace::ShowWhitespace, 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,
};
Expand Down
22 changes: 22 additions & 0 deletions src/conditional_operator.rs
Original file line number Diff line number Diff line change
@@ -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, "=~"),
}
}
}
6 changes: 6 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ pub(crate) enum Error<'src> {
},
NoChoosableRecipes,
NoRecipes,
RegexCompile {
source: regex::Error,
},
Search {
search_error: SearchError,
},
Expand Down Expand Up @@ -507,6 +510,9 @@ impl<'src> ColorDisplay for Error<'src> {
NoRecipes => {
write!(f, "Justfile contains no recipes.")?;
}
RegexCompile { source } => {
write!(f, "{}", source)?;
}
Search { search_error } => Display::fmt(search_error, f)?,
Shebang {
recipe,
Expand Down
14 changes: 10 additions & 4 deletions src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,17 @@ impl<'src, 'run> Evaluator<'src, 'run> {
rhs,
then,
otherwise,
inverted,
operator,
} => {
let lhs = self.evaluate_expression(lhs)?;
let rhs = self.evaluate_expression(rhs)?;
let condition = if *inverted { lhs != rhs } else { lhs == rhs };
let lhs_value = self.evaluate_expression(lhs)?;
let rhs_value = self.evaluate_expression(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 {
self.evaluate_expression(then)
} else {
Expand Down
10 changes: 3 additions & 7 deletions src/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub(crate) enum Expression<'src> {
rhs: Box<Expression<'src>>,
then: Box<Expression<'src>>,
otherwise: Box<Expression<'src>>,
inverted: bool,
operator: ConditionalOperator,
},
/// `(contents)`
Group { contents: Box<Expression<'src>> },
Expand All @@ -52,15 +52,11 @@ impl<'src> Display for Expression<'src> {
rhs,
then,
otherwise,
inverted,
operator,
} => write!(
f,
"if {} {} {} {{ {} }} else {{ {} }}",
lhs,
if *inverted { "!=" } else { "==" },
rhs,
then,
otherwise
lhs, operator, rhs, then, otherwise
),
Expression::StringLiteral { string_literal } => write!(f, "{}", string_literal),
Expression::Variable { name } => write!(f, "{}", name.lexeme()),
Expand Down
56 changes: 25 additions & 31 deletions src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,25 +475,25 @@ impl<'src> Lexer<'src> {
/// Lex token beginning with `start` outside of a recipe body
fn lex_normal(&mut self, start: char) -> CompileResult<'src, ()> {
match start {
'&' => self.lex_digraph('&', '&', AmpersandAmpersand),
' ' | '\t' => self.lex_whitespace(),
'!' => self.lex_digraph('!', '=', BangEquals),
'*' => self.lex_single(Asterisk),
'#' => self.lex_comment(),
'$' => self.lex_single(Dollar),
'&' => self.lex_digraph('&', '&', AmpersandAmpersand),
'(' => self.lex_delimiter(ParenL),
')' => 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_choice('=', EqualsEquals, Equals),
',' => self.lex_single(Comma),
':' => self.lex_colon(),
'(' => self.lex_delimiter(ParenL),
')' => self.lex_delimiter(ParenR),
'`' | '"' | '\'' => self.lex_string(),
'{' => self.lex_delimiter(BraceL),
'}' => 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(),
_ => {
self.advance()?;
Expand Down Expand Up @@ -610,20 +610,23 @@ impl<'src> Lexer<'src> {
/// 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
/// kind `otherwise`
fn lex_choice(
fn lex_choices(
&mut self,
second: char,
then: TokenKind,
first: char,
choices: &[(char, TokenKind)],
otherwise: TokenKind,
) -> CompileResult<'src, ()> {
self.advance()?;
self.presume(first)?;

if self.accepted(second)? {
self.token(then);
} else {
self.token(otherwise);
for (second, then) in choices {
if self.accepted(*second)? {
self.token(*then);
return Ok(());
}
}

self.token(otherwise);

Ok(())
}

Expand Down Expand Up @@ -930,6 +933,7 @@ mod tests {
Eol => "\n",
Equals => "=",
EqualsEquals => "==",
EqualsTilde => "=~",
Indent => " ",
InterpolationEnd => "}}",
InterpolationStart => "{{",
Expand Down Expand Up @@ -2054,7 +2058,7 @@ mod tests {

error! {
name: tokenize_unknown,
input: "~",
input: "%",
offset: 0,
line: 0,
column: 0,
Expand Down Expand Up @@ -2113,16 +2117,6 @@ mod tests {
kind: UnpairedCarriageReturn,
}

error! {
name: unknown_start_of_token_tilde,
input: "~",
offset: 0,
line: 0,
column: 0,
width: 1,
kind: UnknownStartOfToken,
}

error! {
name: invalid_name_start_dash,
input: "-foo",
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ mod compile_error;
mod compile_error_kind;
mod compiler;
mod completions;
mod conditional_operator;
mod config;
mod config_error;
mod count;
Expand Down
8 changes: 2 additions & 6 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,11 @@ impl<'src> Node<'src> for Expression<'src> {
rhs,
then,
otherwise,
inverted,
operator,
} => {
let mut tree = Tree::atom(Keyword::If.lexeme());
tree.push_mut(lhs.tree());
if *inverted {
tree.push_mut("!=");
} else {
tree.push_mut("==");
}
tree.push_mut(operator.to_string());
tree.push_mut(rhs.tree());
tree.push_mut(then.tree());
tree.push_mut(otherwise.tree());
Expand Down
13 changes: 8 additions & 5 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,11 +419,14 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> {
let lhs = self.parse_expression()?;

let inverted = self.accepted(BangEquals)?;

if !inverted {
let operator = if self.accepted(BangEquals)? {
ConditionalOperator::Inequality
} else if self.accepted(EqualsTilde)? {
ConditionalOperator::RegexMatch
} else {
self.expect(EqualsEquals)?;
}
ConditionalOperator::Equality
};

let rhs = self.parse_expression()?;

Expand All @@ -449,7 +452,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
rhs: Box::new(rhs),
then: Box::new(then),
otherwise: Box::new(otherwise),
inverted,
operator,
})
}

Expand Down
Loading