diff --git a/GRAMMAR.md b/GRAMMAR.md index f5f8e65..f4bbadb 100644 --- a/GRAMMAR.md +++ b/GRAMMAR.md @@ -48,7 +48,7 @@ expression : STRING | BACKTICK | expression '+' expression -recipe : NAME argument* ':' dependencies? body? +recipe : '@'? NAME argument* ':' dependencies? body? argument : NAME | NAME '=' STRING diff --git a/README.md b/README.md index bb57806..2d92e5b 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,24 @@ Another recipe. `just` prints each command to standard error before running it, which is why `echo 'This is a recipe!'` was printed. Lines starting with `@` will not be printed which is why `echo 'Another recipe.'` was not printed. +A recipe name may be prefixed with '@' to invert the meaning of '@' before each line: + +```make +@quiet: + echo hello + echo goodbye + @# all done! +``` + +Now only the lines starting with '@' will be echoed: + +```sh +$ j quiet +hello +goodbye +# all done! +``` + Recipes stop running if a command fails. Here `cargo publish` will only run if `cargo test` succeeds: ```make diff --git a/src/integration.rs b/src/integration.rs index da8bc51..fce9800 100644 --- a/src/integration.rs +++ b/src/integration.rs @@ -1205,3 +1205,34 @@ foo: "echo abc\n", ); } + +#[test] +fn quiet_recipe() { + integration_test( + &[], + r#" +@quiet: + # a + # b + @echo c +"#, + 0, + "c\n", + "echo c\n", + ); +} + +#[test] +fn quiet_shebang_recipe() { + integration_test( + &[], + r#" +@quiet: + #!/bin/sh + echo hello +"#, + 0, + "hello\n", + "#!/bin/sh\necho hello\n", + ); +} diff --git a/src/lib.rs b/src/lib.rs index 89e9a37..6276b10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,7 @@ struct Recipe<'a> { dependency_tokens: Vec>, parameters: Vec>, shebang: bool, + quiet: bool, } #[derive(PartialEq, Debug)] @@ -294,10 +295,13 @@ impl<'a> Recipe<'a> { evaluated_lines.push(evaluator.evaluate_line(line, &argument_map)?); } - if options.dry_run { - for line in evaluated_lines { + if options.dry_run || self.quiet { + for line in &evaluated_lines { warn!("{}", line); } + } + + if options.dry_run { return Ok(()); } @@ -377,7 +381,7 @@ impl<'a> Recipe<'a> { if quiet_command { command = &command[1..]; } - if options.dry_run || !(quiet_command || options.quiet) { + if options.dry_run || !((quiet_command ^ self.quiet) || options.quiet) { warn!("{}", command); } if options.dry_run { @@ -1369,6 +1373,7 @@ impl<'a> Token<'a> { #[derive(Debug, PartialEq, Clone, Copy)] enum TokenKind { + At, Backtick, Colon, Comment, @@ -1382,8 +1387,8 @@ enum TokenKind { Line, Name, Plus, - StringToken, RawString, + StringToken, Text, } @@ -1403,6 +1408,7 @@ impl Display for TokenKind { Line => "command", Name => "name", Plus => "\"+\"", + At => "\"@\"", StringToken => "string", RawString => "raw string", Text => "command text", @@ -1424,6 +1430,7 @@ fn tokenize(text: &str) -> Result, CompileError> { lazy_static! { static ref BACKTICK: Regex = token(r"`[^`\n\r]*`" ); static ref COLON: Regex = token(r":" ); + static ref AT: Regex = token(r"@" ); static ref COMMENT: Regex = token(r"#([^!].*)?$" ); static ref EOF: Regex = token(r"(?-m)$" ); static ref EOL: Regex = token(r"\n|\r\n" ); @@ -1585,6 +1592,8 @@ fn tokenize(text: &str) -> Result, CompileError> { (captures.at(1).unwrap(), captures.at(2).unwrap(), Backtick) } else if let Some(captures) = COLON.captures(rest) { (captures.at(1).unwrap(), captures.at(2).unwrap(), Colon) + } else if let Some(captures) = AT.captures(rest) { + (captures.at(1).unwrap(), captures.at(2).unwrap(), At) } else if let Some(captures) = PLUS.captures(rest) { (captures.at(1).unwrap(), captures.at(2).unwrap(), Plus) } else if let Some(captures) = EQUALS.captures(rest) { @@ -1746,7 +1755,7 @@ impl<'a> Parser<'a> { }) } - fn recipe(&mut self, name: Token<'a>) -> Result<(), CompileError<'a>> { + fn recipe(&mut self, name: Token<'a>, quiet: bool) -> Result<(), CompileError<'a>> { if let Some(recipe) = self.recipes.get(name.lexeme) { return Err(name.error(ErrorKind::DuplicateRecipe { recipe: recipe.name, @@ -1871,6 +1880,7 @@ impl<'a> Parser<'a> { parameters: parameters, lines: lines, shebang: shebang, + quiet: quiet, }); Ok(()) @@ -1925,23 +1935,29 @@ impl<'a> Parser<'a> { Some(token) => match token.kind { Eof => break, Eol => continue, + At => if let Some(name) = self.accept(Name) { + self.recipe(name, true)?; + } else { + let unexpected = &self.tokens.next().unwrap(); + return Err(self.unexpected_token(unexpected, &[Name])); + }, Name => if token.lexeme == "export" { let next = self.tokens.next().unwrap(); if next.kind == Name && self.accepted(Equals) { self.assignment(next, true)?; } else { self.tokens.put_back(next); - self.recipe(token)?; + self.recipe(token, false)?; } } else if self.accepted(Equals) { self.assignment(token, false)?; } else { - self.recipe(token)?; + self.recipe(token, false)?; }, Comment => return Err(token.error(ErrorKind::InternalError { message: "found comment in token stream".to_string() })), - _ => return return Err(self.unexpected_token(&token, &[Name])), + _ => return return Err(self.unexpected_token(&token, &[Name, At])), }, None => return Err(CompileError { text: self.text, diff --git a/src/unit.rs b/src/unit.rs index 295bf22..4b01bca 100644 --- a/src/unit.rs +++ b/src/unit.rs @@ -35,22 +35,23 @@ fn tokenize_error(text: &str, expected: CompileError) { fn token_summary(tokens: &[Token]) -> String { tokens.iter().map(|t| { match t.kind { - super::TokenKind::Backtick => "`", - super::TokenKind::Colon => ":", - super::TokenKind::Comment{..} => "#", - super::TokenKind::Dedent => "<", - super::TokenKind::Eof => ".", - super::TokenKind::Eol => "$", - super::TokenKind::Equals => "=", - super::TokenKind::Indent{..} => ">", - super::TokenKind::InterpolationEnd => "}", - super::TokenKind::InterpolationStart => "{", - super::TokenKind::Line{..} => "^", - super::TokenKind::Name => "N", - super::TokenKind::Plus => "+", - super::TokenKind::StringToken => "\"", - super::TokenKind::RawString => "'", - super::TokenKind::Text => "_", + At => "@", + Backtick => "`", + Colon => ":", + Comment{..} => "#", + Dedent => "<", + Eof => ".", + Eol => "$", + Equals => "=", + Indent{..} => ">", + InterpolationEnd => "}", + InterpolationStart => "{", + Line{..} => "^", + Name => "N", + Plus => "+", + RawString => "'", + StringToken => "\"", + Text => "_", } }).collect::>().join("") } @@ -744,7 +745,7 @@ fn interpolation_outside_of_recipe() { line: 0, column: 0, width: Some(2), - kind: ErrorKind::UnexpectedToken{expected: vec![Name], found: InterpolationStart}, + kind: ErrorKind::UnexpectedToken{expected: vec![Name, At], found: InterpolationStart}, }); }