Merge branch 'master' into feat/enums

This commit is contained in:
Rostislav 2025-07-07 14:17:05 +02:00 committed by GitHub
commit 724fd13444
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 1192 additions and 556 deletions

View file

@ -113,7 +113,7 @@ jobs:
shell: bash
- name: Publish Archive
uses: softprops/action-gh-release@v2.2.1
uses: softprops/action-gh-release@v2.3.2
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.2.1
uses: softprops/action-gh-release@v2.3.2
if: >-
${{
startsWith(github.ref, 'refs/tags/')
@ -160,7 +160,7 @@ jobs:
shasum -a 256 * > ../SHA256SUMS
- name: Publish Checksums
uses: softprops/action-gh-release@v2.2.1
uses: softprops/action-gh-release@v2.3.2
with:
draft: false
files: SHA256SUMS

View file

@ -1,6 +1,43 @@
Changelog
=========
[1.41.0](https://github.com/casey/just/releases/tag/1.41.0) - 2025-07-01
------------------------------------------------------------------------
### Changed
- Treat SIGINFO as non-fatal ([#2788](https://github.com/casey/just/pull/2788) by [casey](https://github.com/casey))
- Improve signal handling ([#2488](https://github.com/casey/just/pull/2488) by [casey](https://github.com/casey))
### Added
- Add `dotenv-override` setting ([#2785](https://github.com/casey/just/pull/2785) by [Lun4m](https://github.com/Lun4m))
- Add `PATH_SEP` and `PATH_VAR_SEP` constants ([#2679](https://github.com/casey/just/pull/2679) by [casey](https://github.com/casey))
- Add `--tempdir` command-line option ([#2798](https://github.com/casey/just/pull/2798) by [casey](https://github.com/casey))
### Fixed
- Pin `clap_complete` to last compatible version ([#2800](https://github.com/casey/just/pull/2800) by [casey](https://github.com/casey))
### Misc
- Add `arkade` to readme ([#2700](https://github.com/casey/just/pull/2700) by [rgee0](https://github.com/rgee0))
- Link to pipx instead of pypi in readme ([#2799](https://github.com/casey/just/pull/2799) by [casey](https://github.com/casey))
- Add reasons to `#[ignore]` attributes ([#2797](https://github.com/casey/just/pull/2797) by [casey](https://github.com/casey))
- Mention that command-line environment variables are inherited ([#2783](https://github.com/casey/just/pull/2783) by [philipmgrant](https://github.com/philipmgrant))
- Fix attribute grammar and update documentation ([#2790](https://github.com/casey/just/pull/2790) by [casey](https://github.com/casey))
- Tweak prose in groups section of readme ([#2767](https://github.com/casey/just/pull/2767) by [offby1](https://github.com/offby1))
- Update `extractions/setup-just` version in readme ([#2735](https://github.com/casey/just/pull/2735) by [esadek](https://github.com/esadek))
- Remove `return` in `Recipe::confirm` ([#2789](https://github.com/casey/just/pull/2789) by [casey](https://github.com/casey))
- Add `mise` to alternatives in readem ([#2758](https://github.com/casey/just/pull/2758) by [koppor](https://github.com/koppor))
- Update `softprops/action-gh-release` ([#2781](https://github.com/casey/just/pull/2781) by [app/dependabot](https://github.com/app/dependabot))
- Use `default` as name of `--init` justfile default recipe ([#2777](https://github.com/casey/just/pull/2777) by [casey](https://github.com/casey))
- Add `just.systems` link to `--init` justfile ([#2776](https://github.com/casey/just/pull/2776) by [casey](https://github.com/casey))
- Fix `kitchen-sink.just` comment ([#2738](https://github.com/casey/just/pull/2738) by [azarmadr](https://github.com/azarmadr))
- Update `softprops/action-gh-release` ([#2716](https://github.com/casey/just/pull/2716) by [app/dependabot](https://github.com/app/dependabot))
- Fix clippy lints ([#2768](https://github.com/casey/just/pull/2768) by [casey](https://github.com/casey))
- Add back-to-the-top link to readme ([#2707](https://github.com/casey/just/pull/2707) by [bravesasha](https://github.com/bravesasha))
- Placate clippy lints for 1.86 ([#2708](https://github.com/casey/just/pull/2708) by [casey](https://github.com/casey))
- Use `-S` in `uv` example ([#2696](https://github.com/casey/just/pull/2696) by [rmoorman](https://github.com/rmoorman))
- Handle `--request` without parsing justfile ([#2683](https://github.com/casey/just/pull/2683) by [casey](https://github.com/casey))
- Bump MSRV to 1.77 and enforce on CI ([#2674](https://github.com/casey/just/pull/2674) by [casey](https://github.com/casey))
[1.40.0](https://github.com/casey/just/releases/tag/1.40.0) - 2025-03-09
------------------------------------------------------------------------

501
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "just"
version = "1.40.0"
version = "1.41.0"
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
autotests = false
categories = ["command-line-utilities", "development-tools"]
@ -23,7 +23,7 @@ blake3 = { version = "1.5.0", features = ["rayon", "mmap"] }
camino = "1.0.4"
chrono = "0.4.38"
clap = { version = "4.0.0", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.0.0"
clap_complete = "=4.5.48"
clap_mangen = "0.2.20"
ctrlc = { version = "3.1.1", features = ["termination"] }
derive-where = "1.2.7"
@ -55,7 +55,7 @@ unicode-width = "0.2.0"
uuid = { version = "1.0.0", features = ["v4"] }
[target.'cfg(unix)'.dependencies]
nix = { version = "0.29.0", features = ["user"] }
nix = { version = "0.30.1", features = ["user"] }
[target.'cfg(windows)'.dependencies]
ctrlc = { version = "3.1.1", features = ["termination"] }
@ -64,7 +64,7 @@ ctrlc = { version = "3.1.1", features = ["termination"] }
executable-path = "1.0.0"
pretty_assertions = "1.0.0"
temptree = "0.2.0"
which = "7.0.0"
which = "8.0.0"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
@ -73,6 +73,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
all = { level = "deny", priority = -1 }
arbitrary-source-item-ordering = "deny"
enum_glob_use = "allow"
ignore_without_reason = "allow"
needless_pass_by_value = "allow"
pedantic = { level = "deny", priority = -1 }
similar_names = "allow"

View file

@ -124,9 +124,11 @@ sequence : expression ',' sequence
recipe : attributes* '@'? NAME parameter* variadic? ':' dependencies eol body?
attributes : '[' attribute* ']' eol
attributes : '[' attribute (',' attribute)* ']' eol
attribute : NAME ( '(' string ')' )?
attribute : NAME
| NAME ':' string
| NAME '(' string (',' string)* ')'
parameter : '$'? NAME
| '$'? NAME '=' value

203
README.md
View file

@ -139,6 +139,11 @@ most Windows users.)
</tr>
</thead>
<tbody>
<tr>
<td><a href=https://github.com/alexellis/arkade>arkade</a></td>
<td>just</td>
<td><code>arkade get just</code></td>
</tr>
<tr>
<td><a href=https://asdf-vm.com>asdf</a></td>
<td><a href=https://github.com/olofvndrhr/asdf-just>just</a></td>
@ -173,7 +178,7 @@ most Windows users.)
<td><code>npm install -g rust-just</code></td>
</tr>
<tr>
<td><a href=https://pypi.org/>PyPI</a></td>
<td><a href=https://pipx.pypa.io/stable/>pipx</a></td>
<td><a href=https://pypi.org/project/rust-just/>rust-just</a></td>
<td><code>pipx install rust-just</code></td>
</tr>
@ -387,6 +392,10 @@ to determine the latest version of `just` to install, and those API calls are
rate-limited on a per-IP basis. To make `install.sh` more reliable in such
circumstances, pass a specific tag to install with `--tag`.
Another way to avoid rate-limiting is to pass a GitHub authentication token to
`install.sh` as an environment variable named `GITHUB_TOKEN`, allowing it to
authenticate its requests.
[Releases](https://github.com/casey/just/releases) include a `SHA256SUM` file
which can be used to verify the integrity of pre-built binary archives.
@ -407,7 +416,7 @@ Using package managers pre-installed on GitHub Actions runners on MacOS with
With [extractions/setup-just](https://github.com/extractions/setup-just):
```yaml
- uses: extractions/setup-just@v2
- uses: extractions/setup-just@v3
with:
just-version: 1.5.0 # optional semver specification, otherwise latest
```
@ -685,6 +694,14 @@ cc main.c foo.c bar.c -o main
testing… all tests passed!
```
Recipes may depend on recipes in submodules:
```justfile
mod foo
baz: foo::bar
```
Examples
--------
@ -984,6 +1001,7 @@ foo:
| `allow-duplicate-variables` | boolean | `false` | Allow variables appearing later in a `justfile` to override earlier variables with the same name. |
| `dotenv-filename` | string | - | Load a `.env` file with a custom name, if present. |
| `dotenv-load` | boolean | `false` | Load a `.env` file, if present. |
| `dotenv-override` | boolean | `false` | Override existing environment variables with values from the `.env` file. |
| `dotenv-path` | string | - | Load a `.env` file from a custom path and error if not present. Overrides `dotenv-filename`. |
| `dotenv-required` | boolean | `false` | Error if a `.env` file isn't found. |
| `export` | boolean | `false` | Export all variables as environment variables. |
@ -1055,8 +1073,9 @@ bar
#### Dotenv Settings
If any of `dotenv-load`, `dotenv-filename`, `dotenv-path`, or `dotenv-required`
are set, `just` will try to load environment variables from a file.
If any of `dotenv-load`, `dotenv-filename`, `dotenv-override`, `dotenv-path`,
or `dotenv-required` are set, `just` will try to load environment variables
from a file.
If `dotenv-path` is set, `just` will look for a file at the given path, which
may be absolute, or relative to the working directory.
@ -1081,6 +1100,9 @@ It is not an error if an environment file is not found, unless
The loaded variables are environment variables, not `just` variables, and so
must be accessed using `$VARIABLE_NAME` in recipes and backticks.
If `dotenv-override` is set, variables from the environment file will override
existing environment variables.
For example, if your `.env` file contains:
```console
@ -2006,35 +2028,37 @@ xdg_config_dir := if env('XDG_CONFIG_HOME', '') =~ '^/' {
A number of constants are predefined:
| Name | Value |
|------|-------------|
| `HEX`<sup>1.27.0</sup> | `"0123456789abcdef"` |
| `HEXLOWER`<sup>1.27.0</sup> | `"0123456789abcdef"` |
| `HEXUPPER`<sup>1.27.0</sup> | `"0123456789ABCDEF"` |
| `CLEAR`<sup>1.37.0</sup> | `"\ec"` |
| `NORMAL`<sup>1.37.0</sup> | `"\e[0m"` |
| `BOLD`<sup>1.37.0</sup> | `"\e[1m"` |
| `ITALIC`<sup>1.37.0</sup> | `"\e[3m"` |
| `UNDERLINE`<sup>1.37.0</sup> | `"\e[4m"` |
| `INVERT`<sup>1.37.0</sup> | `"\e[7m"` |
| `HIDE`<sup>1.37.0</sup> | `"\e[8m"` |
| `STRIKETHROUGH`<sup>1.37.0</sup> | `"\e[9m"` |
| `BLACK`<sup>1.37.0</sup> | `"\e[30m"` |
| `RED`<sup>1.37.0</sup> | `"\e[31m"` |
| `GREEN`<sup>1.37.0</sup> | `"\e[32m"` |
| `YELLOW`<sup>1.37.0</sup> | `"\e[33m"` |
| `BLUE`<sup>1.37.0</sup> | `"\e[34m"` |
| `MAGENTA`<sup>1.37.0</sup> | `"\e[35m"` |
| `CYAN`<sup>1.37.0</sup> | `"\e[36m"` |
| `WHITE`<sup>1.37.0</sup> | `"\e[37m"` |
| `BG_BLACK`<sup>1.37.0</sup> | `"\e[40m"` |
| `BG_RED`<sup>1.37.0</sup> | `"\e[41m"` |
| `BG_GREEN`<sup>1.37.0</sup> | `"\e[42m"` |
| `BG_YELLOW`<sup>1.37.0</sup> | `"\e[43m"` |
| `BG_BLUE`<sup>1.37.0</sup> | `"\e[44m"` |
| `BG_MAGENTA`<sup>1.37.0</sup> | `"\e[45m"` |
| `BG_CYAN`<sup>1.37.0</sup> | `"\e[46m"` |
| `BG_WHITE`<sup>1.37.0</sup> | `"\e[47m"` |
| Name | Value | Value on Windows |
|---|---|---|
| `HEX`<sup>1.27.0</sup> | `"0123456789abcdef"` | |
| `HEXLOWER`<sup>1.27.0</sup> | `"0123456789abcdef"` | |
| `HEXUPPER`<sup>1.27.0</sup> | `"0123456789ABCDEF"` | |
| `PATH_SEP`<sup>1.41.0</sup> | `"/"` | "\" |
| `PATH_VAR_SEP`<sup>1.41.0</sup> | `":"` | ";" |
| `CLEAR`<sup>1.37.0</sup> | `"\ec"` | |
| `NORMAL`<sup>1.37.0</sup> | `"\e[0m"` | |
| `BOLD`<sup>1.37.0</sup> | `"\e[1m"` | |
| `ITALIC`<sup>1.37.0</sup> | `"\e[3m"` | |
| `UNDERLINE`<sup>1.37.0</sup> | `"\e[4m"` | |
| `INVERT`<sup>1.37.0</sup> | `"\e[7m"` | |
| `HIDE`<sup>1.37.0</sup> | `"\e[8m"` | |
| `STRIKETHROUGH`<sup>1.37.0</sup> | `"\e[9m"` | |
| `BLACK`<sup>1.37.0</sup> | `"\e[30m"` | |
| `RED`<sup>1.37.0</sup> | `"\e[31m"` | |
| `GREEN`<sup>1.37.0</sup> | `"\e[32m"` | |
| `YELLOW`<sup>1.37.0</sup> | `"\e[33m"` | |
| `BLUE`<sup>1.37.0</sup> | `"\e[34m"` | |
| `MAGENTA`<sup>1.37.0</sup> | `"\e[35m"` | |
| `CYAN`<sup>1.37.0</sup> | `"\e[36m"` | |
| `WHITE`<sup>1.37.0</sup> | `"\e[37m"` | |
| `BG_BLACK`<sup>1.37.0</sup> | `"\e[40m"` | |
| `BG_RED`<sup>1.37.0</sup> | `"\e[41m"` | |
| `BG_GREEN`<sup>1.37.0</sup> | `"\e[42m"` | |
| `BG_YELLOW`<sup>1.37.0</sup> | `"\e[43m"` | |
| `BG_BLUE`<sup>1.37.0</sup> | `"\e[44m"` | |
| `BG_MAGENTA`<sup>1.37.0</sup> | `"\e[45m"` | |
| `BG_CYAN`<sup>1.37.0</sup> | `"\e[46m"` | |
| `BG_WHITE`<sup>1.37.0</sup> | `"\e[47m"` | |
```just
@foo:
@ -2073,12 +2097,13 @@ change their behavior.
| Name | Type | Description |
|------|------|-------------|
| `[confirm]`<sup>1.17.0</sup> | recipe | Require confirmation prior to executing recipe. |
| `[confirm('PROMPT')]`<sup>1.23.0</sup> | recipe | Require confirmation prior to executing recipe with a custom prompt. |
| `[doc('DOC')]`<sup>1.27.0</sup> | module, recipe | Set recipe or module's [documentation comment](#documentation-comments) to `DOC`. |
| `[extension('EXT')]`<sup>1.32.0</sup> | recipe | Set shebang recipe script's file extension to `EXT`. `EXT` should include a period if one is desired. |
| `[group('NAME')]`<sup>1.27.0</sup> | module, recipe | Put recipe or module in in [group](#groups) `NAME`. |
| `[confirm(PROMPT)]`<sup>1.23.0</sup> | recipe | Require confirmation prior to executing recipe with a custom prompt. |
| `[doc(DOC)]`<sup>1.27.0</sup> | module, recipe | Set recipe or module's [documentation comment](#documentation-comments) to `DOC`. |
| `[extension(EXT)]`<sup>1.32.0</sup> | recipe | Set shebang recipe script's file extension to `EXT`. `EXT` should include a period if one is desired. |
| `[group(NAME)]`<sup>1.27.0</sup> | module, recipe | Put recipe or module in in [group](#groups) `NAME`. |
| `[linux]`<sup>1.8.0</sup> | recipe | Enable recipe on Linux. |
| `[macos]`<sup>1.8.0</sup> | recipe | Enable recipe on MacOS. |
| `[metadata(METADATA)]`<sup>master</sup> | recipe | Attach `METADATA` to recipe. |
| `[no-cd]`<sup>1.9.0</sup> | recipe | Don't change directory before executing recipe. |
| `[no-exit-message]`<sup>1.7.0</sup> | recipe | Don't print an error message if recipe fails. |
| `[no-quiet]`<sup>1.23.0</sup> | recipe | Override globally quiet recipes and always echo out the recipe. |
@ -2108,6 +2133,13 @@ foo:
echo "foo"
```
Attributes with a single argument may be written with a colon:
```just
[group: 'bar']
foo:
```
#### Enabling and Disabling Recipes<sup>1.8.0</sup>
The `[linux]`, `[macos]`, `[unix]`, and `[windows]` attributes are
@ -2182,7 +2214,7 @@ delete-everything:
### Groups
Recipes and modules may be annotated with a group name:
Recipes and modules may be annotated with one or more group names:
```just
[group('lint')]
@ -2884,6 +2916,46 @@ 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`.
### Script Recipes
Recipes with a `[script(COMMAND)]`<sup>1.32.0</sup> attribute are run as
scripts interpreted by `COMMAND`. This avoids some of the issues with shebang
recipes, such as the use of `cygpath` on Windows, the need to use
`/usr/bin/env`, inconsistencies in shebang line splitting across Unix OSs, and
requiring a temporary directory from which files can be executed.
Recipes with an empty `[script]` attribute are executed with the value of `set
script-interpreter := […]`<sup>1.33.0</sup>, defaulting to `sh -eu`, and *not*
the value of `set shell`.
The body of the recipe is evaluated, written to disk in the temporary
directory, and run by passing its path as an argument to `COMMAND`.
The `[script(…)]` attribute is unstable, so you'll need to use `set unstable`,
set the `JUST_UNSTABLE` environment variable, or pass `--unstable` on the
command line.
### Script and Shebang Recipe Temporary Files
Both script and shebang recipes write the recipe body to a temporary file for
execution. Script recipes execute that file by passing it to a command, while
shebang recipes execute the file directly. Shebang recipe execution will fail
if the filesystem containing the temporary file is mounted with `noexec` or is
otherwise non-executable.
The directory that `just` writes temporary files to may be configured in a
number of ways, from highest to lowest precedence:
- Globally with the `--tempdir` command-line option or the `JUST_TEMPDIR`
environment variable<sup>1.41.0</sup>.
- On a per-module basis with the `tempdir` setting.
- Globally on Linux with the `XDG_RUNTIME_DIR` environment variable.
- Falling back to the directory returned by
[std::env::temp_dir](https://doc.rust-lang.org/std/env/fn.temp_dir.html).
### Python Recipes with `uv`
[`uv`](https://github.com/astral-sh/uv) is an excellent cross-platform python
@ -2919,23 +2991,6 @@ hello:
print("Hello from Python!")
```
### Script Recipes
Recipes with a `[script(COMMAND)]`<sup>1.32.0</sup> attribute are run as
scripts interpreted by `COMMAND`. This avoids some of the issues with shebang
recipes, such as the use of `cygpath` on Windows, the need to use
`/usr/bin/env`, and inconsistencies in shebang line splitting across Unix OSs.
Recipes with an empty `[script]` attribute are executed with the value of `set
script-interpreter := […]`<sup>1.33.0</sup>, defaulting to `sh -eu`, and *not*
the value of `set shell`.
The body of the recipe is evaluated, written to disk in the temporary
directory, and run by passing its path as an argument to `COMMAND`.
The `[script(…)]` attribute is unstable, so you'll need to use `set unstable`,
set the `JUST_UNSTABLE` environment variable, or pass `--unstable` on the
command line.
### Safer Bash Shebang Recipes
@ -3271,9 +3326,9 @@ recipe:
echo 'back to recipe body'
```
### Command Line Options
### Command-line Options
`just` supports a number of useful command line options for listing, dumping,
`just` supports a number of useful command-line options for listing, dumping,
and debugging recipes and variables:
```console
@ -3292,21 +3347,29 @@ $ just --show polyglot
polyglot: python js perl sh ruby
```
Some command-line options can be set with environment variables. For example:
#### Setting Command-line Options with Environment Variables
Some command-line options can be set with environment variables
For example, unstable features can be enabled either with the `--unstable`
flag:
```console
$ just --unstable
```
Or by setting the `JUST_UNSTABLE` environment variable:
```console
$ export JUST_UNSTABLE=1
$ just
```
Is equivalent to:
Since environment variables are inherited by child processes, command-line
options set with environment variables are inherited by recursive invocations
of `just`, where as command line options set with arguments are not.
```console
$ just --unstable
```
Consult `just --help` to see which options can be set from environment
variables.
Consult `just --help` for which options can be set with environment variables.
### Private Recipes
@ -4006,7 +4069,7 @@ When a child process *is* running, `just` will wait until it terminates, to
avoid leaving it behind.
Additionally, on receipt of `SIGTERM`, `just` will forward `SIGTERM` to any
running children<sup>master</sup>, since unlike other fatal signals, `SIGTERM`,
running children<sup>1.41.0</sup>, since unlike other fatal signals, `SIGTERM`,
was likely sent to `just` alone.
Regardless of whether a child process terminates successfully after `just`
@ -4020,7 +4083,7 @@ user types `ctrl-t` on
operating systems, including MacOS, but not Linux.
`just` responds by printing a list of all child process IDs and
commands<sup>master</sup>.
commands<sup>1.41.0</sup>.
#### Windows
@ -4329,6 +4392,8 @@ to `just` include:
runner written in AWK and shell.
- [haku](https://github.com/VladimirMarkelov/haku): A make-like command runner
written in Rust.
- [mise](https://mise.jdx.dev/): A development environment tool manager written
in Rust supporing tasks in TOML files and standalone scripts.
Contributing
------------
@ -4405,7 +4470,9 @@ and checking the program's stdout, stderr, and exit code .
5. Implement the feature.
6. Run `just ci` to make sure that all tests, lints, and checks pass.
6. Run `just ci` to make sure that all tests, lints, and checks pass. Requires
[mdBook](https://github.com/rust-lang/mdBook) and
[mdbook-linkcheck](https://github.com/Michael-F-Bryan/mdbook-linkcheck).
7. Open a PR with the new code that is editable by maintainers. PRs often
require rebasing and minor tweaks. If the PR is not editable by maintainers,

View file

@ -47,7 +47,7 @@ x := '''
bar
'''
# this string will evaluate to `abc\n wuv\nbar\n`
# this string will evaluate to `abc\n wuv\nxyz\n`
y := """
abc
wuv

View file

@ -150,8 +150,13 @@ impl<'run, 'src> Analyzer<'run, 'src> {
}
}
let recipes =
RecipeResolver::resolve_recipes(&assignments, &self.enums, &settings, deduplicated_recipes)?;
let recipes = RecipeResolver::resolve_recipes(
&assignments,
&self.enums,
&self.modules,
&settings,
deduplicated_recipes,
)?;
let mut aliases = Table::new();
while let Some(alias) = self.aliases.pop() {
@ -302,7 +307,7 @@ impl<'run, 'src> Analyzer<'run, 'src> {
recipes: &'a Table<'src, Rc<Recipe<'src>>>,
alias: Alias<'src, Namepath<'src>>,
) -> CompileResult<'src, Alias<'src>> {
match Self::alias_target(&alias.target, modules, recipes) {
match Self::resolve_recipe(&alias.target, modules, recipes) {
Some(target) => Ok(alias.resolve(target)),
None => Err(alias.name.token.error(UnknownAliasTarget {
alias: alias.name.lexeme(),
@ -311,7 +316,7 @@ impl<'run, 'src> Analyzer<'run, 'src> {
}
}
fn alias_target<'a>(
pub(crate) fn resolve_recipe<'a>(
path: &Namepath<'src>,
mut modules: &'a Table<'src, Justfile<'src>>,
mut recipes: &'a Table<'src, Rc<Recipe<'src>>>,

View file

@ -16,6 +16,7 @@ pub(crate) enum Attribute<'src> {
Group(StringLiteral<'src>),
Linux,
Macos,
Metadata(Vec<StringLiteral<'src>>),
NoCd,
NoExitMessage,
NoQuiet,
@ -32,7 +33,6 @@ impl AttributeDiscriminant {
fn argument_range(self) -> RangeInclusive<usize> {
match self {
Self::Confirm | Self::Doc => 0..=1,
Self::Group | Self::Extension | Self::WorkingDirectory => 1..=1,
Self::ExitMessage
| Self::Linux
| Self::Macos
@ -44,6 +44,8 @@ impl AttributeDiscriminant {
| Self::Private
| Self::Unix
| Self::Windows => 0..=0,
Self::Extension | Self::Group | Self::WorkingDirectory => 1..=1,
Self::Metadata => 1..=usize::MAX,
Self::Script => 0..=usize::MAX,
}
}
@ -85,6 +87,7 @@ impl<'src> Attribute<'src> {
AttributeDiscriminant::Group => Self::Group(arguments.into_iter().next().unwrap()),
AttributeDiscriminant::Linux => Self::Linux,
AttributeDiscriminant::Macos => Self::Macos,
AttributeDiscriminant::Metadata => Self::Metadata(arguments),
AttributeDiscriminant::NoCd => Self::NoCd,
AttributeDiscriminant::NoExitMessage => Self::NoExitMessage,
AttributeDiscriminant::NoQuiet => Self::NoQuiet,
@ -115,7 +118,7 @@ impl<'src> Attribute<'src> {
}
pub(crate) fn repeatable(&self) -> bool {
matches!(self, Attribute::Group(_))
matches!(self, Attribute::Group(_) | Attribute::Metadata(_))
}
}
@ -124,12 +127,6 @@ impl Display for Attribute<'_> {
write!(f, "{}", self.name())?;
match self {
Self::Confirm(Some(argument))
| Self::Doc(Some(argument))
| Self::Extension(argument)
| Self::Group(argument)
| Self::WorkingDirectory(argument) => write!(f, "({argument})")?,
Self::Script(Some(shell)) => write!(f, "({shell})")?,
Self::Confirm(None)
| Self::Doc(None)
| Self::ExitMessage
@ -144,6 +141,22 @@ impl Display for Attribute<'_> {
| Self::Script(None)
| Self::Unix
| Self::Windows => {}
Self::Confirm(Some(argument))
| Self::Doc(Some(argument))
| Self::Extension(argument)
| Self::Group(argument)
| Self::WorkingDirectory(argument) => write!(f, "({argument})")?,
Self::Metadata(arguments) => {
write!(f, "(")?;
for (i, argument) in arguments.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{argument}")?;
}
write!(f, ")")?;
}
Self::Script(Some(shell)) => write!(f, "({shell})")?,
}
Ok(())

View file

@ -18,7 +18,7 @@ pub(crate) enum CompileErrorKind<'src> {
circle: Vec<&'src str>,
},
DependencyArgumentCountMismatch {
dependency: &'src str,
dependency: Namepath<'src>,
found: usize,
min: usize,
max: usize,
@ -148,7 +148,7 @@ pub(crate) enum CompileErrorKind<'src> {
},
UnknownDependency {
recipe: &'src str,
unknown: &'src str,
unknown: Namepath<'src>,
},
UnknownFunction {
function: &'src str,

View file

@ -296,10 +296,7 @@ const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
"#,
),
(r"for i in ${COMP_WORDS[@]}", r"for i in ${words[@]}"),
(
r"elif [[ ${COMP_CWORD} -eq 1 ]]; then",
r"elif [[ ${cword} -eq 1 ]]; then",
),
(r"elif [[ ${COMP_CWORD} -eq 1 ]]; then", r"else"),
(
r#"COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") )"#,
r#"COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") )

View file

@ -17,6 +17,7 @@ pub(crate) struct Config {
pub(crate) check: bool,
pub(crate) color: Color,
pub(crate) command_color: Option<ansi_term::Color>,
pub(crate) cygpath: PathBuf,
pub(crate) dotenv_filename: Option<String>,
pub(crate) dotenv_path: Option<PathBuf>,
pub(crate) dry_run: bool,
@ -36,6 +37,7 @@ pub(crate) struct Config {
pub(crate) shell_args: Option<Vec<String>>,
pub(crate) shell_command: bool,
pub(crate) subcommand: Subcommand,
pub(crate) tempdir: Option<PathBuf>,
pub(crate) timestamp: bool,
pub(crate) timestamp_format: String,
pub(crate) unsorted: bool,
@ -95,6 +97,7 @@ mod arg {
pub(crate) const CLEAR_SHELL_ARGS: &str = "CLEAR-SHELL-ARGS";
pub(crate) const COLOR: &str = "COLOR";
pub(crate) const COMMAND_COLOR: &str = "COMMAND-COLOR";
pub(crate) const CYGPATH: &str = "CYGPATH";
pub(crate) const DOTENV_FILENAME: &str = "DOTENV-FILENAME";
pub(crate) const DOTENV_PATH: &str = "DOTENV-PATH";
pub(crate) const DRY_RUN: &str = "DRY-RUN";
@ -116,6 +119,7 @@ mod arg {
pub(crate) const SHELL: &str = "SHELL";
pub(crate) const SHELL_ARG: &str = "SHELL-ARG";
pub(crate) const SHELL_COMMAND: &str = "SHELL-COMMAND";
pub(crate) const TEMPDIR: &str = "TEMPDIR";
pub(crate) const TIMESTAMP: &str = "TIMESTAMP";
pub(crate) const TIMESTAMP_FORMAT: &str = "TIMESTAMP-FORMAT";
pub(crate) const UNSORTED: &str = "UNSORTED";
@ -198,6 +202,15 @@ impl Config {
.value_parser(clap::value_parser!(CommandColor))
.help("Echo recipe lines in <COMMAND-COLOR>"),
)
.arg(
Arg::new(arg::CYGPATH)
.long("cygpath")
.env("JUST_CYGPATH")
.action(ArgAction::Set)
.value_parser(value_parser!(PathBuf))
.default_value("cygpath")
.help("Use binary at <CYGPATH> to convert between unix and Windows paths."),
)
.arg(
Arg::new(arg::DOTENV_FILENAME)
.long("dotenv-filename")
@ -373,6 +386,14 @@ impl Config {
.action(ArgAction::SetTrue)
.help("Invoke <COMMAND> with the shell used to run recipe lines and backticks"),
)
.arg(
Arg::new(arg::TEMPDIR)
.action(ArgAction::Set)
.env("JUST_TEMPDIR")
.long("tempdir")
.value_parser(value_parser!(PathBuf))
.help("Save temporary files to <TEMPDIR>."),
)
.arg(
Arg::new(arg::TIMESTAMP)
.action(ArgAction::SetTrue)
@ -762,6 +783,7 @@ impl Config {
.get_one::<CommandColor>(arg::COMMAND_COLOR)
.copied()
.map(CommandColor::into),
cygpath: matches.get_one::<PathBuf>(arg::CYGPATH).unwrap().clone(),
dotenv_filename: matches
.get_one::<String>(arg::DOTENV_FILENAME)
.map(Into::into),
@ -792,6 +814,7 @@ impl Config {
},
shell_command: matches.get_flag(arg::SHELL_COMMAND),
subcommand,
tempdir: matches.get_one::<PathBuf>(arg::TEMPDIR).map(Into::into),
timestamp: matches.get_flag(arg::TIMESTAMP),
timestamp_format: matches
.get_one::<String>(arg::TIMESTAMP_FORMAT)

View file

@ -1,41 +1,53 @@
use super::*;
const CONSTANTS: [(&str, &str, &str); 27] = [
("HEX", "0123456789abcdef", "1.27.0"),
("HEXLOWER", "0123456789abcdef", "1.27.0"),
("HEXUPPER", "0123456789ABCDEF", "1.27.0"),
("CLEAR", "\x1bc", "master"),
("NORMAL", "\x1b[0m", "master"),
("BOLD", "\x1b[1m", "master"),
("ITALIC", "\x1b[3m", "master"),
("UNDERLINE", "\x1b[4m", "master"),
("INVERT", "\x1b[7m", "master"),
("HIDE", "\x1b[8m", "master"),
("STRIKETHROUGH", "\x1b[9m", "master"),
("BLACK", "\x1b[30m", "master"),
("RED", "\x1b[31m", "master"),
("GREEN", "\x1b[32m", "master"),
("YELLOW", "\x1b[33m", "master"),
("BLUE", "\x1b[34m", "master"),
("MAGENTA", "\x1b[35m", "master"),
("CYAN", "\x1b[36m", "master"),
("WHITE", "\x1b[37m", "master"),
("BG_BLACK", "\x1b[40m", "master"),
("BG_RED", "\x1b[41m", "master"),
("BG_GREEN", "\x1b[42m", "master"),
("BG_YELLOW", "\x1b[43m", "master"),
("BG_BLUE", "\x1b[44m", "master"),
("BG_MAGENTA", "\x1b[45m", "master"),
("BG_CYAN", "\x1b[46m", "master"),
("BG_WHITE", "\x1b[47m", "master"),
const CONSTANTS: &[(&str, &str, Option<&str>, &str)] = &[
("HEX", "0123456789abcdef", None, "1.27.0"),
("HEXLOWER", "0123456789abcdef", None, "1.27.0"),
("HEXUPPER", "0123456789ABCDEF", None, "1.27.0"),
("PATH_SEP", "/", Some("\\"), "1.41.0"),
("PATH_VAR_SEP", ":", Some(";"), "1.41.0"),
("CLEAR", "\x1bc", None, "1.37.0"),
("NORMAL", "\x1b[0m", None, "1.37.0"),
("BOLD", "\x1b[1m", None, "1.37.0"),
("ITALIC", "\x1b[3m", None, "1.37.0"),
("UNDERLINE", "\x1b[4m", None, "1.37.0"),
("INVERT", "\x1b[7m", None, "1.37.0"),
("HIDE", "\x1b[8m", None, "1.37.0"),
("STRIKETHROUGH", "\x1b[9m", None, "1.37.0"),
("BLACK", "\x1b[30m", None, "1.37.0"),
("RED", "\x1b[31m", None, "1.37.0"),
("GREEN", "\x1b[32m", None, "1.37.0"),
("YELLOW", "\x1b[33m", None, "1.37.0"),
("BLUE", "\x1b[34m", None, "1.37.0"),
("MAGENTA", "\x1b[35m", None, "1.37.0"),
("CYAN", "\x1b[36m", None, "1.37.0"),
("WHITE", "\x1b[37m", None, "1.37.0"),
("BG_BLACK", "\x1b[40m", None, "1.37.0"),
("BG_RED", "\x1b[41m", None, "1.37.0"),
("BG_GREEN", "\x1b[42m", None, "1.37.0"),
("BG_YELLOW", "\x1b[43m", None, "1.37.0"),
("BG_BLUE", "\x1b[44m", None, "1.37.0"),
("BG_MAGENTA", "\x1b[45m", None, "1.37.0"),
("BG_CYAN", "\x1b[46m", None, "1.37.0"),
("BG_WHITE", "\x1b[47m", None, "1.37.0"),
];
pub(crate) fn constants() -> &'static HashMap<&'static str, &'static str> {
static MAP: OnceLock<HashMap<&str, &str>> = OnceLock::new();
MAP.get_or_init(|| {
CONSTANTS
.into_iter()
.map(|(name, value, _version)| (name, value))
.iter()
.copied()
.map(|(name, unix, windows, _version)| {
(
name,
if cfg!(windows) {
windows.unwrap_or(unix)
} else {
unix
},
)
})
.collect()
})
}
@ -46,13 +58,26 @@ mod tests {
#[test]
fn readme_table() {
println!("| Name | Value |");
println!("|------|-------------|");
for (name, value, version) in CONSTANTS {
println!(
"| `{name}`<sup>{version}</sup> | `\"{}\"` |",
value.replace('\x1b', "\\e")
);
let mut table = Vec::<String>::new();
table.push("| Name | Value | Value on Windows |".into());
table.push("|---|---|---|".into());
for (name, unix, windows, version) in CONSTANTS {
table.push(format!(
"| `{name}`<sup>{version}</sup> | `\"{}\"` | {} |",
unix.replace('\x1b', "\\e"),
windows
.map(|value| format!("\"{value}\""))
.unwrap_or_default(),
));
}
let table = table.join("\n");
let readme = fs::read_to_string("README.md").unwrap();
assert!(
readme.contains(&table),
"could not find table in readme:\n{table}",
);
}
}

View file

@ -235,7 +235,7 @@ impl<'src> Error<'src> {
output_error: OutputError::Interrupted(signal),
..
}
| Self::Interrupted { signal } => signal.code(),
| Self::Interrupted { signal } => Some(signal.code()),
_ => None,
}
}

View file

@ -10,6 +10,36 @@ pub(crate) struct ExecutionContext<'src: 'run, 'run> {
}
impl<'src: 'run, 'run> ExecutionContext<'src, 'run> {
pub(crate) fn tempdir<D>(&self, recipe: &Recipe<'src, D>) -> RunResult<'src, TempDir> {
let mut tempdir_builder = tempfile::Builder::new();
tempdir_builder.prefix("just-");
if let Some(tempdir) = &self.config.tempdir {
tempdir_builder.tempdir_in(self.search.working_directory.join(tempdir))
} else {
match &self.module.settings.tempdir {
Some(tempdir) => tempdir_builder.tempdir_in(self.search.working_directory.join(tempdir)),
None => {
if let Some(runtime_dir) = dirs::runtime_dir() {
let path = runtime_dir.join("just");
fs::create_dir_all(&path).map_err(|io_error| Error::RuntimeDirIo {
io_error,
path: path.clone(),
})?;
tempdir_builder.tempdir_in(path)
} else {
tempdir_builder.tempdir()
}
}
}
}
.map_err(|error| Error::TempdirIo {
recipe: recipe.name(),
io_error: error,
})
}
pub(crate) fn working_directory(&self) -> PathBuf {
let base = if self.module.is_submodule() {
&self.module.working_directory

View file

@ -8,6 +8,7 @@ pub(crate) enum Executor<'a> {
impl Executor<'_> {
pub(crate) fn command<'src>(
&self,
config: &Config,
path: &Path,
recipe: &'src str,
working_directory: Option<&Path>,
@ -36,12 +37,12 @@ impl Executor<'_> {
})?;
// create command to run script
Platform::make_shebang_command(path, working_directory, *shebang).map_err(|output_error| {
Error::Cygpath {
Platform::make_shebang_command(config, path, *shebang, working_directory).map_err(
|output_error| Error::Cygpath {
recipe,
output_error,
}
})
},
)
}
}
}

View file

@ -333,6 +333,7 @@ fn file_stem(_context: Context, path: &str) -> FunctionResult {
fn invocation_directory(context: Context) -> FunctionResult {
Platform::convert_native_path(
context.evaluator.context.config,
&context.evaluator.context.search.working_directory,
&context.evaluator.context.config.invocation_directory,
)

View file

@ -9,6 +9,7 @@ pub(crate) enum Keyword {
Assert,
DotenvFilename,
DotenvLoad,
DotenvOverride,
DotenvPath,
DotenvRequired,
Else,

View file

@ -134,7 +134,7 @@ pub(crate) use {
vec,
},
strum::{Display, EnumDiscriminants, EnumString, IntoStaticStr},
tempfile::tempfile,
tempfile::{tempfile, TempDir},
typed_arena::Arena,
unicode_width::{UnicodeWidthChar, UnicodeWidthStr},
};
@ -152,7 +152,7 @@ use request::Request;
// Used in integration tests.
#[doc(hidden)]
pub use {request::Response, unindent::unindent};
pub use {request::Response, subcommand::INIT_JUSTFILE, unindent::unindent};
type CompileResult<'a, T = ()> = Result<T, CompileError<'a>>;
type ConfigResult<T> = Result<T, ConfigError>;

View file

@ -16,9 +16,10 @@ pub(crate) fn load_dotenv(
.or(settings.dotenv_path.as_ref());
if !settings.dotenv_load
&& !settings.dotenv_override
&& !settings.dotenv_required
&& dotenv_filename.is_none()
&& dotenv_path.is_none()
&& !settings.dotenv_required
{
return Ok(BTreeMap::new());
}
@ -26,7 +27,7 @@ pub(crate) fn load_dotenv(
if let Some(path) = dotenv_path {
let path = working_directory.join(path);
if path.is_file() {
return load_from_file(&path);
return load_from_file(&path, settings);
}
}
@ -35,7 +36,7 @@ pub(crate) fn load_dotenv(
for directory in working_directory.ancestors() {
let path = directory.join(filename);
if path.is_file() {
return load_from_file(&path);
return load_from_file(&path, settings);
}
}
@ -46,12 +47,15 @@ pub(crate) fn load_dotenv(
}
}
fn load_from_file(path: &Path) -> RunResult<'static, BTreeMap<String, String>> {
fn load_from_file(
path: &Path,
settings: &Settings,
) -> RunResult<'static, BTreeMap<String, String>> {
let iter = dotenvy::from_path_iter(path)?;
let mut dotenv = BTreeMap::new();
for result in iter {
let (key, value) = result?;
if env::var_os(&key).is_none() {
if settings.dotenv_override || env::var_os(&key).is_none() {
dotenv.insert(key, value);
}
}

View file

@ -32,8 +32,7 @@ impl<'src> Namepath<'src> {
self.0.iter()
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
pub(crate) fn components(&self) -> usize {
self.0.len()
}
}

View file

@ -66,7 +66,7 @@ impl<'src> Node<'src> for Item<'src> {
impl<'src> Node<'src> for Namepath<'src> {
fn tree(&self) -> Tree<'src> {
match self.len() {
match self.components() {
1 => Tree::atom(self.last().lexeme()),
_ => Tree::list(
self
@ -238,7 +238,7 @@ impl<'src> Node<'src> for UnresolvedRecipe<'src> {
let mut subsequents = Tree::atom("sups");
for (i, dependency) in self.dependencies.iter().enumerate() {
let mut d = Tree::atom(dependency.recipe.lexeme());
let mut d = dependency.recipe.tree();
for argument in &dependency.arguments {
d.push_mut(argument.tree());
@ -304,6 +304,7 @@ impl<'src> Node<'src> for Set<'src> {
Setting::AllowDuplicateRecipes(value)
| Setting::AllowDuplicateVariables(value)
| Setting::DotenvLoad(value)
| Setting::DotenvOverride(value)
| Setting::DotenvRequired(value)
| Setting::Export(value)
| Setting::Fallback(value)

View file

@ -246,10 +246,10 @@ impl<'run, 'src> Parser<'run, 'src> {
}
}
/// Accept a token of kind `Identifier` and parse into a `Name`
fn accept_name(&mut self) -> CompileResult<'src, Option<Name<'src>>> {
/// Accept a double-colon separated sequence of identifiers
fn accept_namepath(&mut self) -> CompileResult<'src, Option<Namepath<'src>>> {
if self.next_is(Identifier) {
Ok(Some(self.parse_name()?))
Ok(Some(self.parse_namepath()?))
} else {
Ok(None)
}
@ -268,13 +268,13 @@ impl<'run, 'src> Parser<'run, 'src> {
/// Accept a dependency
fn accept_dependency(&mut self) -> CompileResult<'src, Option<UnresolvedDependency<'src>>> {
if let Some(recipe) = self.accept_name()? {
if let Some(recipe) = self.accept_namepath()? {
Ok(Some(UnresolvedDependency {
arguments: Vec::new(),
recipe,
}))
} else if self.accepted(ParenL)? {
let recipe = self.parse_name()?;
let recipe = self.parse_namepath()?;
let mut arguments = Vec::new();
@ -1157,6 +1157,7 @@ impl<'run, 'src> Parser<'run, 'src> {
Some(Setting::AllowDuplicateVariables(self.parse_set_bool()?))
}
Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)),
Keyword::DotenvOverride => Some(Setting::DotenvOverride(self.parse_set_bool()?)),
Keyword::DotenvRequired => Some(Setting::DotenvRequired(self.parse_set_bool()?)),
Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)),
Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)),
@ -1695,6 +1696,24 @@ mod tests {
tree: (justfile (recipe foo (body ("bar")))),
}
test! {
name: recipe_dependency_module,
text: "foo: bar::baz",
tree: (justfile (recipe foo (deps (bar baz)))),
}
test! {
name: recipe_dependency_parenthesis_module,
text: "foo: (bar::baz)",
tree: (justfile (recipe foo (deps (bar baz)))),
}
test! {
name: recipe_dependency_module_mixed,
text: "foo: bar::baz qux",
tree: (justfile (recipe foo (deps (bar baz) qux))),
}
test! {
name: recipe_line_multiple,
text: "foo:\n bar\n baz\n {{\"bob\"}}biz",
@ -2598,7 +2617,7 @@ mod tests {
column: 9,
width: 1,
kind: UnexpectedToken{
expected: vec![AmpersandAmpersand, Comment, Eof, Eol, Identifier, ParenL],
expected: vec![AmpersandAmpersand, ColonColon, Comment, Eof, Eol, Identifier, ParenL],
found: Equals
},
}

View file

@ -2,9 +2,10 @@ use super::*;
impl PlatformInterface for Platform {
fn make_shebang_command(
_config: &Config,
path: &Path,
working_directory: Option<&Path>,
_shebang: Shebang,
working_directory: Option<&Path>,
) -> Result<Command, OutputError> {
// shebang scripts can be executed directly on unix
let mut command = Command::new(path);
@ -35,7 +36,11 @@ impl PlatformInterface for Platform {
exit_status.signal()
}
fn convert_native_path(_working_directory: &Path, path: &Path) -> FunctionResult {
fn convert_native_path(
_config: &Config,
_working_directory: &Path,
path: &Path,
) -> FunctionResult {
path
.to_str()
.map(str::to_string)

View file

@ -2,16 +2,17 @@ use super::*;
impl PlatformInterface for Platform {
fn make_shebang_command(
config: &Config,
path: &Path,
working_directory: Option<&Path>,
shebang: Shebang,
working_directory: Option<&Path>,
) -> Result<Command, OutputError> {
use std::borrow::Cow;
// If the path contains forward slashes…
let command = if shebang.interpreter.contains('/') {
// …translate path to the interpreter from unix style to windows style.
let mut cygpath = Command::new("cygpath");
let mut cygpath = Command::new(&config.cygpath);
if let Some(working_directory) = working_directory {
cygpath.current_dir(working_directory);
@ -56,9 +57,9 @@ impl PlatformInterface for Platform {
None
}
fn convert_native_path(working_directory: &Path, path: &Path) -> FunctionResult {
fn convert_native_path(config: &Config, working_directory: &Path, path: &Path) -> FunctionResult {
// Translate path from windows style to unix style
let mut cygpath = Command::new("cygpath");
let mut cygpath = Command::new(&config.cygpath);
cygpath
.current_dir(working_directory)

View file

@ -2,7 +2,7 @@ use super::*;
pub(crate) trait PlatformInterface {
/// translate path from "native" path to path interpreter expects
fn convert_native_path(working_directory: &Path, path: &Path) -> FunctionResult;
fn convert_native_path(config: &Config, working_directory: &Path, path: &Path) -> FunctionResult;
/// install handler, may only be called once
fn install_signal_handler<T: Fn(Signal) + Send + 'static>(handler: T) -> RunResult<'static>;
@ -10,9 +10,10 @@ pub(crate) trait PlatformInterface {
/// construct command equivalent to running script at `path` with shebang
/// line `shebang`
fn make_shebang_command(
config: &Config,
path: &Path,
working_directory: Option<&Path>,
shebang: Shebang,
working_directory: Option<&Path>,
) -> Result<Command, OutputError>;
/// set the execute permission on file pointed to by `path`

View file

@ -77,9 +77,10 @@ impl<'src, D> Recipe<'src, D> {
.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(line == "y" || line == "yes")
} else {
Ok(true)
}
Ok(true)
}
pub(crate) fn check_can_be_default_recipe(&self) -> RunResult<'src, ()> {
@ -389,27 +390,8 @@ impl<'src, D> Recipe<'src, D> {
Executor::Shebang(shebang)
};
let mut tempdir_builder = tempfile::Builder::new();
tempdir_builder.prefix("just-");
let tempdir = match &context.module.settings.tempdir {
Some(tempdir) => tempdir_builder.tempdir_in(context.search.working_directory.join(tempdir)),
None => {
if let Some(runtime_dir) = dirs::runtime_dir() {
let path = runtime_dir.join("just");
fs::create_dir_all(&path).map_err(|io_error| Error::RuntimeDirIo {
io_error,
path: path.clone(),
})?;
tempdir_builder.tempdir_in(path)
} else {
tempdir_builder.tempdir()
}
}
}
.map_err(|error| Error::TempdirIo {
recipe: self.name(),
io_error: error,
})?;
let tempdir = context.tempdir(self)?;
let mut path = tempdir.path().to_path_buf();
let extension = self.attributes.iter().find_map(|attribute| {
@ -434,6 +416,7 @@ impl<'src, D> Recipe<'src, D> {
})?;
let mut command = executor.command(
config,
&path,
self.name(),
self.working_directory(context).as_deref(),

View file

@ -2,6 +2,7 @@ use {super::*, CompileErrorKind::*};
pub(crate) struct RecipeResolver<'src: 'run, 'run> {
assignments: &'run Table<'src, Assignment<'src>>,
modules: &'run Table<'src, Justfile<'src>>,
resolved_recipes: Table<'src, Rc<Recipe<'src>>>,
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
enums: &'run Table<'src, Enum<'src>>,
@ -11,6 +12,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
pub(crate) fn resolve_recipes(
assignments: &'run Table<'src, Assignment<'src>>,
enums: &'run Table<'src, Enum<'src>>,
modules: &'run Table<'src, Justfile<'src>>,
settings: &Settings,
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
) -> CompileResult<'src, Table<'src, Rc<Recipe<'src>>>> {
@ -19,6 +21,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
unresolved_recipes,
assignments,
enums,
modules
};
while let Some(unresolved) = resolver.unresolved_recipes.pop() {
@ -101,37 +104,20 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
stack.push(recipe.name());
let mut dependencies: Vec<Rc<Recipe>> = Vec::new();
for dependency in &recipe.dependencies {
let name = dependency.recipe.lexeme();
if let Some(resolved) = self.resolved_recipes.get(name) {
// dependency already resolved
dependencies.push(Rc::clone(resolved));
} else if stack.contains(&name) {
let first = stack[0];
stack.push(first);
return Err(
dependency.recipe.error(CircularRecipeDependency {
recipe: recipe.name(),
circle: stack
.iter()
.skip_while(|name| **name != dependency.recipe.lexeme())
.copied()
.collect(),
}),
);
} else if let Some(unresolved) = self.unresolved_recipes.remove(name) {
// resolve unresolved dependency
dependencies.push(self.resolve_recipe(stack, unresolved)?);
} else {
// dependency is unknown
return Err(dependency.recipe.error(UnknownDependency {
recipe: recipe.name(),
unknown: name,
}));
}
}
let dependencies = recipe
.dependencies
.iter()
.map(|dependency| {
self
.resolve_dependency(dependency, &recipe, stack)?
.ok_or_else(|| {
dependency.recipe.last().error(UnknownDependency {
recipe: recipe.name(),
unknown: dependency.recipe.clone(),
})
})
})
.collect::<CompileResult<Vec<Rc<Recipe>>>>()?;
stack.pop();
@ -139,6 +125,47 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
self.resolved_recipes.insert(Rc::clone(&resolved));
Ok(resolved)
}
fn resolve_dependency(
&mut self,
dependency: &UnresolvedDependency<'src>,
recipe: &UnresolvedRecipe<'src>,
stack: &mut Vec<&'src str>,
) -> CompileResult<'src, Option<Rc<Recipe<'src>>>> {
let name = dependency.recipe.last().lexeme();
if dependency.recipe.components() > 1 {
// recipe is in a submodule and is thus already resovled
Ok(Analyzer::resolve_recipe(
&dependency.recipe,
self.modules,
&self.resolved_recipes,
))
} else if let Some(resolved) = self.resolved_recipes.get(name) {
// recipe is the current module and has already been resolved
Ok(Some(Rc::clone(resolved)))
} else if stack.contains(&name) {
// recipe depends on itself
let first = stack[0];
stack.push(first);
return Err(
dependency.recipe.last().error(CircularRecipeDependency {
recipe: recipe.name(),
circle: stack
.iter()
.skip_while(|name| **name != dependency.recipe.last().lexeme())
.copied()
.collect(),
}),
);
} else if let Some(unresolved) = self.unresolved_recipes.remove(name) {
// recipe is as of yet unresolved
Ok(Some(self.resolve_recipe(stack, unresolved)?))
} else {
// recipe is unknown
Ok(None)
}
}
}
#[cfg(test)]
@ -172,7 +199,18 @@ mod tests {
line: 0,
column: 3,
width: 1,
kind: UnknownDependency{recipe: "a", unknown: "b"},
kind: UnknownDependency{
recipe: "a",
unknown: Namepath::from(Name::from_identifier(
Token{
column: 3,
kind: TokenKind::Identifier,
length: 1,
line: 0,
offset: 3,
path: Path::new("justfile"),
src: "a: b" }))
},
}
analysis_error! {

View file

@ -11,23 +11,18 @@ pub(crate) struct Search {
}
impl Search {
fn global_justfile_paths() -> Vec<PathBuf> {
fn global_justfile_paths() -> Vec<(PathBuf, &'static str)> {
let mut paths = Vec::new();
if let Some(config_dir) = dirs::config_dir() {
paths.push(config_dir.join("just").join(DEFAULT_JUSTFILE_NAME));
paths.push((config_dir.join("just"), DEFAULT_JUSTFILE_NAME));
}
if let Some(home_dir) = dirs::home_dir() {
paths.push(
home_dir
.join(".config")
.join("just")
.join(DEFAULT_JUSTFILE_NAME),
);
paths.push((home_dir.join(".config").join("just"), DEFAULT_JUSTFILE_NAME));
for justfile_name in JUSTFILE_NAMES {
paths.push(home_dir.join(justfile_name));
paths.push((home_dir.clone(), justfile_name));
}
}
@ -51,11 +46,7 @@ impl Search {
})
}
SearchConfig::GlobalJustfile => Ok(Self {
justfile: Self::global_justfile_paths()
.iter()
.find(|path| path.exists())
.cloned()
.ok_or(SearchError::GlobalJustfileNotFound)?,
justfile: Self::find_global_justfile()?,
working_directory: Self::project_root(invocation_directory)?,
}),
SearchConfig::WithJustfile { justfile } => {
@ -76,6 +67,26 @@ impl Search {
}
}
fn find_global_justfile() -> SearchResult<PathBuf> {
for (directory, filename) in Self::global_justfile_paths() {
if let Ok(read_dir) = fs::read_dir(&directory) {
for entry in read_dir {
let entry = entry.map_err(|io_error| SearchError::Io {
io_error,
directory: directory.clone(),
})?;
if let Some(candidate) = entry.file_name().to_str() {
if candidate.eq_ignore_ascii_case(filename) {
return Ok(entry.path());
}
}
}
}
}
Err(SearchError::GlobalJustfileNotFound)
}
/// Find justfile starting from parent directory of current justfile
pub(crate) fn search_parent_directory(&self) -> SearchResult<Self> {
let parent = self
@ -150,6 +161,7 @@ impl Search {
io_error,
directory: directory.to_owned(),
})?;
for entry in entries {
let entry = entry.map_err(|io_error| SearchError::Io {
io_error,

View file

@ -6,6 +6,7 @@ pub(crate) enum Setting<'src> {
AllowDuplicateVariables(bool),
DotenvFilename(StringLiteral<'src>),
DotenvLoad(bool),
DotenvOverride(bool),
DotenvPath(StringLiteral<'src>),
DotenvRequired(bool),
Export(bool),
@ -29,6 +30,7 @@ impl Display for Setting<'_> {
Self::AllowDuplicateRecipes(value)
| Self::AllowDuplicateVariables(value)
| Self::DotenvLoad(value)
| Self::DotenvOverride(value)
| Self::DotenvRequired(value)
| Self::Export(value)
| Self::Fallback(value)

View file

@ -11,6 +11,7 @@ pub(crate) struct Settings<'src> {
pub(crate) allow_duplicate_variables: bool,
pub(crate) dotenv_filename: Option<String>,
pub(crate) dotenv_load: bool,
pub(crate) dotenv_override: bool,
pub(crate) dotenv_path: Option<PathBuf>,
pub(crate) dotenv_required: bool,
pub(crate) export: bool,
@ -50,6 +51,9 @@ impl<'src> Settings<'src> {
Setting::DotenvPath(path) => {
settings.dotenv_path = Some(PathBuf::from(path.cooked));
}
Setting::DotenvOverride(dotenv_overrride) => {
settings.dotenv_override = dotenv_overrride;
}
Setting::DotenvRequired(dotenv_required) => {
settings.dotenv_required = dotenv_required;
}

View file

@ -36,8 +36,23 @@ impl Signal {
Signal::Terminate,
];
pub(crate) fn code(self) -> Option<i32> {
128i32.checked_add(self.number())
pub(crate) fn code(self) -> i32 {
128i32.checked_add(self.number()).unwrap()
}
pub(crate) fn is_fatal(self) -> bool {
match self {
Self::Hangup | Self::Interrupt | Self::Quit | Self::Terminate => true,
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd",
))]
Self::Info => false,
}
}
pub(crate) fn number(self) -> i32 {
@ -108,10 +123,7 @@ impl TryFrom<u8> for Signal {
2 => Ok(Signal::Interrupt),
3 => Ok(Signal::Quit),
15 => Ok(Signal::Terminate),
_ => Err(io::Error::new(
io::ErrorKind::Other,
format!("unexpected signal: {n}"),
)),
_ => Err(io::Error::other(format!("unexpected signal: {n}"))),
}
}
}
@ -128,6 +140,13 @@ mod tests {
}
}
#[test]
fn signals_have_valid_exit_codes() {
for signal in Signal::ALL {
signal.code();
}
}
#[test]
fn signal_numbers_are_correct() {
for &signal in Signal::ALL {

View file

@ -40,20 +40,14 @@ impl SignalHandler {
}
fn interrupt(&mut self, signal: Signal) {
if self.children.is_empty() {
process::exit(signal.code().unwrap_or(1));
}
if signal.is_fatal() {
if self.children.is_empty() {
process::exit(signal.code());
}
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd",
))]
if signal != Signal::Info && self.caught.is_none() {
self.caught = Some(signal);
if self.caught.is_none() {
self.caught = Some(signal);
}
}
match signal {
@ -123,10 +117,7 @@ impl SignalHandler {
let pid = match child.id().try_into() {
Err(err) => {
return (
Err(io::Error::new(
io::ErrorKind::Other,
format!("invalid child PID: {err}"),
)),
Err(io::Error::other(format!("invalid child PID: {err}"))),
None,
)
}

View file

@ -1,6 +1,11 @@
use {super::*, clap_mangen::Man};
const INIT_JUSTFILE: &str = "default:\n echo 'Hello, world!'\n";
pub const INIT_JUSTFILE: &str = "\
# https://just.systems
default:
echo 'Hello, world!'
";
fn backtick_re() -> &'static Regex {
static BACKTICK_RE: OnceLock<Regex> = OnceLock::new();

View file

@ -3,7 +3,7 @@ use super::*;
#[derive(PartialEq, Debug, Clone)]
pub(crate) struct UnresolvedDependency<'src> {
pub(crate) arguments: Vec<Expression<'src>>,
pub(crate) recipe: Name<'src>,
pub(crate) recipe: Namepath<'src>,
}
impl Display for UnresolvedDependency<'_> {

View file

@ -16,21 +16,19 @@ impl<'src> UnresolvedRecipe<'src> {
);
for (unresolved, resolved) in self.dependencies.iter().zip(&resolved) {
assert_eq!(unresolved.recipe.lexeme(), resolved.name.lexeme());
assert_eq!(unresolved.recipe.last().lexeme(), resolved.name.lexeme());
if !resolved
.argument_range()
.contains(&unresolved.arguments.len())
{
return Err(
unresolved
.recipe
.error(CompileErrorKind::DependencyArgumentCountMismatch {
dependency: unresolved.recipe.lexeme(),
found: unresolved.arguments.len(),
min: resolved.min_arguments(),
max: resolved.max_arguments(),
}),
);
return Err(unresolved.recipe.last().error(
CompileErrorKind::DependencyArgumentCountMismatch {
dependency: unresolved.recipe.clone(),
found: unresolved.arguments.len(),
min: resolved.min_arguments(),
max: resolved.max_arguments(),
},
));
}
}

View file

@ -131,6 +131,63 @@ fn unexpected_attribute_argument() {
.run();
}
#[test]
fn multiple_metadata_attributes() {
Test::new()
.justfile(
"
[metadata('example')]
[metadata('sample')]
[no-exit-message]
foo:
exit 1
",
)
.stderr("exit 1\n")
.status(1)
.run();
}
#[test]
fn multiple_metadata_attributes_with_multiple_args() {
Test::new()
.justfile(
"
[metadata('example', 'arg1')]
[metadata('sample', 'argument')]
[no-exit-message]
foo:
exit 1
",
)
.stderr("exit 1\n")
.status(1)
.run();
}
#[test]
fn expected_metadata_attribute_argument() {
Test::new()
.justfile(
"
[metadata]
foo:
exit 1
",
)
.stderr(
"
error: Attribute `metadata` got 0 arguments but takes at least 1 argument
justfile:1:2
1 [metadata]
^^^^^^^^
",
)
.status(1)
.run();
}
#[test]
fn doc_attribute() {
Test::new()

View file

@ -134,7 +134,6 @@ fn no_choosable_recipes() {
}
#[test]
#[ignore]
fn multiple_recipes() {
Test::new()
.arg("--choose")

View file

@ -5,6 +5,12 @@ fn constants_are_defined() {
assert_eval_eq("HEX", "0123456789abcdef");
}
#[test]
fn constants_can_have_different_values_on_windows() {
assert_eval_eq("PATH_SEP", if cfg!(windows) { "\\" } else { "/" });
assert_eval_eq("PATH_VAR_SEP", if cfg!(windows) { ";" } else { ":" });
}
#[test]
fn constants_are_defined_in_recipe_bodies() {
Test::new()

139
tests/dependencies.rs Normal file
View file

@ -0,0 +1,139 @@
use super::*;
#[test]
fn recipe_doubly_nested_module_dependencies() {
Test::new()
.write("foo.just", "mod bar\nbaz: \n @echo FOO")
.write("bar.just", "baz:\n @echo BAZ")
.justfile(
"
mod foo
baz: foo::bar::baz
",
)
.arg("baz")
.stdout("BAZ\n")
.run();
}
#[test]
fn recipe_singly_nested_module_dependencies() {
Test::new()
.write("foo.just", "mod bar\nbaz: \n @echo BAR")
.write("bar.just", "baz:\n @echo BAZ")
.justfile(
"
mod foo
baz: foo::baz
",
)
.arg("baz")
.stdout("BAR\n")
.run();
}
#[test]
fn dependency_not_in_submodule() {
Test::new()
.write("foo.just", "qux: \n @echo QUX")
.justfile(
"
mod foo
baz: foo::baz
",
)
.arg("baz")
.status(1)
.stderr(
"error: Recipe `baz` has unknown dependency `foo::baz`
justfile:2:11
2 baz: foo::baz
^^^
",
)
.run();
}
#[test]
fn dependency_submodule_missing() {
Test::new()
.justfile(
"
foo:
@echo FOO
bar:
@echo BAR
baz: foo::bar
",
)
.arg("baz")
.status(1)
.stderr(
"error: Recipe `baz` has unknown dependency `foo::bar`
justfile:5:11
5 baz: foo::bar
^^^
",
)
.run();
}
#[test]
fn recipe_dependency_on_module_fails() {
Test::new()
.write("foo.just", "mod bar\nbaz: \n @echo BAR")
.write("bar.just", "baz:\n @echo BAZ")
.justfile(
"
mod foo
baz: foo::bar
",
)
.arg("baz")
.status(1)
.stderr(
"error: Recipe `baz` has unknown dependency `foo::bar`
justfile:2:11
2 baz: foo::bar
^^^
",
)
.run();
}
#[test]
fn recipe_module_dependency_subsequent_mix() {
Test::new()
.write("foo.just", "bar: \n @echo BAR")
.justfile(
"
mod foo
baz:
@echo BAZ
quux: foo::bar && baz
@echo QUUX
",
)
.arg("quux")
.stdout("BAR\nQUUX\nBAZ\n")
.run();
}
#[test]
fn recipe_module_dependency_only_runs_once() {
Test::new()
.write("foo.just", "bar: baz \n \nbaz: \n @echo BAZ")
.justfile(
"
mod foo
qux: foo::bar foo::baz
",
)
.arg("qux")
.stdout("BAZ\n")
.run();
}

View file

@ -362,7 +362,7 @@ fn no_dotenv() {
}
#[test]
fn dotenv_env_var_override() {
fn dotenv_env_var_default_no_override() {
Test::new()
.justfile(
"
@ -377,6 +377,41 @@ fn dotenv_env_var_override() {
.run();
}
#[test]
fn dotenv_env_var_override() {
Test::new()
.justfile(
"
set dotenv-load
set dotenv-override := true
echo:
echo $DOTENV_KEY
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.env("DOTENV_KEY", "not-the-dotenv-value")
.stdout("dotenv-value\n")
.stderr("echo $DOTENV_KEY\n")
.run();
}
#[test]
fn dotenv_env_var_override_no_load() {
Test::new()
.justfile(
"
set dotenv-override := true
echo:
echo $DOTENV_KEY
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.env("DOTENV_KEY", "not-the-dotenv-value")
.stdout("dotenv-value\n")
.stderr("echo $DOTENV_KEY\n")
.run();
}
#[test]
fn dotenv_path_usable_from_subdir() {
Test::new()

View file

@ -63,3 +63,20 @@ fn unix() {
.stdout("bar\n")
.run();
}
#[test]
#[cfg(all(unix, not(target_os = "macos")))]
fn case_insensitive() {
let tempdir = tempdir();
let path = tempdir.path().to_owned();
Test::with_tempdir(tempdir)
.no_justfile()
.test_round_trip(false)
.write("just/JUSTFILE", "@default:\n echo foo")
.env("XDG_CONFIG_HOME", path.to_str().unwrap())
.args(["--global-justfile"])
.stdout("foo\n")
.run();
}

View file

@ -1,6 +1,4 @@
use super::*;
const EXPECTED: &str = "default:\n echo 'Hello, world!'\n";
use {super::*, just::INIT_JUSTFILE};
#[test]
fn current_dir() {
@ -16,7 +14,7 @@ fn current_dir() {
assert_eq!(
fs::read_to_string(tmp.path().join("justfile")).unwrap(),
EXPECTED
INIT_JUSTFILE
);
}
@ -72,7 +70,7 @@ fn invocation_directory() {
.arg("--init")
.run();
assert_eq!(fs::read_to_string(justfile_path).unwrap(), EXPECTED);
assert_eq!(fs::read_to_string(justfile_path).unwrap(), INIT_JUSTFILE);
}
#[test]
@ -92,7 +90,7 @@ fn parent_dir() {
assert_eq!(
fs::read_to_string(tmp.path().join("justfile")).unwrap(),
EXPECTED
INIT_JUSTFILE
);
}
@ -112,7 +110,7 @@ fn alternate_marker() {
assert_eq!(
fs::read_to_string(tmp.path().join("justfile")).unwrap(),
EXPECTED
INIT_JUSTFILE
);
}
@ -135,7 +133,7 @@ fn search_directory() {
assert_eq!(
fs::read_to_string(tmp.path().join("sub/justfile")).unwrap(),
EXPECTED
INIT_JUSTFILE
);
}
@ -159,7 +157,7 @@ fn justfile() {
assert_eq!(
fs::read_to_string(tmp.path().join("justfile")).unwrap(),
EXPECTED
INIT_JUSTFILE
);
}
@ -185,7 +183,7 @@ fn justfile_and_working_directory() {
assert_eq!(
fs::read_to_string(tmp.path().join("justfile")).unwrap(),
EXPECTED
INIT_JUSTFILE
);
}

View file

@ -12,7 +12,7 @@ fn convert_native_path(path: &Path) -> String {
#[cfg(windows)]
fn convert_native_path(path: &Path) -> String {
// Translate path from windows style to unix style
let mut cygpath = Command::new("cygpath");
let mut cygpath = Command::new(env::var_os("JUST_CYGPATH").unwrap_or("cygpath".into()));
cygpath.arg("--unix");
cygpath.arg(path);

View file

@ -79,6 +79,7 @@ struct Settings<'a> {
allow_duplicate_variables: bool,
dotenv_filename: Option<&'a str>,
dotenv_load: bool,
dotenv_override: bool,
dotenv_path: Option<&'a str>,
dotenv_required: bool,
export: bool,
@ -782,6 +783,88 @@ fn attribute() {
);
}
#[test]
fn single_metadata_attribute() {
case(
"
[metadata('example')]
foo:
",
Module {
first: Some("foo"),
recipes: [(
"foo",
Recipe {
attributes: [json!({"metadata": ["example"]})].into(),
name: "foo",
namepath: "foo",
..default()
},
)]
.into(),
..default()
},
);
}
#[test]
fn multiple_metadata_attributes() {
case(
"
[metadata('example')]
[metadata('sample')]
foo:
",
Module {
first: Some("foo"),
recipes: [(
"foo",
Recipe {
attributes: [
json!({"metadata": ["example"]}),
json!({"metadata": ["sample"]}),
]
.into(),
name: "foo",
namepath: "foo",
..default()
},
)]
.into(),
..default()
},
);
}
#[test]
fn multiple_metadata_attributes_with_multiple_arguments() {
case(
"
[metadata('example', 'arg1')]
[metadata('sample', 'argument')]
foo:
",
Module {
first: Some("foo"),
recipes: [(
"foo",
Recipe {
attributes: [
json!({"metadata": ["example", "arg1"]}),
json!({"metadata": ["sample", "argument"]}),
]
.into(),
name: "foo",
namepath: "foo",
..default()
},
)]
.into(),
..default()
},
);
}
#[test]
fn module() {
case_with_submodule(

View file

@ -63,6 +63,7 @@ mod confirm;
mod constants;
mod datetime;
mod delimiters;
mod dependencies;
mod directories;
mod dotenv;
mod edit;

View file

@ -86,9 +86,9 @@ fn interrupt_command() {
interrupt_test(&["--command", "sleep", "1"], "");
}
/// This test is sensitive to the process signal mask. Programs like
/// `watchexec` and `cargo-watch` change the signal mask to ignore `SIGHUP`,
/// which causes this test to fail.
// This test is ignored because it is sensitive to the process signal mask.
// Programs like `watchexec` and `cargo-watch` change the signal mask to ignore
// `SIGHUP`, which causes this test to fail.
#[test]
#[ignore]
fn forwarding() {
@ -155,8 +155,6 @@ fn forwarding() {
}
}
/// This test is ignored because it includes a 500ms wait, and because signal
/// tests are often flakey.
#[test]
#[ignore]
#[cfg(any(

View file

@ -16,7 +16,7 @@ pub(crate) fn tempdir() -> TempDir {
}
#[test]
fn test_tempdir_is_set() {
fn setting() {
Test::new()
.justfile(
"
@ -28,10 +28,46 @@ fn test_tempdir_is_set() {
)
.shell(false)
.tree(tree! {
foo: {
bar: {
}
})
.current_dir("foo")
.current_dir("bar")
.stdout(if cfg!(windows) {
"
cat just*/foo
"
} else {
"
#!/usr/bin/env bash
cat just*/foo
"
})
.run();
}
#[test]
fn argument_overrides_setting() {
Test::new()
.args(["--tempdir", "."])
.justfile(
"
set tempdir := 'hello'
foo:
#!/usr/bin/env bash
cat just*/foo
",
)
.shell(false)
.tree(tree! {
bar: {
}
})
.current_dir("bar")
.stdout(if cfg!(windows) {
"

View file

@ -1,11 +1,7 @@
#!/usr/bin/env sh
#!/usr/bin/env bash
set -eu
if [ -n "${GITHUB_ACTIONS-}" ]; then
set -x
fi
# Check pipefail support in a subshell, ignore if unsupported
# shellcheck disable=SC3040
(set -o pipefail 2> /dev/null) && set -o pipefail
@ -55,10 +51,15 @@ download() {
url="$1"
output="$2"
args=()
if [ -n "${GITHUB_TOKEN+x}" ]; then
args+=(--header "Authorization: Bearer $GITHUB_TOKEN")
fi
if command -v curl > /dev/null; then
curl --proto =https --tlsv1.2 -sSfL "$url" "-o$output"
curl --proto =https --tlsv1.2 -sSfL ${args[@]+"${args[@]}"} "$url" -o"$output"
else
wget --https-only --secure-protocol=TLSv1_2 --quiet "$url" "-O$output"
wget --https-only --secure-protocol=TLSv1_2 --quiet ${args[@]+"${args[@]}"} "$url" -O"$output"
fi
}