From 4c6368ecfc058faf80e9666bc4d5917a15c0030a Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 31 Oct 2024 16:56:47 -0700 Subject: [PATCH 01/16] Add advice on printing complex strings (#2446) --- README.md | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 994ec488..ca596854 100644 --- a/README.md +++ b/README.md @@ -3583,9 +3583,9 @@ The following command will create two files, `some` and `argument.txt`: $ just foo "some argument.txt" ``` -The users shell will parse `"some argument.txt"` as a single argument, but when -`just` replaces `touch {{argument}}` with `touch some argument.txt`, the quotes -are not preserved, and `touch` will receive two arguments. +The user's shell will parse `"some argument.txt"` as a single argument, but +when `just` replaces `touch {{argument}}` with `touch some argument.txt`, the +quotes are not preserved, and `touch` will receive two arguments. There are a few ways to avoid this: quoting, positional arguments, and exported arguments. @@ -3910,6 +3910,38 @@ fetch: Given the above `justfile`, after running `just fetch`, the recipes in `foo.just` will be available. +### Printing Complex Strings + +`echo` can be used to print strings, but because it processes escape sequences, +like `\n`, and different implementations of `echo` recognize different escape +sequences, using `printf` is often a better choice. + +`printf` takes a C-style format string and any number of arguments, which are +interpolated into the format string. + +This can be combined with indented, triple quoted strings to emulate shell +heredocs. + +Substitution complex strings into recipe bodies with `{…}` can also lead to +trouble as it may be split by the shell into multiple arguments depending on +the presence of whitespace and quotes. Exporting complex strings as environment +variables and referring to them with `"$NAME"`, note the double quotes, can +also help. + +Putting all this together, to print a string verbatim to standard output, with +all its various escape sequences and quotes undisturbed: + +```just +export FOO := ''' + a complicated string with + some dis\tur\bi\ng escape sequences + and "quotes" of 'different' kinds +''' + +bar: + printf %s "$FOO" +``` + ### Alternatives and Prior Art There is no shortage of command runners! Some more or less similar alternatives From 7030e9cac6f1a13bd4d366fab66f0341edeb3002 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 31 Oct 2024 17:54:46 -0700 Subject: [PATCH 02/16] Add `&&` and `||` operators (#2444) --- GRAMMAR.md | 8 ++- README.md | 43 ++++++++++-- src/analyzer.rs | 5 +- src/assignment_resolver.rs | 130 +++++++------------------------------ src/ast.rs | 1 + src/evaluator.rs | 65 +++++++++++-------- src/expression.rs | 54 +++++++++++---- src/lexer.rs | 2 + src/node.rs | 43 ++++++------ src/parser.rs | 91 ++++++++++++++++++-------- src/summary.rs | 28 ++++++-- src/token_kind.rs | 2 + src/unstable_feature.rs | 5 ++ src/variables.rs | 46 +++++++------ tests/assert_success.rs | 1 + tests/conditional.rs | 2 +- tests/ignore_comments.rs | 2 +- tests/lib.rs | 1 + tests/logical_operators.rs | 83 +++++++++++++++++++++++ tests/shell_expansion.rs | 2 +- 20 files changed, 378 insertions(+), 236 deletions(-) create mode 100644 tests/logical_operators.rs diff --git a/GRAMMAR.md b/GRAMMAR.md index e5847f50..00721f15 100644 --- a/GRAMMAR.md +++ b/GRAMMAR.md @@ -90,7 +90,13 @@ import : 'import' '?'? string? eol module : 'mod' '?'? NAME string? eol -expression : 'if' condition '{' expression '}' 'else' '{' expression '}' +expression : disjunct || expression + | disjunct + +disjunct : conjunct && disjunct + | conjunct + +conjunct : 'if' condition '{' expression '}' 'else' '{' expression '}' | 'assert' '(' condition ',' expression ')' | '/' expression | value '/' expression diff --git a/README.md b/README.md index ca596854..bccf9a13 100644 --- a/README.md +++ b/README.md @@ -1290,9 +1290,11 @@ Available recipes: test ``` -### Variables and Substitution +### Expressions and Substitutions -Variables, strings, concatenation, path joining, substitution using `{{…}}`, and function calls are supported: +Various operators and function calls are supported in expressions, which may be +used in assignments, default recipe arguments, and inside recipe body `{{…}}` +substitutions. ```just tmpdir := `mktemp -d` @@ -1310,6 +1312,39 @@ publish: rm -rf {{tarball}} {{tardir}} ``` +#### Concatenation + +The `+` operator returns the left-hand argument concatenated with the +right-hand argument: + +```just +foobar := 'foo' + 'bar' +``` + +#### Logical Operators + +The logical operators `&&` and `||` can be used to coalesce string +valuesmaster, similar to Python's `and` and `or`. These operators +consider the empty string `''` to be false, and all other strings to be true. + +These operators are currently unstable. + +The `&&` operator returns the empty string if the left-hand argument is the +empty string, otherwise it returns the right-hand argument: + +```mf +foo := '' && 'goodbye' # '' +bar := 'hello' && 'goodbye' # 'goodbye' +``` + +The `||` operator returns the left-hand argument if it is non-empty, otherwise +it returns the right-hand argument: + +```mf +foo := '' || 'goodbye' # 'goodbye' +bar := 'hello' || 'goodbye' # 'hello' +``` + #### Joining Paths The `/` operator can be used to join two strings with a slash: @@ -2367,8 +2402,8 @@ Testing server:unit… ./test --tests unit server ``` -Default values may be arbitrary expressions, but concatenations or path joins -must be parenthesized: +Default values may be arbitrary expressions, but expressions containing the +`+`, `&&`, `||`, or `/` operators must be parenthesized: ```just arch := "wasm" diff --git a/src/analyzer.rs b/src/analyzer.rs index 9a26ef04..0929294c 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -36,12 +36,15 @@ impl<'run, 'src> Analyzer<'run, 'src> { ) -> CompileResult<'src, Justfile<'src>> { let mut definitions = HashMap::new(); let mut imports = HashSet::new(); + let mut unstable_features = BTreeSet::new(); let mut stack = Vec::new(); let ast = asts.get(root).unwrap(); stack.push(ast); while let Some(ast) = stack.pop() { + unstable_features.extend(&ast.unstable_features); + for item in &ast.items { match item { Item::Alias(alias) => { @@ -166,8 +169,6 @@ impl<'run, 'src> Analyzer<'run, 'src> { aliases.insert(Self::resolve_alias(&recipes, alias)?); } - let mut unstable_features = BTreeSet::new(); - for recipe in recipes.values() { for attribute in &recipe.attributes { if let Attribute::Script(_) = attribute { diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs index 53863fc9..511464d4 100644 --- a/src/assignment_resolver.rs +++ b/src/assignment_resolver.rs @@ -31,7 +31,29 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { self.stack.push(name); if let Some(assignment) = self.assignments.get(name) { - self.resolve_expression(&assignment.value)?; + for variable in assignment.value.variables() { + let name = variable.lexeme(); + + if self.evaluated.contains(name) || constants().contains_key(name) { + continue; + } + + if self.stack.contains(&name) { + self.stack.push(name); + return Err( + self.assignments[name] + .name + .error(CircularVariableDependency { + variable: name, + circle: self.stack.clone(), + }), + ); + } else if self.assignments.contains_key(name) { + self.resolve_assignment(name)?; + } else { + return Err(variable.error(UndefinedVariable { variable: name })); + } + } self.evaluated.insert(name); } else { let message = format!("attempted to resolve unknown assignment `{name}`"); @@ -51,112 +73,6 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { Ok(()) } - - fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src> { - match expression { - Expression::Assert { - condition: Condition { - lhs, - rhs, - operator: _, - }, - error, - } => { - self.resolve_expression(lhs)?; - self.resolve_expression(rhs)?; - self.resolve_expression(error) - } - Expression::Call { thunk } => match thunk { - Thunk::Nullary { .. } => Ok(()), - Thunk::Unary { arg, .. } => self.resolve_expression(arg), - Thunk::UnaryOpt { args: (a, b), .. } => { - self.resolve_expression(a)?; - if let Some(b) = b.as_ref() { - self.resolve_expression(b)?; - } - Ok(()) - } - Thunk::UnaryPlus { - args: (a, rest), .. - } => { - self.resolve_expression(a)?; - for arg in rest { - self.resolve_expression(arg)?; - } - Ok(()) - } - Thunk::Binary { args: [a, b], .. } => { - self.resolve_expression(a)?; - self.resolve_expression(b) - } - Thunk::BinaryPlus { - args: ([a, b], rest), - .. - } => { - self.resolve_expression(a)?; - self.resolve_expression(b)?; - for arg in rest { - self.resolve_expression(arg)?; - } - Ok(()) - } - Thunk::Ternary { - args: [a, b, c], .. - } => { - self.resolve_expression(a)?; - self.resolve_expression(b)?; - self.resolve_expression(c) - } - }, - Expression::Concatenation { lhs, rhs } => { - self.resolve_expression(lhs)?; - self.resolve_expression(rhs) - } - Expression::Conditional { - condition: Condition { - lhs, - rhs, - operator: _, - }, - then, - otherwise, - .. - } => { - self.resolve_expression(lhs)?; - self.resolve_expression(rhs)?; - self.resolve_expression(then)?; - self.resolve_expression(otherwise) - } - Expression::Group { contents } => self.resolve_expression(contents), - Expression::Join { lhs, rhs } => { - if let Some(lhs) = lhs { - self.resolve_expression(lhs)?; - } - self.resolve_expression(rhs) - } - Expression::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()), - Expression::Variable { name } => { - let variable = name.lexeme(); - if self.evaluated.contains(variable) || constants().contains_key(variable) { - Ok(()) - } else if self.stack.contains(&variable) { - self.stack.push(variable); - Err( - self.assignments[variable] - .name - .error(CircularVariableDependency { - variable, - circle: self.stack.clone(), - }), - ) - } else if self.assignments.contains_key(variable) { - self.resolve_assignment(variable) - } else { - Err(name.token.error(UndefinedVariable { variable })) - } - } - } - } } #[cfg(test)] diff --git a/src/ast.rs b/src/ast.rs index f9dd10c9..1ad7a8aa 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -6,6 +6,7 @@ use super::*; #[derive(Debug, Clone)] pub(crate) struct Ast<'src> { pub(crate) items: Vec>, + pub(crate) unstable_features: BTreeSet, pub(crate) warnings: Vec, pub(crate) working_directory: PathBuf, } diff --git a/src/evaluator.rs b/src/evaluator.rs index 4ed00036..d48cece8 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -84,24 +84,31 @@ impl<'src, 'run> Evaluator<'src, 'run> { expression: &Expression<'src>, ) -> RunResult<'src, String> { match expression { - Expression::Variable { name, .. } => { - let variable = name.lexeme(); - if let Some(value) = self.scope.value(variable) { - Ok(value.to_owned()) - } else if let Some(assignment) = self - .assignments - .and_then(|assignments| assignments.get(variable)) - { - Ok(self.evaluate_assignment(assignment)?.to_owned()) + Expression::And { lhs, rhs } => { + let lhs = self.evaluate_expression(lhs)?; + if lhs.is_empty() { + return Ok(String::new()); + } + self.evaluate_expression(rhs) + } + Expression::Assert { condition, error } => { + if self.evaluate_condition(condition)? { + Ok(String::new()) } else { - Err(Error::Internal { - message: format!("attempted to evaluate undefined variable `{variable}`"), + Err(Error::Assert { + message: self.evaluate_expression(error)?, }) } } + Expression::Backtick { contents, token } => { + if self.context.config.dry_run { + Ok(format!("`{contents}`")) + } else { + Ok(self.run_backtick(contents, token)?) + } + } Expression::Call { thunk } => { use Thunk::*; - let result = match thunk { Nullary { function, .. } => function(function::Context::new(self, thunk.name())), Unary { function, arg, .. } => { @@ -118,7 +125,6 @@ impl<'src, 'run> Evaluator<'src, 'run> { Some(b) => Some(self.evaluate_expression(b)?), None => None, }; - function(function::Context::new(self, thunk.name()), &a, b.as_deref()) } UnaryPlus { @@ -175,20 +181,11 @@ impl<'src, 'run> Evaluator<'src, 'run> { function(function::Context::new(self, thunk.name()), &a, &b, &c) } }; - result.map_err(|message| Error::FunctionCall { function: thunk.name(), message, }) } - Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()), - Expression::Backtick { contents, token } => { - if self.context.config.dry_run { - Ok(format!("`{contents}`")) - } else { - Ok(self.run_backtick(contents, token)?) - } - } Expression::Concatenation { lhs, rhs } => { Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(rhs)?) } @@ -209,12 +206,26 @@ impl<'src, 'run> Evaluator<'src, 'run> { lhs: Some(lhs), rhs, } => Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?), - Expression::Assert { condition, error } => { - if self.evaluate_condition(condition)? { - Ok(String::new()) + Expression::Or { lhs, rhs } => { + let lhs = self.evaluate_expression(lhs)?; + if !lhs.is_empty() { + return Ok(lhs); + } + self.evaluate_expression(rhs) + } + Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()), + Expression::Variable { name, .. } => { + let variable = name.lexeme(); + if let Some(value) = self.scope.value(variable) { + Ok(value.to_owned()) + } else if let Some(assignment) = self + .assignments + .and_then(|assignments| assignments.get(variable)) + { + Ok(self.evaluate_assignment(assignment)?.to_owned()) } else { - Err(Error::Assert { - message: self.evaluate_expression(error)?, + Err(Error::Internal { + message: format!("attempted to evaluate undefined variable `{variable}`"), }) } } diff --git a/src/expression.rs b/src/expression.rs index c1e4b5e0..3d5c3392 100644 --- a/src/expression.rs +++ b/src/expression.rs @@ -8,6 +8,11 @@ use super::*; /// The parser parses both values and expressions into `Expression`s. #[derive(PartialEq, Debug, Clone)] pub(crate) enum Expression<'src> { + /// `lhs && rhs` + And { + lhs: Box>, + rhs: Box>, + }, /// `assert(condition, error)` Assert { condition: Condition<'src>, @@ -38,6 +43,11 @@ pub(crate) enum Expression<'src> { lhs: Option>>, rhs: Box>, }, + /// `lhs || rhs` + Or { + lhs: Box>, + rhs: Box>, + }, /// `"string_literal"` or `'string_literal'` StringLiteral { string_literal: StringLiteral<'src> }, /// `variable` @@ -53,23 +63,25 @@ impl<'src> Expression<'src> { impl<'src> Display for Expression<'src> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { + Self::And { lhs, rhs } => write!(f, "{lhs} && {rhs}"), Self::Assert { condition, error } => write!(f, "assert({condition}, {error})"), Self::Backtick { token, .. } => write!(f, "{}", token.lexeme()), - Self::Join { lhs: None, rhs } => write!(f, "/ {rhs}"), - Self::Join { - lhs: Some(lhs), - rhs, - } => write!(f, "{lhs} / {rhs}"), + Self::Call { thunk } => write!(f, "{thunk}"), Self::Concatenation { lhs, rhs } => write!(f, "{lhs} + {rhs}"), Self::Conditional { condition, then, otherwise, } => write!(f, "if {condition} {{ {then} }} else {{ {otherwise} }}"), + Self::Group { contents } => write!(f, "({contents})"), + Self::Join { lhs: None, rhs } => write!(f, "/ {rhs}"), + Self::Join { + lhs: Some(lhs), + rhs, + } => write!(f, "{lhs} / {rhs}"), + Self::Or { lhs, rhs } => write!(f, "{lhs} || {rhs}"), Self::StringLiteral { string_literal } => write!(f, "{string_literal}"), Self::Variable { name } => write!(f, "{}", name.lexeme()), - Self::Call { thunk } => write!(f, "{thunk}"), - Self::Group { contents } => write!(f, "({contents})"), } } } @@ -80,6 +92,13 @@ impl<'src> Serialize for Expression<'src> { S: Serializer, { match self { + Self::And { lhs, rhs } => { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element("and")?; + seq.serialize_element(lhs)?; + seq.serialize_element(rhs)?; + seq.end() + } Self::Assert { condition, error } => { let mut seq: ::SerializeSeq = serializer.serialize_seq(None)?; seq.serialize_element("assert")?; @@ -101,13 +120,6 @@ impl<'src> Serialize for Expression<'src> { seq.serialize_element(rhs)?; seq.end() } - Self::Join { lhs, rhs } => { - let mut seq = serializer.serialize_seq(None)?; - seq.serialize_element("join")?; - seq.serialize_element(lhs)?; - seq.serialize_element(rhs)?; - seq.end() - } Self::Conditional { condition, then, @@ -121,6 +133,20 @@ impl<'src> Serialize for Expression<'src> { seq.end() } Self::Group { contents } => contents.serialize(serializer), + Self::Join { lhs, rhs } => { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element("join")?; + seq.serialize_element(lhs)?; + seq.serialize_element(rhs)?; + seq.end() + } + Self::Or { lhs, rhs } => { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element("or")?; + seq.serialize_element(lhs)?; + seq.serialize_element(rhs)?; + seq.end() + } Self::StringLiteral { string_literal } => string_literal.serialize(serializer), Self::Variable { name } => { let mut seq = serializer.serialize_seq(None)?; diff --git a/src/lexer.rs b/src/lexer.rs index 4d28a446..2c56db9d 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -496,6 +496,7 @@ impl<'src> Lexer<'src> { ']' => self.lex_delimiter(BracketR), '`' | '"' | '\'' => self.lex_string(), '{' => self.lex_delimiter(BraceL), + '|' => self.lex_digraph('|', '|', BarBar), '}' => self.lex_delimiter(BraceR), _ if Self::is_identifier_start(start) => self.lex_identifier(), _ => { @@ -948,6 +949,7 @@ mod tests { Asterisk => "*", At => "@", BangEquals => "!=", + BarBar => "||", BraceL => "{", BraceR => "}", BracketL => "[", diff --git a/src/node.rs b/src/node.rs index 3ccf862d..f8788c4d 100644 --- a/src/node.rs +++ b/src/node.rs @@ -88,6 +88,7 @@ impl<'src> Node<'src> for Assignment<'src> { impl<'src> Node<'src> for Expression<'src> { fn tree(&self) -> Tree<'src> { match self { + Self::And { lhs, rhs } => Tree::atom("&&").push(lhs.tree()).push(rhs.tree()), Self::Assert { condition: Condition { lhs, rhs, operator }, error, @@ -96,25 +97,10 @@ impl<'src> Node<'src> for Expression<'src> { .push(operator.to_string()) .push(rhs.tree()) .push(error.tree()), - Self::Concatenation { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()), - Self::Conditional { - condition: Condition { lhs, rhs, operator }, - then, - otherwise, - } => { - let mut tree = Tree::atom(Keyword::If.lexeme()); - tree.push_mut(lhs.tree()); - tree.push_mut(operator.to_string()); - tree.push_mut(rhs.tree()); - tree.push_mut(then.tree()); - tree.push_mut(otherwise.tree()); - tree - } + Self::Backtick { contents, .. } => Tree::atom("backtick").push(Tree::string(contents)), Self::Call { thunk } => { use Thunk::*; - let mut tree = Tree::atom("call"); - match thunk { Nullary { name, .. } => tree.push_mut(name.lexeme()), Unary { name, arg, .. } => { @@ -171,20 +157,33 @@ impl<'src> Node<'src> for Expression<'src> { tree.push_mut(c.tree()); } } - tree } - Self::Variable { name } => Tree::atom(name.lexeme()), - Self::StringLiteral { - string_literal: StringLiteral { cooked, .. }, - } => Tree::string(cooked), - Self::Backtick { contents, .. } => Tree::atom("backtick").push(Tree::string(contents)), + Self::Concatenation { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()), + Self::Conditional { + condition: Condition { lhs, rhs, operator }, + then, + otherwise, + } => { + let mut tree = Tree::atom(Keyword::If.lexeme()); + tree.push_mut(lhs.tree()); + tree.push_mut(operator.to_string()); + tree.push_mut(rhs.tree()); + tree.push_mut(then.tree()); + tree.push_mut(otherwise.tree()); + tree + } Self::Group { contents } => Tree::List(vec![contents.tree()]), Self::Join { lhs: None, rhs } => Tree::atom("/").push(rhs.tree()), Self::Join { lhs: Some(lhs), rhs, } => Tree::atom("/").push(lhs.tree()).push(rhs.tree()), + Self::Or { lhs, rhs } => Tree::atom("||").push(lhs.tree()).push(rhs.tree()), + Self::StringLiteral { + string_literal: StringLiteral { cooked, .. }, + } => Tree::string(cooked), + Self::Variable { name } => Tree::atom(name.lexeme()), } } } diff --git a/src/parser.rs b/src/parser.rs index 99544acd..e03ab783 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -31,6 +31,7 @@ pub(crate) struct Parser<'run, 'src> { next_token: usize, recursion_depth: usize, tokens: &'run [Token<'src>], + unstable_features: BTreeSet, working_directory: &'run Path, } @@ -51,6 +52,7 @@ impl<'run, 'src> Parser<'run, 'src> { next_token: 0, recursion_depth: 0, tokens, + unstable_features: BTreeSet::new(), working_directory, } .parse_ast() @@ -442,18 +444,19 @@ impl<'run, 'src> Parser<'run, 'src> { } } - if self.next_token == self.tokens.len() { - Ok(Ast { - items, - warnings: Vec::new(), - working_directory: self.working_directory.into(), - }) - } else { - Err(self.internal_error(format!( + if self.next_token != self.tokens.len() { + return Err(self.internal_error(format!( "Parse completed with {} unparsed tokens", self.tokens.len() - self.next_token, - ))?) + ))?); } + + Ok(Ast { + items, + unstable_features: self.unstable_features, + warnings: Vec::new(), + working_directory: self.working_directory.into(), + }) } /// Parse an alias, e.g `alias name := target` @@ -517,26 +520,17 @@ impl<'run, 'src> Parser<'run, 'src> { self.recursion_depth += 1; - let expression = if self.accepted_keyword(Keyword::If)? { - self.parse_conditional()? - } else if self.accepted(Slash)? { - let lhs = None; - let rhs = self.parse_expression()?.into(); - Expression::Join { lhs, rhs } - } else { - let value = self.parse_value()?; + let disjunct = self.parse_disjunct()?; - if self.accepted(Slash)? { - let lhs = Some(Box::new(value)); - let rhs = self.parse_expression()?.into(); - Expression::Join { lhs, rhs } - } else if self.accepted(Plus)? { - let lhs = value.into(); - let rhs = self.parse_expression()?.into(); - Expression::Concatenation { lhs, rhs } - } else { - value - } + let expression = if self.accepted(BarBar)? { + self + .unstable_features + .insert(UnstableFeature::LogicalOperators); + let lhs = disjunct.into(); + let rhs = self.parse_expression()?.into(); + Expression::Or { lhs, rhs } + } else { + disjunct }; self.recursion_depth -= 1; @@ -544,6 +538,47 @@ impl<'run, 'src> Parser<'run, 'src> { Ok(expression) } + fn parse_disjunct(&mut self) -> CompileResult<'src, Expression<'src>> { + let conjunct = self.parse_conjunct()?; + + let disjunct = if self.accepted(AmpersandAmpersand)? { + self + .unstable_features + .insert(UnstableFeature::LogicalOperators); + let lhs = conjunct.into(); + let rhs = self.parse_disjunct()?.into(); + Expression::And { lhs, rhs } + } else { + conjunct + }; + + Ok(disjunct) + } + + fn parse_conjunct(&mut self) -> CompileResult<'src, Expression<'src>> { + if self.accepted_keyword(Keyword::If)? { + self.parse_conditional() + } else if self.accepted(Slash)? { + let lhs = None; + let rhs = self.parse_conjunct()?.into(); + Ok(Expression::Join { lhs, rhs }) + } else { + let value = self.parse_value()?; + + if self.accepted(Slash)? { + let lhs = Some(Box::new(value)); + let rhs = self.parse_conjunct()?.into(); + Ok(Expression::Join { lhs, rhs }) + } else if self.accepted(Plus)? { + let lhs = value.into(); + let rhs = self.parse_conjunct()?.into(); + Ok(Expression::Concatenation { lhs, rhs }) + } else { + Ok(value) + } + } + } + /// Parse a conditional, e.g. `if a == b { "foo" } else { "bar" }` fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> { let condition = self.parse_condition()?; diff --git a/src/summary.rs b/src/summary.rs index ee3a8d11..76483d63 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -183,6 +183,10 @@ impl Assignment { #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub enum Expression { + And { + lhs: Box, + rhs: Box, + }, Assert { condition: Condition, error: Box, @@ -209,6 +213,10 @@ pub enum Expression { lhs: Option>, rhs: Box, }, + Or { + lhs: Box, + rhs: Box, + }, String { text: String, }, @@ -221,6 +229,10 @@ impl Expression { fn new(expression: &full::Expression) -> Self { use full::Expression::*; match expression { + And { lhs, rhs } => Self::And { + lhs: Self::new(lhs).into(), + rhs: Self::new(rhs).into(), + }, Assert { condition: full::Condition { lhs, rhs, operator }, error, @@ -250,11 +262,9 @@ impl Expression { .. } => { let mut arguments = Vec::new(); - if let Some(b) = opt_b.as_ref() { arguments.push(Self::new(b)); } - arguments.push(Self::new(a)); Self::Call { name: name.lexeme().to_owned(), @@ -308,10 +318,6 @@ impl Expression { lhs: Self::new(lhs).into(), rhs: Self::new(rhs).into(), }, - Join { lhs, rhs } => Self::Join { - lhs: lhs.as_ref().map(|lhs| Self::new(lhs).into()), - rhs: Self::new(rhs).into(), - }, Conditional { condition: full::Condition { lhs, rhs, operator }, otherwise, @@ -323,13 +329,21 @@ impl Expression { rhs: Self::new(rhs).into(), then: Self::new(then).into(), }, + Group { contents } => Self::new(contents), + Join { lhs, rhs } => Self::Join { + lhs: lhs.as_ref().map(|lhs| Self::new(lhs).into()), + rhs: Self::new(rhs).into(), + }, + Or { lhs, rhs } => Self::Or { + lhs: Self::new(lhs).into(), + rhs: Self::new(rhs).into(), + }, StringLiteral { string_literal } => Self::String { text: string_literal.cooked.clone(), }, Variable { name, .. } => Self::Variable { name: name.lexeme().to_owned(), }, - Group { contents } => Self::new(contents), } } } diff --git a/src/token_kind.rs b/src/token_kind.rs index 0db15d2d..850afa96 100644 --- a/src/token_kind.rs +++ b/src/token_kind.rs @@ -7,6 +7,7 @@ pub(crate) enum TokenKind { At, Backtick, BangEquals, + BarBar, BraceL, BraceR, BracketL, @@ -50,6 +51,7 @@ impl Display for TokenKind { At => "'@'", Backtick => "backtick", BangEquals => "'!='", + BarBar => "'||'", BraceL => "'{'", BraceR => "'}'", BracketL => "'['", diff --git a/src/unstable_feature.rs b/src/unstable_feature.rs index 07d99540..70e26fab 100644 --- a/src/unstable_feature.rs +++ b/src/unstable_feature.rs @@ -3,6 +3,7 @@ use super::*; #[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd)] pub(crate) enum UnstableFeature { FormatSubcommand, + LogicalOperators, ScriptAttribute, ScriptInterpreterSetting, } @@ -11,6 +12,10 @@ impl Display for UnstableFeature { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::FormatSubcommand => write!(f, "The `--fmt` command is currently unstable."), + Self::LogicalOperators => write!( + f, + "The logical operators `&&` and `||` are currently unstable." + ), Self::ScriptAttribute => write!(f, "The `[script]` attribute is currently unstable."), Self::ScriptInterpreterSetting => { write!(f, "The `script-interpreter` setting is currently unstable.") diff --git a/src/variables.rs b/src/variables.rs index 57979563..9de1c98c 100644 --- a/src/variables.rs +++ b/src/variables.rs @@ -16,7 +16,24 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> { fn next(&mut self) -> Option> { loop { match self.stack.pop()? { - Expression::StringLiteral { .. } | Expression::Backtick { .. } => {} + Expression::And { lhs, rhs } | Expression::Or { lhs, rhs } => { + self.stack.push(lhs); + self.stack.push(rhs); + } + Expression::Assert { + condition: + Condition { + lhs, + rhs, + operator: _, + }, + error, + } => { + self.stack.push(error); + self.stack.push(rhs); + self.stack.push(lhs); + } + Expression::Backtick { .. } | Expression::StringLiteral { .. } => {} Expression::Call { thunk } => match thunk { Thunk::Nullary { .. } => {} Thunk::Unary { arg, .. } => self.stack.push(arg), @@ -56,6 +73,10 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> { } } }, + Expression::Concatenation { lhs, rhs } => { + self.stack.push(rhs); + self.stack.push(lhs); + } Expression::Conditional { condition: Condition { @@ -71,10 +92,8 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> { self.stack.push(rhs); self.stack.push(lhs); } - Expression::Variable { name, .. } => return Some(name.token), - Expression::Concatenation { lhs, rhs } => { - self.stack.push(rhs); - self.stack.push(lhs); + Expression::Group { contents } => { + self.stack.push(contents); } Expression::Join { lhs, rhs } => { self.stack.push(rhs); @@ -82,22 +101,7 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> { self.stack.push(lhs); } } - Expression::Group { contents } => { - self.stack.push(contents); - } - Expression::Assert { - condition: - Condition { - lhs, - rhs, - operator: _, - }, - error, - } => { - self.stack.push(error); - self.stack.push(rhs); - self.stack.push(lhs); - } + Expression::Variable { name, .. } => return Some(name.token), } } } diff --git a/tests/assert_success.rs b/tests/assert_success.rs index bcb364f8..f9202b7f 100644 --- a/tests/assert_success.rs +++ b/tests/assert_success.rs @@ -1,3 +1,4 @@ +#[track_caller] pub(crate) fn assert_success(output: &std::process::Output) { if !output.status.success() { eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr)); diff --git a/tests/conditional.rs b/tests/conditional.rs index c1739240..4eab2f4d 100644 --- a/tests/conditional.rs +++ b/tests/conditional.rs @@ -136,7 +136,7 @@ test! { ", stdout: "", stderr: " - error: Expected '!=', '==', '=~', '+', or '/', but found identifier + error: Expected '&&', '!=', '||', '==', '=~', '+', or '/', but found identifier ——▶ justfile:1:12 │ 1 │ a := if '' a '' { '' } else { b } diff --git a/tests/ignore_comments.rs b/tests/ignore_comments.rs index c3028a57..3f068227 100644 --- a/tests/ignore_comments.rs +++ b/tests/ignore_comments.rs @@ -125,7 +125,7 @@ fn comments_still_must_be_parsable_when_ignored() { ) .stderr( " - error: Expected '}}', '(', '+', or '/', but found identifier + error: Expected '&&', '||', '}}', '(', '+', or '/', but found identifier ——▶ justfile:4:12 │ 4 │ # {{ foo bar }} diff --git a/tests/lib.rs b/tests/lib.rs index ec71c665..7c85460b 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -73,6 +73,7 @@ mod invocation_directory; mod json; mod line_prefixes; mod list; +mod logical_operators; mod man; mod misc; mod modules; diff --git a/tests/logical_operators.rs b/tests/logical_operators.rs new file mode 100644 index 00000000..5baa0f52 --- /dev/null +++ b/tests/logical_operators.rs @@ -0,0 +1,83 @@ +use super::*; + +#[track_caller] +fn evaluate(expression: &str, expected: &str) { + Test::new() + .justfile(format!("x := {expression}")) + .env("JUST_UNSTABLE", "1") + .args(["--evaluate", "x"]) + .stdout(expected) + .run(); +} + +#[test] +fn logical_operators_are_unstable() { + Test::new() + .justfile("x := 'foo' && 'bar'") + .args(["--evaluate", "x"]) + .stderr_regex(r"error: The logical operators `&&` and `\|\|` are currently unstable. .*") + .status(EXIT_FAILURE) + .run(); + + Test::new() + .justfile("x := 'foo' || 'bar'") + .args(["--evaluate", "x"]) + .stderr_regex(r"error: The logical operators `&&` and `\|\|` are currently unstable. .*") + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn and_returns_empty_string_if_lhs_is_empty() { + evaluate("'' && 'hello'", ""); +} + +#[test] +fn and_returns_rhs_if_lhs_is_non_empty() { + evaluate("'hello' && 'goodbye'", "goodbye"); +} + +#[test] +fn and_has_lower_precedence_than_plus() { + evaluate("'' && 'goodbye' + 'foo'", ""); + + evaluate("'foo' + 'hello' && 'goodbye'", "goodbye"); + + evaluate("'foo' + '' && 'goodbye'", "goodbye"); + + evaluate("'foo' + 'hello' && 'goodbye' + 'bar'", "goodbyebar"); +} + +#[test] +fn or_returns_rhs_if_lhs_is_empty() { + evaluate("'' || 'hello'", "hello"); +} + +#[test] +fn or_returns_lhs_if_lhs_is_non_empty() { + evaluate("'hello' || 'goodbye'", "hello"); +} + +#[test] +fn or_has_lower_precedence_than_plus() { + evaluate("'' || 'goodbye' + 'foo'", "goodbyefoo"); + + evaluate("'foo' + 'hello' || 'goodbye'", "foohello"); + + evaluate("'foo' + '' || 'goodbye'", "foo"); + + evaluate("'foo' + 'hello' || 'goodbye' + 'bar'", "foohello"); +} + +#[test] +fn and_has_higher_precedence_than_or() { + evaluate("('' && 'foo') || 'bar'", "bar"); + evaluate("'' && 'foo' || 'bar'", "bar"); + evaluate("'a' && 'b' || 'c'", "b"); +} + +#[test] +fn nesting() { + evaluate("'' || '' || '' || '' || 'foo'", "foo"); + evaluate("'foo' && 'foo' && 'foo' && 'foo' && 'bar'", "bar"); +} diff --git a/tests/shell_expansion.rs b/tests/shell_expansion.rs index 67fdc07d..954e2a33 100644 --- a/tests/shell_expansion.rs +++ b/tests/shell_expansion.rs @@ -25,7 +25,7 @@ fn shell_expanded_strings_must_not_have_whitespace() { .status(1) .stderr( " - error: Expected comment, end of file, end of line, '(', '+', or '/', but found string + error: Expected '&&', '||', comment, end of file, end of line, '(', '+', or '/', but found string ——▶ justfile:1:8 │ 1 │ x := x '$JUST_TEST_VARIABLE' From 8cdff483bf80579a130ccecfc4a5aa62b1a55d39 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 31 Oct 2024 18:00:27 -0700 Subject: [PATCH 03/16] Use `justfile` instead of `mf` on invalid examples in readme (#2447) --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index bccf9a13..5b596ca8 100644 --- a/README.md +++ b/README.md @@ -1332,7 +1332,7 @@ These operators are currently unstable. The `&&` operator returns the empty string if the left-hand argument is the empty string, otherwise it returns the right-hand argument: -```mf +```justfile foo := '' && 'goodbye' # '' bar := 'hello' && 'goodbye' # 'goodbye' ``` @@ -1340,7 +1340,7 @@ bar := 'hello' && 'goodbye' # 'goodbye' The `||` operator returns the left-hand argument if it is non-empty, otherwise it returns the right-hand argument: -```mf +```justfile foo := '' || 'goodbye' # 'goodbye' bar := 'hello' || 'goodbye' # 'hello' ``` @@ -2775,7 +2775,7 @@ pass a Windows-style path to the interpreter. Recipe lines are interpreted by the shell, not `just`, so it's not possible to set `just` variables in the middle of a recipe: -```mf +```justfile foo: x := "hello" # This doesn't work! echo {{x}} @@ -2907,7 +2907,7 @@ means that multi-line constructs probably won't do what you want. For example, with the following `justfile`: -```mf +```justfile conditional: if true; then echo 'True!' @@ -3314,7 +3314,7 @@ One `justfile` can include the contents of another using `import` statements. If you have the following `justfile`: -```mf +```justfile import 'foo/bar.just' a: b @@ -3354,7 +3354,7 @@ set, variables in parent modules override variables in imports. Imports may be made optional by putting a `?` after the `import` keyword: -```mf +```just import? 'foo/bar.just' ``` @@ -3363,19 +3363,19 @@ This allows importing multiple justfiles, for example `foo.just` and `bar.just`, which both import a third justfile containing shared recipes, for example `baz.just`, without the duplicate import of `baz.just` being an error: -```mf +```justfile # justfile import 'foo.just' import 'bar.just' ``` -```mf +```justfile # foo.just import 'baz.just' foo: baz ``` -```mf +```justfile # bar.just import 'baz.just' bar: baz @@ -3396,7 +3396,7 @@ versions, you'll need to use the `--unstable` flag, `set unstable`, or set the If you have the following `justfile`: -```mf +```justfile mod bar a: @@ -3434,7 +3434,7 @@ the module file may have any capitalization. Module statements may be of the form: -```mf +```justfile mod foo 'PATH' ``` @@ -3458,7 +3458,7 @@ recipes. Modules may be made optional by putting a `?` after the `mod` keyword: -```mf +```just mod? foo ``` @@ -3468,7 +3468,7 @@ Optional modules with no source file do not conflict, so you can have multiple mod statements with the same name, but with different source file paths, as long as at most one source file exists: -```mf +```just mod? foo 'bar.just' mod? foo 'baz.just' ``` @@ -3476,7 +3476,7 @@ mod? foo 'baz.just' Modules may be given doc comments which appear in `--list` output1.30.0: -```mf +```justfile # foo is a great module! mod foo ``` From 67034cb8b42a570e2a33aa88e6d5daf9cd309c11 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 2 Nov 2024 11:11:24 -0700 Subject: [PATCH 04/16] Don't export constants (#2449) --- src/binding.rs | 2 ++ src/command_ext.rs | 2 +- src/evaluator.rs | 37 +++++++++++++++++++++++-------------- src/parser.rs | 5 +++-- src/scope.rs | 24 ++++++++++-------------- tests/constants.rs | 16 ++++++++++++++++ 6 files changed, 55 insertions(+), 31 deletions(-) diff --git a/src/binding.rs b/src/binding.rs index 7e7890c7..69dcc3b0 100644 --- a/src/binding.rs +++ b/src/binding.rs @@ -3,6 +3,8 @@ use super::*; /// A binding of `name` to `value` #[derive(Debug, Clone, PartialEq, Serialize)] pub(crate) struct Binding<'src, V = String> { + #[serde(skip)] + pub(crate) constant: bool, pub(crate) export: bool, #[serde(skip)] pub(crate) file_depth: u32, diff --git a/src/command_ext.rs b/src/command_ext.rs index 6bd7208d..40d9da16 100644 --- a/src/command_ext.rs +++ b/src/command_ext.rs @@ -39,7 +39,7 @@ impl CommandExt for Command { } for binding in scope.bindings() { - if settings.export || binding.export { + if binding.export || (settings.export && !binding.constant) { self.env(binding.name.lexeme(), &binding.value); } } diff --git a/src/evaluator.rs b/src/evaluator.rs index d48cece8..b83c8cf7 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -32,12 +32,14 @@ impl<'src, 'run> Evaluator<'src, 'run> { for (name, value) in overrides { if let Some(assignment) = module.assignments.get(name) { - scope.bind( - assignment.export, - assignment.name, - assignment.private, - value.clone(), - ); + scope.bind(Binding { + constant: false, + export: assignment.export, + file_depth: 0, + name: assignment.name, + private: assignment.private, + value: value.clone(), + }); } else { unknown_overrides.push(name.clone()); } @@ -68,12 +70,14 @@ impl<'src, 'run> Evaluator<'src, 'run> { if !self.scope.bound(name) { let value = self.evaluate_expression(&assignment.value)?; - self.scope.bind( - assignment.export, - assignment.name, - assignment.private, + self.scope.bind(Binding { + constant: false, + export: assignment.export, + file_depth: 0, + name: assignment.name, + private: assignment.private, value, - ); + }); } Ok(self.scope.value(name).unwrap()) @@ -340,9 +344,14 @@ impl<'src, 'run> Evaluator<'src, 'run> { rest = &rest[1..]; value }; - evaluator - .scope - .bind(parameter.export, parameter.name, false, value); + evaluator.scope.bind(Binding { + constant: false, + export: parameter.export, + file_depth: 0, + name: parameter.name, + private: false, + value, + }); } Ok((evaluator.scope, positional)) diff --git a/src/parser.rs b/src/parser.rs index e03ab783..00246bf3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -500,11 +500,12 @@ impl<'run, 'src> Parser<'run, 'src> { } Ok(Assignment { - file_depth: self.file_depth, + constant: false, export, + file_depth: self.file_depth, name, - value, private: private || name.lexeme().starts_with('_'), + value, }) } diff --git a/src/scope.rs b/src/scope.rs index 36bbedc8..78d12ca0 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -21,9 +21,11 @@ impl<'src, 'run> Scope<'src, 'run> { }; for (key, value) in constants() { - root.bind( - false, - Name { + root.bind(Binding { + constant: true, + export: false, + file_depth: 0, + name: Name { token: Token { column: 0, kind: TokenKind::Identifier, @@ -34,22 +36,16 @@ impl<'src, 'run> Scope<'src, 'run> { src: key, }, }, - false, - (*value).into(), - ); + private: false, + value: (*value).into(), + }); } root } - pub(crate) fn bind(&mut self, export: bool, name: Name<'src>, private: bool, value: String) { - self.bindings.insert(Binding { - export, - file_depth: 0, - name, - private, - value, - }); + pub(crate) fn bind(&mut self, binding: Binding<'src>) { + self.bindings.insert(binding); } pub(crate) fn bound(&self, name: &str) -> bool { diff --git a/tests/constants.rs b/tests/constants.rs index 59f8b9bd..c6a3a853 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -43,3 +43,19 @@ fn constants_can_be_redefined() { .stdout("foo") .run(); } + +#[test] +fn constants_are_not_exported() { + Test::new() + .justfile( + " + set export + + foo: + echo $HEXUPPER + ", + ) + .stderr_regex(".*HEXUPPER: unbound variable.*") + .status(127) + .run(); +} From 225ba343f492b3bcde42c616a6368bcbac5e3364 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:49:24 -0800 Subject: [PATCH 05/16] Update softprops/action-gh-release (#2450) --- .github/workflows/release.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 128c8350..779dfdba 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -110,7 +110,7 @@ jobs: shell: bash - name: Publish Archive - uses: softprops/action-gh-release@v2.0.8 + uses: softprops/action-gh-release@v2.0.9 if: ${{ startsWith(github.ref, 'refs/tags/') }} with: draft: false @@ -120,7 +120,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish Changelog - uses: softprops/action-gh-release@v2.0.8 + uses: softprops/action-gh-release@v2.0.9 if: >- ${{ startsWith(github.ref, 'refs/tags/') @@ -157,7 +157,7 @@ jobs: shasum -a 256 * > ../SHA256SUMS - name: Publish Checksums - uses: softprops/action-gh-release@v2.0.8 + uses: softprops/action-gh-release@v2.0.9 with: draft: false files: SHA256SUMS From 7eea77278543e93a04b46f751a98d60f30822df9 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 6 Nov 2024 01:49:04 -0800 Subject: [PATCH 06/16] Fix shell function example in readme (#2454) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b596ca8..a0a126b7 100644 --- a/README.md +++ b/README.md @@ -1578,11 +1578,11 @@ file. and can be changed with `set shell := […]`. `command` is passed as the first argument, so if the command is `'echo $@'`, - the full command line, with the default shell command `shell -cu` and `args` + the full command line, with the default shell command `sh -cu` and `args` `'foo'` and `'bar'` will be: ``` - 'shell' '-cu' 'echo $@' 'echo $@' 'foo' 'bar' + 'sh' '-cu' 'echo $@' 'echo $@' 'foo' 'bar' ``` This is so that `$@` works as expected, and `$1` refers to the first From a93b0bf3890de6c48d46b40e2cbcc73c97e3bd41 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Wed, 6 Nov 2024 17:53:51 -0500 Subject: [PATCH 07/16] Update setup-just version in README (#2456) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0a126b7..6ec0e0f6 100644 --- a/README.md +++ b/README.md @@ -401,7 +401,7 @@ Using package managers pre-installed on GitHub Actions runners on MacOS with With [extractions/setup-just](https://github.com/extractions/setup-just): ```yaml -- uses: extractions/setup-just@v1 +- uses: extractions/setup-just@v2 with: just-version: 1.5.0 # optional semver specification, otherwise latest ``` From 1ae6a6d6568652e8b21afa1e4d0c2327e06351fe Mon Sep 17 00:00:00 2001 From: Greg Shuflin Date: Sat, 9 Nov 2024 11:05:58 -0800 Subject: [PATCH 08/16] Highlight backticks in docs when listing recipes (#2423) --- justfile | 2 +- src/color.rs | 4 ++++ src/subcommand.rs | 27 ++++++++++++++++++++++++--- tests/list.rs | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/justfile b/justfile index d0ff455c..5ab15362 100755 --- a/justfile +++ b/justfile @@ -29,7 +29,7 @@ fuzz: run: cargo run -# only run tests matching PATTERN +# only run tests matching `PATTERN` [group: 'test'] filter PATTERN: cargo test {{PATTERN}} diff --git a/src/color.rs b/src/color.rs index ccdf2185..ba437eff 100644 --- a/src/color.rs +++ b/src/color.rs @@ -66,6 +66,10 @@ impl Color { self.restyle(Style::new().fg(Blue)) } + pub(crate) fn doc_backtick(self) -> Self { + self.restyle(Style::new().fg(White).on(Black)) + } + pub(crate) fn error(self) -> Self { self.restyle(Style::new().fg(Red).bold()) } diff --git a/src/subcommand.rs b/src/subcommand.rs index b1f13592..40795496 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -2,6 +2,11 @@ use {super::*, clap_mangen::Man}; const INIT_JUSTFILE: &str = "default:\n echo 'Hello, world!'\n"; +fn backtick_re() -> &'static Regex { + static BACKTICK_RE: OnceLock = OnceLock::new(); + BACKTICK_RE.get_or_init(|| Regex::new("(`.*?`)|(`[^`]*$)").unwrap()) +} + #[derive(PartialEq, Clone, Debug)] pub(crate) enum Subcommand { Changelog, @@ -413,15 +418,31 @@ impl Subcommand { ) { if let Some(doc) = doc { if !doc.is_empty() && doc.lines().count() <= 1 { + let color = config.color.stdout(); print!( - "{:padding$}{} {}", + "{:padding$}{} ", "", - config.color.stdout().doc().paint("#"), - config.color.stdout().doc().paint(doc), + color.doc().paint("#"), padding = max_signature_width.saturating_sub(signature_widths[name]) + 1, ); + + let mut end = 0; + for backtick in backtick_re().find_iter(doc) { + let prefix = &doc[end..backtick.start()]; + if !prefix.is_empty() { + print!("{}", color.doc().paint(prefix)); + } + print!("{}", color.doc_backtick().paint(backtick.as_str())); + end = backtick.end(); + } + + let suffix = &doc[end..]; + if !suffix.is_empty() { + print!("{}", color.doc().paint(suffix)); + } } } + println!(); } diff --git a/tests/list.rs b/tests/list.rs index db7b669e..f7ea7528 100644 --- a/tests/list.rs +++ b/tests/list.rs @@ -438,3 +438,39 @@ fn no_space_before_submodules_not_following_groups() { ) .run(); } + +#[test] +fn backticks_highlighted() { + Test::new() + .justfile( + " + # Comment `` `with backticks` and trailing text + recipe: + ", + ) + .args(["--list", "--color=always"]) + .stdout( + " + Available recipes: + recipe \u{1b}[34m#\u{1b}[0m \u{1b}[34mComment \u{1b}[0m\u{1b}[40;37m``\u{1b}[0m\u{1b}[34m \u{1b}[0m\u{1b}[40;37m`with backticks`\u{1b}[0m\u{1b}[34m and trailing text\u{1b}[0m + ") + .run(); +} + +#[test] +fn unclosed_backticks() { + Test::new() + .justfile( + " + # Comment `with unclosed backick + recipe: + ", + ) + .args(["--list", "--color=always"]) + .stdout( + " + Available recipes: + recipe \u{1b}[34m#\u{1b}[0m \u{1b}[34mComment \u{1b}[0m\u{1b}[40;37m`with unclosed backick\u{1b}[0m + ") + .run(); +} From bbc087294781ee36d2b7c0fbb64d70ac2959558f Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 11 Nov 2024 14:30:06 -0800 Subject: [PATCH 09/16] Terminal escape sequence constants (#2461) --- README.md | 46 ++++++++++++++++++++++++++++++++++- justfile | 4 +++ src/constants.rs | 63 ++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 102 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6ec0e0f6..b318a5db 100644 --- a/README.md +++ b/README.md @@ -1877,6 +1877,30 @@ A number of constants are predefined: | `HEX`1.27.0 | `"0123456789abcdef"` | | `HEXLOWER`1.27.0 | `"0123456789abcdef"` | | `HEXUPPER`1.27.0 | `"0123456789ABCDEF"` | +| `CLEAR`master | `"\ec"` | +| `NORMAL`master | `"\e[0m"` | +| `BOLD`master | `"\e[1m"` | +| `ITALIC`master | `"\e[3m"` | +| `UNDERLINE`master | `"\e[4m"` | +| `INVERT`master | `"\e[7m"` | +| `HIDE`master | `"\e[8m"` | +| `STRIKETHROUGH`master | `"\e[9m"` | +| `BLACK`master | `"\e[30m"` | +| `RED`master | `"\e[31m"` | +| `GREEN`master | `"\e[32m"` | +| `YELLOW`master | `"\e[33m"` | +| `BLUE`master | `"\e[34m"` | +| `MAGENTA`master | `"\e[35m"` | +| `CYAN`master | `"\e[36m"` | +| `WHITE`master | `"\e[37m"` | +| `BG_BLACK`master | `"\e[40m"` | +| `BG_RED`master | `"\e[41m"` | +| `BG_GREEN`master | `"\e[42m"` | +| `BG_YELLOW`master | `"\e[43m"` | +| `BG_BLUE`master | `"\e[44m"` | +| `BG_MAGENTA`master | `"\e[45m"` | +| `BG_CYAN`master | `"\e[46m"` | +| `BG_WHITE`master | `"\e[47m"` | ```just @foo: @@ -1888,9 +1912,29 @@ $ just foo 0123456789abcdef ``` +Constants starting with `\e` are +[ANSI escape sequences](https://en.wikipedia.org/wiki/ANSI_escape_code). + +`CLEAR` clears the screen, similar to the `clear` command. The rest are of the +form `\e[Nm`, where `N` is an integer, and set terminal display attributes. + +Terminal display attribute escape sequences can be combined, for example text +weight `BOLD`, text style `STRIKETHROUGH`, foreground color `CYAN`, and +background color `BG_BLUE`. They should be followed by `NORMAL`, to reset the +terminal back to normal. + +Escape sequences should be quoted, since `[` is treated as a special character +by some shells. + +```just +@foo: + echo '{{BOLD + STRIKETHROUGH + CYAN + BG_BLUE}}Hi!{{NORMAL}}' +``` + ### Attributes -Recipes, `mod` statements, and aliases may be annotated with attributes that change their behavior. +Recipes, `mod` statements, and aliases may be annotated with attributes that +change their behavior. | Name | Type | Description | |------|------|-------------| diff --git a/justfile b/justfile index 5ab15362..e975d506 100755 --- a/justfile +++ b/justfile @@ -169,6 +169,10 @@ build-book: mdbook build book/en mdbook build book/zh +[group: 'dev'] +print-readme-constants-table: + cargo test constants::tests::readme_table -- --nocapture + # run all polyglot recipes [group: 'demo'] polyglot: _python _js _perl _sh _ruby diff --git a/src/constants.rs b/src/constants.rs index e9007ea7..5dd17681 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,15 +1,58 @@ use super::*; -pub(crate) fn constants() -> &'static HashMap<&'static str, &'static str> { - static CONSTANTS: OnceLock> = OnceLock::new(); +const CONSTANTS: [(&str, &str, &str); 27] = [ + ("HEX", "0123456789abcdef", "1.27.0"), + ("HEXLOWER", "0123456789abcdef", "1.27.0"), + ("HEXUPPER", "0123456789ABCDEF", "1.27.0"), + ("CLEAR", "\x1bc", "master"), + ("NORMAL", "\x1b[0m", "master"), + ("BOLD", "\x1b[1m", "master"), + ("ITALIC", "\x1b[3m", "master"), + ("UNDERLINE", "\x1b[4m", "master"), + ("INVERT", "\x1b[7m", "master"), + ("HIDE", "\x1b[8m", "master"), + ("STRIKETHROUGH", "\x1b[9m", "master"), + ("BLACK", "\x1b[30m", "master"), + ("RED", "\x1b[31m", "master"), + ("GREEN", "\x1b[32m", "master"), + ("YELLOW", "\x1b[33m", "master"), + ("BLUE", "\x1b[34m", "master"), + ("MAGENTA", "\x1b[35m", "master"), + ("CYAN", "\x1b[36m", "master"), + ("WHITE", "\x1b[37m", "master"), + ("BG_BLACK", "\x1b[40m", "master"), + ("BG_RED", "\x1b[41m", "master"), + ("BG_GREEN", "\x1b[42m", "master"), + ("BG_YELLOW", "\x1b[43m", "master"), + ("BG_BLUE", "\x1b[44m", "master"), + ("BG_MAGENTA", "\x1b[45m", "master"), + ("BG_CYAN", "\x1b[46m", "master"), + ("BG_WHITE", "\x1b[47m", "master"), +]; - CONSTANTS.get_or_init(|| { - vec![ - ("HEX", "0123456789abcdef"), - ("HEXLOWER", "0123456789abcdef"), - ("HEXUPPER", "0123456789ABCDEF"), - ] - .into_iter() - .collect() +pub(crate) fn constants() -> &'static HashMap<&'static str, &'static str> { + static MAP: OnceLock> = OnceLock::new(); + MAP.get_or_init(|| { + CONSTANTS + .into_iter() + .map(|(name, value, _version)| (name, value)) + .collect() }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn readme_table() { + println!("| Name | Value |"); + println!("|------|-------------|"); + for (name, value, version) in CONSTANTS { + println!( + "| `{name}`{version} | `\"{}\"` |", + value.replace('\x1b', "\\e") + ); + } + } +} From 5db910f400b13daa87b5c37dc62fb1da1ff6533d Mon Sep 17 00:00:00 2001 From: laniakea64 Date: Fri, 15 Nov 2024 19:04:10 -0500 Subject: [PATCH 10/16] Replace `derivative` with `derive-where` (#2465) --- Cargo.lock | 37 +++++++++++++------------------------ Cargo.toml | 2 +- src/lib.rs | 2 +- src/thunk.rs | 18 +++++++++--------- 4 files changed, 24 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80a7276d..e96dd65b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,7 +237,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -329,14 +329,14 @@ dependencies = [ ] [[package]] -name = "derivative" -version = "2.2.0" +name = "derive-where" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -531,7 +531,7 @@ dependencies = [ "clap_complete", "clap_mangen", "ctrlc", - "derivative", + "derive-where", "dirs", "dotenvy", "edit-distance", @@ -861,7 +861,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -930,7 +930,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -958,18 +958,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.79", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "syn", ] [[package]] @@ -1038,7 +1027,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -1142,7 +1131,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.79", + "syn", "wasm-bindgen-shared", ] @@ -1164,7 +1153,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1396,5 +1385,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index f74367d0..c579e2f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ clap = { version = "4.0.0", features = ["derive", "env", "wrap_help"] } clap_complete = "4.0.0" clap_mangen = "0.2.20" ctrlc = { version = "3.1.1", features = ["termination"] } -derivative = "2.0.0" +derive-where = "1.2.7" dirs = "5.0.1" dotenvy = "0.15" edit-distance = "2.0.0" diff --git a/src/lib.rs b/src/lib.rs index 75e3332b..799cc378 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,7 @@ pub(crate) use { }, camino::Utf8Path, clap::ValueEnum, - derivative::Derivative, + derive_where::derive_where, edit_distance::edit_distance, lexiclean::Lexiclean, libc::EXIT_FAILURE, diff --git a/src/thunk.rs b/src/thunk.rs index 82668998..8c7ddfa8 100644 --- a/src/thunk.rs +++ b/src/thunk.rs @@ -1,46 +1,46 @@ use super::*; -#[derive(Derivative)] -#[derivative(Debug, Clone, PartialEq = "feature_allow_slow_enum")] +#[derive_where(Debug, PartialEq)] +#[derive(Clone)] pub(crate) enum Thunk<'src> { Nullary { name: Name<'src>, - #[derivative(Debug = "ignore", PartialEq = "ignore")] + #[derive_where(skip(Debug, EqHashOrd))] function: fn(function::Context) -> FunctionResult, }, Unary { name: Name<'src>, - #[derivative(Debug = "ignore", PartialEq = "ignore")] + #[derive_where(skip(Debug, EqHashOrd))] function: fn(function::Context, &str) -> FunctionResult, arg: Box>, }, UnaryOpt { name: Name<'src>, - #[derivative(Debug = "ignore", PartialEq = "ignore")] + #[derive_where(skip(Debug, EqHashOrd))] function: fn(function::Context, &str, Option<&str>) -> FunctionResult, args: (Box>, Box>>), }, UnaryPlus { name: Name<'src>, - #[derivative(Debug = "ignore", PartialEq = "ignore")] + #[derive_where(skip(Debug, EqHashOrd))] function: fn(function::Context, &str, &[String]) -> FunctionResult, args: (Box>, Vec>), }, Binary { name: Name<'src>, - #[derivative(Debug = "ignore", PartialEq = "ignore")] + #[derive_where(skip(Debug, EqHashOrd))] function: fn(function::Context, &str, &str) -> FunctionResult, args: [Box>; 2], }, BinaryPlus { name: Name<'src>, - #[derivative(Debug = "ignore", PartialEq = "ignore")] + #[derive_where(skip(Debug, EqHashOrd))] function: fn(function::Context, &str, &str, &[String]) -> FunctionResult, args: ([Box>; 2], Vec>), }, Ternary { name: Name<'src>, - #[derivative(Debug = "ignore", PartialEq = "ignore")] + #[derive_where(skip(Debug, EqHashOrd))] function: fn(function::Context, &str, &str, &str) -> FunctionResult, args: [Box>; 3], }, From a73c0976a1ece6bcdf760fd7adcdb063dca06993 Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Sat, 16 Nov 2024 16:58:35 -0700 Subject: [PATCH 11/16] Note that `set shell` is not used for `[script]` recipes (#2468) --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b318a5db..9ae33a62 100644 --- a/README.md +++ b/README.md @@ -2755,8 +2755,9 @@ scripts interpreted by `COMMAND`. This avoids some of the issues with shebang recipes, such as the use of `cygpath` on Windows, the need to use `/usr/bin/env`, and inconsistences in shebang line splitting across Unix OSs. -Recipes with an empty `[script]` attribute are executed with the value of -`set script-interpreter := […]`1.33.0, defaulting to `sh -eu`. +Recipes with an empty `[script]` attribute are executed with the value of `set +script-interpreter := […]`1.33.0, defaulting to `sh -eu`, and *not* +the value of `set shell`. The body of the recipe is evaluated, written to disk in the temporary directory, and run by passing its path as an argument to `COMMAND`. From 520cf91423ed644d6b73252fb552074a1180ba5f Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 16 Nov 2024 16:21:14 -0800 Subject: [PATCH 12/16] Change doc backtick color to cyan (#2469) --- src/color.rs | 2 +- tests/list.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/color.rs b/src/color.rs index ba437eff..953b8ae9 100644 --- a/src/color.rs +++ b/src/color.rs @@ -67,7 +67,7 @@ impl Color { } pub(crate) fn doc_backtick(self) -> Self { - self.restyle(Style::new().fg(White).on(Black)) + self.restyle(Style::new().fg(Cyan)) } pub(crate) fn error(self) -> Self { diff --git a/tests/list.rs b/tests/list.rs index f7ea7528..53a1c737 100644 --- a/tests/list.rs +++ b/tests/list.rs @@ -452,7 +452,7 @@ fn backticks_highlighted() { .stdout( " Available recipes: - recipe \u{1b}[34m#\u{1b}[0m \u{1b}[34mComment \u{1b}[0m\u{1b}[40;37m``\u{1b}[0m\u{1b}[34m \u{1b}[0m\u{1b}[40;37m`with backticks`\u{1b}[0m\u{1b}[34m and trailing text\u{1b}[0m + recipe \u{1b}[34m#\u{1b}[0m \u{1b}[34mComment \u{1b}[0m\u{1b}[36m``\u{1b}[0m\u{1b}[34m \u{1b}[0m\u{1b}[36m`with backticks`\u{1b}[0m\u{1b}[34m and trailing text\u{1b}[0m ") .run(); } @@ -470,7 +470,7 @@ fn unclosed_backticks() { .stdout( " Available recipes: - recipe \u{1b}[34m#\u{1b}[0m \u{1b}[34mComment \u{1b}[0m\u{1b}[40;37m`with unclosed backick\u{1b}[0m + recipe \u{1b}[34m#\u{1b}[0m \u{1b}[34mComment \u{1b}[0m\u{1b}[36m`with unclosed backick\u{1b}[0m ") .run(); } From c7b2b78dcc43e3bcd9b6aade5092a1bcc9364675 Mon Sep 17 00:00:00 2001 From: Naveen Prashanth <78990165+gnpaone@users.noreply.github.com> Date: Sun, 17 Nov 2024 07:04:04 +0530 Subject: [PATCH 13/16] Add `-g` to `rust-just` install instructions (#2459) --- README.md | 2 +- README.中文.md | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ae33a62..ae57d71a 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ most Windows users.) npm rust-just - npm install rust-just + npm install -g rust-just PyPI diff --git a/README.中文.md b/README.中文.md index 936fb183..f586f78e 100644 --- a/README.中文.md +++ b/README.中文.md @@ -220,6 +220,22 @@ list: asdf install just <version> + + Various + PyPI + rust-just + + pipx install rust-just
+ + + + Various + npm + rust-just + + npm install -g rust-just
+ + Debian and Ubuntu derivatives MPR From eb6e3741b8b02f53b22c4f684cf58587e2af42f5 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 16 Nov 2024 18:02:27 -0800 Subject: [PATCH 14/16] Make recipe doc attribute override comment (#2470) --- src/node.rs | 2 +- src/parser.rs | 8 ++++++++ src/recipe.rs | 15 ++++++++++---- tests/fmt.rs | 24 ++++++++++++++++++++++ tests/json.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 5 deletions(-) diff --git a/src/node.rs b/src/node.rs index f8788c4d..6bfb042a 100644 --- a/src/node.rs +++ b/src/node.rs @@ -197,7 +197,7 @@ impl<'src> Node<'src> for UnresolvedRecipe<'src> { t.push_mut("quiet"); } - if let Some(doc) = self.doc { + if let Some(doc) = &self.doc { t.push_mut(Tree::string(doc)); } diff --git a/src/parser.rs b/src/parser.rs index 00246bf3..229f5aea 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -936,6 +936,14 @@ impl<'run, 'src> Parser<'run, 'src> { let private = name.lexeme().starts_with('_') || attributes.contains(&Attribute::Private); + let mut doc = doc.map(ToOwned::to_owned); + + for attribute in &attributes { + if let Attribute::Doc(attribute_doc) = attribute { + doc = attribute_doc.as_ref().map(|doc| doc.cooked.clone()); + } + } + Ok(Recipe { shebang: shebang || script, attributes, diff --git a/src/recipe.rs b/src/recipe.rs index 05516225..d57bc938 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -22,7 +22,7 @@ pub(crate) struct Recipe<'src, D = Dependency<'src>> { pub(crate) attributes: BTreeSet>, pub(crate) body: Vec>, pub(crate) dependencies: Vec, - pub(crate) doc: Option<&'src str>, + pub(crate) doc: Option, #[serde(skip)] pub(crate) file_depth: u32, #[serde(skip)] @@ -465,7 +465,8 @@ impl<'src, D> Recipe<'src, D> { return doc.as_ref().map(|s| s.cooked.as_ref()); } } - self.doc + + self.doc.as_deref() } pub(crate) fn subsequents(&self) -> impl Iterator { @@ -475,8 +476,14 @@ impl<'src, D> Recipe<'src, D> { impl<'src, D: Display> ColorDisplay for Recipe<'src, D> { fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result { - if let Some(doc) = self.doc { - writeln!(f, "# {doc}")?; + if !self + .attributes + .iter() + .any(|attribute| matches!(attribute, Attribute::Doc(_))) + { + if let Some(doc) = &self.doc { + writeln!(f, "# {doc}")?; + } } for attribute in &self.attributes { diff --git a/tests/fmt.rs b/tests/fmt.rs index 013c0a27..110b6e59 100644 --- a/tests/fmt.rs +++ b/tests/fmt.rs @@ -1096,3 +1096,27 @@ fn multi_argument_attribute() { ) .run(); } + +#[test] +fn doc_attribute_suppresses_comment() { + Test::new() + .justfile( + " + set unstable + + # COMMENT + [doc('ATTRIBUTE')] + foo: + ", + ) + .arg("--dump") + .stdout( + " + set unstable := true + + [doc('ATTRIBUTE')] + foo: + ", + ) + .run(); +} diff --git a/tests/json.rs b/tests/json.rs index 3819c040..d3ea7d56 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -1429,3 +1429,58 @@ fn recipes_with_private_attribute_are_private() { }), ); } + +#[test] +fn doc_attribute_overrides_comment() { + case( + " + # COMMENT + [doc('ATTRIBUTE')] + foo: + ", + json!({ + "aliases": {}, + "assignments": {}, + "first": "foo", + "doc": null, + "groups": [], + "modules": {}, + "recipes": { + "foo": { + "attributes": [{"doc": "ATTRIBUTE"}], + "body": [], + "dependencies": [], + "doc": "ATTRIBUTE", + "name": "foo", + "namepath": "foo", + "parameters": [], + "priors": 0, + "private": false, + "quiet": false, + "shebang": false, + } + }, + "settings": { + "allow_duplicate_recipes": false, + "allow_duplicate_variables": false, + "dotenv_filename": null, + "dotenv_load": false, + "dotenv_path": null, + "dotenv_required": false, + "export": false, + "fallback": false, + "ignore_comments": false, + "positional_arguments": false, + "quiet": false, + "shell": null, + "tempdir" : null, + "unstable": false, + "windows_powershell": false, + "windows_shell": null, + "working_directory" : null, + }, + "unexports": [], + "warnings": [], + }), + ); +} From 084a2d2de3b80e7a4c62181ff1a25642ac075223 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 16 Nov 2024 18:26:11 -0800 Subject: [PATCH 15/16] Add `style()` function (#2462) --- README.md | 18 +++++++++++ src/color.rs | 1 - src/function.rs | 15 ++++++++++ tests/functions.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ae57d71a..622f4170 100644 --- a/README.md +++ b/README.md @@ -1851,6 +1851,24 @@ for details. `requirement`, e.g., `">=0.1.0"`, returning `"true"` if so and `"false"` otherwise. +#### Style + +- `style(name)`master - Return a named terminal display attribute + escape sequence used by `just`. Unlike terminal display attribute escape + sequence constants, which contain standard colors and styles, `style(name)` + returns an escape sequence used by `just` itself, and can be used to make + recipe output match `just`'s own output. + + Recognized values for `name` are `'command'`, for echoed recipe lines, + `error`, and `warning`. + + For example, to style an error message: + + ```just + scary: + @echo '{{ style("error") }}OH NO{{ NORMAL }}' + ``` + ##### XDG Directories1.23.0 These functions return paths to user-specific directories for things like diff --git a/src/color.rs b/src/color.rs index 953b8ae9..7742597b 100644 --- a/src/color.rs +++ b/src/color.rs @@ -35,7 +35,6 @@ impl Color { Self::default() } - #[cfg(test)] pub(crate) fn always() -> Self { Self { use_color: UseColor::Always, diff --git a/src/function.rs b/src/function.rs index a714a8d0..abeae943 100644 --- a/src/function.rs +++ b/src/function.rs @@ -98,6 +98,7 @@ pub(crate) fn get(name: &str) -> Option { "snakecase" => Unary(snakecase), "source_directory" => Nullary(source_directory), "source_file" => Nullary(source_file), + "style" => Unary(style), "titlecase" => Unary(titlecase), "trim" => Unary(trim), "trim_end" => Unary(trim_end), @@ -623,6 +624,20 @@ fn source_file(context: Context) -> FunctionResult { }) } +fn style(context: Context, s: &str) -> FunctionResult { + match s { + "command" => Ok( + Color::always() + .command(context.evaluator.context.config.command_color) + .prefix() + .to_string(), + ), + "error" => Ok(Color::always().error().prefix().to_string()), + "warning" => Ok(Color::always().warning().prefix().to_string()), + _ => Err(format!("unknown style: `{s}`")), + } +} + fn titlecase(_context: Context, s: &str) -> FunctionResult { Ok(s.to_title_case()) } diff --git a/tests/functions.rs b/tests/functions.rs index 76964b74..d68b3946 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -1183,3 +1183,78 @@ bar: .args(["foo", "bar"]) .run(); } + +#[test] +fn style_command_default() { + Test::new() + .justfile( + r#" + foo: + @echo '{{ style("command") }}foo{{NORMAL}}' + "#, + ) + .stdout("\x1b[1mfoo\x1b[0m\n") + .run(); +} + +#[test] +fn style_command_non_default() { + Test::new() + .justfile( + r#" + foo: + @echo '{{ style("command") }}foo{{NORMAL}}' + "#, + ) + .args(["--command-color", "red"]) + .stdout("\x1b[1;31mfoo\x1b[0m\n") + .run(); +} + +#[test] +fn style_error() { + Test::new() + .justfile( + r#" + foo: + @echo '{{ style("error") }}foo{{NORMAL}}' + "#, + ) + .stdout("\x1b[1;31mfoo\x1b[0m\n") + .run(); +} + +#[test] +fn style_warning() { + Test::new() + .justfile( + r#" + foo: + @echo '{{ style("warning") }}foo{{NORMAL}}' + "#, + ) + .stdout("\x1b[1;33mfoo\x1b[0m\n") + .run(); +} + +#[test] +fn style_unknown() { + Test::new() + .justfile( + r#" + foo: + @echo '{{ style("hippo") }}foo{{NORMAL}}' + "#, + ) + .stderr( + r#" + error: Call to function `style` failed: unknown style: `hippo` + ——▶ justfile:2:13 + │ + 2 │ @echo '{{ style("hippo") }}foo{{NORMAL}}' + │ ^^^^^ + "#, + ) + .status(EXIT_FAILURE) + .run(); +} From 17350a603e7c29f7c2547e2c47df0a21c06a3d46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:04:52 -0800 Subject: [PATCH 16/16] Update `softprops/action-gh-release` (#2471) --- .github/workflows/release.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 779dfdba..323f6f00 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -110,7 +110,7 @@ jobs: shell: bash - name: Publish Archive - uses: softprops/action-gh-release@v2.0.9 + uses: softprops/action-gh-release@v2.1.0 if: ${{ startsWith(github.ref, 'refs/tags/') }} with: draft: false @@ -120,7 +120,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish Changelog - uses: softprops/action-gh-release@v2.0.9 + uses: softprops/action-gh-release@v2.1.0 if: >- ${{ startsWith(github.ref, 'refs/tags/') @@ -157,7 +157,7 @@ jobs: shasum -a 256 * > ../SHA256SUMS - name: Publish Checksums - uses: softprops/action-gh-release@v2.0.9 + uses: softprops/action-gh-release@v2.1.0 with: draft: false files: SHA256SUMS