From 7d56d5214e90a18ce404274feaa3793438b69426 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 1 Dec 2024 14:54:28 -0800 Subject: [PATCH 01/27] [bin/forbid] Improve error message if ripgrep is missing (#2493) --- bin/forbid | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bin/forbid b/bin/forbid index 538c780c0d..8638e832c4 100755 --- a/bin/forbid +++ b/bin/forbid @@ -1,8 +1,12 @@ #!/usr/bin/env bash -set -euxo pipefail +set -euo pipefail + +if ! which rg > /dev/null; then + echo 'error: `rg` not found, please install ripgrep: https://github.com/BurntSushi/ripgrep/' + exit 1 +fi -which rg ! rg \ --glob !bin/forbid \ From cdf104bf8cce5faa1e8649856cf747d6584d6e36 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 1 Dec 2024 16:37:08 -0800 Subject: [PATCH 02/27] Add `--request` subcommand for testing (#2498) --- src/config.rs | 18 ++++++++++++++++++ src/config_error.rs | 2 ++ src/error.rs | 6 +++--- src/lib.rs | 11 +++++++++-- src/request.rs | 13 +++++++++++++ src/subcommand.rs | 24 +++++++++++++++++++----- tests/constants.rs | 9 ++++----- tests/lib.rs | 3 ++- tests/request.rs | 29 +++++++++++++++++++++++++++++ tests/test.rs | 16 ++++++++++++++++ 10 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 src/request.rs create mode 100644 tests/request.rs diff --git a/src/config.rs b/src/config.rs index 6eb8d23b1d..86ec4bfeb5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -53,6 +53,7 @@ mod cmd { pub(crate) const INIT: &str = "INIT"; pub(crate) const LIST: &str = "LIST"; pub(crate) const MAN: &str = "MAN"; + pub(crate) const REQUEST: &str = "REQUEST"; pub(crate) const SHOW: &str = "SHOW"; pub(crate) const SUMMARY: &str = "SUMMARY"; pub(crate) const VARIABLES: &str = "VARIABLES"; @@ -69,6 +70,7 @@ mod cmd { INIT, LIST, MAN, + REQUEST, SHOW, SUMMARY, VARIABLES, @@ -517,6 +519,17 @@ impl Config { .help("Print man page") .help_heading(cmd::HEADING), ) + .arg( + Arg::new(cmd::REQUEST) + .long("request") + .action(ArgAction::Set) + .hide(true) + .help( + "Execute . For internal testing purposes only. May be changed or removed at \ + any time.", + ) + .help_heading(cmd::REQUEST), + ) .arg( Arg::new(cmd::SHOW) .short('s') @@ -696,6 +709,11 @@ impl Config { } } else if matches.get_flag(cmd::MAN) { Subcommand::Man + } else if let Some(request) = matches.get_one::(cmd::REQUEST) { + Subcommand::Request { + request: serde_json::from_str(request) + .map_err(|source| ConfigError::RequestParse { source })?, + } } else if let Some(path) = matches.get_many::(cmd::SHOW) { Subcommand::Show { path: Self::parse_module_path(path)?, diff --git a/src/config_error.rs b/src/config_error.rs index 6935b81cd5..3023346386 100644 --- a/src/config_error.rs +++ b/src/config_error.rs @@ -12,6 +12,8 @@ pub(crate) enum ConfigError { Internal { message: String }, #[snafu(display("Invalid module path `{}`", path.join(" ")))] ModulePath { path: Vec }, + #[snafu(display("Failed to parse request: {source}"))] + RequestParse { source: serde_json::Error }, #[snafu(display( "Path-prefixed recipes may not be used with `--working-directory` or `--justfile`." ))] diff --git a/src/error.rs b/src/error.rs index b8774a2515..a07c4efd62 100644 --- a/src/error.rs +++ b/src/error.rs @@ -81,7 +81,7 @@ pub(crate) enum Error<'src> { }, DotenvRequired, DumpJson { - serde_json_error: serde_json::Error, + source: serde_json::Error, }, EditorInvoke { editor: OsString, @@ -359,8 +359,8 @@ impl ColorDisplay for Error<'_> { DotenvRequired => { write!(f, "Dotenv file not found")?; } - DumpJson { serde_json_error } => { - write!(f, "Failed to dump JSON to stdout: {serde_json_error}")?; + DumpJson { source } => { + write!(f, "Failed to dump JSON to stdout: {source}")?; } EditorInvoke { editor, io_error } => { let editor = editor.to_string_lossy(); diff --git a/src/lib.rs b/src/lib.rs index 799cc37832..cda797c7c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ pub(crate) use { regex::Regex, serde::{ ser::{SerializeMap, SerializeSeq}, - Serialize, Serializer, + Deserialize, Serialize, Serializer, }, snafu::{ResultExt, Snafu}, std::{ @@ -76,9 +76,12 @@ pub(crate) use crate::{node::Node, tree::Tree}; pub use crate::run::run; +#[doc(hidden)] +use request::Request; + // Used in integration tests. #[doc(hidden)] -pub use unindent::unindent; +pub use {request::Response, unindent::unindent}; type CompileResult<'a, T = ()> = Result>; type ConfigResult = Result; @@ -106,6 +109,10 @@ pub mod fuzzing; #[doc(hidden)] pub mod summary; +// Used for testing with the `--request` subcommand. +#[doc(hidden)] +pub mod request; + mod alias; mod analyzer; mod argument_parser; diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000000..a84a84c2fa --- /dev/null +++ b/src/request.rs @@ -0,0 +1,13 @@ +use super::*; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum Request { + EnvironmentVariable(String), +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum Response { + EnvironmentVariable(Option), +} diff --git a/src/subcommand.rs b/src/subcommand.rs index cc47d879a1..8d34578845 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -35,6 +35,9 @@ pub(crate) enum Subcommand { path: ModulePath, }, Man, + Request { + request: Request, + }, Run { arguments: Vec, overrides: BTreeMap, @@ -71,10 +74,6 @@ impl Subcommand { let justfile = &compilation.justfile; match self { - Run { - arguments, - overrides, - } => Self::run(config, loader, search, compilation, arguments, overrides)?, Choose { overrides, chooser } => { Self::choose(config, justfile, &search, overrides, chooser.as_deref())?; } @@ -85,6 +84,11 @@ impl Subcommand { Format => Self::format(config, &search, compilation)?, Groups => Self::groups(config, justfile), List { path } => Self::list(config, justfile, path)?, + Request { request } => Self::request(request)?, + Run { + arguments, + overrides, + } => Self::run(config, loader, search, compilation, arguments, overrides)?, Show { path } => Self::show(config, justfile, path)?, Summary => Self::summary(config, justfile), Variables => Self::variables(justfile), @@ -280,7 +284,7 @@ impl Subcommand { match config.dump_format { DumpFormat::Json => { serde_json::to_writer(io::stdout(), &compilation.justfile) - .map_err(|serde_json_error| Error::DumpJson { serde_json_error })?; + .map_err(|source| Error::DumpJson { source })?; println!(); } DumpFormat::Just => print!("{}", compilation.root_ast()), @@ -402,6 +406,16 @@ impl Subcommand { Ok(()) } + fn request(request: &Request) -> RunResult<'static> { + let response = match request { + Request::EnvironmentVariable(key) => Response::EnvironmentVariable(env::var_os(key)), + }; + + serde_json::to_writer(io::stdout(), &response).map_err(|source| Error::DumpJson { source })?; + + Ok(()) + } + fn list(config: &Config, mut module: &Justfile, path: &ModulePath) -> RunResult<'static> { for name in &path.path { module = module diff --git a/tests/constants.rs b/tests/constants.rs index c6a3a85329..5c30109d8a 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -48,14 +48,13 @@ fn constants_can_be_redefined() { fn constants_are_not_exported() { Test::new() .justfile( - " + r#" set export foo: - echo $HEXUPPER - ", + @'{{just_executable()}}' --request '{"environment-variable": "HEXUPPER"}' + "#, ) - .stderr_regex(".*HEXUPPER: unbound variable.*") - .status(127) + .response(Response::EnvironmentVariable(None)) .run(); } diff --git a/tests/lib.rs b/tests/lib.rs index 675e1fe048..3ee9c3801f 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -6,7 +6,7 @@ pub(crate) use { test::{assert_eval_eq, Output, Test}, }, executable_path::executable_path, - just::unindent, + just::{unindent, Response}, libc::{EXIT_FAILURE, EXIT_SUCCESS}, pretty_assertions::Comparison, regex::Regex, @@ -99,6 +99,7 @@ mod quote; mod readme; mod recursion_limit; mod regexes; +mod request; mod run; mod script; mod search; diff --git a/tests/request.rs b/tests/request.rs new file mode 100644 index 0000000000..da8e144fa2 --- /dev/null +++ b/tests/request.rs @@ -0,0 +1,29 @@ +use super::*; + +#[test] +fn environment_variable_set() { + Test::new() + .justfile( + r#" + export BAR := 'baz' + + @foo: + '{{just_executable()}}' --request '{"environment-variable": "BAR"}' + "#, + ) + .response(Response::EnvironmentVariable(Some("baz".into()))) + .run(); +} + +#[test] +fn environment_variable_missing() { + Test::new() + .justfile( + r#" + @foo: + '{{just_executable()}}' --request '{"environment-variable": "FOO_BAR_BAZ"}' + "#, + ) + .response(Response::EnvironmentVariable(None)) + .run(); +} diff --git a/tests/test.rs b/tests/test.rs index cd7c0cceb7..e02b5a85b6 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -50,6 +50,7 @@ pub(crate) struct Test { pub(crate) env: BTreeMap, pub(crate) expected_files: BTreeMap>, pub(crate) justfile: Option, + pub(crate) response: Option, pub(crate) shell: bool, pub(crate) status: i32, pub(crate) stderr: String, @@ -74,6 +75,7 @@ impl Test { env: BTreeMap::new(), expected_files: BTreeMap::new(), justfile: Some(String::new()), + response: None, shell: true, status: EXIT_SUCCESS, stderr: String::new(), @@ -139,6 +141,11 @@ impl Test { self } + pub(crate) fn response(mut self, response: Response) -> Self { + self.response = Some(response); + self.stdout_regex(".*") + } + pub(crate) fn shell(mut self, shell: bool) -> Self { self.shell = shell; self @@ -293,6 +300,15 @@ impl Test { panic!("Output mismatch."); } + if let Some(ref response) = self.response { + assert_eq!( + &serde_json::from_str::(output_stdout) + .expect("failed to deserialize stdout as response"), + response, + "response mismatch" + ); + } + for (path, expected) in &self.expected_files { let actual = fs::read(self.tempdir.path().join(path)).unwrap(); assert_eq!( From 563eee660941f3e144de87eacf8bad220f1ad1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladimir=20T=C3=A1mara=20Pati=C3=B1o?= Date: Sun, 1 Dec 2024 20:01:34 -0500 Subject: [PATCH 03/27] Add `[openbsd]` recipe attribute (#2497) --- README.md | 7 ++++--- crates-io-readme.md | 8 +++++--- src/attribute.rs | 4 ++++ src/recipe.rs | 10 ++++++---- tests/attributes.rs | 15 ++++++++------- tests/os_attributes.rs | 13 +++++++++++-- 6 files changed, 38 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 27e3307a50..976892075a 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ Yay, all your tests passed! [`make`'s complexity and idiosyncrasies](#what-are-the-idiosyncrasies-of-make-that-just-avoids). No need for `.PHONY` recipes! -- Linux, MacOS, and Windows are supported with no additional dependencies. - (Although if your system doesn't have an `sh`, you'll need to - [choose a different shell](#shell).) +- Linux, MacOS, Windows, and other reasonable unices are supported with no + additional dependencies. (Although if your system doesn't have an `sh`, + you'll need to [choose a different shell](#shell).) - Errors are specific and informative, and syntax errors are reported along with their source context. @@ -1986,6 +1986,7 @@ change their behavior. | `[no-cd]`1.9.0 | recipe | Don't change directory before executing recipe. | | `[no-exit-message]`1.7.0 | recipe | Don't print an error message if recipe fails. | | `[no-quiet]`1.23.0 | recipe | Override globally quiet recipes and always echo out the recipe. | +| `[openbsd]`master | recipe | Enable recipe on OpenBSD. | | `[positional-arguments]`1.29.0 | recipe | Turn on [positional arguments](#positional-arguments) for this recipe. | | `[private]`1.10.0 | alias, recipe | Make recipe, alias, or variable private. See [Private Recipes](#private-recipes). | | `[script]`1.33.0 | recipe | Execute recipe as script. See [script recipes](#script-recipes) for more details. | diff --git a/crates-io-readme.md b/crates-io-readme.md index c05ac0a639..63c900f2b6 100644 --- a/crates-io-readme.md +++ b/crates-io-readme.md @@ -1,6 +1,7 @@ `just` is a handy way to save and run project-specific commands. -Commands are stored in a file called `justfile` or `Justfile` with syntax inspired by `make`: +Commands are stored in a file called `justfile` or `Justfile` with syntax +inspired by `make`: ```make build: @@ -15,8 +16,9 @@ test TEST: build ./test --test {{TEST}} ``` -`just` produces detailed error messages and avoids `make`'s idiosyncrasies, so debugging a justfile is easier and less surprising than debugging a makefile. +`just` produces detailed error messages and avoids `make`'s idiosyncrasies, so +debugging a justfile is easier and less surprising than debugging a makefile. -It works on Linux, MacOS, and Windows. +It works on all operating systems supported by Rust. Read more on [GitHub](https://github.com/casey/just). diff --git a/src/attribute.rs b/src/attribute.rs index 39a0a80060..4ec813f076 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -18,6 +18,7 @@ pub(crate) enum Attribute<'src> { NoCd, NoExitMessage, NoQuiet, + Openbsd, PositionalArguments, Private, Script(Option>), @@ -36,6 +37,7 @@ impl AttributeDiscriminant { | Self::NoCd | Self::NoExitMessage | Self::NoQuiet + | Self::Openbsd | Self::PositionalArguments | Self::Private | Self::Unix @@ -83,6 +85,7 @@ impl<'src> Attribute<'src> { AttributeDiscriminant::NoCd => Self::NoCd, AttributeDiscriminant::NoExitMessage => Self::NoExitMessage, AttributeDiscriminant::NoQuiet => Self::NoQuiet, + AttributeDiscriminant::Openbsd => Self::Openbsd, AttributeDiscriminant::PositionalArguments => Self::PositionalArguments, AttributeDiscriminant::Private => Self::Private, AttributeDiscriminant::Script => Self::Script({ @@ -131,6 +134,7 @@ impl Display for Attribute<'_> { | Self::NoCd | Self::NoExitMessage | Self::NoQuiet + | Self::Openbsd | Self::PositionalArguments | Self::Private | Self::Script(None) diff --git a/src/recipe.rs b/src/recipe.rs index 81fbc7ac2f..ac7fdd6596 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -113,17 +113,19 @@ impl<'src, D> Recipe<'src, D> { } pub(crate) fn enabled(&self) -> bool { - let windows = self.attributes.contains(&Attribute::Windows); let linux = self.attributes.contains(&Attribute::Linux); let macos = self.attributes.contains(&Attribute::Macos); + let openbsd = self.attributes.contains(&Attribute::Openbsd); let unix = self.attributes.contains(&Attribute::Unix); + let windows = self.attributes.contains(&Attribute::Windows); - (!windows && !linux && !macos && !unix) - || (cfg!(target_os = "windows") && windows) + (!windows && !linux && !macos && !openbsd && !unix) || (cfg!(target_os = "linux") && (linux || unix)) || (cfg!(target_os = "macos") && (macos || unix)) - || (cfg!(windows) && windows) + || (cfg!(target_os = "openbsd") && (openbsd || unix)) + || (cfg!(target_os = "windows") && windows) || (cfg!(unix) && unix) + || (cfg!(windows) && windows) } fn print_exit_message(&self) -> bool { diff --git a/tests/attributes.rs b/tests/attributes.rs index 9f022025de..80393f1aa2 100644 --- a/tests/attributes.rs +++ b/tests/attributes.rs @@ -6,9 +6,10 @@ fn all() { .justfile( " [macos] - [windows] [linux] + [openbsd] [unix] + [windows] [no-exit-message] foo: exit 1 @@ -48,7 +49,7 @@ fn multiple_attributes_one_line() { Test::new() .justfile( " - [macos, windows,linux] + [macos,windows,linux,openbsd] [no-exit-message] foo: exit 1 @@ -64,7 +65,7 @@ fn multiple_attributes_one_line_error_message() { Test::new() .justfile( " - [macos, windows linux] + [macos,windows linux,openbsd] [no-exit-message] foo: exit 1 @@ -73,10 +74,10 @@ fn multiple_attributes_one_line_error_message() { .stderr( " error: Expected ']', ':', ',', or '(', but found identifier - ——▶ justfile:1:17 + ——▶ justfile:1:16 │ - 1 │ [macos, windows linux] - │ ^^^^^ + 1 │ [macos,windows linux,openbsd] + │ ^^^^^ ", ) .status(1) @@ -88,7 +89,7 @@ fn multiple_attributes_one_line_duplicate_check() { Test::new() .justfile( " - [macos, windows, linux] + [macos, windows, linux, openbsd] [linux] foo: exit 1 diff --git a/tests/os_attributes.rs b/tests/os_attributes.rs index 81729850f7..de74058043 100644 --- a/tests/os_attributes.rs +++ b/tests/os_attributes.rs @@ -47,6 +47,10 @@ fn os() { [linux] foo: echo quxx + + [openbsd] + foo: + echo bob ", ) .stdout(if cfg!(target_os = "macos") { @@ -55,6 +59,8 @@ fn os() { "baz\n" } else if cfg!(target_os = "linux") { "quxx\n" + } else if cfg!(target_os = "openbsd") { + "bob\n" } else { panic!("unexpected os family") }) @@ -64,6 +70,8 @@ fn os() { "echo baz\n" } else if cfg!(target_os = "linux") { "echo quxx\n" + } else if cfg!(target_os = "openbsd") { + "echo bob\n" } else { panic!("unexpected os family") }) @@ -75,10 +83,11 @@ fn all() { Test::new() .justfile( " - [macos] - [windows] [linux] + [macos] + [openbsd] [unix] + [windows] foo: echo bar ", From e8d6f2dbedf82ac6562e23688ff897eb27d7ed90 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 2 Dec 2024 13:13:29 -0800 Subject: [PATCH 04/27] Style help text (#2501) --- src/config.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/config.rs b/src/config.rs index 86ec4bfeb5..6a5897c996 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,10 @@ use { super::*, clap::{ - builder::{styling::AnsiColor, FalseyValueParser, Styles}, + builder::{ + styling::{AnsiColor, Effects}, + FalseyValueParser, Styles, + }, parser::ValuesRef, value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command, }, @@ -134,10 +137,12 @@ impl Config { .trailing_var_arg(true) .styles( Styles::styled() - .header(AnsiColor::Yellow.on_default()) + .header(AnsiColor::Yellow.on_default() | Effects::BOLD) + .invalid(AnsiColor::Red.on_default()) .literal(AnsiColor::Green.on_default()) - .placeholder(AnsiColor::Green.on_default()) - .usage(AnsiColor::Yellow.on_default()), + .placeholder(AnsiColor::Cyan.on_default()) + .usage(AnsiColor::Yellow.on_default() | Effects::BOLD) + .valid(AnsiColor::Green.on_default()), ) .arg( Arg::new(arg::CHECK) From 79be6c14addcfc5c998aa220a41e5686c68bc9fc Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 2 Dec 2024 13:17:26 -0800 Subject: [PATCH 05/27] Embolden help text errors (#2502) --- src/config.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config.rs b/src/config.rs index 6a5897c996..df7e91ef56 100644 --- a/src/config.rs +++ b/src/config.rs @@ -137,6 +137,7 @@ impl Config { .trailing_var_arg(true) .styles( Styles::styled() + .error(AnsiColor::Red.on_default() | Effects::BOLD) .header(AnsiColor::Yellow.on_default() | Effects::BOLD) .invalid(AnsiColor::Red.on_default()) .literal(AnsiColor::Green.on_default()) From e1b85d9d0bc160c1ac8ca3bcadb2342ea37e8e9b Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 6 Dec 2024 20:52:27 -0500 Subject: [PATCH 06/27] Add snap package back to readme (#2506) --- README.md | 5 +++++ snapcraft.yaml | 32 -------------------------------- 2 files changed, 5 insertions(+), 32 deletions(-) delete mode 100644 snapcraft.yaml diff --git a/README.md b/README.md index 976892075a..1fc7cccd2d 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,11 @@ most Windows users.) rust-just pipx install rust-just + + Snap + just + snap install --edge --classic just + diff --git a/snapcraft.yaml b/snapcraft.yaml deleted file mode 100644 index 5da4c06200..0000000000 --- a/snapcraft.yaml +++ /dev/null @@ -1,32 +0,0 @@ -base: core22 -confinement: classic -contact: casey@rodarmor.com -description: Just is a handy way to save and run project-specific commands. -grade: stable -icon: icon.png -issues: https://github.com/casey/just/issues -license: CC0-1.0 -name: just -source-code: https://github.com/casey/just -summary: Just a command runner -version: '1.2.0' -website: https://just.systems - -apps: - just: - command: bin/just - completer: just.bash - -parts: - just: - plugin: rust - source-depth: 1 - source-tag: '1.2.0' - source-type: git - source: https://github.com/casey/just - - completions: - plugin: dump - source: completions - stage: - - just.bash From 523801af5f6efc6e90c4d7a2d8746933ebffcfe3 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 10 Dec 2024 13:07:10 -0800 Subject: [PATCH 07/27] Note lack of support for string interpolation (#2515) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 1fc7cccd2d..1c626eef31 100644 --- a/README.md +++ b/README.md @@ -1439,6 +1439,10 @@ braces: ### Strings +`'single'`, `"double"`, and `'''triple'''` quoted string literals are +supported. Unlike in recipe bodies, `{{…}}` interpolations are not supported +inside strings. + Double-quoted strings support escape sequences: ```just From c62485d0234a9171522d31ca877a24fada4391da Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 10 Dec 2024 13:26:38 -0800 Subject: [PATCH 08/27] Publish docs to GitHub pages on release only (#2516) --- .github/workflows/ci.yaml | 22 +++-------------- .github/workflows/release.yaml | 43 ++++++++++++++++++++++++++++++++++ README.md | 5 +++- 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 13d7132149..1ce47d5660 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,13 +12,13 @@ defaults: run: shell: bash +env: + RUSTFLAGS: --deny warnings + jobs: lint: runs-on: ubuntu-latest - env: - RUSTFLAGS: --deny warnings - steps: - uses: actions/checkout@v4 @@ -43,11 +43,6 @@ jobs: pages: runs-on: ubuntu-latest - permissions: - contents: write - - env: - RUSTFLAGS: --deny warnings steps: - uses: actions/checkout@v4 @@ -72,14 +67,6 @@ jobs: mdbook build book/en mdbook build book/zh - - name: Deploy Pages - uses: peaceiris/actions-gh-pages@v4 - if: github.ref == 'refs/heads/master' - with: - github_token: ${{secrets.GITHUB_TOKEN}} - publish_branch: gh-pages - publish_dir: www - test: strategy: matrix: @@ -90,9 +77,6 @@ jobs: runs-on: ${{matrix.os}} - env: - RUSTFLAGS: --deny warnings - steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 323f6f009f..72c63a90f9 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,6 +9,9 @@ defaults: run: shell: bash +env: + RUSTFLAGS: --deny warnings + jobs: prerelease: runs-on: ubuntu-latest @@ -164,3 +167,43 @@ jobs: prerelease: ${{ needs.prerelease.outputs.value }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + pages: + runs-on: ubuntu-latest + + needs: + - prerelease + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - uses: Swatinem/rust-cache@v2 + + - name: Install `mdbook` + run: cargo install mdbook + + - name: Install `mdbook-linkcheck` + run: | + mkdir -p mdbook-linkcheck + cd mdbook-linkcheck + wget https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/latest/download/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip + unzip mdbook-linkcheck.x86_64-unknown-linux-gnu.zip + chmod +x mdbook-linkcheck + pwd >> $GITHUB_PATH + + - name: Build book + run: | + cargo run --package generate-book + mdbook build book/en + mdbook build book/zh + + - name: Deploy Pages + uses: peaceiris/actions-gh-pages@v4 + if: ${{ needs.prerelease.outputs.value }} + with: + github_token: ${{secrets.GITHUB_TOKEN}} + publish_branch: gh-pages + publish_dir: www diff --git a/README.md b/README.md index 1c626eef31..fa197cac94 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,10 @@ `just` is a handy way to save and run project-specific commands. -This readme is also available as a [book](https://just.systems/man/en/). +This readme is also available as a [book](https://just.systems/man/en/). The +book reflects the latest release, whereas the +[readme on GitHub](https://github.com/casey/just/blob/master/README.md) +reflects latest master. (中文文档在 [这里](https://github.com/casey/just/blob/master/README.中文.md), 快看过来!) From 37dc2e490e765b4a54c182b7b35a67f7e09e2049 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 10 Dec 2024 13:37:46 -0800 Subject: [PATCH 09/27] Release 1.38.0 (#2517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump version: 1.37.0 → 1.38.0 - Update changelog - Update changelog contributor credits - Update dependencies - Update version references in readme --- CHANGELOG.md | 24 ++++++++++++++ Cargo.lock | 92 ++++++++++++++++++++++++++-------------------------- Cargo.toml | 2 +- README.md | 6 ++-- justfile | 4 +++ 5 files changed, 78 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09dd7588f1..7866f14c0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,30 @@ Changelog ========= +[1.38.0](https://github.com/casey/just/releases/tag/1.38.0) - 2024-12-10 +------------------------------------------------------------------------ + +### Added +- Add `[openbsd]` recipe attribute ([#2497](https://github.com/casey/just/pull/2497) by [vtamara](https://github.com/vtamara)) +- Add `[working-directory]` recipe attribute ([#2438](https://github.com/casey/just/pull/2438) by [bcheidemann](https://github.com/bcheidemann)) +- Add `--allow-missing` to ignore missing recipe and submodule errors ([#2460](https://github.com/casey/just/pull/2460) by [R3ZV](https://github.com/R3ZV)) + +### Changed +- Add snap package back to readme ([#2506](https://github.com/casey/just/pull/2506) by [casey](https://github.com/casey)) +- Forbid duplicate non-repeatable attributes ([#2483](https://github.com/casey/just/pull/2483) by [casey](https://github.com/casey)) + +### Misc +- Publish docs to GitHub pages on release only ([#2516](https://github.com/casey/just/pull/2516) by [casey](https://github.com/casey)) +- Note lack of support for string interpolation ([#2515](https://github.com/casey/just/pull/2515) by [casey](https://github.com/casey)) +- Embolden help text errors ([#2502](https://github.com/casey/just/pull/2502) by [casey](https://github.com/casey)) +- Style help text ([#2501](https://github.com/casey/just/pull/2501) by [casey](https://github.com/casey)) +- Add `--request` subcommand for testing ([#2498](https://github.com/casey/just/pull/2498) by [casey](https://github.com/casey)) +- [bin/forbid] Improve error message if ripgrep is missing ([#2493](https://github.com/casey/just/pull/2493) by [casey](https://github.com/casey)) +- Fix Rust 1.83 clippy warnings ([#2487](https://github.com/casey/just/pull/2487) by [casey](https://github.com/casey)) +- Refactor JSON tests ([#2484](https://github.com/casey/just/pull/2484) by [casey](https://github.com/casey)) +- Get `Config` from `ExecutionContext` instead of passing separately ([#2481](https://github.com/casey/just/pull/2481) by [casey](https://github.com/casey)) +- Don't write justfiles unchanged by formatting ([#2479](https://github.com/casey/just/pull/2479) by [casey](https://github.com/casey)) + [1.37.0](https://github.com/casey/just/releases/tag/1.37.0) - 2024-11-20 ------------------------------------------------------------------------ diff --git a/Cargo.lock b/Cargo.lock index 38bbaec345..5bec3df72d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,9 +110,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "blake3" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" dependencies = [ "arrayref", "arrayvec", @@ -163,9 +163,9 @@ checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" [[package]] name = "cc" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ "shlex", ] @@ -184,9 +184,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -198,9 +198,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -208,9 +208,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -242,9 +242,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clap_mangen" @@ -276,9 +276,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -396,12 +396,12 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -412,9 +412,9 @@ checksum = "3ebc5a6d89e3c90b84e8f33c8737933dda8f1c106b5415900b38b9d433841478" [[package]] name = "fastrand" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "generate-book" @@ -506,22 +506,23 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "just" -version = "1.37.0" +version = "1.38.0" dependencies = [ "ansi_term", "blake3", @@ -571,9 +572,9 @@ checksum = "441225017b106b9f902e97947a6d31e44ebcf274b91bdbfb51e5c477fcd468e5" [[package]] name = "libc" -version = "0.2.164" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libredox" @@ -682,9 +683,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -807,15 +808,15 @@ checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" [[package]] name = "rustix" -version = "0.38.41" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -955,9 +956,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -994,9 +995,9 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ "rustix", "windows-sys 0.59.0", @@ -1100,9 +1101,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -1111,13 +1112,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -1126,9 +1126,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1136,9 +1136,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", @@ -1149,9 +1149,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "which" diff --git a/Cargo.toml b/Cargo.toml index f10743b9af..cd922e35ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "just" -version = "1.37.0" +version = "1.38.0" authors = ["Casey Rodarmor "] autotests = false categories = ["command-line-utilities", "development-tools"] diff --git a/README.md b/README.md index fa197cac94..4e0dcf5c94 100644 --- a/README.md +++ b/README.md @@ -910,7 +910,7 @@ $ just foo ``` You can override the working directory for a specific recipe with the -`working-directory` attributemaster: +`working-directory` attribute1.38.0: ```just [working-directory: 'bar'] @@ -1998,14 +1998,14 @@ change their behavior. | `[no-cd]`1.9.0 | recipe | Don't change directory before executing recipe. | | `[no-exit-message]`1.7.0 | recipe | Don't print an error message if recipe fails. | | `[no-quiet]`1.23.0 | recipe | Override globally quiet recipes and always echo out the recipe. | -| `[openbsd]`master | recipe | Enable recipe on OpenBSD. | +| `[openbsd]`1.38.0 | recipe | Enable recipe on OpenBSD. | | `[positional-arguments]`1.29.0 | recipe | Turn on [positional arguments](#positional-arguments) for this recipe. | | `[private]`1.10.0 | alias, recipe | Make recipe, alias, or variable private. See [Private Recipes](#private-recipes). | | `[script]`1.33.0 | recipe | Execute recipe as script. See [script recipes](#script-recipes) for more details. | | `[script(COMMAND)]`1.32.0 | recipe | Execute recipe as a script interpreted by `COMMAND`. See [script recipes](#script-recipes) for more details. | | `[unix]`1.8.0 | recipe | Enable recipe on Unixes. (Includes MacOS). | | `[windows]`1.8.0 | recipe | Enable recipe on Windows. | -| `[working-directory(PATH)]`master | recipe | Set recipe working directory. `PATH` may be relative or absolute. If relative, it is interpreted relative to the default working directory. | +| `[working-directory(PATH)]`1.38.0 | recipe | Set recipe working directory. `PATH` may be relative or absolute. If relative, it is interpreted relative to the default working directory. | A recipe can have multiple attributes, either on multiple lines: diff --git a/justfile b/justfile index e975d506fb..513f319b32 100755 --- a/justfile +++ b/justfile @@ -69,6 +69,10 @@ update-contributors: outdated: cargo outdated -R +[group: 'check'] +unused: + cargo +nightly udeps --workspace + # publish current GitHub master branch [group: 'release'] publish: From 464fdbc1bb07b4bd0f70dd6dfe7d454befd8b81c Mon Sep 17 00:00:00 2001 From: Alexander Demin Date: Tue, 10 Dec 2024 22:52:19 +0000 Subject: [PATCH 10/27] Add `read_to_string(path)` function (#2507) --- README.md | 2 ++ src/function.rs | 6 ++++++ tests/functions.rs | 20 ++++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/README.md b/README.md index 4e0dcf5c94..f076218b45 100644 --- a/README.md +++ b/README.md @@ -1842,6 +1842,8 @@ which will halt execution. - `path_exists(path)` - Returns `true` if the path points at an existing entity and `false` otherwise. Traverses symbolic links, and returns `false` if the path is inaccessible or points to a broken symlink. +- `read_to_string(path)`master - Returns the content of file at + `path` as string. ##### Error Reporting diff --git a/src/function.rs b/src/function.rs index abeae94368..d292955af5 100644 --- a/src/function.rs +++ b/src/function.rs @@ -87,6 +87,7 @@ pub(crate) fn get(name: &str) -> Option { "path_exists" => Unary(path_exists), "prepend" => Binary(prepend), "quote" => Unary(quote), + "read_to_string" => Unary(read_to_string), "replace" => Ternary(replace), "replace_regex" => Ternary(replace_regex), "semver_matches" => Binary(semver_matches), @@ -528,6 +529,11 @@ fn quote(_context: Context, s: &str) -> FunctionResult { Ok(format!("'{}'", s.replace('\'', "'\\''"))) } +fn read_to_string(context: Context, filename: &str) -> FunctionResult { + fs::read_to_string(context.evaluator.context.working_directory().join(filename)) + .map_err(|err| format!("I/O error reading `{filename}`: {err}")) +} + fn replace(_context: Context, s: &str, from: &str, to: &str) -> FunctionResult { Ok(s.replace(from, to)) } diff --git a/tests/functions.rs b/tests/functions.rs index d68b3946b0..e29e04112d 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -1258,3 +1258,23 @@ fn style_unknown() { .status(EXIT_FAILURE) .run(); } + +#[test] +fn read_to_string() { + Test::new() + .justfile("foo := read_to_string('bar')") + .write("bar", "baz") + .args(["--evaluate", "foo"]) + .stdout("baz") + .run(); +} + +#[test] +fn read_to_string_not_found() { + Test::new() + .justfile("foo := read_to_string('bar')") + .args(["--evaluate", "foo"]) + .stderr_regex(r"error: Call to function `read_to_string` failed: I/O error reading `bar`: .*") + .status(EXIT_FAILURE) + .run(); +} From 8b35450c55f1b75321f5c2d7cd7f5c20806f63a7 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 10 Dec 2024 14:59:11 -0800 Subject: [PATCH 11/27] Rename `read_to_string()` to `read()` (#2518) --- README.md | 4 ++-- src/function.rs | 4 ++-- tests/functions.rs | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f076218b45..586ad997fe 100644 --- a/README.md +++ b/README.md @@ -1842,8 +1842,8 @@ which will halt execution. - `path_exists(path)` - Returns `true` if the path points at an existing entity and `false` otherwise. Traverses symbolic links, and returns `false` if the path is inaccessible or points to a broken symlink. -- `read_to_string(path)`master - Returns the content of file at - `path` as string. +- `read(path)`master - Returns the content of file at `path` as + string. ##### Error Reporting diff --git a/src/function.rs b/src/function.rs index d292955af5..104aeb0240 100644 --- a/src/function.rs +++ b/src/function.rs @@ -87,7 +87,7 @@ pub(crate) fn get(name: &str) -> Option { "path_exists" => Unary(path_exists), "prepend" => Binary(prepend), "quote" => Unary(quote), - "read_to_string" => Unary(read_to_string), + "read" => Unary(read), "replace" => Ternary(replace), "replace_regex" => Ternary(replace_regex), "semver_matches" => Binary(semver_matches), @@ -529,7 +529,7 @@ fn quote(_context: Context, s: &str) -> FunctionResult { Ok(format!("'{}'", s.replace('\'', "'\\''"))) } -fn read_to_string(context: Context, filename: &str) -> FunctionResult { +fn read(context: Context, filename: &str) -> FunctionResult { fs::read_to_string(context.evaluator.context.working_directory().join(filename)) .map_err(|err| format!("I/O error reading `{filename}`: {err}")) } diff --git a/tests/functions.rs b/tests/functions.rs index e29e04112d..e4766325e0 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -1260,9 +1260,9 @@ fn style_unknown() { } #[test] -fn read_to_string() { +fn read() { Test::new() - .justfile("foo := read_to_string('bar')") + .justfile("foo := read('bar')") .write("bar", "baz") .args(["--evaluate", "foo"]) .stdout("baz") @@ -1270,11 +1270,11 @@ fn read_to_string() { } #[test] -fn read_to_string_not_found() { +fn read_file_not_found() { Test::new() - .justfile("foo := read_to_string('bar')") + .justfile("foo := read('bar')") .args(["--evaluate", "foo"]) - .stderr_regex(r"error: Call to function `read_to_string` failed: I/O error reading `bar`: .*") + .stderr_regex(r"error: Call to function `read` failed: I/O error reading `bar`: .*") .status(EXIT_FAILURE) .run(); } From 462d381593c70c0e18b7cd0e69993cb4abf769cf Mon Sep 17 00:00:00 2001 From: Greg Shuflin Date: Tue, 10 Dec 2024 15:32:30 -0800 Subject: [PATCH 12/27] Add attribute set (#2419) --- src/alias.rs | 4 +- src/analyzer.rs | 53 +++++++++----------- src/attribute_set.rs | 60 ++++++++++++++++++++++ src/item.rs | 2 +- src/lib.rs | 116 +++++++++++++++++++++++++++++++++---------- src/parser.rs | 51 +++++++------------ src/recipe.rs | 59 +++++++++++----------- 7 files changed, 225 insertions(+), 120 deletions(-) create mode 100644 src/attribute_set.rs diff --git a/src/alias.rs b/src/alias.rs index 5d95849911..18280546fe 100644 --- a/src/alias.rs +++ b/src/alias.rs @@ -3,7 +3,7 @@ use super::*; /// An alias, e.g. `name := target` #[derive(Debug, PartialEq, Clone, Serialize)] pub(crate) struct Alias<'src, T = Rc>> { - pub(crate) attributes: BTreeSet>, + pub(crate) attributes: AttributeSet<'src>, pub(crate) name: Name<'src>, #[serde( bound(serialize = "T: Keyed<'src>"), @@ -26,7 +26,7 @@ impl<'src> Alias<'src, Name<'src>> { impl Alias<'_> { pub(crate) fn is_private(&self) -> bool { - self.name.lexeme().starts_with('_') || self.attributes.contains(&Attribute::Private) + self.name.lexeme().starts_with('_') || self.attributes.contains(AttributeDiscriminant::Private) } } diff --git a/src/analyzer.rs b/src/analyzer.rs index 0929294c5d..ff5dd18754 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -72,17 +72,21 @@ impl<'run, 'src> Analyzer<'run, 'src> { } => { let mut doc_attr: Option<&str> = None; let mut groups = Vec::new(); + attributes.ensure_valid_attributes( + "Module", + **name, + &[AttributeDiscriminant::Doc, AttributeDiscriminant::Group], + )?; + for attribute in attributes { - if let Attribute::Doc(ref doc) = attribute { - doc_attr = Some(doc.as_ref().map(|s| s.cooked.as_ref()).unwrap_or_default()); - } else if let Attribute::Group(ref group) = attribute { - groups.push(group.cooked.clone()); - } else { - return Err(name.token.error(InvalidAttribute { - item_kind: "Module", - item_name: name.lexeme(), - attribute: attribute.clone(), - })); + match attribute { + Attribute::Doc(ref doc) => { + doc_attr = Some(doc.as_ref().map(|s| s.cooked.as_ref()).unwrap_or_default()); + } + Attribute::Group(ref group) => { + groups.push(group.cooked.clone()); + } + _ => unreachable!(), } } @@ -170,11 +174,9 @@ impl<'run, 'src> Analyzer<'run, 'src> { } for recipe in recipes.values() { - for attribute in &recipe.attributes { - if let Attribute::Script(_) = attribute { - unstable_features.insert(UnstableFeature::ScriptAttribute); - break; - } + if recipe.attributes.contains(AttributeDiscriminant::Script) { + unstable_features.insert(UnstableFeature::ScriptAttribute); + break; } } @@ -284,11 +286,7 @@ impl<'run, 'src> Analyzer<'run, 'src> { } if !recipe.is_script() { - if let Some(attribute) = recipe - .attributes - .iter() - .find(|attribute| matches!(attribute, Attribute::Extension(_))) - { + if let Some(attribute) = recipe.attributes.get(AttributeDiscriminant::Extension) { return Err(recipe.name.error(InvalidAttribute { item_kind: "Recipe", item_name: recipe.name.lexeme(), @@ -301,16 +299,11 @@ impl<'run, 'src> Analyzer<'run, 'src> { } fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src> { - for attribute in &alias.attributes { - if *attribute != Attribute::Private { - return Err(alias.name.token.error(InvalidAttribute { - item_kind: "Alias", - item_name: alias.name.lexeme(), - attribute: attribute.clone(), - })); - } - } - + alias.attributes.ensure_valid_attributes( + "Alias", + *alias.name, + &[AttributeDiscriminant::Private], + )?; Ok(()) } diff --git a/src/attribute_set.rs b/src/attribute_set.rs new file mode 100644 index 0000000000..49d10d2e82 --- /dev/null +++ b/src/attribute_set.rs @@ -0,0 +1,60 @@ +use {super::*, std::collections}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize)] +pub(crate) struct AttributeSet<'src>(BTreeSet>); + +impl<'src> AttributeSet<'src> { + pub(crate) fn len(&self) -> usize { + self.0.len() + } + + pub(crate) fn contains(&self, target: AttributeDiscriminant) -> bool { + self.0.iter().any(|attr| attr.discriminant() == target) + } + + pub(crate) fn get(&self, discriminant: AttributeDiscriminant) -> Option<&Attribute<'src>> { + self + .0 + .iter() + .find(|attr| discriminant == attr.discriminant()) + } + + pub(crate) fn iter<'a>(&'a self) -> collections::btree_set::Iter<'a, Attribute<'src>> { + self.0.iter() + } + + pub(crate) fn ensure_valid_attributes( + &self, + item_kind: &'static str, + item_token: Token<'src>, + valid: &[AttributeDiscriminant], + ) -> Result<(), CompileError<'src>> { + for attribute in &self.0 { + let discriminant = attribute.discriminant(); + if !valid.contains(&discriminant) { + return Err(item_token.error(CompileErrorKind::InvalidAttribute { + item_kind, + item_name: item_token.lexeme(), + attribute: attribute.clone(), + })); + } + } + Ok(()) + } +} + +impl<'src> FromIterator> for AttributeSet<'src> { + fn from_iter>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl<'src, 'a> IntoIterator for &'a AttributeSet<'src> { + type Item = &'a Attribute<'src>; + + type IntoIter = collections::btree_set::Iter<'a, Attribute<'src>>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} diff --git a/src/item.rs b/src/item.rs index 875951f098..60e2831461 100644 --- a/src/item.rs +++ b/src/item.rs @@ -13,7 +13,7 @@ pub(crate) enum Item<'src> { relative: StringLiteral<'src>, }, Module { - attributes: BTreeSet>, + attributes: AttributeSet<'src>, absolute: Option, doc: Option<&'src str>, name: Name<'src>, diff --git a/src/lib.rs b/src/lib.rs index cda797c7c3..7ce669ea1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,31 +6,96 @@ pub(crate) use { crate::{ - alias::Alias, analyzer::Analyzer, argument_parser::ArgumentParser, assignment::Assignment, - assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding, - color::Color, color_display::ColorDisplay, command_color::CommandColor, - command_ext::CommandExt, compilation::Compilation, compile_error::CompileError, - compile_error_kind::CompileErrorKind, compiler::Compiler, condition::Condition, - conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError, - constants::constants, count::Count, delimiter::Delimiter, dependency::Dependency, - dump_format::DumpFormat, enclosure::Enclosure, error::Error, evaluator::Evaluator, - execution_context::ExecutionContext, executor::Executor, expression::Expression, - fragment::Fragment, function::Function, interpreter::Interpreter, - interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item, - justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List, - load_dotenv::load_dotenv, loader::Loader, module_path::ModulePath, name::Name, - namepath::Namepath, ordinal::Ordinal, output::output, output_error::OutputError, - parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform, - platform_interface::PlatformInterface, position::Position, positional::Positional, ran::Ran, - range_ext::RangeExt, recipe::Recipe, recipe_resolver::RecipeResolver, - recipe_signature::RecipeSignature, scope::Scope, search::Search, search_config::SearchConfig, - search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang, - show_whitespace::ShowWhitespace, source::Source, string_delimiter::StringDelimiter, - string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand, - suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind, - unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe, - unstable_feature::UnstableFeature, use_color::UseColor, variables::Variables, - verbosity::Verbosity, warning::Warning, + alias::Alias, + analyzer::Analyzer, + argument_parser::ArgumentParser, + assignment::Assignment, + assignment_resolver::AssignmentResolver, + ast::Ast, + attribute::{Attribute, AttributeDiscriminant}, + attribute_set::AttributeSet, + binding::Binding, + color::Color, + color_display::ColorDisplay, + command_color::CommandColor, + command_ext::CommandExt, + compilation::Compilation, + compile_error::CompileError, + compile_error_kind::CompileErrorKind, + compiler::Compiler, + condition::Condition, + conditional_operator::ConditionalOperator, + config::Config, + config_error::ConfigError, + constants::constants, + count::Count, + delimiter::Delimiter, + dependency::Dependency, + dump_format::DumpFormat, + enclosure::Enclosure, + error::Error, + evaluator::Evaluator, + execution_context::ExecutionContext, + executor::Executor, + expression::Expression, + fragment::Fragment, + function::Function, + interpreter::Interpreter, + interrupt_guard::InterruptGuard, + interrupt_handler::InterruptHandler, + item::Item, + justfile::Justfile, + keyed::Keyed, + keyword::Keyword, + lexer::Lexer, + line::Line, + list::List, + load_dotenv::load_dotenv, + loader::Loader, + module_path::ModulePath, + name::Name, + namepath::Namepath, + ordinal::Ordinal, + output::output, + output_error::OutputError, + parameter::Parameter, + parameter_kind::ParameterKind, + parser::Parser, + platform::Platform, + platform_interface::PlatformInterface, + position::Position, + positional::Positional, + ran::Ran, + range_ext::RangeExt, + recipe::Recipe, + recipe_resolver::RecipeResolver, + recipe_signature::RecipeSignature, + scope::Scope, + search::Search, + search_config::SearchConfig, + search_error::SearchError, + set::Set, + setting::Setting, + settings::Settings, + shebang::Shebang, + show_whitespace::ShowWhitespace, + source::Source, + string_delimiter::StringDelimiter, + string_kind::StringKind, + string_literal::StringLiteral, + subcommand::Subcommand, + suggestion::Suggestion, + table::Table, + thunk::Thunk, + token::Token, + token_kind::TokenKind, + unresolved_dependency::UnresolvedDependency, + unresolved_recipe::UnresolvedRecipe, + unstable_feature::UnstableFeature, + use_color::UseColor, + variables::Variables, + verbosity::Verbosity, + warning::Warning, }, camino::Utf8Path, clap::ValueEnum, @@ -120,6 +185,7 @@ mod assignment; mod assignment_resolver; mod ast; mod attribute; +mod attribute_set; mod binding; mod color; mod color_display; diff --git a/src/parser.rs b/src/parser.rs index 32a5575159..5903e5b492 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -462,7 +462,7 @@ impl<'run, 'src> Parser<'run, 'src> { /// Parse an alias, e.g `alias name := target` fn parse_alias( &mut self, - attributes: BTreeSet>, + attributes: AttributeSet<'src>, ) -> CompileResult<'src, Alias<'src, Name<'src>>> { self.presume_keyword(Keyword::Alias)?; let name = self.parse_name()?; @@ -480,24 +480,16 @@ impl<'run, 'src> Parser<'run, 'src> { fn parse_assignment( &mut self, export: bool, - attributes: BTreeSet>, + attributes: AttributeSet<'src>, ) -> CompileResult<'src, Assignment<'src>> { let name = self.parse_name()?; self.presume(ColonEquals)?; let value = self.parse_expression()?; self.expect_eol()?; - let private = attributes.contains(&Attribute::Private); + let private = attributes.contains(AttributeDiscriminant::Private); - for attribute in attributes { - if attribute != Attribute::Private { - return Err(name.error(CompileErrorKind::InvalidAttribute { - item_kind: "Assignment", - item_name: name.lexeme(), - attribute, - })); - } - } + attributes.ensure_valid_attributes("Assignment", *name, &[AttributeDiscriminant::Private])?; Ok(Assignment { constant: false, @@ -863,7 +855,7 @@ impl<'run, 'src> Parser<'run, 'src> { &mut self, doc: Option<&'src str>, quiet: bool, - attributes: BTreeSet>, + attributes: AttributeSet<'src>, ) -> CompileResult<'src, UnresolvedRecipe<'src>> { let name = self.parse_name()?; @@ -924,9 +916,7 @@ impl<'run, 'src> Parser<'run, 'src> { let body = self.parse_body()?; let shebang = body.first().map_or(false, Line::is_shebang); - let script = attributes - .iter() - .any(|attribute| matches!(attribute, Attribute::Script(_))); + let script = attributes.contains(AttributeDiscriminant::Script); if shebang && script { return Err(name.error(CompileErrorKind::ShebangAndScriptAttribute { @@ -934,23 +924,18 @@ impl<'run, 'src> Parser<'run, 'src> { })); } - let working_directory = attributes - .iter() - .any(|attribute| matches!(attribute, Attribute::WorkingDirectory(_))); - - if working_directory { - for attribute in &attributes { - if let Attribute::NoCd = attribute { - return Err( - name.error(CompileErrorKind::NoCdAndWorkingDirectoryAttribute { - recipe: name.lexeme(), - }), - ); - } - } + let working_directory = attributes.contains(AttributeDiscriminant::WorkingDirectory); + + if working_directory && attributes.contains(AttributeDiscriminant::NoCd) { + return Err( + name.error(CompileErrorKind::NoCdAndWorkingDirectoryAttribute { + recipe: name.lexeme(), + }), + ); } - let private = name.lexeme().starts_with('_') || attributes.contains(&Attribute::Private); + let private = + name.lexeme().starts_with('_') || attributes.contains(AttributeDiscriminant::Private); let mut doc = doc.map(ToOwned::to_owned); @@ -1138,9 +1123,7 @@ impl<'run, 'src> Parser<'run, 'src> { } /// Item attributes, i.e., `[macos]` or `[confirm: "warning!"]` - fn parse_attributes( - &mut self, - ) -> CompileResult<'src, Option<(Token<'src>, BTreeSet>)>> { + fn parse_attributes(&mut self) -> CompileResult<'src, Option<(Token<'src>, AttributeSet<'src>)>> { let mut attributes = BTreeMap::new(); let mut discriminants = BTreeMap::new(); diff --git a/src/recipe.rs b/src/recipe.rs index ac7fdd6596..d53a44bb40 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -19,7 +19,7 @@ fn error_from_signal(recipe: &str, line_number: Option, exit_status: Exit /// A recipe, e.g. `foo: bar baz` #[derive(PartialEq, Debug, Clone, Serialize)] pub(crate) struct Recipe<'src, D = Dependency<'src>> { - pub(crate) attributes: BTreeSet>, + pub(crate) attributes: AttributeSet<'src>, pub(crate) body: Vec>, pub(crate) dependencies: Vec, pub(crate) doc: Option, @@ -66,20 +66,20 @@ impl<'src, D> Recipe<'src, D> { } pub(crate) fn confirm(&self) -> RunResult<'src, bool> { - for attribute in &self.attributes { - if let Attribute::Confirm(prompt) = attribute { - if let Some(prompt) = prompt { - eprint!("{} ", prompt.cooked); - } else { - eprint!("Run recipe `{}`? ", self.name); - } - let mut line = String::new(); - std::io::stdin() - .read_line(&mut line) - .map_err(|io_error| Error::GetConfirmation { io_error })?; - let line = line.trim().to_lowercase(); - return Ok(line == "y" || line == "yes"); + if let Some(Attribute::Confirm(ref prompt)) = + self.attributes.get(AttributeDiscriminant::Confirm) + { + if let Some(prompt) = prompt { + eprint!("{} ", prompt.cooked); + } else { + eprint!("Run recipe `{}`? ", self.name); } + let mut line = String::new(); + std::io::stdin() + .read_line(&mut line) + .map_err(|io_error| Error::GetConfirmation { io_error })?; + let line = line.trim().to_lowercase(); + return Ok(line == "y" || line == "yes"); } Ok(true) } @@ -97,7 +97,7 @@ impl<'src, D> Recipe<'src, D> { } pub(crate) fn is_public(&self) -> bool { - !self.private && !self.attributes.contains(&Attribute::Private) + !self.private && !self.attributes.contains(AttributeDiscriminant::Private) } pub(crate) fn is_script(&self) -> bool { @@ -105,19 +105,22 @@ impl<'src, D> Recipe<'src, D> { } pub(crate) fn takes_positional_arguments(&self, settings: &Settings) -> bool { - settings.positional_arguments || self.attributes.contains(&Attribute::PositionalArguments) + settings.positional_arguments + || self + .attributes + .contains(AttributeDiscriminant::PositionalArguments) } pub(crate) fn change_directory(&self) -> bool { - !self.attributes.contains(&Attribute::NoCd) + !self.attributes.contains(AttributeDiscriminant::NoCd) } pub(crate) fn enabled(&self) -> bool { - let linux = self.attributes.contains(&Attribute::Linux); - let macos = self.attributes.contains(&Attribute::Macos); - let openbsd = self.attributes.contains(&Attribute::Openbsd); - let unix = self.attributes.contains(&Attribute::Unix); - let windows = self.attributes.contains(&Attribute::Windows); + let linux = self.attributes.contains(AttributeDiscriminant::Linux); + let macos = self.attributes.contains(AttributeDiscriminant::Macos); + let openbsd = self.attributes.contains(AttributeDiscriminant::Openbsd); + let unix = self.attributes.contains(AttributeDiscriminant::Unix); + let windows = self.attributes.contains(AttributeDiscriminant::Windows); (!windows && !linux && !macos && !openbsd && !unix) || (cfg!(target_os = "linux") && (linux || unix)) @@ -129,7 +132,9 @@ impl<'src, D> Recipe<'src, D> { } fn print_exit_message(&self) -> bool { - !self.attributes.contains(&Attribute::NoExitMessage) + !self + .attributes + .contains(AttributeDiscriminant::NoExitMessage) } fn working_directory<'a>(&'a self, context: &'a ExecutionContext) -> Option { @@ -149,7 +154,7 @@ impl<'src, D> Recipe<'src, D> { } fn no_quiet(&self) -> bool { - self.attributes.contains(&Attribute::NoQuiet) + self.attributes.contains(AttributeDiscriminant::NoQuiet) } pub(crate) fn run<'run>( @@ -351,10 +356,8 @@ impl<'src, D> Recipe<'src, D> { return Ok(()); } - let executor = if let Some(Attribute::Script(interpreter)) = self - .attributes - .iter() - .find(|attribute| matches!(attribute, Attribute::Script(_))) + let executor = if let Some(Attribute::Script(interpreter)) = + self.attributes.get(AttributeDiscriminant::Script) { Executor::Command( interpreter From 5c691a8a57c787a6eca86445bd7df88d34f374a0 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Wed, 11 Dec 2024 00:43:14 +0100 Subject: [PATCH 13/27] Handle recipes in submodules in fish completion script (#2514) --- src/completions.rs | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/src/completions.rs b/src/completions.rs index 093011906b..7d137acf9e 100644 --- a/src/completions.rs +++ b/src/completions.rs @@ -97,33 +97,7 @@ const FISH_RECIPE_COMPLETIONS: &str = r#"function __fish_just_complete_recipes if string match -rq '(-f|--justfile)\s*=?(?[^\s]+)' -- (string split -- ' -- ' (commandline -pc))[1] set -fx JUST_JUSTFILE "$justfile" end - just --list 2> /dev/null | tail -n +2 | awk '{ - command = $1; - args = $0; - desc = ""; - delim = ""; - sub(/^[[:space:]]*[^[:space:]]*/, "", args); - gsub(/^[[:space:]]+|[[:space:]]+$/, "", args); - - if (match(args, /#.*/)) { - desc = substr(args, RSTART+2, RLENGTH); - args = substr(args, 0, RSTART-1); - gsub(/^[[:space:]]+|[[:space:]]+$/, "", args); - } - - gsub(/\+|=[`\'"][^`\'"]*[`\'"]/, "", args); - gsub(/ /, ",", args); - - if (args != ""){ - args = "Args: " args; - } - - if (args != "" && desc != "") { - delim = "; "; - } - - print command "\t" args delim desc - }' + printf "%s\n" (string split " " (just --summary)) end # don't suggest files right off From 5393105beffda00093a9fab1e292958d18b91b1b Mon Sep 17 00:00:00 2001 From: Sibi Prabakaran Date: Wed, 11 Dec 2024 06:01:53 +0530 Subject: [PATCH 14/27] Include source path in dump JSON (#2466) --- src/analyzer.rs | 3 ++- src/function.rs | 55 +++++++++++++------------------------------------ src/justfile.rs | 1 - tests/json.rs | 31 +++++++++++++++++++++++----- 4 files changed, 42 insertions(+), 48 deletions(-) diff --git a/src/analyzer.rs b/src/analyzer.rs index ff5dd18754..1087bdeec9 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -184,6 +184,7 @@ impl<'run, 'src> Analyzer<'run, 'src> { unstable_features.insert(UnstableFeature::ScriptInterpreterSetting); } + let source = root.to_owned(); let root = paths.get(root).unwrap(); Ok(Justfile { @@ -207,7 +208,7 @@ impl<'run, 'src> Analyzer<'run, 'src> { name, recipes, settings, - source: root.into(), + source, unexports: self.unexports, unstable_features, warnings: self.warnings, diff --git a/src/function.rs b/src/function.rs index 104aeb0240..e5e93632e7 100644 --- a/src/function.rs +++ b/src/function.rs @@ -447,50 +447,23 @@ fn lowercase(_context: Context, s: &str) -> FunctionResult { } fn module_directory(context: Context) -> FunctionResult { - context - .evaluator - .context - .search - .justfile - .parent() - .unwrap() - .join(&context.evaluator.context.module.source) - .parent() - .unwrap() - .to_str() - .map(str::to_owned) - .ok_or_else(|| { - format!( - "Module directory is not valid unicode: {}", - context - .evaluator - .context - .module - .source - .parent() - .unwrap() - .display(), - ) - }) + let module_directory = context.evaluator.context.module.source.parent().unwrap(); + module_directory.to_str().map(str::to_owned).ok_or_else(|| { + format!( + "Module directory is not valid unicode: {}", + module_directory.display(), + ) + }) } fn module_file(context: Context) -> FunctionResult { - context - .evaluator - .context - .search - .justfile - .parent() - .unwrap() - .join(&context.evaluator.context.module.source) - .to_str() - .map(str::to_owned) - .ok_or_else(|| { - format!( - "Module file path is not valid unicode: {}", - context.evaluator.context.module.source.display(), - ) - }) + let module_file = &context.evaluator.context.module.source; + module_file.to_str().map(str::to_owned).ok_or_else(|| { + format!( + "Module file path is not valid unicode: {}", + module_file.display(), + ) + }) } fn num_cpus(_context: Context) -> FunctionResult { diff --git a/src/justfile.rs b/src/justfile.rs index 4af175c1ed..2ce96ece40 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -23,7 +23,6 @@ pub(crate) struct Justfile<'src> { pub(crate) name: Option>, pub(crate) recipes: Table<'src, Rc>>, pub(crate) settings: Settings<'src>, - #[serde(skip)] pub(crate) source: PathBuf, pub(crate) unexports: HashSet, #[serde(skip)] diff --git a/tests/json.rs b/tests/json.rs index a0fa9f6ee8..4e740e52e7 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -31,7 +31,7 @@ struct Interpreter<'a> { command: &'a str, } -#[derive(Debug, Default, PartialEq, Serialize)] +#[derive(Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] struct Module<'a> { aliases: BTreeMap<&'a str, Alias<'a>>, @@ -44,6 +44,7 @@ struct Module<'a> { settings: Settings<'a>, unexports: Vec<&'a str>, warnings: Vec<&'a str>, + source: PathBuf, } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] @@ -98,8 +99,26 @@ fn case(justfile: &str, expected: Module) { case_with_submodule(justfile, None, expected); } +fn fix_source(dir: &Path, module: &mut Module) { + let filename = if module.source.as_os_str().is_empty() { + Path::new("justfile") + } else { + &module.source + }; + + module.source = if cfg!(target_os = "macos") { + dir.canonicalize().unwrap().join(filename) + } else { + dir.join(filename) + }; + + for module in module.modules.values_mut() { + fix_source(dir, module); + } +} + #[track_caller] -fn case_with_submodule(justfile: &str, submodule: Option<(&str, &str)>, expected: Module) { +fn case_with_submodule(justfile: &str, submodule: Option<(&str, &str)>, mut expected: Module) { let mut test = Test::new() .justfile(justfile) .args(["--dump", "--dump-format", "json"]) @@ -109,11 +128,11 @@ fn case_with_submodule(justfile: &str, submodule: Option<(&str, &str)>, expected test = test.write(path, source); } - let actual = test.run().stdout; + fix_source(test.tempdir.path(), &mut expected); - let mut expected = serde_json::to_string(&expected).unwrap(); - expected.push('\n'); + let actual = test.run().stdout; + let actual: Module = serde_json::from_str(actual.as_str()).unwrap(); pretty_assertions::assert_eq!(actual, expected); } @@ -776,6 +795,7 @@ fn module() { Module { doc: Some("hello"), first: Some("bar"), + source: "foo.just".into(), recipes: [( "bar", Recipe { @@ -808,6 +828,7 @@ fn module_group() { Module { first: Some("bar"), groups: ["alpha"].into(), + source: "foo.just".into(), recipes: [( "bar", Recipe { From 53f8619e36b6e9460ca3e6bf178c9cb2fe724686 Mon Sep 17 00:00:00 2001 From: laniakea64 Date: Tue, 10 Dec 2024 21:23:48 -0500 Subject: [PATCH 15/27] Add regex mismatch conditional operator (#2490) --- src/compile_error.rs | 4 +++- src/compile_error_kind.rs | 2 +- src/conditional_operator.rs | 3 +++ src/evaluator.rs | 3 +++ src/lexer.rs | 32 ++++++++++++++++++++++++++------ src/parser.rs | 2 ++ src/summary.rs | 2 ++ src/token_kind.rs | 2 ++ tests/conditional.rs | 2 +- tests/parser.rs | 21 +++++++++++++++++++++ tests/regexes.rs | 25 +++++++++++++++++++++++++ 11 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/compile_error.rs b/src/compile_error.rs index 7fa2e0a3cb..02e29af213 100644 --- a/src/compile_error.rs +++ b/src/compile_error.rs @@ -246,7 +246,9 @@ impl Display for CompileError<'_> { "Non-default parameter `{parameter}` follows default parameter" ), UndefinedVariable { variable } => write!(f, "Variable `{variable}` not defined"), - UnexpectedCharacter { expected } => write!(f, "Expected character `{expected}`"), + UnexpectedCharacter { expected } => { + write!(f, "Expected character {}", List::or_ticked(expected.iter())) + } UnexpectedClosingDelimiter { close } => { write!(f, "Unexpected closing delimiter `{}`", close.close()) } diff --git a/src/compile_error_kind.rs b/src/compile_error_kind.rs index bc013b9025..91c5cd071b 100644 --- a/src/compile_error_kind.rs +++ b/src/compile_error_kind.rs @@ -107,7 +107,7 @@ pub(crate) enum CompileErrorKind<'src> { variable: &'src str, }, UnexpectedCharacter { - expected: char, + expected: Vec, }, UnexpectedClosingDelimiter { close: Delimiter, diff --git a/src/conditional_operator.rs b/src/conditional_operator.rs index bb297c22ab..87832a036f 100644 --- a/src/conditional_operator.rs +++ b/src/conditional_operator.rs @@ -9,6 +9,8 @@ pub(crate) enum ConditionalOperator { Inequality, /// `=~` RegexMatch, + /// `!~` + RegexMismatch, } impl Display for ConditionalOperator { @@ -17,6 +19,7 @@ impl Display for ConditionalOperator { Self::Equality => write!(f, "=="), Self::Inequality => write!(f, "!="), Self::RegexMatch => write!(f, "=~"), + Self::RegexMismatch => write!(f, "!~"), } } } diff --git a/src/evaluator.rs b/src/evaluator.rs index b83c8cf70c..46955980f4 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -245,6 +245,9 @@ impl<'src, 'run> Evaluator<'src, 'run> { ConditionalOperator::RegexMatch => Regex::new(&rhs_value) .map_err(|source| Error::RegexCompile { source })? .is_match(&lhs_value), + ConditionalOperator::RegexMismatch => !Regex::new(&rhs_value) + .map_err(|source| Error::RegexCompile { source })? + .is_match(&lhs_value), }; Ok(condition) } diff --git a/src/lexer.rs b/src/lexer.rs index 2c56db9dcf..0e7f76c67e 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -475,7 +475,7 @@ impl<'src> Lexer<'src> { match start { ' ' | '\t' => self.lex_whitespace(), '!' if self.rest().starts_with("!include") => Err(self.error(Include)), - '!' => self.lex_digraph('!', '=', BangEquals), + '!' => self.lex_choices('!', &[('=', BangEquals), ('~', BangTilde)], None), '#' => self.lex_comment(), '$' => self.lex_single(Dollar), '&' => self.lex_digraph('&', '&', AmpersandAmpersand), @@ -486,7 +486,11 @@ impl<'src> Lexer<'src> { ',' => self.lex_single(Comma), '/' => self.lex_single(Slash), ':' => self.lex_colon(), - '=' => self.lex_choices('=', &[('=', EqualsEquals), ('~', EqualsTilde)], Equals), + '=' => self.lex_choices( + '=', + &[('=', EqualsEquals), ('~', EqualsTilde)], + Some(Equals), + ), '?' => self.lex_single(QuestionMark), '@' => self.lex_single(At), '[' => self.lex_delimiter(BracketL), @@ -618,7 +622,7 @@ impl<'src> Lexer<'src> { &mut self, first: char, choices: &[(char, TokenKind)], - otherwise: TokenKind, + otherwise: Option, ) -> CompileResult<'src> { self.presume(first)?; @@ -629,7 +633,20 @@ impl<'src> Lexer<'src> { } } - self.token(otherwise); + if let Some(token) = otherwise { + self.token(token); + } else { + // Emit an unspecified token to consume the current character, + self.token(Unspecified); + + // …and advance past another character, + self.advance()?; + + // …so that the error we produce highlights the unexpected character. + return Err(self.error(UnexpectedCharacter { + expected: choices.iter().map(|choice| choice.0).collect(), + })); + } Ok(()) } @@ -700,7 +717,9 @@ impl<'src> Lexer<'src> { self.advance()?; // …so that the error we produce highlights the unexpected character. - Err(self.error(UnexpectedCharacter { expected: right })) + Err(self.error(UnexpectedCharacter { + expected: vec![right], + })) } } @@ -949,6 +968,7 @@ mod tests { Asterisk => "*", At => "@", BangEquals => "!=", + BangTilde => "!~", BarBar => "||", BraceL => "{", BraceR => "}", @@ -2272,7 +2292,7 @@ mod tests { column: 1, width: 1, kind: UnexpectedCharacter { - expected: '&', + expected: vec!['&'], }, } diff --git a/src/parser.rs b/src/parser.rs index 5903e5b492..5d7821a335 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -606,6 +606,8 @@ impl<'run, 'src> Parser<'run, 'src> { ConditionalOperator::Inequality } else if self.accepted(EqualsTilde)? { ConditionalOperator::RegexMatch + } else if self.accepted(BangTilde)? { + ConditionalOperator::RegexMismatch } else { self.expect(EqualsEquals)?; ConditionalOperator::Equality diff --git a/src/summary.rs b/src/summary.rs index 76483d63ec..79adaba4df 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -360,6 +360,7 @@ pub enum ConditionalOperator { Equality, Inequality, RegexMatch, + RegexMismatch, } impl ConditionalOperator { @@ -368,6 +369,7 @@ impl ConditionalOperator { full::ConditionalOperator::Equality => Self::Equality, full::ConditionalOperator::Inequality => Self::Inequality, full::ConditionalOperator::RegexMatch => Self::RegexMatch, + full::ConditionalOperator::RegexMismatch => Self::RegexMismatch, } } } diff --git a/src/token_kind.rs b/src/token_kind.rs index 850afa9629..be8af19f5f 100644 --- a/src/token_kind.rs +++ b/src/token_kind.rs @@ -7,6 +7,7 @@ pub(crate) enum TokenKind { At, Backtick, BangEquals, + BangTilde, BarBar, BraceL, BraceR, @@ -51,6 +52,7 @@ impl Display for TokenKind { At => "'@'", Backtick => "backtick", BangEquals => "'!='", + BangTilde => "'!~'", BarBar => "'||'", BraceL => "'{'", BraceR => "'}'", diff --git a/tests/conditional.rs b/tests/conditional.rs index 4eab2f4d72..8eae1351d6 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/parser.rs b/tests/parser.rs index 307f1aea5a..3512991ca0 100644 --- a/tests/parser.rs +++ b/tests/parser.rs @@ -11,3 +11,24 @@ fn dont_run_duplicate_recipes() { ) .run(); } + +#[test] +fn invalid_bang_operator() { + Test::new() + .justfile( + " + x := if '' !! '' { '' } else { '' } + ", + ) + .status(1) + .stderr( + r" +error: Expected character `=` or `~` + ——▶ justfile:1:13 + │ +1 │ x := if '' !! '' { '' } else { '' } + │ ^ +", + ) + .run(); +} diff --git a/tests/regexes.rs b/tests/regexes.rs index 7a53a0af5c..8194381efb 100644 --- a/tests/regexes.rs +++ b/tests/regexes.rs @@ -64,3 +64,28 @@ fn bad_regex_fails_at_runtime() { .status(EXIT_FAILURE) .run(); } + +#[test] +fn mismatch() { + Test::new() + .justfile( + " + foo := if 'Foo' !~ '^ab+c' { + 'mismatch' + } else { + 'match' + } + + bar := if 'Foo' !~ 'Foo' { + 'mismatch' + } else { + 'match' + } + + @default: + echo {{ foo }} {{ bar }} + ", + ) + .stdout("mismatch match\n") + .run(); +} From 5642b1414253e131cc485c7bba3744ec410e8b3f Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 11 Dec 2024 12:49:51 -0800 Subject: [PATCH 16/27] Fix error when lexing `!` at end-of-file (#2520) --- src/compile_error.rs | 8 ++++++-- src/compile_error_kind.rs | 2 +- src/lexer.rs | 29 ++++++++++++++++++++++++----- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/compile_error.rs b/src/compile_error.rs index 02e29af213..593f4f5912 100644 --- a/src/compile_error.rs +++ b/src/compile_error.rs @@ -247,13 +247,17 @@ impl Display for CompileError<'_> { ), UndefinedVariable { variable } => write!(f, "Variable `{variable}` not defined"), UnexpectedCharacter { expected } => { - write!(f, "Expected character {}", List::or_ticked(expected.iter())) + write!(f, "Expected character {}", List::or_ticked(expected)) } UnexpectedClosingDelimiter { close } => { write!(f, "Unexpected closing delimiter `{}`", close.close()) } UnexpectedEndOfToken { expected } => { - write!(f, "Expected character `{expected}` but found end-of-file") + write!( + f, + "Expected character `{}` but found end-of-file", + List::or_ticked(expected), + ) } UnexpectedToken { ref expected, diff --git a/src/compile_error_kind.rs b/src/compile_error_kind.rs index 91c5cd071b..09e2eb337c 100644 --- a/src/compile_error_kind.rs +++ b/src/compile_error_kind.rs @@ -113,7 +113,7 @@ pub(crate) enum CompileErrorKind<'src> { close: Delimiter, }, UnexpectedEndOfToken { - expected: char, + expected: Vec, }, UnexpectedToken { expected: Vec, diff --git a/src/lexer.rs b/src/lexer.rs index 0e7f76c67e..afef81c21e 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -639,13 +639,17 @@ impl<'src> Lexer<'src> { // Emit an unspecified token to consume the current character, self.token(Unspecified); + let expected = choices.iter().map(|choice| choice.0).collect(); + + if self.at_eof() { + return Err(self.error(UnexpectedEndOfToken { expected })); + } + // …and advance past another character, self.advance()?; // …so that the error we produce highlights the unexpected character. - return Err(self.error(UnexpectedCharacter { - expected: choices.iter().map(|choice| choice.0).collect(), - })); + return Err(self.error(UnexpectedCharacter { expected })); } Ok(()) @@ -710,7 +714,9 @@ impl<'src> Lexer<'src> { self.token(Unspecified); if self.at_eof() { - return Err(self.error(UnexpectedEndOfToken { expected: right })); + return Err(self.error(UnexpectedEndOfToken { + expected: vec![right], + })); } // …and advance past another character, @@ -2281,9 +2287,10 @@ mod tests { column: 1, width: 0, kind: UnexpectedEndOfToken { - expected: '&', + expected: vec!['&'], }, } + error! { name: ampersand_unexpected, input: "&%", @@ -2296,6 +2303,18 @@ mod tests { }, } + error! { + name: bang_eof, + input: "!", + offset: 1, + line: 0, + column: 1, + width: 0, + kind: UnexpectedEndOfToken { + expected: vec!['=', '~'], + }, + } + #[test] fn presume_error() { let compile_error = Lexer::new("justfile".as_ref(), "!") From a5d884ed10e9d221a142b982f00770e596654037 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 11 Dec 2024 15:24:42 -0800 Subject: [PATCH 17/27] Fix truncated bang operator error message (#2522) --- src/compile_error.rs | 2 +- tests/parser.rs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/compile_error.rs b/src/compile_error.rs index 593f4f5912..ce53c12f70 100644 --- a/src/compile_error.rs +++ b/src/compile_error.rs @@ -255,7 +255,7 @@ impl Display for CompileError<'_> { UnexpectedEndOfToken { expected } => { write!( f, - "Expected character `{}` but found end-of-file", + "Expected character {} but found end-of-file", List::or_ticked(expected), ) } diff --git a/tests/parser.rs b/tests/parser.rs index 3512991ca0..32a57a9216 100644 --- a/tests/parser.rs +++ b/tests/parser.rs @@ -32,3 +32,20 @@ error: Expected character `=` or `~` ) .run(); } + +#[test] +fn truncated_bang_operator() { + Test::new() + .justfile("x := if '' !") + .status(1) + .stderr( + r" +error: Expected character `=` or `~` but found end-of-file + ——▶ justfile:1:13 + │ +1 │ x := if '' ! + │ ^ +", + ) + .run(); +} From 3ec4245bf89e60af647f71e1550cb64e17236d27 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 12 Dec 2024 12:52:19 -0800 Subject: [PATCH 18/27] Sort functions alphabetically (#2525) --- src/function.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/function.rs b/src/function.rs index e5e93632e7..66e7c6e2dd 100644 --- a/src/function.rs +++ b/src/function.rs @@ -263,6 +263,13 @@ fn encode_uri_component(_context: Context, s: &str) -> FunctionResult { Ok(percent_encoding::utf8_percent_encode(s, &PERCENT_ENCODE).to_string()) } +fn env(context: Context, key: &str, default: Option<&str>) -> FunctionResult { + match default { + Some(val) => env_var_or_default(context, key, val), + None => env_var(context, key), + } +} + fn env_var(context: Context, key: &str) -> FunctionResult { use std::env::VarError::*; @@ -295,13 +302,6 @@ fn env_var_or_default(context: Context, key: &str, default: &str) -> FunctionRes } } -fn env(context: Context, key: &str, default: Option<&str>) -> FunctionResult { - match default { - Some(val) => env_var_or_default(context, key, val), - None => env_var(context, key), - } -} - fn error(_context: Context, message: &str) -> FunctionResult { Err(message.to_owned()) } From c56b6548309441378e1d75f6661a442616dee26d Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 12 Dec 2024 15:19:50 -0800 Subject: [PATCH 19/27] Document running python recipes with `uv` (#2526) --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 586ad997fe..027e556718 100644 --- a/README.md +++ b/README.md @@ -2802,6 +2802,41 @@ the final argument. For example, on Windows, if a recipe starts with `#! py`, the final command the OS runs will be something like `py C:\Temp\PATH_TO_SAVED_RECIPE_BODY`. +### Python Recipes with `uv` + +[`uv`](https://github.com/astral-sh/uv) is an excellent cross-platform python +project manager, written in Rust. + +Using the `[script]` attribute and `script-interpreter` setting, `just` can +easily be configured to run Python recipes with `uv`: + +```just +set unstable + +set script-interpreter := ['uv', 'run', '--script'] + +[script] +hello: + print("Hello from Python!") + +[script] +goodbye: + # /// script + # requires-python = ">=3.11" + # dependencies=["sh"] + # /// + import sh + print(sh.echo("Goodbye from Python!"), end='') +``` + +Of course, a shebang also works: + +```just +hello: + #!/usr/bin/env uv run --script + print("Hello from Python!") +``` + ### Script Recipes Recipes with a `[script(COMMAND)]`1.32.0 attribute are run as From 3442c2be1c4554a01adf8f404c3367bc2d107e5f Mon Sep 17 00:00:00 2001 From: Marc Addeo Date: Thu, 12 Dec 2024 18:36:24 -0500 Subject: [PATCH 20/27] Configure alias style in `--list` with `--alias-style` (#2342) --- src/alias_style.rs | 8 +++ src/analyzer.rs | 2 +- src/color.rs | 126 ++++++++++++++++++++++--------------------- src/config.rs | 16 ++++++ src/lib.rs | 2 + src/parser.rs | 2 +- src/subcommand.rs | 87 +++++++++++++++++++++--------- tests/alias_style.rs | 123 ++++++++++++++++++++++++++++++++++++++++++ tests/lib.rs | 1 + tests/misc.rs | 40 +++++++++----- 10 files changed, 306 insertions(+), 101 deletions(-) create mode 100644 src/alias_style.rs create mode 100644 tests/alias_style.rs diff --git a/src/alias_style.rs b/src/alias_style.rs new file mode 100644 index 0000000000..2f380d7c8f --- /dev/null +++ b/src/alias_style.rs @@ -0,0 +1,8 @@ +use super::*; + +#[derive(Debug, PartialEq, Clone, ValueEnum)] +pub(crate) enum AliasStyle { + Left, + Right, + Separate, +} diff --git a/src/analyzer.rs b/src/analyzer.rs index 1087bdeec9..e160d8bcb4 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -201,7 +201,7 @@ impl<'run, 'src> Analyzer<'run, 'src> { Rc::clone(next) }), }), - doc, + doc: doc.filter(|doc| !doc.is_empty()), groups: groups.into(), loaded: loaded.into(), modules: self.modules, diff --git a/src/color.rs b/src/color.rs index 7742597be4..723dcf615d 100644 --- a/src/color.rs +++ b/src/color.rs @@ -12,27 +12,16 @@ pub(crate) struct Color { } impl Color { - fn restyle(self, style: Style) -> Self { - Self { style, ..self } - } - - fn redirect(self, stream: impl IsTerminal) -> Self { - Self { - is_terminal: stream.is_terminal(), - ..self - } - } - - fn effective_style(&self) -> Style { - if self.active() { - self.style - } else { - Style::new() + pub(crate) fn active(&self) -> bool { + match self.use_color { + UseColor::Always => true, + UseColor::Never => false, + UseColor::Auto => self.is_terminal, } } - pub(crate) fn auto() -> Self { - Self::default() + pub(crate) fn alias(self) -> Self { + self.restyle(Style::new().fg(Purple)) } pub(crate) fn always() -> Self { @@ -42,25 +31,38 @@ impl Color { } } - pub(crate) fn never() -> Self { - Self { - use_color: UseColor::Never, - ..Self::default() - } + pub(crate) fn annotation(self) -> Self { + self.restyle(Style::new().fg(Purple)) } - pub(crate) fn stderr(self) -> Self { - self.redirect(io::stderr()) + pub(crate) fn auto() -> Self { + Self::default() } - pub(crate) fn stdout(self) -> Self { - self.redirect(io::stdout()) + pub(crate) fn banner(self) -> Self { + self.restyle(Style::new().fg(Cyan).bold()) + } + + pub(crate) fn command(self, foreground: Option) -> Self { + self.restyle(Style { + foreground, + is_bold: true, + ..Style::default() + }) } pub(crate) fn context(self) -> Self { self.restyle(Style::new().fg(Blue).bold()) } + pub(crate) fn diff_added(self) -> Self { + self.restyle(Style::new().fg(Green)) + } + + pub(crate) fn diff_deleted(self) -> Self { + self.restyle(Style::new().fg(Red)) + } + pub(crate) fn doc(self) -> Self { self.restyle(Style::new().fg(Blue)) } @@ -69,6 +71,14 @@ impl Color { self.restyle(Style::new().fg(Cyan)) } + fn effective_style(&self) -> Style { + if self.active() { + self.style + } else { + Style::new() + } + } + pub(crate) fn error(self) -> Self { self.restyle(Style::new().fg(Red).bold()) } @@ -77,65 +87,59 @@ impl Color { self.restyle(Style::new().fg(Yellow).bold()) } - pub(crate) fn warning(self) -> Self { - self.restyle(Style::new().fg(Yellow).bold()) + pub(crate) fn message(self) -> Self { + self.restyle(Style::new().bold()) } - pub(crate) fn banner(self) -> Self { - self.restyle(Style::new().fg(Cyan).bold()) + pub(crate) fn never() -> Self { + Self { + use_color: UseColor::Never, + ..Self::default() + } } - pub(crate) fn command(self, foreground: Option) -> Self { - self.restyle(Style { - foreground, - is_bold: true, - ..Style::default() - }) + pub(crate) fn paint<'a>(&self, text: &'a str) -> ANSIGenericString<'a, str> { + self.effective_style().paint(text) } pub(crate) fn parameter(self) -> Self { self.restyle(Style::new().fg(Cyan)) } - pub(crate) fn message(self) -> Self { - self.restyle(Style::new().bold()) - } - - pub(crate) fn annotation(self) -> Self { - self.restyle(Style::new().fg(Purple)) - } - - pub(crate) fn string(self) -> Self { - self.restyle(Style::new().fg(Green)) + pub(crate) fn prefix(&self) -> Prefix { + self.effective_style().prefix() } - pub(crate) fn diff_added(self) -> Self { - self.restyle(Style::new().fg(Green)) + fn redirect(self, stream: impl IsTerminal) -> Self { + Self { + is_terminal: stream.is_terminal(), + ..self + } } - pub(crate) fn diff_deleted(self) -> Self { - self.restyle(Style::new().fg(Red)) + fn restyle(self, style: Style) -> Self { + Self { style, ..self } } - pub(crate) fn active(&self) -> bool { - match self.use_color { - UseColor::Always => true, - UseColor::Never => false, - UseColor::Auto => self.is_terminal, - } + pub(crate) fn stderr(self) -> Self { + self.redirect(io::stderr()) } - pub(crate) fn paint<'a>(&self, text: &'a str) -> ANSIGenericString<'a, str> { - self.effective_style().paint(text) + pub(crate) fn stdout(self) -> Self { + self.redirect(io::stdout()) } - pub(crate) fn prefix(&self) -> Prefix { - self.effective_style().prefix() + pub(crate) fn string(self) -> Self { + self.restyle(Style::new().fg(Green)) } pub(crate) fn suffix(&self) -> Suffix { self.effective_style().suffix() } + + pub(crate) fn warning(self) -> Self { + self.restyle(Style::new().fg(Yellow).bold()) + } } impl From for Color { diff --git a/src/config.rs b/src/config.rs index df7e91ef56..575e73cd1e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,7 @@ use { #[derive(Debug, PartialEq)] pub(crate) struct Config { + pub(crate) alias_style: AliasStyle, pub(crate) allow_missing: bool, pub(crate) check: bool, pub(crate) color: Color, @@ -86,6 +87,7 @@ mod cmd { } mod arg { + pub(crate) const ALIAS_STYLE: &str = "ALIAS_STYLE"; pub(crate) const ALLOW_MISSING: &str = "ALLOW-MISSING"; pub(crate) const ARGUMENTS: &str = "ARGUMENTS"; pub(crate) const CHECK: &str = "CHECK"; @@ -145,6 +147,16 @@ impl Config { .usage(AnsiColor::Yellow.on_default() | Effects::BOLD) .valid(AnsiColor::Green.on_default()), ) + .arg( + Arg::new(arg::ALIAS_STYLE) + .long("alias-style") + .env("JUST_ALIAS_STYLE") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(AliasStyle)) + .default_value("right") + .help("Set list command alias display style") + .conflicts_with(arg::NO_ALIASES), + ) .arg( Arg::new(arg::CHECK) .long("check") @@ -739,6 +751,10 @@ impl Config { let explain = matches.get_flag(arg::EXPLAIN); Ok(Self { + alias_style: matches + .get_one::(arg::ALIAS_STYLE) + .unwrap() + .clone(), allow_missing: matches.get_flag(arg::ALLOW_MISSING), check: matches.get_flag(arg::CHECK), color: (*matches.get_one::(arg::COLOR).unwrap()).into(), diff --git a/src/lib.rs b/src/lib.rs index 7ce669ea1a..30777e513d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub(crate) use { crate::{ alias::Alias, + alias_style::AliasStyle, analyzer::Analyzer, argument_parser::ArgumentParser, assignment::Assignment, @@ -179,6 +180,7 @@ pub mod summary; pub mod request; mod alias; +mod alias_style; mod analyzer; mod argument_parser; mod assignment; diff --git a/src/parser.rs b/src/parser.rs index 5d7821a335..2258c13b3b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -952,7 +952,7 @@ impl<'run, 'src> Parser<'run, 'src> { attributes, body, dependencies, - doc, + doc: doc.filter(|doc| !doc.is_empty()), file_depth: self.file_depth, import_offsets: self.import_offsets.clone(), name, diff --git a/src/subcommand.rs b/src/subcommand.rs index 8d34578845..fbb5cb0ba8 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -432,40 +432,64 @@ impl Subcommand { } fn list_module(config: &Config, module: &Justfile, depth: usize) { - fn format_doc( + fn print_doc_and_aliases( config: &Config, name: &str, doc: Option<&str>, + aliases: &[&str], max_signature_width: usize, signature_widths: &BTreeMap<&str, usize>, ) { - if let Some(doc) = doc { - if !doc.is_empty() && doc.lines().count() <= 1 { - let color = config.color.stdout(); - print!( - "{:padding$}{} ", - "", - color.doc().paint("#"), - padding = max_signature_width.saturating_sub(signature_widths[name]) + 1, - ); + let color = config.color.stdout(); - 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 inline_aliases = config.alias_style != AliasStyle::Separate && !aliases.is_empty(); + + if inline_aliases || doc.is_some() { + print!( + "{:padding$}{}", + "", + color.doc().paint("#"), + padding = max_signature_width.saturating_sub(signature_widths[name]) + 1, + ); + } - let suffix = &doc[end..]; - if !suffix.is_empty() { - print!("{}", color.doc().paint(suffix)); + let print_aliases = || { + print!( + " {}", + color.alias().paint(&format!( + "[alias{}: {}]", + if aliases.len() == 1 { "" } else { "es" }, + aliases.join(", ") + )) + ); + }; + + if inline_aliases && config.alias_style == AliasStyle::Left { + print_aliases(); + } + + if let Some(doc) = doc { + print!(" "); + 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)); } } + if inline_aliases && config.alias_style == AliasStyle::Right { + print_aliases(); + } + println!(); } @@ -589,8 +613,14 @@ impl Subcommand { if let Some(recipes) = recipe_groups.get(&group) { for recipe in recipes { + let recipe_alias_entries = if config.alias_style == AliasStyle::Separate { + aliases.get(recipe.name()) + } else { + None + }; + for (i, name) in iter::once(&recipe.name()) - .chain(aliases.get(recipe.name()).unwrap_or(&Vec::new())) + .chain(recipe_alias_entries.unwrap_or(&Vec::new())) .enumerate() { let doc = if i == 0 { @@ -616,10 +646,14 @@ impl Subcommand { RecipeSignature { name, recipe }.color_display(config.color.stdout()) ); - format_doc( + print_doc_and_aliases( config, name, - doc.as_deref(), + doc.filter(|doc| doc.lines().count() <= 1).as_deref(), + aliases + .get(recipe.name()) + .map(Vec::as_slice) + .unwrap_or_default(), max_signature_width, &signature_widths, ); @@ -638,10 +672,11 @@ impl Subcommand { Self::list_module(config, submodule, depth + 1); } else { print!("{list_prefix}{} ...", submodule.name()); - format_doc( + print_doc_and_aliases( config, submodule.name(), submodule.doc.as_deref(), + &[], max_signature_width, &signature_widths, ); diff --git a/tests/alias_style.rs b/tests/alias_style.rs new file mode 100644 index 0000000000..dddea0d8ce --- /dev/null +++ b/tests/alias_style.rs @@ -0,0 +1,123 @@ +use super::*; + +#[test] +fn default() { + Test::new() + .justfile( + " + alias f := foo + + # comment + foo: + + bar: + ", + ) + .args(["--list"]) + .stdout( + " + Available recipes: + bar + foo # comment [alias: f] + ", + ) + .run(); +} + +#[test] +fn multiple() { + Test::new() + .justfile( + " + alias a := foo + alias b := foo + + # comment + foo: + + bar: + ", + ) + .args(["--list"]) + .stdout( + " + Available recipes: + bar + foo # comment [aliases: a, b] + ", + ) + .run(); +} + +#[test] +fn right() { + Test::new() + .justfile( + " + alias f := foo + + # comment + foo: + + bar: + ", + ) + .args(["--alias-style=right", "--list"]) + .stdout( + " + Available recipes: + bar + foo # comment [alias: f] + ", + ) + .run(); +} + +#[test] +fn left() { + Test::new() + .justfile( + " + alias f := foo + + # comment + foo: + + bar: + ", + ) + .args(["--alias-style=left", "--list"]) + .stdout( + " + Available recipes: + bar + foo # [alias: f] comment + ", + ) + .run(); +} + +#[test] +fn separate() { + Test::new() + .justfile( + " + alias f := foo + + # comment + foo: + + bar: + ", + ) + .args(["--alias-style=separate", "--list"]) + .stdout( + " + Available recipes: + bar + foo # comment + f # alias for `foo` + ", + ) + .run(); +} diff --git a/tests/lib.rs b/tests/lib.rs index 3ee9c3801f..a86a3da769 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -36,6 +36,7 @@ fn default() -> T { #[macro_use] mod test; +mod alias_style; mod allow_duplicate_recipes; mod allow_duplicate_variables; mod allow_missing; diff --git a/tests/misc.rs b/tests/misc.rs index ab16d5e208..79b609f314 100644 --- a/tests/misc.rs +++ b/tests/misc.rs @@ -11,9 +11,30 @@ test! { args: ("--list"), stdout: " Available recipes: - foo - f # alias for `foo` - ", + foo # [alias: f] + ", +} + +#[test] +fn alias_listing_with_doc() { + Test::new() + .justfile( + " + # foo command + foo: + echo foo + + alias f := foo + ", + ) + .arg("--list") + .stdout( + " + Available recipes: + foo # foo command [alias: f] + ", + ) + .run(); } test! { @@ -22,9 +43,7 @@ test! { args: ("--list"), stdout: " Available recipes: - foo - f # alias for `foo` - fo # alias for `foo` + foo # [aliases: f, fo] ", } @@ -34,8 +53,7 @@ test! { args: ("--list"), stdout: " Available recipes: - foo PARAM='foo' - f PARAM='foo' # alias for `foo` + foo PARAM='foo' # [alias: f] ", } @@ -927,8 +945,7 @@ a: stdout: r" Available recipes: a - b - c # alias for `b` + b # [alias: c] ", } @@ -942,8 +959,7 @@ a: args: ("--list", "--unsorted"), stdout: r" Available recipes: - b - c # alias for `b` + b # [alias: c] a ", } From 20192d4910e13d3c81638546ab5a61f515b3a5e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:03:46 -0800 Subject: [PATCH 21/27] Update softprops/action-gh-release to 2.2.0 (#2530) --- .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 72c63a90f9..b9f280bbb4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -113,7 +113,7 @@ jobs: shell: bash - name: Publish Archive - uses: softprops/action-gh-release@v2.1.0 + uses: softprops/action-gh-release@v2.2.0 if: ${{ startsWith(github.ref, 'refs/tags/') }} with: draft: false @@ -123,7 +123,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish Changelog - uses: softprops/action-gh-release@v2.1.0 + uses: softprops/action-gh-release@v2.2.0 if: >- ${{ startsWith(github.ref, 'refs/tags/') @@ -160,7 +160,7 @@ jobs: shasum -a 256 * > ../SHA256SUMS - name: Publish Checksums - uses: softprops/action-gh-release@v2.1.0 + uses: softprops/action-gh-release@v2.2.0 with: draft: false files: SHA256SUMS From 76362bf81dff289f47cd0c9c102622150ccce129 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 20 Dec 2024 11:19:02 -0800 Subject: [PATCH 22/27] Update readme to reflect actual behavior of user directory functions (#2535) --- README.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 027e556718..74e3d50918 100644 --- a/README.md +++ b/README.md @@ -1903,14 +1903,20 @@ for details. @echo '{{ style("error") }}OH NO{{ NORMAL }}' ``` -##### XDG Directories1.23.0 +##### User Directories1.23.0 These functions return paths to user-specific directories for things like -configuration, data, caches, executables, and the user's home directory. These -functions follow the -[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html), -and are implemented with the -[`dirs`](https://docs.rs/dirs/latest/dirs/index.html) crate. +configuration, data, caches, executables, and the user's home directory. + +On Unix, these functions follow the +[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). + +On MacOS and Windows, these functions return the system-specified user-specific +directories. For example, `cache_directory()` returns `~/Library/Caches` on +MacOS and `{FOLDERID_LocalAppData}` on Windows. + +See the [`dirs`](https://docs.rs/dirs/latest/dirs/index.html) crate for more +details. - `cache_directory()` - The user-specific cache directory. - `config_directory()` - The user-specific configuration directory. @@ -1920,6 +1926,10 @@ and are implemented with the - `executable_directory()` - The user-specific executable directory. - `home_directory()` - The user's home directory. +If you would like to use XDG base directories on all platforms you can use the +`env(…)` function with the appropriate environment variable, e.g., +`env('XDG_CACHE_HOME')`. + ### Constants A number of constants are predefined: From 6a2edd9f92036a3fef25ae5bc494891d336377b9 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 21 Dec 2024 18:30:50 -0800 Subject: [PATCH 23/27] Document weird behavior of duplicate definitions in imports (#2541) --- README.md | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 74e3d50918..837c64e44e 100644 --- a/README.md +++ b/README.md @@ -3492,9 +3492,39 @@ and recipes defined after the `import` statement. Imported files can themselves contain `import`s, which are processed recursively. -When `allow-duplicate-recipes` is set, recipes in parent modules override -recipes in imports. In a similar manner, when `allow-duplicate-variables` is -set, variables in parent modules override variables in imports. +`allow-duplicate-recipes` and `allow-duplicate-variables` allow duplicate +recipes and variables, respectively, to override each other, instead of +producing an error. + +Within a module, later definitions override earlier definitions: + +```just +set allow-duplicate-recipes + +foo: + +foo: + echo 'yes' +``` + +When `import`s are involved, things unfortunately get much more complicated and +hard to explain. + +Shallower definitions always override deeper definitions, so recipes at the top +level will override recipes in imports, and recipes in an import will override +recipes in an import which itself imports those recipes. + +When two duplicate definitions are imported and are at the same depth, the one +from the earlier import will override the one from the later import. + +This is because `just` uses a stack when processing imports, pushing imports +onto the stack in source-order, and always processing the top of the stack +next, so earlier imports are actually handled later by the compiler. + +This is definitely a bug, but since `just` has very strong backwards +compatibility guarantees and we take enormous pains not to break anyone's +`justfile`, we have created issue #2540 to discuss whether or not we can +actually fix it. Imports may be made optional by putting a `?` after the `import` keyword: From 82415e5456c6853f4f23d420bcfc4deca2dccb57 Mon Sep 17 00:00:00 2001 From: laniakea64 Date: Sat, 21 Dec 2024 22:21:14 -0500 Subject: [PATCH 24/27] Add full example getting XDG user directory to readme (#2536) --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 837c64e44e..707abdb724 100644 --- a/README.md +++ b/README.md @@ -1927,8 +1927,18 @@ details. - `home_directory()` - The user's home directory. If you would like to use XDG base directories on all platforms you can use the -`env(…)` function with the appropriate environment variable, e.g., -`env('XDG_CACHE_HOME')`. +`env(…)` function with the appropriate environment variable and fallback, +although note that the XDG specificiation requires ignoring non-absolute paths, +so for full compatibility with spec-complient applications, you would need to +do: + +```just +xdg_config_dir := if env('XDG_CONFIG_HOME', '') =~ '^/' { + env('XDG_CONFIG_HOME') +} else { + home_directory() / '.config' +} +``` ### Constants From b81ba66694ae259587c823b73017e8a44f22ac44 Mon Sep 17 00:00:00 2001 From: laniakea64 Date: Sat, 21 Dec 2024 23:21:12 -0500 Subject: [PATCH 25/27] Fix typos in README.md (#2542) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 707abdb724..99251e8fa9 100644 --- a/README.md +++ b/README.md @@ -1928,8 +1928,8 @@ details. If you would like to use XDG base directories on all platforms you can use the `env(…)` function with the appropriate environment variable and fallback, -although note that the XDG specificiation requires ignoring non-absolute paths, -so for full compatibility with spec-complient applications, you would need to +although note that the XDG specification requires ignoring non-absolute paths, +so for full compatibility with spec-compliant applications, you would need to do: ```just From 790995815faf88989bbc5b9e83b975d3f5fee2b9 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 22 Dec 2024 13:50:36 -0800 Subject: [PATCH 26/27] Refactor `Line` predicates (#2543) --- src/line.rs | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/line.rs b/src/line.rs index d0446118b3..8a58604676 100644 --- a/src/line.rs +++ b/src/line.rs @@ -10,15 +10,16 @@ pub(crate) struct Line<'src> { } impl Line<'_> { - pub(crate) fn is_empty(&self) -> bool { - self.fragments.is_empty() + fn first(&self) -> Option<&str> { + if let Fragment::Text { token } = self.fragments.first()? { + Some(token.lexeme()) + } else { + None + } } pub(crate) fn is_comment(&self) -> bool { - matches!( - self.fragments.first(), - Some(Fragment::Text { token }) if token.lexeme().starts_with('#'), - ) + self.first().is_some_and(|text| text.starts_with('#')) } pub(crate) fn is_continuation(&self) -> bool { @@ -28,26 +29,23 @@ impl Line<'_> { ) } - pub(crate) fn is_shebang(&self) -> bool { - matches!( - self.fragments.first(), - Some(Fragment::Text { token }) if token.lexeme().starts_with("#!"), - ) + pub(crate) fn is_empty(&self) -> bool { + self.fragments.is_empty() + } + + pub(crate) fn is_infallible(&self) -> bool { + self + .first() + .is_some_and(|text| text.starts_with('-') || text.starts_with("@-")) } pub(crate) fn is_quiet(&self) -> bool { - matches!( - self.fragments.first(), - Some(Fragment::Text { token }) - if token.lexeme().starts_with('@') || token.lexeme().starts_with("-@"), - ) + self + .first() + .is_some_and(|text| text.starts_with('@') || text.starts_with("-@")) } - pub(crate) fn is_infallible(&self) -> bool { - matches!( - self.fragments.first(), - Some(Fragment::Text { token }) - if token.lexeme().starts_with('-') || token.lexeme().starts_with("@-"), - ) + pub(crate) fn is_shebang(&self) -> bool { + self.first().is_some_and(|text| text.starts_with("#!")) } } From 206b0895fd071693c8688864d3f26d2a0b3f204c Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 22 Dec 2024 14:02:11 -0800 Subject: [PATCH 27/27] Document using `||` to provide default for empty environment variable (#2545) --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 99251e8fa9..a8c1559817 100644 --- a/README.md +++ b/README.md @@ -1645,11 +1645,11 @@ olleh := shell('import sys; print(sys.argv[2][::-1])', 'hello') #### Environment Variables -- `env_var(key)` — Retrieves the environment variable with name `key`, aborting +- `env(key)` — Retrieves the environment variable with name `key`, aborting if it is not present. ```just -home_dir := env_var('HOME') +home_dir := env('HOME') test: echo "{{home_dir}}" @@ -1665,6 +1665,15 @@ $ just - `env(key)`1.15.0 — Alias for `env_var(key)`. - `env(key, default)`1.15.0 — Alias for `env_var_or_default(key, default)`. +A default can be substituted for an empty environment variable value with the +`||` operator, currently unstable: + +```just +set unstable + +foo := env('FOO') || 'DEFAULT_VALUE' +``` + #### Invocation Information - `is_dependency()` - Returns the string `true` if the current recipe is being