From e2c0d86bdd3fe7e66195e99d1c0de0eed624be56 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 29 Dec 2023 12:16:31 -0800 Subject: [PATCH] Optional modules and imports (#1797) --- GRAMMAR.md | 14 +- README.md | 929 ++++++++++++++++++++++++++----------- src/analyzer.rs | 31 +- src/assignment_resolver.rs | 6 +- src/compiler.rs | 55 ++- src/error.rs | 5 + src/item.rs | 40 +- src/justfile.rs | 4 +- src/lexer.rs | 46 +- src/lib.rs | 4 +- src/node.rs | 33 +- src/parser.rs | 78 +++- src/recipe_resolver.rs | 2 +- src/token_kind.rs | 2 + src/tree.rs | 4 + tests/imports.rs | 73 +++ tests/misc.rs | 4 +- tests/modules.rs | 68 +++ 18 files changed, 1046 insertions(+), 352 deletions(-) diff --git a/GRAMMAR.md b/GRAMMAR.md index 6a0c40a..920ad84 100644 --- a/GRAMMAR.md +++ b/GRAMMAR.md @@ -43,12 +43,14 @@ grammar ``` justfile : item* EOF -item : recipe - | alias +item : alias | assignment - | export - | setting | eol + | export + | import + | module + | recipe + | setting eol : NEWLINE | COMMENT NEWLINE @@ -72,6 +74,10 @@ setting : 'set' 'allow-duplicate-recipes' boolean? | 'set' 'windows-powershell' boolean? | 'set' 'windows-shell' ':=' '[' string (',' string)* ','? ']' +import : 'import' '?'? string? + +module : 'mod' '?'? NAME string? + boolean : ':=' ('true' | 'false') expression : 'if' condition '{' expression '}' 'else' '{' expression '}' diff --git a/README.md b/README.md index 5ba69f0..8fcf91f 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,11 @@ This readme is also available as a [book](https://just.systems/man/en/). -(中文文档在 [这里](https://github.com/casey/just/blob/master/README.中文.md), 快看过来!) +(中文文档在 [这里](https://github.com/casey/just/blob/master/README.中文.md), +快看过来!) -Commands, called recipes, are stored in a file called `justfile` with syntax inspired by `make`: +Commands, called recipes, are stored in a file called `justfile` with syntax +inspired by `make`: ![screenshot](https://raw.githubusercontent.com/casey/just/master/screenshot.png) @@ -42,40 +44,57 @@ Yay, all your tests passed! `just` has a ton of useful features, and many improvements over `make`: -- `just` is a command runner, not a build system, so it avoids much of [`make`'s complexity and idiosyncrasies](#what-are-the-idiosyncrasies-of-make-that-just-avoids). No need for `.PHONY` recipes! +- `just` is a command runner, not a build system, so it avoids much of + [`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, 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).) -- Errors are specific and informative, and syntax errors are reported along with their source context. +- Errors are specific and informative, and syntax errors are reported along + with their source context. - Recipes can accept [command line arguments](#recipe-parameters). -- Wherever possible, errors are resolved statically. Unknown recipes and circular dependencies are reported before anything runs. +- Wherever possible, errors are resolved statically. Unknown recipes and + circular dependencies are reported before anything runs. -- `just` [loads `.env` files](#dotenv-settings), making it easy to populate environment variables. +- `just` [loads `.env` files](#dotenv-settings), making it easy to populate + environment variables. - Recipes can be [listed from the command line](#listing-available-recipes). -- Command line completion scripts are [available for most popular shells](#shell-completion-scripts). +- Command line completion scripts are + [available for most popular shells](#shell-completion-scripts). -- Recipes can be written in [arbitrary languages](#writing-recipes-in-other-languages), like Python or NodeJS. +- Recipes can be written in + [arbitrary languages](#writing-recipes-in-other-languages), like Python or NodeJS. -- `just` can be invoked from any subdirectory, not just the directory that contains the `justfile`. +- `just` can be invoked from any subdirectory, not just the directory that + contains the `justfile`. - And [much more](https://just.systems/man/en/)! -If you need help with `just` please feel free to open an issue or ping me on [Discord](https://discord.gg/ezYScXR). Feature requests and bug reports are always welcome! +If you need help with `just` please feel free to open an issue or ping me on +[Discord](https://discord.gg/ezYScXR). Feature requests and bug reports are +always welcome! Installation ------------ ### Prerequisites -`just` should run on any system with a reasonable `sh`, including Linux, MacOS, and the BSDs. +`just` should run on any system with a reasonable `sh`, including Linux, MacOS, +and the BSDs. -On Windows, `just` works with the `sh` provided by [Git for Windows](https://git-scm.com), [GitHub Desktop](https://desktop.github.com), or [Cygwin](http://www.cygwin.com). +On Windows, `just` works with the `sh` provided by +[Git for Windows](https://git-scm.com), +[GitHub Desktop](https://desktop.github.com), or +[Cygwin](http://www.cygwin.com). -If you'd rather not install `sh`, you can use the `shell` setting to use the shell of your choice. +If you'd rather not install `sh`, you can use the `shell` setting to use the +shell of your choice. Like PowerShell: @@ -97,9 +116,12 @@ list: dir ``` -You can also set the shell using command-line arguments. For example, to use PowerShell, launch `just` with `--shell powershell.exe --shell-arg -c`. +You can also set the shell using command-line arguments. For example, to use +PowerShell, launch `just` with `--shell powershell.exe --shell-arg -c`. -(PowerShell is installed by default on Windows 7 SP1 and Windows Server 2008 R2 S1 and later, and `cmd.exe` is quite fiddly, so PowerShell is recommended for most Windows users.) +(PowerShell is installed by default on Windows 7 SP1 and Windows Server 2008 R2 +S1 and later, and `cmd.exe` is quite fiddly, so PowerShell is recommended for +most Windows users.) ### Packages @@ -254,9 +276,12 @@ You can also set the shell using command-line arguments. For example, to use Pow ### Pre-Built Binaries -Pre-built binaries for Linux, MacOS, and Windows can be found on [the releases page](https://github.com/casey/just/releases). +Pre-built binaries for Linux, MacOS, and Windows can be found on +[the releases page](https://github.com/casey/just/releases). -You can use the following command on Linux, MacOS, or Windows to download the latest release, just replace `DEST` with the directory where you'd like to put `just`: +You can use the following command on Linux, MacOS, or Windows to download the +latest release, just replace `DEST` with the directory where you'd like to put +`just`: ```sh curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to DEST @@ -308,38 +333,58 @@ An [RSS feed](https://en.wikipedia.org/wiki/RSS) of `just` releases is available ### Node.js Installation -[just-install](https://npmjs.com/package/just-install) can be used to automate installation of `just` in Node.js applications. +[just-install](https://npmjs.com/package/just-install) can be used to automate +installation of `just` in Node.js applications. -`just` is a great, more robust alternative to npm scripts. If you want to include `just` in the dependencies of a Node.js application, `just-install` will install a local, platform-specific binary as part of the `npm install` command. This removes the need for every developer to install `just` independently using one of the processes mentioned above. After installation, the `just` command will work in npm scripts or with npx. It's great for teams who want to make the set up process for their project as easy as possible. +`just` is a great, more robust alternative to npm scripts. If you want to +include `just` in the dependencies of a Node.js application, `just-install` +will install a local, platform-specific binary as part of the `npm install` +command. This removes the need for every developer to install `just` +independently using one of the processes mentioned above. After installation, +the `just` command will work in npm scripts or with npx. It's great for teams +who want to make the set up process for their project as easy as possible. -For more information, see the [just-install README file](https://github.com/brombal/just-install#readme). +For more information, see the +[just-install README file](https://github.com/brombal/just-install#readme). Backwards Compatibility ----------------------- -With the release of version 1.0, `just` features a strong commitment to backwards compatibility and stability. +With the release of version 1.0, `just` features a strong commitment to +backwards compatibility and stability. -Future releases will not introduce backwards incompatible changes that make existing `justfile`s stop working, or break working invocations of the command-line interface. +Future releases will not introduce backwards incompatible changes that make +existing `justfile`s stop working, or break working invocations of the +command-line interface. -This does not, however, preclude fixing outright bugs, even if doing so might break `justfiles` that rely on their behavior. +This does not, however, preclude fixing outright bugs, even if doing so might +break `justfiles` that rely on their behavior. -There will never be a `just` 2.0. Any desirable backwards-incompatible changes will be opt-in on a per-`justfile` basis, so users may migrate at their leisure. - -Features that aren't yet ready for stabilization are gated behind the `--unstable` flag. Features enabled by `--unstable` may change in backwards incompatible ways at any time. Unstable features can also be enabled by setting the environment variable `JUST_UNSTABLE` to any value other than `false`, `0`, or the empty string. +There will never be a `just` 2.0. Any desirable backwards-incompatible changes +will be opt-in on a per-`justfile` basis, so users may migrate at their +leisure. +Features that aren't yet ready for stabilization are gated behind the +`--unstable` flag. Features enabled by `--unstable` may change in backwards +incompatible ways at any time. Unstable features can also be enabled by setting +the environment variable `JUST_UNSTABLE` to any value other than `false`, `0`, +or the empty string. Editor Support -------------- -`justfile` syntax is close enough to `make` that you may want to tell your editor to use `make` syntax highlighting for `just`. +`justfile` syntax is close enough to `make` that you may want to tell your +editor to use `make` syntax highlighting for `just`. ### Vim and Neovim #### `vim-just` -The [vim-just](https://github.com/NoahTheDuke/vim-just) plugin provides syntax highlighting for `justfile`s. +The [vim-just](https://github.com/NoahTheDuke/vim-just) plugin provides syntax +highlighting for `justfile`s. -Install it with your favorite package manager, like [Plug](https://github.com/junegunn/vim-plug): +Install it with your favorite package manager, like +[Plug](https://github.com/junegunn/vim-plug): ```vim call plug#begin() @@ -359,11 +404,14 @@ git clone https://github.com/NoahTheDuke/vim-just.git #### `tree-sitter-just` -[tree-sitter-just](https://github.com/IndianBoy42/tree-sitter-just) is an [Nvim Treesitter](https://github.com/nvim-treesitter/nvim-treesitter) plugin for Neovim. +[tree-sitter-just](https://github.com/IndianBoy42/tree-sitter-just) is an +[Nvim Treesitter](https://github.com/nvim-treesitter/nvim-treesitter) plugin +for Neovim. #### Makefile Syntax Highlighting -Vim's built-in makefile syntax highlighting isn't perfect for `justfile`s, but it's better than nothing. You can put the following in `~/.vim/filetype.vim`: +Vim's built-in makefile syntax highlighting isn't perfect for `justfile`s, but +it's better than nothing. You can put the following in `~/.vim/filetype.vim`: ```vimscript if exists("did_load_filetypes") @@ -375,7 +423,8 @@ augroup filetypedetect augroup END ``` -Or add the following to an individual `justfile` to enable `make` mode on a per-file basis: +Or add the following to an individual `justfile` to enable `make` mode on a +per-file basis: ```text # vim: set ft=make : @@ -383,11 +432,15 @@ Or add the following to an individual `justfile` to enable `make` mode on a per- ### Emacs -[just-mode](https://github.com/leon-barrett/just-mode.el) provides syntax highlighting and automatic indentation of `justfile`s. It is available on [MELPA](https://melpa.org/) as [just-mode](https://melpa.org/#/just-mode). +[just-mode](https://github.com/leon-barrett/just-mode.el) provides syntax +highlighting and automatic indentation of `justfile`s. It is available on +[MELPA](https://melpa.org/) as [just-mode](https://melpa.org/#/just-mode). -[justl](https://github.com/psibi/justl.el) provides commands for executing and listing recipes. +[justl](https://github.com/psibi/justl.el) provides commands for executing and +listing recipes. -You can add the following to an individual `justfile` to enable `make` mode on a per-file basis: +You can add the following to an individual `justfile` to enable `make` mode on +a per-file basis: ```text # Local Variables: @@ -397,7 +450,10 @@ You can add the following to an individual `justfile` to enable `make` mode on a ### Visual Studio Code -An extension for VS Code by [skellock](https://github.com/skellock) is [available here](https://marketplace.visualstudio.com/items?itemName=skellock.just) ([repository](https://github.com/skellock/vscode-just)), but is no longer actively developed. +An extension for VS Code by [skellock](https://github.com/skellock) is +[available here](https://marketplace.visualstudio.com/items?itemName=skellock.just) +([repository](https://github.com/skellock/vscode-just)), but is no longer +actively developed. You can install it from the command line by running: @@ -405,40 +461,52 @@ You can install it from the command line by running: code --install-extension skellock.just ``` -An more recently active fork by [sclu1034](https://github.com/sclu1034) is available [here](https://github.com/sclu1034/vscode-just). +An more recently active fork by [sclu1034](https://github.com/sclu1034) is +available [here](https://github.com/sclu1034/vscode-just). ### JetBrains IDEs -A plugin for JetBrains IDEs by [linux_china](https://github.com/linux-china) is [available here](https://plugins.jetbrains.com/plugin/18658-just). +A plugin for JetBrains IDEs by [linux_china](https://github.com/linux-china) is +[available here](https://plugins.jetbrains.com/plugin/18658-just). ### Kakoune -Kakoune supports `justfile` syntax highlighting out of the box, thanks to TeddyDD. +Kakoune supports `justfile` syntax highlighting out of the box, thanks to +TeddyDD. ### Helix -[Helix](https://helix-editor.com/) supports `justfile` syntax highlighting out-of-the-box since version 23.05. +[Helix](https://helix-editor.com/) supports `justfile` syntax highlighting +out-of-the-box since version 23.05. ### Sublime Text -The [Just package](https://github.com/nk9/just_sublime) by [nk9](https://github.com/nk9) with `just` syntax and some other tools is available on [PackageControl](https://packagecontrol.io/packages/Just). +The [Just package](https://github.com/nk9/just_sublime) by +[nk9](https://github.com/nk9) with `just` syntax and some other tools is +available on [PackageControl](https://packagecontrol.io/packages/Just). ### Micro -[Micro](https://micro-editor.github.io/) supports Justfile syntax highlighting out of the box, thanks to [tomodachi94](https://github.com/tomodachi94). +[Micro](https://micro-editor.github.io/) supports Justfile syntax highlighting +out of the box, thanks to [tomodachi94](https://github.com/tomodachi94). ### Other Editors -Feel free to send me the commands necessary to get syntax highlighting working in your editor of choice so that I may include them here. +Feel free to send me the commands necessary to get syntax highlighting working +in your editor of choice so that I may include them here. Quick Start ----------- -See [the installation section](#installation) for how to install `just` on your computer. Try running `just --version` to make sure that it's installed correctly. +See [the installation section](#installation) for how to install `just` on your +computer. Try running `just --version` to make sure that it's installed +correctly. -For an overview of the syntax, check out [this cheatsheet](https://cheatography.com/linux-china/cheat-sheets/justfile/). +For an overview of the syntax, check out +[this cheatsheet](https://cheatography.com/linux-china/cheat-sheets/justfile/). -Once `just` is installed and working, create a file named `justfile` in the root of your project with the following contents: +Once `just` is installed and working, create a file named `justfile` in the +root of your project with the following contents: ```just recipe-name: @@ -449,9 +517,12 @@ another-recipe: @echo 'This is another recipe.' ``` -When you invoke `just` it looks for file `justfile` in the current directory and upwards, so you can invoke it from any subdirectory of your project. +When you invoke `just` it looks for file `justfile` in the current directory +and upwards, so you can invoke it from any subdirectory of your project. -The search for a `justfile` is case insensitive, so any case, like `Justfile`, `JUSTFILE`, or `JuStFiLe`, will work. `just` will also look for files with the name `.justfile`, in case you'd like to hide a `justfile`. +The search for a `justfile` is case insensitive, so any case, like `Justfile`, +`JUSTFILE`, or `JuStFiLe`, will work. `just` will also look for files with the +name `.justfile`, in case you'd like to hide a `justfile`. Running `just` with no arguments runs the first recipe in the `justfile`: @@ -468,9 +539,12 @@ $ just another-recipe This is another recipe. ``` -`just` prints each command to standard error before running it, which is why `echo 'This is a recipe!'` was printed. This is suppressed for lines starting with `@`, which is why `echo 'This is another recipe.'` was not printed. +`just` prints each command to standard error before running it, which is why +`echo 'This is a recipe!'` was printed. This is suppressed for lines starting +with `@`, which is why `echo 'This is another recipe.'` was not printed. -Recipes stop running if a command fails. Here `cargo publish` will only run if `cargo test` succeeds: +Recipes stop running if a command fails. Here `cargo publish` will only run if +`cargo test` succeeds: ```just publish: @@ -479,7 +553,8 @@ publish: cargo publish ``` -Recipes can depend on other recipes. Here the `test` recipe depends on the `build` recipe, so `build` will run before `test`: +Recipes can depend on other recipes. Here the `test` recipe depends on the +`build` recipe, so `build` will run before `test`: ```just build: @@ -499,7 +574,8 @@ cc main.c foo.c bar.c -o main testing… all tests passed! ``` -Recipes without dependencies will run in the order they're given on the command line: +Recipes without dependencies will run in the order they're given on the command +line: ```sh $ just build sloc @@ -507,7 +583,8 @@ cc main.c foo.c bar.c -o main 1337 lines of code ``` -Dependencies will always run first, even if they are passed after a recipe that depends on them: +Dependencies will always run first, even if they are passed after a recipe that +depends on them: ```sh $ just test build @@ -519,14 +596,17 @@ testing… all tests passed! Examples -------- -A variety of example `justfile`s can be found in the [examples directory](https://github.com/casey/just/tree/master/examples). +A variety of example `justfile`s can be found in the +[examples directory](https://github.com/casey/just/tree/master/examples). Features -------- ### The Default Recipe -When `just` is invoked without a recipe, it runs the first recipe in the `justfile`. This recipe might be the most frequently run command in the project, like running the tests: +When `just` is invoked without a recipe, it runs the first recipe in the +`justfile`. This recipe might be the most frequently run command in the +project, like running the tests: ```just test: @@ -548,7 +628,8 @@ lint: echo Linting… ``` -If no recipe makes sense as the default recipe, you can add a recipe to the beginning of your `justfile` that lists the available recipes: +If no recipe makes sense as the default recipe, you can add a recipe to the +beginning of your `justfile` that lists the available recipes: ```just default: @@ -597,14 +678,20 @@ $ just --summary --unsorted test build ``` -If you'd like `just` to default to listing the recipes in the `justfile`, you can use this as your default recipe: +If you'd like `just` to default to listing the recipes in the `justfile`, you +can use this as your default recipe: ```just default: @just --list ``` -Note that you may need to add `--justfile {{justfile()}}` to the line above above. Without it, if you executed `just -f /some/distant/justfile -d .` or `just -f ./non-standard-justfile`, the plain `just --list` inside the recipe would not necessarily use the file you provided. It would try to find a justfile in your current path, maybe even resulting in a `No justfile found` error. +Note that you may need to add `--justfile {{justfile()}}` to the line above +above. Without it, if you executed `just -f /some/distant/justfile -d .` or +`just -f ./non-standard-justfile`, the plain `just --list` inside the recipe +would not necessarily use the file you provided. It would try to find a +justfile in your current path, maybe even resulting in a `No justfile found` +error. The heading text can be customized with `--list-heading`: @@ -624,7 +711,9 @@ Available recipes: ····build ``` -The argument to `--list-heading` replaces both the heading and the newline following it, so it should contain a newline if non-empty. It works this way so you can suppress the heading line entirely by passing the empty string: +The argument to `--list-heading` replaces both the heading and the newline +following it, so it should contain a newline if non-empty. It works this way so +you can suppress the heading line entirely by passing the empty string: ```sh $ just --list --list-heading '' @@ -651,7 +740,8 @@ Building! ### Settings -Settings control interpretation and execution. Each setting may be specified at most once, anywhere in the `justfile`. +Settings control interpretation and execution. Each setting may be specified at +most once, anywhere in the `justfile`. For example: @@ -665,20 +755,20 @@ foo: #### Table of Settings -| Name | Value | Default | Description | -| ------------------------- | ------------------ | ------- |---------------------------------------------------------------------------------------------- | -| `allow-duplicate-recipes` | boolean | `false` | Allow recipes appearing later in a `justfile` to override earlier recipes 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-path` | string | - | Load a `.env` file from a custom path, if present. Overrides `dotenv-filename`. | -| `export` | boolean | `false` | Export all variables as environment variables. | -| `fallback` | boolean | `false` | Search `justfile` in parent directory if the first recipe on the command line is not found. | -| `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. | -| `positional-arguments` | boolean | `false` | Pass positional arguments. | -| `shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. | -| `tempdir` | string | - | Create temporary directories in `tempdir` instead of the system default temporary directory. | -| `windows-powershell` | boolean | `false` | Use PowerShell on Windows as default shell. (Deprecated. Use `windows-shell` instead. | -| `windows-shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. | +| Name | Value | Default | Description | +| -----| ------| ------- |-------------| +| `allow-duplicate-recipes` | boolean | `false` | Allow recipes appearing later in a `justfile` to override earlier recipes 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-path` | string | - | Load a `.env` file from a custom path, if present. Overrides `dotenv-filename`. | +| `export` | boolean | `false` | Export all variables as environment variables. | +| `fallback` | boolean | `false` | Search `justfile` in parent directory if the first recipe on the command line is not found. | +| `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. | +| `positional-arguments` | boolean | `false` | Pass positional arguments. | +| `shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. | +| `tempdir` | string | - | Create temporary directories in `tempdir` instead of the system default temporary directory. | +| `windows-powershell` | boolean | `false` | Use PowerShell on Windows as default shell. (Deprecated. Use `windows-shell` instead. | +| `windows-shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. | Boolean settings can be written as: @@ -694,7 +784,9 @@ set NAME := true #### Allow Duplicate Recipes -If `allow-duplicate-recipes` is set to `true`, defining multiple recipes with the same name is not an error and the last definition is used. Defaults to `false`. +If `allow-duplicate-recipes` is set to `true`, defining multiple recipes with +the same name is not an error and the last definition is used. Defaults to +`false`. ```just set allow-duplicate-recipes @@ -713,13 +805,18 @@ bar #### Dotenv Settings -If `dotenv-load`, `dotenv-filename` or `dotenv-path` is set, `just` will load environment variables from a file. +If `dotenv-load`, `dotenv-filename` or `dotenv-path` is set, `just` will load +environment variables from a file. If `dotenv-path` is set, `just` will look for a file at the given path. -Otherwise, `just` looks for a file named `.env` by default, unless `dotenv-filename` set, in which case the value of `dotenv-filename` is used. This file can be located in the same directory as your `justfile` or in a parent directory. +Otherwise, `just` looks for a file named `.env` by default, unless +`dotenv-filename` set, in which case the value of `dotenv-filename` is used. +This file can be located in the same directory as your `justfile` or in a +parent directory. -The loaded variables are environment variables, not `just` variables, and so must be accessed using `$VARIABLE_NAME` in recipes and backticks. +The loaded variables are environment variables, not `just` variables, and so +must be accessed using `$VARIABLE_NAME` in recipes and backticks. For example, if your `.env` file contains: @@ -749,7 +846,8 @@ Starting server with database localhost:6379 on port 1337… #### Export -The `export` setting causes all `just` variables to be exported as environment variables. Defaults to `false`. +The `export` setting causes all `just` variables to be exported as environment +variables. Defaults to `false`. ```just set export @@ -769,7 +867,9 @@ goodbye #### Positional Arguments -If `positional-arguments` is `true`, recipe arguments will be passed as positional arguments to commands. For linewise recipes, argument `$0` will be the name of the recipe. +If `positional-arguments` is `true`, recipe arguments will be passed as +positional arguments to commands. For linewise recipes, argument `$0` will be +the name of the recipe. For example, running this recipe: @@ -789,7 +889,12 @@ foo hello ``` -When using an `sh`-compatible shell, such as `bash` or `zsh`, `$@` expands to the positional arguments given to the recipe, starting from one. When used within double quotes as `"$@"`, arguments including whitespace will be passed on as if they were double-quoted. That is, `"$@"` is equivalent to `"$1" "$2"`… When there are no positional parameters, `"$@"` and `$@` expand to nothing (i.e., they are removed). +When using an `sh`-compatible shell, such as `bash` or `zsh`, `$@` expands to +the positional arguments given to the recipe, starting from one. When used +within double quotes as `"$@"`, arguments including whitespace will be passed +on as if they were double-quoted. That is, `"$@"` is equivalent to `"$1" "$2"`… +When there are no positional parameters, `"$@"` and `$@` expand to nothing +(i.e., they are removed). This example recipe will print arguments one by one on separate lines: @@ -810,7 +915,8 @@ $ just test foo "bar baz" #### Shell -The `shell` setting controls the command used to invoke recipe lines and backticks. Shebang recipes are unaffected. +The `shell` setting controls the command used to invoke recipe lines and +backticks. Shebang recipes are unaffected. ```just # use python3 to execute recipe lines and backticks @@ -824,11 +930,13 @@ foo: print("{{foos}}") ``` -`just` passes the command to be executed as an argument. Many shells will need an additional flag, often `-c`, to make them evaluate the first argument. +`just` passes the command to be executed as an argument. Many shells will need +an additional flag, often `-c`, to make them evaluate the first argument. ##### Windows Shell -`just` uses `sh` on Windows by default. To use a different shell on Windows, use `windows-shell`: +`just` uses `sh` on Windows by default. To use a different shell on Windows, +use `windows-shell`: ```just set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] @@ -837,13 +945,18 @@ hello: Write-Host "Hello, world!" ``` -See [powershell.just](https://github.com/casey/just/blob/master/examples/powershell.just) for a justfile that uses PowerShell on all platforms. +See +[powershell.just](https://github.com/casey/just/blob/master/examples/powershell.just) +for a justfile that uses PowerShell on all platforms. ##### Windows PowerShell -*`set windows-powershell` uses the legacy `powershell.exe` binary, and is no longer recommended. See the `windows-shell` setting above for a more flexible way to control which shell is used on Windows.* +*`set windows-powershell` uses the legacy `powershell.exe` binary, and is no +longer recommended. See the `windows-shell` setting above for a more flexible +way to control which shell is used on Windows.* -`just` uses `sh` on Windows by default. To use `powershell.exe` instead, set `windows-powershell` to true. +`just` uses `sh` on Windows by default. To use `powershell.exe` instead, set +`windows-powershell` to true. ```just set windows-powershell := true @@ -888,7 +1001,8 @@ If you want to change the default table mode to `light`: set shell := ['nu', '-m', 'light', '-c'] ``` -*[Nushell](https://github.com/nushell/nushell) was written in Rust, and **has cross-platform support for Windows / macOS and Linux**.* +*[Nushell](https://github.com/nushell/nushell) was written in Rust, and **has +cross-platform support for Windows / macOS and Linux**.* ### Documentation Comments @@ -913,7 +1027,8 @@ Available recipes: ### Variables and Substitution -Variables, strings, concatenation, path joining, and substitution using `{{…}}` are supported: +Variables, strings, concatenation, path joining, and substitution using `{{…}}` +are supported: ```just tmpdir := `mktemp -d` @@ -966,7 +1081,10 @@ $ just --evaluate foo /b ``` -The `/` operator uses the `/` character, even on Windows. Thus, using the `/` operator should be avoided with paths that use universal naming convention (UNC), i.e., those that start with `\?`, since forward slashes are not supported with UNC paths. +The `/` operator uses the `/` character, even on Windows. Thus, using the `/` +operator should be avoided with paths that use universal naming convention +(UNC), i.e., those that start with `\?`, since forward slashes are not +supported with UNC paths. #### Escaping `{{` @@ -979,7 +1097,8 @@ braces: (An unmatched `}}` is ignored, so it doesn't need to be escaped.) -Another option is to put all the text you'd like to escape inside of an interpolation: +Another option is to put all the text you'd like to escape inside of an +interpolation: ```just braces: @@ -1041,7 +1160,10 @@ $ just --evaluate escapes := "\t\n\r\"\\" ``` -Indented versions of both single- and double-quoted strings, delimited by triple single- or triple double-quotes, are supported. Indented string lines are stripped of a leading line break, and leading whitespace common to all non-blank lines: +Indented versions of both single- and double-quoted strings, delimited by +triple single- or triple double-quotes, are supported. Indented string lines +are stripped of a leading line break, and leading whitespace common to all +non-blank lines: ```just # this string will evaluate to `foo\nbar\n` @@ -1058,11 +1180,17 @@ y := """ """ ``` -Similar to unindented strings, indented double-quoted strings process escape sequences, and indented single-quoted strings ignore escape sequences. Escape sequence processing takes place after unindentation. The unindentation algorithm does not take escape-sequence produced whitespace or newlines into account. +Similar to unindented strings, indented double-quoted strings process escape +sequences, and indented single-quoted strings ignore escape sequences. Escape +sequence processing takes place after unindentation. The unindentation +algorithm does not take escape-sequence produced whitespace or newlines into +account. ### Ignoring Errors -Normally, if a command returns a non-zero exit status, execution will stop. To continue execution after a command, even if it fails, prefix the command with `-`: +Normally, if a command returns a non-zero exit status, execution will stop. To +continue execution after a command, even if it fails, prefix the command with +`-`: ```just foo: @@ -1080,14 +1208,21 @@ Done! ### Functions -`just` provides a few built-in functions that might be useful when writing recipes. +`just` provides a few built-in functions that might be useful when writing +recipes. #### System Information -- `arch()` — Instruction set architecture. Possible values are: `"aarch64"`, `"arm"`, `"asmjs"`, `"hexagon"`, `"mips"`, `"msp430"`, `"powerpc"`, `"powerpc64"`, `"s390x"`, `"sparc"`, `"wasm32"`, `"x86"`, `"x86_64"`, and `"xcore"`. +- `arch()` — Instruction set architecture. Possible values are: `"aarch64"`, + `"arm"`, `"asmjs"`, `"hexagon"`, `"mips"`, `"msp430"`, `"powerpc"`, + `"powerpc64"`, `"s390x"`, `"sparc"`, `"wasm32"`, `"x86"`, `"x86_64"`, and + `"xcore"`. - `num_cpus()`1.15.0 - Number of logical CPUs. -- `os()` — Operating system. Possible values are: `"android"`, `"bitrig"`, `"dragonfly"`, `"emscripten"`, `"freebsd"`, `"haiku"`, `"ios"`, `"linux"`, `"macos"`, `"netbsd"`, `"openbsd"`, `"solaris"`, and `"windows"`. -- `os_family()` — Operating system family; possible values are: `"unix"` and `"windows"`. +- `os()` — Operating system. Possible values are: `"android"`, `"bitrig"`, + `"dragonfly"`, `"emscripten"`, `"freebsd"`, `"haiku"`, `"ios"`, `"linux"`, + `"macos"`, `"netbsd"`, `"openbsd"`, `"solaris"`, and `"windows"`. +- `os_family()` — Operating system family; possible values are: `"unix"` and + `"windows"`. For example: @@ -1101,11 +1236,15 @@ $ just system-info This is an x86_64 machine ``` -The `os_family()` function can be used to create cross-platform `justfile`s that work on various operating systems. For an example, see [cross-platform.just](https://github.com/casey/just/blob/master/examples/cross-platform.just) file. +The `os_family()` function can be used to create cross-platform `justfile`s +that work on various operating systems. For an example, see +[cross-platform.just](https://github.com/casey/just/blob/master/examples/cross-platform.just) +file. #### Environment Variables -- `env_var(key)` — Retrieves the environment variable with name `key`, aborting if it is not present. +- `env_var(key)` — Retrieves the environment variable with name `key`, aborting + if it is not present. ```just home_dir := env_var('HOME') @@ -1119,8 +1258,8 @@ $ just /home/user1 ``` -- `env_var_or_default(key, default)` — Retrieves the environment variable with name `key`, returning `default` if it is not present. - +- `env_var_or_default(key, default)` — Retrieves the environment variable with + name `key`, returning `default` if it is not present. - `env(key)`1.15.0 — Alias for `env_var(key)`. - `env(key, default)`1.15.0 — Alias for `env_var_or_default(key, default)`. @@ -1157,9 +1296,11 @@ build: - `justfile()` - Retrieves the path of the current `justfile`. -- `justfile_directory()` - Retrieves the path of the parent directory of the current `justfile`. +- `justfile_directory()` - Retrieves the path of the parent directory of the + current `justfile`. -For example, to run a command relative to the location of the current `justfile`: +For example, to run a command relative to the location of the current +`justfile`: ```just script: @@ -1184,20 +1325,30 @@ The executable is at: /bin/just #### String Manipulation -- `quote(s)` - Replace all single quotes with `'\''` and prepend and append single quotes to `s`. This is sufficient to escape special characters for many shells, including most Bourne shell descendants. +- `quote(s)` - Replace all single quotes with `'\''` and prepend and append + single quotes to `s`. This is sufficient to escape special characters for + many shells, including most Bourne shell descendants. - `replace(s, from, to)` - Replace all occurrences of `from` in `s` to `to`. -- `replace_regex(s, regex, replacement)` - Replace all occurrences of `regex` in `s` to `replacement`. Regular expressions are provided by the [Rust `regex` crate](https://docs.rs/regex/latest/regex/). See the [syntax documentation](https://docs.rs/regex/latest/regex/#syntax) for usage examples. Capture groups are supported. The `replacement` string uses [Replacement string syntax](https://docs.rs/regex/latest/regex/struct.Regex.html#replacement-string-syntax). +- `replace_regex(s, regex, replacement)` - Replace all occurrences of `regex` + in `s` to `replacement`. Regular expressions are provided by the + [Rust `regex` crate](https://docs.rs/regex/latest/regex/). See the + [syntax documentation](https://docs.rs/regex/latest/regex/#syntax) for usage + examples. Capture groups are supported. The `replacement` string uses + [Replacement string syntax](https://docs.rs/regex/latest/regex/struct.Regex.html#replacement-string-syntax). - `trim(s)` - Remove leading and trailing whitespace from `s`. - `trim_end(s)` - Remove trailing whitespace from `s`. - `trim_end_match(s, pat)` - Remove suffix of `s` matching `pat`. -- `trim_end_matches(s, pat)` - Repeatedly remove suffixes of `s` matching `pat`. +- `trim_end_matches(s, pat)` - Repeatedly remove suffixes of `s` matching + `pat`. - `trim_start(s)` - Remove leading whitespace from `s`. - `trim_start_match(s, pat)` - Remove prefix of `s` matching `pat`. -- `trim_start_matches(s, pat)` - Repeatedly remove prefixes of `s` matching `pat`. +- `trim_start_matches(s, pat)` - Repeatedly remove prefixes of `s` matching + `pat`. #### Case Conversion -- `capitalize(s)`1.7.0 - Convert first character of `s` to uppercase and the rest to lowercase. +- `capitalize(s)`1.7.0 - Convert first character of `s` to uppercase + and the rest to lowercase. - `kebabcase(s)`1.7.0 - Convert `s` to `kebab-case`. - `lowercamelcase(s)`1.7.0 - Convert `s` to `lowerCamelCase`. - `lowercase(s)` - Convert `s` to lowercase. @@ -1212,23 +1363,38 @@ The executable is at: /bin/just ##### Fallible -- `absolute_path(path)` - Absolute path to relative `path` in the working directory. `absolute_path("./bar.txt")` in directory `/foo` is `/foo/bar.txt`. -- `extension(path)` - Extension of `path`. `extension("/foo/bar.txt")` is `txt`. -- `file_name(path)` - File name of `path` with any leading directory components removed. `file_name("/foo/bar.txt")` is `bar.txt`. -- `file_stem(path)` - File name of `path` without extension. `file_stem("/foo/bar.txt")` is `bar`. -- `parent_directory(path)` - Parent directory of `path`. `parent_directory("/foo/bar.txt")` is `/foo`. -- `without_extension(path)` - `path` without extension. `without_extension("/foo/bar.txt")` is `/foo/bar`. +- `absolute_path(path)` - Absolute path to relative `path` in the working + directory. `absolute_path("./bar.txt")` in directory `/foo` is `/foo/bar.txt`. +- `extension(path)` - Extension of `path`. `extension("/foo/bar.txt")` is + `txt`. +- `file_name(path)` - File name of `path` with any leading directory components + removed. `file_name("/foo/bar.txt")` is `bar.txt`. +- `file_stem(path)` - File name of `path` without extension. + `file_stem("/foo/bar.txt")` is `bar`. +- `parent_directory(path)` - Parent directory of `path`. + `parent_directory("/foo/bar.txt")` is `/foo`. +- `without_extension(path)` - `path` without extension. + `without_extension("/foo/bar.txt")` is `/foo/bar`. -These functions can fail, for example if a path does not have an extension, which will halt execution. +These functions can fail, for example if a path does not have an extension, +which will halt execution. ##### Infallible -- `clean(path)` - Simplify `path` by removing extra path separators, intermediate `.` components, and `..` where possible. `clean("foo//bar")` is `foo/bar`, `clean("foo/..")` is `.`, `clean("foo/./bar")` is `foo/bar`. -- `join(a, b…)` - *This function uses `/` on Unix and `\` on Windows, which can be lead to unwanted behavior. The `/` operator, e.g., `a / b`, which always uses `/`, should be considered as a replacement unless `\`s are specifically desired on Windows.* Join path `a` with path `b`. `join("foo/bar", "baz")` is `foo/bar/baz`. Accepts two or more arguments. +- `clean(path)` - Simplify `path` by removing extra path separators, + intermediate `.` components, and `..` where possible. `clean("foo//bar")` is + `foo/bar`, `clean("foo/..")` is `.`, `clean("foo/./bar")` is `foo/bar`. +- `join(a, b…)` - *This function uses `/` on Unix and `\` on Windows, which can + be lead to unwanted behavior. The `/` operator, e.g., `a / b`, which always + uses `/`, should be considered as a replacement unless `\`s are specifically + desired on Windows.* Join path `a` with path `b`. `join("foo/bar", "baz")` is + `foo/bar/baz`. Accepts two or more arguments. #### Filesystem Access -- `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. +- `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. ##### Error Reporting @@ -1236,28 +1402,33 @@ These functions can fail, for example if a path does not have an extension, whic #### UUID and Hash Generation -- `sha256(string)` - Return the SHA-256 hash of `string` as a hexadecimal string. -- `sha256_file(path)` - Return the SHA-256 hash of the file at `path` as a hexadecimal string. +- `sha256(string)` - Return the SHA-256 hash of `string` as a hexadecimal + string. +- `sha256_file(path)` - Return the SHA-256 hash of the file at `path` as a + hexadecimal string. - `uuid()` - Return a randomly generated UUID. #### Semantic Versions -- `semver_matches(version, requirement)`1.16.0 - Check whether a [semantic `version`](https://semver.org), e.g., `"0.1.0"` matches a `requirement`, e.g., `">=0.1.0"`, returning `"true"` if so and `"false"` otherwise. +- `semver_matches(version, requirement)`1.16.0 - Check whether a + [semantic `version`](https://semver.org), e.g., `"0.1.0"` matches a + `requirement`, e.g., `">=0.1.0"`, returning `"true"` if so and `"false"` + otherwise. ### Recipe Attributes Recipes may be annotated with attributes that change their behavior. -| Name | Description | -| ----------------------------------- | ----------------------------------------------- | -| `[confirm]`1.17.0 | Require confirmation prior to executing recipe. | -| `[linux]`1.8.0 | Enable recipe on Linux. | -| `[macos]`1.8.0 | Enable recipe on MacOS. | -| `[no-cd]`1.9.0 | Don't change directory before executing recipe. | -| `[no-exit-message]`1.7.0 | Don't print an error message if recipe fails. | -| `[private]`1.10.0 | See [Private Recipes](#private-recipes). | -| `[unix]`1.8.0 | Enable recipe on Unixes. (Includes MacOS). | -| `[windows]`1.8.0 | Enable recipe on Windows. | +| Name | Description | +| -----| ------------| +| `[confirm]`1.17.0 | Require confirmation prior to executing recipe. | +| `[linux]`1.8.0 | Enable recipe on Linux. | +| `[macos]`1.8.0 | Enable recipe on MacOS. | +| `[no-cd]`1.9.0 | Don't change directory before executing recipe. | +| `[no-exit-message]`1.7.0 | Don't print an error message if recipe fails. | +| `[private]`1.10.0 | See [Private Recipes](#private-recipes). | +| `[unix]`1.8.0 | Enable recipe on Unixes. (Includes MacOS). | +| `[windows]`1.8.0 | Enable recipe on Windows. | A recipe can have multiple attributes, either on multiple lines: @@ -1349,7 +1520,8 @@ serve: ./serve {{localhost}} 8080 ``` -Indented backticks, delimited by three backticks, are de-indented in the same manner as indented strings: +Indented backticks, delimited by three backticks, are de-indented in the same +manner as indented strings: ````just # This backtick evaluates the command `echo foo\necho bar\n`, which produces the value `foo\nbar\n`. @@ -1361,11 +1533,13 @@ stuff := ``` See the [Strings](#strings) section for details on unindenting. -Backticks may not start with `#!`. This syntax is reserved for a future upgrade. +Backticks may not start with `#!`. This syntax is reserved for a future +upgrade. ### Conditional Expressions -`if`/`else` expressions evaluate different branches depending on if two expressions evaluate to the same value: +`if`/`else` expressions evaluate different branches depending on if two +expressions evaluate to the same value: ```just foo := if "2" == "2" { "Good!" } else { "1984" } @@ -1407,9 +1581,15 @@ $ just bar match ``` -Regular expressions are provided by the [regex crate](https://github.com/rust-lang/regex), whose syntax is documented on [docs.rs](https://docs.rs/regex/1.5.4/regex/#syntax). Since regular expressions commonly use backslash escape sequences, consider using single-quoted string literals, which will pass slashes to the regex parser unmolested. +Regular expressions are provided by the +[regex crate](https://github.com/rust-lang/regex), whose syntax is documented on +[docs.rs](https://docs.rs/regex/1.5.4/regex/#syntax). Since regular expressions +commonly use backslash escape sequences, consider using single-quoted string +literals, which will pass slashes to the regex parser unmolested. -Conditional expressions short-circuit, which means they only evaluate one of their branches. This can be used to make sure that backtick expressions don't run when they shouldn't. +Conditional expressions short-circuit, which means they only evaluate one of +their branches. This can be used to make sure that backtick expressions don't +run when they shouldn't. ```just foo := if env_var("RELEASE") == "true" { `get-something-from-release-database` } else { "dummy-value" } @@ -1422,7 +1602,8 @@ bar foo: echo {{ if foo == "bar" { "hello" } else { "goodbye" } }} ``` -Note the space after the final `}`! Without the space, the interpolation will be prematurely closed. +Note the space after the final `}`! Without the space, the interpolation will +be prematurely closed. Multiple conditionals can be chained: @@ -1506,7 +1687,8 @@ $ just --set os bsd #### Exporting `just` Variables -Assignments prefixed with the `export` keyword will be exported to recipes as environment variables: +Assignments prefixed with the `export` keyword will be exported to recipes as +environment variables: ```just export RUST_BACKTRACE := "1" @@ -1538,11 +1720,13 @@ a $A $B=`echo $A`: echo $A $B ``` -When [export](#export) is set, all `just` variables are exported as environment variables. +When [export](#export) is set, all `just` variables are exported as environment +variables. #### Getting Environment Variables from the environment -Environment variables from the environment are passed automatically to the recipes. +Environment variables from the environment are passed automatically to the +recipes. ```just print_home_folder: @@ -1556,12 +1740,14 @@ HOME is '/home/myuser' #### Setting `just` Variables from Environment Variables -Environment variables can be propagated to `just` variables using the functions `env_var()` and `env_var_or_default()`. -See [environment-variables](#environment-variables). +Environment variables can be propagated to `just` variables using the functions +`env_var()` and `env_var_or_default()`. See +[environment-variables](#environment-variables). ### Recipe Parameters -Recipes may have parameters. Here recipe `build` has a parameter called `target`: +Recipes may have parameters. Here recipe `build` has a parameter called +`target`: ```just build target: @@ -1577,7 +1763,8 @@ Building my-awesome-project… cd my-awesome-project && make ``` -To pass arguments to a dependency, put the dependency in parentheses along with the arguments: +To pass arguments to a dependency, put the dependency in parentheses along with +the arguments: ```just default: (build "main") @@ -1599,7 +1786,8 @@ _build version: build: (_build target) ``` -A command's arguments can be passed to dependency by putting the dependency in parentheses along with the arguments: +A command's arguments can be passed to dependency by putting the dependency in +parentheses along with the arguments: ```just build target: @@ -1635,7 +1823,8 @@ Testing server:unit… ./test --tests unit server ``` -Default values may be arbitrary expressions, but concatenations or path joins must be parenthesized: +Default values may be arbitrary expressions, but concatenations or path joins +must be parenthesized: ```just arch := "wasm" @@ -1644,14 +1833,16 @@ test triple=(arch + "-unknown-unknown") input=(arch / "input.dat"): ./test {{triple}} ``` -The last parameter of a recipe may be variadic, indicated with either a `+` or a `*` before the argument name: +The last parameter of a recipe may be variadic, indicated with either a `+` or +a `*` before the argument name: ```just backup +FILES: scp {{FILES}} me@server.com: ``` -Variadic parameters prefixed with `+` accept _one or more_ arguments and expand to a string containing those arguments separated by spaces: +Variadic parameters prefixed with `+` accept _one or more_ arguments and expand +to a string containing those arguments separated by spaces: ```sh $ just backup FAQ.md GRAMMAR.md @@ -1660,21 +1851,25 @@ FAQ.md 100% 1831 1.8KB/s 00:00 GRAMMAR.md 100% 1666 1.6KB/s 00:00 ``` -Variadic parameters prefixed with `*` accept _zero or more_ arguments and expand to a string containing those arguments separated by spaces, or an empty string if no arguments are present: +Variadic parameters prefixed with `*` accept _zero or more_ arguments and +expand to a string containing those arguments separated by spaces, or an empty +string if no arguments are present: ```just commit MESSAGE *FLAGS: git commit {{FLAGS}} -m "{{MESSAGE}}" ``` -Variadic parameters can be assigned default values. These are overridden by arguments passed on the command line: +Variadic parameters can be assigned default values. These are overridden by +arguments passed on the command line: ```just test +FLAGS='-q': cargo test {{FLAGS}} ``` -`{{…}}` substitutions may need to be quoted if they contain spaces. For example, if you have the following recipe: +`{{…}}` substitutions may need to be quoted if they contain spaces. For +example, if you have the following recipe: ```just search QUERY: @@ -1687,7 +1882,9 @@ And you type: $ just search "cat toupee" ``` -`just` will run the command `lynx https://www.google.com/?q=cat toupee`, which will get parsed by `sh` as `lynx`, `https://www.google.com/?q=cat`, and `toupee`, and not the intended `lynx` and `https://www.google.com/?q=cat toupee`. +`just` will run the command `lynx https://www.google.com/?q=cat toupee`, which +will get parsed by `sh` as `lynx`, `https://www.google.com/?q=cat`, and +`toupee`, and not the intended `lynx` and `https://www.google.com/?q=cat toupee`. You can fix this by adding quotes: @@ -1705,9 +1902,12 @@ foo $bar: ### Running Recipes at the End of a Recipe -Normal dependencies of a recipes always run before a recipe starts. That is to say, the dependee always runs before the depender. These dependencies are called "prior dependencies". +Normal dependencies of a recipes always run before a recipe starts. That is to +say, the dependee always runs before the depender. These dependencies are +called "prior dependencies". -A recipe can also have subsequent dependencies, which run after the recipe and are introduced with an `&&`: +A recipe can also have subsequent dependencies, which run after the recipe and +are introduced with an `&&`: ```just a: @@ -1739,7 +1939,9 @@ D! ### Running Recipes in the Middle of a Recipe -`just` doesn't support running recipes in the middle of another recipe, but you can call `just` recursively in the middle of a recipe. Given the following `justfile`: +`just` doesn't support running recipes in the middle of another recipe, but you +can call `just` recursively in the middle of a recipe. Given the following +`justfile`: ```just a: @@ -1768,7 +1970,9 @@ echo 'B end!' B end! ``` -This has limitations, since recipe `c` is run with an entirely new invocation of `just`: Assignments will be recalculated, dependencies might run twice, and command line arguments will not be propagated to the child `just` process. +This has limitations, since recipe `c` is run with an entirely new invocation +of `just`: Assignments will be recalculated, dependencies might run twice, and +command line arguments will not be propagated to the child `just` process. ### Writing Recipes in Other Languages @@ -1834,7 +2038,8 @@ C:\Temp\PATH_TO_SAVED_RECIPE_BODY`. ### Safer Bash Shebang Recipes -If you're writing a `bash` shebang recipe, consider adding `set -euxo pipefail`: +If you're writing a `bash` shebang recipe, consider adding `set -euxo +pipefail`: ```just foo: @@ -1844,7 +2049,9 @@ foo: echo "$hello from Bash!" ``` -It isn't strictly necessary, but `set -euxo pipefail` turns on a few useful features that make `bash` shebang recipes behave more like normal, linewise `just` recipe: +It isn't strictly necessary, but `set -euxo pipefail` turns on a few useful +features that make `bash` shebang recipes behave more like normal, linewise +`just` recipe: - `set -e` makes `bash` exit if a command fails. @@ -1852,13 +2059,16 @@ It isn't strictly necessary, but `set -euxo pipefail` turns on a few useful feat - `set -x` makes `bash` print each script line before it's run. -- `set -o pipefail` makes `bash` exit if a command in a pipeline fails. This is `bash`-specific, so isn't turned on in normal linewise `just` recipes. +- `set -o pipefail` makes `bash` exit if a command in a pipeline fails. This is + `bash`-specific, so isn't turned on in normal linewise `just` recipes. Together, these avoid a lot of shell scripting gotchas. #### Shebang Recipe Execution on Windows -On Windows, shebang interpreter paths containing a `/` are translated from Unix-style paths to Windows-style paths using `cygpath`, a utility that ships with [Cygwin](http://www.cygwin.com). +On Windows, shebang interpreter paths containing a `/` are translated from +Unix-style paths to Windows-style paths using `cygpath`, a utility that ships +with [Cygwin](http://www.cygwin.com). For example, to execute this recipe on Windows: @@ -1868,13 +2078,17 @@ echo: echo "Hello!" ``` -The interpreter path `/bin/sh` will be translated to a Windows-style path using `cygpath` before being executed. +The interpreter path `/bin/sh` will be translated to a Windows-style path using +`cygpath` before being executed. -If the interpreter path does not contain a `/` it will be executed without being translated. This is useful if `cygpath` is not available, or you wish to pass a Windows-style path to the interpreter. +If the interpreter path does not contain a `/` it will be executed without +being translated. This is useful if `cygpath` is not available, or you wish to +pass a Windows-style path to the interpreter. ### Setting Variables in a Recipe -Recipe lines are interpreted by the shell, not `just`, so it's not possible to set `just` variables in the middle of a recipe: +Recipe lines are interpreted by the shell, not `just`, so it's not possible to +set `just` variables in the middle of a recipe: ```mf foo: @@ -1882,7 +2096,9 @@ foo: echo {{x}} ``` -It is possible to use shell variables, but there's another problem. Every recipe line is run by a new shell instance, so variables set in one line won't be set in the next: +It is possible to use shell variables, but there's another problem. Every +recipe line is run by a new shell instance, so variables set in one line won't +be set in the next: ```just foo: @@ -1891,7 +2107,9 @@ foo: echo $y # This doesn't, `y` is undefined here! ``` -The best way to work around this is to use a shebang recipe. Shebang recipe bodies are extracted and run as scripts, so a single shell instance will run the whole thing: +The best way to work around this is to use a shebang recipe. Shebang recipe +bodies are extracted and run as scripts, so a single shell instance will run +the whole thing: ```just foo: @@ -1903,11 +2121,15 @@ foo: ### Sharing Environment Variables Between Recipes -Each line of each recipe is executed by a fresh shell, so it is not possible to share environment variables between recipes. +Each line of each recipe is executed by a fresh shell, so it is not possible to +share environment variables between recipes. #### Using Python Virtual Environments -Some tools, like [Python's venv](https://docs.python.org/3/library/venv.html), require loading environment variables in order to work, making them challenging to use with `just`. As a workaround, you can execute the virtual environment binaries directly: +Some tools, like [Python's venv](https://docs.python.org/3/library/venv.html), +require loading environment variables in order to work, making them challenging +to use with `just`. As a workaround, you can execute the virtual environment +binaries directly: ```just venv: @@ -1919,7 +2141,8 @@ run: venv ### Changing the Working Directory in a Recipe -Each recipe line is executed by a new shell, so if you change the working directory on one line, it won't have an effect on later lines: +Each recipe line is executed by a new shell, so if you change the working +directory on one line, it won't have an effect on later lines: ```just foo: @@ -1928,14 +2151,17 @@ foo: pwd # …as this `pwd`! ``` -There are a couple ways around this. One is to call `cd` on the same line as the command you want to run: +There are a couple ways around this. One is to call `cd` on the same line as +the command you want to run: ```just foo: cd bar && pwd ``` -The other is to use a shebang recipe. Shebang recipe bodies are extracted and run as scripts, so a single shell instance will run the whole thing, and thus a `pwd` on one line will affect later lines, just like a shell script: +The other is to use a shebang recipe. Shebang recipe bodies are extracted and +run as scripts, so a single shell instance will run the whole thing, and thus a +`pwd` on one line will affect later lines, just like a shell script: ```just foo: @@ -1947,11 +2173,15 @@ foo: ### Indentation -Recipe lines can be indented with spaces or tabs, but not a mix of both. All of a recipe's lines must have the same type of indentation, but different recipes in the same `justfile` may use different indentation. +Recipe lines can be indented with spaces or tabs, but not a mix of both. All of +a recipe's lines must have the same type of indentation, but different recipes +in the same `justfile` may use different indentation. -Each recipe must be indented at least one level from the `recipe-name` but after that may be further indented. +Each recipe must be indented at least one level from the `recipe-name` but +after that may be further indented. -Here's a justfile with a recipe indented with spaces, represented as `·`, and tabs, represented as `→`. +Here's a justfile with a recipe indented with spaces, represented as `·`, and +tabs, represented as `→`. ```justfile set windows-shell := ["pwsh", "-NoLogo", "-NoProfileLoadTime", "-Command"] @@ -1987,7 +2217,8 @@ Downloads ### Multi-Line Constructs -Recipes without an initial shebang are evaluated and run line-by-line, which means that multi-line constructs probably won't do what you want. +Recipes without an initial shebang are evaluated and run line-by-line, which +means that multi-line constructs probably won't do what you want. For example, with the following `justfile`: @@ -1998,7 +2229,8 @@ conditional: fi ``` -The extra leading whitespace before the second line of the `conditional` recipe will produce a parse error: +The extra leading whitespace before the second line of the `conditional` recipe +will produce a parse error: ```sh $ just conditional @@ -2008,7 +2240,9 @@ error: Recipe line has extra leading whitespace | ^^^^^^^^^^^^^^^^ ``` -To work around this, you can write conditionals on one line, escape newlines with slashes, or add a shebang to your recipe. Some examples of multi-line constructs are provided for reference. +To work around this, you can write conditionals on one line, escape newlines +with slashes, or add a shebang to your recipe. Some examples of multi-line +constructs are provided for reference. #### `if` statements @@ -2102,7 +2336,8 @@ bar: (foo echo 'Bar!' ``` -Lines ending with a backslash continue on to the next line as if the lines were joined by whitespace1.15.0: +Lines ending with a backslash continue on to the next line as if the lines were +joined by whitespace1.15.0: ```just a := 'foo' + \ @@ -2123,7 +2358,9 @@ dep2 \ echo 'Dependency with parameter {{param}}' ``` -Backslash line continuations can also be used in interpolations. The line following the backslash must start with the same indentation as the recipe body, although additional indentation is accepted. +Backslash line continuations can also be used in interpolations. The line +following the backslash must start with the same indentation as the recipe +body, although additional indentation is accepted. ```just recipe: @@ -2136,7 +2373,8 @@ recipe: ### Command Line Options -`just` supports a number of useful command line options for listing, dumping, and debugging recipes and variables: +`just` supports a number of useful command line options for listing, dumping, +and debugging recipes and variables: ```sh $ just --list @@ -2181,7 +2419,8 @@ $ just --summary test ``` -The `[private]` attribute1.10.0 may also be used to hide recipes or aliases without needing to change the name: +The `[private]` attribute1.10.0 may also be used to hide recipes or +aliases without needing to change the name: ```just [private] @@ -2199,11 +2438,13 @@ Available recipes: bar ``` -This is useful for helper recipes which are only meant to be used as dependencies of other recipes. +This is useful for helper recipes which are only meant to be used as +dependencies of other recipes. ### Quiet Recipes -A recipe name may be prefixed with `@` to invert the meaning of `@` before each line: +A recipe name may be prefixed with `@` to invert the meaning of `@` before each +line: ```just @quiet: @@ -2234,7 +2475,8 @@ $ just foo Foo! ``` -Adding `@` to a shebang recipe name makes `just` print the recipe before executing it: +Adding `@` to a shebang recipe name makes `just` print the recipe before +executing it: ```just @bar: @@ -2250,8 +2492,8 @@ Bar! ``` `just` normally prints error messages when a recipe line fails. These error -messages can be suppressed using the `[no-exit-message]`1.7.0 attribute. You may find -this especially useful with a recipe that wraps a tool: +messages can be suppressed using the `[no-exit-message]`1.7.0 +attribute. You may find this especially useful with a recipe that wraps a tool: ```just git *args: @@ -2280,17 +2522,27 @@ fatal: not a git repository (or any of the parent directories): .git ### Selecting Recipes to Run With an Interactive Chooser -The `--choose` subcommand makes `just` invoke a chooser to select which recipes to run. Choosers should read lines containing recipe names from standard input and print one or more of those names separated by spaces to standard output. +The `--choose` subcommand makes `just` invoke a chooser to select which recipes +to run. Choosers should read lines containing recipe names from standard input +and print one or more of those names separated by spaces to standard output. -Because there is currently no way to run a recipe that requires arguments with `--choose`, such recipes will not be given to the chooser. Private recipes and aliases are also skipped. +Because there is currently no way to run a recipe that requires arguments with +`--choose`, such recipes will not be given to the chooser. Private recipes and +aliases are also skipped. -The chooser can be overridden with the `--chooser` flag. If `--chooser` is not given, then `just` first checks if `$JUST_CHOOSER` is set. If it isn't, then the chooser defaults to `fzf`, a popular fuzzy finder. +The chooser can be overridden with the `--chooser` flag. If `--chooser` is not +given, then `just` first checks if `$JUST_CHOOSER` is set. If it isn't, then +the chooser defaults to `fzf`, a popular fuzzy finder. Arguments can be included in the chooser, i.e. `fzf --exact`. -The chooser is invoked in the same way as recipe lines. For example, if the chooser is `fzf`, it will be invoked with `sh -cu 'fzf'`, and if the shell, or the shell arguments are overridden, the chooser invocation will respect those overrides. +The chooser is invoked in the same way as recipe lines. For example, if the +chooser is `fzf`, it will be invoked with `sh -cu 'fzf'`, and if the shell, or +the shell arguments are overridden, the chooser invocation will respect those +overrides. -If you'd like `just` to default to selecting recipes with a chooser, you can use this as your default recipe: +If you'd like `just` to default to selecting recipes with a chooser, you can +use this as your default recipe: ```just default: @@ -2299,17 +2551,23 @@ default: ### Invoking `justfile`s in Other Directories -If the first argument passed to `just` contains a `/`, then the following occurs: +If the first argument passed to `just` contains a `/`, then the following +occurs: 1. The argument is split at the last `/`. -2. The part before the last `/` is treated as a directory. `just` will start its search for the `justfile` there, instead of in the current directory. +2. The part before the last `/` is treated as a directory. `just` will start + its search for the `justfile` there, instead of in the current directory. -3. The part after the last slash is treated as a normal argument, or ignored if it is empty. +3. The part after the last slash is treated as a normal argument, or ignored + if it is empty. -This may seem a little strange, but it's useful if you wish to run a command in a `justfile` that is in a subdirectory. +This may seem a little strange, but it's useful if you wish to run a command in +a `justfile` that is in a subdirectory. -For example, if you are in a directory which contains a subdirectory named `foo`, which contains a `justfile` with the recipe `build`, which is also the default recipe, the following are all equivalent: +For example, if you are in a directory which contains a subdirectory named +`foo`, which contains a `justfile` with the recipe `build`, which is also the +default recipe, the following are all equivalent: ```sh $ (cd foo && just build) @@ -2370,6 +2628,14 @@ recursively. When `allow-duplicate-recipes` is set, recipes in parent modules override recipes in imports. +Imports may be made optional by putting a `?` after the `import` keyword: + +```mf +import? 'foo/bar.just' +``` + +Missing source files for optional imports do not produce an error. + ### Modules1.19.0 A `justfile` can declare modules using `mod` statements. `mod` statements are @@ -2428,16 +2694,36 @@ directory set to the directory containing the submodule source file. justfile and the directory that contains it, even when called from submodule recipes. -See the [module stabilization tracking issue](https://github.com/casey/just/issues/929) +Modules may be made optional by putting a `?` after the `mod` keyword: + +```mf +mod? foo +``` + +Missing source files for optional modules do not produce an error. + +Optional modules with no source file do not conflict, so you can have multiple +mod statements with the same name, but with different source file paths, as +long as at most one source file exists: + +```mf +mod? foo 'bar.just' +mod? foo 'baz.just' +``` + +See the +[module stabilization tracking issue](https://github.com/casey/just/issues/929) for more information. ### Hiding `justfile`s -`just` looks for `justfile`s named `justfile` and `.justfile`, which can be used to keep a `justfile` hidden. +`just` looks for `justfile`s named `justfile` and `.justfile`, which can be +used to keep a `justfile` hidden. ### Just Scripts -By adding a shebang line to the top of a `justfile` and making it executable, `just` can be used as an interpreter for scripts: +By adding a shebang line to the top of a `justfile` and making it executable, +`just` can be used as an interpreter for scripts: ```sh $ cat > script < just.zsh ``` -*macOS Note:* Recent versions of macOS use zsh as the default shell. If you use Homebrew to install `just`, it will automatically install the most recent copy of the zsh completion script in the Homebrew zsh directory, which the built-in version of zsh doesn't know about by default. It's best to use this copy of the script if possible, since it will be updated whenever you update `just` via Homebrew. Also, many other Homebrew packages use the same location for completion scripts, and the built-in zsh doesn't know about those either. To take advantage of `just` completion in zsh in this scenario, you can set `fpath` to the Homebrew location before calling `compinit`. Note also that Oh My Zsh runs `compinit` by default. So your `.zshrc` file could look like this: +*macOS Note:* Recent versions of macOS use zsh as the default shell. If you use +Homebrew to install `just`, it will automatically install the most recent copy +of the zsh completion script in the Homebrew zsh directory, which the built-in +version of zsh doesn't know about by default. It's best to use this copy of the +script if possible, since it will be updated whenever you update `just` via +Homebrew. Also, many other Homebrew packages use the same location for +completion scripts, and the built-in zsh doesn't know about those either. To +take advantage of `just` completion in zsh in this scenario, you can set +`fpath` to the Homebrew location before calling `compinit`. Note also that Oh +My Zsh runs `compinit` by default. So your `.zshrc` file could look like this: ```zsh # Init Homebrew, which adds environment variables @@ -2661,11 +2990,14 @@ fpath=($HOMEBREW_PREFIX/share/zsh/site-functions $fpath) ### Grammar -A non-normative grammar of `justfile`s can be found in [GRAMMAR.md](https://github.com/casey/just/blob/master/GRAMMAR.md). +A non-normative grammar of `justfile`s can be found in +[GRAMMAR.md](https://github.com/casey/just/blob/master/GRAMMAR.md). ### just.sh -Before `just` was a fancy Rust program it was a tiny shell script that called `make`. You can find the old version in [extras/just.sh](https://github.com/casey/just/blob/master/extras/just.sh). +Before `just` was a fancy Rust program it was a tiny shell script that called +`make`. You can find the old version in +[extras/just.sh](https://github.com/casey/just/blob/master/extras/just.sh). ### User `justfile`s @@ -2675,7 +3007,9 @@ First, create a `justfile` in `~/.user.justfile` with some recipes. #### Recipe Aliases -If you want to call the recipes in `~/.user.justfile` by name, and don't mind creating an alias for every recipe, add the following to your shell's initialization script: +If you want to call the recipes in `~/.user.justfile` by name, and don't mind +creating an alias for every recipe, add the following to your shell's +initialization script: ```sh for recipe in `just --justfile ~/.user.justfile --summary`; do @@ -2683,9 +3017,12 @@ for recipe in `just --justfile ~/.user.justfile --summary`; do done ``` -Now, if you have a recipe called `foo` in `~/.user.justfile`, you can just type `foo` at the command line to run it. +Now, if you have a recipe called `foo` in `~/.user.justfile`, you can just type +`foo` at the command line to run it. -It took me way too long to realize that you could create recipe aliases like this. Notwithstanding my tardiness, I am very pleased to bring you this major advance in `justfile` technology. +It took me way too long to realize that you could create recipe aliases like +this. Notwithstanding my tardiness, I am very pleased to bring you this major +advance in `justfile` technology. #### Forwarding Alias @@ -2695,7 +3032,8 @@ If you'd rather not create aliases for every recipe, you can create a single ali alias .j='just --justfile ~/.user.justfile --working-directory .' ``` -Now, if you have a recipe called `foo` in `~/.user.justfile`, you can just type `.j foo` at the command line to run it. +Now, if you have a recipe called `foo` in `~/.user.justfile`, you can just type +`.j foo` at the command line to run it. I'm pretty sure that nobody actually uses this feature, but it's there. @@ -2703,7 +3041,9 @@ I'm pretty sure that nobody actually uses this feature, but it's there. #### Customization -You can customize the above aliases with additional options. For example, if you'd prefer to have the recipes in your `justfile` run in your home directory, instead of the current directory: +You can customize the above aliases with additional options. For example, if +you'd prefer to have the recipes in your `justfile` run in your home directory, +instead of the current directory: ```sh alias .j='just --justfile ~/.user.justfile --working-directory ~' @@ -2711,7 +3051,9 @@ alias .j='just --justfile ~/.user.justfile --working-directory ~' ### Node.js `package.json` Script Compatibility -The following export statement gives `just` recipes access to local Node module binaries, and makes `just` recipe commands behave more like `script` entries in Node.js `package.json` files: +The following export statement gives `just` recipes access to local Node module +binaries, and makes `just` recipe commands behave more like `script` entries in +Node.js `package.json` files: ```just export PATH := "./node_modules/.bin:" + env_var('PATH') @@ -2719,37 +3061,61 @@ export PATH := "./node_modules/.bin:" + env_var('PATH') ### Alternatives and Prior Art -There is no shortage of command runners! Some more or less similar alternatives to `just` include: +There is no shortage of command runners! Some more or less similar alternatives +to `just` include: -- [make](https://en.wikipedia.org/wiki/Make_(software)): The Unix build tool that inspired `just`. There are a few different modern day descendents of the original `make`, including [FreeBSD Make](https://www.freebsd.org/cgi/man.cgi?make(1)) and [GNU Make](https://www.gnu.org/software/make/). -- [task](https://github.com/go-task/task): A YAML-based command runner written in Go. -- [maid](https://github.com/egoist/maid): A Markdown-based command runner written in JavaScript. -- [microsoft/just](https://github.com/microsoft/just): A JavaScript-based command runner written in JavaScript. -- [cargo-make](https://github.com/sagiegurari/cargo-make): A command runner for Rust projects. -- [mmake](https://github.com/tj/mmake): A wrapper around `make` with a number of improvements, including remote includes. -- [robo](https://github.com/tj/robo): A YAML-based command runner written in Go. -- [mask](https://github.com/jakedeichert/mask): A Markdown-based command runner written in Rust. -- [makesure](https://github.com/xonixx/makesure): A simple and portable command runner written in AWK and shell. -- [haku](https://github.com/VladimirMarkelov/haku): A make-like command runner written in Rust. +- [make](https://en.wikipedia.org/wiki/Make_(software)): The Unix build tool + that inspired `just`. There are a few different modern day descendents of the + original `make`, including + [FreeBSD Make](https://www.freebsd.org/cgi/man.cgi?make(1)) and + [GNU Make](https://www.gnu.org/software/make/). +- [task](https://github.com/go-task/task): A YAML-based command runner written + in Go. +- [maid](https://github.com/egoist/maid): A Markdown-based command runner + written in JavaScript. +- [microsoft/just](https://github.com/microsoft/just): A JavaScript-based + command runner written in JavaScript. +- [cargo-make](https://github.com/sagiegurari/cargo-make): A command runner for + Rust projects. +- [mmake](https://github.com/tj/mmake): A wrapper around `make` with a number + of improvements, including remote includes. +- [robo](https://github.com/tj/robo): A YAML-based command runner written in + Go. +- [mask](https://github.com/jakedeichert/mask): A Markdown-based command runner + written in Rust. +- [makesure](https://github.com/xonixx/makesure): A simple and portable command + runner written in AWK and shell. +- [haku](https://github.com/VladimirMarkelov/haku): A make-like command runner + written in Rust. Contributing ------------ -`just` welcomes your contributions! `just` is released under the maximally permissive [CC0](https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt) public domain dedication and fallback license, so your changes must also be released under this license. +`just` welcomes your contributions! `just` is released under the maximally +permissive +[CC0](https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt) public +domain dedication and fallback license, so your changes must also be released +under this license. ### Janus -[Janus](https://github.com/casey/janus) is a tool that collects and analyzes `justfile`s, and can determine if a new version of `just` breaks or changes the interpretation of existing `justfile`s. +[Janus](https://github.com/casey/janus) is a tool that collects and analyzes +`justfile`s, and can determine if a new version of `just` breaks or changes the +interpretation of existing `justfile`s. -Before merging a particularly large or gruesome change, Janus should be run to make sure that nothing breaks. Don't worry about running Janus yourself, Casey will happily run it for you on changes that need it. +Before merging a particularly large or gruesome change, Janus should be run to +make sure that nothing breaks. Don't worry about running Janus yourself, Casey +will happily run it for you on changes that need it. ### Minimum Supported Rust Version -The minimum supported Rust version, or MSRV, is current stable Rust. It may build on older versions of Rust, but this is not guaranteed. +The minimum supported Rust version, or MSRV, is current stable Rust. It may +build on older versions of Rust, but this is not guaranteed. ### New Releases -New releases of `just` are made frequently so that users quickly get access to new features. +New releases of `just` are made frequently so that users quickly get access to +new features. Release commit messages use the following template: @@ -2769,9 +3135,12 @@ Frequently Asked Questions ### What are the idiosyncrasies of Make that Just avoids? -`make` has some behaviors which are confusing, complicated, or make it unsuitable for use as a general command runner. +`make` has some behaviors which are confusing, complicated, or make it +unsuitable for use as a general command runner. -One example is that under some circumstances, `make` won't actually run the commands in a recipe. For example, if you have a file called `test` and the following makefile: +One example is that under some circumstances, `make` won't actually run the +commands in a recipe. For example, if you have a file called `test` and the +following makefile: ```just test: @@ -2785,30 +3154,58 @@ $ make test make: `test' is up to date. ``` -`make` assumes that the `test` recipe produces a file called `test`. Since this file exists and the recipe has no other dependencies, `make` thinks that it doesn't have anything to do and exits. +`make` assumes that the `test` recipe produces a file called `test`. Since this +file exists and the recipe has no other dependencies, `make` thinks that it +doesn't have anything to do and exits. -To be fair, this behavior is desirable when using `make` as a build system, but not when using it as a command runner. You can disable this behavior for specific targets using `make`'s built-in [`.PHONY` target name](https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html), but the syntax is verbose and can be hard to remember. The explicit list of phony targets, written separately from the recipe definitions, also introduces the risk of accidentally defining a new non-phony target. In `just`, all recipes are treated as if they were phony. +To be fair, this behavior is desirable when using `make` as a build system, but +not when using it as a command runner. You can disable this behavior for +specific targets using `make`'s built-in +[`.PHONY` target name](https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html), +but the syntax is verbose and can be hard to remember. The explicit list of +phony targets, written separately from the recipe definitions, also introduces +the risk of accidentally defining a new non-phony target. In `just`, all +recipes are treated as if they were phony. -Other examples of `make`'s idiosyncrasies include the difference between `=` and `:=` in assignments, the confusing error messages that are produced if you mess up your makefile, needing `$$` to use environment variables in recipes, and incompatibilities between different flavors of `make`. +Other examples of `make`'s idiosyncrasies include the difference between `=` +and `:=` in assignments, the confusing error messages that are produced if you +mess up your makefile, needing `$$` to use environment variables in recipes, +and incompatibilities between different flavors of `make`. ### What's the relationship between Just and Cargo build scripts? -[`cargo` build scripts](http://doc.crates.io/build-script.html) have a pretty specific use, which is to control how `cargo` builds your Rust project. This might include adding flags to `rustc` invocations, building an external dependency, or running some kind of codegen step. +[`cargo` build scripts](http://doc.crates.io/build-script.html) have a pretty +specific use, which is to control how `cargo` builds your Rust project. This +might include adding flags to `rustc` invocations, building an external +dependency, or running some kind of codegen step. -`just`, on the other hand, is for all the other miscellaneous commands you might run as part of development. Things like running tests in different configurations, linting your code, pushing build artifacts to a server, removing temporary files, and the like. +`just`, on the other hand, is for all the other miscellaneous commands you +might run as part of development. Things like running tests in different +configurations, linting your code, pushing build artifacts to a server, +removing temporary files, and the like. -Also, although `just` is written in Rust, it can be used regardless of the language or build system your project uses. +Also, although `just` is written in Rust, it can be used regardless of the +language or build system your project uses. Further Ramblings ----------------- -I personally find it very useful to write a `justfile` for almost every project, big or small. +I personally find it very useful to write a `justfile` for almost every +project, big or small. -On a big project with multiple contributors, it's very useful to have a file with all the commands needed to work on the project close at hand. +On a big project with multiple contributors, it's very useful to have a file +with all the commands needed to work on the project close at hand. -There are probably different commands to test, build, lint, deploy, and the like, and having them all in one place is useful and cuts down on the time you have to spend telling people which commands to run and how to type them. +There are probably different commands to test, build, lint, deploy, and the +like, and having them all in one place is useful and cuts down on the time you +have to spend telling people which commands to run and how to type them. -And, with an easy place to put commands, it's likely that you'll come up with other useful things which are part of the project's collective wisdom, but which aren't written down anywhere, like the arcane commands needed for some part of your revision control workflow, to install all your project's dependencies, or all the random flags you might need to pass to the build system. +And, with an easy place to put commands, it's likely that you'll come up with +other useful things which are part of the project's collective wisdom, but +which aren't written down anywhere, like the arcane commands needed for some +part of your revision control workflow, to install all your project's +dependencies, or all the random flags you might need to pass to the build +system. Some ideas for recipes: @@ -2822,16 +3219,28 @@ Some ideas for recipes: - Updating dependencies -- Running different sets of tests, for example fast tests vs slow tests, or running them with verbose output +- Running different sets of tests, for example fast tests vs slow tests, or + running them with verbose output -- Any complex set of commands that you really should write down somewhere, if only to be able to remember them +- Any complex set of commands that you really should write down somewhere, if + only to be able to remember them -Even for small, personal projects it's nice to be able to remember commands by name instead of ^Reverse searching your shell history, and it's a huge boon to be able to go into an old project written in a random language with a mysterious build system and know that all the commands you need to do whatever you need to do are in the `justfile`, and that if you type `just` something useful (or at least interesting!) will probably happen. +Even for small, personal projects it's nice to be able to remember commands by +name instead of ^Reverse searching your shell history, and it's a huge boon to +be able to go into an old project written in a random language with a +mysterious build system and know that all the commands you need to do whatever +you need to do are in the `justfile`, and that if you type `just` something +useful (or at least interesting!) will probably happen. -For ideas for recipes, check out [this project's `justfile`](https://github.com/casey/just/blob/master/justfile), or some of the `justfile`s [out in the wild](https://github.com/search?q=path%3A**%2Fjustfile&type=code). +For ideas for recipes, check out +[this project's `justfile`](https://github.com/casey/just/blob/master/justfile), +or some of the +`justfile`s +[out in the wild](https://github.com/search?q=path%3A**%2Fjustfile&type=code). Anyways, I think that's about it for this incredibly long-winded README. -I hope you enjoy using `just` and find great success and satisfaction in all your computational endeavors! +I hope you enjoy using `just` and find great success and satisfaction in all +your computational endeavors! 😸 diff --git a/src/analyzer.rs b/src/analyzer.rs index d6f3aed..dcaf93e 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -38,7 +38,7 @@ impl<'src> Analyzer<'src> { let mut define = |name: Name<'src>, second_type: &'static str, duplicates_allowed: bool| - -> CompileResult<'src, ()> { + -> CompileResult<'src> { if let Some((first_type, original)) = definitions.get(name.lexeme()) { if !(*first_type == second_type && duplicates_allowed) { let (original, redefinition) = if name.line < original.line { @@ -75,17 +75,18 @@ impl<'src> Analyzer<'src> { } Item::Comment(_) => (), Item::Import { absolute, .. } => { - stack.push(asts.get(absolute.as_ref().unwrap()).unwrap()); + if let Some(absolute) = absolute { + stack.push(asts.get(absolute).unwrap()); + } } - Item::Mod { absolute, name, .. } => { - define(*name, "module", false)?; - modules.insert( - name.to_string(), - ( - *name, - Self::analyze(loaded, paths, asts, absolute.as_ref().unwrap())?, - ), - ); + Item::Module { absolute, name, .. } => { + if let Some(absolute) = absolute { + define(*name, "module", false)?; + modules.insert( + name.to_string(), + (*name, Self::analyze(loaded, paths, asts, absolute)?), + ); + } } Item::Recipe(recipe) => { if recipe.enabled() { @@ -153,7 +154,7 @@ impl<'src> Analyzer<'src> { }) } - fn analyze_recipe(recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src, ()> { + fn analyze_recipe(recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src> { let mut parameters = BTreeSet::new(); let mut passed_default = false; @@ -198,7 +199,7 @@ impl<'src> Analyzer<'src> { Ok(()) } - fn analyze_assignment(&self, assignment: &Assignment<'src>) -> CompileResult<'src, ()> { + fn analyze_assignment(&self, assignment: &Assignment<'src>) -> CompileResult<'src> { if self.assignments.contains_key(assignment.name.lexeme()) { return Err(assignment.name.token().error(DuplicateVariable { variable: assignment.name.lexeme(), @@ -207,7 +208,7 @@ impl<'src> Analyzer<'src> { Ok(()) } - fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src, ()> { + fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src> { let name = alias.name.lexeme(); for attr in &alias.attributes { @@ -222,7 +223,7 @@ impl<'src> Analyzer<'src> { Ok(()) } - fn analyze_set(&self, set: &Set<'src>) -> CompileResult<'src, ()> { + fn analyze_set(&self, set: &Set<'src>) -> CompileResult<'src> { if let Some(original) = self.sets.get(set.name.lexeme()) { return Err(set.name.error(DuplicateSet { setting: original.name.lexeme(), diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs index 72d825f..ee42810 100644 --- a/src/assignment_resolver.rs +++ b/src/assignment_resolver.rs @@ -9,7 +9,7 @@ pub(crate) struct AssignmentResolver<'src: 'run, 'run> { impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { pub(crate) fn resolve_assignments( assignments: &'run Table<'src, Assignment<'src>>, - ) -> CompileResult<'src, ()> { + ) -> CompileResult<'src> { let mut resolver = Self { stack: Vec::new(), evaluated: BTreeSet::new(), @@ -23,7 +23,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { Ok(()) } - fn resolve_assignment(&mut self, name: &'src str) -> CompileResult<'src, ()> { + fn resolve_assignment(&mut self, name: &'src str) -> CompileResult<'src> { if self.evaluated.contains(name) { return Ok(()); } @@ -52,7 +52,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { Ok(()) } - fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src, ()> { + fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src> { match expression { Expression::Variable { name } => { let variable = name.lexeme(); diff --git a/src/compiler.rs b/src/compiler.rs index bc836ed..40573a2 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -27,10 +27,11 @@ impl Compiler { for item in &mut ast.items { match item { - Item::Mod { - name, + Item::Module { absolute, - path, + name, + optional, + relative, } => { if !unstable { return Err(Error::Unstable { @@ -40,29 +41,49 @@ impl Compiler { let parent = current.parent().unwrap(); - let import = if let Some(path) = path { - parent.join(Self::expand_tilde(&path.cooked)?) + let import = if let Some(relative) = relative { + let path = parent.join(Self::expand_tilde(&relative.cooked)?); + + if path.is_file() { + Some(path) + } else { + None + } } else { Self::find_module_file(parent, *name)? }; - if srcs.contains_key(&import) { - return Err(Error::CircularImport { current, import }); + if let Some(import) = import { + if srcs.contains_key(&import) { + return Err(Error::CircularImport { current, import }); + } + *absolute = Some(import.clone()); + stack.push((import, depth + 1)); + } else if !*optional { + return Err(Error::MissingModuleFile { module: *name }); } - *absolute = Some(import.clone()); - stack.push((import, depth + 1)); } - Item::Import { relative, absolute } => { + Item::Import { + relative, + absolute, + optional, + path, + } => { let import = current .parent() .unwrap() .join(Self::expand_tilde(&relative.cooked)?) .lexiclean(); - if srcs.contains_key(&import) { - return Err(Error::CircularImport { current, import }); + + if import.is_file() { + if srcs.contains_key(&import) { + return Err(Error::CircularImport { current, import }); + } + *absolute = Some(import.clone()); + stack.push((import, depth + 1)); + } else if !*optional { + return Err(Error::MissingImportFile { path: *path }); } - *absolute = Some(import.clone()); - stack.push((import, depth + 1)); } _ => {} } @@ -81,7 +102,7 @@ impl Compiler { }) } - fn find_module_file<'src>(parent: &Path, module: Name<'src>) -> RunResult<'src, PathBuf> { + fn find_module_file<'src>(parent: &Path, module: Name<'src>) -> RunResult<'src, Option> { let mut candidates = vec![format!("{module}.just"), format!("{module}/mod.just")] .into_iter() .filter(|path| parent.join(path).is_file()) @@ -112,8 +133,8 @@ impl Compiler { } match candidates.as_slice() { - [] => Err(Error::MissingModuleFile { module }), - [file] => Ok(parent.join(file).lexiclean()), + [] => Ok(None), + [file] => Ok(Some(parent.join(file).lexiclean())), found => Err(Error::AmbiguousModuleFile { found: found.into(), module, diff --git a/src/error.rs b/src/error.rs index 3a0da1b..1cc072d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -110,6 +110,9 @@ pub(crate) enum Error<'src> { path: PathBuf, io_error: io::Error, }, + MissingImportFile { + path: Token<'src>, + }, MissingModuleFile { module: Name<'src>, }, @@ -181,6 +184,7 @@ impl<'src> Error<'src> { Self::Backtick { token, .. } => Some(*token), Self::Compile { compile_error } => Some(compile_error.context()), Self::FunctionCall { function, .. } => Some(function.token()), + Self::MissingImportFile { path } => Some(*path), _ => None, } } @@ -369,6 +373,7 @@ impl<'src> ColorDisplay for Error<'src> { let path = path.display(); write!(f, "Failed to read justfile at `{path}`: {io_error}")?; } + MissingImportFile { .. } => write!(f, "Could not find source file for import.")?, MissingModuleFile { module } => write!(f, "Could not find source file for module `{module}`.")?, NoChoosableRecipes => write!(f, "Justfile contains no choosable recipes.")?, NoDefaultRecipe => write!(f, "Justfile contains no default recipe.")?, diff --git a/src/item.rs b/src/item.rs index 654b6ba..502e69b 100644 --- a/src/item.rs +++ b/src/item.rs @@ -7,13 +7,16 @@ pub(crate) enum Item<'src> { Assignment(Assignment<'src>), Comment(&'src str), Import { + absolute: Option, + optional: bool, + path: Token<'src>, relative: StringLiteral<'src>, - absolute: Option, }, - Mod { - name: Name<'src>, + Module { absolute: Option, - path: Option>, + name: Name<'src>, + optional: bool, + relative: Option>, }, Recipe(UnresolvedRecipe<'src>), Set(Set<'src>), @@ -25,11 +28,32 @@ impl<'src> Display for Item<'src> { Item::Alias(alias) => write!(f, "{alias}"), Item::Assignment(assignment) => write!(f, "{assignment}"), Item::Comment(comment) => write!(f, "{comment}"), - Item::Import { relative, .. } => write!(f, "import {relative}"), - Item::Mod { name, path, .. } => { - write!(f, "mod {name}")?; + Item::Import { + relative, optional, .. + } => { + write!(f, "import")?; - if let Some(path) = path { + if *optional { + write!(f, "?")?; + } + + write!(f, " {relative}") + } + Item::Module { + name, + relative, + optional, + .. + } => { + write!(f, "mod")?; + + if *optional { + write!(f, "?")?; + } + + write!(f, " {name}")?; + + if let Some(path) = relative { write!(f, " {path}")?; } diff --git a/src/justfile.rs b/src/justfile.rs index d921d97..6105e15 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -116,7 +116,7 @@ impl<'src> Justfile<'src> { search: &Search, overrides: &BTreeMap, arguments: &[String], - ) -> RunResult<'src, ()> { + ) -> RunResult<'src> { let unknown_overrides = overrides .keys() .filter(|name| !self.assignments.contains_key(name.as_str())) @@ -393,7 +393,7 @@ impl<'src> Justfile<'src> { dotenv: &BTreeMap, search: &Search, ran: &mut BTreeSet>, - ) -> RunResult<'src, ()> { + ) -> RunResult<'src> { let mut invocation = vec![recipe.name().to_owned()]; for argument in arguments { invocation.push((*argument).to_string()); diff --git a/src/lexer.rs b/src/lexer.rs index 6d380ae..1072698 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -75,7 +75,7 @@ impl<'src> Lexer<'src> { /// Advance over the character in `self.next`, updating `self.token_end` /// accordingly. - fn advance(&mut self) -> CompileResult<'src, ()> { + fn advance(&mut self) -> CompileResult<'src> { match self.next { Some(c) => { let len_utf8 = c.len_utf8(); @@ -97,7 +97,7 @@ impl<'src> Lexer<'src> { } /// Advance over N characters. - fn skip(&mut self, n: usize) -> CompileResult<'src, ()> { + fn skip(&mut self, n: usize) -> CompileResult<'src> { for _ in 0..n { self.advance()?; } @@ -124,7 +124,7 @@ impl<'src> Lexer<'src> { } } - fn presume(&mut self, c: char) -> CompileResult<'src, ()> { + fn presume(&mut self, c: char) -> CompileResult<'src> { if !self.next_is(c) { return Err(self.internal_error(format!("Lexer presumed character `{c}`"))); } @@ -134,7 +134,7 @@ impl<'src> Lexer<'src> { Ok(()) } - fn presume_str(&mut self, s: &str) -> CompileResult<'src, ()> { + fn presume_str(&mut self, s: &str) -> CompileResult<'src> { for c in s.chars() { self.presume(c)?; } @@ -328,7 +328,7 @@ impl<'src> Lexer<'src> { } /// Handle blank lines and indentation - fn lex_line_start(&mut self) -> CompileResult<'src, ()> { + fn lex_line_start(&mut self) -> CompileResult<'src> { enum Indentation<'src> { // Line only contains whitespace Blank, @@ -478,7 +478,7 @@ impl<'src> Lexer<'src> { } /// Lex token beginning with `start` outside of a recipe body - fn lex_normal(&mut self, start: char) -> CompileResult<'src, ()> { + fn lex_normal(&mut self, start: char) -> CompileResult<'src> { match start { ' ' | '\t' => self.lex_whitespace(), '!' if self.rest().starts_with("!include") => Err(self.error(Include)), @@ -493,10 +493,11 @@ impl<'src> Lexer<'src> { ',' => self.lex_single(Comma), '/' => self.lex_single(Slash), ':' => self.lex_colon(), - '\\' => self.lex_escape(), '=' => self.lex_choices('=', &[('=', EqualsEquals), ('~', EqualsTilde)], Equals), + '?' => self.lex_single(QuestionMark), '@' => self.lex_single(At), '[' => self.lex_delimiter(BracketL), + '\\' => self.lex_escape(), '\n' | '\r' => self.lex_eol(), '\u{feff}' => self.lex_single(ByteOrderMark), ']' => self.lex_delimiter(BracketR), @@ -516,7 +517,7 @@ impl<'src> Lexer<'src> { &mut self, interpolation_start: Token<'src>, start: char, - ) -> CompileResult<'src, ()> { + ) -> CompileResult<'src> { if self.rest_starts_with("}}") { // end current interpolation if self.interpolation_stack.pop().is_none() { @@ -539,7 +540,7 @@ impl<'src> Lexer<'src> { } /// Lex token while in recipe body - fn lex_body(&mut self) -> CompileResult<'src, ()> { + fn lex_body(&mut self) -> CompileResult<'src> { enum Terminator { Newline, NewlineCarriageReturn, @@ -602,14 +603,14 @@ impl<'src> Lexer<'src> { } /// Lex a single-character token - fn lex_single(&mut self, kind: TokenKind) -> CompileResult<'src, ()> { + fn lex_single(&mut self, kind: TokenKind) -> CompileResult<'src> { self.advance()?; self.token(kind); Ok(()) } /// Lex a double-character token - fn lex_double(&mut self, kind: TokenKind) -> CompileResult<'src, ()> { + fn lex_double(&mut self, kind: TokenKind) -> CompileResult<'src> { self.advance()?; self.advance()?; self.token(kind); @@ -624,7 +625,7 @@ impl<'src> Lexer<'src> { first: char, choices: &[(char, TokenKind)], otherwise: TokenKind, - ) -> CompileResult<'src, ()> { + ) -> CompileResult<'src> { self.presume(first)?; for (second, then) in choices { @@ -640,7 +641,7 @@ impl<'src> Lexer<'src> { } /// Lex an opening or closing delimiter - fn lex_delimiter(&mut self, kind: TokenKind) -> CompileResult<'src, ()> { + fn lex_delimiter(&mut self, kind: TokenKind) -> CompileResult<'src> { use Delimiter::*; match kind { @@ -669,7 +670,7 @@ impl<'src> Lexer<'src> { } /// Pop a delimiter from the open delimiter stack and error if incorrect type - fn close_delimiter(&mut self, close: Delimiter) -> CompileResult<'src, ()> { + fn close_delimiter(&mut self, close: Delimiter) -> CompileResult<'src> { match self.open_delimiters.pop() { Some((open, _)) if open == close => Ok(()), Some((open, open_line)) => Err(self.error(MismatchedClosingDelimiter { @@ -687,7 +688,7 @@ impl<'src> Lexer<'src> { } /// Lex a two-character digraph - fn lex_digraph(&mut self, left: char, right: char, token: TokenKind) -> CompileResult<'src, ()> { + fn lex_digraph(&mut self, left: char, right: char, token: TokenKind) -> CompileResult<'src> { self.presume(left)?; if self.accepted(right)? { @@ -710,7 +711,7 @@ impl<'src> Lexer<'src> { } /// Lex a token starting with ':' - fn lex_colon(&mut self) -> CompileResult<'src, ()> { + fn lex_colon(&mut self) -> CompileResult<'src> { self.presume(':')?; if self.accepted('=')? { @@ -724,7 +725,7 @@ impl<'src> Lexer<'src> { } /// Lex an token starting with '\' escape - fn lex_escape(&mut self) -> CompileResult<'src, ()> { + fn lex_escape(&mut self) -> CompileResult<'src> { self.presume('\\')?; // Treat newline escaped with \ as whitespace @@ -749,7 +750,7 @@ impl<'src> Lexer<'src> { } /// Lex a carriage return and line feed - fn lex_eol(&mut self) -> CompileResult<'src, ()> { + fn lex_eol(&mut self) -> CompileResult<'src> { if self.accepted('\r')? { if !self.accepted('\n')? { return Err(self.error(UnpairedCarriageReturn)); @@ -770,7 +771,7 @@ impl<'src> Lexer<'src> { } /// Lex name: [a-zA-Z_][a-zA-Z0-9_]* - fn lex_identifier(&mut self) -> CompileResult<'src, ()> { + fn lex_identifier(&mut self) -> CompileResult<'src> { self.advance()?; while let Some(c) = self.next { @@ -787,7 +788,7 @@ impl<'src> Lexer<'src> { } /// Lex comment: #[^\r\n] - fn lex_comment(&mut self) -> CompileResult<'src, ()> { + fn lex_comment(&mut self) -> CompileResult<'src> { self.presume('#')?; while !self.at_eol_or_eof() { @@ -800,7 +801,7 @@ impl<'src> Lexer<'src> { } /// Lex whitespace: [ \t]+ - fn lex_whitespace(&mut self) -> CompileResult<'src, ()> { + fn lex_whitespace(&mut self) -> CompileResult<'src> { while self.next_is_whitespace() { self.advance()?; } @@ -815,7 +816,7 @@ impl<'src> Lexer<'src> { /// Backtick: ``[^`]*`` /// Cooked string: "[^"]*" # also processes escape sequences /// Raw string: '[^']*' - fn lex_string(&mut self) -> CompileResult<'src, ()> { + fn lex_string(&mut self) -> CompileResult<'src> { let kind = if let Some(kind) = StringKind::from_token_start(self.rest()) { kind } else { @@ -975,6 +976,7 @@ mod tests { ParenL => "(", ParenR => ")", Plus => "+", + QuestionMark => "?", Slash => "/", Whitespace => " ", diff --git a/src/lib.rs b/src/lib.rs index 0ace8fb..a30dd45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,9 +83,9 @@ pub use crate::run::run; #[doc(hidden)] pub use unindent::unindent; -pub(crate) type CompileResult<'a, T> = Result>; +pub(crate) type CompileResult<'a, T = ()> = Result>; pub(crate) type ConfigResult = Result; -pub(crate) type RunResult<'a, T> = Result>; +pub(crate) type RunResult<'a, T = ()> = Result>; pub(crate) type SearchResult = Result; #[cfg(test)] diff --git a/src/node.rs b/src/node.rs index 3433bb8..77309d6 100644 --- a/src/node.rs +++ b/src/node.rs @@ -21,8 +21,37 @@ impl<'src> Node<'src> for Item<'src> { Item::Alias(alias) => alias.tree(), Item::Assignment(assignment) => assignment.tree(), Item::Comment(comment) => comment.tree(), - Item::Import { relative, .. } => Tree::atom("import").push(format!("{relative}")), - Item::Mod { name, .. } => Tree::atom("mod").push(name.lexeme()), + Item::Import { + relative, optional, .. + } => { + let mut tree = Tree::atom("import"); + + if *optional { + tree = tree.push("?"); + } + + tree.push(format!("{relative}")) + } + Item::Module { + name, + optional, + relative, + .. + } => { + let mut tree = Tree::atom("mod"); + + if *optional { + tree = tree.push("?"); + } + + tree = tree.push(name.lexeme()); + + if let Some(relative) = relative { + tree = tree.push(format!("{relative}")); + } + + tree + } Item::Recipe(recipe) => recipe.tree(), Item::Set(set) => set.tree(), } diff --git a/src/parser.rs b/src/parser.rs index b8ca0cc..92941a8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -150,7 +150,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Return an unexpected token error if the next token is not an EOL - fn expect_eol(&mut self) -> CompileResult<'src, ()> { + fn expect_eol(&mut self) -> CompileResult<'src> { self.accept(Comment)?; if self.next_is(Eof) { @@ -160,7 +160,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { self.expect(Eol).map(|_| ()) } - fn expect_keyword(&mut self, expected: Keyword) -> CompileResult<'src, ()> { + fn expect_keyword(&mut self, expected: Keyword) -> CompileResult<'src> { let found = self.advance()?; if found.kind == Identifier && expected == found.lexeme() { @@ -175,7 +175,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { /// Return an internal error if the next token is not of kind `Identifier` /// with lexeme `lexeme`. - fn presume_keyword(&mut self, keyword: Keyword) -> CompileResult<'src, ()> { + fn presume_keyword(&mut self, keyword: Keyword) -> CompileResult<'src> { let next = self.advance()?; if next.kind != Identifier { @@ -231,7 +231,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Return an error if the next token is of kind `forbidden` - fn forbid(&self, forbidden: TokenKind, error: F) -> CompileResult<'src, ()> + fn forbid(&self, forbidden: TokenKind, error: F) -> CompileResult<'src> where F: FnOnce(Token) -> CompileError, { @@ -334,31 +334,43 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { self.presume_keyword(Keyword::Export)?; items.push(Item::Assignment(self.parse_assignment(true)?)); } - Some(Keyword::Import) if self.next_are(&[Identifier, StringToken]) => { + Some(Keyword::Import) + if self.next_are(&[Identifier, StringToken]) + || self.next_are(&[Identifier, QuestionMark]) => + { self.presume_keyword(Keyword::Import)?; + let optional = self.accepted(QuestionMark)?; + let (path, relative) = self.parse_string_literal_token()?; items.push(Item::Import { - relative: self.parse_string_literal()?, absolute: None, + optional, + path, + relative, }); } Some(Keyword::Mod) if self.next_are(&[Identifier, Identifier, StringToken]) || self.next_are(&[Identifier, Identifier, Eof]) - || self.next_are(&[Identifier, Identifier, Eol]) => + || self.next_are(&[Identifier, Identifier, Eol]) + || self.next_are(&[Identifier, QuestionMark]) => { self.presume_keyword(Keyword::Mod)?; + + let optional = self.accepted(QuestionMark)?; + let name = self.parse_name()?; - let path = if self.next_is(StringToken) { + let relative = if self.next_is(StringToken) { Some(self.parse_string_literal()?) } else { None }; - items.push(Item::Mod { - name, + items.push(Item::Module { absolute: None, - path, + name, + optional, + relative, }); } Some(Keyword::Set) @@ -574,8 +586,10 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } } - /// Parse a string literal, e.g. `"FOO"` - fn parse_string_literal(&mut self) -> CompileResult<'src, StringLiteral<'src>> { + /// Parse a string literal, e.g. `"FOO"`, returning the string literal and the string token + fn parse_string_literal_token( + &mut self, + ) -> CompileResult<'src, (Token<'src>, StringLiteral<'src>)> { let token = self.expect(StringToken)?; let kind = StringKind::from_string_or_backtick(token)?; @@ -620,7 +634,13 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { unindented }; - Ok(StringLiteral { kind, raw, cooked }) + Ok((token, StringLiteral { kind, raw, cooked })) + } + + /// Parse a string literal, e.g. `"FOO"` + fn parse_string_literal(&mut self) -> CompileResult<'src, StringLiteral<'src>> { + let (_token, string_literal) = self.parse_string_literal_token()?; + Ok(string_literal) } /// Parse a name from an identifier token @@ -2000,6 +2020,36 @@ mod tests { tree: (justfile (import "some/file/path.txt")), } + test! { + name: optional_import, + text: "import? \"some/file/path.txt\" \n", + tree: (justfile (import ? "some/file/path.txt")), + } + + test! { + name: module_with, + text: "mod foo", + tree: (justfile (mod foo )), + } + + test! { + name: optional_module, + text: "mod? foo", + tree: (justfile (mod ? foo)), + } + + test! { + name: module_with_path, + text: "mod foo \"some/file/path.txt\" \n", + tree: (justfile (mod foo "some/file/path.txt")), + } + + test! { + name: optional_module_with_path, + text: "mod? foo \"some/file/path.txt\" \n", + tree: (justfile (mod ? foo "some/file/path.txt")), + } + error! { name: alias_syntax_multiple_rhs, input: "alias foo := bar baz", diff --git a/src/recipe_resolver.rs b/src/recipe_resolver.rs index b89c7c6..88e4978 100644 --- a/src/recipe_resolver.rs +++ b/src/recipe_resolver.rs @@ -56,7 +56,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> { &self, variable: &Token<'src>, parameters: &[Parameter], - ) -> CompileResult<'src, ()> { + ) -> CompileResult<'src> { let name = variable.lexeme(); let undefined = !self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name); diff --git a/src/token_kind.rs b/src/token_kind.rs index b00517f..bb91589 100644 --- a/src/token_kind.rs +++ b/src/token_kind.rs @@ -30,6 +30,7 @@ pub(crate) enum TokenKind { ParenL, ParenR, Plus, + QuestionMark, Slash, StringToken, Text, @@ -72,6 +73,7 @@ impl Display for TokenKind { ParenL => "'('", ParenR => "')'", Plus => "'+'", + QuestionMark => "?", Slash => "'/'", StringToken => "string", Text => "command text", diff --git a/src/tree.rs b/src/tree.rs index d1b1c2a..d39d3bc 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -23,6 +23,10 @@ macro_rules! tree { $crate::tree::Tree::atom("#") }; + { ? } => { + $crate::tree::Tree::atom("?") + }; + { + } => { $crate::tree::Tree::atom("+") }; diff --git a/tests/imports.rs b/tests/imports.rs index 22a04b7..7adc5d5 100644 --- a/tests/imports.rs +++ b/tests/imports.rs @@ -23,6 +23,49 @@ fn import_succeeds() { .run(); } +#[test] +fn missing_import_file_error() { + Test::new() + .justfile( + " + import './import.justfile' + + a: + @echo A + ", + ) + .test_round_trip(false) + .arg("a") + .status(EXIT_FAILURE) + .stderr( + " + error: Could not find source file for import. + --> justfile:1:8 + | + 1 | import './import.justfile' + | ^^^^^^^^^^^^^^^^^^^ + ", + ) + .run(); +} + +#[test] +fn missing_optional_imports_are_ignored() { + Test::new() + .justfile( + " + import? './import.justfile' + + a: + @echo A + ", + ) + .test_round_trip(false) + .arg("a") + .stdout("A\n") + .run(); +} + #[test] fn trailing_spaces_after_import_are_ignored() { Test::new() @@ -169,3 +212,33 @@ fn import_paths_beginning_with_tilde_are_expanded_to_homdir() { .env("HOME", "foobar") .run(); } + +#[test] +fn imports_dump_correctly() { + Test::new() + .write("import.justfile", "") + .justfile( + " + import './import.justfile' + ", + ) + .test_round_trip(false) + .arg("--dump") + .stdout("import './import.justfile'\n") + .run(); +} + +#[test] +fn optional_imports_dump_correctly() { + Test::new() + .write("import.justfile", "") + .justfile( + " + import? './import.justfile' + ", + ) + .test_round_trip(false) + .arg("--dump") + .stdout("import? './import.justfile'\n") + .run(); +} diff --git a/tests/misc.rs b/tests/misc.rs index 1592df2..e45ff2f 100644 --- a/tests/misc.rs +++ b/tests/misc.rs @@ -552,13 +552,13 @@ test! { -??? +^^^ "#, stdout: "", stderr: "error: Unknown start of token: --> justfile:10:1 | -10 | ??? +10 | ^^^ | ^ ", status: EXIT_FAILURE, diff --git a/tests/modules.rs b/tests/modules.rs index 8c2e772..9edf028 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -266,6 +266,22 @@ fn modules_are_dumped_correctly() { .run(); } +#[test] +fn optional_modules_are_dumped_correctly() { + Test::new() + .write("foo.just", "foo:\n @echo FOO") + .justfile( + " + mod? foo + ", + ) + .test_round_trip(false) + .arg("--unstable") + .arg("--dump") + .stdout("mod? foo\n") + .run(); +} + #[test] fn modules_can_be_in_subdirectory() { Test::new() @@ -382,6 +398,42 @@ fn missing_module_file_error() { .run(); } +#[test] +fn missing_optional_modules_do_not_trigger_error() { + Test::new() + .justfile( + " + mod? foo + + bar: + @echo BAR + ", + ) + .test_round_trip(false) + .arg("--unstable") + .stdout("BAR\n") + .run(); +} + +#[test] +fn missing_optional_modules_do_not_conflict() { + Test::new() + .justfile( + " + mod? foo + mod? foo + mod foo 'baz.just' + ", + ) + .write("baz.just", "baz:\n @echo BAZ") + .test_round_trip(false) + .arg("--unstable") + .arg("foo") + .arg("baz") + .stdout("BAZ\n") + .run(); +} + #[test] fn list_displays_recipes_in_submodules() { Test::new() @@ -478,6 +530,22 @@ fn modules_with_paths_are_dumped_correctly() { .run(); } +#[test] +fn optional_modules_with_paths_are_dumped_correctly() { + Test::new() + .write("commands/foo.just", "foo:\n @echo FOO") + .justfile( + " + mod? foo 'commands/foo.just' + ", + ) + .test_round_trip(false) + .arg("--unstable") + .arg("--dump") + .stdout("mod? foo 'commands/foo.just'\n") + .run(); +} + #[test] fn recipes_may_be_named_mod() { Test::new()