Unify string lexing (#790)

Unify lexing of backticks, cooked strings, and raw strings. Also allow
newlines in backticks and cooked strings, since I can't think of a reason
not to.
This commit is contained in:
Casey Rodarmor 2021-04-04 16:41:02 -07:00 committed by GitHub
parent 78b67f6cae
commit dd578d141c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 385 additions and 302 deletions

View File

@ -9,7 +9,7 @@ tokens
------ ------
``` ```
BACKTICK = `[^`\n\r]*` BACKTICK = `[^`]*`
COMMENT = #([^!].*)?$ COMMENT = #([^!].*)?$
DEDENT = emitted when indentation decreases DEDENT = emitted when indentation decreases
EOF = emitted at the end of the file EOF = emitted at the end of the file
@ -17,7 +17,7 @@ INDENT = emitted when indentation increases
LINE = emitted before a recipe line LINE = emitted before a recipe line
NAME = [a-zA-Z_][a-zA-Z0-9_-]* NAME = [a-zA-Z_][a-zA-Z0-9_-]*
NEWLINE = \n|\r\n NEWLINE = \n|\r\n
RAW_STRING = '[^'\r\n]*' RAW_STRING = '[^']*'
STRING = "[^"]*" # also processes \n \r \t \" \\ escapes STRING = "[^"]*" # also processes \n \r \t \" \\ escapes
TEXT = recipe text, only matches in a recipe body TEXT = recipe text, only matches in a recipe body
``` ```

View File

@ -557,31 +557,27 @@ string-with-slash := "\"
string-with-tab := " " string-with-tab := " "
``` ```
Single-quoted strings do not recognize escape sequences and may contain line breaks: Strings may contain line breaks:
```make
single := '
hello
'
double := "
goodbye
"
```
Single-quoted strings do not recognize escape sequences:
```make ```make
escapes := '\t\n\r\"\\' escapes := '\t\n\r\"\\'
line-breaks := 'hello
this
is
a
raw
string!
'
``` ```
```sh ```sh
$ just --evaluate $ just --evaluate
escapes := "\t\n\r\"\\" escapes := "\t\n\r\"\\"
line-breaks := "hello
this
is
a
raw
string!
"
``` ```
=== Ignoring Errors === Ignoring Errors

View File

@ -55,10 +55,10 @@ pub(crate) use crate::{
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError, recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set, scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set,
setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table, string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind,
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe,
verbosity::Verbosity, warning::Warning, use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning,
}; };
// type aliases // type aliases

View File

@ -242,10 +242,10 @@ impl Display for CompilationError<'_> {
UnterminatedInterpolation => { UnterminatedInterpolation => {
writeln!(f, "Unterminated interpolation")?; writeln!(f, "Unterminated interpolation")?;
}, },
UnterminatedString => { UnterminatedString(StringKind::Cooked) | UnterminatedString(StringKind::Raw) => {
writeln!(f, "Unterminated string")?; writeln!(f, "Unterminated string")?;
}, },
UnterminatedBacktick => { UnterminatedString(StringKind::Backtick) => {
writeln!(f, "Unterminated backtick")?; writeln!(f, "Unterminated backtick")?;
}, },
Internal { ref message } => { Internal { ref message } => {

View File

@ -107,6 +107,5 @@ pub(crate) enum CompilationErrorKind<'src> {
open_line: usize, open_line: usize,
}, },
UnterminatedInterpolation, UnterminatedInterpolation,
UnterminatedString, UnterminatedString(StringKind),
UnterminatedBacktick,
} }

View File

