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/README.md b/README.md index 6ec0e0f6..622f4170 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 @@ -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 @@ -1877,6 +1895,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 +1930,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 | |------|------|-------------| @@ -2711,8 +2773,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`. 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 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/color.rs b/src/color.rs index ba437eff..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, @@ -67,7 +66,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/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") + ); + } + } +} 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/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/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/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], }, 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/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(); +} 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": [], + }), + ); +} 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(); }