Clippy fixes, bump version 0.2.3, string escapes

This commit is contained in:
Casey Rodarmor 2016-10-28 00:06:36 -07:00
parent fa2fae5fb8
commit 0a16803247
6 changed files with 159 additions and 79 deletions

20
Cargo.lock generated
View File

@ -1,10 +1,10 @@
[root] [root]
name = "j" name = "j"
version = "0.2.2" version = "0.2.3"
dependencies = [ dependencies = [
"clap 2.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.16.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.1.77 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)",
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -28,7 +28,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "clap" name = "clap"
version = "2.15.0" version = "2.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -78,19 +78,19 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "0.1.77" version = "0.1.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", "thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
"utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.3.5" version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
@ -167,14 +167,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66" "checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66"
"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6" "checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6"
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" "checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
"checksum clap 2.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c3ad95014a5d1926493801463817b2e7e3ee64e051361a560f805c5320cd17b1" "checksum clap 2.16.2 (registry+https://github.com/rust-lang/crates.io-index)" = "08aac7b078ec0a58e1d4b43cfb11d47001f8eb7c6f6f2bda4f5eed43c82491f1"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "49247ec2a285bb3dcb23cbd9c35193c025e7251bfce77c1d5da97e6362dffe7f" "checksum lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "49247ec2a285bb3dcb23cbd9c35193c025e7251bfce77c1d5da97e6362dffe7f"
"checksum libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "408014cace30ee0f767b1c4517980646a573ec61a57957aeeabcac8ac0a02e8d" "checksum libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "408014cace30ee0f767b1c4517980646a573ec61a57957aeeabcac8ac0a02e8d"
"checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" "checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20"
"checksum rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2791d88c6defac799c3f20d74f094ca33b9332612d9aef9078519c82e4fe04a5" "checksum rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2791d88c6defac799c3f20d74f094ca33b9332612d9aef9078519c82e4fe04a5"
"checksum regex 0.1.77 (registry+https://github.com/rust-lang/crates.io-index)" = "64b03446c466d35b42f2a8b203c8e03ed8b91c0f17b56e1f84f7210a257aa665" "checksum regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f"
"checksum regex-syntax 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279401017ae31cf4e15344aa3f085d0e2e5c1e70067289ef906906fdbe92c8fd" "checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957"
"checksum strsim 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "50c069df92e4b01425a8bf3576d5d417943a6a7272fbabaf5bd80b1aaa76442e" "checksum strsim 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "50c069df92e4b01425a8bf3576d5d417943a6a7272fbabaf5bd80b1aaa76442e"
"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" "checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6"
"checksum term_size 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f7f5f3f71b0040cecc71af239414c23fd3c73570f5ff54cf50e03cef637f2a0" "checksum term_size 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f7f5f3f71b0040cecc71af239414c23fd3c73570f5ff54cf50e03cef637f2a0"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "j" name = "j"
version = "0.2.2" version = "0.2.3"
authors = ["Casey Rodarmor <casey@rodarmor.com>"] authors = ["Casey Rodarmor <casey@rodarmor.com>"]
license = "WTFPL/MIT/Apache-2.0" license = "WTFPL/MIT/Apache-2.0"
description = "a command runner" description = "a command runner"

27
notes
View File

@ -1,35 +1,13 @@
notes notes
----- -----
- implement string parsing - get weird of that weird extra printing
\n \r \t \\ \"
let mut evaluated = String::new();
let mut escape = false;
for c in contents.chars() {
if escape {
match c {
'n' => evaluated.push('\n'),
'r' => evaluated.push('\r'),
't' => evaluated.push('\t'),
'\\' => evaluated.push('\\'),
'"' => evaluated.push('"'),
other => panic!("bad escape sequence: {}", other),
}
} else if c == '\\' {
escape = true;
} else {
evaluated.push(c);
}
}
if escape {
}
evaluated
- integration testing - integration testing
. run app with command line options and test output . run app with command line options and test output
. exercise all features and all command line options . exercise all features and all command line options
. test that first recipe runs by default . test that first recipe runs by default
. test that a few error messages are correct
- underline problem token in error messages - underline problem token in error messages
@ -53,6 +31,7 @@ notes
- before release: - before release:
- rewrite grammar.txt - rewrite grammar.txt
- make it clear it's beta
- change name back to 'just', suggest j as alias - change name back to 'just', suggest j as alias
- change description to "a polyglot command runner"? - change description to "a polyglot command runner"?
- update readme - update readme

View File

@ -21,7 +21,7 @@ macro_rules! die {
pub fn app() { pub fn app() {
let matches = App::new("j") let matches = App::new("j")
.version("0.2.2") .version("0.2.3")
.author("Casey R. <casey@rodarmor.com>") .author("Casey R. <casey@rodarmor.com>")
.about("Just a command runner - https://github.com/casey/j") .about("Just a command runner - https://github.com/casey/j")
.arg(Arg::with_name("list") .arg(Arg::with_name("list")

View File

@ -26,6 +26,7 @@ macro_rules! warn {
let _ = writeln!(&mut std::io::stderr(), $($arg)*); let _ = writeln!(&mut std::io::stderr(), $($arg)*);
}}; }};
} }
macro_rules! die { macro_rules! die {
($($arg:tt)*) => {{ ($($arg:tt)*) => {{
extern crate std; extern crate std;
@ -72,7 +73,7 @@ enum Fragment<'a> {
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
enum Expression<'a> { enum Expression<'a> {
Variable{name: &'a str, token: Token<'a>}, Variable{name: &'a str, token: Token<'a>},
String{contents: &'a str}, String{raw: &'a str, cooked: String},
Concatination{lhs: Box<Expression<'a>>, rhs: Box<Expression<'a>>}, Concatination{lhs: Box<Expression<'a>>, rhs: Box<Expression<'a>>},
} }
@ -93,9 +94,8 @@ impl<'a> Iterator for Variables<'a> {
fn next(&mut self) -> Option<&'a Token<'a>> { fn next(&mut self) -> Option<&'a Token<'a>> {
match self.stack.pop() { match self.stack.pop() {
None => None, None | Some(&Expression::String{..}) => None,
Some(&Expression::Variable{ref token,..}) => Some(token), Some(&Expression::Variable{ref token,..}) => Some(token),
Some(&Expression::String{..}) => None,
Some(&Expression::Concatination{ref lhs, ref rhs}) => { Some(&Expression::Concatination{ref lhs, ref rhs}) => {
self.stack.push(lhs); self.stack.push(lhs);
self.stack.push(rhs); self.stack.push(rhs);
@ -109,7 +109,7 @@ impl<'a> Display for Expression<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self { match *self {
Expression::Variable {name, .. } => try!(write!(f, "{}", name)), Expression::Variable {name, .. } => try!(write!(f, "{}", name)),
Expression::String {contents } => try!(write!(f, "\"{}\"", contents)), Expression::String {raw, .. } => try!(write!(f, "\"{}\"", raw)),
Expression::Concatination{ref lhs, ref rhs} => try!(write!(f, "{} + {}", lhs, rhs)), Expression::Concatination{ref lhs, ref rhs} => try!(write!(f, "{} + {}", lhs, rhs)),
} }
Ok(()) Ok(())
@ -155,7 +155,7 @@ impl<'a> Recipe<'a> {
text += "\n" text += "\n"
} }
for line in &self.evaluated_lines[1..] { for line in &self.evaluated_lines[1..] {
text += &line; text += line;
text += "\n"; text += "\n";
} }
try!( try!(
@ -238,10 +238,10 @@ impl<'a> Display for Recipe<'a> {
if j == 0 { if j == 0 {
try!(write!(f, " ")); try!(write!(f, " "));
} }
match piece { match *piece {
&Fragment::Text{ref text} => try!(write!(f, "{}", text.lexeme)), Fragment::Text{ref text} => try!(write!(f, "{}", text.lexeme)),
&Fragment::Expression{ref expression, value: None} => try!(write!(f, "{}{} # ? {}", "{{", expression, "}}")), Fragment::Expression{ref expression, value: None} => try!(write!(f, "{}{} # ? {}", "{{", expression, "}}")),
&Fragment::Expression{ref expression, value: Some(ref string)} => try!(write!(f, "{}{} # \"{}\"{}", "{{", expression, string, "}}")), Fragment::Expression{ref expression, value: Some(ref string)} => try!(write!(f, "{}{} # \"{}\"{}", "{{", expression, string, "}}")),
} }
} }
if i + 1 < self.lines.len() { if i + 1 < self.lines.len() {
@ -325,12 +325,12 @@ fn evaluate<'a>(
} }
for recipe in recipes.values_mut() { for recipe in recipes.values_mut() {
for mut fragments in recipe.lines.iter_mut() { for fragments in &mut recipe.lines {
let mut line = String::new(); let mut line = String::new();
for mut fragment in fragments.iter_mut() { for mut fragment in fragments.iter_mut() {
match fragment { match *fragment {
&mut Fragment::Text{ref text} => line += text.lexeme, Fragment::Text{ref text} => line += text.lexeme,
&mut Fragment::Expression{ref expression, ref mut value} => { Fragment::Expression{ref expression, ref mut value} => {
let evaluated = &try!(evaluator.evaluate_expression(&expression)); let evaluated = &try!(evaluator.evaluate_expression(&expression));
*value = Some(evaluated.clone()); *value = Some(evaluated.clone());
line += evaluated; line += evaluated;
@ -392,8 +392,8 @@ impl<'a, 'b> Evaluator<'a, 'b> {
self.evaluated.get(name).unwrap().clone() self.evaluated.get(name).unwrap().clone()
} }
} }
Expression::String{contents} => { Expression::String{ref cooked, ..} => {
contents.to_string() cooked.clone()
} }
Expression::Concatination{ref lhs, ref rhs} => { Expression::Concatination{ref lhs, ref rhs} => {
try!(self.evaluate_expression(lhs)) try!(self.evaluate_expression(lhs))
@ -416,23 +416,25 @@ struct Error<'a> {
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum ErrorKind<'a> { enum ErrorKind<'a> {
ArgumentShadowsVariable{argument: &'a str},
BadName{name: &'a str}, BadName{name: &'a str},
CircularRecipeDependency{recipe: &'a str, circle: Vec<&'a str>}, CircularRecipeDependency{recipe: &'a str, circle: Vec<&'a str>},
CircularVariableDependency{variable: &'a str, circle: Vec<&'a str>}, CircularVariableDependency{variable: &'a str, circle: Vec<&'a str>},
DuplicateDependency{recipe: &'a str, dependency: &'a str},
DuplicateArgument{recipe: &'a str, argument: &'a str}, DuplicateArgument{recipe: &'a str, argument: &'a str},
DuplicateDependency{recipe: &'a str, dependency: &'a str},
DuplicateRecipe{recipe: &'a str, first: usize}, DuplicateRecipe{recipe: &'a str, first: usize},
DuplicateVariable{variable: &'a str}, DuplicateVariable{variable: &'a str},
ArgumentShadowsVariable{argument: &'a str},
MixedLeadingWhitespace{whitespace: &'a str},
ExtraLeadingWhitespace, ExtraLeadingWhitespace,
InconsistentLeadingWhitespace{expected: &'a str, found: &'a str}, InconsistentLeadingWhitespace{expected: &'a str, found: &'a str},
OuterShebang,
UnknownDependency{recipe: &'a str, unknown: &'a str},
UnknownVariable{variable: &'a str},
UnknownStartOfToken,
UnexpectedToken{expected: Vec<TokenKind>, found: TokenKind},
InternalError{message: String}, InternalError{message: String},
InvalidEscapeSequence{character: char},
MixedLeadingWhitespace{whitespace: &'a str},
OuterShebang,
UnexpectedToken{expected: Vec<TokenKind>, found: TokenKind},
UnknownDependency{recipe: &'a str, unknown: &'a str},
UnknownStartOfToken,
UnknownVariable{variable: &'a str},
UnterminatedString,
} }
fn show_whitespace(text: &str) -> String { fn show_whitespace(text: &str) -> String {
@ -485,6 +487,9 @@ impl<'a> Display for Error<'a> {
try!(write!(f, "assignment to {} has circular dependency: {}", variable, circle.join(" -> "))); try!(write!(f, "assignment to {} has circular dependency: {}", variable, circle.join(" -> ")));
return Ok(()); return Ok(());
} }
ErrorKind::InvalidEscapeSequence{character} => {
try!(writeln!(f, "\\{}", character.escape_default().collect::<String>()));
}
ErrorKind::DuplicateArgument{recipe, argument} => { ErrorKind::DuplicateArgument{recipe, argument} => {
try!(writeln!(f, "recipe {} has duplicate argument: {}", recipe, argument)); try!(writeln!(f, "recipe {} has duplicate argument: {}", recipe, argument));
} }
@ -532,6 +537,9 @@ impl<'a> Display for Error<'a> {
ErrorKind::UnknownStartOfToken => { ErrorKind::UnknownStartOfToken => {
try!(writeln!(f, "unknown start of token:")); try!(writeln!(f, "unknown start of token:"));
} }
ErrorKind::UnterminatedString => {
try!(writeln!(f, "unterminated string"));
}
ErrorKind::InternalError{ref message} => { ErrorKind::InternalError{ref message} => {
try!(writeln!(f, "internal error, this may indicate a bug in j: {}\n consider filing an issue: https://github.com/casey/j/issues/new", message)); try!(writeln!(f, "internal error, this may indicate a bug in j: {}\n consider filing an issue: https://github.com/casey/j/issues/new", message));
} }
@ -760,7 +768,7 @@ fn token(pattern: &str) -> Regex {
re(&s) re(&s)
} }
fn tokenize<'a>(text: &'a str) -> Result<Vec<Token>, Error> { fn tokenize(text: &str) -> Result<Vec<Token>, Error> {
lazy_static! { lazy_static! {
static ref EOF: Regex = token(r"(?-m)$" ); static ref EOF: Regex = token(r"(?-m)$" );
static ref NAME: Regex = token(r"([a-zA-Z0-9_-]+)" ); static ref NAME: Regex = token(r"([a-zA-Z0-9_-]+)" );
@ -768,7 +776,7 @@ fn tokenize<'a>(text: &'a str) -> Result<Vec<Token>, Error> {
static ref EQUALS: Regex = token(r"=" ); static ref EQUALS: Regex = token(r"=" );
static ref PLUS: Regex = token(r"[+]" ); static ref PLUS: Regex = token(r"[+]" );
static ref COMMENT: Regex = token(r"#([^!].*)?$" ); static ref COMMENT: Regex = token(r"#([^!].*)?$" );
static ref STRING: Regex = token("\"[^\"]*\"" ); static ref STRING: Regex = token("\"" );
static ref EOL: Regex = token(r"\n|\r\n" ); static ref EOL: Regex = token(r"\n|\r\n" );
static ref INTERPOLATION_END: Regex = token(r"[}][}]" ); static ref INTERPOLATION_END: Regex = token(r"[}][}]" );
static ref INTERPOLATION_START_TOKEN: Regex = token(r"[{][{]" ); static ref INTERPOLATION_START_TOKEN: Regex = token(r"[{][{]" );
@ -864,18 +872,16 @@ fn tokenize<'a>(text: &'a str) -> Result<Vec<Token>, Error> {
} }
// insert a dedent if we're indented and we hit the end of the file // insert a dedent if we're indented and we hit the end of the file
if &State::Start != state.last().unwrap() { if &State::Start != state.last().unwrap() && EOF.is_match(rest) {
if EOF.is_match(rest) { tokens.push(Token {
tokens.push(Token { index: index,
index: index, line: line,
line: line, column: column,
column: column, text: text,
text: text, prefix: "",
prefix: "", lexeme: "",
lexeme: "", kind: Dedent,
kind: Dedent, });
});
}
} }
let (prefix, lexeme, kind) = let (prefix, lexeme, kind) =
@ -888,7 +894,7 @@ fn tokenize<'a>(text: &'a str) -> Result<Vec<Token>, Error> {
(&line[0..indent.len()], "", Line) (&line[0..indent.len()], "", Line)
} else if let Some(captures) = EOF.captures(rest) { } else if let Some(captures) = EOF.captures(rest) {
(captures.at(1).unwrap(), captures.at(2).unwrap(), Eof) (captures.at(1).unwrap(), captures.at(2).unwrap(), Eof)
} else if let &State::Text = state.last().unwrap() { } else if let State::Text = *state.last().unwrap() {
if let Some(captures) = INTERPOLATION_START.captures(rest) { if let Some(captures) = INTERPOLATION_START.captures(rest) {
state.push(State::Interpolation); state.push(State::Interpolation);
("", captures.at(0).unwrap(), InterpolationStart) ("", captures.at(0).unwrap(), InterpolationStart)
@ -927,7 +933,34 @@ fn tokenize<'a>(text: &'a str) -> Result<Vec<Token>, Error> {
} else if let Some(captures) = COMMENT.captures(rest) { } else if let Some(captures) = COMMENT.captures(rest) {
(captures.at(1).unwrap(), captures.at(2).unwrap(), Comment) (captures.at(1).unwrap(), captures.at(2).unwrap(), Comment)
} else if let Some(captures) = STRING.captures(rest) { } else if let Some(captures) = STRING.captures(rest) {
(captures.at(1).unwrap(), captures.at(2).unwrap(), StringToken) let prefix = captures.at(1).unwrap();
let contents = &rest[prefix.len()+1..];
if contents.is_empty() {
return error!(ErrorKind::UnterminatedString);
}
// die on \n or \r
// stop on unescaped "
let mut len = 0;
let mut escape = false;
for c in contents.chars() {
if c == '\n' || c == '\r' {
return error!(ErrorKind::UnterminatedString);
} else if !escape && c == '"' {
break;
} else if !escape && c == '\\' {
escape = true;
} else if escape {
escape = false;
}
len += c.len_utf8();
}
let start = prefix.len();
let content_end = start + len + 1;
if escape || content_end >= rest.len() {
return error!(ErrorKind::UnterminatedString);
}
println!("{} {} {:?}", start, content_end, contents.chars().collect::<Vec<_>>());
(prefix, &rest[start..content_end + 1], StringToken)
} else if rest.starts_with("#!") { } else if rest.starts_with("#!") {
return error!(ErrorKind::OuterShebang) return error!(ErrorKind::OuterShebang)
} else { } else {
@ -1105,7 +1138,7 @@ impl<'a> Parser<'a> {
if token.lexeme.starts_with("#!") { if token.lexeme.starts_with("#!") {
shebang = true; shebang = true;
} }
} else if !shebang && token.lexeme.starts_with(" ") || token.lexeme.starts_with("\t") { } else if !shebang && token.lexeme.starts_with(' ') || token.lexeme.starts_with('\t') {
return Err(token.error(ErrorKind::ExtraLeadingWhitespace)); return Err(token.error(ErrorKind::ExtraLeadingWhitespace));
} }
} }
@ -1143,8 +1176,34 @@ impl<'a> Parser<'a> {
let first = self.tokens.next().unwrap(); let first = self.tokens.next().unwrap();
let lhs = match first.kind { let lhs = match first.kind {
Name => Expression::Variable{name: first.lexeme, token: first}, Name => Expression::Variable{name: first.lexeme, token: first},
StringToken => Expression::String{contents: &first.lexeme[1..first.lexeme.len() - 1]}, StringToken => {
_ => return Err(self.unexpected_token(&first, &[Name, StringToken])), let raw = &first.lexeme[1..first.lexeme.len() - 1];
let mut cooked = String::new();
let mut escape = false;
for c in raw.chars() {
if escape {
match c {
'n' => cooked.push('\n'),
'r' => cooked.push('\r'),
't' => cooked.push('\t'),
'\\' => cooked.push('\\'),
'"' => cooked.push('"'),
other => return Err(first.error(ErrorKind::InvalidEscapeSequence {
character: other,
})),
}
escape = false;
continue;
}
if c == '\\' {
escape = true;
continue;
}
cooked.push(c);
}
Expression::String{raw: raw, cooked: cooked}
}
_ => return Err(self.unexpected_token(&first, &[Name, StringToken])),
}; };
if self.accepted(Plus) { if self.accepted(Plus) {
@ -1227,7 +1286,7 @@ impl<'a> Parser<'a> {
for line in &recipe.lines { for line in &recipe.lines {
for piece in line { for piece in line {
if let &Fragment::Expression{ref expression, ..} = piece { if let Fragment::Expression{ref expression, ..} = *piece {
for variable in expression.variables() { for variable in expression.variables() {
let name = variable.lexeme; let name = variable.lexeme;
if !(assignments.contains_key(&name) || recipe.arguments.contains(&name)) { if !(assignments.contains_key(&name) || recipe.arguments.contains(&name)) {

View File

@ -395,6 +395,48 @@ fn duplicate_variable() {
}); });
} }
#[test]
fn unterminated_string() {
let text = r#"a = ""#;
parse_error(text, Error {
text: text,
index: 3,
line: 0,
column: 3,
width: None,
kind: ErrorKind::UnterminatedString,
});
}
#[test]
fn unterminated_string_with_escapes() {
let text = r#"a = "\n\t\r\"\\"#;
parse_error(text, Error {
text: text,
index: 3,
line: 0,
column: 3,
width: None,
kind: ErrorKind::UnterminatedString,
});
}
#[test]
fn string_quote_escape() {
parse_summary(
r#"a = "hello\"""#,
r#"a = "hello\"" # "hello"""#
);
}
#[test]
fn string_escapes() {
parse_summary(
r#"a = "\n\t\r\"\\""#,
concat!(r#"a = "\n\t\r\"\\" "#, "# \"\n\t\r\"\\\"")
);
}
#[test] #[test]
fn self_recipe_dependency() { fn self_recipe_dependency() {
let text = "a: a"; let text = "a: a";