@ -30,8 +30,8 @@ pub(crate) struct Lexer<'src> {
recipe_body: bool, recipe_body: bool,
/// Indentation stack /// Indentation stack
indentation: Vec<&'src str>, indentation: Vec<&'src str>,
/// Current interpolation start token /// Interpolation token start stack
interpolation_start: Option<Token<'src>>, interpolation_stack: Vec<Token<'src>>,
/// Current open delimiters /// Current open delimiters
open_delimiters: Vec<(Delimiter, usize)>, open_delimiters: Vec<(Delimiter, usize)>,
} }
@ -60,7 +60,7 @@ impl<'src> Lexer<'src> {
token_end: start, token_end: start,
recipe_body_pending: false, recipe_body_pending: false,
recipe_body: false, recipe_body: false,
interpolation_start: None, interpolation_stack: Vec::new(),
open_delimiters: Vec::new(), open_delimiters: Vec::new(),
chars, chars,
next, next,
@ -210,10 +210,8 @@ impl<'src> Lexer<'src> {
// The width of the error site to highlight depends on the kind of error: // The width of the error site to highlight depends on the kind of error:
let length = match kind { let length = match kind {
// highlight ' or " // highlight ', ", or `
UnterminatedString => 1, UnterminatedString(_) => 1,
// highlight `
UnterminatedBacktick => 1,
// highlight the full token // highlight the full token
_ => self.lexeme().len(), _ => self.lexeme().len(),
}; };
@ -280,7 +278,7 @@ impl<'src> Lexer<'src> {
match self.next { match self.next {
Some(first) => { Some(first) => {
if let Some(interpolation_start) = self.interpolation_start { if let Some(&interpolation_start) = self.interpolation_stack.last() {
self.lex_interpolation(interpolation_start, first)? self.lex_interpolation(interpolation_start, first)?
} else if self.recipe_body { } else if self.recipe_body {
self.lex_body()? self.lex_body()?
@ -292,7 +290,7 @@ impl<'src> Lexer<'src> {
} }
} }
if let Some(interpolation_start) = self.interpolation_start { if let Some(&interpolation_start) = self.interpolation_stack.last() {
return Err(Self::unterminated_interpolation_error(interpolation_start)); return Err(Self::unterminated_interpolation_error(interpolation_start));
} }
@ -477,10 +475,10 @@ impl<'src> Lexer<'src> {
'}' => self.lex_delimiter(BraceR), '}' => self.lex_delimiter(BraceR),
'+' => self.lex_single(Plus), '+' => self.lex_single(Plus),
'#' => self.lex_comment(), '#' => self.lex_comment(),
'`' => self.lex_backtick(),
' ' => self.lex_whitespace(), ' ' => self.lex_whitespace(),
'"' => self.lex_cooked_string(), '`' => self.lex_string(StringKind::Backtick),
'\'' => self.lex_raw_string(), '"' => self.lex_string(StringKind::Cooked),
'\'' => self.lex_string(StringKind::Raw),
'\n' => self.lex_eol(), '\n' => self.lex_eol(),
'\r' => self.lex_eol(), '\r' => self.lex_eol(),
'\t' => self.lex_whitespace(), '\t' => self.lex_whitespace(),
@ -500,7 +498,13 @@ impl<'src> Lexer<'src> {
) -> CompilationResult<'src, ()> { ) -> CompilationResult<'src, ()> {
if self.rest_starts_with("}}") { if self.rest_starts_with("}}") {
// end current interpolation // end current interpolation
self.interpolation_start = None; if self.interpolation_stack.pop().is_none() {
self.advance()?;
self.advance()?;
return Err(self.internal_error(
"Lexer::lex_interpolation found `}}` but was called with empty interpolation stack.",
));
}
// Emit interpolation end token // Emit interpolation end token
self.lex_double(InterpolationEnd) self.lex_double(InterpolationEnd)
} else if self.at_eol_or_eof() { } else if self.at_eol_or_eof() {
@ -530,7 +534,7 @@ impl<'src> Lexer<'src> {
continue; continue;
} }
if let Some('\n') = self.next { if self.rest_starts_with("\n") {
break Newline; break Newline;
} }
@ -542,7 +546,7 @@ impl<'src> Lexer<'src> {
break Interpolation; break Interpolation;
} }
if self.next.is_none() { if self.rest().is_empty() {
break EndOfFile; break EndOfFile;
} }
@ -559,7 +563,9 @@ impl<'src> Lexer<'src> {
NewlineCarriageReturn => self.lex_double(Eol), NewlineCarriageReturn => self.lex_double(Eol),
Interpolation => { Interpolation => {
self.lex_double(InterpolationStart)?; self.lex_double(InterpolationStart)?;
self.interpolation_start = Some(self.tokens[self.tokens.len() - 1]); self
.interpolation_stack
.push(self.tokens[self.tokens.len() - 1]);
Ok(()) Ok(())
}, },
EndOfFile => Ok(()), EndOfFile => Ok(()),
@ -738,25 +744,6 @@ impl<'src> Lexer<'src> {
Ok(()) Ok(())
} }
/// Lex backtick: `[^\r\n]*`
fn lex_backtick(&mut self) -> CompilationResult<'src, ()> {
// advance over initial `
self.advance()?;
while !self.next_is('`') {
if self.at_eol_or_eof() {
return Err(self.error(UnterminatedBacktick));
}
self.advance()?;
}
self.advance()?;
self.token(Backtick);
Ok(())
}
/// Lex whitespace: [ \t]+ /// Lex whitespace: [ \t]+
fn lex_whitespace(&mut self) -> CompilationResult<'src, ()> { fn lex_whitespace(&mut self) -> CompilationResult<'src, ()> {
while self.next_is_whitespace() { while self.next_is_whitespace() {
@ -768,48 +755,29 @@ impl<'src> Lexer<'src> {
Ok(()) Ok(())
} }
/// Lex raw string: '[^']*' /// Lex a backtick, cooked string, or raw string.
fn lex_raw_string(&mut self) -> CompilationResult<'src, ()> { ///
self.presume('\'')?; /// Backtick: `[^`]*`
/// Cooked string: "[^"]*" # also processes escape sequences
loop { /// Raw string: '[^']*'
match self.next { fn lex_string(&mut self, kind: StringKind) -> CompilationResult<'src, ()> {
Some('\'') => break, self.presume(kind.delimiter())?;
None => return Err(self.error(UnterminatedString)),
Some(_) => {},
}
self.advance()?;
}
self.presume('\'')?;
self.token(StringRaw);
Ok(())
}
/// Lex cooked string: "[^"\n\r]*" (also processes escape sequences)
fn lex_cooked_string(&mut self) -> CompilationResult<'src, ()> {
self.presume('"')?;
let mut escape = false; let mut escape = false;
loop { loop {
match self.next { match self.next {
Some('\r') | Some('\n') | None => return Err(self.error(UnterminatedString)), Some(c) if c == kind.delimiter() && !escape => break,
Some('"') if !escape => break, Some('\\') if kind.processes_escape_sequences() && !escape => escape = true,
Some('\\') if !escape => escape = true, Some(_) => escape = false,
_ => escape = false, None => return Err(self.error(kind.unterminated_error_kind())),
} }
self.advance()?; self.advance()?;
} }
// advance over closing " self.presume(kind.delimiter())?;
self.advance()?; self.token(kind.token_kind());
self.token(StringCooked);
Ok(()) Ok(())
} }
@ -821,6 +789,10 @@ mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
const STRING_BACKTICK: TokenKind = StringToken(StringKind::Backtick);
const STRING_RAW: TokenKind = StringToken(StringKind::Raw);
const STRING_COOKED: TokenKind = StringToken(StringKind::Cooked);
macro_rules! test { macro_rules! test {
{ {
name: $name:ident, name: $name:ident,
@ -929,7 +901,7 @@ mod tests {
Dedent | Eof => "", Dedent | Eof => "",
// Variable lexemes // Variable lexemes
Text | StringCooked | StringRaw | Identifier | Comment | Backtick | Unspecified => Text | StringToken(_) | Identifier | Comment | Unspecified =>
panic!("Token {:?} has no default lexeme", kind), panic!("Token {:?} has no default lexeme", kind),
} }
} }
@ -993,19 +965,37 @@ mod tests {
test! { test! {
name: backtick, name: backtick,
text: "`echo`", text: "`echo`",
tokens: (Backtick:"`echo`"), tokens: (STRING_BACKTICK:"`echo`"),
}
test! {
name: backtick_multi_line,
text: "`echo\necho`",
tokens: (STRING_BACKTICK:"`echo\necho`"),
} }
test! { test! {
name: raw_string, name: raw_string,
text: "'hello'", text: "'hello'",
tokens: (StringRaw:"'hello'"), tokens: (STRING_RAW:"'hello'"),
}
test! {
name: raw_string_multi_line,
text: "'hello\ngoodbye'",
tokens: (STRING_RAW:"'hello\ngoodbye'"),
} }
test! { test! {
name: cooked_string, name: cooked_string,
text: "\"hello\"", text: "\"hello\"",
tokens: (StringCooked:"\"hello\""), tokens: (STRING_COOKED:"\"hello\""),
}
test! {
name: cooked_string_multi_line,
text: "\"hello\ngoodbye\"",
tokens: (STRING_COOKED:"\"hello\ngoodbye\""),
} }
test! { test! {
@ -1066,11 +1056,11 @@ mod tests {
Whitespace, Whitespace,
Equals, Equals,
Whitespace, Whitespace,
StringRaw:"'foo'", STRING_RAW:"'foo'",
Whitespace, Whitespace,
Plus, Plus,
Whitespace, Whitespace,
StringRaw:"'bar'", STRING_RAW:"'bar'",
) )
} }
@ -1085,16 +1075,16 @@ mod tests {
Equals, Equals,
Whitespace, Whitespace,
ParenL, ParenL,
StringRaw:"'foo'", STRING_RAW:"'foo'",
Whitespace, Whitespace,
Plus, Plus,
Whitespace, Whitespace,
StringRaw:"'bar'", STRING_RAW:"'bar'",
ParenR, ParenR,
Whitespace, Whitespace,
Plus, Plus,
Whitespace, Whitespace,
Backtick:"`baz`", STRING_BACKTICK:"`baz`",
), ),
} }
@ -1421,11 +1411,11 @@ mod tests {
Indent:" ", Indent:" ",
Text:"echo ", Text:"echo ",
InterpolationStart, InterpolationStart,
Backtick:"`echo hello`", STRING_BACKTICK:"`echo hello`",
Whitespace, Whitespace,
Plus, Plus,
Whitespace, Whitespace,
Backtick:"`echo goodbye`", STRING_BACKTICK:"`echo goodbye`",
InterpolationEnd, InterpolationEnd,
Dedent, Dedent,
), ),
@ -1441,7 +1431,7 @@ mod tests {
Indent:" ", Indent:" ",
Text:"echo ", Text:"echo ",
InterpolationStart, InterpolationStart,
StringRaw:"'\n'", STRING_RAW:"'\n'",
InterpolationEnd, InterpolationEnd,
Dedent, Dedent,
), ),
@ -1513,19 +1503,19 @@ mod tests {
Whitespace, Whitespace,
Equals, Equals,
Whitespace, Whitespace,
StringCooked:"\"'a'\"", STRING_COOKED:"\"'a'\"",
Whitespace, Whitespace,
Plus, Plus,
Whitespace, Whitespace,
StringRaw:"'\"b\"'", STRING_RAW:"'\"b\"'",
Whitespace, Whitespace,
Plus, Plus,
Whitespace, Whitespace,
StringCooked:"\"'c'\"", STRING_COOKED:"\"'c'\"",
Whitespace, Whitespace,
Plus, Plus,
Whitespace, Whitespace,
StringRaw:"'\"d\"'", STRING_RAW:"'\"d\"'",
Comment:"#echo hello", Comment:"#echo hello",
) )
} }
@ -1593,7 +1583,7 @@ mod tests {
Whitespace, Whitespace,
Plus, Plus,
Whitespace, Whitespace,
StringCooked:"\"z\"", STRING_COOKED:"\"z\"",
Whitespace, Whitespace,
Plus, Plus,
Whitespace, Whitespace,
@ -1717,7 +1707,7 @@ mod tests {
Eol, Eol,
Identifier:"A", Identifier:"A",
Equals, Equals,
StringRaw:"'1'", STRING_RAW:"'1'",
Eol, Eol,
Identifier:"echo", Identifier:"echo",
Colon, Colon,
@ -1742,11 +1732,11 @@ mod tests {
Indent:" ", Indent:" ",
Text:"echo ", Text:"echo ",
InterpolationStart, InterpolationStart,
Backtick:"`echo hello`", STRING_BACKTICK:"`echo hello`",
Whitespace, Whitespace,
Plus, Plus,
Whitespace, Whitespace,
Backtick:"`echo goodbye`", STRING_BACKTICK:"`echo goodbye`",
InterpolationEnd, InterpolationEnd,
Dedent Dedent
), ),
@ -1775,11 +1765,11 @@ mod tests {
Whitespace, Whitespace,
Equals, Equals,
Whitespace, Whitespace,
Backtick:"`echo hello`", STRING_BACKTICK:"`echo hello`",
Whitespace, Whitespace,
Plus, Plus,
Whitespace, Whitespace,
Backtick:"`echo goodbye`", STRING_BACKTICK:"`echo goodbye`",
), ),
} }
@ -2021,7 +2011,7 @@ mod tests {
line: 0, line: 0,
column: 4, column: 4,
width: 1, width: 1,
kind: UnterminatedString, kind: UnterminatedString(StringKind::Cooked),
} }
error! { error! {
@ -2031,7 +2021,7 @@ mod tests {
line: 0, line: 0,
column: 4, column: 4,
width: 1, width: 1,
kind: UnterminatedString, kind: UnterminatedString(StringKind::Raw),
} }
error! { error! {
@ -2052,7 +2042,7 @@ mod tests {
line: 0, line: 0,
column: 0, column: 0,
width: 1, width: 1,
kind: UnterminatedBacktick, kind: UnterminatedString(StringKind::Backtick),
} }
error! { error! {
@ -2112,7 +2102,7 @@ mod tests {
line: 0, line: 0,
column: 4, column: 4,
width: 1, width: 1,
kind: UnterminatedString, kind: UnterminatedString(StringKind::Cooked),
} }
error! { error! {

View File

@ -117,6 +117,7 @@ mod setting;
mod settings; mod settings;
mod shebang; mod shebang;
mod show_whitespace; mod show_whitespace;
mod string_kind;
mod string_literal; mod string_literal;
mod subcommand; mod subcommand;
mod suggestion; mod suggestion;

View File

@ -453,11 +453,11 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
/// Parse a value, e.g. `(bar)` /// Parse a value, e.g. `(bar)`
fn parse_value(&mut self) -> CompilationResult<'src, Expression<'src>> { fn parse_value(&mut self) -> CompilationResult<'src, Expression<'src>> {
if self.next_is(StringCooked) || self.next_is(StringRaw) { if self.next_is(StringToken(StringKind::Cooked)) || self.next_is(StringToken(StringKind::Raw)) {
Ok(Expression::StringLiteral { Ok(Expression::StringLiteral {
string_literal: self.parse_string_literal()?, string_literal: self.parse_string_literal()?,
}) })
} else if self.next_is(Backtick) { } else if self.next_is(StringToken(StringKind::Backtick)) {
let next = self.next()?; let next = self.next()?;
let contents = &next.lexeme()[1..next.lexeme().len() - 1]; let contents = &next.lexeme()[1..next.lexeme().len() - 1];
@ -486,16 +486,19 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
/// Parse a string literal, e.g. `"FOO"` /// Parse a string literal, e.g. `"FOO"`
fn parse_string_literal(&mut self) -> CompilationResult<'src, StringLiteral<'src>> { fn parse_string_literal(&mut self) -> CompilationResult<'src, StringLiteral<'src>> {
let token = self.expect_any(&[StringRaw, StringCooked])?; let token = self.expect_any(&[
StringToken(StringKind::Raw),
StringToken(StringKind::Cooked),
])?;
let raw = &token.lexeme()[1..token.lexeme().len() - 1]; let raw = &token.lexeme()[1..token.lexeme().len() - 1];
match token.kind { match token.kind {
StringRaw => Ok(StringLiteral { StringToken(StringKind::Raw) => Ok(StringLiteral {
raw, raw,
cooked: Cow::Borrowed(raw), cooked: Cow::Borrowed(raw),
}), }),
StringCooked => { StringToken(StringKind::Cooked) => {
let mut cooked = String::new(); let mut cooked = String::new();
let mut escape = false; let mut escape = false;
for c in raw.chars() { for c in raw.chars() {
@ -1720,7 +1723,13 @@ mod tests {
column: 10, column: 10,
width: 1, width: 1,
kind: UnexpectedToken { kind: UnexpectedToken {
expected: vec![Backtick, Identifier, ParenL, StringCooked, StringRaw], expected: vec![
Identifier,
ParenL,
StringToken(StringKind::Backtick),
StringToken(StringKind::Cooked),
StringToken(StringKind::Raw)
],
found: Eol found: Eol
}, },
} }
@ -1733,7 +1742,13 @@ mod tests {
column: 10, column: 10,
width: 0, width: 0,
kind: UnexpectedToken { kind: UnexpectedToken {
expected: vec![Backtick, Identifier, ParenL, StringCooked, StringRaw], expected: vec![
Identifier,
ParenL,
StringToken(StringKind::Backtick),
StringToken(StringKind::Cooked),
StringToken(StringKind::Raw)
],
found: Eof, found: Eof,
}, },
} }
@ -1769,7 +1784,14 @@ mod tests {
column: 9, column: 9,
width: 0, width: 0,
kind: UnexpectedToken{ kind: UnexpectedToken{
expected: vec![Backtick, Identifier, ParenL, ParenR, StringCooked, StringRaw], expected: vec![
Identifier,
ParenL,
ParenR,
StringToken(StringKind::Backtick),
StringToken(StringKind::Cooked),
StringToken(StringKind::Raw)
],
found: Eof, found: Eof,
}, },
} }
@ -1782,7 +1804,14 @@ mod tests {
column: 12, column: 12,
width: 2, width: 2,
kind: UnexpectedToken{ kind: UnexpectedToken{
expected: vec![Backtick, Identifier, ParenL, ParenR, StringCooked, StringRaw], expected: vec![
Identifier,
ParenL,
ParenR,
StringToken(StringKind::Backtick),
StringToken(StringKind::Cooked),
StringToken(StringKind::Raw)
],
found: InterpolationEnd, found: InterpolationEnd,
}, },
} }
@ -1858,7 +1887,7 @@ mod tests {
column: 14, column: 14,
width: 1, width: 1,
kind: UnexpectedToken { kind: UnexpectedToken {
expected: vec![StringCooked, StringRaw], expected: vec![StringToken(StringKind::Cooked), StringToken(StringKind::Raw)],
found: BracketR, found: BracketR,
}, },
} }
@ -1897,7 +1926,7 @@ mod tests {
column: 21, column: 21,
width: 0, width: 0,
kind: UnexpectedToken { kind: UnexpectedToken {
expected: vec![BracketR, StringCooked, StringRaw], expected: vec![BracketR, StringToken(StringKind::Cooked), StringToken(StringKind::Raw)],
found: Eof, found: Eof,
}, },
} }

33
src/string_kind.rs Normal file
View File

@ -0,0 +1,33 @@
use crate::common::*;
#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
pub(crate) enum StringKind {
Backtick,
Cooked,
Raw,
}
impl StringKind {
pub(crate) fn delimiter(self) -> char {
match self {
Self::Backtick => '`',
Self::Cooked => '"',
Self::Raw => '\'',
}
}
pub(crate) fn token_kind(self) -> TokenKind {
TokenKind::StringToken(self)
}
pub(crate) fn unterminated_error_kind(self) -> CompilationErrorKind<'static> {
CompilationErrorKind::UnterminatedString(self)
}
pub(crate) fn processes_escape_sequences(self) -> bool {
match self {
Self::Backtick | Self::Raw => false,
Self::Cooked => true,
}
}
}

View File

@ -4,7 +4,6 @@ use crate::common::*;
pub(crate) enum TokenKind { pub(crate) enum TokenKind {
Asterisk, Asterisk,
At, At,
Backtick,
BangEquals, BangEquals,
BraceL, BraceL,
BraceR, BraceR,
@ -27,8 +26,7 @@ pub(crate) enum TokenKind {
ParenL, ParenL,
ParenR, ParenR,
Plus, Plus,
StringCooked, StringToken(StringKind),
StringRaw,
Text, Text,
Unspecified, Unspecified,
Whitespace, Whitespace,
@ -40,7 +38,6 @@ impl Display for TokenKind {
write!(f, "{}", match *self { write!(f, "{}", match *self {
Asterisk => "'*'", Asterisk => "'*'",
At => "'@'", At => "'@'",
Backtick => "backtick",
BangEquals => "'!='", BangEquals => "'!='",
BraceL => "'{'", BraceL => "'{'",
BraceR => "'}'", BraceR => "'}'",
@ -63,8 +60,9 @@ impl Display for TokenKind {
ParenL => "'('", ParenL => "'('",
ParenR => "')'", ParenR => "')'",
Plus => "'+'", Plus => "'+'",
StringCooked => "cooked string", StringToken(StringKind::Backtick) => "backtick",
StringRaw => "raw string", StringToken(StringKind::Cooked) => "cooked string",
StringToken(StringKind::Raw) => "raw string",
Text => "command text", Text => "command text",
Whitespace => "whitespace", Whitespace => "whitespace",
Unspecified => "unspecified", Unspecified => "unspecified",

View File

@ -20,4 +20,5 @@ mod quiet;
mod readme; mod readme;
mod search; mod search;
mod shell; mod shell;
mod string;
mod working_directory; mod working_directory;

View File

@ -588,18 +588,6 @@ hello := "c"
"#, "#,
} }
test! {
name: raw_string,
justfile: r#"
export EXPORTED_VARIABLE := '\z'
recipe:
printf "$EXPORTED_VARIABLE"
"#,
stdout: "\\z",
stderr: "printf \"$EXPORTED_VARIABLE\"\n",
}
test! { test! {
name: line_error_spacing, name: line_error_spacing,
justfile: r#" justfile: r#"
@ -1508,145 +1496,6 @@ test! {
status: EXIT_FAILURE, status: EXIT_FAILURE,
} }
test! {
name: invalid_escape_sequence,
justfile: r#"x := "\q"
a:"#,
args: ("a"),
stdout: "",
stderr: "error: `\\q` is not a valid escape sequence
|
1 | x := \"\\q\"
| ^^^^
",
status: EXIT_FAILURE,
}
test! {
name: multiline_raw_string,
justfile: "
string := 'hello
whatever'
a:
echo '{{string}}'
",
args: ("a"),
stdout: "hello
whatever
",
stderr: "echo 'hello
whatever'
",
}
test! {
name: error_line_after_multiline_raw_string,
justfile: "
string := 'hello
whatever' + 'yo'
a:
echo '{{foo}}'
",
args: ("a"),
stdout: "",
stderr: "error: Variable `foo` not defined
|
7 | echo '{{foo}}'
| ^^^
",
status: EXIT_FAILURE,
}
test! {
name: error_column_after_multiline_raw_string,
justfile: "
string := 'hello
whatever' + bar
a:
echo '{{string}}'
",
args: ("a"),
stdout: "",
stderr: "error: Variable `bar` not defined
|
4 | whatever' + bar
| ^^^
",
status: EXIT_FAILURE,
}
test! {
name: multiline_raw_string_in_interpolation,
justfile: r#"
a:
echo '{{"a" + '
' + "b"}}'
"#,
args: ("a"),
stdout: "
a
b
",
stderr: "
echo 'a
b'
",
}
test! {
name: error_line_after_multiline_raw_string_in_interpolation,
justfile: r#"
a:
echo '{{"a" + '
' + "b"}}'
echo {{b}}
"#,
args: ("a"),
stdout: "",
stderr: "error: Variable `b` not defined
|
6 | echo {{b}}
| ^
",
status: EXIT_FAILURE,
}
test! {
name: unterminated_raw_string,
justfile: "
a b= ':
",
args: ("a"),
stdout: "",
stderr: "error: Unterminated string
|
2 | a b= ':
| ^
",
status: EXIT_FAILURE,
}
test! {
name: unterminated_string,
justfile: r#"
a b= ":
"#,
args: ("a"),
stdout: "",
stderr: r#"error: Unterminated string
|
2 | a b= ":
| ^
"#,
status: EXIT_FAILURE,
}
test! { test! {
name: plus_variadic_recipe, name: plus_variadic_recipe,
justfile: " justfile: "
@ -2059,20 +1908,6 @@ foo:
status: EXIT_FAILURE, status: EXIT_FAILURE,
} }
test! {
name: unterminated_backtick,
justfile: "
foo a=\t`echo blaaaaaah:
echo {{a}}",
stderr: r#"
error: Unterminated backtick
|
2 | foo a= `echo blaaaaaah:
| ^
"#,
status: EXIT_FAILURE,
}
test! { test! {
name: unknown_start_of_token, name: unknown_start_of_token,
justfile: " justfile: "

201
tests/string.rs Normal file
View File

@ -0,0 +1,201 @@
use crate::common::*;
test! {
name: raw_string,
justfile: r#"
export EXPORTED_VARIABLE := '\z'
recipe:
printf "$EXPORTED_VARIABLE"
"#,
stdout: "\\z",
stderr: "printf \"$EXPORTED_VARIABLE\"\n",
}
test! {
name: multiline_raw_string,
justfile: "
string := 'hello
whatever'
a:
echo '{{string}}'
",
args: ("a"),
stdout: "hello
whatever
",
stderr: "echo 'hello
whatever'
",
}
test! {
name: multiline_backtick,
justfile: "
string := `echo hello
echo goodbye
`
a:
echo '{{string}}'
",
args: ("a"),
stdout: "hello\ngoodbye\n",
stderr: "echo 'hello
goodbye'
",
}
test! {
name: multiline_cooked_string,
justfile: r#"
string := "hello
whatever"
a:
echo '{{string}}'
"#,
args: ("a"),
stdout: "hello
whatever
",
stderr: "echo 'hello
whatever'
",
}
test! {
name: invalid_escape_sequence,
justfile: r#"x := "\q"
a:"#,
args: ("a"),
stdout: "",
stderr: "error: `\\q` is not a valid escape sequence
|
1 | x := \"\\q\"
| ^^^^
",
status: EXIT_FAILURE,
}
test! {
name: error_line_after_multiline_raw_string,
justfile: "
string := 'hello
whatever' + 'yo'
a:
echo '{{foo}}'
",
args: ("a"),
stdout: "",
stderr: "error: Variable `foo` not defined
|
7 | echo '{{foo}}'
| ^^^
",
status: EXIT_FAILURE,
}
test! {
name: error_column_after_multiline_raw_string,
justfile: "
string := 'hello
whatever' + bar
a:
echo '{{string}}'
",
args: ("a"),
stdout: "",
stderr: "error: Variable `bar` not defined
|
4 | whatever' + bar
| ^^^
",
status: EXIT_FAILURE,
}
test! {
name: multiline_raw_string_in_interpolation,
justfile: r#"
a:
echo '{{"a" + '
' + "b"}}'
"#,
args: ("a"),
stdout: "
a
b
",
stderr: "
echo 'a
b'
",
}
test! {
name: error_line_after_multiline_raw_string_in_interpolation,
justfile: r#"
a:
echo '{{"a" + '
' + "b"}}'
echo {{b}}
"#,
args: ("a"),
stdout: "",
stderr: "error: Variable `b` not defined
|
6 | echo {{b}}
| ^
",
status: EXIT_FAILURE,
}
test! {
name: unterminated_raw_string,
justfile: "
a b= ':
",
args: ("a"),
stdout: "",
stderr: "error: Unterminated string
|
2 | a b= ':
| ^
",
status: EXIT_FAILURE,
}
test! {
name: unterminated_string,
justfile: r#"
a b= ":
"#,
args: ("a"),
stdout: "",
stderr: r#"error: Unterminated string
|
2 | a b= ":
| ^
"#,
status: EXIT_FAILURE,
}
test! {
name: unterminated_backtick,
justfile: "
foo a=\t`echo blaaaaaah:
echo {{a}}",
stderr: r#"
error: Unterminated backtick
|
2 | foo a= `echo blaaaaaah:
| ^
"#,
status: EXIT_FAILURE,
}