[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: https://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n[*]\n# Text is UTF-8\ncharset = utf-8\n# Unix-style newlines\nend_of_line = lf\n# Newline ending every file\ninsert_final_newline = true\n# Soft tabs\nindent_style = space\n# Two-space indentation\nindent_size = 2\n# Trim trailing whitespace\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".gitattributes",
    "content": "* -text\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: CI\n\non:\n  pull_request:\n    branches:\n    - '*'\n  push:\n    branches:\n    - master\n\ndefaults:\n  run:\n    shell: bash\n\nenv:\n  RUSTFLAGS: --deny warnings\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - uses: Swatinem/rust-cache@v2\n\n    - name: Clippy\n      run: cargo clippy --all --all-targets\n\n    - name: Format\n      run: cargo fmt --all -- --check\n\n    - name: Install Dependencies\n      run: |\n        sudo apt-get update\n        sudo apt-get install ripgrep shellcheck\n\n    - name: Check for Forbidden Words\n      run: ./bin/forbid\n\n    - name: Check Install Script\n      run: shellcheck www/install.sh\n\n  msrv:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - uses: actions-rust-lang/setup-rust-toolchain@v1\n      with:\n        toolchain: 1.85.0\n\n    - uses: Swatinem/rust-cache@v2\n\n    - name: Check\n      run: cargo check\n\n  pages:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - uses: Swatinem/rust-cache@v2\n\n    - name: Install `mdbook`\n      run: cargo install --locked mdbook@0.4.52\n\n    - name: Install `mdbook-linkcheck`\n      run: cargo install --locked mdbook-linkcheck@0.7.7\n\n    - name: Build book\n      run: |\n        cargo run --package generate-book\n        mdbook build book/en\n        mdbook build book/zh\n\n  test:\n    strategy:\n      matrix:\n        os:\n        - ubuntu-latest\n        - macos-latest\n        - windows-latest\n\n    runs-on: ${{matrix.os}}\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - name: Remove Broken WSL bash executable\n      if: ${{ matrix.os == 'windows-latest' }}\n      shell: cmd\n      run: |\n        takeown /F C:\\Windows\\System32\\bash.exe\n        icacls C:\\Windows\\System32\\bash.exe /grant administrators:F\n        del C:\\Windows\\System32\\bash.exe\n\n    - uses: Swatinem/rust-cache@v2\n\n    - name: Test\n      run: cargo test --all\n\n    - name: Test install.sh\n      run: |\n        bash www/install.sh --to /tmp --tag 1.25.0\n        /tmp/just --version\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n    - '*'\n\ndefaults:\n  run:\n    shell: bash\n\nenv:\n  RUSTFLAGS: --deny warnings\n\njobs:\n  prerelease:\n    runs-on: ubuntu-latest\n\n    outputs:\n      value: ${{ steps.prerelease.outputs.value }}\n\n    steps:\n    - name: Prerelease Check\n      id: prerelease\n      run: |\n        if [[ ${{ github.ref_name }} =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]; then\n            echo value=false >> $GITHUB_OUTPUT\n        else\n            echo value=true >> $GITHUB_OUTPUT\n        fi\n\n  package:\n    strategy:\n      matrix:\n        target:\n        - aarch64-apple-darwin\n        - aarch64-pc-windows-msvc\n        - aarch64-unknown-linux-musl\n        - arm-unknown-linux-musleabihf\n        - armv7-unknown-linux-musleabihf\n        - loongarch64-unknown-linux-musl\n        - x86_64-apple-darwin\n        - x86_64-pc-windows-msvc\n        - x86_64-unknown-linux-musl\n        include:\n        - target: aarch64-apple-darwin\n          os: macos-latest\n          target_rustflags: ''\n        - target: aarch64-pc-windows-msvc\n          os: windows-latest\n          target_rustflags: ''\n        - target: aarch64-unknown-linux-musl\n          os: ubuntu-latest\n          target_rustflags: '--codegen linker=aarch64-linux-gnu-gcc'\n        - target: arm-unknown-linux-musleabihf\n          os: ubuntu-latest\n          target_rustflags: '--codegen linker=arm-linux-gnueabihf-gcc'\n        - target: armv7-unknown-linux-musleabihf\n          os: ubuntu-latest\n          target_rustflags: '--codegen linker=arm-linux-gnueabihf-gcc'\n        - target: loongarch64-unknown-linux-musl\n          os: ubuntu-latest\n          target_rustflags: '--codegen linker=loongarch64-linux-gnu-gcc-14'\n        - target: x86_64-apple-darwin\n          os: macos-latest\n          target_rustflags: ''\n        - target: x86_64-pc-windows-msvc\n          os: windows-latest\n        - target: x86_64-unknown-linux-musl\n          os: ubuntu-latest\n          target_rustflags: ''\n\n    runs-on: ${{matrix.os}}\n\n    needs:\n    - prerelease\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - name: Install Target Dependencies\n      run: |\n        case ${{ matrix.target }} in\n          aarch64-pc-windows-msvc)\n            rustup target add aarch64-pc-windows-msvc\n            ;;\n          aarch64-unknown-linux-musl)\n            sudo apt-get update\n            sudo apt-get install gcc-aarch64-linux-gnu libc6-dev-i386\n            ;;\n          arm-unknown-linux-musleabihf|armv7-unknown-linux-musleabihf)\n            sudo apt-get update\n            sudo apt-get install gcc-arm-linux-gnueabihf\n            ;;\n          loongarch64-unknown-linux-musl)\n            sudo apt-get update\n            sudo apt-get install gcc-14-loongarch64-linux-gnu\n            ;;\n        esac\n\n    - name: Generate Manpage\n      run: |\n        set -euxo pipefail\n        cargo build\n        mkdir -p man\n        ./target/debug/just --man > man/just.1\n\n    - name: Package\n      id: package\n      env:\n        TARGET: ${{ matrix.target }}\n        REF: ${{ github.ref }}\n        OS: ${{ matrix.os }}\n        TARGET_RUSTFLAGS: ${{ matrix.target_rustflags }}\n      run: ./bin/package\n      shell: bash\n\n    - name: Publish Archive\n      uses: softprops/action-gh-release@v2.5.0\n      if: ${{ startsWith(github.ref, 'refs/tags/') }}\n      with:\n        draft: false\n        files: ${{ steps.package.outputs.archive }}\n        prerelease: ${{ needs.prerelease.outputs.value }}\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n    - name: Publish Changelog\n      uses: softprops/action-gh-release@v2.5.0\n      if: >-\n        ${{\n          startsWith(github.ref, 'refs/tags/')\n          && matrix.target == 'x86_64-unknown-linux-musl'\n        }}\n      with:\n        draft: false\n        files: CHANGELOG.md\n        prerelease: ${{ needs.prerelease.outputs.value }}\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  checksum:\n    runs-on: ubuntu-latest\n\n    needs:\n    - package\n    - prerelease\n\n    steps:\n    - name: Download Release Archives\n      env:\n        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      run: >-\n        gh release download\n        --repo casey/just\n        --pattern '*'\n        --dir release\n        ${{ github.ref_name }}\n\n    - name: Create Checksums\n      run: |\n        cd release\n        shasum -a 256 * > ../SHA256SUMS\n\n    - name: Publish Checksums\n      uses: softprops/action-gh-release@v2.5.0\n      with:\n        draft: false\n        files: SHA256SUMS\n        prerelease: ${{ needs.prerelease.outputs.value }}\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  pages:\n    runs-on: ubuntu-latest\n\n    needs:\n    - prerelease\n\n    permissions:\n      contents: write\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - uses: Swatinem/rust-cache@v2\n\n    - name: Install `mdbook`\n      run: cargo install --locked mdbook@0.4.52\n\n    - name: Install `mdbook-linkcheck`\n      run: cargo install --locked mdbook-linkcheck@0.7.7\n\n    - name: Build book\n      run: |\n        cargo run --package generate-book\n        mdbook build book/en\n        mdbook build book/zh\n\n    - name: Deploy Pages\n      uses: peaceiris/actions-gh-pages@v4\n      if: ${{ needs.prerelease.outputs.value == 'false' }}\n      with:\n        github_token: ${{ secrets.GITHUB_TOKEN }}\n        publish_branch: gh-pages\n        publish_dir: www\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.idea\n/.vagrant\n/.vscode\n/README.html\n/book/en/build\n/book/en/src\n/book/zh/build\n/book/zh/src\n/fuzz/artifacts\n/fuzz/corpus\n/fuzz/target\n/man\n/target\n/test-utilities/Cargo.lock\n/test-utilities/target\n/tmp\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "Changelog\n=========\n\n[1.47.1](https://github.com/casey/just/releases/tag/1.47.1) - 2026-03-16\n------------------------------------------------------------------------\n\n### Fixed\n- Block on running parallel dependencies ([#3139](https://github.com/casey/just/pull/3139) by [casey](https://github.com/casey))\n- Fix setting-exported assignment visibility in child modules ([#3128](https://github.com/casey/just/pull/3128) by [casey](https://github.com/casey))\n\n### Added\n- Add `eager` keyword to force evaluation of unused assignments ([#3131](https://github.com/casey/just/pull/3131) by [casey](https://github.com/casey))\n\n### Changed\n- Only evaluate used variables in --evaluate and --command ([#3130](https://github.com/casey/just/pull/3130) by [casey](https://github.com/casey))\n\n### Misc\n- Make eager assignments unstable ([#3140](https://github.com/casey/just/pull/3140) by [casey](https://github.com/casey))\n- Include path to .env file in error messages ([#3135](https://github.com/casey/just/pull/3135) by [casey](https://github.com/casey))\n- Consolidate override checking ([#3127](https://github.com/casey/just/pull/3127) by [casey](https://github.com/casey))\n- Update readme version references ([#3126](https://github.com/casey/just/pull/3126) by [casey](https://github.com/casey))\n\n[1.47.0](https://github.com/casey/just/releases/tag/1.47.0) - 2026-03-14\n------------------------------------------------------------------------\n\n### Added\n- Add lazy evaluation setting ([#3083](https://github.com/casey/just/pull/3083) by [casey](https://github.com/casey))\n- Add guard sigil `?` ([#2547](https://github.com/casey/just/pull/2547) by [casey](https://github.com/casey))\n- Add `--group` flag to filter `--list` output by group ([#3117](https://github.com/casey/just/pull/3117) by [terror](https://github.com/terror))\n- Add attributes for DragonFly BSD, FreeBSD, and NetBSD ([#3115](https://github.com/casey/just/pull/3115) by [jakewilliami](https://github.com/jakewilliami))\n- Add `[env(NAME, VALUE)` recipe attribute ([#2957](https://github.com/casey/just/pull/2957) by [neunenak](https://github.com/neunenak))\n\n### Changed\n- Make `--timestamp` print timestamps unconditionally ([#3114](https://github.com/casey/just/pull/3114) by [casey](https://github.com/casey))\n- Print `--timestamps` with script recipes ([#3050](https://github.com/casey/just/pull/3050) by [casey](https://github.com/casey))\n- `[private]` modules are excluded from `--list` output ([#2889](https://github.com/casey/just/pull/2889) by [Scott-Guest](https://github.com/Scott-Guest))\n\n### Misc\n- Fix readme typo ([#3122](https://github.com/casey/just/pull/3122) by [Rohan5commit](https://github.com/Rohan5commit))\n- Move choose and run into match statement ([#3120](https://github.com/casey/just/pull/3120) by [casey](https://github.com/casey))\n- Add uv install instructions to readme ([#3062](https://github.com/casey/just/pull/3062) by [npikall](https://github.com/npikall))\n- Suppress error when --choose is cancelled by user ([#3098](https://github.com/casey/just/pull/3098) by [cobyfrombrooklyn-bot](https://github.com/cobyfrombrooklyn-bot))\n- Test formatting justfile with undefined variable succeeds ([#3110](https://github.com/casey/just/pull/3110) by [casey](https://github.com/casey))\n- Format without compiling ([#3103](https://github.com/casey/just/pull/3103) by [terror](https://github.com/terror))\n- Fix Gentoo installation instructions ([#3085](https://github.com/casey/just/pull/3085) by [DarthGandalf](https://github.com/DarthGandalf))\n- Deny unreachable pub ([#3080](https://github.com/casey/just/pull/3080) by [casey](https://github.com/casey))\n- Fix readme typo ([#3079](https://github.com/casey/just/pull/3079) by [pvinis](https://github.com/pvinis))\n- Include blank chapters in book ([#3076](https://github.com/casey/just/pull/3076) by [casey](https://github.com/casey))\n- Clean up build script ([#3078](https://github.com/casey/just/pull/3078) by [casey](https://github.com/casey))\n- Increase stack size on Windows ([#3077](https://github.com/casey/just/pull/3077) by [casey](https://github.com/casey))\n- Fix readme typo ([#3066](https://github.com/casey/just/pull/3066) by [kenden](https://github.com/kenden))\n- Remove dependency on executable-path ([#3058](https://github.com/casey/just/pull/3058) by [casey](https://github.com/casey))\n- Fix typos ([#3056](https://github.com/casey/just/pull/3056) by [galenseilis](https://github.com/galenseilis))\n- Avoid conditional compilation in integration tests ([#3055](https://github.com/casey/just/pull/3055) by [casey](https://github.com/casey))\n- Assert exit status last in `Test` builder ([#3054](https://github.com/casey/just/pull/3054) by [casey](https://github.com/casey))\n- Remove makedeb/MPR installation instructions ([#3053](https://github.com/casey/just/pull/3053) by [Chengings](https://github.com/Chengings))\n- Handle errors when checking for files ([#3051](https://github.com/casey/just/pull/3051) by [casey](https://github.com/casey))\n\n[1.46.0](https://github.com/casey/just/releases/tag/1.46.0) - 2026-01-01\n------------------------------------------------------------------------\n\n### Fixed\n- Don't leak signal handler pipe into child processes ([#3035](https://github.com/casey/just/pull/3035) by [rjmac](https://github.com/rjmac))\n\n### Added\n- Allow `long` to default to to parameter name ([#3041](https://github.com/casey/just/pull/3041) by [casey](https://github.com/casey))\n- Allow const expressions in all settings ([#3037](https://github.com/casey/just/pull/3037) by [casey](https://github.com/casey))\n- Allow const expressions in `working-directory` ([#3033](https://github.com/casey/just/pull/3033) by [casey](https://github.com/casey))\n- Add --usage subcommand and argument help strings ([#3031](https://github.com/casey/just/pull/3031) by [casey](https://github.com/casey))\n- Add flags without values ([#3029](https://github.com/casey/just/pull/3029) by [casey](https://github.com/casey))\n- Allow passing arguments as short `-x` options ([#3028](https://github.com/casey/just/pull/3028) by [casey](https://github.com/casey))\n- Allow recipes to take `--long` options ([#3026](https://github.com/casey/just/pull/3026) by [casey](https://github.com/casey))\n\n### Misc\n- Add original token to string literal ([#3042](https://github.com/casey/just/pull/3042) by [casey](https://github.com/casey))\n- Remove string literal lifetime ([#3036](https://github.com/casey/just/pull/3036) by [casey](https://github.com/casey))\n- Move overrides into config ([#3032](https://github.com/casey/just/pull/3032) by [casey](https://github.com/casey))\n- Test that options are passed as positional arguments ([#3030](https://github.com/casey/just/pull/3030) by [casey](https://github.com/casey))\n- Group arguments by parameter ([#3025](https://github.com/casey/just/pull/3025) by [casey](https://github.com/casey))\n- Add OpenBSD package to readme ([#2900](https://github.com/casey/just/pull/2900) by [vext01](https://github.com/vext01))\n- Re-enable mdbook-linkcheck ([#3011](https://github.com/casey/just/pull/3011) by [casey](https://github.com/casey))\n- Disable dependabot ([#3010](https://github.com/casey/just/pull/3010) by [casey](https://github.com/casey))\n- Fix pre-release check in pages deploy job ([#3009](https://github.com/casey/just/pull/3009) by [casey](https://github.com/casey))\n\n[1.45.0](https://github.com/casey/just/releases/tag/1.45.0) - 2025-12-10\n------------------------------------------------------------------------\n\n### Added\n- Allow requiring recipe arguments to match regular expression patterns ([#3000](https://github.com/casey/just/pull/3000) by [casey](https://github.com/casey))\n\n### Fixed\n- Allow shell-expanded strings in attributes ([#3007](https://github.com/casey/just/pull/3007) by [casey](https://github.com/casey))\n- Fix arg pattern anchoring ([#3002](https://github.com/casey/just/pull/3002) by [casey](https://github.com/casey))\n\n### Misc\n- Use non-capturing group in arg pattern regex ([#3006](https://github.com/casey/just/pull/3006) by [casey](https://github.com/casey))\n- Remove redundant type annotation ([#3004](https://github.com/casey/just/pull/3004) by [casey](https://github.com/casey))\n\n[1.44.1](https://github.com/casey/just/releases/tag/1.44.1) - 2025-12-09\n------------------------------------------------------------------------\n\n### Fixed\n- Properly close format string delimiter ([#2997](https://github.com/casey/just/pull/2997) by [casey](https://github.com/casey))\n\n[1.44.0](https://github.com/casey/just/releases/tag/1.44.0) - 2025-12-06\n------------------------------------------------------------------------\n\n### Added\n- Add f'{format}' strings ([#2985](https://github.com/casey/just/pull/2985) by [casey](https://github.com/casey))\n- Use double braces `{{…}}` for format strings ([#2993](https://github.com/casey/just/pull/2993) by [casey](https://github.com/casey))\n- Stabilize `[script]` attribute ([#2988](https://github.com/casey/just/pull/2988) by [casey](https://github.com/casey))\n\n### Changed\n- Allow newlines in interpolations and `}` to abut interpolation `}}` ([#2992](https://github.com/casey/just/pull/2992) by [casey](https://github.com/casey))\n\n### Misc\n- Test format strings with conditionals ([#2991](https://github.com/casey/just/pull/2991) by [casey](https://github.com/casey))\n- Move StringState into module ([#2989](https://github.com/casey/just/pull/2989) by [casey](https://github.com/casey))\n- Test undefined variable in format string error ([#2987](https://github.com/casey/just/pull/2987) by [casey](https://github.com/casey))\n- Update `softprops/action-gh-release` to 2.5.0 ([#2979](https://github.com/casey/just/pull/2979) by [app/dependabot](https://github.com/app/dependabot))\n- Link to `just-lsp` in readme ([#2846](https://github.com/casey/just/pull/2846) by [terror](https://github.com/terror))\n- Fix `just --list` submodule example in readme ([#2973](https://github.com/casey/just/pull/2973) by [neodejack](https://github.com/neodejack))\n- Update `actions/checkout` ([#2969](https://github.com/casey/just/pull/2969) by [app/dependabot](https://github.com/app/dependabot))\n- Disable mdbook-linkcheck ([#2970](https://github.com/casey/just/pull/2970) by [casey](https://github.com/casey))\n\n[1.43.1](https://github.com/casey/just/releases/tag/1.43.1) - 2025-11-12\n------------------------------------------------------------------------\n\n### Fixed\n- Only initialize signal handler once ([#2953](https://github.com/casey/just/pull/2953) by [casey](https://github.com/casey))\n- Preserve module docs when formatting ([#2931](https://github.com/casey/just/pull/2931) by [casey](https://github.com/casey))\n- Preserve module groups when formatting ([#2930](https://github.com/casey/just/pull/2930) by [casey](https://github.com/casey))\n- Don't suggest private recipes and aliases ([#2916](https://github.com/casey/just/pull/2916) by [casey](https://github.com/casey))\n\n### Misc\n- Update softprops/action-gh-release to 2.4.2 ([#2948](https://github.com/casey/just/pull/2948) by [app/dependabot](https://github.com/app/dependabot))\n- Fix `env()` usage in readme ([#2936](https://github.com/casey/just/pull/2936) by [laniakea64](https://github.com/laniakea64))\n- Use a case statement to install target dependencies ([#2929](https://github.com/casey/just/pull/2929) by [casey](https://github.com/casey))\n- Build loongarch64 release binaries ([#2886](https://github.com/casey/just/pull/2886) by [SkyBird233](https://github.com/SkyBird233))\n- Bump softprops/action-gh-release to 2.4.1 ([#2919](https://github.com/casey/just/pull/2919) by [app/dependabot](https://github.com/app/dependabot))\n- Update softprops/action-gh-release to 2.3.4 ([#2910](https://github.com/casey/just/pull/2910) by [app/dependabot](https://github.com/app/dependabot))\n\n[1.43.0](https://github.com/casey/just/releases/tag/1.43.0) - 2025-09-27\n------------------------------------------------------------------------\n\n### Added\n- Add `[default]` attribute ([#2878](https://github.com/casey/just/pull/2878) by [casey](https://github.com/casey))\n- Do not ascend above `--ceiling` when looking for justfile ([#2870](https://github.com/casey/just/pull/2870) by [casey](https://github.com/casey))\n\n### Misc\n- Don't generate completions at runtime ([#2896](https://github.com/casey/just/pull/2896) by [casey](https://github.com/casey))\n- Update `softprops/action-gh-release` to 2.3.3 ([#2879](https://github.com/casey/just/pull/2879) by [app/dependabot](https://github.com/app/dependabot))\n- Add submodule alias and dependency targets to grammar ([#2877](https://github.com/casey/just/pull/2877) by [casey](https://github.com/casey))\n- Bump `actions/checkout` to v5 ([#2864](https://github.com/casey/just/pull/2864) by [app/dependabot](https://github.com/app/dependabot))\n- Fix Windows `PATH_SEP` value in readme ([#2859](https://github.com/casey/just/pull/2859) by [casey](https://github.com/casey))\n- Fix lints for Rust 1.89 ([#2860](https://github.com/casey/just/pull/2860) by [casey](https://github.com/casey))\n- Note that Debian 13 has been released ([#2856](https://github.com/casey/just/pull/2856) by [sblondon](https://github.com/sblondon))\n- Mention `just-mcp` in readme ([#2843](https://github.com/casey/just/pull/2843) by [casey](https://github.com/casey))\n- Expand Windows instructions in readme ([#2842](https://github.com/casey/just/pull/2842) by [casey](https://github.com/casey))\n- Note `[parallel]` attribute in parallelism section ([#2837](https://github.com/casey/just/pull/2837) by [casey](https://github.com/casey))\n\n[1.42.4](https://github.com/casey/just/releases/tag/1.42.4) - 2025-07-24\n------------------------------------------------------------------------\n\n### Fixed\n- Run imported recipes in correct scope ([#2835](https://github.com/casey/just/pull/2835) by [casey](https://github.com/casey))\n- Fix alias doc comment ([#2833](https://github.com/casey/just/pull/2833) by [casey](https://github.com/casey))\n\n[1.42.3](https://github.com/casey/just/releases/tag/1.42.3) - 2025-07-18\n------------------------------------------------------------------------\n\n### Fixed\n- Run recipes from submodules in correct directory ([#2829](https://github.com/casey/just/pull/2829) by [eisbaw](https://github.com/eisbaw))\n\n[1.42.2](https://github.com/casey/just/releases/tag/1.42.2) - 2025-07-15\n------------------------------------------------------------------------\n\n### Fixed\n- Fix scope lookup for nested submodules ([#2820](https://github.com/casey/just/pull/2820) by [casey](https://github.com/casey))\n\n[1.42.1](https://github.com/casey/just/releases/tag/1.42.1) - 2025-07-14\n------------------------------------------------------------------------\n\n### Fixed\n- Export variables to submodules ([#2816](https://github.com/casey/just/pull/2816) by [casey](https://github.com/casey))\n- Only override root-justfile variable assignments ([#2815](https://github.com/casey/just/pull/2815) by [casey](https://github.com/casey))\n\n[1.42.0](https://github.com/casey/just/releases/tag/1.42.0) - 2025-07-13\n------------------------------------------------------------------------\n\n### Fixed\n- Use correct scope when running recipes in submodules ([#2810](https://github.com/casey/just/pull/2810) by [casey](https://github.com/casey))\n\n### Added\n- Add `[parallel]` attribute to run dependencies in parallel ([#2803](https://github.com/casey/just/pull/2803) by [casey](https://github.com/casey))\n- Allow configuring `cygpath` with `--cygpath` and `$JUST_CYGPATH` ([#2804](https://github.com/casey/just/pull/2804) by [hyrious](https://github.com/hyrious))\n- Use GitHub token in `install.sh` if available ([#2676](https://github.com/casey/just/pull/2676) by [jpeeler](https://github.com/jpeeler))\n- Add `[metadata]` recipe attribute ([#2794](https://github.com/casey/just/pull/2794) by [wiktor-k](https://github.com/wiktor-k))\n- Allow depending on recipes in submodules ([#2672](https://github.com/casey/just/pull/2672) by [corvusrabus](https://github.com/corvusrabus))\n\n### Changed\n- Allow completing multiple recipes in bash ([#2764](https://github.com/casey/just/pull/2764) by [antirais](https://github.com/antirais))\n- Make global justfile filename case-insensitive ([#2802](https://github.com/casey/just/pull/2802) by [casey](https://github.com/casey))\n\n[1.41.0](https://github.com/casey/just/releases/tag/1.41.0) - 2025-07-01\n------------------------------------------------------------------------\n\n### Changed\n- Treat SIGINFO as non-fatal ([#2788](https://github.com/casey/just/pull/2788) by [casey](https://github.com/casey))\n- Improve signal handling ([#2488](https://github.com/casey/just/pull/2488) by [casey](https://github.com/casey))\n\n### Added\n- Add `dotenv-override` setting ([#2785](https://github.com/casey/just/pull/2785) by [Lun4m](https://github.com/Lun4m))\n- Add `PATH_SEP` and `PATH_VAR_SEP` constants ([#2679](https://github.com/casey/just/pull/2679) by [casey](https://github.com/casey))\n- Add `--tempdir` command-line option ([#2798](https://github.com/casey/just/pull/2798) by [casey](https://github.com/casey))\n\n### Fixed\n- Pin `clap_complete` to last compatible version ([#2800](https://github.com/casey/just/pull/2800) by [casey](https://github.com/casey))\n\n### Misc\n- Add `arkade` to readme ([#2700](https://github.com/casey/just/pull/2700) by [rgee0](https://github.com/rgee0))\n- Link to pipx instead of pypi in readme ([#2799](https://github.com/casey/just/pull/2799) by [casey](https://github.com/casey))\n- Add reasons to `#[ignore]` attributes ([#2797](https://github.com/casey/just/pull/2797) by [casey](https://github.com/casey))\n- Mention that command-line environment variables are inherited ([#2783](https://github.com/casey/just/pull/2783) by [philipmgrant](https://github.com/philipmgrant))\n- Fix attribute grammar and update documentation ([#2790](https://github.com/casey/just/pull/2790) by [casey](https://github.com/casey))\n- Tweak prose in groups section of readme ([#2767](https://github.com/casey/just/pull/2767) by [offby1](https://github.com/offby1))\n- Update `extractions/setup-just` version in readme ([#2735](https://github.com/casey/just/pull/2735) by [esadek](https://github.com/esadek))\n- Remove `return` in `Recipe::confirm` ([#2789](https://github.com/casey/just/pull/2789) by [casey](https://github.com/casey))\n- Add `mise` to alternatives in readem ([#2758](https://github.com/casey/just/pull/2758) by [koppor](https://github.com/koppor))\n- Update `softprops/action-gh-release` ([#2781](https://github.com/casey/just/pull/2781) by [app/dependabot](https://github.com/app/dependabot))\n- Use `default` as name of `--init` justfile default recipe ([#2777](https://github.com/casey/just/pull/2777) by [casey](https://github.com/casey))\n- Add `just.systems` link to `--init` justfile ([#2776](https://github.com/casey/just/pull/2776) by [casey](https://github.com/casey))\n- Fix `kitchen-sink.just` comment ([#2738](https://github.com/casey/just/pull/2738) by [azarmadr](https://github.com/azarmadr))\n- Update `softprops/action-gh-release` ([#2716](https://github.com/casey/just/pull/2716) by [app/dependabot](https://github.com/app/dependabot))\n- Fix clippy lints ([#2768](https://github.com/casey/just/pull/2768) by [casey](https://github.com/casey))\n- Add back-to-the-top link to readme ([#2707](https://github.com/casey/just/pull/2707) by [bravesasha](https://github.com/bravesasha))\n- Placate clippy lints for 1.86 ([#2708](https://github.com/casey/just/pull/2708) by [casey](https://github.com/casey))\n- Use `-S` in `uv` example ([#2696](https://github.com/casey/just/pull/2696) by [rmoorman](https://github.com/rmoorman))\n- Handle `--request` without parsing justfile ([#2683](https://github.com/casey/just/pull/2683) by [casey](https://github.com/casey))\n- Bump MSRV to 1.77 and enforce on CI ([#2674](https://github.com/casey/just/pull/2674) by [casey](https://github.com/casey))\n\n[1.40.0](https://github.com/casey/just/releases/tag/1.40.0) - 2025-03-09\n------------------------------------------------------------------------\n\n### Added\n- Allow the target of aliases to be recipes in submodules ([#2632](https://github.com/casey/just/pull/2632) by [corvusrabus](https://github.com/corvusrabus))\n- Make `--list-submodules` require `--list` ([#2622](https://github.com/casey/just/pull/2622) by [casey](https://github.com/casey))\n\n### Fixed\n- Star parameters may follow default parameters ([#2660](https://github.com/casey/just/pull/2660) by [casey](https://github.com/casey))\n\n### Misc\n- Remove `test!` macro from readme ([#2648](https://github.com/casey/just/pull/2648) by [casey](https://github.com/casey))\n- Sort enum variant, struct member, and trait members alphabetically ([#2646](https://github.com/casey/just/pull/2646) by [casey](https://github.com/casey))\n- Add Zed extension to readme ([#2640](https://github.com/casey/just/pull/2640) by [sectore](https://github.com/sectore))\n- Refactor error checking in choose function ([#2643](https://github.com/casey/just/pull/2643) by [casey](https://github.com/casey))\n- Use `Test` struct instead of `test!` macro ([#2642](https://github.com/casey/just/pull/2642) by [casey](https://github.com/casey))\n- Include unicode codepoint in unknown start of token error  ([#2637](https://github.com/casey/just/pull/2637) by [CramBL](https://github.com/CramBL))\n- Ignore broken pipe error from chooser ([#2639](https://github.com/casey/just/pull/2639) by [casey](https://github.com/casey))\n- Guarantee that `Namepath`s are non-empty ([#2638](https://github.com/casey/just/pull/2638) by [casey](https://github.com/casey))\n- Remove unnecessary binding modifiers ([#2636](https://github.com/casey/just/pull/2636) by [casey](https://github.com/casey))\n- Document Vim and Neovim built-in syntax highlighting ([#2619](https://github.com/casey/just/pull/2619) by [laniakea64](https://github.com/laniakea64))\n- Remove rust:just Repology badge ([#2614](https://github.com/casey/just/pull/2614) by [laniakea64](https://github.com/laniakea64))\n- Clarify --list argument ([#2609](https://github.com/casey/just/pull/2609) by [casey](https://github.com/casey))\n- Expand Windows path documentation ([#2602](https://github.com/casey/just/pull/2602) by [casey](https://github.com/casey))\n- Fix readme typos ([#2596](https://github.com/casey/just/pull/2596) by [CramBL](https://github.com/CramBL))\n\n[1.39.0](https://github.com/casey/just/releases/tag/1.39.0) - 2025-01-22\n------------------------------------------------------------------------\n\n### Added\n- Add `which()` and `require()` for finding executables ([#2440](https://github.com/casey/just/pull/2440) by [0xzhzh](https://github.com/0xzhzh))\n- Add `no-exit-message` Setting and `[exit-message]` attribute ([#2568](https://github.com/casey/just/pull/2568) by [ArchieAtkinson](https://github.com/ArchieAtkinson))\n- Configure alias style in `--list` with `--alias-style` ([#2342](https://github.com/casey/just/pull/2342) by [marcaddeo](https://github.com/marcaddeo))\n- Add regex mismatch conditional operator ([#2490](https://github.com/casey/just/pull/2490) by [laniakea64](https://github.com/laniakea64))\n- Add `read_to_string(path)` function ([#2507](https://github.com/casey/just/pull/2507) by [begoon](https://github.com/begoon))\n\n### Changed\n- Rename `read_to_string()` to `read()` ([#2518](https://github.com/casey/just/pull/2518) by [casey](https://github.com/casey))\n\n### Fixed\n- Keep `[private]` attribute when formatting assignments ([#2592](https://github.com/casey/just/pull/2592) by [casey](https://github.com/casey))\n- Format `if … else if …` without superfluous braces ([#2573](https://github.com/casey/just/pull/2573) by [casey](https://github.com/casey))\n- Fix error when lexing `!` at end-of-file ([#2520](https://github.com/casey/just/pull/2520) by [casey](https://github.com/casey))\n- Handle recipes in submodules in fish completion script ([#2514](https://github.com/casey/just/pull/2514) by [senekor](https://github.com/senekor))\n\n### Misc\n- Add tests for `require()` ([#2594](https://github.com/casey/just/pull/2594) by [casey](https://github.com/casey))\n- Evaluate concatenations and joins from left to right ([#2593](https://github.com/casey/just/pull/2593) by [casey](https://github.com/casey))\n- Disable links to empty chapters in book ([#2589](https://github.com/casey/just/pull/2589) by [casey](https://github.com/casey))\n- Link to CI workflow in readme ([#2586](https://github.com/casey/just/pull/2586) by [bravesasha](https://github.com/bravesasha))\n- Clarify that `trim_*_match` functions take subtrings ([#2574](https://github.com/casey/just/pull/2574) by [xavdid](https://github.com/xavdid))\n- Update `softprops/action-gh-release` from 2.2.0 to 2.2.1 ([#2570](https://github.com/casey/just/pull/2570) by [app/dependabot](https://github.com/app/dependabot))\n- Check attributes in parser instead of analyzer ([#2560](https://github.com/casey/just/pull/2560) by [casey](https://github.com/casey))\n- Ignore I/O errors when writing changelog to stdout ([#2558](https://github.com/casey/just/pull/2558) by [casey](https://github.com/casey))\n- Add `quiet` setting and fix typos in readme ([#2549](https://github.com/casey/just/pull/2549) by [unennhexium](https://github.com/unennhexium))\n- Update readme to use `env()` instead of `env_var*()` ([#2546](https://github.com/casey/just/pull/2546) by [laniakea64](https://github.com/laniakea64))\n- Document using `||` to provide default for empty environment variable ([#2545](https://github.com/casey/just/pull/2545) by [casey](https://github.com/casey))\n- Refactor `Line` predicates ([#2543](https://github.com/casey/just/pull/2543) by [casey](https://github.com/casey))\n- Fix typos in README.md ([#2542](https://github.com/casey/just/pull/2542) by [laniakea64](https://github.com/laniakea64))\n- Add full example getting XDG user directory to readme ([#2536](https://github.com/casey/just/pull/2536) by [laniakea64](https://github.com/laniakea64))\n- Document weird behavior of duplicate definitions in imports ([#2541](https://github.com/casey/just/pull/2541) by [casey](https://github.com/casey))\n- Update readme to reflect actual behavior of user directory functions ([#2535](https://github.com/casey/just/pull/2535) by [casey](https://github.com/casey))\n- Update softprops/action-gh-release to 2.2.0 ([#2530](https://github.com/casey/just/pull/2530) by [app/dependabot](https://github.com/app/dependabot))\n- Document running python recipes with `uv` ([#2526](https://github.com/casey/just/pull/2526) by [casey](https://github.com/casey))\n- Sort functions alphabetically ([#2525](https://github.com/casey/just/pull/2525) by [casey](https://github.com/casey))\n- Fix truncated bang operator error message ([#2522](https://github.com/casey/just/pull/2522) by [casey](https://github.com/casey))\n- Include source path in dump JSON ([#2466](https://github.com/casey/just/pull/2466) by [psibi](https://github.com/psibi))\n- Add attribute set ([#2419](https://github.com/casey/just/pull/2419) by [neunenak](https://github.com/neunenak))\n\n[1.38.0](https://github.com/casey/just/releases/tag/1.38.0) - 2024-12-10\n------------------------------------------------------------------------\n\n### Added\n- Add `[openbsd]` recipe attribute ([#2497](https://github.com/casey/just/pull/2497) by [vtamara](https://github.com/vtamara))\n- Add `[working-directory]` recipe attribute ([#2438](https://github.com/casey/just/pull/2438) by [bcheidemann](https://github.com/bcheidemann))\n- Add `--allow-missing` to ignore missing recipe and submodule errors ([#2460](https://github.com/casey/just/pull/2460) by [R3ZV](https://github.com/R3ZV))\n\n### Changed\n- Add snap package back to readme ([#2506](https://github.com/casey/just/pull/2506) by [casey](https://github.com/casey))\n- Forbid duplicate non-repeatable attributes ([#2483](https://github.com/casey/just/pull/2483) by [casey](https://github.com/casey))\n\n### Misc\n- Publish docs to GitHub pages on release only ([#2516](https://github.com/casey/just/pull/2516) by [casey](https://github.com/casey))\n- Note lack of support for string interpolation ([#2515](https://github.com/casey/just/pull/2515) by [casey](https://github.com/casey))\n- Embolden help text errors ([#2502](https://github.com/casey/just/pull/2502) by [casey](https://github.com/casey))\n- Style help text ([#2501](https://github.com/casey/just/pull/2501) by [casey](https://github.com/casey))\n- Add `--request` subcommand for testing ([#2498](https://github.com/casey/just/pull/2498) by [casey](https://github.com/casey))\n- [bin/forbid] Improve error message if ripgrep is missing ([#2493](https://github.com/casey/just/pull/2493) by [casey](https://github.com/casey))\n- Fix Rust 1.83 clippy warnings ([#2487](https://github.com/casey/just/pull/2487) by [casey](https://github.com/casey))\n- Refactor JSON tests ([#2484](https://github.com/casey/just/pull/2484) by [casey](https://github.com/casey))\n- Get `Config` from `ExecutionContext` instead of passing separately ([#2481](https://github.com/casey/just/pull/2481) by [casey](https://github.com/casey))\n- Don't write justfiles unchanged by formatting ([#2479](https://github.com/casey/just/pull/2479) by [casey](https://github.com/casey))\n\n[1.37.0](https://github.com/casey/just/releases/tag/1.37.0) - 2024-11-20\n------------------------------------------------------------------------\n\n### Added\n- Add `style()` function ([#2462](https://github.com/casey/just/pull/2462) by [casey](https://github.com/casey))\n- Terminal escape sequence constants ([#2461](https://github.com/casey/just/pull/2461) by [casey](https://github.com/casey))\n- Add `&&` and `||` operators ([#2444](https://github.com/casey/just/pull/2444) by [casey](https://github.com/casey))\n\n### Changed\n- Make recipe doc attribute override comment ([#2470](https://github.com/casey/just/pull/2470) by [casey](https://github.com/casey))\n- Don't export constants ([#2449](https://github.com/casey/just/pull/2449) by [casey](https://github.com/casey))\n- Allow duplicate imports ([#2437](https://github.com/casey/just/pull/2437) by [casey](https://github.com/casey))\n- Publish single SHA256SUM file with releases ([#2417](https://github.com/casey/just/pull/2417) by [casey](https://github.com/casey))\n- Mark recipes with private attribute as private in JSON dump ([#2415](https://github.com/casey/just/pull/2415) by [casey](https://github.com/casey))\n- Forbid invalid attributes on assignments ([#2412](https://github.com/casey/just/pull/2412) by [casey](https://github.com/casey))\n\n### Misc\n- Update `softprops/action-gh-release` ([#2471](https://github.com/casey/just/pull/2471) by [app/dependabot](https://github.com/app/dependabot))\n- Add `-g` to `rust-just` install instructions ([#2459](https://github.com/casey/just/pull/2459) by [gnpaone](https://github.com/gnpaone))\n- Change doc backtick color to cyan ([#2469](https://github.com/casey/just/pull/2469) by [casey](https://github.com/casey))\n- Note that `set shell` is not used for `[script]` recipes ([#2468](https://github.com/casey/just/pull/2468) by [iloveitaly](https://github.com/iloveitaly))\n- Replace `derivative` with `derive-where` ([#2465](https://github.com/casey/just/pull/2465) by [laniakea64](https://github.com/laniakea64))\n- Highlight backticks in docs when listing recipes ([#2423](https://github.com/casey/just/pull/2423) by [neunenak](https://github.com/neunenak))\n- Update setup-just version in README ([#2456](https://github.com/casey/just/pull/2456) by [Julian](https://github.com/Julian))\n- Fix shell function example in readme ([#2454](https://github.com/casey/just/pull/2454) by [casey](https://github.com/casey))\n- Update softprops/action-gh-release ([#2450](https://github.com/casey/just/pull/2450) by [app/dependabot](https://github.com/app/dependabot))\n- Use `justfile` instead of `mf` on invalid examples in readme ([#2447](https://github.com/casey/just/pull/2447) by [casey](https://github.com/casey))\n- Add advice on printing complex strings ([#2446](https://github.com/casey/just/pull/2446) by [casey](https://github.com/casey))\n- Document using functions in variable assignments ([#2431](https://github.com/casey/just/pull/2431) by [offby1](https://github.com/offby1))\n- Use prettier string comparison in tests ([#2435](https://github.com/casey/just/pull/2435) by [neunenak](https://github.com/neunenak))\n- Note `shell(…)` as an alternative to backticks ([#2430](https://github.com/casey/just/pull/2430) by [offby1](https://github.com/offby1))\n- Update nix package links ([#2441](https://github.com/casey/just/pull/2441) by [yunusey](https://github.com/yunusey))\n- Update README.中文.md ([#2424](https://github.com/casey/just/pull/2424) by [Jannchie](https://github.com/Jannchie))\n- Add Recipe::subsequents ([#2428](https://github.com/casey/just/pull/2428) by [casey](https://github.com/casey))\n- Add subsequents to grammar ([#2427](https://github.com/casey/just/pull/2427) by [casey](https://github.com/casey))\n- Document checking releases hashes  ([#2418](https://github.com/casey/just/pull/2418) by [casey](https://github.com/casey))\n- Show how to access positional arguments with powershell ([#2405](https://github.com/casey/just/pull/2405) by [casey](https://github.com/casey))\n- Use `-CommandWithArgs` instead of `-cwa` ([#2404](https://github.com/casey/just/pull/2404) by [casey](https://github.com/casey))\n- Document `-cwa` flag for PowerShell positional arguments ([#2403](https://github.com/casey/just/pull/2403) by [casey](https://github.com/casey))\n- Use `unwrap_or` when creating relative path in loader ([#2400](https://github.com/casey/just/pull/2400) by [casey](https://github.com/casey))\n\n[1.36.0](https://github.com/casey/just/releases/tag/1.36.0) - 2024-09-30\n------------------------------------------------------------------------\n\n### Changed\n- Allow default values to use earlier recipe arguments ([#2382](https://github.com/casey/just/pull/2382) by [casey](https://github.com/casey))\n\n### Added\n- Add `--one` flag to forbid multiple recipes from being invoked on the command line ([#2374](https://github.com/casey/just/pull/2374) by [casey](https://github.com/casey))\n- Allow including arbitrary characters in strings with `\\u{…}` ([#2360](https://github.com/casey/just/pull/2360) by [laniakea64](https://github.com/laniakea64))\n- Print recipe doc string when`--explain` flag  is passed ([#2319](https://github.com/casey/just/pull/2319) by [neunenak](https://github.com/neunenak))\n\n### Misc\n- Use unwrap_or_default() when getting default color and verbosity ([#2397](https://github.com/casey/just/pull/2397) by [casey](https://github.com/casey))\n- De-duplicate suggestion methods ([#2392](https://github.com/casey/just/pull/2392) by [neunenak](https://github.com/neunenak))\n- Refactor analyzer ([#2378](https://github.com/casey/just/pull/2378) by [neunenak](https://github.com/neunenak))\n- Use `console` codeblocks in readme ([#2388](https://github.com/casey/just/pull/2388) by [casey](https://github.com/casey))\n- Split packages table by platform ([#2385](https://github.com/casey/just/pull/2385) by [casey](https://github.com/casey))\n- Document npm package ([#2384](https://github.com/casey/just/pull/2384) by [casey](https://github.com/casey))\n- Add PyPI install instructions ([#2383](https://github.com/casey/just/pull/2383) by [casey](https://github.com/casey))\n- Remove alias shadows recipe error ([#2375](https://github.com/casey/just/pull/2375) by [neunenak](https://github.com/neunenak))\n- Name instead of number book chapter files ([#2372](https://github.com/casey/just/pull/2372) by [casey](https://github.com/casey))\n- Add groups to project justfile ([#2351](https://github.com/casey/just/pull/2351) by [neunenak](https://github.com/neunenak))\n- Document `\\u{...}` ([#2371](https://github.com/casey/just/pull/2371) by [laniakea64](https://github.com/laniakea64))\n- Remove old recipes from project justfile ([#2367](https://github.com/casey/just/pull/2367) by [casey](https://github.com/casey))\n- Document `--dotenv-path` in readme ([#2366](https://github.com/casey/just/pull/2366) by [willie](https://github.com/willie))\n- Remove ref-type crate ([#2364](https://github.com/casey/just/pull/2364) by [casey](https://github.com/casey))\n- Fix type names in redefinition error message ([#2353](https://github.com/casey/just/pull/2353) by [marcaddeo](https://github.com/marcaddeo))\n- Use relative in `.sha256sum` files ([#2358](https://github.com/casey/just/pull/2358) by [casey](https://github.com/casey))\n- Link to readme in CONTRIBUTING.md ([#2348](https://github.com/casey/just/pull/2348) by [casey](https://github.com/casey))\n- Fix clippy lints ([#2347](https://github.com/casey/just/pull/2347) by [casey](https://github.com/casey))\n- Simplify `Subcommand::run` ([#2336](https://github.com/casey/just/pull/2336) by [neunenak](https://github.com/neunenak))\n- Update module issue link in readme ([#2345](https://github.com/casey/just/pull/2345) by [casey](https://github.com/casey))\n- Add blank line between CI workflow jobs ([#2343](https://github.com/casey/just/pull/2343) by [casey](https://github.com/casey))\n- Color groups in `--list` output ([#2340](https://github.com/casey/just/pull/2340) by [casey](https://github.com/casey))\n- Refactor and document subcommand and search ([#2335](https://github.com/casey/just/pull/2335) by [neunenak](https://github.com/neunenak))\n- Document private variables ([#2331](https://github.com/casey/just/pull/2331) by [Jasha10](https://github.com/Jasha10))\n\n[1.35.0](https://github.com/casey/just/releases/tag/1.35.0) - 2024-08-28\n------------------------------------------------------------------------\n\n### Changed\n- Allow fallback with recipes in submodules ([#2329](https://github.com/casey/just/pull/2329) by [casey](https://github.com/casey))\n- Allow `[private]` attribute on assignments ([#2300](https://github.com/casey/just/pull/2300) by [adsnaider](https://github.com/adsnaider))\n\n### Misc\n- Generate `.sha256sum` files for release artifacts ([#2323](https://github.com/casey/just/pull/2323) by [twm](https://github.com/twm))\n- Clarify that subsequent dependencies run immediately after recipe ([#2326](https://github.com/casey/just/pull/2326) by [casey](https://github.com/casey))\n- Fix readme typo ([#2321](https://github.com/casey/just/pull/2321) by [arminius-smh](https://github.com/arminius-smh))\n- Remove Config::run ([#2320](https://github.com/casey/just/pull/2320) by [neunenak](https://github.com/neunenak))\n- Bump MSRV to 1.74 ([#2306](https://github.com/casey/just/pull/2306) by [casey](https://github.com/casey))\n- Remove logging ([#2305](https://github.com/casey/just/pull/2305) by [casey](https://github.com/casey))\n- Group commands under dedicated heading in `--help` output ([#2302](https://github.com/casey/just/pull/2302) by [casey](https://github.com/casey))\n- Fix readme typo ([#2297](https://github.com/casey/just/pull/2297) by [nyurik](https://github.com/nyurik))\n\n[1.34.0](https://github.com/casey/just/releases/tag/1.34.0) - 2024-08-02\n------------------------------------------------------------------------\n\n### Fixed\n- Make function paths relative to correct working directory ([#2294](https://github.com/casey/just/pull/2294) by [casey](https://github.com/casey))\n\n### Changed\n- Keep multi-line shebangs together ([#2276](https://github.com/casey/just/pull/2276) by [vkstrm](https://github.com/vkstrm))\n\n### Misc\n- Document `set working-directory` ([#2288](https://github.com/casey/just/pull/2288) by [nyurik](https://github.com/nyurik))\n- Fix readme typos ([#2289](https://github.com/casey/just/pull/2289) by [casey](https://github.com/casey))\n\n[1.33.0](https://github.com/casey/just/releases/tag/1.33.0) - 2024-07-30\n------------------------------------------------------------------------\n\n### Fixed\n- Use correct backtick and `shell()` expression working directory in submodules ([#2285](https://github.com/casey/just/pull/2285) by [casey](https://github.com/casey))\n\n### Added\n- Add `working-directory` setting ([#2283](https://github.com/casey/just/pull/2283) by [nyurik](https://github.com/nyurik))\n- Allow `[group]` attribute on submodules ([#2263](https://github.com/casey/just/pull/2263) by [jmwoliver](https://github.com/jmwoliver))\n- Allow empty `[script]` attribute and add `set script-interpreter` ([#2264](https://github.com/casey/just/pull/2264) by [casey](https://github.com/casey))\n\n### Misc\n- Document which attributes apply to which items ([#2282](https://github.com/casey/just/pull/2282) by [casey](https://github.com/casey))\n- Add missing productions ([#2280](https://github.com/casey/just/pull/2280) by [poliorcetics](https://github.com/poliorcetics))\n- Fix Rust 1.80.0 warnings ([#2281](https://github.com/casey/just/pull/2281) by [casey](https://github.com/casey))\n- Update softprops/action-gh-release ([#2269](https://github.com/casey/just/pull/2269) by [app/dependabot](https://github.com/app/dependabot))\n- Remove `(no group)` header before ungrouped recipes ([#2268](https://github.com/casey/just/pull/2268) by [casey](https://github.com/casey))\n- Document `script-interpreter` setting ([#2265](https://github.com/casey/just/pull/2265) by [casey](https://github.com/casey))\n- `set dotenv-path` does not override `set dotenv-filename` ([#2262](https://github.com/casey/just/pull/2262) by [casey](https://github.com/casey))\n\n[1.32.0](https://github.com/casey/just/releases/tag/1.32.0) - 2024-07-17\n------------------------------------------------------------------------\n\n### Added\n- Add unstable `[script(…)]` attribute ([#2259](https://github.com/casey/just/pull/2259) by [casey](https://github.com/casey))\n- Add `[extension: 'EXT']` attribute to set shebang recipe script file extension ([#2256](https://github.com/casey/just/pull/2256) by [casey](https://github.com/casey))\n- Suppress mod doc comment with empty `[doc]` attribute ([#2254](https://github.com/casey/just/pull/2254) by [casey](https://github.com/casey))\n- Allow `[doc]` annotation on modules ([#2247](https://github.com/casey/just/pull/2247) by [neunenak](https://github.com/neunenak))\n\n[1.31.0](https://github.com/casey/just/releases/tag/1.31.0) - 2024-07-14\n------------------------------------------------------------------------\n\n### Stabilized\n- Stabilize modules ([#2250](https://github.com/casey/just/pull/2250) by [casey](https://github.com/casey))\n\n### Added\n- Allow `mod` path to be directory containing module source ([#2238](https://github.com/casey/just/pull/2238) by [casey](https://github.com/casey))\n- Allow enabling unstable features with `set unstable` ([#2237](https://github.com/casey/just/pull/2237) by [casey](https://github.com/casey))\n- Allow abbreviating functions ending in `_directory` to `_dir` ([#2235](https://github.com/casey/just/pull/2235) by [casey](https://github.com/casey))\n\n### Fixed\n- Lexiclean search directory so `..` does not check the current directory ([#2236](https://github.com/casey/just/pull/2236) by [casey](https://github.com/casey))\n\n### Misc\n- Print space before submodules in `--list` with groups ([#2244](https://github.com/casey/just/pull/2244) by [casey](https://github.com/casey))\n\n[1.30.1](https://github.com/casey/just/releases/tag/1.30.1) - 2024-07-06\n------------------------------------------------------------------------\n\n### Fixed\n- Fix function argument count mismatch error message ([#2231](https://github.com/casey/just/pull/2231) by [casey](https://github.com/casey))\n\n[1.30.0](https://github.com/casey/just/releases/tag/1.30.0) - 2024-07-06\n------------------------------------------------------------------------\n\n### Fixed\n- Allow comments after `mod` statements ([#2201](https://github.com/casey/just/pull/2201) by [casey](https://github.com/casey))\n\n### Changed\n- Allow unstable features with `--summary` ([#2210](https://github.com/casey/just/pull/2210) by [casey](https://github.com/casey))\n- Don't analyze comments when `ignore-comments` is set ([#2180](https://github.com/casey/just/pull/2180) by [casey](https://github.com/casey))\n- List recipes by group in group justfile order with `just --list --unsorted` ([#2164](https://github.com/casey/just/pull/2164) by [casey](https://github.com/casey))\n- List groups in source order with `just --groups --unsorted` ([#2160](https://github.com/casey/just/pull/2160) by [casey](https://github.com/casey))\n\n### Added\n- Avoid `install` and add 32-bit arm targets to `install.sh` ([#2214](https://github.com/casey/just/pull/2214) by [CramBL](https://github.com/CramBL))\n- Give modules doc comments for `--list` ([#2199](https://github.com/casey/just/pull/2199) by [Spatenheinz](https://github.com/Spatenheinz))\n- Add `datetime()` and `datetime_utc()` functions ([#2167](https://github.com/casey/just/pull/2167) by [casey](https://github.com/casey))\n- Allow setting more command-line options with environment variables ([#2161](https://github.com/casey/just/pull/2161) by [casey](https://github.com/casey))\n\n### Library\n- Don't exit process in `run()` on argument parse error ([#2176](https://github.com/casey/just/pull/2176) by [casey](https://github.com/casey))\n- Allow passing command-line arguments into `run()` ([#2173](https://github.com/casey/just/pull/2173) by [casey](https://github.com/casey))\n- Ignore env_logger initialization errors ([#2170](https://github.com/casey/just/pull/2170) by [EnigmaCurry](https://github.com/EnigmaCurry))\n\n### Misc\n- Tweak readme ([#2227](https://github.com/casey/just/pull/2227) by [casey](https://github.com/casey))\n- Add development guide to readme ([#2226](https://github.com/casey/just/pull/2226) by [casey](https://github.com/casey))\n- Add shell-expanded string syntax to grammar ([#2223](https://github.com/casey/just/pull/2223) by [casey](https://github.com/casey))\n- Add recipe for testing bash completion script ([#2221](https://github.com/casey/just/pull/2221) by [casey](https://github.com/casey))\n- Fix use of `justfile_directory()` in readme ([#2219](https://github.com/casey/just/pull/2219) by [casey](https://github.com/casey))\n- Use default values for `--list-heading` and `--list-prefix` ([#2213](https://github.com/casey/just/pull/2213) by [casey](https://github.com/casey))\n- Use `clap::ValueParser` ([#2211](https://github.com/casey/just/pull/2211) by [neunenak](https://github.com/neunenak))\n- Document module doc comments in readme ([#2208](https://github.com/casey/just/pull/2208) by [casey](https://github.com/casey))\n- Use `-and` instead of `&&` in PowerShell completion script ([#2204](https://github.com/casey/just/pull/2204) by [casey](https://github.com/casey))\n- Fix readme formatting ([#2203](https://github.com/casey/just/pull/2203) by [casey](https://github.com/casey))\n- Link to justfiles on GitHub in readme ([#2198](https://github.com/casey/just/pull/2198) by [bukowa](https://github.com/bukowa))\n- Link to modules when first introduced in readme ([#2193](https://github.com/casey/just/pull/2193) by [casey](https://github.com/casey))\n- Update `softprops/action-gh-release` ([#2183](https://github.com/casey/just/pull/2183) by [app/dependabot](https://github.com/app/dependabot))\n- Document remote justfile workaround ([#2175](https://github.com/casey/just/pull/2175) by [casey](https://github.com/casey))\n- Document library interface ([#2174](https://github.com/casey/just/pull/2174) by [casey](https://github.com/casey))\n- Remove dependency on cradle ([#2169](https://github.com/casey/just/pull/2169) by [nc7s](https://github.com/nc7s))\n- Add note to readme about quoting paths on Windows ([#2166](https://github.com/casey/just/pull/2166) by [casey](https://github.com/casey))\n- Add missing changelog credits ([#2163](https://github.com/casey/just/pull/2163) by [casey](https://github.com/casey))\n- Credit myself in changelog ([#2162](https://github.com/casey/just/pull/2162) by [casey](https://github.com/casey))\n\n[1.29.1](https://github.com/casey/just/releases/tag/1.29.1) - 2024-06-14\n------------------------------------------------------------------------\n\n### Fixed\n- Fix unexport syntax conflicts ([#2158](https://github.com/casey/just/pull/2158) by [casey](https://github.com/casey))\n\n[1.29.0](https://github.com/casey/just/releases/tag/1.29.0) - 2024-06-13\n------------------------------------------------------------------------\n\n### Added\n- Add [positional-arguments] attribute ([#2151](https://github.com/casey/just/pull/2151) by [casey](https://github.com/casey))\n- Use `--justfile` in Fish shell completions ([#2148](https://github.com/casey/just/pull/2148) by [rubot](https://github.com/rubot))\n- Add `is_dependency()` function ([#2139](https://github.com/casey/just/pull/2139) by [neunenak](https://github.com/neunenak))\n- Allow printing nu completion script with `just --completions nushell` ([#2140](https://github.com/casey/just/pull/2140) by [casey](https://github.com/casey))\n- Add `[ATTRIBUTE: VALUE]` shorthand ([#2136](https://github.com/casey/just/pull/2136) by [neunenak](https://github.com/neunenak))\n- Allow unexporting environment variables ([#2098](https://github.com/casey/just/pull/2098) by [neunenak](https://github.com/neunenak))\n\n### Fixed\n- Load environment file from dotenv-path relative to working directory ([#2152](https://github.com/casey/just/pull/2152) by [casey](https://github.com/casey))\n- Fix `fzf` chooser preview with space-separated module paths ([#2141](https://github.com/casey/just/pull/2141) by [casey](https://github.com/casey))\n\n### Misc\n- Improve argument parsing and error handling for submodules ([#2154](https://github.com/casey/just/pull/2154) by [casey](https://github.com/casey))\n- Document shell expanded string defaults ([#2153](https://github.com/casey/just/pull/2153) by [casey](https://github.com/casey))\n- Test bare bash path in shebang on windows ([#2144](https://github.com/casey/just/pull/2144) by [casey](https://github.com/casey))\n- Test shell not found error messages ([#2145](https://github.com/casey/just/pull/2145) by [casey](https://github.com/casey))\n- Refactor evaluator ([#2138](https://github.com/casey/just/pull/2138) by [neunenak](https://github.com/neunenak))\n- Fix man page generation in release workflow ([#2132](https://github.com/casey/just/pull/2132) by [casey](https://github.com/casey))\n\n[1.28.0](https://github.com/casey/just/releases/tag/1.28.0) - 2024-06-05\n------------------------------------------------------------------------\n\n### Changed\n- Write shebang recipes to $XDG_RUNTIME_DIR ([#2128](https://github.com/casey/just/pull/2128) by [casey](https://github.com/casey))\n- Add `set dotenv-required` to require an environment file ([#2116](https://github.com/casey/just/pull/2116) by [casey](https://github.com/casey))\n- Don't display submodule recipes in `--list` ([#2112](https://github.com/casey/just/pull/2112) by [casey](https://github.com/casey))\n\n### Added\n- Allow listing recipes in submodules with `--list-submodules` ([#2113](https://github.com/casey/just/pull/2113) by [casey](https://github.com/casey))\n- Show recipes in submodules with `--show RECIPE::PATH` ([#2111](https://github.com/casey/just/pull/2111) by [casey](https://github.com/casey))\n- Add `--timestamp-format` ([#2106](https://github.com/casey/just/pull/2106) by [neunenak](https://github.com/neunenak))\n- Allow listing submodule recipes with `--list PATH` ([#2108](https://github.com/casey/just/pull/2108) by [casey](https://github.com/casey))\n- Print recipe command timestamps with `--timestamps` ([#2084](https://github.com/casey/just/pull/2084) by [neunenak](https://github.com/neunenak))\n- Add `module_file()` and `module_directory()` functions ([#2105](https://github.com/casey/just/pull/2105) by [casey](https://github.com/casey))\n\n### Fixed\n- Use space-separated recipe paths in `--choose` ([#2115](https://github.com/casey/just/pull/2115) by [casey](https://github.com/casey))\n- Fix bash completion for aliases ([#2104](https://github.com/casey/just/pull/2104) by [laniakea64](https://github.com/laniakea64))\n\n### Misc\n- Don't check in manpage ([#2130](https://github.com/casey/just/pull/2130) by [casey](https://github.com/casey))\n- Document default shell ([#2129](https://github.com/casey/just/pull/2129) by [casey](https://github.com/casey))\n- Remove duplicate section in Chinese readme ([#2127](https://github.com/casey/just/pull/2127) by [potterxu](https://github.com/potterxu))\n- Update Chinese readme ([#2124](https://github.com/casey/just/pull/2124) by [potterxu](https://github.com/potterxu))\n- Fix typo in readme ([#2122](https://github.com/casey/just/pull/2122) by [potterxu](https://github.com/potterxu))\n- Don't check in auto-generated completion scripts ([#2120](https://github.com/casey/just/pull/2120) by [casey](https://github.com/casey))\n- Document when dependencies run in readme ([#2103](https://github.com/casey/just/pull/2103) by [casey](https://github.com/casey))\n- Build aarch64-pc-windows-msvc release binaries ([#2100](https://github.com/casey/just/pull/2100) by [alshdavid](https://github.com/alshdavid))\n- Clarify that `dotenv-path`-given env file is required ([#2099](https://github.com/casey/just/pull/2099) by [casey](https://github.com/casey))\n- Print multi-line doc comments before recipe in `--list` ([#2090](https://github.com/casey/just/pull/2090) by [casey](https://github.com/casey))\n- List unsorted imported recipes by import depth and offset ([#2092](https://github.com/casey/just/pull/2092) by [casey](https://github.com/casey))\n- Update README.md ([#2091](https://github.com/casey/just/pull/2091) by [laniakea64](https://github.com/laniakea64))\n\n[1.27.0](https://github.com/casey/just/releases/tag/1.27.0) - 2024-05-25\n------------------------------------------------------------------------\n\n### Changed\n- Use cache dir for temporary files ([#2067](https://github.com/casey/just/pull/2067) by [casey](https://github.com/casey))\n\n### Added\n- Add `[doc]` attribute to set and suppress documentation comments ([#2050](https://github.com/casey/just/pull/2050) by [neunenak](https://github.com/neunenak))\n- Add source_file() and source_directory() functions ([#2088](https://github.com/casey/just/pull/2088) by [casey](https://github.com/casey))\n- Add recipe groups ([#1842](https://github.com/casey/just/pull/1842) by [neunenak](https://github.com/neunenak))\n- Add shell() function for running external commands ([#2047](https://github.com/casey/just/pull/2047) by [gyreas](https://github.com/gyreas))\n- Add `--global-justfile` flag ([#1846](https://github.com/casey/just/pull/1846) by [neunenak](https://github.com/neunenak))\n- Add shell-expanded strings ([#2055](https://github.com/casey/just/pull/2055) by [casey](https://github.com/casey))\n- Add `encode_uri_component` function ([#2052](https://github.com/casey/just/pull/2052) by [laniakea64](https://github.com/laniakea64))\n- Add `choose` function for generating random strings ([#2049](https://github.com/casey/just/pull/2049) by [laniakea64](https://github.com/laniakea64))\n- Add predefined constants ([#2054](https://github.com/casey/just/pull/2054) by [casey](https://github.com/casey))\n- Allow setting some command-line options with environment variables ([#2044](https://github.com/casey/just/pull/2044) by [neunenak](https://github.com/neunenak))\n- Add prepend() function ([#2045](https://github.com/casey/just/pull/2045) by [gyreas](https://github.com/gyreas))\n- Add append() function ([#2046](https://github.com/casey/just/pull/2046) by [gyreas](https://github.com/gyreas))\n- Add --man subcommand ([#2041](https://github.com/casey/just/pull/2041) by [casey](https://github.com/casey))\n- Make `dotenv-path` relative to working directory ([#2040](https://github.com/casey/just/pull/2040) by [casey](https://github.com/casey))\n- Add `assert` expression ([#1845](https://github.com/casey/just/pull/1845) by [de1iza](https://github.com/de1iza))\n- Add 'allow-duplicate-variables' setting ([#1922](https://github.com/casey/just/pull/1922) by [Mijago](https://github.com/Mijago))\n\n### Fixed\n- List modules in source order with `--unsorted` ([#2085](https://github.com/casey/just/pull/2085) by [casey](https://github.com/casey))\n- Show submodule recipes in --choose ([#2069](https://github.com/casey/just/pull/2069) by [casey](https://github.com/casey))\n- Allow multiple imports of the same file in different modules ([#2065](https://github.com/casey/just/pull/2065) by [casey](https://github.com/casey))\n- Fix submodule recipe listing indentation ([#2063](https://github.com/casey/just/pull/2063) by [casey](https://github.com/casey))\n- Pass command as first argument to `shell` ([#2061](https://github.com/casey/just/pull/2061) by [casey](https://github.com/casey))\n- Allow shell expanded strings in mod and import paths ([#2059](https://github.com/casey/just/pull/2059) by [casey](https://github.com/casey))\n- Run imported recipes in root justfile with correct working directory ([#2056](https://github.com/casey/just/pull/2056) by [casey](https://github.com/casey))\n- Fix output `\\r\\n` stripping ([#2035](https://github.com/casey/just/pull/2035) by [casey](https://github.com/casey))\n\n### Misc\n- Forbid whitespace in shell-expanded string prefixes ([#2083](https://github.com/casey/just/pull/2083) by [casey](https://github.com/casey))\n- Add Debian and Ubuntu install instructions to readme ([#2072](https://github.com/casey/just/pull/2072) by [casey](https://github.com/casey))\n- Remove snap installation instructions from readme ([#2070](https://github.com/casey/just/pull/2070) by [casey](https://github.com/casey))\n- Fallback to wget in install script if curl isn't available([#1913](https://github.com/casey/just/pull/1913) by [tgross35](https://github.com/tgross35))\n- Use std::io::IsTerminal instead of atty crate ([#2066](https://github.com/casey/just/pull/2066) by [casey](https://github.com/casey))\n- Improve `shell()` documentation ([#2060](https://github.com/casey/just/pull/2060) by [laniakea64](https://github.com/laniakea64))\n- Add bash completion for snap ([#2058](https://github.com/casey/just/pull/2058) by [albertodonato](https://github.com/albertodonato))\n- Refactor list subcommand ([#2062](https://github.com/casey/just/pull/2062) by [casey](https://github.com/casey))\n- Document working directory ([#2053](https://github.com/casey/just/pull/2053) by [casey](https://github.com/casey))\n- Replace FunctionContext with Evaluator ([#2048](https://github.com/casey/just/pull/2048) by [casey](https://github.com/casey))\n- Update clap to version 4 ([#1924](https://github.com/casey/just/pull/1924) by [poliorcetics](https://github.com/poliorcetics))\n- Cleanup ([#2026](https://github.com/casey/just/pull/2026) by [adamnemecek](https://github.com/adamnemecek))\n- Increase --list maximum alignable width from 30 to 50 ([#2039](https://github.com/casey/just/pull/2039) by [casey](https://github.com/casey))\n- Document using `env -S` ([#2038](https://github.com/casey/just/pull/2038) by [casey](https://github.com/casey))\n- Update line continuation documentation ([#1998](https://github.com/casey/just/pull/1998) by [laniakea64](https://github.com/laniakea64))\n- Add example using GNU parallel to run tasks in concurrently ([#1915](https://github.com/casey/just/pull/1915) by [amarao](https://github.com/amarao))\n- Placate clippy: use `clone_into` ([#2037](https://github.com/casey/just/pull/2037) by [casey](https://github.com/casey))\n- Use --command-color when printing shebang recipe commands ([#1911](https://github.com/casey/just/pull/1911) by [avi-cenna](https://github.com/avi-cenna))\n- Document how to use watchexec to re-run recipes when files change ([#2036](https://github.com/casey/just/pull/2036) by [casey](https://github.com/casey))\n- Update VS Code extensions in readme ([#2034](https://github.com/casey/just/pull/2034) by [casey](https://github.com/casey))\n- Add rust:just repology package table to readme ([#2032](https://github.com/casey/just/pull/2032) by [casey](https://github.com/casey))\n\n[1.26.0](https://github.com/casey/just/releases/tag/1.26.0) - 2024-05-13\n------------------------------------------------------------------------\n\n### Added\n- Add --no-aliases to hide aliases in --list ([#1961](https://github.com/casey/just/pull/1961) by [WJehee](https://github.com/WJehee))\n- Add -E as alias for --dotenv-path ([#1910](https://github.com/casey/just/pull/1910) by [amarao](https://github.com/amarao))\n\n### Misc\n- Update softprops/action-gh-release ([#2029](https://github.com/casey/just/pull/2029) by [app/dependabot](https://github.com/app/dependabot))\n- Update dependencies ([#1999](https://github.com/casey/just/pull/1999) by [neunenak](https://github.com/neunenak))\n- Bump peaceiris/actions-gh-pages to version 4 ([#2005](https://github.com/casey/just/pull/2005) by [app/dependabot](https://github.com/app/dependabot))\n- Clarify that janus operates on public justfiles only ([#2021](https://github.com/casey/just/pull/2021) by [casey](https://github.com/casey))\n- Fix Error::TmpdirIo error message ([#1987](https://github.com/casey/just/pull/1987) by [casey](https://github.com/casey))\n- Update softprops/action-gh-release ([#1973](https://github.com/casey/just/pull/1973) by [app/dependabot](https://github.com/app/dependabot))\n- Rename `delete` example recipe to `delete-all` ([#1966](https://github.com/casey/just/pull/1966) by [aarmn](https://github.com/aarmn))\n- Update softprops/action-gh-release ([#1954](https://github.com/casey/just/pull/1954) by [app/dependabot](https://github.com/app/dependabot))\n- Fix function name typo ([#1953](https://github.com/casey/just/pull/1953) by [racerole](https://github.com/racerole))\n\n[1.25.2](https://github.com/casey/just/releases/tag/1.25.2) - 2024-03-10\n------------------------------------------------------------------------\n\n- Unpin ctrlc ([#1951](https://github.com/casey/just/pull/1951) by [casey](https://github.com/casey))\n\n[1.25.1](https://github.com/casey/just/releases/tag/1.25.1) - 2024-03-09\n------------------------------------------------------------------------\n\n### Misc\n- Pin ctrlc to version 3.1.1 ([#1945](https://github.com/casey/just/pull/1945) by [casey](https://github.com/casey))\n- Fix AArch64 release build error ([#1942](https://github.com/casey/just/pull/1942) by [casey](https://github.com/casey))\n\n[1.25.0](https://github.com/casey/just/releases/tag/1.25.0) - 2024-03-07\n------------------------------------------------------------------------\n\n### Added\n- Add `blake3` and `blake3_file` functions ([#1860](https://github.com/casey/just/pull/1860) by [tgross35](https://github.com/tgross35))\n\n### Misc\n- Fix readme typo ([#1936](https://github.com/casey/just/pull/1936) by [Justintime50](https://github.com/Justintime50))\n- Use unwrap_or_default ([#1928](https://github.com/casey/just/pull/1928) by [casey](https://github.com/casey))\n- Set codegen-units to 1 reduce release binary size ([#1920](https://github.com/casey/just/pull/1920) by [amarao](https://github.com/amarao))\n- Document openSUSE package ([#1918](https://github.com/casey/just/pull/1918) by [sfalken](https://github.com/sfalken))\n- Fix install.sh shellcheck warnings ([#1912](https://github.com/casey/just/pull/1912) by [tgross35](https://github.com/tgross35))\n\n[1.24.0](https://github.com/casey/just/releases/tag/1.24.0) - 2024-02-11\n------------------------------------------------------------------------\n\n### Added\n- Support recipe paths containing `::` in Bash completion script ([#1863](https://github.com/casey/just/pull/1863) by [crdx](https://github.com/crdx))\n- Add function to canonicalize paths ([#1859](https://github.com/casey/just/pull/1859) by [casey](https://github.com/casey))\n\n### Misc\n- Document installing just on Github Actions in readme ([#1867](https://github.com/casey/just/pull/1867) by [cclauss](https://github.com/cclauss))\n- Use unlikely-to-be-set variable name in env tests ([#1882](https://github.com/casey/just/pull/1882) by [casey](https://github.com/casey))\n- Skip write_error test if running as root ([#1881](https://github.com/casey/just/pull/1881) by [casey](https://github.com/casey))\n- Convert run_shebang into integration test ([#1880](https://github.com/casey/just/pull/1880) by [casey](https://github.com/casey))\n- Install mdbook with cargo in CI workflow ([#1877](https://github.com/casey/just/pull/1877) by [casey](https://github.com/casey))\n- Remove deprecated actions-rs/toolchain ([#1874](https://github.com/casey/just/pull/1874) by [cclauss](https://github.com/cclauss))\n- Fix Gentoo package link ([#1875](https://github.com/casey/just/pull/1875) by [vozbu](https://github.com/vozbu))\n- Fix typos found by codespell ([#1872](https://github.com/casey/just/pull/1872) by [cclauss](https://github.com/cclauss))\n- Replace deprecated set-output command in Github Actions workflows ([#1869](https://github.com/casey/just/pull/1869) by [cclauss](https://github.com/cclauss))\n- Update `actions/checkout` and `softprops/action-gh-release` ([#1871](https://github.com/casey/just/pull/1871) by [app/dependabot](https://github.com/app/dependabot))\n- Keep GitHub Actions up to date with Dependabot ([#1868](https://github.com/casey/just/pull/1868) by [cclauss](https://github.com/cclauss))\n- Add contrib directory ([#1870](https://github.com/casey/just/pull/1870) by [casey](https://github.com/casey))\n- Fix install script ([#1844](https://github.com/casey/just/pull/1844) by [casey](https://github.com/casey))\n\n[1.23.0](https://github.com/casey/just/releases/tag/1.23.0) - 2024-01-12\n------------------------------------------------------------------------\n\n### Added\n- Allow setting custom confirm prompt ([#1834](https://github.com/casey/just/pull/1834) by [CramBL](https://github.com/CramBL))\n- Add `set quiet` and `[no-quiet]` ([#1704](https://github.com/casey/just/pull/1704) by [dharrigan](https://github.com/dharrigan))\n- Add `just_pid` function ([#1833](https://github.com/casey/just/pull/1833) by [Swordelf2](https://github.com/Swordelf2))\n- Add functions to return XDG base directories ([#1822](https://github.com/casey/just/pull/1822) by [tgross35](https://github.com/tgross35))\n- Add `--no-deps` to skip running recipe dependencies ([#1819](https://github.com/casey/just/pull/1819) by [ngharrington](https://github.com/ngharrington))\n\n### Fixed\n- Run imports in working directory of importer ([#1817](https://github.com/casey/just/pull/1817) by [casey](https://github.com/casey))\n\n### Misc\n- Include completion scripts in releases ([#1837](https://github.com/casey/just/pull/1837) by [casey](https://github.com/casey))\n- Tweak readme table formatting ([#1836](https://github.com/casey/just/pull/1836) by [casey](https://github.com/casey))\n- Don't abbreviate just in README ([#1831](https://github.com/casey/just/pull/1831) by [thled](https://github.com/thled))\n- Ignore [private] recipes in just --list ([#1816](https://github.com/casey/just/pull/1816) by [crdx](https://github.com/crdx))\n- Add a dash to tempdir prefix ([#1828](https://github.com/casey/just/pull/1828) by [casey](https://github.com/casey))\n\n[1.22.1](https://github.com/casey/just/releases/tag/1.22.1) - 2024-01-08\n------------------------------------------------------------------------\n\n### Fixed\n- Don't conflate recipes with the same name in different modules ([#1825](https://github.com/casey/just/pull/1825) by [casey](https://github.com/casey))\n\n### Misc\n- Clarify that UUID is version 4 ([#1821](https://github.com/casey/just/pull/1821) by [tgross35](https://github.com/tgross35))\n- Make sigil stripping from recipe lines less incomprehensible ([#1812](https://github.com/casey/just/pull/1812) by [casey](https://github.com/casey))\n- Refactor invalid path argument check ([#1811](https://github.com/casey/just/pull/1811) by [casey](https://github.com/casey))\n\n[1.22.0](https://github.com/casey/just/releases/tag/1.22.0) - 2023-12-31\n------------------------------------------------------------------------\n\n### Added\n- Recipes can be invoked with path syntax ([#1809](https://github.com/casey/just/pull/1809) by [casey](https://github.com/casey))\n- Add `--format` and `--initialize` as aliases for `--fmt` and `--init` ([#1802](https://github.com/casey/just/pull/1802) by [casey](https://github.com/casey))\n\n### Misc\n- Move table of contents pointer to right ([#1806](https://github.com/casey/just/pull/1806) by [casey](https://github.com/casey))\n\n[1.21.0](https://github.com/casey/just/releases/tag/1.21.0) - 2023-12-29\n------------------------------------------------------------------------\n\n### Added\n- Optional modules and imports ([#1797](https://github.com/casey/just/pull/1797) by [casey](https://github.com/casey))\n- Print submodule recipes in --summary ([#1794](https://github.com/casey/just/pull/1794) by [casey](https://github.com/casey))\n\n### Misc\n- Use box-drawing characters in error messages ([#1798](https://github.com/casey/just/pull/1798) by [casey](https://github.com/casey))\n- Use Self ([#1795](https://github.com/casey/just/pull/1795) by [casey](https://github.com/casey))\n\n[1.20.0](https://github.com/casey/just/releases/tag/1.20.0) - 2023-12-28\n------------------------------------------------------------------------\n\n### Added\n- Allow mod statements with path to source file ([#1786](https://github.com/casey/just/pull/1786) by [casey](https://github.com/casey))\n\n### Changed\n- Expand tilde in import and module paths ([#1792](https://github.com/casey/just/pull/1792) by [casey](https://github.com/casey))\n- Override imported recipes ([#1790](https://github.com/casey/just/pull/1790) by [casey](https://github.com/casey))\n- Run recipes with working directory set to submodule directory ([#1788](https://github.com/casey/just/pull/1788) by [casey](https://github.com/casey))\n\n### Misc\n- Document import override behavior ([#1791](https://github.com/casey/just/pull/1791) by [casey](https://github.com/casey))\n- Document submodule working directory ([#1789](https://github.com/casey/just/pull/1789) by [casey](https://github.com/casey))\n\n[1.19.0](https://github.com/casey/just/releases/tag/1.19.0) - 2023-12-27\n------------------------------------------------------------------------\n\n### Added\n- Add modules ([#1782](https://github.com/casey/just/pull/1782) by [casey](https://github.com/casey))\n\n[1.18.1](https://github.com/casey/just/releases/tag/1.18.1) - 2023-12-24\n------------------------------------------------------------------------\n\n### Added\n- Display a descriptive error for `!include` directives ([#1779](https://github.com/casey/just/pull/1779) by [casey](https://github.com/casey))\n\n[1.18.0](https://github.com/casey/just/releases/tag/1.18.0) - 2023-12-24\n------------------------------------------------------------------------\n\n### Added\n- Stabilize `!include path` as `import 'path'` ([#1771](https://github.com/casey/just/pull/1771) by [casey](https://github.com/casey))\n\n### Misc\n- Tweak readme ([#1775](https://github.com/casey/just/pull/1775) by [casey](https://github.com/casey))\n\n[1.17.0](https://github.com/casey/just/releases/tag/1.17.0) - 2023-12-20\n------------------------------------------------------------------------\n\n### Added\n- Add `[confirm]` attribute ([#1723](https://github.com/casey/just/pull/1723) by [Hwatwasthat](https://github.com/Hwatwasthat))\n\n### Changed\n- Don't default to included recipes ([#1740](https://github.com/casey/just/pull/1740) by [casey](https://github.com/casey))\n\n### Fixed\n- Pass justfile path to default chooser ([#1759](https://github.com/casey/just/pull/1759) by [Qeole](https://github.com/Qeole))\n- Pass `--unstable` and `--color always` to default chooser ([#1758](https://github.com/casey/just/pull/1758) by [Qeole](https://github.com/Qeole))\n\n### Misc\n- Update Gentoo package repository ([#1757](https://github.com/casey/just/pull/1757) by [paul-jewell](https://github.com/paul-jewell))\n- Fix readme header level ([#1752](https://github.com/casey/just/pull/1752) by [laniakea64](https://github.com/laniakea64))\n- Document line continuations ([#1751](https://github.com/casey/just/pull/1751) by [laniakea64](https://github.com/laniakea64))\n- List included recipes in load order ([#1745](https://github.com/casey/just/pull/1745) by [casey](https://github.com/casey))\n- Fix build badge in zh readme ([#1743](https://github.com/casey/just/pull/1743) by [chenrui333](https://github.com/chenrui333))\n- Rename Justfile::first → Justfile::default ([#1741](https://github.com/casey/just/pull/1741) by [casey](https://github.com/casey))\n- Add file paths to error messages ([#1737](https://github.com/casey/just/pull/1737) by [casey](https://github.com/casey))\n- Move !include processing into compiler ([#1618](https://github.com/casey/just/pull/1618) by [neunenak](https://github.com/neunenak))\n- Update Arch Linux package URL in readme ([#1733](https://github.com/casey/just/pull/1733) by [felixonmars](https://github.com/felixonmars))\n- Clarify that aliases can only be used on the command line ([#1726](https://github.com/casey/just/pull/1726) by [laniakea64](https://github.com/laniakea64))\n- Remove VALID_ALIAS_ATTRIBUTES array ([#1731](https://github.com/casey/just/pull/1731) by [casey](https://github.com/casey))\n- Fix justfile search link in Chinese docs ([#1730](https://github.com/casey/just/pull/1730) by [oluceps](https://github.com/oluceps))\n- Add example of Windows shebang handling ([#1709](https://github.com/casey/just/pull/1709) by [pfmoore](https://github.com/pfmoore))\n- Fix CI ([#1728](https://github.com/casey/just/pull/1728) by [casey](https://github.com/casey))\n\n[1.16.0](https://github.com/casey/just/releases/tag/1.16.0) - 2023-11-08\n------------------------------------------------------------------------\n\n### Added\n- Add ARMv6 release target ([#1715](https://github.com/casey/just/pull/1715) by [ragazenta](https://github.com/ragazenta))\n- Add `semver_matches` function ([#1713](https://github.com/casey/just/pull/1713) by [t3hmrman](https://github.com/t3hmrman))\n- Add `dotenv-filename` and `dotenv-path` settings ([#1692](https://github.com/casey/just/pull/1692) by [ltfourrier](https://github.com/ltfourrier))\n- Allow setting echoed recipe line color ([#1670](https://github.com/casey/just/pull/1670) by [avi-cenna](https://github.com/avi-cenna))\n\n### Fixed\n- Fix Fish completion script ([#1710](https://github.com/casey/just/pull/1710) by [l4zygreed](https://github.com/l4zygreed))\n\n### Misc\n- Fix readme typo ([#1717](https://github.com/casey/just/pull/1717) by [barraponto](https://github.com/barraponto))\n- Clean up error display ([#1699](https://github.com/casey/just/pull/1699) by [nyurik](https://github.com/nyurik))\n- Misc fixes ([#1700](https://github.com/casey/just/pull/1700) by [nyurik](https://github.com/nyurik))\n- Fix readme build badge ([#1697](https://github.com/casey/just/pull/1697) by [casey](https://github.com/casey))\n- Fix set tempdir grammar ([#1695](https://github.com/casey/just/pull/1695) by [casey](https://github.com/casey))\n- Add version to attributes ([#1694](https://github.com/casey/just/pull/1694) by [JoeyTeng](https://github.com/JoeyTeng))\n- Update README.md ([#1691](https://github.com/casey/just/pull/1691) by [laniakea64](https://github.com/laniakea64))\n\n\n[1.15.0](https://github.com/casey/just/releases/tag/1.15.0) - 2023-10-09\n------------------------------------------------------------------------\n\n### Added\n- Add Nushell completion script ([#1571](https://github.com/casey/just/pull/1571) by [presidento](https://github.com/presidento))\n- Allow unstable features to be enabled with environment variable ([#1588](https://github.com/casey/just/pull/1588) by [neunenak](https://github.com/neunenak))\n- Add num_cpus() function ([#1568](https://github.com/casey/just/pull/1568) by [schultetwin1](https://github.com/schultetwin1))\n- Allow escaping newlines ([#1551](https://github.com/casey/just/pull/1551) by [ids1024](https://github.com/ids1024))\n- Stabilize JSON dump format ([#1633](https://github.com/casey/just/pull/1633) by [casey](https://github.com/casey))\n- Add env() function ([#1613](https://github.com/casey/just/pull/1613) by [kykyi](https://github.com/kykyi))\n\n### Changed\n- Allow selecting multiple recipes with default chooser ([#1547](https://github.com/casey/just/pull/1547) by [fzdwx](https://github.com/fzdwx))\n\n### Misc\n- Don't recommend `vim-polyglot` in readme ([#1644](https://github.com/casey/just/pull/1644) by [laniakea64](https://github.com/laniakea64))\n- Note Micro support in readme ([#1316](https://github.com/casey/just/pull/1316) by [tomodachi94](https://github.com/tomodachi94))\n- Update Indentation Documentation ([#1600](https://github.com/casey/just/pull/1600) by [GinoMan](https://github.com/GinoMan))\n- Fix triple-quoted string example in readme ([#1620](https://github.com/casey/just/pull/1620) by [avi-cenna](https://github.com/avi-cenna))\n- README fix: the -d in `mktemp -d` is required to created folders. ([#1688](https://github.com/casey/just/pull/1688) by [gl-yziquel](https://github.com/gl-yziquel))\n- Placate clippy ([#1689](https://github.com/casey/just/pull/1689) by [casey](https://github.com/casey))\n- Fix README typos ([#1660](https://github.com/casey/just/pull/1660) by [akuhnregnier](https://github.com/akuhnregnier))\n- Document Windows Package Manager install instructions ([#1656](https://github.com/casey/just/pull/1656) by [casey](https://github.com/casey))\n- Test unpaired escaped carriage return error ([#1650](https://github.com/casey/just/pull/1650) by [casey](https://github.com/casey))\n- Avoid grep aliases in bash completions ([#1622](https://github.com/casey/just/pull/1622) by [BojanStipic](https://github.com/BojanStipic))\n- Clarify [unix] attribute in readme ([#1619](https://github.com/casey/just/pull/1619) by [neunenak](https://github.com/neunenak))\n- Add descriptions to fish recipe completions ([#1578](https://github.com/casey/just/pull/1578) by [patricksjackson](https://github.com/patricksjackson))\n- Add better documentation for --dump and --fmt ([#1603](https://github.com/casey/just/pull/1603) by [neunenak](https://github.com/neunenak))\n- Cleanup ([#1566](https://github.com/casey/just/pull/1566) by [nyurik](https://github.com/nyurik))\n- Document Helix editor support in readme ([#1604](https://github.com/casey/just/pull/1604) by [kenden](https://github.com/kenden))\n\n[1.14.0](https://github.com/casey/just/releases/tag/1.14.0) - 2023-06-02\n------------------------------------------------------------------------\n\n### Changed\n- Use `just --show` in default chooser ([#1539](https://github.com/casey/just/pull/1539) by [fzdwx](https://github.com/fzdwx))\n\n### Misc\n- Fix justfile search link ([#1607](https://github.com/casey/just/pull/1607) by [jbaber](https://github.com/jbaber))\n- Ignore clippy::let_underscore_untyped ([#1609](https://github.com/casey/just/pull/1609) by [casey](https://github.com/casey))\n- Link to private recipes section in readme ([#1542](https://github.com/casey/just/pull/1542) by [quad](https://github.com/quad))\n- Update README to reflect new attribute syntax ([#1538](https://github.com/casey/just/pull/1538) by [neunenak](https://github.com/neunenak))\n- Allow multiple attributes on one line ([#1537](https://github.com/casey/just/pull/1537) by [neunenak](https://github.com/neunenak))\n- Analyze and Compiler tweaks ([#1534](https://github.com/casey/just/pull/1534) by [neunenak](https://github.com/neunenak))\n- Downgrade to TLS 1.2 in install script ([#1536](https://github.com/casey/just/pull/1536) by [casey](https://github.com/casey))\n\n[1.13.0](https://github.com/casey/just/releases/tag/1.13.0) - 2023-01-24\n------------------------------------------------------------------------\n\n### Added\n- Add -n as a short flag for --for dry-run ([#1524](https://github.com/casey/just/pull/1524) by [maiha](https://github.com/maiha))\n- Add invocation_directory_native() ([#1507](https://github.com/casey/just/pull/1507) by [casey](https://github.com/casey))\n\n### Changed\n- Ignore additional search path arguments ([#1528](https://github.com/casey/just/pull/1528) by [neunenak](https://github.com/neunenak))\n- Only print fallback message when verbose ([#1510](https://github.com/casey/just/pull/1510) by [casey](https://github.com/casey))\n- Print format diff to stdout ([#1506](https://github.com/casey/just/pull/1506) by [casey](https://github.com/casey))\n\n### Fixed\n- Test passing dot as argument between justfiles ([#1530](https://github.com/casey/just/pull/1530) by [casey](https://github.com/casey))\n- Fix install script default directory ([#1525](https://github.com/casey/just/pull/1525) by [casey](https://github.com/casey))\n\n### Misc\n- Note that justfiles are order-insensitive ([#1529](https://github.com/casey/just/pull/1529) by [casey](https://github.com/casey))\n- Borrow Ast in Analyser ([#1527](https://github.com/casey/just/pull/1527) by [neunenak](https://github.com/neunenak))\n- Ignore chooser tests ([#1513](https://github.com/casey/just/pull/1513) by [casey](https://github.com/casey))\n- Put default setting values in backticks ([#1512](https://github.com/casey/just/pull/1512) by [s1ck](https://github.com/s1ck))\n- Use lowercase boolean literals in readme ([#1511](https://github.com/casey/just/pull/1511) by [s1ck](https://github.com/s1ck))\n- Document invocation_directory_native() ([#1508](https://github.com/casey/just/pull/1508) by [casey](https://github.com/casey))\n- Fix interrupt tests ([#1505](https://github.com/casey/just/pull/1505) by [casey](https://github.com/casey))\n\n[1.12.0](https://github.com/casey/just/releases/tag/1.12.0) - 2023-01-12\n------------------------------------------------------------------------\n\n### Added\n- Add `!include` directives ([#1470](https://github.com/casey/just/pull/1470) by [neunenak](https://github.com/neunenak))\n\n### Changed\n- Allow matching search path arguments ([#1475](https://github.com/casey/just/pull/1475) by [neunenak](https://github.com/neunenak))\n- Allow recipe parameters to shadow variables ([#1480](https://github.com/casey/just/pull/1480) by [casey](https://github.com/casey))\n\n### Misc\n- Remove --unstable from fallback example in readme ([#1502](https://github.com/casey/just/pull/1502) by [casey](https://github.com/casey))\n- Specify minimum rust version ([#1496](https://github.com/casey/just/pull/1496) by [benmoss](https://github.com/benmoss))\n- Note that install.sh may fail on GitHub actions ([#1499](https://github.com/casey/just/pull/1499) by [casey](https://github.com/casey))\n- Fix readme typo ([#1489](https://github.com/casey/just/pull/1489) by [auberisky](https://github.com/auberisky))\n- Update install script and readmes to use tls v1.3 ([#1481](https://github.com/casey/just/pull/1481) by [casey](https://github.com/casey))\n- Re-enable install.sh test on CI([#1478](https://github.com/casey/just/pull/1478) by [casey](https://github.com/casey))\n- Don't test install.sh on CI ([#1477](https://github.com/casey/just/pull/1477) by [casey](https://github.com/casey))\n- Update Chinese translation of readme ([#1476](https://github.com/casey/just/pull/1476) by [hustcer](https://github.com/hustcer))\n- Fix install.sh for Windows ([#1474](https://github.com/casey/just/pull/1474) by [bloodearnest](https://github.com/bloodearnest))\n\n[1.11.0](https://github.com/casey/just/releases/tag/1.11.0) - 2023-01-03\n------------------------------------------------------------------------\n\n### Added\n- Stabilize fallback ([#1471](https://github.com/casey/just/pull/1471) by [casey](https://github.com/casey))\n\n### Misc\n- Update Sublime syntax instructions ([#1455](https://github.com/casey/just/pull/1455) by [nk9](https://github.com/nk9))\n\n[1.10.0](https://github.com/casey/just/releases/tag/1.10.0) - 2023-01-01\n------------------------------------------------------------------------\n\n### Added\n- Allow private attribute on aliases ([#1434](https://github.com/casey/just/pull/1434) by [neunenak](https://github.com/neunenak))\n\n### Changed\n- Suppress --fmt --check diff if --quiet is passed ([#1457](https://github.com/casey/just/pull/1457) by [casey](https://github.com/casey))\n\n### Fixed\n- Format exported variadic parameters correctly ([#1451](https://github.com/casey/just/pull/1451) by [casey](https://github.com/casey))\n\n### Misc\n- Fix section title grammar ([#1466](https://github.com/casey/just/pull/1466) by [brettcannon](https://github.com/brettcannon))\n- Give pages job write permissions([#1464](https://github.com/casey/just/pull/1464) by [jsoref](https://github.com/jsoref))\n- Fix spelling ([#1463](https://github.com/casey/just/pull/1463) by [jsoref](https://github.com/jsoref))\n- Merge imports ([#1462](https://github.com/casey/just/pull/1462) by [casey](https://github.com/casey))\n- Add instructions for taiki-e/install-action ([#1459](https://github.com/casey/just/pull/1459) by [azzamsa](https://github.com/azzamsa))\n- Differentiate between shell and nushell example ([#1427](https://github.com/casey/just/pull/1427) by [Dialga](https://github.com/Dialga))\n- Link regex docs in readme ([#1454](https://github.com/casey/just/pull/1454) by [casey](https://github.com/casey))\n- Linkify changelog PRs and usernames ([#1440](https://github.com/casey/just/pull/1440) by [nk9](https://github.com/nk9))\n- Eliminate lazy_static ([#1442](https://github.com/casey/just/pull/1442) by [camsteffen](https://github.com/camsteffen))\n- Add attributes to sublime syntax file ([#1452](https://github.com/casey/just/pull/1452) by [crdx](https://github.com/crdx))\n- Fix homepage style ([#1453](https://github.com/casey/just/pull/1453) by [casey](https://github.com/casey))\n- Linkify homepage letters ([#1448](https://github.com/casey/just/pull/1448) by [nk9](https://github.com/nk9))\n- Use `just` in readme codeblocks ([#1447](https://github.com/casey/just/pull/1447) by [nicochatzi](https://github.com/nicochatzi))\n- Update MSRV in readme ([#1446](https://github.com/casey/just/pull/1446) by [casey](https://github.com/casey))\n- Merge CI workflows ([#1444](https://github.com/casey/just/pull/1444) by [casey](https://github.com/casey))\n- Use dotenvy instead of dotenv ([#1443](https://github.com/casey/just/pull/1443) by [mike-burns](https://github.com/mike-burns))\n- Update Chinese translation of readme ([#1428](https://github.com/casey/just/pull/1428) by [hustcer](https://github.com/hustcer))\n\n[1.9.0](https://github.com/casey/just/releases/tag/1.9.0) - 2022-11-25\n----------------------------------------------------------------------\n\n### Breaking Changes to Unstable Features\n- Change `fallback` setting default to false ([#1425](https://github.com/casey/just/pull/1425) by [casey](https://github.com/casey))\n\n### Added\n- Hide recipes with `[private]` attribute ([#1422](https://github.com/casey/just/pull/1422) by [casey](https://github.com/casey))\n- Add replace_regex function ([#1393](https://github.com/casey/just/pull/1393) by [miles170](https://github.com/miles170))\n- Add [no-cd] attribute ([#1400](https://github.com/casey/just/pull/1400) by [casey](https://github.com/casey))\n\n### Changed\n- Omit shebang lines on Windows ([#1417](https://github.com/casey/just/pull/1417) by [casey](https://github.com/casey))\n\n### Misc\n- Placate clippy ([#1423](https://github.com/casey/just/pull/1423) by [casey](https://github.com/casey))\n- Make include_shebang_line clearer ([#1418](https://github.com/casey/just/pull/1418) by [casey](https://github.com/casey))\n- Use more secure cURL options in install.sh ([#1416](https://github.com/casey/just/pull/1416) by [casey](https://github.com/casey))\n- Document how shebang recipes are executed ([#1412](https://github.com/casey/just/pull/1412) by [casey](https://github.com/casey))\n- Fix typo: regec → regex ([#1409](https://github.com/casey/just/pull/1409) by [casey](https://github.com/casey))\n- Use powershell.exe instead of pwsh.exe in readme ([#1394](https://github.com/casey/just/pull/1394) by [asdf8dfafjk](https://github.com/asdf8dfafjk))\n- Expand alternatives and prior art in readme ([#1401](https://github.com/casey/just/pull/1401) by [casey](https://github.com/casey))\n- Split up CI workflow ([#1399](https://github.com/casey/just/pull/1399) by [casey](https://github.com/casey))\n\n[1.8.0](https://github.com/casey/just/releases/tag/1.8.0) - 2022-11-02\n----------------------------------------------------------------------\n\n### Added\n- Add OS Configuration Attributes ([#1387](https://github.com/casey/just/pull/1387) by [casey](https://github.com/casey))\n\n### Misc\n- Link to sclu1034/vscode-just in readme ([#1396](https://github.com/casey/just/pull/1396) by [casey](https://github.com/casey))\n\n[1.7.0](https://github.com/casey/just/releases/tag/1.7.0) - 2022-10-26\n----------------------------------------------------------------------\n\n### Breaking Changes to Unstable Features\n- Make `fallback` setting default to true ([#1384](https://github.com/casey/just/pull/1384) by [casey](https://github.com/casey))\n\n### Added\n- Add more case-conversion functions ([#1383](https://github.com/casey/just/pull/1383) by [gVirtu](https://github.com/gVirtu))\n- Add `tempdir` setting ([#1369](https://github.com/casey/just/pull/1369) by [dmatos2012](https://github.com/dmatos2012))\n- Add [no-exit-message] recipe annotation ([#1354](https://github.com/casey/just/pull/1354) by [gokhanettin](https://github.com/gokhanettin))\n- Add `capitalize(s)` function ([#1375](https://github.com/casey/just/pull/1375) by [femnad](https://github.com/femnad))\n\n### Misc\n- Credit contributors in changelog ([#1385](https://github.com/casey/just/pull/1385) by [casey](https://github.com/casey))\n- Update asdf just plugin repository ([#1380](https://github.com/casey/just/pull/1380) by [kachick](https://github.com/kachick))\n- Prepend commit messages with `- ` in changelog ([#1379](https://github.com/casey/just/pull/1379) by [casey](https://github.com/casey))\n- Fail publish if `<sup>master</sup>` is found in README.md ([#1378](https://github.com/casey/just/pull/1378) by [casey](https://github.com/casey))\n- Use for loop in capitalize implementation ([#1377](https://github.com/casey/just/pull/1377) by [casey](https://github.com/casey))\n\n[1.6.0](https://github.com/casey/just/releases/tag/1.6.0) - 2022-10-19\n----------------------------------------------------------------------\n\n### Breaking Changes to Unstable Features\n- Require `set fallback := true` to enable recipe fallback ([#1368](https://github.com/casey/just/pull/1368) by [casey](https://github.com/casey))\n\n### Changed\n- Allow fallback with search directory ([#1348](https://github.com/casey/just/pull/1348) by [casey](https://github.com/casey))\n\n### Added\n- Don't evaluate comments ([#1358](https://github.com/casey/just/pull/1358) by [casey](https://github.com/casey))\n- Add skip-comments setting ([#1333](https://github.com/casey/just/pull/1333) by [neunenak](https://github.com/neunenak))\n- Allow bash completion to complete tasks in other directories ([#1303](https://github.com/casey/just/pull/1303) by [jpbochi](https://github.com/jpbochi))\n\n### Misc\n- Restore www/CNAME ([#1364](https://github.com/casey/just/pull/1364) by [casey](https://github.com/casey))\n- Improve book config ([#1363](https://github.com/casey/just/pull/1363) by [casey](https://github.com/casey))\n- Add kitchen sink justfile to test syntax highlighting ([#1362](https://github.com/casey/just/pull/1362) by [nk9](https://github.com/nk9))\n- Note version in which absolute path construction was added ([#1361](https://github.com/casey/just/pull/1361) by [casey](https://github.com/casey))\n- Inline setup and cleanup functions in completion script test ([#1352](https://github.com/casey/just/pull/1352) by [casey](https://github.com/casey))\n\n[1.5.0](https://github.com/casey/just/releases/tag/1.5.0) - 2022-9-11\n---------------------------------------------------------------------\n\n### Changed\n- Allow constructing absolute paths with `/` operator ([#1320](https://github.com/casey/just/pull/1320) by [erikkrieg](https://github.com/erikkrieg))\n\n### Misc\n- Allow fewer lints ([#1340](https://github.com/casey/just/pull/1340) by [casey](https://github.com/casey))\n- Fix issues reported by nightly clippy ([#1336](https://github.com/casey/just/pull/1336) by [neunenak](https://github.com/neunenak))\n- Refactor run.rs ([#1335](https://github.com/casey/just/pull/1335) by [neunenak](https://github.com/neunenak))\n- Allow comments on same line as settings ([#1339](https://github.com/casey/just/pull/1339) by [casey](https://github.com/casey))\n- Fix justfile env shebang on Linux ([#1330](https://github.com/casey/just/pull/1330) by [casey](https://github.com/casey))\n- Update Chinese translation of README.md ([#1325](https://github.com/casey/just/pull/1325) by [hustcer](https://github.com/hustcer))\n- Add additional settings to grammar ([#1321](https://github.com/casey/just/pull/1321) by [psibi](https://github.com/psibi))\n- Add an example of using a variable in a recipe parameter ([#1311](https://github.com/casey/just/pull/1311) by [papertigers](https://github.com/papertigers))\n\n[1.4.0](https://github.com/casey/just/releases/tag/1.4.0) - 2022-8-08\n---------------------------------------------------------------------\n\n### Fixed\n- Fix shell setting precedence ([#1306](https://github.com/casey/just/pull/1306) by [casey](https://github.com/casey))\n\n### Misc\n- Don't hardcode homebrew prefix ([#1295](https://github.com/casey/just/pull/1295) by [casey](https://github.com/casey))\n- Exclude files from cargo package ([#1283](https://github.com/casey/just/pull/1283) by [casey](https://github.com/casey))\n- Add usage note to default list recipe ([#1296](https://github.com/casey/just/pull/1296) by [jpbochi](https://github.com/jpbochi))\n- Add MPR/Prebuilt-MPR installation instructions to README.md ([#1280](https://github.com/casey/just/pull/1280) by [hwittenborn](https://github.com/hwittenborn))\n- Add make and makesure to readme ([#1299](https://github.com/casey/just/pull/1299) by [casey](https://github.com/casey))\n- Document how to configure zsh completions on MacOS ([#1285](https://github.com/casey/just/pull/1285) by [nk9](https://github.com/nk9))\n- Convert package table to HTML ([#1291](https://github.com/casey/just/pull/1291) by [casey](https://github.com/casey))\n\n[1.3.0](https://github.com/casey/just/releases/tag/1.3.0) - 2022-7-25\n---------------------------------------------------------------------\n\n### Added\n- Add `/` operator ([#1237](https://github.com/casey/just/pull/1237) by [casey](https://github.com/casey))\n\n### Fixed\n- Fix multibyte codepoint crash ([#1243](https://github.com/casey/just/pull/1243) by [casey](https://github.com/casey))\n\n### Misc\n- Update just-install reference on README.md ([#1275](https://github.com/casey/just/pull/1275) by [0xradical](https://github.com/0xradical))\n- Split Recipe::run into Recipe::{run_shebang,run_linewise} ([#1270](https://github.com/casey/just/pull/1270) by [casey](https://github.com/casey))\n- Add asdf package to readme([#1264](https://github.com/casey/just/pull/1264) by [jaacko-torus](https://github.com/jaacko-torus))\n- Add mdbook deps for build-book recipe ([#1259](https://github.com/casey/just/pull/1259) by [TopherIsSwell](https://github.com/TopherIsSwell))\n- Fix typo: argumant -> argument ([#1257](https://github.com/casey/just/pull/1257) by [kianmeng](https://github.com/kianmeng))\n- Improve error message if `if` is missing the `else` ([#1252](https://github.com/casey/just/pull/1252) by [nk9](https://github.com/nk9))\n- Explain how to pass arguments of a command to a dependency ([#1254](https://github.com/casey/just/pull/1254) by [heavelock](https://github.com/heavelock))\n- Update Chinese translation of README.md ([#1253](https://github.com/casey/just/pull/1253) by [hustcer](https://github.com/hustcer))\n- Improvements to Sublime syntax file ([#1250](https://github.com/casey/just/pull/1250) by [nk9](https://github.com/nk9))\n- Prevent unbounded recursion when parsing expressions ([#1248](https://github.com/casey/just/pull/1248) by [evanrichter](https://github.com/evanrichter))\n- Publish to snap store ([#1245](https://github.com/casey/just/pull/1245) by [casey](https://github.com/casey))\n- Restore fuzz test harness ([#1246](https://github.com/casey/just/pull/1246) by [evanrichter](https://github.com/evanrichter))\n- Add just-install to README file ([#1241](https://github.com/casey/just/pull/1241) by [brombal](https://github.com/brombal))\n- Fix dead readme link ([#1240](https://github.com/casey/just/pull/1240) by [wdroz](https://github.com/wdroz))\n- Do `use super::*;` instead of `use crate::common::*;` ([#1239](https://github.com/casey/just/pull/1239) by [casey](https://github.com/casey))\n- Fix readme punctuation ([#1235](https://github.com/casey/just/pull/1235) by [casey](https://github.com/casey))\n- Add argument splitting section to readme ([#1230](https://github.com/casey/just/pull/1230) by [casey](https://github.com/casey))\n- Add notes about environment variables to readme ([#1229](https://github.com/casey/just/pull/1229) by [casey](https://github.com/casey))\n- Fix book links ([#1227](https://github.com/casey/just/pull/1227) by [casey](https://github.com/casey))\n- Add nushell README.md ([#1224](https://github.com/casey/just/pull/1224) by [hustcer](https://github.com/hustcer))\n- Use absolute links in readme ([#1223](https://github.com/casey/just/pull/1223) by [casey](https://github.com/casey))\n- Copy changelog into manual ([#1222](https://github.com/casey/just/pull/1222) by [casey](https://github.com/casey))\n- Translate Chinese manual introduction and title ([#1220](https://github.com/casey/just/pull/1220) by [hustcer](https://github.com/hustcer))\n- Build Chinese language user manual ([#1219](https://github.com/casey/just/pull/1219) by [casey](https://github.com/casey))\n- Update Chinese translation of README.md ([#1218](https://github.com/casey/just/pull/1218) by [hustcer](https://github.com/hustcer))\n- Translate all of README.md into Chinese ([#1217](https://github.com/casey/just/pull/1217) by [hustcer](https://github.com/hustcer))\n- Translate all of features in README into Chinese ([#1215](https://github.com/casey/just/pull/1215) by [hustcer](https://github.com/hustcer))\n- Make link to examples directory absolute ([#1213](https://github.com/casey/just/pull/1213) by [casey](https://github.com/casey))\n- Translate part of features in README into Chinese ([#1211](https://github.com/casey/just/pull/1211) by [hustcer](https://github.com/hustcer))\n- Add JetBrains IDE plugin to readme ([#1209](https://github.com/casey/just/pull/1209) by [linux-china](https://github.com/linux-china))\n- Translate features chapter of readme to Chinese ([#1208](https://github.com/casey/just/pull/1208) by [hustcer](https://github.com/hustcer))\n\n[1.2.0](https://github.com/casey/just/releases/tag/1.2.0) - 2022-5-31\n---------------------------------------------------------------------\n\n### Added\n- Add `windows-shell` setting ([#1198](https://github.com/casey/just/pull/1198) by [casey](https://github.com/casey))\n- SHA-256 and UUID functions ([#1170](https://github.com/casey/just/pull/1170) by [mbodmer](https://github.com/mbodmer))\n\n### Misc\n- Translate editor support and quick start to Chinese ([#1206](https://github.com/casey/just/pull/1206) by [hustcer](https://github.com/hustcer))\n- Translate first section of readme into Chinese ([#1205](https://github.com/casey/just/pull/1205) by [hustcer](https://github.com/hustcer))\n- Fix a bunch of typos ([#1204](https://github.com/casey/just/pull/1204) by [casey](https://github.com/casey))\n- Remove cargo-limit usage from justfile ([#1199](https://github.com/casey/just/pull/1199) by [casey](https://github.com/casey))\n- Add nix package manager install instructions ([#1194](https://github.com/casey/just/pull/1194) by [risingBirdSong](https://github.com/risingBirdSong))\n- Fix broken link in readme ([#1183](https://github.com/casey/just/pull/1183) by [Vlad-Shcherbina](https://github.com/Vlad-Shcherbina))\n- Add screenshot to manual ([#1181](https://github.com/casey/just/pull/1181) by [casey](https://github.com/casey))\n- Style homepage ([#1180](https://github.com/casey/just/pull/1180) by [casey](https://github.com/casey))\n- Center readme ([#1178](https://github.com/casey/just/pull/1178) by [casey](https://github.com/casey))\n- Style and add links to homepage ([#1177](https://github.com/casey/just/pull/1177) by [casey](https://github.com/casey))\n- Fix readme badge links ([#1176](https://github.com/casey/just/pull/1176) by [casey](https://github.com/casey))\n- Generate book from readme ([#1155](https://github.com/casey/just/pull/1155) by [casey](https://github.com/casey))\n\n[1.1.3](https://github.com/casey/just/releases/tag/1.1.3) - 2022-5-3\n--------------------------------------------------------------------\n\n### Fixed\n- Skip duplicate recipe arguments ([#1174](https://github.com/casey/just/pull/1174) by [casey](https://github.com/casey))\n\n### Misc\n- Fix install script ([#1172](https://github.com/casey/just/pull/1172) by [casey](https://github.com/casey))\n- Document that `invocation_directory()` returns an absolute path ([#1162](https://github.com/casey/just/pull/1162) by [casey](https://github.com/casey))\n- Fix absolute_path documentation ([#1160](https://github.com/casey/just/pull/1160) by [casey](https://github.com/casey))\n- Add cross-platform justfile example ([#1152](https://github.com/casey/just/pull/1152) by [presidento](https://github.com/presidento))\n\n[1.1.2](https://github.com/casey/just/releases/tag/1.1.2) - 2022-3-30\n---------------------------------------------------------------------\n\n### Misc\n- Document indentation rules ([#1142](https://github.com/casey/just/pull/1142) by [casey](https://github.com/casey))\n- Remove stale link from readme ([#1141](https://github.com/casey/just/pull/1141) by [casey](https://github.com/casey))\n\n### Unstable\n- Search for missing recipes in parent directory justfiles ([#1149](https://github.com/casey/just/pull/1149) by [casey](https://github.com/casey))\n\n[1.1.1](https://github.com/casey/just/releases/tag/1.1.1) - 2022-3-22\n---------------------------------------------------------------------\n\n### Misc\n- Build MacOS ARM release binaries ([#1138](https://github.com/casey/just/pull/1138) by [casey](https://github.com/casey))\n- Upgrade Windows Actions runners to windows-latest ([#1137](https://github.com/casey/just/pull/1137) by [casey](https://github.com/casey))\n\n[1.1.0](https://github.com/casey/just/releases/tag/1.1.0) - 2022-3-10\n---------------------------------------------------------------------\n\n### Added\n- Add `error()` function ([#1118](https://github.com/casey/just/pull/1118) by [chamons](https://github.com/chamons))\n- Add `absolute_path` function ([#1121](https://github.com/casey/just/pull/1121) by [Laura7089](https://github.com/Laura7089))\n\n[1.0.1](https://github.com/casey/just/releases/tag/1.0.1) - 2022-2-28\n---------------------------------------------------------------------\n\n### Fixed\n- Make path_exists() relative to current directory ([#1122](https://github.com/casey/just/pull/1122) by [casey](https://github.com/casey))\n\n### Misc\n- Detail environment variable usage in readme ([#1086](https://github.com/casey/just/pull/1086) by [kenden](https://github.com/kenden))\n- Format --init justfile ([#1116](https://github.com/casey/just/pull/1116) by [TheLocehiliosan](https://github.com/TheLocehiliosan))\n- Add hint for Node.js script compatibility ([#1113](https://github.com/casey/just/pull/1113) by [casey](https://github.com/casey))\n\n[1.0.0](https://github.com/casey/just/releases/tag/1.0.0) - 2022-2-22\n---------------------------------------------------------------------\n\n### Added\n- Add path_exists() function ([#1106](https://github.com/casey/just/pull/1106) by [heavelock](https://github.com/heavelock))\n\n### Misc\n- Note that `pipefail` isn't normally set ([#1108](https://github.com/casey/just/pull/1108) by [casey](https://github.com/casey))\n\n[0.11.2](https://github.com/casey/just/releases/tag/0.11.2) - 2022-2-15\n-----------------------------------------------------------------------\n\n### Misc\n- Fix dotenv-load documentation ([#1104](https://github.com/casey/just/pull/1104) by [casey](https://github.com/casey))\n- Fixup broken release package script ([#1100](https://github.com/casey/just/pull/1100) by [lutostag](https://github.com/lutostag))\n\n[0.11.1](https://github.com/casey/just/releases/tag/0.11.1) - 2022-2-14\n-----------------------------------------------------------------------\n\n### Added\n- Allow duplicate recipes ([#1095](https://github.com/casey/just/pull/1095) by [lutostag](https://github.com/lutostag))\n\n### Misc\n- Add arrow pointing to table of contents button ([#1096](https://github.com/casey/just/pull/1096) by [casey](https://github.com/casey))\n- Improve readme ([#1093](https://github.com/casey/just/pull/1093) by [halostatue](https://github.com/halostatue))\n- Remove asciidoc readme ([#1092](https://github.com/casey/just/pull/1092) by [casey](https://github.com/casey))\n- Convert README.adoc to markdown ([#1091](https://github.com/casey/just/pull/1091) by [casey](https://github.com/casey))\n- Add choco package to README ([#1090](https://github.com/casey/just/pull/1090) by [michidk](https://github.com/michidk))\n\n[0.11.0](https://github.com/casey/just/releases/tag/0.11.0) - 2022-2-3\n----------------------------------------------------------------------\n\n### Breaking\n- Change dotenv-load default to false ([#1082](https://github.com/casey/just/pull/1082) by [casey](https://github.com/casey))\n\n[0.10.7](https://github.com/casey/just/releases/tag/0.10.7) - 2022-1-30\n-----------------------------------------------------------------------\n\n### Misc\n- Don't run tests in release workflow ([#1080](https://github.com/casey/just/pull/1080) by [casey](https://github.com/casey))\n- Fix windows chooser invocation error message test ([#1079](https://github.com/casey/just/pull/1079) by [casey](https://github.com/casey))\n- Remove call to sed in justfile ([#1078](https://github.com/casey/just/pull/1078) by [casey](https://github.com/casey))\n\n[0.10.6](https://github.com/casey/just/releases/tag/0.10.6) - 2022-1-29\n-----------------------------------------------------------------------\n\n### Added\n- Add windows-powershell setting ([#1057](https://github.com/casey/just/pull/1057) by [michidk](https://github.com/michidk))\n\n### Changed\n- Allow using `-` and `@` in any order ([#1063](https://github.com/casey/just/pull/1063) by [casey](https://github.com/casey))\n\n### Misc\n- Use `Context` suffix for snafu error contexts ([#1068](https://github.com/casey/just/pull/1068) by [casey](https://github.com/casey))\n- Upgrade snafu to 0.7 ([#1067](https://github.com/casey/just/pull/1067) by [shepmaster](https://github.com/shepmaster))\n- Mention \"$@\" in the README ([#1064](https://github.com/casey/just/pull/1064) by [mpdude](https://github.com/mpdude))\n- Note how to use PowerShell with CLI in readme ([#1056](https://github.com/casey/just/pull/1056) by [michidk](https://github.com/michidk))\n- Link to cheatsheet from readme ([#1053](https://github.com/casey/just/pull/1053) by [casey](https://github.com/casey))\n- Link to Homebrew installation docs in readme ([#1049](https://github.com/casey/just/pull/1049) by [michidk](https://github.com/michidk))\n- Workflow tweaks ([#1045](https://github.com/casey/just/pull/1045) by [casey](https://github.com/casey))\n- Push to correct origin in publish recipe ([#1044](https://github.com/casey/just/pull/1044) by [casey](https://github.com/casey))\n\n[0.10.5](https://github.com/casey/just/releases/tag/0.10.5) - 2021-12-4\n-----------------------------------------------------------------------\n\n### Changed\n- Use musl libc for ARM binaries ([#1037](https://github.com/casey/just/pull/1037) by [casey](https://github.com/casey))\n\n### Misc\n- Make completions work with Bash alias ([#1035](https://github.com/casey/just/pull/1035) by [kurtbuilds](https://github.com/kurtbuilds))\n- Run tests on PRs ([#1040](https://github.com/casey/just/pull/1040) by [casey](https://github.com/casey))\n- Improve GitHub Actions workflow triggers ([#1033](https://github.com/casey/just/pull/1033) by [casey](https://github.com/casey))\n- Publish from GitHub master branch instead of local master ([#1032](https://github.com/casey/just/pull/1032) by [casey](https://github.com/casey))\n\n[0.10.4](https://github.com/casey/just/releases/tag/0.10.4) - 2021-11-21\n------------------------------------------------------------------------\n\n### Added\n- Add `--dump-format json` ([#992](https://github.com/casey/just/pull/992) by [casey](https://github.com/casey))\n- Add `quote(s)` function for escaping strings ([#1022](https://github.com/casey/just/pull/1022) by [casey](https://github.com/casey))\n- fmt: check formatting with `--check` ([#1001](https://github.com/casey/just/pull/1001) by [hdhoang](https://github.com/hdhoang))\n\n### Misc\n- Refactor github actions ([#1028](https://github.com/casey/just/pull/1028) by [casey](https://github.com/casey))\n- Fix readme formatting ([#1030](https://github.com/casey/just/pull/1030) by [soenkehahn](https://github.com/soenkehahn))\n- Use ps1 extension for pwsh shebangs ([#1027](https://github.com/casey/just/pull/1027) by [dmringo](https://github.com/dmringo))\n- Ignore leading byte order mark in source files ([#1021](https://github.com/casey/just/pull/1021) by [casey](https://github.com/casey))\n- Add color to `just --fmt --check` diff ([#1015](https://github.com/casey/just/pull/1015) by [casey](https://github.com/casey))\n\n[0.10.3](https://github.com/casey/just/releases/tag/0.10.3) - 2021-10-30\n------------------------------------------------------------------------\n\n### Added\n- Add `trim_end(s)` and `trim_start(s)` functions ([#999](https://github.com/casey/just/pull/999) by [casey](https://github.com/casey))\n- Add more string manipulation functions ([#998](https://github.com/casey/just/pull/998) by [casey](https://github.com/casey))\n\n### Changed\n- Make `join` accept two or more arguments ([#1000](https://github.com/casey/just/pull/1000) by [casey](https://github.com/casey))\n\n### Misc\n- Add alternatives and prior art section to readme ([#1008](https://github.com/casey/just/pull/1008) by [casey](https://github.com/casey))\n- Fix readme `make`'s not correctly displayed ([#1007](https://github.com/casey/just/pull/1007) by [peter50216](https://github.com/peter50216))\n- Document the default recipe ([#1006](https://github.com/casey/just/pull/1006) by [casey](https://github.com/casey))\n- Document creating user justfile recipe aliases ([#1005](https://github.com/casey/just/pull/1005) by [casey](https://github.com/casey))\n- Fix readme typo ([#1004](https://github.com/casey/just/pull/1004) by [0xflotus](https://github.com/0xflotus))\n- Add packaging status table to readme ([#1003](https://github.com/casey/just/pull/1003) by [casey](https://github.com/casey))\n- Reword `sh` not found error messages ([#1002](https://github.com/casey/just/pull/1002) by [hdhoang](https://github.com/hdhoang))\n- Only pass +crt-static to cargo build ([#997](https://github.com/casey/just/pull/997) by [casey](https://github.com/casey))\n- Stop using tabs in justfile in editorconfig ([#996](https://github.com/casey/just/pull/996) by [casey](https://github.com/casey))\n- Use consistent rustflags formatting ([#994](https://github.com/casey/just/pull/994) by [casey](https://github.com/casey))\n- Use `cargo build` instead of `cargo rustc` ([#993](https://github.com/casey/just/pull/993) by [casey](https://github.com/casey))\n- Don't skip variables in variable iterator ([#991](https://github.com/casey/just/pull/991) by [casey](https://github.com/casey))\n- Remove deprecated equals error ([#985](https://github.com/casey/just/pull/985) by [casey](https://github.com/casey))\n\n[0.10.2](https://github.com/casey/just/releases/tag/0.10.2) - 2021-9-26\n-----------------------------------------------------------------------\n\n### Added\n- Implement regular expression match conditionals ([#970](https://github.com/casey/just/pull/970) by [casey](https://github.com/casey))\n\n### Misc\n- Add detailed instructions for installing prebuilt binaries ([#978](https://github.com/casey/just/pull/978) by [casey](https://github.com/casey))\n- Improve readme package table formatting ([#977](https://github.com/casey/just/pull/977) by [casey](https://github.com/casey))\n- Add conda package to README ([#976](https://github.com/casey/just/pull/976) by [kellpossible](https://github.com/kellpossible))\n- Change MSRV to 1.46.0 ([#968](https://github.com/casey/just/pull/968) by [casey](https://github.com/casey))\n- Use stable rustfmt instead of nightly ([#967](https://github.com/casey/just/pull/967) by [casey](https://github.com/casey))\n- Fix readme typo: FOO → WORLD ([#964](https://github.com/casey/just/pull/964) by [casey](https://github.com/casey))\n- Reword Emacs section in readme ([#962](https://github.com/casey/just/pull/962) by [casey](https://github.com/casey))\n- Mention justl mode for Emacs ([#961](https://github.com/casey/just/pull/961) by [psibi](https://github.com/psibi))\n\n[0.10.1](https://github.com/casey/just/releases/tag/0.10.1) - 2021-8-27\n-----------------------------------------------------------------------\n\n### Added\n- Add flags for specifying name and path to environment file ([#941](https://github.com/casey/just/pull/941) by [Celeo](https://github.com/Celeo))\n\n### Misc\n- Fix error message tests for Alpine Linux ([#956](https://github.com/casey/just/pull/956) by [casey](https://github.com/casey))\n- Bump `target` version to 2.0 ([#957](https://github.com/casey/just/pull/957) by [casey](https://github.com/casey))\n- Mention `tree-sitter-just` in readme ([#951](https://github.com/casey/just/pull/951) by [casey](https://github.com/casey))\n- Document release RSS feed in readme ([#950](https://github.com/casey/just/pull/950) by [casey](https://github.com/casey))\n- Add installation instructions for Gentoo Linux ([#946](https://github.com/casey/just/pull/946) by [dm9pZCAq](https://github.com/dm9pZCAq))\n- Make GitHub Actions instructions more prominent ([#944](https://github.com/casey/just/pull/944) by [casey](https://github.com/casey))\n- Wrap `--help` text to terminal width ([#940](https://github.com/casey/just/pull/940) by [casey](https://github.com/casey))\n- Add `.justfile` to sublime syntax file_extensions ([#938](https://github.com/casey/just/pull/938) by [casey](https://github.com/casey))\n- Suggest using `~/.global.justfile` instead of `~/.justfile` ([#937](https://github.com/casey/just/pull/937) by [casey](https://github.com/casey))\n- Update man page ([#935](https://github.com/casey/just/pull/935) by [casey](https://github.com/casey))\n\n[0.10.0](https://github.com/casey/just/releases/tag/0.10.0) - 2021-8-2\n----------------------------------------------------------------------\n\n### Changed\n- Warn if `.env` file is loaded in `dotenv-load` isn't explicitly set ([#925](https://github.com/casey/just/pull/925) by [casey](https://github.com/casey))\n\n### Added\n- Add `--changelog` subcommand ([#932](https://github.com/casey/just/pull/932) by [casey](https://github.com/casey))\n- Support `.justfile` as an alternative to `justfile` ([#931](https://github.com/casey/just/pull/931) by [casey](https://github.com/casey))\n\n### Misc\n- Use cargo-limit for all recipes ([#928](https://github.com/casey/just/pull/928) by [casey](https://github.com/casey))\n- Fix colors ([#927](https://github.com/casey/just/pull/927) by [casey](https://github.com/casey))\n- Use ColorDisplay trait to print objects to the terminal ([#926](https://github.com/casey/just/pull/926) by [casey](https://github.com/casey))\n- Deduplicate recipe parsing ([#923](https://github.com/casey/just/pull/923) by [casey](https://github.com/casey))\n- Move subcommand functions into Subcommand ([#918](https://github.com/casey/just/pull/918) by [casey](https://github.com/casey))\n- Check GitHub Actions workflow with actionlint ([#921](https://github.com/casey/just/pull/921) by [casey](https://github.com/casey))\n- Add loader and refactor errors ([#917](https://github.com/casey/just/pull/917) by [casey](https://github.com/casey))\n- Rename: Module → Ast ([#915](https://github.com/casey/just/pull/915) by [casey](https://github.com/casey))\n\n[0.9.9](https://github.com/casey/just/releases/tag/0.9.9) - 2021-7-22\n---------------------------------------------------------------------\n\n### Added\n- Add subsequent dependencies ([#820](https://github.com/casey/just/pull/820) by [casey](https://github.com/casey))\n- Implement `else if` chaining ([#910](https://github.com/casey/just/pull/910) by [casey](https://github.com/casey))\n\n### Fixed\n- Fix circular variable dependency error message ([#909](https://github.com/casey/just/pull/909) by [casey](https://github.com/casey))\n\n### Misc\n- Improve readme ([#904](https://github.com/casey/just/pull/904) by [mtsknn](https://github.com/mtsknn))\n- Add screenshot to readme ([#911](https://github.com/casey/just/pull/911) by [casey](https://github.com/casey))\n- Add install instructions for Fedora Linux ([#898](https://github.com/casey/just/pull/898) by [olivierlemasle](https://github.com/olivierlemasle))\n- Fix readme typos ([#903](https://github.com/casey/just/pull/903) by [rokf](https://github.com/rokf))\n- Actually fix release tagging and publish changelog with releases ([#901](https://github.com/casey/just/pull/901) by [casey](https://github.com/casey))\n- Fix broken prerelease tagging ([#900](https://github.com/casey/just/pull/900) by [casey](https://github.com/casey))\n- Use string value for ref-type check ([#897](https://github.com/casey/just/pull/897) by [casey](https://github.com/casey))\n\n[0.9.8](https://github.com/casey/just/releases/tag/0.9.8) - 2021-7-3\n--------------------------------------------------------------------\n\n### Misc\n- Fix changelog formatting ([#894](https://github.com/casey/just/pull/894) by [casey](https://github.com/casey))\n- Only run install script on CI for non-releases ([#895](https://github.com/casey/just/pull/895) by [casey](https://github.com/casey))\n\n[0.9.7](https://github.com/casey/just/releases/tag/0.9.7) - 2021-7-3\n--------------------------------------------------------------------\n\n### Added\n- Add string manipulation functions ([#888](https://github.com/casey/just/pull/888) by [terror](https://github.com/terror))\n\n### Misc\n- Remove test-utilities crate ([#892](https://github.com/casey/just/pull/892) by [casey](https://github.com/casey))\n- Remove outdated note in `Cargo.toml` ([#891](https://github.com/casey/just/pull/891) by [casey](https://github.com/casey))\n- Link to GitHub release pages in changelog ([#886](https://github.com/casey/just/pull/886) by [casey](https://github.com/casey))\n\n[0.9.6](https://github.com/casey/just/releases/tag/0.9.6) - 2021-6-24\n---------------------------------------------------------------------\n\n### Added\n- Add `clean` function for simplifying paths ([#883](https://github.com/casey/just/pull/883) by [casey](https://github.com/casey))\n- Add `join` function for joining paths ([#882](https://github.com/casey/just/pull/882) by [casey](https://github.com/casey))\n- Add path manipulation functions ([#872](https://github.com/casey/just/pull/872) by [TonioGela](https://github.com/TonioGela))\n\n### Misc\n- Add `file_extensions` to Sublime syntax file ([#878](https://github.com/casey/just/pull/878) by [Frederick888](https://github.com/Frederick888))\n- Document path manipulation functions in readme ([#877](https://github.com/casey/just/pull/877) by [casey](https://github.com/casey))\n\n[0.9.5](https://github.com/casey/just/releases/tag/0.9.5) - 2021-6-12\n---------------------------------------------------------------------\n\n### Added\n- Add `--unstable` flag ([#869](https://github.com/casey/just/pull/869) by [casey](https://github.com/casey))\n- Add Sublime Text syntax file ([#864](https://github.com/casey/just/pull/864) by [casey](https://github.com/casey))\n- Add `--fmt` subcommand ([#837](https://github.com/casey/just/pull/837) by [vglfr](https://github.com/vglfr))\n\n### Misc\n- Mention doniogela.dev/just/ in readme ([#866](https://github.com/casey/just/pull/866) by [casey](https://github.com/casey))\n- Mention that vim-just is now available from vim-polyglot ([#865](https://github.com/casey/just/pull/865) by [casey](https://github.com/casey))\n- Mention `--list-heading` newline behavior ([#860](https://github.com/casey/just/pull/860) by [casey](https://github.com/casey))\n- Check for `rg` in `bin/forbid` ([#859](https://github.com/casey/just/pull/859) by [casey](https://github.com/casey))\n- Document that variables are not exported to backticks in the same scope ([#856](https://github.com/casey/just/pull/856) by [casey](https://github.com/casey))\n- Remove `dotenv_load` from tests ([#853](https://github.com/casey/just/pull/853) by [casey](https://github.com/casey))\n- Remove `v` prefix from version ([#850](https://github.com/casey/just/pull/850) by [casey](https://github.com/casey))\n- Improve install script ([#847](https://github.com/casey/just/pull/847) by [casey](https://github.com/casey))\n- Move pages assets back to `docs` ([#846](https://github.com/casey/just/pull/846) by [casey](https://github.com/casey))\n- Move pages assets to `www` ([#845](https://github.com/casey/just/pull/845) by [casey](https://github.com/casey))\n\n[0.9.4](https://github.com/casey/just/releases/tag/v0.9.4) - 2021-5-27\n----------------------------------------------------------------------\n\n### Misc\n- Release `aarch64-unknown-linux-gnu` binaries ([#843](https://github.com/casey/just/pull/843) by [casey](https://github.com/casey))\n- Add `$` to non-default parameter grammar ([#839](https://github.com/casey/just/pull/839) by [casey](https://github.com/casey))\n- Add `$` to parameter grammar ([#838](https://github.com/casey/just/pull/838) by [NoahTheDuke](https://github.com/NoahTheDuke))\n- Fix readme links ([#836](https://github.com/casey/just/pull/836) by [casey](https://github.com/casey))\n- Add `vim-just` installation instructions to readme ([#835](https://github.com/casey/just/pull/835) by [casey](https://github.com/casey))\n- Refactor shebang handling ([#833](https://github.com/casey/just/pull/833) by [casey](https://github.com/casey))\n\n[0.9.3](https://github.com/casey/just/releases/tag/v0.9.3) - 2021-5-16\n----------------------------------------------------------------------\n\n### Added\n- Add shebang support for 'cmd.exe' ([#828](https://github.com/casey/just/pull/828) by [pansila](https://github.com/pansila))\n- Add `.exe` to powershell scripts ([#826](https://github.com/casey/just/pull/826) by [sigoden](https://github.com/sigoden))\n- Add the `--command` subcommand ([#824](https://github.com/casey/just/pull/824) by [casey](https://github.com/casey))\n\n### Fixed\n- Fix bang lexing and placate clippy ([#821](https://github.com/casey/just/pull/821) by [casey](https://github.com/casey))\n\n### Misc\n- Fixed missing close apostrophe in GRAMMAR.md ([#830](https://github.com/casey/just/pull/830) by [SOF3](https://github.com/SOF3))\n- Make 'else' keyword in grammar ([#829](https://github.com/casey/just/pull/829) by [SOF3](https://github.com/SOF3))\n- Add forbid script ([#827](https://github.com/casey/just/pull/827) by [casey](https://github.com/casey))\n- Remove `summary` feature ([#823](https://github.com/casey/just/pull/823) by [casey](https://github.com/casey))\n- Document that just is now in Arch official repo ([#814](https://github.com/casey/just/pull/814) by [svenstaro](https://github.com/svenstaro))\n- Fix changelog years ([#813](https://github.com/casey/just/pull/813) by [casey](https://github.com/casey))\n\n[0.9.2](https://github.com/casey/just/releases/tag/v0.9.2) - 2021-5-02\n----------------------------------------------------------------------\n\n### Fixed\n- Pass evaluated arguments as positional arguments ([#810](https://github.com/casey/just/pull/810) by [casey](https://github.com/casey))\n\n[0.9.1](https://github.com/casey/just/releases/tag/v0.9.1) - 2021-4-24\n----------------------------------------------------------------------\n\n### Added\n- Change `--eval` to print variable value only ([#806](https://github.com/casey/just/pull/806) by [casey](https://github.com/casey))\n- Add `positional-arguments` setting ([#804](https://github.com/casey/just/pull/804) by [casey](https://github.com/casey))\n- Allow filtering variables to evaluate ([#795](https://github.com/casey/just/pull/795) by [casey](https://github.com/casey))\n\n### Changed\n- Reform and improve string literals ([#793](https://github.com/casey/just/pull/793) by [casey](https://github.com/casey))\n- Allow evaluating justfiles with no recipes ([#794](https://github.com/casey/just/pull/794) by [casey](https://github.com/casey))\n- Unify string lexing ([#790](https://github.com/casey/just/pull/790) by [casey](https://github.com/casey))\n\n### Misc\n- Test multi-line strings in interpolation ([#789](https://github.com/casey/just/pull/789) by [casey](https://github.com/casey))\n- Add shell setting examples to README ([#787](https://github.com/casey/just/pull/787) by [casey](https://github.com/casey))\n- Disable .env warning for now ([#786](https://github.com/casey/just/pull/786) by [casey](https://github.com/casey))\n- Warn if `.env` file loaded and `dotenv-load` unset ([#784](https://github.com/casey/just/pull/784) by [casey](https://github.com/casey))\n\n[0.9.0](https://github.com/casey/just/releases/tag/v0.9.0) - 2021-3-28\n----------------------------------------------------------------------\n\n### Changed\n- Turn `=` deprecation warning into a hard error ([#780](https://github.com/casey/just/pull/780) by [casey](https://github.com/casey))\n\n[0.8.7](https://github.com/casey/just/releases/tag/v0.8.7) - 2021-3-28\n----------------------------------------------------------------------\n\n### Added\n- Add `dotenv-load` setting ([#778](https://github.com/casey/just/pull/778) by [casey](https://github.com/casey))\n\n### Misc\n- Change publish recipe to use stable rust ([#777](https://github.com/casey/just/pull/777) by [casey](https://github.com/casey))\n\n[0.8.6](https://github.com/casey/just/releases/tag/v0.8.6) - 2021-3-28\n----------------------------------------------------------------------\n\n### Added\n- Add just_executable() function ([#775](https://github.com/casey/just/pull/775) by [bew](https://github.com/bew))\n- Prefix parameters with `$` to export to environment ([#773](https://github.com/casey/just/pull/773) by [casey](https://github.com/casey))\n- Add `set export` to export all variables as environment variables ([#767](https://github.com/casey/just/pull/767) by [casey](https://github.com/casey))\n\n### Changed\n- Suppress all output to stderr when `--quiet` ([#771](https://github.com/casey/just/pull/771) by [casey](https://github.com/casey))\n\n### Misc\n- Improve chooser invocation error message ([#772](https://github.com/casey/just/pull/772) by [casey](https://github.com/casey))\n- De-emphasize cmd.exe in readme ([#768](https://github.com/casey/just/pull/768) by [casey](https://github.com/casey))\n- Fix warnings ([#770](https://github.com/casey/just/pull/770) by [casey](https://github.com/casey))\n\n[0.8.5](https://github.com/casey/just/releases/tag/v0.8.5) - 2021-3-24\n----------------------------------------------------------------------\n\n### Added\n- Allow escaping double braces with `{{{{` ([#765](https://github.com/casey/just/pull/765) by [casey](https://github.com/casey))\n\n### Misc\n- Reorganize readme to highlight editor support ([#764](https://github.com/casey/just/pull/764) by [casey](https://github.com/casey))\n- Add categories and keywords to Cargo manifest ([#763](https://github.com/casey/just/pull/763) by [casey](https://github.com/casey))\n- Fix command output in readme ([#760](https://github.com/casey/just/pull/760) by [vvv](https://github.com/vvv))\n- Note Emacs package `just-mode` in readme ([#759](https://github.com/casey/just/pull/759) by [leon-barrett](https://github.com/leon-barrett))\n- Note shebang line splitting inconsistency in readme ([#757](https://github.com/casey/just/pull/757) by [casey](https://github.com/casey))\n\n[0.8.4](https://github.com/casey/just/releases/tag/v0.8.4) - 2021-2-9\n---------------------------------------------------------------------\n\n### Added\n- Add options to control list formatting ([#753](https://github.com/casey/just/pull/753) by [casey](https://github.com/casey))\n\n### Misc\n- Document how to change the working directory in a recipe ([#752](https://github.com/casey/just/pull/752) by [casey](https://github.com/casey))\n- Implement `Default` for `Table` ([#748](https://github.com/casey/just/pull/748) by [casey](https://github.com/casey))\n- Add Alpine Linux package to readme ([#736](https://github.com/casey/just/pull/736) by [jirutka](https://github.com/jirutka))\n- Update to actions/cache@v2 ([#742](https://github.com/casey/just/pull/742) by [zyctree](https://github.com/zyctree))\n- Add link in readme to GitHub Action ([#729](https://github.com/casey/just/pull/729) by [rossmacarthur](https://github.com/rossmacarthur))\n- Add docs for justfile() and justfile_directory() ([#726](https://github.com/casey/just/pull/726) by [rminderhoud](https://github.com/rminderhoud))\n- Fix CI ([#727](https://github.com/casey/just/pull/727) by [casey](https://github.com/casey))\n- Improve readme ([#725](https://github.com/casey/just/pull/725) by [casey](https://github.com/casey))\n- Replace saythanks.io link with malto: link ([#723](https://github.com/casey/just/pull/723) by [casey](https://github.com/casey))\n- Update man page to v0.8.3 ([#720](https://github.com/casey/just/pull/720) by [casey](https://github.com/casey))\n\n[0.8.3](https://github.com/casey/just/releases/tag/v0.8.3) - 2020-10-27\n-----------------------------------------------------------------------\n\n### Added\n- Allow ignoring line endings inside delimiters ([#717](https://github.com/casey/just/pull/717) by [casey](https://github.com/casey))\n\n[0.8.2](https://github.com/casey/just/releases/tag/v0.8.2) - 2020-10-26\n-----------------------------------------------------------------------\n\n### Added\n- Add conditional expressions ([#714](https://github.com/casey/just/pull/714) by [casey](https://github.com/casey))\n\n### Fixed\n- Allow completing variables and recipes after `--set` in zsh completion script ([#697](https://github.com/casey/just/pull/697) by [heyrict](https://github.com/heyrict))\n\n### Misc\n- Add Parser::forbid ([#712](https://github.com/casey/just/pull/712) by [casey](https://github.com/casey))\n- Automatically track expected tokens while parsing ([#711](https://github.com/casey/just/pull/711) by [casey](https://github.com/casey))\n- Document feature flags in Cargo.toml ([#709](https://github.com/casey/just/pull/709) by [casey](https://github.com/casey))\n\n[0.8.1](https://github.com/casey/just/releases/tag/v0.8.1) - 2020-10-15\n-----------------------------------------------------------------------\n\n### Changed\n- Allow choosing multiple recipes to run ([#700](https://github.com/casey/just/pull/700) by [casey](https://github.com/casey))\n- Complete recipes in bash completion script ([#685](https://github.com/casey/just/pull/685) by [vikesh-raj](https://github.com/vikesh-raj))\n- Complete recipes names in PowerShell completion script ([#651](https://github.com/casey/just/pull/651) by [Insomniak47](https://github.com/Insomniak47))\n\n### Misc\n- Add FreeBSD port to readme ([#705](https://github.com/casey/just/pull/705) by [casey](https://github.com/casey))\n- Placate clippy ([#698](https://github.com/casey/just/pull/698) by [casey](https://github.com/casey))\n- Fix build fix ([#693](https://github.com/casey/just/pull/693) by [casey](https://github.com/casey))\n- Fix readme documentation for ignoring errors ([#692](https://github.com/casey/just/pull/692) by [kenden](https://github.com/kenden))\n\n[0.8.0](https://github.com/casey/just/releases/tag/v0.8.0) - 2020-10-3\n----------------------------------------------------------------------\n\n### Breaking\n- Allow suppressing failures with `-` prefix ([#687](https://github.com/casey/just/pull/687) by [iwillspeak](https://github.com/iwillspeak))\n\n### Misc\n- Document how to ignore errors with `-` in readme ([#690](https://github.com/casey/just/pull/690) by [casey](https://github.com/casey))\n- Install BSD Tar on GitHub Actions to fix CI errors ([#689](https://github.com/casey/just/pull/689) by [casey](https://github.com/casey))\n- Move separate quiet config value to verbosity ([#686](https://github.com/casey/just/pull/686) by [Celeo](https://github.com/Celeo))\n\n[0.7.3](https://github.com/casey/just/releases/tag/v0.7.3) - 2020-9-17\n----------------------------------------------------------------------\n\n### Added\n- Add the `--choose` subcommand ([#680](https://github.com/casey/just/pull/680) by [casey](https://github.com/casey))\n\n### Misc\n- Combine integration tests into single binary ([#679](https://github.com/casey/just/pull/679) by [casey](https://github.com/casey))\n- Document `--unsorted` flag in readme ([#672](https://github.com/casey/just/pull/672) by [casey](https://github.com/casey))\n\n[0.7.2](https://github.com/casey/just/releases/tag/v0.7.2) - 2020-8-23\n----------------------------------------------------------------------\n\n### Added\n- Add option to print recipes in source order ([#669](https://github.com/casey/just/pull/669) by [casey](https://github.com/casey))\n\n### Misc\n- Mention Linux, MacOS and Windows support in readme ([#666](https://github.com/casey/just/pull/666) by [casey](https://github.com/casey))\n- Add list highlighting nice features to readme ([#664](https://github.com/casey/just/pull/664) by [casey](https://github.com/casey))\n\n[0.7.1](https://github.com/casey/just/releases/tag/v0.7.1) - 2020-7-19\n----------------------------------------------------------------------\n\n### Fixed\n- Search for `.env` file from working directory ([#661](https://github.com/casey/just/pull/661) by [casey](https://github.com/casey))\n\n### Misc\n- Move link-time optimization config into `Cargo.toml` ([#658](https://github.com/casey/just/pull/658) by [casey](https://github.com/casey))\n\n[0.7.0](https://github.com/casey/just/releases/tag/v0.7.0) - 2020-7-16\n----------------------------------------------------------------------\n\n### Breaking\n- Skip `.env` items which are set in environment ([#656](https://github.com/casey/just/pull/656) by [casey](https://github.com/casey))\n\n### Misc\n- Mark tags that start with `v` as releases ([#654](https://github.com/casey/just/pull/654) by [casey](https://github.com/casey))\n\n[0.6.1](https://github.com/casey/just/releases/tag/v0.6.1) - 2020-6-28\n----------------------------------------------------------------------\n\n### Changed\n- Only use `cygpath` on shebang if it contains `/` ([#652](https://github.com/casey/just/pull/652) by [casey](https://github.com/casey))\n\n[0.6.0](https://github.com/casey/just/releases/tag/v0.6.0) - 2020-6-18\n----------------------------------------------------------------------\n\n### Changed\n- Ignore '@' returned from interpolation evaluation ([#636](https://github.com/casey/just/pull/636) by [rjsberry](https://github.com/rjsberry))\n- Strip leading spaces after line continuation ([#635](https://github.com/casey/just/pull/635) by [casey](https://github.com/casey))\n\n### Added\n- Add variadic parameters that accept zero or more arguments ([#645](https://github.com/casey/just/pull/645) by [rjsberry](https://github.com/rjsberry))\n\n### Misc\n- Clarify variadic parameter default values ([#646](https://github.com/casey/just/pull/646) by [rjsberry](https://github.com/rjsberry))\n- Add keybase example justfile  ([#640](https://github.com/casey/just/pull/640) by [blaggacao](https://github.com/blaggacao))\n- Strip trailing whitespace in `examples/pre-commit.just` ([#644](https://github.com/casey/just/pull/644) by [casey](https://github.com/casey))\n- Test that example justfiles successfully parse ([#643](https://github.com/casey/just/pull/643) by [casey](https://github.com/casey))\n- Link example justfiles in readme ([#641](https://github.com/casey/just/pull/641) by [casey](https://github.com/casey))\n- Add example justfile ([#639](https://github.com/casey/just/pull/639) by [blaggacao](https://github.com/blaggacao))\n- Document how to run recipes after another recipe ([#630](https://github.com/casey/just/pull/630) by [casey](https://github.com/casey))\n\n[0.5.11](https://github.com/casey/just/releases/tag/v0.5.11) - 2020-5-23\n------------------------------------------------------------------------\n\n### Added\n- Don't load `.env` file when `--no-dotenv` is passed ([#627](https://github.com/casey/just/pull/627) by [casey](https://github.com/casey))\n\n### Changed\n- Complete recipe names in fish completion script ([#625](https://github.com/casey/just/pull/625) by [tyehle](https://github.com/tyehle))\n- Suggest aliases for unknown recipes ([#624](https://github.com/casey/just/pull/624) by [Celeo](https://github.com/Celeo))\n\n[0.5.10](https://github.com/casey/just/releases/tag/v0.5.10) - 2020-3-18\n------------------------------------------------------------------------\n\n[0.5.9](https://github.com/casey/just/releases/tag/v0.5.9) - 2020-3-18\n----------------------------------------------------------------------\n\n### Added\n- Update zsh completion file ([#606](https://github.com/casey/just/pull/606) by [heyrict](https://github.com/heyrict))\n- Add `--variables` subcommand that prints variable names ([#608](https://github.com/casey/just/pull/608) by [casey](https://github.com/casey))\n- Add github pages site with improved install script ([#597](https://github.com/casey/just/pull/597) by [casey](https://github.com/casey))\n\n### Fixed\n- Don't require justfile to print completions ([#596](https://github.com/casey/just/pull/596) by [casey](https://github.com/casey))\n\n### Misc\n- Only build for linux on docs.rs ([#611](https://github.com/casey/just/pull/611) by [casey](https://github.com/casey))\n- Trim completions and ensure final newline ([#609](https://github.com/casey/just/pull/609) by [casey](https://github.com/casey))\n- Trigger build on pushes and pull requests ([#607](https://github.com/casey/just/pull/607) by [casey](https://github.com/casey))\n- Document behavior of `@` on shebang recipes ([#602](https://github.com/casey/just/pull/602) by [casey](https://github.com/casey))\n- Add `.nojekyll` file to github pages site ([#599](https://github.com/casey/just/pull/599) by [casey](https://github.com/casey))\n- Add `:` favicon ([#598](https://github.com/casey/just/pull/598) by [casey](https://github.com/casey))\n- Delete old CI configuration and update build badge ([#595](https://github.com/casey/just/pull/595) by [casey](https://github.com/casey))\n- Add download count badge to readme ([#594](https://github.com/casey/just/pull/594) by [casey](https://github.com/casey))\n- Wrap comments at 80 characters ([#593](https://github.com/casey/just/pull/593) by [casey](https://github.com/casey))\n- Use unstable rustfmt configuration options ([#592](https://github.com/casey/just/pull/592) by [casey](https://github.com/casey))\n\n[0.5.8](https://github.com/casey/just/releases/tag/v0.5.8) - 2020-1-28\n----------------------------------------------------------------------\n\n### Changed\n- Only use `cygpath` on windows if present ([#586](https://github.com/casey/just/pull/586) by [casey](https://github.com/casey))\n\n### Misc\n- Improve comments in justfile ([#588](https://github.com/casey/just/pull/588) by [casey](https://github.com/casey))\n- Remove unused dependencies ([#587](https://github.com/casey/just/pull/587) by [casey](https://github.com/casey))\n\n[0.5.7](https://github.com/casey/just/releases/tag/v0.5.7) - 2020-1-28\n----------------------------------------------------------------------\n\n### Misc\n- Don't include directories in release archive ([#583](https://github.com/casey/just/pull/583) by [casey](https://github.com/casey))\n\n[0.5.6](https://github.com/casey/just/releases/tag/v0.5.6) - 2020-1-28\n----------------------------------------------------------------------\n\n### Misc\n- Build and upload release artifacts from GitHub Actions ([#581](https://github.com/casey/just/pull/581) by [casey](https://github.com/casey))\n- List solus package in readme ([#579](https://github.com/casey/just/pull/579) by [casey](https://github.com/casey))\n- Expand use of GitHub Actions ([#580](https://github.com/casey/just/pull/580) by [casey](https://github.com/casey))\n- Fix readme typo: interpetation -> interpretation ([#578](https://github.com/casey/just/pull/578) by [Plommonsorbet](https://github.com/Plommonsorbet))\n\n[0.5.5](https://github.com/casey/just/releases/tag/v0.5.5) - 2020-1-15\n----------------------------------------------------------------------\n\n### Added\n- Generate shell completion scripts with `--completions` ([#572](https://github.com/casey/just/pull/572) by [casey](https://github.com/casey))\n\n### Misc\n- Check long lines and FIXME/TODO on CI ([#575](https://github.com/casey/just/pull/575) by [casey](https://github.com/casey))\n- Add additional continuous integration checks ([#574](https://github.com/casey/just/pull/574) by [casey](https://github.com/casey))\n\n[0.5.4](https://github.com/casey/just/releases/tag/v0.5.4) - 2019-12-25\n-----------------------------------------------------------------------\n\n### Added\n- Add `justfile_directory()` and `justfile()` ([#569](https://github.com/casey/just/pull/569) by [casey](https://github.com/casey))\n\n### Misc\n- Add table of package managers that include just to readme ([#568](https://github.com/casey/just/pull/568) by [casey](https://github.com/casey))\n- Remove yaourt AUR helper from readme ([#567](https://github.com/casey/just/pull/567) by [ky0n](https://github.com/ky0n))\n- Fix regression in error message color printing ([#566](https://github.com/casey/just/pull/566) by [casey](https://github.com/casey))\n- Reform indentation handling ([#565](https://github.com/casey/just/pull/565) by [casey](https://github.com/casey))\n- Update Cargo.lock with new version ([#564](https://github.com/casey/just/pull/564) by [casey](https://github.com/casey))\n\n[0.5.3](https://github.com/casey/just/releases/tag/v0.5.3) - 2019-12-11\n-----------------------------------------------------------------------\n\n### Misc\n- Assert that lexer advances over entire input ([#560](https://github.com/casey/just/pull/560) by [casey](https://github.com/casey))\n- Fix typo: `chracter` -> `character` ([#561](https://github.com/casey/just/pull/561) by [casey](https://github.com/casey))\n- Improve pre-publish check ([#562](https://github.com/casey/just/pull/562) by [casey](https://github.com/casey))\n\n[0.5.2](https://github.com/casey/just/releases/tag/v0.5.2) - 2019-12-7\n----------------------------------------------------------------------\n\n### Added\n- Add flags to set and clear shell arguments ([#551](https://github.com/casey/just/pull/551) by [casey](https://github.com/casey))\n- Allow passing arguments to dependencies ([#555](https://github.com/casey/just/pull/555) by [casey](https://github.com/casey))\n\n### Misc\n- Un-implement Deref for Table ([#546](https://github.com/casey/just/pull/546) by [casey](https://github.com/casey))\n- Resolve recipe dependencies ([#547](https://github.com/casey/just/pull/547) by [casey](https://github.com/casey))\n- Resolve alias targets ([#548](https://github.com/casey/just/pull/548) by [casey](https://github.com/casey))\n- Remove unnecessary type argument to Alias ([#549](https://github.com/casey/just/pull/549) by [casey](https://github.com/casey))\n- Resolve functions ([#550](https://github.com/casey/just/pull/550) by [casey](https://github.com/casey))\n- Reform scope and binding ([#556](https://github.com/casey/just/pull/556) by [casey](https://github.com/casey))\n\n[0.5.1](https://github.com/casey/just/releases/tag/v0.5.1) - 2019-11-20\n-----------------------------------------------------------------------\n\n### Added\n- Add `--init` subcommand ([#541](https://github.com/casey/just/pull/541) by [casey](https://github.com/casey))\n\n### Changed\n- Avoid fs::canonicalize ([#539](https://github.com/casey/just/pull/539) by [casey](https://github.com/casey))\n\n### Misc\n- Mention `set shell` as alternative to installing `sh` ([#533](https://github.com/casey/just/pull/533) by [casey](https://github.com/casey))\n- Refactor Compilation error to contain a Token ([#535](https://github.com/casey/just/pull/535) by [casey](https://github.com/casey))\n- Move lexer comment ([#536](https://github.com/casey/just/pull/536) by [casey](https://github.com/casey))\n- Add missing `--init` test ([#543](https://github.com/casey/just/pull/543) by [casey](https://github.com/casey))\n\n[0.5.0](https://github.com/casey/just/releases/tag/v0.5.0) - 2019-11-12\n-----------------------------------------------------------------------\n\n### Added\n\n- Add `set shell := [...]` to grammar ([#526](https://github.com/casey/just/pull/526) by [casey](https://github.com/casey))\n- Add `shell` setting ([#525](https://github.com/casey/just/pull/525) by [casey](https://github.com/casey))\n- Document settings in readme ([#527](https://github.com/casey/just/pull/527) by [casey](https://github.com/casey))\n\n### Changed\n- Reform positional argument parsing ([#523](https://github.com/casey/just/pull/523) by [casey](https://github.com/casey))\n- Highlight echoed recipe lines in bold by default ([#512](https://github.com/casey/just/pull/512) by [casey](https://github.com/casey))\n\n### Misc\n\n- Gargantuan refactor ([#522](https://github.com/casey/just/pull/522) by [casey](https://github.com/casey))\n- Move subcommand execution into Subcommand ([#514](https://github.com/casey/just/pull/514) by [casey](https://github.com/casey))\n- Move `cd` out of Config::from_matches ([#513](https://github.com/casey/just/pull/513) by [casey](https://github.com/casey))\n- Remove now-unnecessary borrow checker appeasement ([#511](https://github.com/casey/just/pull/511) by [casey](https://github.com/casey))\n- Reform Parser ([#509](https://github.com/casey/just/pull/509) by [casey](https://github.com/casey))\n- Note need to publish with nightly cargo ([#506](https://github.com/casey/just/pull/506) by [casey](https://github.com/casey))\n\n[0.4.5](https://github.com/casey/just/releases/tag/v0.4.5) - 2019-10-31\n-----------------------------------------------------------------------\n\n### User-visible\n\n### Changed\n- Display alias with `--show NAME` if one exists ([#466](https://github.com/casey/just/pull/466) by [casey](https://github.com/casey))\n\n### Documented\n- Document multi-line constructs (for/if/while) ([#453](https://github.com/casey/just/pull/453) by [casey](https://github.com/casey))\n- Generate man page with help2man ([#463](https://github.com/casey/just/pull/463) by [casey](https://github.com/casey))\n- Add context to deprecation warnings ([#473](https://github.com/casey/just/pull/473) by [casey](https://github.com/casey))\n- Improve messages for alias error messages ([#500](https://github.com/casey/just/pull/500) by [casey](https://github.com/casey))\n\n### Misc\n\n### Cleanup\n- Update deprecated rust range patterns and clippy config ([#450](https://github.com/casey/just/pull/450) by [light4](https://github.com/light4))\n- Make comments in common.rs lowercase ([#470](https://github.com/casey/just/pull/470) by [casey](https://github.com/casey))\n- Use `pub(crate)` instead of `pub` ([#471](https://github.com/casey/just/pull/471) by [casey](https://github.com/casey))\n- Hide summary functionality behind feature flag ([#472](https://github.com/casey/just/pull/472) by [casey](https://github.com/casey))\n- Fix `summary` feature conditional compilation ([#475](https://github.com/casey/just/pull/475) by [casey](https://github.com/casey))\n- Allow integration test cases to omit common values ([#480](https://github.com/casey/just/pull/480) by [casey](https://github.com/casey))\n- Add `unindent()` for nicer integration test strings ([#481](https://github.com/casey/just/pull/481) by [casey](https://github.com/casey))\n- Start pulling argument parsing out of run::run() ([#483](https://github.com/casey/just/pull/483) by [casey](https://github.com/casey))\n- Add explicit `Subcommand` enum ([#484](https://github.com/casey/just/pull/484) by [casey](https://github.com/casey))\n- Avoid using error code `1` in integration tests ([#486](https://github.com/casey/just/pull/486) by [casey](https://github.com/casey))\n- Use more indented strings in integration tests ([#489](https://github.com/casey/just/pull/489) by [casey](https://github.com/casey))\n- Refactor `run::run` and Config ([#490](https://github.com/casey/just/pull/490) by [casey](https://github.com/casey))\n- Remove `misc.rs` ([#491](https://github.com/casey/just/pull/491) by [casey](https://github.com/casey))\n- Remove unused `use` statements ([#497](https://github.com/casey/just/pull/497) by [casey](https://github.com/casey))\n- Refactor lexer tests ([#498](https://github.com/casey/just/pull/498) by [casey](https://github.com/casey))\n- Use constants instead of literals in arg parser ([#504](https://github.com/casey/just/pull/504) by [casey](https://github.com/casey))\n\n### Infrastructure\n- Add repository attribute to Cargo.toml ([#493](https://github.com/casey/just/pull/493) by [SOF3](https://github.com/SOF3))\n- Check minimal version compatibility before publishing ([#487](https://github.com/casey/just/pull/487) by [casey](https://github.com/casey))\n\n### Continuous Integration\n- Disable FreeBSD builds ([#474](https://github.com/casey/just/pull/474) by [casey](https://github.com/casey))\n- Use `bash` as shell for all integration tests ([#479](https://github.com/casey/just/pull/479) by [casey](https://github.com/casey))\n- Don't install `dash` on Travis ([#482](https://github.com/casey/just/pull/482) by [casey](https://github.com/casey))\n\n### Dependencies\n- Use `tempfile` crate instead of `tempdir` ([#455](https://github.com/casey/just/pull/455) by [NickeZ](https://github.com/NickeZ))\n- Bump clap dependency to 2.33.0 ([#458](https://github.com/casey/just/pull/458) by [NickeZ](https://github.com/NickeZ))\n- Minimize dependency version requirements ([#461](https://github.com/casey/just/pull/461) by [casey](https://github.com/casey))\n- Remove dependency on brev ([#462](https://github.com/casey/just/pull/462) by [casey](https://github.com/casey))\n- Update dependencies ([#501](https://github.com/casey/just/pull/501) by [casey](https://github.com/casey))\n\n[0.4.4](https://github.com/casey/just/releases/tag/v0.4.4) - 2019-06-02\n-----------------------------------------------------------------------\n\n### Changed\n- Ignore file name case while searching for justfile ([#436](https://github.com/casey/just/pull/436) by [shevtsiv](https://github.com/shevtsiv))\n\n### Added\n- Display alias target with `--show` ([#443](https://github.com/casey/just/pull/443) by [casey](https://github.com/casey))\n\n[0.4.3](https://github.com/casey/just/releases/tag/v0.4.3) - 2019-05-07\n-----------------------------------------------------------------------\n\n### Changed\n- Deprecate `=` in assignments, aliases, and exports in favor of `:=` ([#413](https://github.com/casey/just/pull/413) by [casey](https://github.com/casey))\n\n### Added\n- Pass stdin handle to backtick process ([#409](https://github.com/casey/just/pull/409) by [casey](https://github.com/casey))\n\n### Documented\n- Fix readme command line ([#411](https://github.com/casey/just/pull/411) by [casey](https://github.com/casey))\n- Typo: \"command equivelant\" -> \"command equivalent\" ([#418](https://github.com/casey/just/pull/418) by [casey](https://github.com/casey))\n- Mention Make’s “phony target” workaround in the comparison ([#421](https://github.com/casey/just/pull/421) by [roryokane](https://github.com/roryokane))\n- Add Void Linux install instructions to readme ([#423](https://github.com/casey/just/pull/423) by [casey](https://github.com/casey))\n\n### Cleaned up or Refactored\n- Remove stray source files ([#408](https://github.com/casey/just/pull/408) by [casey](https://github.com/casey))\n- Replace some calls to brev crate ([#410](https://github.com/casey/just/pull/410) by [casey](https://github.com/casey))\n- Lexer code deduplication and refactoring ([#414](https://github.com/casey/just/pull/414) by [casey](https://github.com/casey))\n- Refactor and rename test macros ([#415](https://github.com/casey/just/pull/415) by [casey](https://github.com/casey))\n- Move CompilationErrorKind into separate module ([#416](https://github.com/casey/just/pull/416) by [casey](https://github.com/casey))\n- Remove `write_token_error_context` ([#417](https://github.com/casey/just/pull/417) by [casey](https://github.com/casey))\n\n[0.4.2](https://github.com/casey/just/releases/tag/v0.4.2) - 2019-04-12\n-----------------------------------------------------------------------\n\n### Changed\n- Regex-based lexer replaced with much nicer character-at-a-time lexer ([#406](https://github.com/casey/just/pull/406) by [casey](https://github.com/casey))\n\n[0.4.1](https://github.com/casey/just/releases/tag/v0.4.1) - 2019-04-12\n-----------------------------------------------------------------------\n\n### Changed\n- Make summary function non-generic ([#404](https://github.com/casey/just/pull/404) by [casey](https://github.com/casey))\n\n[0.4.0](https://github.com/casey/just/releases/tag/v0.4.0) - 2019-04-12\n-----------------------------------------------------------------------\n\n### Added\n- Add recipe aliases ([#390](https://github.com/casey/just/pull/390) by [ryloric](https://github.com/ryloric))\n- Allow arbitrary expressions as default arguments ([#400](https://github.com/casey/just/pull/400) by [casey](https://github.com/casey))\n- Add justfile summaries ([#399](https://github.com/casey/just/pull/399) by [casey](https://github.com/casey))\n- Allow outer shebang lines so justfiles can be used as scripts ([#393](https://github.com/casey/just/pull/393) by [casey](https://github.com/casey))\n- Allow `--justfile` without `--working-directory` ([#392](https://github.com/casey/just/pull/392) by [smonami](https://github.com/smonami))\n- Add link to Chinese translation of readme by chinanf-boy ([#377](https://github.com/casey/just/pull/377) by [casey](https://github.com/casey))\n\n### Changed\n- Upgrade to Rust 2018 ([#394](https://github.com/casey/just/pull/394) by [casey](https://github.com/casey))\n- Format the codebase with rustfmt ([#346](https://github.com/casey/just/pull/346) by [casey](https://github.com/casey))\n\n[0.3.13](https://github.com/casey/just/releases/tag/v0.3.13) - 2018-11-06\n-------------------------------------------------------------------------\n\n### Added\n- Print recipe signature if missing arguments ([#369](https://github.com/casey/just/pull/369) by [ladysamantha](https://github.com/ladysamantha))\n- Add grandiloquent verbosity level that echos shebang recipes ([#348](https://github.com/casey/just/pull/348) by [casey](https://github.com/casey))\n- Wait for child processes to finish ([#345](https://github.com/casey/just/pull/345) by [casey](https://github.com/casey))\n- Improve invalid escape sequence error messages ([#328](https://github.com/casey/just/pull/328) by [casey](https://github.com/casey))\n\n### Fixed\n- Use PutBackN instead of PutBack in parser ([#364](https://github.com/casey/just/pull/364) by [casey](https://github.com/casey))\n\n[0.3.12](https://github.com/casey/just/releases/tag/v0.3.12) - 2018-06-19\n-------------------------------------------------------------------------\n\n### Added\n- Implemented invocation_directory function ([#323](https://github.com/casey/just/pull/323) by [casey](https://github.com/casey))\n\n[0.3.11](https://github.com/casey/just/releases/tag/v0.3.11) - 2018-05-6\n------------------------------------------------------------------------\n\n### Fixed\n- Fixed colors on windows ([#317](https://github.com/casey/just/pull/317) by [casey](https://github.com/casey))\n\n[0.3.10](https://github.com/casey/just/releases/tag/v0.3.10) - 2018-3-19\n------------------------------------------------------------------------\n\n### Added\n- Make .env vars available in env_var functions ([#310](https://github.com/casey/just/pull/310) by [casey](https://github.com/casey))\n\n[0.3.8](https://github.com/casey/just/releases/tag/v0.3.8) - 2018-3-5\n---------------------------------------------------------------------\n\n### Added\n- Add dotenv integration ([#306](https://github.com/casey/just/pull/306) by [casey](https://github.com/casey))\n\n[0.3.7](https://github.com/casey/just/releases/tag/v0.3.7) - 2017-12-11\n-----------------------------------------------------------------------\n\n### Fixed\n- Fix error if ! appears in comment ([#296](https://github.com/casey/just/pull/296) by [casey](https://github.com/casey))\n\n[0.3.6](https://github.com/casey/just/releases/tag/v0.3.6) - 2017-12-11\n-----------------------------------------------------------------------\n\n### Fixed\n- Lex CRLF line endings properly ([#292](https://github.com/casey/just/pull/292) by [casey](https://github.com/casey))\n\n[0.3.5](https://github.com/casey/just/releases/tag/v0.3.5) - 2017-12-11\n-----------------------------------------------------------------------\n\n### Added\n- Align doc-comments in `--list` output ([#273](https://github.com/casey/just/pull/273) by [casey](https://github.com/casey))\n- Add `arch()`, `os()`, and `os_family()` functions ([#277](https://github.com/casey/just/pull/277) by [casey](https://github.com/casey))\n- Add `env_var(key)` and `env_var_or_default(key, default)` functions ([#280](https://github.com/casey/just/pull/280) by [casey](https://github.com/casey))\n\n[0.3.4](https://github.com/casey/just/releases/tag/v0.3.4) - 2017-10-06\n-----------------------------------------------------------------------\n\n### Added\n- Do not evaluate backticks in assignments during dry runs ([#253](https://github.com/casey/just/pull/253) by [aoeu](https://github.com/aoeu))\n\n### Changed\n- Change license to CC0 going forward ([#270](https://github.com/casey/just/pull/270) by [casey](https://github.com/casey))\n\n[0.3.1](https://github.com/casey/just/releases/tag/v0.3.1) - 2017-10-06\n-----------------------------------------------------------------------\n\n### Added\n- Started keeping a changelog in CHANGELOG.md ([#220](https://github.com/casey/just/pull/220) by [casey](https://github.com/casey))\n- Recipes whose names begin with an underscore will not appear in `--list` or `--summary` ([#229](https://github.com/casey/just/pull/229) by [casey](https://github.com/casey))\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "Contributing\n============\n\nUnless you explicitly state otherwise, any contribution intentionally submitted\nfor inclusion in the work by you shall be licensed as in [LICENSE](LICENSE),\nwithout any additional terms or conditions.\n\nSee [the readme](README.md#contributing) for contribution workflow suggestions.\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"just\"\nversion = \"1.47.1\"\nauthors = [\"Casey Rodarmor <casey@rodarmor.com>\"]\nautotests = false\ncategories = [\"command-line-utilities\", \"development-tools\"]\ndescription = \"🤖 Just a command runner\"\nedition = \"2024\"\nexclude = [\"/book\", \"/icon.png\", \"/screenshot.png\", \"/www\"]\nhomepage = \"https://github.com/casey/just\"\nkeywords = [\"command-line\", \"task\", \"runner\", \"development\", \"utility\"]\nlicense = \"CC0-1.0\"\nreadme = \"crates-io-readme.md\"\nrepository = \"https://github.com/casey/just\"\nrust-version = \"1.85.0\"\n\n[workspace]\nmembers = [\".\", \"crates/*\"]\n\n[dependencies]\nansi_term = \"0.12.0\"\nblake3 = { version = \"1.5.0\", features = [\"rayon\", \"mmap\"] }\ncamino = \"1.0.4\"\nchrono = \"0.4.38\"\nclap = { version = \"4.0.0\", features = [\"derive\", \"env\", \"wrap_help\"] }\nclap_mangen = \"0.2.20\"\nderive-where = \"1.2.7\"\ndirs = \"6.0.0\"\ndotenvy = \"0.15\"\nedit-distance = \"2.0.0\"\nheck = \"0.5.0\"\nis_executable = \"1.0.4\"\nlexiclean = \"0.0.1\"\nlibc = \"0.2.0\"\nnum_cpus = \"1.15.0\"\npercent-encoding = \"2.3.1\"\nrand = \"0.10.0\"\nregex = \"1.10.4\"\nrustversion = \"1.0.18\"\nsemver = \"1.0.20\"\nserde = { version = \"1.0.130\", features = [\"derive\", \"rc\"] }\nserde_json = \"1.0.68\"\nsha2 = \"0.10\"\nshellexpand = \"3.1.0\"\nsimilar = { version = \"2.1.0\", features = [\"unicode\"] }\nsnafu = \"0.9.0\"\nstrum = { version = \"0.28.0\", features = [\"derive\"] }\ntarget = \"2.0.0\"\ntempfile = \"3.0.0\"\ntyped-arena = \"2.0.1\"\nunicode-width = \"0.2.0\"\nuuid = { version = \"1.0.0\", features = [\"v4\"] }\n\n[target.'cfg(unix)'.dependencies]\nnix = { version = \"0.31.0\", features = [\"signal\", \"user\", \"fs\"] }\n\n[target.'cfg(windows)'.dependencies]\nctrlc = { version = \"3.1.1\", features = [\"termination\"] }\n\n[dev-dependencies]\nclap_complete = \"=4.5.48\"\npretty_assertions = \"1.0.0\"\ntemptree = \"0.2.0\"\nwhich = \"8.0.0\"\n\n[lints.rust]\nmismatched_lifetime_syntaxes = \"allow\"\nunexpected_cfgs = { level = \"warn\", check-cfg = ['cfg(fuzzing)'] }\nunreachable_pub = \"deny\"\n\n[lints.clippy]\nall = { level = \"deny\", priority = -1 }\narbitrary_source_item_ordering = \"deny\"\nenum_glob_use = \"allow\"\nignore_without_reason = \"allow\"\nneedless_pass_by_value = \"allow\"\npedantic = { level = \"deny\", priority = -1 }\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_arguments = \"allow\"\ntoo_many_lines = \"allow\"\ntype_complexity = \"allow\"\nundocumented_unsafe_blocks = \"deny\"\nunnecessary_wraps = \"allow\"\nwildcard_imports = \"allow\"\n\n[lib]\ndoctest = false\n\n[[bin]]\npath = \"src/main.rs\"\nname = \"just\"\ntest = false\n\n# The public documentation is minimal and doesn't change between\n# platforms, so we only build them for linux on docs.rs to save\n# their build machines some cycles.\n[package.metadata.docs.rs]\ntargets = [\"x86_64-unknown-linux-gnu\"]\n\n[profile.release]\nlto = true\ncodegen-units = 1\n\n[[test]]\nname = \"integration\"\npath = \"tests/lib.rs\"\n"
  },
  {
    "path": "GRAMMAR.md",
    "content": "justfile grammar\n================\n\nJustfiles are processed by a mildly context-sensitive tokenizer\nand a recursive descent parser. The grammar is LL(k), for an\nunknown but hopefully reasonable value of k.\n\ntokens\n------\n\n```\nBACKTICK            = `[^`]*`\nINDENTED_BACKTICK   = ```[^(```)]*```\nCOMMENT             = #([^!].*)?$\nDEDENT              = emitted when indentation decreases\nEOF                 = emitted at the end of the file\nINDENT              = emitted when indentation increases\nLINE                = emitted before a recipe line\nNAME                = [a-zA-Z_][a-zA-Z0-9_-]*\nNEWLINE             = \\n|\\r\\n\nRAW_STRING          = '[^']*'\nINDENTED_RAW_STRING = '''[^(''')]*'''\nSTRING              = \"[^\"]*\" # also processes \\n \\r \\t \\\" \\\\ escapes\nINDENTED_STRING     = \"\"\"[^(\"\"\")]*\"\"\" # also processes \\n \\r \\t \\\" \\\\ escapes\nLINE_PREFIX         = @-|-@|@|-\nTEXT                = recipe text, only matches in a recipe body\n```\n\ngrammar syntax\n--------------\n\n```\n|   alternation\n()  grouping\n_?  option (0 or 1 times)\n_*  repetition (0 or more times)\n_+  repetition (1 or more times)\n```\n\ngrammar\n-------\n\n```\njustfile      : item* EOF\n\nitem          : alias\n              | assignment\n              | eol\n              | export\n              | import\n              | module\n              | recipe\n              | set\n\neol           : NEWLINE\n              | COMMENT NEWLINE\n\nalias         : 'alias' NAME ':=' target eol\n\ntarget        : NAME ('::' NAME)*\n\nassignment    : NAME ':=' expression eol\n\nexport        : 'export' assignment\n\nset           : 'set' setting eol\n\nsetting       : 'allow-duplicate-recipes' boolean?\n              | 'allow-duplicate-variables' boolean?\n              | 'dotenv-filename' ':=' string\n              | 'dotenv-load' boolean?\n              | 'dotenv-path' ':=' string\n              | 'dotenv-required' boolean?\n              | 'export' boolean?\n              | 'fallback' boolean?\n              | 'ignore-comments' boolean?\n              | 'positional-arguments' boolean?\n              | 'script-interpreter' ':=' string_list\n              | 'quiet' boolean?\n              | 'shell' ':=' string_list\n              | 'tempdir' ':=' string\n              | 'unstable' boolean?\n              | 'windows-powershell' boolean?\n              | 'windows-shell' ':=' string_list\n              | 'working-directory' ':=' string\n\nboolean       : ':=' ('true' | 'false')\n\nstring_list   : '[' string (',' string)* ','? ']'\n\nimport        : 'import' '?'? string? eol\n\nmodule        : 'mod' '?'? NAME string? eol\n\nexpression    : disjunct || expression\n              | disjunct\n\ndisjunct      : conjunct && disjunct\n              | conjunct\n\nconjunct      : 'if' condition '{' expression '}' 'else' '{' expression '}'\n              | 'assert' '(' condition ',' expression ')'\n              | '/' expression\n              | value '/' expression\n              | value '+' expression\n              | value\n\ncondition     : expression '==' expression\n              | expression '!=' expression\n              | expression '=~' expression\n\nvalue         : NAME '(' sequence? ')'\n              | BACKTICK\n              | INDENTED_BACKTICK\n              | NAME\n              | string\n              | '(' expression ')'\n\nstring        : 'x'? STRING\n              | 'x'? INDENTED_STRING\n              | 'x'? RAW_STRING\n              | 'x'? INDENTED_RAW_STRING\n\nsequence      : expression ',' sequence\n              | expression ','?\n\nrecipe        : attributes* '@'? NAME parameter* variadic? ':' dependencies eol body?\n\nattributes    : '[' attribute (',' attribute)* ']' eol\n\nattribute     : NAME\n              | NAME ':' string\n              | NAME '(' string (',' string)* ')'\n\nparameter     : '$'? NAME\n              | '$'? NAME '=' value\n\nvariadic      : '*' parameter\n              | '+' parameter\n\ndependencies  : dependency* ('&&' dependency+)?\n\ndependency    : target\n              | '(' target expression* ')'\n\nbody          : INDENT line+ DEDENT\n\nline          : LINE LINE_PREFIX? (TEXT | interpolation)+ NEWLINE\n              | NEWLINE\n\ninterpolation : '{{' expression '}}'\n```\n"
  },
  {
    "path": "LICENSE",
    "content": "Creative Commons Legal Code\n\nCC0 1.0 Universal\n\n    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN\n    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS\n    INFORMATION ON AN \"AS-IS\" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES\n    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS\n    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM\n    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED\n    HEREUNDER.\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator\nand subsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for\nthe purpose of contributing to a commons of creative, cultural and\nscientific works (\"Commons\") that the public can reliably and without fear\nof later claims of infringement build upon, modify, incorporate in other\nworks, reuse and redistribute as freely as possible in any form whatsoever\nand for any purposes, including without limitation commercial purposes.\nThese owners may contribute to the Commons to promote the ideal of a free\nculture and the further production of creative, cultural and scientific\nworks, or to gain reputation or greater distribution for their Work in\npart through the use and efforts of others.\n\nFor these and/or other purposes and motivations, and without any\nexpectation of additional consideration or compensation, the person\nassociating CC0 with a Work (the \"Affirmer\"), to the extent that he or she\nis an owner of Copyright and Related Rights in the Work, voluntarily\nelects to apply CC0 to the Work and publicly distribute the Work under its\nterms, with knowledge of his or her Copyright and Related Rights in the\nWork and the meaning and intended legal effect of CC0 on those rights.\n\n1. Copyright and Related Rights. A Work made available under CC0 may be\nprotected by copyright and related or neighboring rights (\"Copyright and\nRelated Rights\"). Copyright and Related Rights include, but are not\nlimited to, the following:\n\n  i. the right to reproduce, adapt, distribute, perform, display,\n     communicate, and translate a Work;\n ii. moral rights retained by the original author(s) and/or performer(s);\niii. publicity and privacy rights pertaining to a person's image or\n     likeness depicted in a Work;\n iv. rights protecting against unfair competition in regards to a Work,\n     subject to the limitations in paragraph 4(a), below;\n  v. rights protecting the extraction, dissemination, use and reuse of data\n     in a Work;\n vi. database rights (such as those arising under Directive 96/9/EC of the\n     European Parliament and of the Council of 11 March 1996 on the legal\n     protection of databases, and under any national implementation\n     thereof, including any amended or successor version of such\n     directive); and\nvii. other similar, equivalent or corresponding rights throughout the\n     world based on applicable law or treaty, and any national\n     implementations thereof.\n\n2. Waiver. To the greatest extent permitted by, but not in contravention\nof, applicable law, Affirmer hereby overtly, fully, permanently,\nirrevocably and unconditionally waives, abandons, and surrenders all of\nAffirmer's Copyright and Related Rights and associated claims and causes\nof action, whether now known or unknown (including existing as well as\nfuture claims and causes of action), in the Work (i) in all territories\nworldwide, (ii) for the maximum duration provided by applicable law or\ntreaty (including future time extensions), (iii) in any current or future\nmedium and for any number of copies, and (iv) for any purpose whatsoever,\nincluding without limitation commercial, advertising or promotional\npurposes (the \"Waiver\"). Affirmer makes the Waiver for the benefit of each\nmember of the public at large and to the detriment of Affirmer's heirs and\nsuccessors, fully intending that such Waiver shall not be subject to\nrevocation, rescission, cancellation, termination, or any other legal or\nequitable action to disrupt the quiet enjoyment of the Work by the public\nas contemplated by Affirmer's express Statement of Purpose.\n\n3. Public License Fallback. Should any part of the Waiver for any reason\nbe judged legally invalid or ineffective under applicable law, then the\nWaiver shall be preserved to the maximum extent permitted taking into\naccount Affirmer's express Statement of Purpose. In addition, to the\nextent the Waiver is so judged Affirmer hereby grants to each affected\nperson a royalty-free, non transferable, non sublicensable, non exclusive,\nirrevocable and unconditional license to exercise Affirmer's Copyright and\nRelated Rights in the Work (i) in all territories worldwide, (ii) for the\nmaximum duration provided by applicable law or treaty (including future\ntime extensions), (iii) in any current or future medium and for any number\nof copies, and (iv) for any purpose whatsoever, including without\nlimitation commercial, advertising or promotional purposes (the\n\"License\"). The License shall be deemed effective as of the date CC0 was\napplied by Affirmer to the Work. Should any part of the License for any\nreason be judged legally invalid or ineffective under applicable law, such\npartial invalidity or ineffectiveness shall not invalidate the remainder\nof the License, and in such case Affirmer hereby affirms that he or she\nwill not (i) exercise any of his or her remaining Copyright and Related\nRights in the Work or (ii) assert any associated claims and causes of\naction with respect to the Work, in either case contrary to Affirmer's\nexpress Statement of Purpose.\n\n4. Limitations and Disclaimers.\n\n a. No trademark or patent rights held by Affirmer are waived, abandoned,\n    surrendered, licensed or otherwise affected by this document.\n b. Affirmer offers the Work as-is and makes no representations or\n    warranties of any kind concerning the Work, express, implied,\n    statutory or otherwise, including without limitation warranties of\n    title, merchantability, fitness for a particular purpose, non\n    infringement, or the absence of latent or other defects, accuracy, or\n    the present or absence of errors, whether or not discoverable, all to\n    the greatest extent permissible under applicable law.\n c. Affirmer disclaims responsibility for clearing rights of other persons\n    that may apply to the Work or any use thereof, including without\n    limitation any person's Copyright and Related Rights in the Work.\n    Further, Affirmer disclaims responsibility for obtaining any necessary\n    consents, permissions or other rights required for any use of the\n    Work.\n d. Affirmer understands and acknowledges that Creative Commons is not a\n    party to this document and has no duty or obligation with respect to\n    this CC0 or use of the Work.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=right>Table of Contents↗️</div>\n\n<h1 align=center><code>just</code></h1>\n\n<div align=center>\n  <a href=https://crates.io/crates/just>\n    <img src=https://img.shields.io/crates/v/just.svg alt=\"crates.io version\">\n  </a>\n  <a href=https://github.com/casey/just/actions/workflows/ci.yaml>\n    <img src=https://github.com/casey/just/actions/workflows/ci.yaml/badge.svg alt=\"build status\">\n  </a>\n  <a href=https://github.com/casey/just/releases>\n    <img src=https://img.shields.io/github/downloads/casey/just/total.svg alt=downloads>\n  </a>\n  <a href=https://discord.gg/ezYScXR>\n    <img src=https://img.shields.io/discord/695580069837406228?logo=discord alt=\"chat on discord\">\n  </a>\n  <a href=mailto:casey@rodarmor.com?subject=Thanks%20for%20Just!>\n    <img src=https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg alt=\"say thanks\">\n  </a>\n</div>\n<br>\n\n`just` is a handy way to save and run project-specific commands.\n\nThis readme is also available as a [book](https://just.systems/man/en/). The\nbook reflects the latest release, whereas the\n[readme on GitHub](https://github.com/casey/just/blob/master/README.md)\nreflects latest master.\n\n(中文文档在 [这里](https://github.com/casey/just/blob/master/README.中文.md),\n快看过来!)\n\nCommands, called recipes, are stored in a file called `justfile` with syntax\ninspired by `make`:\n\n![screenshot](https://raw.githubusercontent.com/casey/just/master/screenshot.png)\n\nYou can then run them with `just RECIPE`:\n\n```console\n$ just test-all\ncc *.c -o main\n./test --all\nYay, all your tests passed!\n```\n\n`just` has a ton of useful features, and many improvements over `make`:\n\n- `just` is a command runner, not a build system, so it avoids much of\n  [`make`'s complexity and idiosyncrasies](#what-are-the-idiosyncrasies-of-make-that-just-avoids).\n  No need for `.PHONY` recipes!\n\n- Linux, MacOS, Windows, and other reasonable unices are supported with no\n  additional dependencies. (Although if your system doesn't have an `sh`,\n  you'll need to [choose a different shell](#shell).)\n\n- Errors are specific and informative, and syntax errors are reported along\n  with their source context.\n\n- Recipes can accept [command line arguments](#recipe-parameters).\n\n- Wherever possible, errors are resolved statically. Unknown recipes and\n  circular dependencies are reported before anything runs.\n\n- `just` [loads `.env` files](#dotenv-settings), making it easy to populate\n  environment variables.\n\n- Recipes can be [listed from the command line](#listing-available-recipes).\n\n- Command line completion scripts are\n  [available for most popular shells](#shell-completion-scripts).\n\n- Recipes can be written in\n  [arbitrary languages](#shebang-recipes), like Python or NodeJS.\n\n- `just` can be invoked from any subdirectory, not just the directory that\n  contains the `justfile`.\n\n- And [much more](https://just.systems/man/en/)!\n\nIf you need help with `just` please feel free to open an issue or ping me on\n[Discord](https://discord.gg/ezYScXR). Feature requests and bug reports are\nalways welcome!\n\nInstallation\n------------\n\n### Prerequisites\n\n`just` should run on any system with a reasonable `sh`, including Linux, MacOS,\nand the BSDs.\n\n#### Windows\n\nOn Windows, `just` works with the `sh` provided by\n[Git for Windows](https://git-scm.com),\n[GitHub Desktop](https://desktop.github.com), or\n[Cygwin](http://www.cygwin.com). After installation, `sh` must be available in\nthe `PATH` of the shell you want to invoke `just` from.\n\nIf you'd rather not install `sh`, you can use the `shell` setting to use the\nshell of your choice.\n\nLike PowerShell:\n\n```just\n# use PowerShell instead of sh:\nset shell := [\"powershell.exe\", \"-c\"]\n\nhello:\n  Write-Host \"Hello, world!\"\n```\n\n…or `cmd.exe`:\n\n```just\n# use cmd.exe instead of sh:\nset shell := [\"cmd.exe\", \"/c\"]\n\nlist:\n  dir\n```\n\nYou can also set the shell using command-line arguments. For example, to use\nPowerShell, launch `just` with `--shell powershell.exe --shell-arg -c`.\n\n(PowerShell is installed by default on Windows 7 SP1 and Windows Server 2008 R2\nS1 and later, and `cmd.exe` is quite fiddly, so PowerShell is recommended for\nmost Windows users.)\n\n### Packages\n\n#### Cross-platform\n\n<table>\n  <thead>\n    <tr>\n      <th>Package Manager</th>\n      <th>Package</th>\n      <th>Command</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><a href=https://github.com/alexellis/arkade>arkade</a></td>\n      <td>just</td>\n      <td><code>arkade get just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://asdf-vm.com>asdf</a></td>\n      <td><a href=https://github.com/olofvndrhr/asdf-just>just</a></td>\n      <td>\n        <code>asdf plugin add just</code><br>\n        <code>asdf install just &lt;version&gt;</code>\n      </td>\n    </tr>\n    <tr>\n      <td><a href=https://www.rust-lang.org>Cargo</a></td>\n      <td><a href=https://crates.io/crates/just>just</a></td>\n      <td><code>cargo install just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://docs.conda.io/projects/conda/en/latest/index.html>Conda</a></td>\n      <td><a href=https://anaconda.org/conda-forge/just>just</a></td>\n      <td><code>conda install -c conda-forge just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://brew.sh>Homebrew</a></td>\n      <td><a href=https://formulae.brew.sh/formula/just>just</a></td>\n      <td><code>brew install just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://nixos.org/nix/>Nix</a></td>\n      <td><a href=https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/just/package.nix>just</a></td>\n      <td><code>nix-env -iA nixpkgs.just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://www.npmjs.com/>npm</a></td>\n      <td><a href=https://www.npmjs.com/package/rust-just>rust-just</a></td>\n      <td><code>npm install -g rust-just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://pipx.pypa.io/stable/>pipx</a></td>\n      <td><a href=https://pypi.org/project/rust-just/>rust-just</a></td>\n      <td><code>pipx install rust-just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://snapcraft.io>Snap</a></td>\n      <td><a href=https://snapcraft.io/just>just</a></td>\n      <td><code>snap install --edge --classic just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://docs.astral.sh/uv/>uv</a></td>\n      <td><a href=https://pypi.org/project/rust-just/>rust-just</a></td>\n      <td><code>uv tool install rust-just</code></td>\n    </tr>\n  </tbody>\n</table>\n\n#### BSD\n\n<table>\n  <thead>\n    <tr>\n      <th>Operating System</th>\n      <th>Package Manager</th>\n      <th>Package</th>\n      <th>Command</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><a href=https://www.freebsd.org>FreeBSD</a></td>\n      <td><a href=https://www.freebsd.org/doc/handbook/pkgng-intro.html>pkg</a></td>\n      <td><a href=https://www.freshports.org/deskutils/just/>just</a></td>\n      <td><code>pkg install just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://www.openbsd.org>OpenBSD</a></td>\n      <td><a href=https://www.openbsd.org/faq/faq15.html>pkg_*</a></td>\n      <td><a href=https://cvsweb.openbsd.org/cgi-bin/cvsweb/ports/sysutils/just>just</a></td>\n      <td><code>pkg_add just</code></td>\n    </tr>\n  </tbody>\n</table>\n\n#### Linux\n\n<table>\n  <thead>\n    <tr>\n      <th>Operating System</th>\n      <th>Package Manager</th>\n      <th>Package</th>\n      <th>Command</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><a href=https://alpinelinux.org>Alpine</a></td>\n      <td><a href=https://wiki.alpinelinux.org/wiki/Alpine_Linux_package_management>apk-tools</a></td>\n      <td><a href=https://pkgs.alpinelinux.org/package/edge/community/x86_64/just>just</a></td>\n      <td><code>apk add just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://www.archlinux.org>Arch</a></td>\n      <td><a href=https://wiki.archlinux.org/title/Pacman>pacman</a></td>\n      <td><a href=https://archlinux.org/packages/extra/x86_64/just/>just</a></td>\n      <td><code>pacman -S just</code></td>\n    </tr>\n    <tr>\n      <td>\n        <a href=https://debian.org>Debian 13</a> and\n        <a href=https://ubuntu.com>Ubuntu 24.04</a> derivatives</td>\n      <td><a href=https://en.wikipedia.org/wiki/APT_(software)>apt</a></td>\n      <td><a href=https://packages.debian.org/trixie/just>just</a></td>\n      <td><code>apt install just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://getfedora.org>Fedora</a></td>\n      <td><a href=https://dnf.readthedocs.io/en/latest/>DNF</a></td>\n      <td><a href=https://src.fedoraproject.org/rpms/rust-just>just</a></td>\n      <td><code>dnf install just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://www.gentoo.org>Gentoo</a></td>\n      <td><a href=https://wiki.gentoo.org/wiki/Portage>Portage</a></td>\n      <td><a href=https://packages.gentoo.org/packages/dev-build/just>dev-build/just</a></td>\n      <td>\n        <code>emerge -av dev-build/just</code>\n      </td>\n    </tr>\n    <tr>\n      <td><a href=https://nixos.org/nixos/>NixOS</a></td>\n      <td><a href=https://nixos.org/nix/>Nix</a></td>\n      <td><a href=https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/just/package.nix>just</a></td>\n      <td><code>nix-env -iA nixos.just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://opensuse.org>openSUSE</a></td>\n      <td><a href=https://en.opensuse.org/Portal:Zypper>Zypper</a></td>\n      <td><a href=https://build.opensuse.org/package/show/Base:System/just>just</a></td>\n      <td><code>zypper in just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://getsol.us>Solus</a></td>\n      <td><a href=https://getsol.us/articles/package-management/basics/en>eopkg</a></td>\n      <td><a href=https://dev.getsol.us/source/just/>just</a></td>\n      <td><code>eopkg install just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://voidlinux.org>Void</a></td>\n      <td><a href=https://wiki.voidlinux.org/XBPS>XBPS</a></td>\n      <td><a href=https://github.com/void-linux/void-packages/blob/master/srcpkgs/just/template>just</a></td>\n      <td><code>xbps-install -S just</code></td>\n    </tr>\n  </tbody>\n</table>\n\n#### Windows\n\n<table>\n  <thead>\n    <tr>\n      <th>Package Manager</th>\n      <th>Package</th>\n      <th>Command</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><a href=https://chocolatey.org>Chocolatey</a></td>\n      <td><a href=https://github.com/michidk/just-choco>just</a></td>\n      <td><code>choco install just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://scoop.sh>Scoop</a></td>\n      <td><a href=https://github.com/ScoopInstaller/Main/blob/master/bucket/just.json>just</a></td>\n      <td><code>scoop install just</code></td>\n    </tr>\n    <tr>\n      <td><a href=https://learn.microsoft.com/en-us/windows/package-manager/>Windows Package Manager</a></td>\n      <td><a href=https://github.com/microsoft/winget-pkgs/tree/master/manifests/c/Casey/Just>Casey/Just</a></td>\n      <td><code>winget install --id Casey.Just --exact</code></td>\n    </tr>\n  </tbody>\n</table>\n\n#### macOS\n\n<table>\n  <thead>\n    <tr>\n      <th>Package Manager</th>\n      <th>Package</th>\n      <th>Command</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><a href=https://www.macports.org>MacPorts</a></td>\n      <td><a href=https://ports.macports.org/port/just/summary>just</a></td>\n      <td><code>port install just</code></td>\n    </tr>\n  </tbody>\n</table>\n\n![just package version table](https://repology.org/badge/vertical-allrepos/just.svg)\n\n### Pre-Built Binaries\n\nPre-built binaries for Linux, MacOS, and Windows can be found on\n[the releases page](https://github.com/casey/just/releases).\n\nYou can use the following command on Linux, MacOS, or Windows to download the\nlatest release, just replace `DEST` with the directory where you'd like to put\n`just`:\n\n```console\ncurl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to DEST\n```\n\nFor example, to install `just` to `~/bin`:\n\n```console\n# create ~/bin\nmkdir -p ~/bin\n\n# download and extract just to ~/bin/just\ncurl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/bin\n\n# add `~/bin` to the paths that your shell searches for executables\n# this line should be added to your shells initialization file,\n# e.g. `~/.bashrc` or `~/.zshrc`\nexport PATH=\"$PATH:$HOME/bin\"\n\n# just should now be executable\njust --help\n```\n\nNote that `install.sh` may fail on GitHub Actions, or in other environments\nwhere many machines share IP addresses. `install.sh` calls GitHub APIs in order\nto determine the latest version of `just` to install, and those API calls are\nrate-limited on a per-IP basis. To make `install.sh` more reliable in such\ncircumstances, pass a specific tag to install with `--tag`.\n\nAnother way to avoid rate-limiting is to pass a GitHub authentication token to\n`install.sh` as an environment variable named `GITHUB_TOKEN`, allowing it to\nauthenticate its requests.\n\n[Releases](https://github.com/casey/just/releases) include a `SHA256SUM` file\nwhich can be used to verify the integrity of pre-built binary archives.\n\nTo verify a release, download the pre-built binary archive along with the\n`SHA256SUM` file and run:\n\n```sh\nshasum --algorithm 256 --ignore-missing --check SHA256SUMS\n```\n\n### GitHub Actions\n\n`just` can be installed on GitHub Actions in a few ways.\n\nUsing package managers pre-installed on GitHub Actions runners on MacOS with\n`brew install just`, and on Windows with `choco install just`.\n\nWith [extractions/setup-just](https://github.com/extractions/setup-just):\n\n```yaml\n- uses: extractions/setup-just@v3\n  with:\n    just-version: 1.5.0  # optional semver specification, otherwise latest\n```\n\nOr with [taiki-e/install-action](https://github.com/taiki-e/install-action):\n\n```yaml\n- uses: taiki-e/install-action@just\n```\n\n### Release RSS Feed\n\nAn [RSS feed](https://en.wikipedia.org/wiki/RSS) of `just` releases is available [here](https://github.com/casey/just/releases.atom).\n\n### Node.js Installation\n\n[just-install](https://npmjs.com/package/just-install) can be used to automate\ninstallation of `just` in Node.js applications.\n\n`just` is a great, more robust alternative to npm scripts. If you want to\ninclude `just` in the dependencies of a Node.js application, `just-install`\nwill install a local, platform-specific binary as part of the `npm install`\ncommand. This removes the need for every developer to install `just`\nindependently using one of the processes mentioned above. After installation,\nthe `just` command will work in npm scripts or with npx. It's great for teams\nwho want to make the set up process for their project as easy as possible.\n\nFor more information, see the\n[just-install README file](https://github.com/brombal/just-install#readme).\n\nBackwards Compatibility\n-----------------------\n\nWith the release of version 1.0, `just` features a strong commitment to\nbackwards compatibility and stability.\n\nFuture releases will not introduce backwards incompatible changes that make\nexisting `justfile`s stop working, or break working invocations of the\ncommand-line interface.\n\nThis does not, however, preclude fixing outright bugs, even if doing so might\nbreak `justfiles` that rely on their behavior.\n\nThere will never be a `just` 2.0. Any desirable backwards-incompatible changes\nwill be opt-in on a per-`justfile` basis, so users may migrate at their\nleisure.\n\nFeatures that aren't yet ready for stabilization are marked as unstable and may\nbe changed or removed at any time. Using unstable features produces an error by\ndefault, which can be suppressed with by passing the `--unstable` flag,\n`set unstable`, or setting the environment variable `JUST_UNSTABLE`, to any\nvalue other than `false`, `0`, or the empty string.\n\nEditor Support\n--------------\n\n`justfile` syntax is close enough to `make` that you may want to tell your\neditor to use `make` syntax highlighting for `just`.\n\n### Vim and Neovim\n\nVim version 9.1.1042 or better and Neovim version 0.11 or better support\nJustfile syntax highlighting out of the box, thanks to\n[pbnj](https://github.com/pbnj).\n\n#### `vim-just`\n\nThe [vim-just](https://github.com/NoahTheDuke/vim-just) plugin provides syntax\nhighlighting for `justfile`s.\n\nInstall it with your favorite package manager, like\n[Plug](https://github.com/junegunn/vim-plug):\n\n```vim\ncall plug#begin()\n\nPlug 'NoahTheDuke/vim-just'\n\ncall plug#end()\n```\n\nOr with Vim's built-in package support:\n\n```console\nmkdir -p ~/.vim/pack/vendor/start\ncd ~/.vim/pack/vendor/start\ngit clone https://github.com/NoahTheDuke/vim-just.git\n```\n\n#### `tree-sitter-just`\n\n[tree-sitter-just](https://github.com/IndianBoy42/tree-sitter-just) is an\n[Nvim Treesitter](https://github.com/nvim-treesitter/nvim-treesitter) plugin\nfor Neovim.\n\n#### Makefile Syntax Highlighting\n\nVim's built-in makefile syntax highlighting isn't perfect for `justfile`s, but\nit's better than nothing. You can put the following in `~/.vim/filetype.vim`:\n\n```vimscript\nif exists(\"did_load_filetypes\")\n  finish\nendif\n\naugroup filetypedetect\n  au BufNewFile,BufRead justfile setf make\naugroup END\n```\n\nOr add the following to an individual `justfile` to enable `make` mode on a\nper-file basis:\n\n```text\n# vim: set ft=make :\n```\n\n### Emacs\n\n[just-mode](https://github.com/leon-barrett/just-mode.el) provides syntax\nhighlighting and automatic indentation of `justfile`s. It is available on\n[MELPA](https://melpa.org/) as [just-mode](https://melpa.org/#/just-mode).\n\n[justl](https://github.com/psibi/justl.el) provides commands for executing and\nlisting recipes.\n\nYou can add the following to an individual `justfile` to enable `make` mode on\na per-file basis:\n\n```text\n# Local Variables:\n# mode: makefile\n# End:\n```\n\n### Visual Studio Code\n\nAn extension for VS Code is [available here](https://github.com/nefrob/vscode-just).\n\nUnmaintained VS Code extensions include\n[skellock/vscode-just](https://github.com/skellock/vscode-just) and\n[sclu1034/vscode-just](https://github.com/sclu1034/vscode-just).\n\n### JetBrains IDEs\n\nA plugin for JetBrains IDEs by [linux_china](https://github.com/linux-china) is\n[available here](https://plugins.jetbrains.com/plugin/18658-just).\n\n### Kakoune\n\nKakoune supports `justfile` syntax highlighting out of the box, thanks to\nTeddyDD.\n\n### Helix\n\n[Helix](https://helix-editor.com/) supports `justfile` syntax highlighting\nout-of-the-box since version 23.05.\n\n### Sublime Text\n\nThe [Just package](https://github.com/nk9/just_sublime) by\n[nk9](https://github.com/nk9) with `just` syntax and some other tools is\navailable on [PackageControl](https://packagecontrol.io/packages/Just).\n\n### Micro\n\n[Micro](https://micro-editor.github.io/) supports Justfile syntax highlighting\nout of the box, thanks to [tomodachi94](https://github.com/tomodachi94).\n\n### Zed\n\nThe [zed-just](https://github.com/jackTabsCode/zed-just/) extension by\n[jackTabsCode](https://github.com/jackTabsCode) is available on the\n[Zed extensions page](https://zed.dev/extensions?query=just).\n\n### Other Editors\n\nFeel free to send me the commands necessary to get syntax highlighting working\nin your editor of choice so that I may include them here.\n\n### Language Server Protocol\n\n[just-lsp](https://github.com/terror/just-lsp) provides a [language server\nprotocol](https://en.wikipedia.org/wiki/Language_Server_Protocol)\nimplementation, enabling features such as go-to-definition, inline diagnostics,\nand code completion.\n\n### Model Context Protocol\n\n[just-mcp](http://github.com/promptexecution/just-mcp) provides a\n[model context protocol](https://en.wikipedia.org/wiki/Model_Context_Protocol)\nadapter to allow LLMs to query the contents of `justfiles` and run recipes.\n\nQuick Start\n-----------\n\nSee the installation section for how to install `just` on your computer. Try\nrunning `just --version` to make sure that it's installed correctly.\n\nFor an overview of the syntax, check out\n[this cheatsheet](https://cheatography.com/linux-china/cheat-sheets/justfile/).\n\nOnce `just` is installed and working, create a file named `justfile` in the\nroot of your project with the following contents:\n\n```just\nrecipe-name:\n  echo 'This is a recipe!'\n\n# this is a comment\nanother-recipe:\n  @echo 'This is another recipe.'\n```\n\nWhen you invoke `just` it looks for file `justfile` in the current directory\nand upwards, so you can invoke it from any subdirectory of your project.\n\nThe search for a `justfile` is case insensitive, so any case, like `Justfile`,\n`JUSTFILE`, or `JuStFiLe`, will work. `just` will also look for files with the\nname `.justfile`, in case you'd like to hide a `justfile`.\n\nRunning `just` with no arguments runs the first recipe in the `justfile`:\n\n```console\n$ just\necho 'This is a recipe!'\nThis is a recipe!\n```\n\nOne or more arguments specify the recipe(s) to run:\n\n```console\n$ just another-recipe\nThis is another recipe.\n```\n\n`just` prints each command to standard error before running it, which is why\n`echo 'This is a recipe!'` was printed. This is suppressed for lines starting\nwith `@`, which is why `echo 'This is another recipe.'` was not printed.\n\nRecipes stop running if a command fails. Here `cargo publish` will only run if\n`cargo test` succeeds:\n\n```just\npublish:\n  cargo test\n  # tests passed, time to publish!\n  cargo publish\n```\n\nRecipes can depend on other recipes. Here the `test` recipe depends on the\n`build` recipe, so `build` will run before `test`:\n\n```just\nbuild:\n  cc main.c foo.c bar.c -o main\n\ntest: build\n  ./test\n\nsloc:\n  @echo \"`wc -l *.c` lines of code\"\n```\n\n```console\n$ just test\ncc main.c foo.c bar.c -o main\n./test\ntesting… all tests passed!\n```\n\nRecipes without dependencies will run in the order they're given on the command\nline:\n\n```console\n$ just build sloc\ncc main.c foo.c bar.c -o main\n1337 lines of code\n```\n\nDependencies will always run first, even if they are passed after a recipe that\ndepends on them:\n\n```console\n$ just test build\ncc main.c foo.c bar.c -o main\n./test\ntesting… all tests passed!\n```\n\nRecipes may depend on recipes in submodules:\n\n```justfile\nmod foo\n\nbaz: foo::bar\n```\n\nExamples\n--------\n\nA variety of `justfile`s can be found in the\n[examples directory](https://github.com/casey/just/tree/master/examples) and on\n[GitHub](https://github.com/search?q=path%3A**%2Fjustfile&type=code).\n\nFeatures\n--------\n\n### The Default Recipe\n\nWhen `just` is invoked without a recipe, it runs the recipe with the\n`[default]` attribute, or the first recipe in the `justfile` if no recipe has\nthe `[default]` attribute.\n\nThis recipe might be the most frequently run command in the project, like\nrunning the tests:\n\n```just\ntest:\n  cargo test\n```\n\nYou can also use dependencies to run multiple recipes by default:\n\n```just\ndefault: lint build test\n\nbuild:\n  echo Building…\n\ntest:\n  echo Testing…\n\nlint:\n  echo Linting…\n```\n\nIf no recipe makes sense as the default recipe, you can add a recipe to the\nbeginning of your `justfile` that lists the available recipes:\n\n```just\ndefault:\n  just --list\n```\n\n### Listing Available Recipes\n\nRecipes can be listed in alphabetical order with `just --list`:\n\n```console\n$ just --list\nAvailable recipes:\n    build\n    test\n    deploy\n    lint\n```\n\nRecipes in [submodules](#modules) can be listed with `just --list PATH`, where\n`PATH` is a space- or `::`-separated module path:\n\n```\n$ cat justfile\nmod foo\n$ cat foo.just\nmod bar\n$ cat bar.just\nbaz:\n$ just --list foo bar\nAvailable recipes:\n    baz\n$ just --list foo::bar\nAvailable recipes:\n    baz\n```\n\n`just --summary` is more concise:\n\n```console\n$ just --summary\nbuild test deploy lint\n```\n\nPass `--unsorted` to print recipes in the order they appear in the `justfile`:\n\n```just\ntest:\n  echo 'Testing!'\n\nbuild:\n  echo 'Building!'\n```\n\n```console\n$ just --list --unsorted\nAvailable recipes:\n    test\n    build\n```\n\n```console\n$ just --summary --unsorted\ntest build\n```\n\nIf you'd like `just` to default to listing the recipes in the `justfile`, you\ncan use this as your default recipe:\n\n```just\ndefault:\n  @just --list\n```\n\nNote that you may need to add `--justfile {{justfile()}}` to the line above.\nWithout it, if you executed `just -f /some/distant/justfile -d .` or\n`just -f ./non-standard-justfile`, the plain `just --list` inside the recipe\nwould not necessarily use the file you provided. It would try to find a\njustfile in your current path, maybe even resulting in a `No justfile found`\nerror.\n\nThe heading text can be customized with `--list-heading`:\n\n```console\n$ just --list --list-heading $'Cool stuff…\\n'\nCool stuff…\n    test\n    build\n```\n\nAnd the indentation can be customized with `--list-prefix`:\n\n```console\n$ just --list --list-prefix ····\nAvailable recipes:\n····test\n····build\n```\n\nThe argument to `--list-heading` replaces both the heading and the newline\nfollowing it, so it should contain a newline if non-empty. It works this way so\nyou can suppress the heading line entirely by passing the empty string:\n\n```console\n$ just --list --list-heading ''\n    test\n    build\n```\n\n### Invoking Multiple Recipes\n\nMultiple recipes may be invoked on the command line at once:\n\n```just\nbuild:\n  make web\n\nserve:\n  python3 -m http.server -d out 8000\n```\n\n```console\n$ just build serve\nmake web\npython3 -m http.server -d out 8000\n```\n\nKeep in mind that recipes with parameters will swallow arguments, even if they\nmatch the names of other recipes:\n\n```just\nbuild project:\n  make {{project}}\n\nserve:\n  python3 -m http.server -d out 8000\n```\n\n```console\n$ just build serve\nmake: *** No rule to make target `serve'.  Stop.\n```\n\nThe `--one` flag can be used to restrict command-line invocations to a single\nrecipe:\n\n```console\n$ just --one build serve\nerror: Expected 1 command-line recipe invocation but found 2.\n```\n\n### Working Directory\n\nBy default, recipes run with the working directory set to the directory that\ncontains the `justfile`.\n\nThe `[no-cd]` attribute can be used to make recipes run with the working\ndirectory set to directory in which `just` was invoked.\n\n```just\n@foo:\n  pwd\n\n[no-cd]\n@bar:\n  pwd\n```\n\n```console\n$ cd subdir\n$ just foo\n/\n$ just bar\n/subdir\n```\n\nYou can override the working directory for all recipes with\n`set working-directory := '…'`:\n\n```just\nset working-directory := 'bar'\n\n@foo:\n  pwd\n```\n\n```console\n$ pwd\n/home/bob\n$ just foo\n/home/bob/bar\n```\n\nYou can override the working directory for a specific recipe with the\n`working-directory` attribute<sup>1.38.0</sup>:\n\n```just\n[working-directory: 'bar']\n@foo:\n  pwd\n```\n\n```console\n$ pwd\n/home/bob\n$ just foo\n/home/bob/bar\n```\n\nThe argument to the `working-directory` setting or `working-directory`\nattribute may be absolute or relative. If it is relative it is interpreted\nrelative to the default working directory.\n\n### Aliases\n\nAliases allow recipes to be invoked on the command line with alternative names:\n\n```just\nalias b := build\n\nbuild:\n  echo 'Building!'\n```\n\n```console\n$ just b\necho 'Building!'\nBuilding!\n```\n\nThe target of an alias may be a recipe in a submodule:\n\n```justfile\nmod foo\n\nalias baz := foo::bar\n```\n\n### Settings\n\nSettings control interpretation and execution. Each setting may be specified at\nmost once, anywhere in the `justfile`.\n\nFor example:\n\n```just\nset shell := [\"zsh\", \"-cu\"]\n\nfoo:\n  # this line will be run as `zsh -cu 'ls **/*.txt'`\n  ls **/*.txt\n```\n\n#### Table of Settings\n\n| Name | Value | Default | Description |\n|------|-------|---------|-------------|\n| `allow-duplicate-recipes` | boolean | `false` | Allow recipes appearing later in a `justfile` to override earlier recipes with the same name. |\n| `allow-duplicate-variables` | boolean | `false` | Allow variables appearing later in a `justfile` to override earlier variables with the same name. |\n| `dotenv-filename` | string | - | Load a `.env` file with a custom name, if present. |\n| `dotenv-load` | boolean | `false` | Load a `.env` file, if present. |\n| `dotenv-override` | boolean | `false` | Override existing environment variables with values from the `.env` file. |\n| `dotenv-path` | string | - | Load a `.env` file from a custom path and error if not present. Overrides `dotenv-filename`. |\n| `dotenv-required` | boolean | `false` | Error if a `.env` file isn't found. |\n| `export` | boolean | `false` | Export all variables as environment variables. |\n| `fallback` | boolean | `false` | Search `justfile` in parent directory if the first recipe on the command line is not found. |\n| `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. |\n| `lazy`<sup>1.47.0</sup> | boolean | `false` | Don't evaluate unused variables. |\n| `positional-arguments` | boolean | `false` | Pass positional arguments. |\n| `quiet` | boolean | `false` | Disable echoing recipe lines before executing. |\n| `script-interpreter`<sup>1.33.0</sup> | `[COMMAND, ARGS…]` | `['sh', '-eu']` | Set command used to invoke recipes with empty `[script]` attribute. |\n| `shell` | `[COMMAND, ARGS…]` | - | Set command used to invoke recipes and evaluate backticks. |\n| `tempdir` | string | - | Create temporary directories in `tempdir` instead of the system default temporary directory. |\n| `unstable`<sup>1.31.0</sup> | boolean | `false` | Enable unstable features. |\n| `windows-powershell` | boolean | `false` | Use PowerShell on Windows as default shell. (Deprecated. Use `windows-shell` instead. |\n| `windows-shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. |\n| `working-directory`<sup>1.33.0</sup> | string | - | Set the working directory for recipes and backticks, relative to the default working directory. |\n\nBoolean settings can be written as:\n\n```justfile\nset NAME\n```\n\nWhich is equivalent to:\n\n```justfile\nset NAME := true\n```\n\nNon-boolean settings can be set to both strings and\nexpressions.<sup>1.46.0</sup>\n\nHowever, because settings affect the behavior of backticks and many functions,\nthose expressions may not contain backticks or function calls, directly or\ntransitively via reference.\n\n#### Allow Duplicate Recipes\n\nIf `allow-duplicate-recipes` is set to `true`, defining multiple recipes with\nthe same name is not an error and the last definition is used. Defaults to\n`false`.\n\n```just\nset allow-duplicate-recipes\n\n@foo:\n  echo foo\n\n@foo:\n  echo bar\n```\n\n```console\n$ just foo\nbar\n```\n\n#### Allow Duplicate Variables\n\nIf `allow-duplicate-variables` is set to `true`, defining multiple variables\nwith the same name is not an error and the last definition is used. Defaults to\n`false`.\n\n```just\nset allow-duplicate-variables\n\na := \"foo\"\na := \"bar\"\n\n@foo:\n  echo {{a}}\n```\n\n```console\n$ just foo\nbar\n```\n\n#### Dotenv Settings\n\nIf any of `dotenv-load`, `dotenv-filename`, `dotenv-override`, `dotenv-path`,\nor `dotenv-required` are set, `just` will try to load environment variables\nfrom a file.\n\nIf `dotenv-path` is set, `just` will look for a file at the given path, which\nmay be absolute, or relative to the working directory.\n\nThe command-line option `--dotenv-path`, short form `-E`, can be used to set or\noverride `dotenv-path` at runtime.\n\nIf `dotenv-filename` is set `just` will look for a file at the given path,\nrelative to the working directory and each of its ancestors.\n\nIf `dotenv-filename` is not set, but `dotenv-load` or `dotenv-required` are\nset, just will look for a file named `.env`, relative to the working directory\nand each of its ancestors.\n\n`dotenv-filename` and `dotenv-path` are similar, but `dotenv-path` is only\nchecked relative to the working directory, whereas `dotenv-filename` is checked\nrelative to the working directory and each of its ancestors.\n\nIt is not an error if an environment file is not found, unless\n`dotenv-required` is set.\n\nThe loaded variables are environment variables, not `just` variables, and so\nmust be accessed using `$VARIABLE_NAME` in recipes and backticks.\n\nIf `dotenv-override` is set, variables from the environment file will override\nexisting environment variables.\n\nFor example, if your `.env` file contains:\n\n```console\n# a comment, will be ignored\nDATABASE_ADDRESS=localhost:6379\nSERVER_PORT=1337\n```\n\nAnd your `justfile` contains:\n\n```just\nset dotenv-load\n\nserve:\n  @echo \"Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…\"\n  ./server --database $DATABASE_ADDRESS --port $SERVER_PORT\n```\n\n`just serve` will output:\n\n```console\n$ just serve\nStarting server with database localhost:6379 on port 1337…\n./server --database $DATABASE_ADDRESS --port $SERVER_PORT\n```\n\n#### Export\n\nThe `export` setting causes all `just` variables to be exported as environment\nvariables. Defaults to `false`.\n\n```just\nset export\n\na := \"hello\"\n\n@foo b:\n  echo $a\n  echo $b\n```\n\n```console\n$ just foo goodbye\nhello\ngoodbye\n```\n\n#### Lazy\n\nThe `lazy` setting<sup>1.47.0</sup>, currently unstable, causes the evaluator\nto skip evaluating unused variables. This can be beneficial when a `justfile`\ncontains variables that are expensive to evaluate but only sometimes used.\n\nIn the following `justfile`, `token` will be skipped when only invoking `bar`:\n\n```just\nset lazy\nset unstable\n\ntoken := `expensive-script-to-get-credentials`\n\nfoo:\n  curl -H \"Authorization: Bearer {{ token }}\" https://example.com/foo\n\nbar:\n  cargo test\n```\n\nBecause `just` cannot determine when exported variables are used, assignments\nwith `export` and assignments in a module with `set export` will always be\nevaluated.\n\n#### Positional Arguments\n\nIf `positional-arguments` is `true`, recipe arguments will be passed as\npositional arguments to commands. For linewise recipes, argument `$0` will be\nthe name of the recipe.\n\nFor example, running this recipe:\n\n```just\nset positional-arguments\n\n@foo bar:\n  echo $0\n  echo $1\n```\n\nWill produce the following output:\n\n```console\n$ just foo hello\nfoo\nhello\n```\n\nWhen using an `sh`-compatible shell, such as `bash` or `zsh`, `$@` expands to\nthe positional arguments given to the recipe, starting from one. When used\nwithin double quotes as `\"$@\"`, arguments including whitespace will be passed\non as if they were double-quoted. That is, `\"$@\"` is equivalent to `\"$1\" \"$2\"`…\nWhen there are no positional parameters, `\"$@\"` and `$@` expand to nothing\n(i.e., they are removed).\n\nThis example recipe will print arguments one by one on separate lines:\n\n```just\nset positional-arguments\n\n@test *args='':\n  bash -c 'while (( \"$#\" )); do echo - $1; shift; done' -- \"$@\"\n```\n\nRunning it with _two_ arguments:\n\n```console\n$ just test foo \"bar baz\"\n- foo\n- bar baz\n```\n\nPositional arguments may also be turned on on a per-recipe basis with the\n`[positional-arguments]` attribute<sup>1.29.0</sup>:\n\n```just\n[positional-arguments]\n@foo bar:\n  echo $0\n  echo $1\n```\n\nNote that PowerShell does not handle positional arguments in the same way as\nother shells, so turning on positional arguments will likely break recipes that\nuse PowerShell.\n\nIf using PowerShell 7.4 or better, the `-CommandWithArgs` flag will make\npositional arguments work as expected:\n\n```just\nset shell := ['pwsh.exe', '-CommandWithArgs']\nset positional-arguments\n\nprint-args a b c:\n  Write-Output @($args[1..($args.Count - 1)])\n```\n\n#### Shell\n\nThe `shell` setting controls the command used to invoke recipe lines and\nbackticks. Shebang recipes are unaffected. The default shell is `sh -cu`.\n\n```just\n# use python3 to execute recipe lines and backticks\nset shell := [\"python3\", \"-c\"]\n\n# use print to capture result of evaluation\nfoos := `print(\"foo\" * 4)`\n\nfoo:\n  print(\"Snake snake snake snake.\")\n  print(\"{{foos}}\")\n```\n\n`just` passes the command to be executed as an argument. Many shells will need\nan additional flag, often `-c`, to make them evaluate the first argument.\n\n##### Windows Shell\n\n`just` uses `sh` on Windows by default. To use a different shell on Windows,\nuse `windows-shell`:\n\n```just\nset windows-shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"]\n\nhello:\n  Write-Host \"Hello, world!\"\n```\n\nSee\n[powershell.just](https://github.com/casey/just/blob/master/examples/powershell.just)\nfor a justfile that uses PowerShell on all platforms.\n\n##### Windows PowerShell\n\n*`set windows-powershell` uses the legacy `powershell.exe` binary, and is no\nlonger recommended. See the `windows-shell` setting above for a more flexible\nway to control which shell is used on Windows.*\n\n`just` uses `sh` on Windows by default. To use `powershell.exe` instead, set\n`windows-powershell` to true.\n\n```just\nset windows-powershell := true\n\nhello:\n  Write-Host \"Hello, world!\"\n```\n\n##### Python 3\n\n```just\nset shell := [\"python3\", \"-c\"]\n```\n\n##### Bash\n\n```just\nset shell := [\"bash\", \"-uc\"]\n```\n\n##### Z Shell\n\n```just\nset shell := [\"zsh\", \"-uc\"]\n```\n\n##### Fish\n\n```just\nset shell := [\"fish\", \"-c\"]\n```\n\n##### Nushell\n\n```just\nset shell := [\"nu\", \"-c\"]\n```\n\nIf you want to change the default table mode to `light`:\n\n```just\nset shell := ['nu', '-m', 'light', '-c']\n```\n\n*[Nushell](https://github.com/nushell/nushell) was written in Rust, and **has\ncross-platform support for Windows / macOS and Linux**.*\n\n### Documentation Comments\n\nComments immediately preceding a recipe will appear in `just --list`:\n\n```just\n# build stuff\nbuild:\n  ./bin/build\n\n# test stuff\ntest:\n  ./bin/test\n```\n\n```console\n$ just --list\nAvailable recipes:\n    build # build stuff\n    test # test stuff\n```\n\nThe `[doc]` attribute can be used to set or suppress a recipe's doc comment:\n\n```just\n# This comment won't appear\n[doc('Build stuff')]\nbuild:\n  ./bin/build\n\n# This one won't either\n[doc]\ntest:\n  ./bin/test\n```\n\n```console\n$ just --list\nAvailable recipes:\n    build # Build stuff\n    test\n```\n\n### Expressions and Substitutions\n\nVarious operators and function calls are supported in expressions, which may be\nused in assignments, default recipe arguments, and inside recipe body `{{…}}`\nsubstitutions.\n\n```just\ntmpdir  := `mktemp -d`\nversion := \"0.2.7\"\ntardir  := tmpdir / \"awesomesauce-\" + version\ntarball := tardir + \".tar.gz\"\nconfig  := quote(config_dir() / \".project-config\")\n\npublish:\n  rm -f {{tarball}}\n  mkdir {{tardir}}\n  cp README.md *.c {{ config }} {{tardir}}\n  tar zcvf {{tarball}} {{tardir}}\n  scp {{tarball}} me@server.com:release/\n  rm -rf {{tarball}} {{tardir}}\n```\n\n#### Concatenation\n\nThe `+` operator returns the left-hand argument concatenated with the\nright-hand argument:\n\n```just\nfoobar := 'foo' + 'bar'\n```\n\n#### Logical Operators\n\nThe logical operators `&&` and `||` can be used to coalesce string\nvalues<sup>1.37.0</sup>, similar to Python's `and` and `or`. These operators\nconsider the empty string `''` to be false, and all other strings to be true.\n\nThese operators are currently unstable.\n\nThe `&&` operator returns the empty string if the left-hand argument is the\nempty string, otherwise it returns the right-hand argument:\n\n```justfile\nfoo := '' && 'goodbye'      # ''\nbar := 'hello' && 'goodbye' # 'goodbye'\n```\n\nThe `||` operator returns the left-hand argument if it is non-empty, otherwise\nit returns the right-hand argument:\n\n```justfile\nfoo := '' || 'goodbye'      # 'goodbye'\nbar := 'hello' || 'goodbye' # 'hello'\n```\n\n#### Joining Paths\n\nThe `/` operator can be used to join two strings with a slash:\n\n```just\nfoo := \"a\" / \"b\"\n```\n\n```\n$ just --evaluate foo\na/b\n```\n\nNote that a `/` is added even if one is already present:\n\n```just\nfoo := \"a/\"\nbar := foo / \"b\"\n```\n\n```\n$ just --evaluate bar\na//b\n```\n\nAbsolute paths can also be constructed<sup>1.5.0</sup>:\n\n```just\nfoo := / \"b\"\n```\n\n```\n$ just --evaluate foo\n/b\n```\n\nThe `/` operator uses the `/` character, even on Windows. Thus, using the `/`\noperator should be avoided with paths that use universal naming convention\n(UNC), i.e., those that start with `\\?`, since forward slashes are not\nsupported with UNC paths.\n\n#### Escaping `{{`\n\nTo write a recipe containing `{{`, use `{{{{`:\n\n```just\nbraces:\n  echo 'I {{{{LOVE}} curly braces!'\n```\n\n(An unmatched `}}` is ignored, so it doesn't need to be escaped.)\n\nAnother option is to put all the text you'd like to escape inside of an\ninterpolation:\n\n```just\nbraces:\n  echo '{{'I {{LOVE}} curly braces!'}}'\n```\n\nYet another option is to use `{{ \"{{\" }}`:\n\n```just\nbraces:\n  echo 'I {{ \"{{\" }}LOVE}} curly braces!'\n```\n\n### Strings\n\n`'single'`, `\"double\"`, and `'''triple'''` quoted string literals are\nsupported. Unlike in recipe bodies, `{{…}}` interpolations are not supported\ninside strings.\n\nDouble-quoted strings support escape sequences:\n\n```just\ncarriage-return   := \"\\r\"\ndouble-quote      := \"\\\"\"\nnewline           := \"\\n\"\nno-newline        := \"\\\n\"\nslash             := \"\\\\\"\ntab               := \"\\t\"\nunicode-codepoint := \"\\u{1F916}\"\n```\n\n```console\n$ just --evaluate\n\"arriage-return   := \"\ndouble-quote      := \"\"\"\nnewline           := \"\n\"\nno-newline        := \"\"\nslash             := \"\\\"\ntab               := \"     \"\nunicode-codepoint := \"🤖\"\n```\n\nThe unicode character escape sequence `\\u{…}`<sup>1.36.0</sup> accepts up to\nsix hex digits.\n\nStrings may contain line breaks:\n\n```just\nsingle := '\nhello\n'\n\ndouble := \"\ngoodbye\n\"\n```\n\nSingle-quoted strings do not recognize escape sequences:\n\n```just\nescapes := '\\t\\n\\r\\\"\\\\'\n```\n\n```console\n$ just --evaluate\nescapes := \"\\t\\n\\r\\\"\\\\\"\n```\n\nIndented versions of both single- and double-quoted strings, delimited by\ntriple single- or double-quotes, are supported. Indented string lines are\nstripped of a leading line break, and leading whitespace common to all\nnon-blank lines:\n\n```just\n# this string will evaluate to `foo\\nbar\\n`\nx := '''\n  foo\n  bar\n'''\n\n# this string will evaluate to `abc\\n  wuv\\nxyz\\n`\ny := \"\"\"\n  abc\n    wuv\n  xyz\n\"\"\"\n```\n\nSimilar to unindented strings, indented double-quoted strings process escape\nsequences, and indented single-quoted strings ignore escape sequences. Escape\nsequence processing takes place after unindentation. The unindentation\nalgorithm does not take escape-sequence produced whitespace or newlines into\naccount.\n\n#### Shell-expanded strings\n\nStrings prefixed with `x` are shell expanded<sup>1.27.0</sup>:\n\n```justfile\nfoobar := x'~/$FOO/${BAR}'\n```\n\n| Value | Replacement |\n|------|-------------|\n| `$VAR` | value of environment variable `VAR` |\n| `${VAR}` | value of environment variable `VAR` |\n| `${VAR:-DEFAULT}` | value of environment variable `VAR`, or `DEFAULT` if `VAR` is not set |\n| Leading `~` | path to current user's home directory |\n| Leading `~USER` | path to `USER`'s home directory |\n\nThis expansion is performed at compile time, so variables from `.env` files and\nexported `just` variables cannot be used. However, this allows shell expanded\nstrings to be used in places like settings and import paths, which cannot\ndepend on `just` variables and `.env` files.\n\n#### Format strings\n\nStrings prefixed with `f` are format strings<sup>1.44.0</sup>:\n\n```justfile\nname := \"world\"\nmessage := f'Hello, {{name}}!'\n```\n\nFormat strings may contain interpolations delimited with `{{…}}` that contain\nexpressions. Format strings evaluate to the concatenated string fragments and\nevaluated expressions.\n\nUse `{{{{` to include a literal `{{` in a format string:\n\n```justfile\nfoo := f'I {{{{LOVE} curly braces!'\n```\n\n### Sigils\n\nCommands in linewise recipes may be prefixed with any combination of the sigils\n`-`, `@`, and `?`.\n\nThe `@` sigil toggles command echoing:\n\n```just\nfoo:\n  @echo \"This line won't be echoed!\"\n  echo \"This line will be echoed!\"\n\n@bar:\n  @echo \"This line will be echoed!\"\n  echo \"This line won't be echoed!\"\n```\n\nThe `-` sigil cause recipe execution to continue even if the command returns a\nnonzero exit status:\n\n```just\n# execution will continue, even if bar doesn't exist\nfoo:\n  -rmdir bar\n  mkdir bar\n  echo 'so much good stuff' > bar/stuff.txt\n```\n\nThe `?` sigil<sup>1.47.0</sup> causes the current recipe to stop executing if\nthe command exits with status code `1`, however execution of other recipes will\ncontinue. Exit status `0` causes the current recipe to continue execution as\nnormal. All other exit codes are reserved and should not be used, as they may\nbe given meaning in a future version of `just`.\n\nIf the `guards` setting is unset or false, `?` sigils are ignored and instead\ntreated as part of the command.\n\n```just\nset guards\n\n@foo: bar\n  echo FOO\n\n@bar:\n  ?[[ -f baz ]]\n  echo BAR\n```\n\n```console\n$ just foo\nFOO\n$ touch baz\n$ just foo\nBAR\nFOO\n```\n\n### Functions\n\n`just` provides many built-in functions for use in expressions, including\nrecipe body `{{…}}` substitutions, assignments, and default parameter values.\n\nAll functions ending in `_directory` can be abbreviated to `_dir`. So\n`home_directory()` can also be written as `home_dir()`. In addition,\n`invocation_directory_native()` can be abbreviated to\n`invocation_dir_native()`.\n\n#### System Information\n\n- `arch()` — Instruction set architecture. Possible values are: `\"aarch64\"`,\n  `\"arm\"`, `\"asmjs\"`, `\"hexagon\"`, `\"mips\"`, `\"msp430\"`, `\"powerpc\"`,\n  `\"powerpc64\"`, `\"s390x\"`, `\"sparc\"`, `\"wasm32\"`, `\"x86\"`, `\"x86_64\"`, and\n  `\"xcore\"`.\n- `num_cpus()`<sup>1.15.0</sup> - Number of logical CPUs.\n- `os()` — Operating system. Possible values are: `\"android\"`, `\"bitrig\"`,\n  `\"dragonfly\"`, `\"emscripten\"`, `\"freebsd\"`, `\"haiku\"`, `\"ios\"`, `\"linux\"`,\n  `\"macos\"`, `\"netbsd\"`, `\"openbsd\"`, `\"solaris\"`, and `\"windows\"`.\n- `os_family()` — Operating system family; possible values are: `\"unix\"` and\n  `\"windows\"`.\n\nFor example:\n\n```just\nsystem-info:\n  @echo \"This is an {{arch()}} machine\".\n```\n\n```console\n$ just system-info\nThis is an x86_64 machine\n```\n\nThe `os_family()` function can be used to create cross-platform `justfile`s\nthat work on various operating systems. For an example, see\n[cross-platform.just](https://github.com/casey/just/blob/master/examples/cross-platform.just)\nfile.\n\n#### External Commands\n\n- `shell(command, args...)`<sup>1.27.0</sup> returns the standard output of shell script\n  `command` with zero or more positional arguments `args`. The shell used to\n  interpret `command` is the same shell that is used to evaluate recipe lines,\n  and can be changed with `set shell := […]`.\n\n  `command` is passed as the first argument, so if the command is `'echo $@'`,\n  the full command line, with the default shell command `sh -cu` and `args`\n  `'foo'` and `'bar'` will be:\n\n  ```\n  'sh' '-cu' 'echo $@' 'echo $@' 'foo' 'bar'\n  ```\n\n  This is so that `$@` works as expected, and `$1` refers to the first\n  argument. `$@` does not include the first positional argument, which is\n  expected to be the name of the program being run.\n\n```just\n# arguments can be variables or expressions\nfile := '/sys/class/power_supply/BAT0/status'\nbat0stat := shell('cat $1', file)\n\n# commands can be variables or expressions\ncommand := 'wc -l'\noutput := shell(command + ' \"$1\"', 'main.c')\n\n# arguments referenced by the shell command must be used\nempty := shell('echo', 'foo')\nfull := shell('echo $1', 'foo')\nerror := shell('echo $1')\n```\n\n```just\n# Using python as the shell. Since `python -c` sets `sys.argv[0]` to `'-c'`,\n# the first \"real\" positional argument will be `sys.argv[2]`.\nset shell := [\"python3\", \"-c\"]\nolleh := shell('import sys; print(sys.argv[2][::-1])', 'hello')\n```\n\n#### Environment Variables\n\n- `env(key)`<sup>1.15.0</sup> — Retrieves the environment variable with name `key`, aborting\n  if it is not present.\n\n```just\nhome_dir := env('HOME')\n\ntest:\n  echo \"{{home_dir}}\"\n```\n\n```console\n$ just\n/home/user1\n```\n\n- `env(key, default)`<sup>1.15.0</sup> — Retrieves the environment variable with\n  name `key`, returning `default` if it is not present.\n- `env_var(key)` — Deprecated alias for `env(key)`.\n- `env_var_or_default(key, default)` — Deprecated alias for `env(key, default)`.\n\nA default can be substituted for an empty environment variable value with the\n`||` operator, currently unstable:\n\n```just\nset unstable\n\nfoo := env('FOO', '') || 'DEFAULT_VALUE'\n```\n\n#### Executables\n\n- `require(name)`<sup>1.39.0</sup> — Search directories in the `PATH`\n  environment variable for the executable `name` and return its full path, or\n  halt with an error if no executable with `name` exists.\n\n  ```just\n  bash := require(\"bash\")\n\n  @test:\n      echo \"bash: '{{bash}}'\"\n  ```\n\n  ```console\n  $ just\n  bash: '/bin/bash'\n  ```\n\n- `which(name)`<sup>1.39.0</sup> — Search directories in the `PATH` environment\n  variable for the executable `name` and return its full path, or the empty\n  string if no executable with `name` exists. Currently unstable.\n\n\n  ```just\n  set unstable\n\n  bosh := which(\"bosh\")\n\n  @test:\n      echo \"bosh: '{{bosh}}'\"\n  ```\n\n  ```console\n  $ just\n  bosh: ''\n  ```\n\n#### Invocation Information\n\n- `is_dependency()` - Returns the string `true` if the current recipe is being\n  run as a dependency of another recipe, rather than being run directly,\n  otherwise returns the string `false`.\n\n#### Invocation Directory\n\n- `invocation_directory()` - Retrieves the absolute path to the current\n  directory when `just` was invoked, before  `just` changed it (chdir'd) prior\n  to executing commands. On Windows, `invocation_directory()` uses `cygpath` to\n  convert the invocation directory to a Cygwin-compatible `/`-separated path.\n  Use `invocation_directory_native()` to return the verbatim invocation\n  directory on all platforms.\n\nFor example, to call `rustfmt` on files just under the \"current directory\"\n(from the user/invoker's perspective), use the following rule:\n\n```just\nrustfmt:\n  find {{invocation_directory()}} -name \\*.rs -exec rustfmt {} \\;\n```\n\nAlternatively, if your command needs to be run from the current directory, you\ncould use (e.g.):\n\n```just\nbuild:\n  cd {{invocation_directory()}}; ./some_script_that_needs_to_be_run_from_here\n```\n\n- `invocation_directory_native()` - Retrieves the absolute path to the current\n  directory when `just` was invoked, before  `just` changed it (chdir'd) prior\n  to executing commands.\n\n#### Justfile and Justfile Directory\n\n- `justfile()` - Retrieves the path of the current `justfile`.\n\n- `justfile_directory()` - Retrieves the path of the parent directory of the\n  current `justfile`.\n\nFor example, to run a command relative to the location of the current\n`justfile`:\n\n```just\nscript:\n  {{justfile_directory()}}/scripts/some_script\n```\n\n#### Source and Source Directory\n\n- `source_file()`<sup>1.27.0</sup> - Retrieves the path of the current source file.\n\n- `source_directory()`<sup>1.27.0</sup> - Retrieves the path of the parent directory of the\n  current source file.\n\n`source_file()` and `source_directory()` behave the same as `justfile()` and\n`justfile_directory()` in the root `justfile`, but will return the path and\ndirectory, respectively, of the current `import` or `mod` source file when\ncalled from within an import or submodule.\n\n#### Just Executable\n\n- `just_executable()` - Absolute path to the `just` executable.\n\nFor example:\n\n```just\nexecutable:\n  @echo The executable is at: {{just_executable()}}\n```\n\n```console\n$ just\nThe executable is at: /bin/just\n```\n\n#### Just Process ID\n\n- `just_pid()` - Process ID of the `just` executable.\n\nFor example:\n\n```just\npid:\n  @echo The process ID is: {{ just_pid() }}\n```\n\n```console\n$ just\nThe process ID is: 420\n```\n\n#### String Manipulation\n\n- `append(suffix, s)`<sup>1.27.0</sup> Append `suffix` to whitespace-separated\n  strings in `s`. `append('/src', 'foo bar baz')` → `'foo/src bar/src baz/src'`\n- `prepend(prefix, s)`<sup>1.27.0</sup> Prepend `prefix` to\n  whitespace-separated strings in `s`. `prepend('src/', 'foo bar baz')` →\n  `'src/foo src/bar src/baz'`\n- `encode_uri_component(s)`<sup>1.27.0</sup> - Percent-encode characters in `s`\n  except `[A-Za-z0-9_.!~*'()-]`, matching the behavior of the\n  [JavaScript `encodeURIComponent` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent).\n- `quote(s)` - Replace all single quotes with `'\\''` and prepend and append\n  single quotes to `s`. This is sufficient to escape special characters for\n  many shells, including most Bourne shell descendants.\n- `replace(s, from, to)` - Replace all occurrences of `from` in `s` with `to`.\n- `replace_regex(s, regex, replacement)` - Replace all occurrences of `regex`\n  in `s` with `replacement`. Regular expressions are provided by the\n  [Rust `regex` crate](https://docs.rs/regex/latest/regex/). See the\n  [syntax documentation](https://docs.rs/regex/latest/regex/#syntax) for usage\n  examples. Capture groups are supported. The `replacement` string uses\n  [Replacement string syntax](https://docs.rs/regex/latest/regex/struct.Regex.html#replacement-string-syntax).\n- `trim(s)` - Remove leading and trailing whitespace from `s`.\n- `trim_end(s)` - Remove trailing whitespace from `s`.\n- `trim_end_match(s, substring)` - Remove suffix of `s` matching `substring`.\n- `trim_end_matches(s, substring)` - Repeatedly remove suffixes of `s` matching\n  `substring`.\n- `trim_start(s)` - Remove leading whitespace from `s`.\n- `trim_start_match(s, substring)` - Remove prefix of `s` matching `substring`.\n- `trim_start_matches(s, substring)` - Repeatedly remove prefixes of `s`\n  matching `substring`.\n\n#### Case Conversion\n\n- `capitalize(s)`<sup>1.7.0</sup> - Convert first character of `s` to uppercase\n  and the rest to lowercase.\n- `kebabcase(s)`<sup>1.7.0</sup> - Convert `s` to `kebab-case`.\n- `lowercamelcase(s)`<sup>1.7.0</sup> - Convert `s` to `lowerCamelCase`.\n- `lowercase(s)` - Convert `s` to lowercase.\n- `shoutykebabcase(s)`<sup>1.7.0</sup> - Convert `s` to `SHOUTY-KEBAB-CASE`.\n- `shoutysnakecase(s)`<sup>1.7.0</sup> - Convert `s` to `SHOUTY_SNAKE_CASE`.\n- `snakecase(s)`<sup>1.7.0</sup> - Convert `s` to `snake_case`.\n- `titlecase(s)`<sup>1.7.0</sup> - Convert `s` to `Title Case`.\n- `uppercamelcase(s)`<sup>1.7.0</sup> - Convert `s` to `UpperCamelCase`.\n- `uppercase(s)` - Convert `s` to uppercase.\n\n#### Path Manipulation\n\n##### Fallible\n\n- `absolute_path(path)` - Absolute path to relative `path` in the working\n  directory. `absolute_path(\"./bar.txt\")` in directory `/foo` is\n  `/foo/bar.txt`.\n- `canonicalize(path)`<sup>1.24.0</sup> - Canonicalize `path` by resolving symlinks and removing\n  `.`, `..`, and extra `/`s where possible.\n- `extension(path)` - Extension of `path`. `extension(\"/foo/bar.txt\")` is\n  `txt`.\n- `file_name(path)` - File name of `path` with any leading directory components\n  removed. `file_name(\"/foo/bar.txt\")` is `bar.txt`.\n- `file_stem(path)` - File name of `path` without extension.\n  `file_stem(\"/foo/bar.txt\")` is `bar`.\n- `parent_directory(path)` - Parent directory of `path`.\n  `parent_directory(\"/foo/bar.txt\")` is `/foo`.\n- `without_extension(path)` - `path` without extension.\n  `without_extension(\"/foo/bar.txt\")` is `/foo/bar`.\n\nThese functions can fail, for example if a path does not have an extension,\nwhich will halt execution.\n\n##### Infallible\n\n- `clean(path)` - Simplify `path` by removing extra path separators,\n  intermediate `.` components, and `..` where possible. `clean(\"foo//bar\")` is\n  `foo/bar`, `clean(\"foo/..\")` is `.`, `clean(\"foo/./bar\")` is `foo/bar`.\n- `join(a, b…)` - *This function uses `/` on Unix and `\\` on Windows, which can\n  be lead to unwanted behavior. The `/` operator, e.g., `a / b`, which always\n  uses `/`, should be considered as a replacement unless `\\`s are specifically\n  desired on Windows.* Join path `a` with path `b`. `join(\"foo/bar\", \"baz\")` is\n  `foo/bar/baz`. Accepts two or more arguments.\n\n#### Filesystem Access\n\n- `path_exists(path)` - Returns the string `true` if the path points at an\n  existing entity and the string `false` otherwise. Traverses symbolic links,\n  and returns the string `false` if the path is inaccessible or points to a\n  broken symlink.\n- `read(path)`<sup>1.39.0</sup> - Returns the content of file at `path` as\n  string.\n\n##### Error Reporting\n\n- `error(message)` - Abort execution and report error `message` to user.\n\n#### UUID and Hash Generation\n\n- `blake3(string)`<sup>1.25.0</sup> - Return [BLAKE3] hash of `string` as hexadecimal string.\n- `blake3_file(path)`<sup>1.25.0</sup> - Return [BLAKE3] hash of file at `path` as hexadecimal\n  string.\n- `sha256(string)` - Return the SHA-256 hash of `string` as hexadecimal string.\n- `sha256_file(path)` - Return SHA-256 hash of file at `path` as hexadecimal\n  string.\n- `uuid()` - Generate a random version 4 UUID.\n\n[BLAKE3]: https://github.com/BLAKE3-team/BLAKE3/\n\n#### Random\n\n- `choose(n, alphabet)`<sup>1.27.0</sup> - Generate a string of `n` randomly\n  selected characters from `alphabet`, which may not contain repeated\n  characters. For example, `choose('64', HEX)` will generate a random\n  64-character lowercase hex string.\n\n#### Datetime\n\n- `datetime(format)`<sup>1.30.0</sup> - Return local time with `format`.\n- `datetime_utc(format)`<sup>1.30.0</sup> - Return UTC time with `format`.\n\nThe arguments to `datetime` and `datetime_utc` are `strftime`-style format\nstrings, see the\n[`chrono` library docs](https://docs.rs/chrono/latest/chrono/format/strftime/index.html)\nfor details.\n\n#### Semantic Versions\n\n- `semver_matches(version, requirement)`<sup>1.16.0</sup> - Check whether a\n  [semantic `version`](https://semver.org), e.g., `\"0.1.0\"` matches a\n  `requirement`, e.g., `\">=0.1.0\"`, returning `\"true\"` if so and `\"false\"`\n  otherwise.\n\n#### Style\n\n- `style(name)`<sup>1.37.0</sup> - Return a named terminal display attribute\n  escape sequence used by `just`. Unlike terminal display attribute escape\n  sequence constants, which contain standard colors and styles, `style(name)`\n  returns an escape sequence used by `just` itself, and can be used to make\n  recipe output match `just`'s own output.\n\n  Recognized values for `name` are `'command'`, for echoed recipe lines,\n  `error`, and `warning`.\n\n  For example, to style an error message:\n\n  ```just\n  scary:\n    @echo '{{ style(\"error\") }}OH NO{{ NORMAL }}'\n  ```\n\n##### User Directories\n\nThese functions<sup>1.23.0</sup> return paths to user-specific directories for\nthings like configuration, data, caches, executables, and the user's home\ndirectory.\n\nOn Unix, these functions follow the\n[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).\n\nOn MacOS and Windows, these functions return the system-specified user-specific\ndirectories. For example, `cache_directory()` returns `~/Library/Caches` on\nMacOS and `{FOLDERID_LocalAppData}` on Windows.\n\nSee the [`dirs`](https://docs.rs/dirs/latest/dirs/index.html) crate for more\ndetails.\n\n- `cache_directory()` - The user-specific cache directory.\n- `config_directory()` - The user-specific configuration directory.\n- `config_local_directory()` - The local user-specific configuration directory.\n- `data_directory()` - The user-specific data directory.\n- `data_local_directory()` - The local user-specific data directory.\n- `executable_directory()` - The user-specific executable directory.\n- `home_directory()` - The user's home directory.\n\nIf you would like to use XDG base directories on all platforms you can use the\n`env(…)` function with the appropriate environment variable and fallback,\nalthough note that the XDG specification requires ignoring non-absolute paths,\nso for full compatibility with spec-compliant applications, you would need to\ndo:\n\n```just\nxdg_config_dir := if env('XDG_CONFIG_HOME', '') =~ '^/' {\n  env('XDG_CONFIG_HOME')\n} else {\n  home_directory() / '.config'\n}\n```\n\n### Constants\n\nA number of constants are predefined:\n\n| Name | Value | Value on Windows |\n|---|---|---|\n| `HEX`<sup>1.27.0</sup> | `\"0123456789abcdef\"` |  |\n| `HEXLOWER`<sup>1.27.0</sup> | `\"0123456789abcdef\"` |  |\n| `HEXUPPER`<sup>1.27.0</sup> | `\"0123456789ABCDEF\"` |  |\n| `PATH_SEP`<sup>1.41.0</sup> | `\"/\"` | `\"\\\"` |\n| `PATH_VAR_SEP`<sup>1.41.0</sup> | `\":\"` | `\";\"` |\n| `CLEAR`<sup>1.37.0</sup> | `\"\\ec\"` |  |\n| `NORMAL`<sup>1.37.0</sup> | `\"\\e[0m\"` |  |\n| `BOLD`<sup>1.37.0</sup> | `\"\\e[1m\"` |  |\n| `ITALIC`<sup>1.37.0</sup> | `\"\\e[3m\"` |  |\n| `UNDERLINE`<sup>1.37.0</sup> | `\"\\e[4m\"` |  |\n| `INVERT`<sup>1.37.0</sup> | `\"\\e[7m\"` |  |\n| `HIDE`<sup>1.37.0</sup> | `\"\\e[8m\"` |  |\n| `STRIKETHROUGH`<sup>1.37.0</sup> | `\"\\e[9m\"` |  |\n| `BLACK`<sup>1.37.0</sup> | `\"\\e[30m\"` |  |\n| `RED`<sup>1.37.0</sup> | `\"\\e[31m\"` |  |\n| `GREEN`<sup>1.37.0</sup> | `\"\\e[32m\"` |  |\n| `YELLOW`<sup>1.37.0</sup> | `\"\\e[33m\"` |  |\n| `BLUE`<sup>1.37.0</sup> | `\"\\e[34m\"` |  |\n| `MAGENTA`<sup>1.37.0</sup> | `\"\\e[35m\"` |  |\n| `CYAN`<sup>1.37.0</sup> | `\"\\e[36m\"` |  |\n| `WHITE`<sup>1.37.0</sup> | `\"\\e[37m\"` |  |\n| `BG_BLACK`<sup>1.37.0</sup> | `\"\\e[40m\"` |  |\n| `BG_RED`<sup>1.37.0</sup> | `\"\\e[41m\"` |  |\n| `BG_GREEN`<sup>1.37.0</sup> | `\"\\e[42m\"` |  |\n| `BG_YELLOW`<sup>1.37.0</sup> | `\"\\e[43m\"` |  |\n| `BG_BLUE`<sup>1.37.0</sup> | `\"\\e[44m\"` |  |\n| `BG_MAGENTA`<sup>1.37.0</sup> | `\"\\e[45m\"` |  |\n| `BG_CYAN`<sup>1.37.0</sup> | `\"\\e[46m\"` |  |\n| `BG_WHITE`<sup>1.37.0</sup> | `\"\\e[47m\"` |  |\n\n```just\n@foo:\n  echo {{HEX}}\n```\n\n```console\n$ just foo\n0123456789abcdef\n```\n\nConstants starting with `\\e` are\n[ANSI escape sequences](https://en.wikipedia.org/wiki/ANSI_escape_code).\n\n`CLEAR` clears the screen, similar to the `clear` command. The rest are of the\nform `\\e[Nm`, where `N` is an integer, and set terminal display attributes.\n\nTerminal display attribute escape sequences can be combined, for example text\nweight `BOLD`, text style `STRIKETHROUGH`, foreground color `CYAN`, and\nbackground color `BG_BLUE`. They should be followed by `NORMAL`, to reset the\nterminal back to normal.\n\nEscape sequences should be quoted, since `[` is treated as a special character\nby some shells.\n\n```just\n@foo:\n  echo '{{BOLD + STRIKETHROUGH + CYAN + BG_BLUE}}Hi!{{NORMAL}}'\n```\n\n### Attributes\n\nRecipes, `mod` statements, and aliases may be annotated with attributes that\nchange their behavior.\n\n| Name | Type | Description |\n|------|------|-------------|\n| `[arg(ARG, help=\"HELP\")]`<sup>1.46.0</sup> | recipe | Print help string `HELP` for `ARG` in usage messages. |\n| `[arg(ARG, long=\"LONG\")]`<sup>1.46.0</sup> | recipe | Require values of argument `ARG` to be passed as `--LONG` option. |\n| `[arg(ARG, pattern=\"PATTERN\")]`<sup>1.45.0</sup> | recipe | Require values of argument `ARG` to match regular expression `PATTERN`. |\n| `[arg(ARG, short=\"S\")]`<sup>1.46.0</sup> | recipe | Require values of argument `ARG` to be passed as short `-S` option. |\n| `[arg(ARG, value=\"VALUE\")]`<sup>1.46.0</sup> | recipe | Makes option `ARG` a flag which does not take a value. |\n| `[confirm(PROMPT)]`<sup>1.23.0</sup> | recipe | Require confirmation prior to executing recipe with a custom prompt. |\n| `[confirm]`<sup>1.17.0</sup> | recipe | Require confirmation prior to executing recipe. |\n| `[default]`<sup>1.43.0</sup> | recipe | Use recipe as module's default recipe. |\n| `[doc(DOC)]`<sup>1.27.0</sup> | module, recipe | Set recipe or module's [documentation comment](#documentation-comments) to `DOC`. |\n| `[dragonfly]`<sup>1.47.0</sup> | recipe | Enable recipe on DragonFly BSD. |\n| `[env(ENV_VAR, VALUE)]` <sup>1.47.0</sup> | recipe | Set environment variables for recipe. |\n| `[extension(EXT)]`<sup>1.32.0</sup> | recipe | Set shebang recipe script's file extension to `EXT`. `EXT` should include a period if one is desired. |\n| `[freebsd]`<sup>1.47.0</sup> | recipe | Enable recipe on FreeBSD. |\n| `[group(NAME)]`<sup>1.27.0</sup> | module, recipe | Put recipe or module in [group](#groups) `NAME`. |\n| `[linux]`<sup>1.8.0</sup> | recipe | Enable recipe on Linux. |\n| `[macos]`<sup>1.8.0</sup> | recipe | Enable recipe on MacOS. |\n| `[metadata(METADATA)]`<sup>1.42.0</sup> | recipe | Attach `METADATA` to recipe. |\n| `[netbsd]`<sup>1.47.0</sup> | recipe | Enable recipe on NetBSD. |\n| `[no-cd]`<sup>1.9.0</sup> | recipe | Don't change directory before executing recipe. |\n| `[no-exit-message]`<sup>1.7.0</sup> | recipe | Don't print an error message if recipe fails. |\n| `[no-quiet]`<sup>1.23.0</sup> | recipe | Override globally quiet recipes and always echo out the recipe. |\n| `[openbsd]`<sup>1.38.0</sup> | recipe | Enable recipe on OpenBSD. |\n| `[parallel]`<sup>1.42.0</sup> | recipe | Run this recipe's dependencies in parallel. |\n| `[positional-arguments]`<sup>1.29.0</sup> | recipe | Turn on [positional arguments](#positional-arguments) for this recipe. |\n| `[private]`<sup>1.10.0</sup> | alias, recipe | Make recipe, alias, or variable private. See [Private Recipes](#private-recipes). |\n| `[script(COMMAND)]`<sup>1.32.0</sup> | recipe | Execute recipe as a script interpreted by `COMMAND`. See [script recipes](#script-recipes) for more details. |\n| `[script]`<sup>1.33.0</sup> | recipe | Execute recipe as script. See [script recipes](#script-recipes) for more details. |\n| `[unix]`<sup>1.8.0</sup> | recipe | Enable recipe on Unixes. (Includes MacOS). |\n| `[windows]`<sup>1.8.0</sup> | recipe | Enable recipe on Windows. |\n| `[working-directory(PATH)]`<sup>1.38.0</sup> | recipe | Set recipe working directory. `PATH` may be relative or absolute. If relative, it is interpreted relative to the default working directory. |\n\nA recipe can have multiple attributes, either on multiple lines:\n\n```just\n[no-cd]\n[private]\nfoo:\n    echo \"foo\"\n```\n\nOr separated by commas on a single line<sup>1.14.0</sup>:\n\n```just\n[no-cd, private]\nfoo:\n    echo \"foo\"\n```\n\nAttributes with a single argument may be written with a colon:\n\n```just\n[group: 'bar']\nfoo:\n```\n\n#### Enabling and Disabling Recipes\n\nThe `[linux]`, `[macos]`, `[unix]`, and `[windows]` attributes<sup>1.8.0</sup>\nare configuration attributes. By default, recipes are always enabled. A recipe\nwith one or more configuration attributes will only be enabled when one or more\nof those configurations is active.\n\nThis can be used to write `justfile`s that behave differently depending on\nwhich operating system they run on. The `run` recipe in this `justfile` will\ncompile and run `main.c`, using a different C compiler and using the correct\noutput binary name for that compiler depending on the operating system:\n\n```just\n[unix]\nrun:\n  cc main.c\n  ./a.out\n\n[windows]\nrun:\n  cl main.c\n  main.exe\n```\n\n#### Disabling Changing Directory\n\n`just` normally executes recipes with the current directory set to the\ndirectory that contains the `justfile`. This can be disabled using the\n`[no-cd]` attribute<sup>1.9.0</sup>. This can be used to create recipes which\nuse paths relative to the invocation directory, or which operate on the current\ndirectory.\n\nFor example, this `commit` recipe:\n\n```just\n[no-cd]\ncommit file:\n  git add {{file}}\n  git commit\n```\n\nCan be used with paths that are relative to the current directory, because\n`[no-cd]` prevents `just` from changing the current directory when executing\n`commit`.\n\n#### Requiring Confirmation for Recipes\n\n`just` normally executes all recipes unless there is an error. The `[confirm]`\nattribute<sup>1.17.0</sup> allows recipes require confirmation in the terminal\nprior to running. This can be overridden by passing `--yes` to `just`, which\nwill automatically confirm any recipes marked by this attribute.\n\nRecipes dependent on a recipe that requires confirmation will not be run if the\nrelied upon recipe is not confirmed, as well as recipes passed after any recipe\nthat requires confirmation.\n\n```just\n[confirm]\ndelete-all:\n  rm -rf *\n```\n\n#### Custom Confirmation Prompt\n\nThe default confirmation prompt can be overridden with\n`[confirm(PROMPT)]`<sup>1.23.0</sup>:\n\n```just\n[confirm(\"Are you sure you want to delete everything?\")]\ndelete-everything:\n  rm -rf *\n```\n\n#### Metadata\n\nMetadata in the form of lists of strings may be attached to recipes with the\n`[metadata(METADATA)]` attribute<sup>1.42.0</sup>:\n\n```just\n[metadata(\"hello\", \"goodbye\")]\nfoo:\n```\n\nMetadata can be read using `just --dump --dump-format json`.\n\n### Groups\n\nRecipes and modules may be annotated with one or more group names:\n\n```just\n[group('lint')]\njs-lint:\n    echo 'Running JS linter…'\n\n[group('rust recipes')]\n[group('lint')]\nrust-lint:\n    echo 'Running Rust linter…'\n\n[group('lint')]\ncpp-lint:\n  echo 'Running C++ linter…'\n\n# not in any group\nemail-everyone:\n    echo 'Sending mass email…'\n```\n\nRecipes are listed by group:\n\n```\n$ just --list\nAvailable recipes:\n    email-everyone # not in any group\n\n    [lint]\n    cpp-lint\n    js-lint\n    rust-lint\n\n    [rust recipes]\n    rust-lint\n```\n\n`just --list --unsorted` prints recipes in their justfile order within each group:\n\n```\n$ just --list --unsorted\nAvailable recipes:\n    (no group)\n    email-everyone # not in any group\n\n    [lint]\n    js-lint\n    rust-lint\n    cpp-lint\n\n    [rust recipes]\n    rust-lint\n```\n\nGroups can be listed with `--groups`:\n\n```\n$ just --groups\nRecipe groups:\n  lint\n  rust recipes\n```\n\nUse `just --groups --unsorted` to print groups in their justfile order.\n\n### Command Evaluation Using Backticks\n\nBackticks can be used to store the result of commands:\n\n```just\nlocalhost := `dumpinterfaces | cut -d: -f2 | sed 's/\\/.*//' | sed 's/ //g'`\n\nserve:\n  ./serve {{localhost}} 8080\n```\n\nIndented backticks, delimited by three backticks, are de-indented in the same\nmanner as indented strings:\n\n````just\n# This backtick evaluates the command `echo foo\\necho bar\\n`, which produces the value `foo\\nbar\\n`.\nstuff := ```\n    echo foo\n    echo bar\n  ```\n````\n\nSee the [Strings](#strings) section for details on unindenting.\n\nBackticks may not start with `#!`. This syntax is reserved for a future\nupgrade.\n\nThe [`shell(…)` function](#external-commands) provides a more general mechanism\nto invoke external commands, including the ability to execute the contents of a\nvariable as a command, and to pass arguments to a command.\n\n### Conditional Expressions\n\n`if`/`else` expressions evaluate different branches depending on if two\nexpressions evaluate to the same value:\n\n```just\nfoo := if \"2\" == \"2\" { \"Good!\" } else { \"1984\" }\n\nbar:\n  @echo \"{{foo}}\"\n```\n\n```console\n$ just bar\nGood!\n```\n\nIt is also possible to test for inequality:\n\n```just\nfoo := if \"hello\" != \"goodbye\" { \"xyz\" } else { \"abc\" }\n\nbar:\n  @echo {{foo}}\n```\n\n```console\n$ just bar\nxyz\n```\n\nAnd match against regular expressions:\n\n```just\nfoo := if \"hello\" =~ 'hel+o' { \"match\" } else { \"mismatch\" }\n\nbar:\n  @echo {{foo}}\n```\n\n```console\n$ just bar\nmatch\n```\n\nRegular expressions are provided by the\n[regex crate](https://github.com/rust-lang/regex), whose syntax is documented on\n[docs.rs](https://docs.rs/regex/1.5.4/regex/#syntax). Since regular expressions\ncommonly use backslash escape sequences, consider using single-quoted string\nliterals, which will pass slashes to the regex parser unmolested.\n\nConditional expressions short-circuit, which means they only evaluate one of\ntheir branches. This can be used to make sure that backtick expressions don't\nrun when they shouldn't.\n\n```just\nfoo := if env_var(\"RELEASE\") == \"true\" { `get-something-from-release-database` } else { \"dummy-value\" }\n```\n\nConditionals can be used inside of recipes:\n\n```just\nbar foo:\n  echo {{ if foo == \"bar\" { \"hello\" } else { \"goodbye\" } }}\n```\n\nMultiple conditionals can be chained:\n\n```just\nfoo := if \"hello\" == \"goodbye\" {\n  \"xyz\"\n} else if \"a\" == \"a\" {\n  \"abc\"\n} else {\n  \"123\"\n}\n\nbar:\n  @echo {{foo}}\n```\n\n```console\n$ just bar\nabc\n```\n\n### Stopping execution with error\n\nExecution can be halted with the `error` function. For example:\n\n```just\nfoo := if \"hello\" == \"goodbye\" {\n  \"xyz\"\n} else if \"a\" == \"b\" {\n  \"abc\"\n} else {\n  error(\"123\")\n}\n```\n\nWhich produce the following error when run:\n\n```\nerror: Call to function `error` failed: 123\n   |\n16 |   error(\"123\")\n```\n\n### Setting Variables from the Command Line\n\nVariables can be overridden from the command line.\n\n```just\nos := \"linux\"\n\ntest: build\n  ./test --test {{os}}\n\nbuild:\n  ./build {{os}}\n```\n\n```console\n$ just\n./build linux\n./test --test linux\n```\n\nAny number of arguments of the form `NAME=VALUE` can be passed before recipes:\n\n```console\n$ just os=plan9\n./build plan9\n./test --test plan9\n```\n\nOr you can use the `--set` flag:\n\n```console\n$ just --set os bsd\n./build bsd\n./test --test bsd\n```\n\nVariables in submodules can be overridden using the `::`-separated path to the\nvariable. A variable named `bar` in a submodule named `foo` may be overridden\nwith `foo::bar=VALUE` or `--set foo::bar VALUE`.\n\n### Getting and Setting Environment Variables\n\n#### Exporting `just` Variables\n\nAssignments prefixed with the `export` keyword will be exported to recipes as\nenvironment variables:\n\n```just\nexport RUST_BACKTRACE := \"1\"\n\ntest:\n  # will print a stack trace if it crashes\n  cargo test\n```\n\nParameters prefixed with a `$` will be exported as environment variables:\n\n```just\ntest $RUST_BACKTRACE=\"1\":\n  # will print a stack trace if it crashes\n  cargo test\n```\n\nYou can also use the `[env(NAME, VALUE)]` attribute to export environment\nvariables to a specific recipe:\n\n```just\n[env(\"RUST_BACKTRACE\", \"1\")]\ntest:\n  # will print a stack trace if it crashes\n  cargo test\n```\n\nExported variables and parameters are not exported to backticks in the same scope.\n\n```just\nexport WORLD := \"world\"\n# This backtick will fail with \"WORLD: unbound variable\"\nBAR := `echo hello $WORLD`\n```\n\n```just\n# Running `just a foo` will fail with \"A: unbound variable\"\na $A $B=`echo $A`:\n  echo $A $B\n```\n\nWhen [export](#export) is set, all `just` variables are exported as environment\nvariables.\n\n#### Unexporting Environment Variables\n\nEnvironment variables can be unexported with the `unexport\nkeyword`<sup>1.29.0</sup>:\n\n```just\nunexport FOO\n\n@foo:\n  echo $FOO\n```\n\n```\n$ export FOO=bar\n$ just foo\nsh: FOO: unbound variable\n```\n\n#### Getting Environment Variables from the environment\n\nEnvironment variables from the environment are passed automatically to the\nrecipes.\n\n```just\nprint_home_folder:\n  echo \"HOME is: '${HOME}'\"\n```\n\n```console\n$ just\nHOME is '/home/myuser'\n```\n\n#### Setting `just` Variables from Environment Variables\n\nEnvironment variables can be propagated to `just` variables using the `env()` function.\nSee\n[environment-variables](#environment-variables).\n\n### Recipe Parameters\n\nRecipes may have parameters. Here recipe `build` has a parameter called\n`target`:\n\n```just\nbuild target:\n  @echo 'Building {{target}}…'\n  cd {{target}} && make\n```\n\nTo pass arguments on the command line, put them after the recipe name:\n\n```console\n$ just build my-awesome-project\nBuilding my-awesome-project…\ncd my-awesome-project && make\n```\n\nTo pass arguments to a dependency, put the dependency in parentheses along with\nthe arguments:\n\n```just\ndefault: (build \"main\")\n\nbuild target:\n  @echo 'Building {{target}}…'\n  cd {{target}} && make\n```\n\nVariables can also be passed as arguments to dependencies:\n\n```just\ntarget := \"main\"\n\n_build version:\n  @echo 'Building {{version}}…'\n  cd {{version}} && make\n\nbuild: (_build target)\n```\n\nA command's arguments can be passed to dependency by putting the dependency in\nparentheses along with the arguments:\n\n```just\nbuild target:\n  @echo \"Building {{target}}…\"\n\npush target: (build target)\n  @echo 'Pushing {{target}}…'\n```\n\nParameters may have default values:\n\n```just\ndefault := 'all'\n\ntest target tests=default:\n  @echo 'Testing {{target}}:{{tests}}…'\n  ./test --tests {{tests}} {{target}}\n```\n\nParameters with default values may be omitted:\n\n```console\n$ just test server\nTesting server:all…\n./test --tests all server\n```\n\nOr supplied:\n\n```console\n$ just test server unit\nTesting server:unit…\n./test --tests unit server\n```\n\nDefault values may be arbitrary expressions, but expressions containing the\n`+`, `&&`, `||`, or `/` operators must be parenthesized:\n\n```just\narch := \"wasm\"\n\ntest triple=(arch + \"-unknown-unknown\") input=(arch / \"input.dat\"):\n  ./test {{triple}}\n```\n\nThe last parameter of a recipe may be variadic, indicated with either a `+` or\na `*` before the argument name:\n\n```just\nbackup +FILES:\n  scp {{FILES}} me@server.com:\n```\n\nVariadic parameters prefixed with `+` accept _one or more_ arguments and expand\nto a string containing those arguments separated by spaces:\n\n```console\n$ just backup FAQ.md GRAMMAR.md\nscp FAQ.md GRAMMAR.md me@server.com:\nFAQ.md                  100% 1831     1.8KB/s   00:00\nGRAMMAR.md              100% 1666     1.6KB/s   00:00\n```\n\nVariadic parameters prefixed with `*` accept _zero or more_ arguments and\nexpand to a string containing those arguments separated by spaces, or an empty\nstring if no arguments are present:\n\n```just\ncommit MESSAGE *FLAGS:\n  git commit {{FLAGS}} -m \"{{MESSAGE}}\"\n```\n\nVariadic parameters can be assigned default values. These are overridden by\narguments passed on the command line:\n\n```just\ntest +FLAGS='-q':\n  cargo test {{FLAGS}}\n```\n\n`{{…}}` substitutions may need to be quoted if they contain spaces. For\nexample, if you have the following recipe:\n\n```just\nsearch QUERY:\n  lynx https://www.google.com/?q={{QUERY}}\n```\n\nAnd you type:\n\n```console\n$ just search \"cat toupee\"\n```\n\n`just` will run the command `lynx https://www.google.com/?q=cat toupee`, which\nwill get parsed by `sh` as `lynx`, `https://www.google.com/?q=cat`, and\n`toupee`, and not the intended `lynx` and `https://www.google.com/?q=cat toupee`.\n\nYou can fix this by adding quotes:\n\n```just\nsearch QUERY:\n  lynx 'https://www.google.com/?q={{QUERY}}'\n```\n\nParameters prefixed with a `$` will be exported as environment variables:\n\n```just\nfoo $bar:\n  echo $bar\n```\n\nParameters may be constrained to match regular expression patterns using the\n`[arg(\"name\", pattern=\"pattern\")]` attribute<sup>1.45.0</sup>:\n\n```just\n[arg('n', pattern='\\d+')]\ndouble n:\n  echo $(({{n}} * 2))\n```\n\nA leading `^` and trailing `$` are added to the pattern, so it must match the\nentire argument value.\n\nYou may constrain the pattern to a number of alternatives using the `|`\noperator:\n\n```just\n[arg('flag', pattern='--help|--version')]\ninfo flag:\n  just {{flag}}\n```\n\nRegular expressions are provided by the\n[Rust `regex` crate](https://docs.rs/regex/latest/regex/). See the\n[syntax documentation](https://docs.rs/regex/latest/regex/#syntax) for usage\nexamples.\n\nUsage information for a recipe may be printed with the `--usage`\nsubcommand<sup>1.46.0</sup>:\n\n```console\n$ just --usage foo\nUsage: just foo [OPTIONS] bar\n\nArguments:\n  bar\n```\n\nHelp strings may be added to arguments using the `[arg(ARG, help=HELP)]` attribute:\n\n```just\n[arg(\"bar\", help=\"hello\")]\nfoo bar:\n```\n\n```console\n$ just --usage foo\nUsage: just foo bar\n\nArguments:\n  bar hello\n```\n\n#### Recipe Flags and Options\n\nRecipe parameters are positional by default.\n\nIn this `justfile`:\n\n```just\n@foo bar:\n  echo bar={{bar}}\n```\n\nThe parameter `bar` is positional:\n\n```console\n$ just foo hello\nbar=hello\n```\n\nThe `[arg(ARG, long=OPTION)]`<sup>1.46.0</sup> attribute can be used to make a\nparameter a long option.\n\nIn this `justfile`:\n\n```just\n[arg(\"bar\", long=\"bar\")]\nfoo bar:\n```\n\nThe parameter `bar` is given with the `--bar` option:\n\n```console\n$ just foo --bar hello\nbar=hello\n```\n\nOptions may also be passed with `--name=value` syntax:\n\n```console\n$ just foo --bar=hello\nbar=hello\n```\n\nThe value of `long` can be omitted, in which case the option defaults to the\nname of the parameter:\n\n```just\n[arg(\"bar\", long)]\nfoo bar:\n```\n\nThe `[arg(ARG, short=OPTION)]`<sup>1.46.0</sup> attribute can be used to make a\nparameter a short option.\n\nIn this `justfile`:\n\n```just\n[arg(\"bar\", short=\"b\")]\nfoo bar:\n```\n\nThe parameter `bar` is given with the `-b` option:\n\n```console\n$ just foo -b hello\nbar=hello\n```\n\nIf a parameter has both a long and short option, it may be passed using either.\n\nVariadic `*` and `+` parameters cannot be options.\n\nThe `[arg(ARG, value=VALUE, …)]`<sup>1.46.0</sup> attribute can be used with\n`long` or `short` to make a parameter a flag which does not take a value.\n\nIn this `justfile`:\n\n```just\n[arg(\"bar\", long=\"bar\", value=\"hello\")]\nfoo bar:\n```\n\nThe parameter `bar` is given with the `--bar` option, but does not take a\nvalue, and instead takes the value given in the `[arg]` attribute:\n\n```console\n$ just foo --bar\nbar=hello\n```\n\nThis is useful for unconditionally requiring a flag like `--force` on dangerous\ncommands.\n\nA flag is optional if its parameter has a default:\n\n```just\n[arg(\"bar\", long=\"bar\", value=\"hello\")]\nfoo bar=\"goodbye\":\n```\n\nCausing it to receive the default when not passed in the invocation:\n\n```console\n$ just foo\nbar=goodbye\n```\n\n### Dependencies\n\nDependencies run before recipes that depend on them:\n\n```just\na: b\n  @echo A\n\nb:\n  @echo B\n```\n\n```\n$ just a\nB\nA\n```\n\nIn a given invocation of `just`, a recipe with the same arguments will only run\nonce, regardless of how many times it appears in the command-line invocation,\nor how many times it appears as a dependency:\n\n```just\na:\n  @echo A\n\nb: a\n  @echo B\n\nc: a\n  @echo C\n```\n\n```\n$ just a a a a a\nA\n$ just b c\nA\nB\nC\n```\n\nMultiple recipes may depend on a recipe that performs some kind of setup, and\nwhen those recipes run, that setup will only be performed once:\n\n```just\nbuild:\n  cc main.c\n\ntest-foo: build\n  ./a.out --test foo\n\ntest-bar: build\n  ./a.out --test bar\n```\n\n```\n$ just test-foo test-bar\ncc main.c\n./a.out --test foo\n./a.out --test bar\n```\n\nRecipes in a given run are only skipped when they receive the same arguments:\n\n```just\nbuild:\n  cc main.c\n\ntest TEST: build\n  ./a.out --test {{TEST}}\n```\n\n```\n$ just test foo test bar\ncc main.c\n./a.out --test foo\n./a.out --test bar\n```\n\n#### Running Recipes at the End of a Recipe\n\nNormal dependencies of a recipes always run before a recipe starts. That is to\nsay, the dependee always runs before the depender. These dependencies are\ncalled \"prior dependencies\".\n\nA recipe can also have subsequent dependencies, which run immediately after the\nrecipe and are introduced with an `&&`:\n\n```just\na:\n  echo 'A!'\n\nb: a && c d\n  echo 'B!'\n\nc:\n  echo 'C!'\n\nd:\n  echo 'D!'\n```\n\n…running _b_ prints:\n\n```console\n$ just b\necho 'A!'\nA!\necho 'B!'\nB!\necho 'C!'\nC!\necho 'D!'\nD!\n```\n\n#### Running Recipes in the Middle of a Recipe\n\n`just` doesn't support running recipes in the middle of another recipe, but you\ncan call `just` recursively in the middle of a recipe. Given the following\n`justfile`:\n\n```just\na:\n  echo 'A!'\n\nb: a\n  echo 'B start!'\n  just c\n  echo 'B end!'\n\nc:\n  echo 'C!'\n```\n\n…running _b_ prints:\n\n```console\n$ just b\necho 'A!'\nA!\necho 'B start!'\nB start!\necho 'C!'\nC!\necho 'B end!'\nB end!\n```\n\nThis has limitations, since recipe `c` is run with an entirely new invocation\nof `just`: Assignments will be recalculated, dependencies might run twice, and\ncommand line arguments will not be propagated to the child `just` process.\n\n### Shebang Recipes\n\nRecipes that start with `#!` are called shebang recipes, and are executed by\nsaving the recipe body to a file and running it. This lets you write recipes in\ndifferent languages:\n\n```just\npolyglot: python js perl sh ruby nu\n\npython:\n  #!/usr/bin/env python3\n  print('Hello from python!')\n\njs:\n  #!/usr/bin/env node\n  console.log('Greetings from JavaScript!')\n\nperl:\n  #!/usr/bin/env perl\n  print \"Larry Wall says Hi!\\n\";\n\nsh:\n  #!/usr/bin/env sh\n  hello='Yo'\n  echo \"$hello from a shell script!\"\n\nnu:\n  #!/usr/bin/env nu\n  let hello = 'Hola'\n  echo $\"($hello) from a nushell script!\"\n\nruby:\n  #!/usr/bin/env ruby\n  puts \"Hello from ruby!\"\n```\n\n```console\n$ just polyglot\nHello from python!\nGreetings from JavaScript!\nLarry Wall says Hi!\nYo from a shell script!\nHola from a nushell script!\nHello from ruby!\n```\n\nOn Unix-like operating systems, including Linux and MacOS, shebang recipes are\nexecuted by saving the recipe body to a file in a temporary directory, marking\nthe file as executable, and executing it. The OS then parses the shebang line\ninto a command line and invokes it, including the path to the file. For\nexample, if a recipe starts with `#!/usr/bin/env bash`, the final command that\nthe OS runs will be something like `/usr/bin/env bash\n/tmp/PATH_TO_SAVED_RECIPE_BODY`.\n\nShebang line splitting is operating system dependent. When passing a command\nwith arguments, you may need to tell `env` to split them explicitly by using\nthe `-S` flag:\n\n```just\nrun:\n  #!/usr/bin/env -S bash -x\n  ls\n```\n\nWindows does not support shebang lines. On Windows, `just` splits the shebang\nline into a command and arguments, saves the recipe body to a file, and invokes\nthe split command and arguments, adding the path to the saved recipe body as\nthe final argument. For example, on Windows, if a recipe starts with `#! py`,\nthe final command the OS runs will be something like\n`py C:\\Temp\\PATH_TO_SAVED_RECIPE_BODY`.\n\n### Script Recipes\n\nRecipes with a `[script(COMMAND)]`<sup>1.32.0</sup> attribute are run as\nscripts interpreted by `COMMAND`. This avoids some of the issues with shebang\nrecipes, such as the use of `cygpath` on Windows, the need to use\n`/usr/bin/env`, inconsistencies in shebang line splitting across Unix OSs, and\nrequiring a temporary directory from which files can be executed.\n\nRecipes with an empty `[script]` attribute are executed with the value of `set\nscript-interpreter := […]`<sup>1.33.0</sup>, defaulting to `sh -eu`, and *not*\nthe value of `set shell`.\n\nThe body of the recipe is evaluated, written to disk in the temporary\ndirectory, and run by passing its path as an argument to `COMMAND`.\n\n### Script and Shebang Recipe Temporary Files\n\nBoth script and shebang recipes write the recipe body to a temporary file for\nexecution. Script recipes execute that file by passing it to a command, while\nshebang recipes execute the file directly. Shebang recipe execution will fail\nif the filesystem containing the temporary file is mounted with `noexec` or is\notherwise non-executable.\n\nThe directory that `just` writes temporary files to may be configured in a\nnumber of ways, from highest to lowest precedence:\n\n- Globally with the `--tempdir` command-line option or the `JUST_TEMPDIR`\n  environment variable<sup>1.41.0</sup>.\n\n- On a per-module basis with the `tempdir` setting.\n\n- Globally on Linux with the `XDG_RUNTIME_DIR` environment variable.\n\n- Falling back to the directory returned by\n  [std::env::temp_dir](https://doc.rust-lang.org/std/env/fn.temp_dir.html).\n\n### Python Recipes with `uv`\n\n[`uv`](https://github.com/astral-sh/uv) is an excellent cross-platform python\nproject manager, written in Rust.\n\nUsing the `[script]` attribute and `script-interpreter` setting, `just` can\neasily be configured to run Python recipes with `uv`:\n\n```just\nset unstable\n\nset script-interpreter := ['uv', 'run', '--script']\n\n[script]\nhello:\n  print(\"Hello from Python!\")\n\n[script]\ngoodbye:\n  # /// script\n  # requires-python = \">=3.11\"\n  # dependencies=[\"sh\"]\n  # ///\n  import sh\n  print(sh.echo(\"Goodbye from Python!\"), end='')\n```\n\nOf course, a shebang also works:\n\n```just\nhello:\n  #!/usr/bin/env -S uv run --script\n  print(\"Hello from Python!\")\n```\n\n\n### Safer Bash Shebang Recipes\n\nIf you're writing a `bash` shebang recipe, consider adding `set -euxo\npipefail`:\n\n```just\nfoo:\n  #!/usr/bin/env bash\n  set -euxo pipefail\n  hello='Yo'\n  echo \"$hello from Bash!\"\n```\n\nIt isn't strictly necessary, but `set -euxo pipefail` turns on a few useful\nfeatures that make `bash` shebang recipes behave more like normal, linewise\n`just` recipe:\n\n- `set -e` makes `bash` exit if a command fails.\n\n- `set -u` makes `bash` exit if a variable is undefined.\n\n- `set -x` makes `bash` print each script line before it's run.\n\n- `set -o pipefail` makes `bash` exit if a command in a pipeline fails. This is\n  `bash`-specific, so isn't turned on in normal linewise `just` recipes.\n\nTogether, these avoid a lot of shell scripting gotchas.\n\n#### Shebang Recipe Execution on Windows\n\nOn Windows, shebang interpreter paths containing a `/` are translated from\nUnix-style paths to Windows-style paths using `cygpath`, a utility that ships\nwith [Cygwin](http://www.cygwin.com).\n\nFor example, to execute this recipe on Windows:\n\n```just\necho:\n  #!/bin/sh\n  echo \"Hello!\"\n```\n\nThe interpreter path `/bin/sh` will be translated to a Windows-style path using\n`cygpath` before being executed.\n\nIf the interpreter path does not contain a `/` it will be executed without\nbeing translated. This is useful if `cygpath` is not available, or you wish to\npass a Windows-style path to the interpreter.\n\n### Setting Variables in a Recipe\n\nRecipe lines are interpreted by the shell, not `just`, so it's not possible to\nset `just` variables in the middle of a recipe:\n\n```justfile\nfoo:\n  x := \"hello\" # This doesn't work!\n  echo {{x}}\n```\n\nIt is possible to use shell variables, but there's another problem. Every\nrecipe line is run by a new shell instance, so variables set in one line won't\nbe set in the next:\n\n```just\nfoo:\n  x=hello && echo $x # This works!\n  y=bye\n  echo $y            # This doesn't, `y` is undefined here!\n```\n\nThe best way to work around this is to use a shebang recipe. Shebang recipe\nbodies are extracted and run as scripts, so a single shell instance will run\nthe whole thing:\n\n```just\nfoo:\n  #!/usr/bin/env bash\n  set -euxo pipefail\n  x=hello\n  echo $x\n```\n\n### Sharing Environment Variables Between Recipes\n\nEach line of each recipe is executed by a fresh shell, so it is not possible to\nshare environment variables between recipes.\n\n#### Using Python Virtual Environments\n\nSome tools, like [Python's venv](https://docs.python.org/3/library/venv.html),\nrequire loading environment variables in order to work, making them challenging\nto use with `just`. As a workaround, you can execute the virtual environment\nbinaries directly:\n\n```just\nvenv:\n  [ -d foo ] || python3 -m venv foo\n\nrun: venv\n  ./foo/bin/python3 main.py\n```\n\n### Changing the Working Directory in a Recipe\n\nEach recipe line is executed by a new shell, so if you change the working\ndirectory on one line, it won't have an effect on later lines:\n\n```just\nfoo:\n  pwd    # This `pwd` will print the same directory…\n  cd bar\n  pwd    # …as this `pwd`!\n```\n\nThere are a couple ways around this. One is to call `cd` on the same line as\nthe command you want to run:\n\n```just\nfoo:\n  cd bar && pwd\n```\n\nThe other is to use a shebang recipe. Shebang recipe bodies are extracted and\nrun as scripts, so a single shell instance will run the whole thing, and thus a\n`cd` on one line will affect later lines, just like a shell script:\n\n```just\nfoo:\n  #!/usr/bin/env bash\n  set -euxo pipefail\n  cd bar\n  pwd\n```\n\n### Indentation\n\nRecipe lines can be indented with spaces or tabs, but not a mix of both. All of\na recipe's lines must have the same type of indentation, but different recipes\nin the same `justfile` may use different indentation.\n\nEach recipe must be indented at least one level from the `recipe-name` but\nafter that may be further indented.\n\nHere's a justfile with a recipe indented with spaces, represented as `·`, and\ntabs, represented as `→`.\n\n```justfile\nset windows-shell := [\"pwsh\", \"-NoLogo\", \"-NoProfileLoadTime\", \"-Command\"]\n\nset ignore-comments\n\nlist-space directory:\n··#!pwsh\n··foreach ($item in $(Get-ChildItem {{directory}} )) {\n····echo $item.Name\n··}\n··echo \"\"\n\n# indentation nesting works even when newlines are escaped\nlist-tab directory:\n→ @foreach ($item in $(Get-ChildItem {{directory}} )) { \\\n→ → echo $item.Name \\\n→ }\n→ @echo \"\"\n```\n\n```pwsh\nPS > just list-space ~\nDesktop\nDocuments\nDownloads\n\nPS > just list-tab ~\nDesktop\nDocuments\nDownloads\n```\n\n### Multi-Line Constructs\n\nRecipes without an initial shebang are evaluated and run line-by-line, which\nmeans that multi-line constructs probably won't do what you want.\n\nFor example, with the following `justfile`:\n\n```justfile\nconditional:\n  if true; then\n    echo 'True!'\n  fi\n```\n\nThe extra leading whitespace before the second line of the `conditional` recipe\nwill produce a parse error:\n\n```console\n$ just conditional\nerror: Recipe line has extra leading whitespace\n  |\n3 |         echo 'True!'\n  |     ^^^^^^^^^^^^^^^^\n```\n\nTo work around this, you can write conditionals on one line, escape newlines\nwith slashes, or add a shebang to your recipe. Some examples of multi-line\nconstructs are provided for reference.\n\n#### `if` statements\n\n```just\nconditional:\n  if true; then echo 'True!'; fi\n```\n\n```just\nconditional:\n  if true; then \\\n    echo 'True!'; \\\n  fi\n```\n\n```just\nconditional:\n  #!/usr/bin/env sh\n  if true; then\n    echo 'True!'\n  fi\n```\n\n#### `for` loops\n\n```just\nfor:\n  for file in `ls .`; do echo $file; done\n```\n\n```just\nfor:\n  for file in `ls .`; do \\\n    echo $file; \\\n  done\n```\n\n```just\nfor:\n  #!/usr/bin/env sh\n  for file in `ls .`; do\n    echo $file\n  done\n```\n\n#### `while` loops\n\n```just\nwhile:\n  while `server-is-dead`; do ping -c 1 server; done\n```\n\n```just\nwhile:\n  while `server-is-dead`; do \\\n    ping -c 1 server; \\\n  done\n```\n\n```just\nwhile:\n  #!/usr/bin/env sh\n  while `server-is-dead`; do\n    ping -c 1 server\n  done\n```\n\n#### Outside Recipe Bodies\n\nParenthesized expressions can span multiple lines:\n\n```just\nabc := ('a' +\n        'b'\n         + 'c')\n\nabc2 := (\n  'a' +\n  'b' +\n  'c'\n)\n\nfoo param=('foo'\n      + 'bar'\n    ):\n  echo {{param}}\n\nbar: (foo\n        'Foo'\n     )\n  echo 'Bar!'\n```\n\nLines ending with a backslash continue on to the next line as if the lines were\njoined by whitespace<sup>1.15.0</sup>:\n\n```just\na := 'foo' + \\\n     'bar'\n\nfoo param1 \\\n  param2='foo' \\\n  *varparam='': dep1 \\\n                (dep2 'foo')\n  echo {{param1}} {{param2}} {{varparam}}\n\ndep1: \\\n    # this comment is not part of the recipe body\n  echo 'dep1'\n\ndep2 \\\n  param:\n    echo 'Dependency with parameter {{param}}'\n```\n\nBackslash line continuations can also be used in interpolations. The line\nfollowing the backslash must be indented.\n\n```just\nrecipe:\n  echo '{{ \\\n  \"This interpolation \" + \\\n    \"has a lot of text.\" \\\n  }}'\n  echo 'back to recipe body'\n```\n\n### Command-line Options\n\n`just` supports a number of useful command-line options for listing, dumping,\nand debugging recipes and variables:\n\n```console\n$ just --list\nAvailable recipes:\n  js\n  perl\n  polyglot\n  python\n  ruby\n$ just --show perl\nperl:\n  #!/usr/bin/env perl\n  print \"Larry Wall says Hi!\\n\";\n$ just --show polyglot\npolyglot: python js perl sh ruby\n```\n\n#### Setting Command-line Options with Environment Variables\n\nSome command-line options can be set with environment variables\n\nFor example, unstable features can be enabled either with the `--unstable`\nflag:\n\n```console\n$ just --unstable\n```\n\nOr by setting the `JUST_UNSTABLE` environment variable:\n\n```console\n$ export JUST_UNSTABLE=1\n$ just\n```\n\nSince environment variables are inherited by child processes, command-line\noptions set with environment variables are inherited by recursive invocations\nof `just`, where as command line options set with arguments are not.\n\nConsult `just --help` for which options can be set with environment variables.\n\n### Private Recipes\n\nRecipes and aliases whose name starts with a `_` are omitted from `just --list`:\n\n```just\ntest: _test-helper\n  ./bin/test\n\n_test-helper:\n  ./bin/super-secret-test-helper-stuff\n```\n\n```console\n$ just --list\nAvailable recipes:\n    test\n```\n\nAnd from `just --summary`:\n\n```console\n$ just --summary\ntest\n```\n\nThe `[private]` attribute<sup>1.10.0</sup> may also be used to hide recipes or\naliases without needing to change the name:\n\n```just\n[private]\nfoo:\n\n[private]\nalias b := bar\n\nbar:\n```\n\n```console\n$ just --list\nAvailable recipes:\n    bar\n```\n\nThis is useful for helper recipes which are only meant to be used as\ndependencies of other recipes.\n\n### Quiet Recipes\n\nA recipe name may be prefixed with `@` to invert the meaning of `@` before each\nline:\n\n```just\n@quiet:\n  echo hello\n  echo goodbye\n  @# all done!\n```\n\nNow only the lines starting with `@` will be echoed:\n\n```console\n$ just quiet\nhello\ngoodbye\n# all done!\n```\n\nAll recipes in a Justfile can be made quiet with `set quiet`:\n\n```just\nset quiet\n\nfoo:\n  echo \"This is quiet\"\n\n@foo2:\n  echo \"This is also quiet\"\n```\n\nThe `[no-quiet]` attribute overrides this setting:\n\n```just\nset quiet\n\nfoo:\n  echo \"This is quiet\"\n\n[no-quiet]\nfoo2:\n  echo \"This is not quiet\"\n```\n\nShebang recipes are quiet by default:\n\n```just\nfoo:\n  #!/usr/bin/env bash\n  echo 'Foo!'\n```\n\n```console\n$ just foo\nFoo!\n```\n\nAdding `@` to a shebang recipe name makes `just` print the recipe before\nexecuting it:\n\n```just\n@bar:\n  #!/usr/bin/env bash\n  echo 'Bar!'\n```\n\n```console\n$ just bar\n#!/usr/bin/env bash\necho 'Bar!'\nBar!\n```\n\n`just` normally prints error messages when a recipe line fails. These error\nmessages can be suppressed using the `[no-exit-message]`<sup>1.7.0</sup>\nattribute. You may find this especially useful with a recipe that wraps a tool:\n\n```just\ngit *args:\n    @git {{args}}\n```\n\n```console\n$ just git status\nfatal: not a git repository (or any of the parent directories): .git\nerror: Recipe `git` failed on line 2 with exit code 128\n```\n\nAdd the attribute to suppress the exit error message when the tool exits with a\nnon-zero code:\n\n```just\n[no-exit-message]\ngit *args:\n    @git {{args}}\n```\n\n```console\n$ just git status\nfatal: not a git repository (or any of the parent directories): .git\n```\n\n### Selecting Recipes to Run With an Interactive Chooser\n\nThe `--choose` subcommand makes `just` invoke a chooser to select which recipes\nto run. Choosers should read lines containing recipe names from standard input\nand print one or more of those names separated by spaces to standard output.\n\nBecause there is currently no way to run a recipe that requires arguments with\n`--choose`, such recipes will not be given to the chooser. Private recipes and\naliases are also skipped.\n\nThe chooser can be overridden with the `--chooser` flag. If `--chooser` is not\ngiven, then `just` first checks if `$JUST_CHOOSER` is set. If it isn't, then\nthe chooser defaults to `fzf`, a popular fuzzy finder.\n\nArguments can be included in the chooser, i.e. `fzf --exact`.\n\nThe chooser is invoked in the same way as recipe lines. For example, if the\nchooser is `fzf`, it will be invoked with `sh -cu 'fzf'`, and if the shell, or\nthe shell arguments are overridden, the chooser invocation will respect those\noverrides.\n\nIf you'd like `just` to default to selecting recipes with a chooser, you can\nuse this as your default recipe:\n\n```just\ndefault:\n  @just --choose\n```\n\n### Invoking `justfile`s in Other Directories\n\nIf the first argument passed to `just` contains a `/`, then the following\noccurs:\n\n1.  The argument is split at the last `/`.\n\n2.  The part before the last `/` is treated as a directory. `just` will start\n    its search for the `justfile` there, instead of in the current directory.\n\n3.  The part after the last slash is treated as a normal argument, or ignored\n    if it is empty.\n\nThis may seem a little strange, but it's useful if you wish to run a command in\na `justfile` that is in a subdirectory.\n\nFor example, if you are in a directory which contains a subdirectory named\n`foo`, which contains a `justfile` with the recipe `build`, which is also the\ndefault recipe, the following are all equivalent:\n\n```console\n$ (cd foo && just build)\n$ just foo/build\n$ just foo/\n```\n\nAdditional recipes after the first are sought in the same `justfile`. For\nexample, the following are both equivalent:\n\n```console\n$ just foo/a b\n$ (cd foo && just a b)\n```\n\nAnd will both invoke recipes `a` and `b` in `foo/justfile`.\n\n### Imports\n\nOne `justfile` can include the contents of another using `import` statements.\n\nIf you have the following `justfile`:\n\n```justfile\nimport 'foo/bar.just'\n\na: b\n  @echo A\n```\n\nAnd the following text in `foo/bar.just`:\n\n```just\nb:\n  @echo B\n```\n\n`foo/bar.just` will be included in `justfile` and recipe `b` will be defined:\n\n```console\n$ just b\nB\n$ just a\nB\nA\n```\n\nThe `import` path can be absolute or relative to the location of the justfile\ncontaining it. A leading `~/` in the import path is replaced with the current\nusers home directory.\n\nJustfiles are insensitive to order, so included files can reference variables\nand recipes defined after the `import` statement.\n\nImported files can themselves contain `import`s, which are processed\nrecursively.\n\n`allow-duplicate-recipes` and `allow-duplicate-variables` allow duplicate\nrecipes and variables, respectively, to override each other, instead of\nproducing an error.\n\nWithin a module, later definitions override earlier definitions:\n\n```just\nset allow-duplicate-recipes\n\nfoo:\n\nfoo:\n  echo 'yes'\n```\n\nWhen `import`s are involved, things unfortunately get much more complicated and\nhard to explain.\n\nShallower definitions always override deeper definitions, so recipes at the top\nlevel will override recipes in imports, and recipes in an import will override\nrecipes in an import which itself imports those recipes.\n\nWhen two duplicate definitions are imported and are at the same depth, the one\nfrom the earlier import will override the one from the later import.\n\nThis is because `just` uses a stack when processing imports, pushing imports\nonto the stack in source-order, and always processing the top of the stack\nnext, so earlier imports are actually handled later by the compiler.\n\nThis is definitely a bug, but since `just` has very strong backwards\ncompatibility guarantees and we take enormous pains not to break anyone's\n`justfile`, we have created issue #2540 to discuss whether or not we can\nactually fix it.\n\nImports may be made optional by putting a `?` after the `import` keyword:\n\n```just\nimport? 'foo/bar.just'\n```\n\nImporting the same source file multiple times is not an error<sup>1.37.0</sup>.\nThis allows importing multiple justfiles, for example `foo.just` and\n`bar.just`, which both import a third justfile containing shared recipes, for\nexample `baz.just`, without the duplicate import of `baz.just` being an error:\n\n```justfile\n# justfile\nimport 'foo.just'\nimport 'bar.just'\n```\n\n```justfile\n# foo.just\nimport 'baz.just'\nfoo: baz\n```\n\n```justfile\n# bar.just\nimport 'baz.just'\nbar: baz\n```\n\n```just\n# baz\nbaz:\n```\n\n### Modules\n\nA `justfile` can declare modules using `mod` statements<sup>1.19.0</sup>.\n\n`mod` statements were stabilized in `just`<sup>1.31.0</sup>. In earlier\nversions, you'll need to use the `--unstable` flag, `set unstable`, or set the\n`JUST_UNSTABLE` environment variable to use them.\n\nIf you have the following `justfile`:\n\n```justfile\nmod bar\n\na:\n  @echo A\n```\n\nAnd the following text in `bar.just`:\n\n```just\nb:\n  @echo B\n```\n\n`bar.just` will be included in `justfile` as a submodule. Recipes, aliases, and\nvariables defined in one submodule cannot be used in another, and each module\nuses its own settings.\n\nRecipes in submodules can be invoked as subcommands:\n\n```console\n$ just bar b\nB\n```\n\nOr with path syntax:\n\n```console\n$ just bar::b\nB\n```\n\nIf a module is named `foo`, just will search for the module file in `foo.just`,\n`foo/mod.just`, `foo/justfile`, and `foo/.justfile`. In the latter two cases,\nthe module file may have any capitalization.\n\nModule statements may be of the form:\n\n```justfile\nmod foo 'PATH'\n```\n\nWhich loads the module's source file from `PATH`, instead of from the usual\nlocations. A leading `~/` in `PATH` is replaced with the current user's home\ndirectory. `PATH` may point to the module source file itself, or to a directory\ncontaining the module source file with the name `mod.just`, `justfile`, or\n`.justfile`. In the latter two cases, the module file may have any\ncapitalization.\n\nEnvironment files are only loaded for the root justfile, and loaded environment\nvariables are available in submodules. Settings in submodules that affect\nenvironment file loading are ignored.\n\nRecipes in submodules without the `[no-cd]` attribute run with the working\ndirectory set to the directory containing the submodule source file.\n\n`justfile()` and `justfile_directory()` always return the path to the root\njustfile and the directory that contains it, even when called from submodule\nrecipes.\n\nModules may be made optional by putting a `?` after the `mod` keyword:\n\n```just\nmod? foo\n```\n\nMissing source files for optional modules do not produce an error.\n\nOptional modules with no source file do not conflict, so you can have multiple\nmod statements with the same name, but with different source file paths, as\nlong as at most one source file exists:\n\n```just\nmod? foo 'bar.just'\nmod? foo 'baz.just'\n```\n\nModules may be given doc comments which appear in `--list`\noutput<sup>1.30.0</sup>:\n\n```justfile\n# foo is a great module!\nmod foo\n```\n\n```console\n$ just --list\nAvailable recipes:\n    foo ... # foo is a great module!\n```\n\nModules are still missing a lot of features, for example, the ability to refer\nto variables in other modules. See the [module improvement tracking\nissue](https://github.com/casey/just/issues/2252) for more information.\n\n### Hiding `justfile`s\n\n`just` looks for `justfile`s named `justfile` and `.justfile`, which can be\nused to keep a `justfile` hidden.\n\n### Just Scripts\n\nBy adding a shebang line to the top of a `justfile` and making it executable,\n`just` can be used as an interpreter for scripts:\n\n```console\n$ cat > script <<EOF\n#!/usr/bin/env just --justfile\n\nfoo:\n  echo foo\nEOF\n$ chmod +x script\n$ ./script foo\necho foo\nfoo\n```\n\nWhen a script with a shebang is executed, the system supplies the path to the\nscript as an argument to the command in the shebang. So, with a shebang of\n`#!/usr/bin/env just --justfile`, the command will be `/usr/bin/env just --justfile PATH_TO_SCRIPT`.\n\nWith the above shebang, `just` will change its working directory to the\nlocation of the script. If you'd rather leave the working directory unchanged,\nuse `#!/usr/bin/env just --working-directory . --justfile`.\n\nNote: Shebang line splitting is not consistent across operating systems. The\nprevious examples have only been tested on macOS. On Linux, you may need to\npass the `-S` flag to `env`:\n\n```just\n#!/usr/bin/env -S just --justfile\n\ndefault:\n  echo foo\n```\n\n### Formatting and dumping `justfile`s\n\nEach `justfile` has a canonical formatting with respect to whitespace and\nnewlines.\n\nYou can overwrite the current justfile with a canonically-formatted version\nusing the currently-unstable `--fmt` flag:\n\n```console\n$ cat justfile\n# A lot of blank lines\n\n\n\n\n\nsome-recipe:\n  echo \"foo\"\n$ just --fmt --unstable\n$ cat justfile\n# A lot of blank lines\n\nsome-recipe:\n    echo \"foo\"\n```\n\nInvoking `just --fmt --check --unstable` runs `--fmt` in check mode. Instead of\noverwriting the `justfile`, `just` will exit with an exit code of 0 if it is\nformatted correctly, and will exit with 1 and print a diff if it is not.\n\nYou can use the `--dump` command to output a formatted version of the\n`justfile` to stdout:\n\n```console\n$ just --dump > formatted-justfile\n```\n\nThe `--dump` command can be used with `--dump-format json` to print a JSON\nrepresentation of a `justfile`.\n\n### Fallback to parent `justfile`s\n\nIf a recipe is not found in a `justfile` and the `fallback` setting is set,\n`just` will look for `justfile`s in the parent directory and up, until it\nreaches the root directory. `just` will stop after it reaches a `justfile` in\nwhich the `fallback` setting is `false` or unset.\n\nAs an example, suppose the current directory contains this `justfile`:\n\n```just\nset fallback\nfoo:\n  echo foo\n```\n\nAnd the parent directory contains this `justfile`:\n\n```just\nbar:\n  echo bar\n```\n\n```console\n$ just bar\nTrying ../justfile\necho bar\nbar\n```\n\n### Avoiding Argument Splitting\n\nGiven this `justfile`:\n\n```just\nfoo argument:\n  touch {{argument}}\n```\n\nThe following command will create two files, `some` and `argument.txt`:\n\n```console\n$ just foo \"some argument.txt\"\n```\n\nThe user's shell will parse `\"some argument.txt\"` as a single argument, but\nwhen `just` replaces `touch {{argument}}` with `touch some argument.txt`, the\nquotes are not preserved, and `touch` will receive two arguments.\n\nThere are a few ways to avoid this: quoting, positional arguments, and exported\narguments.\n\n#### Quoting\n\nQuotes can be added around the `{{argument}}` interpolation:\n\n```just\nfoo argument:\n  touch '{{argument}}'\n```\n\nThis preserves `just`'s ability to catch variable name typos before running,\nfor example if you were to write `{{argument}}`, but will not do what you want\nif the value of `argument` contains single quotes.\n\n#### Positional Arguments\n\nThe `positional-arguments` setting causes all arguments to be passed as\npositional arguments, allowing them to be accessed with `$1`, `$2`, …, and\n`$@`, which can be then double-quoted to avoid further splitting by the shell:\n\n```just\nset positional-arguments\n\nfoo argument:\n  touch \"$1\"\n```\n\nThis defeats `just`'s ability to catch typos, for example if you type `$2`\ninstead of `$1`, but works for all possible values of `argument`, including\nthose with double quotes.\n\n#### Exported Arguments\n\nAll arguments are exported when the `export` setting is set:\n\n```just\nset export\n\nfoo argument:\n  touch \"$argument\"\n```\n\nOr individual arguments may be exported by prefixing them with `$`:\n\n```just\nfoo $argument:\n  touch \"$argument\"\n```\n\nThis defeats `just`'s ability to catch typos, for example if you type\n`$argument`, but works for all possible values of `argument`, including those\nwith double quotes.\n\n### Configuring the Shell\n\nThere are a number of ways to configure the shell for linewise recipes, which\nare the default when a recipe does not start with a `#!` shebang. Their\nprecedence, from highest to lowest, is:\n\n1. The `--shell` and `--shell-arg` command line options. Passing either of\n   these will cause `just` to ignore any settings in the current justfile.\n2. `set windows-shell := [...]`\n3. `set windows-powershell` (deprecated)\n4. `set shell := [...]`\n\nSince `set windows-shell` has higher precedence than `set shell`, you can use\n`set windows-shell` to pick a shell on Windows, and `set shell` to pick a shell\nfor all other platforms.\n\n### Timestamps\n\n`just` can print timestamps before each recipe commands:\n\n```just\nrecipe:\n  echo one\n  sleep 2\n  echo two\n```\n\n```\n$ just --timestamp recipe\n[07:28:46] echo one\none\n[07:28:46] sleep 2\n[07:28:48] echo two\ntwo\n```\n\nBy default, timestamps are formatted as `HH:MM:SS`. The format can be changed\nwith `--timestamp-format`:\n\n```\n$ just --timestamp recipe --timestamp-format '%H:%M:%S%.3f %Z'\n[07:32:11:.349 UTC] echo one\none\n[07:32:11:.350 UTC] sleep 2\n[07:32:13:.352 UTC] echo two\ntwo\n```\n\nThe argument to `--timestamp-format` is a `strftime`-style format string, see\nthe\n[`chrono` library docs](https://docs.rs/chrono/latest/chrono/format/strftime/index.html)\nfor details.\n\n### Signal Handling\n\n[Signals](https://en.wikipedia.org/wiki/Signal_(IPC)) are messages sent to\nrunning programs to trigger specific behavior. For example, `SIGINT` is sent to\nall processes in the terminal forground process group when `CTRL-C` is pressed.\n\n`just` tries to exit when requested by a signal, but it also tries to avoid\nleaving behind running child proccesses, two goals which are somewhat in\nconflict.\n\nIf `just` exits leaving behind child processes, the user will have no recourse\nbut to `ps aux | grep` for the children and manually `kill` them, a tedious\nendevour.\n\n#### Fatal Signals\n\n`SIGHUP`, `SIGINT`, and `SIGQUIT` are generated when the user closes the\nterminal, types `ctrl-c`, or types `ctrl-\\`, respectively, and are sent to all\nprocesses in the foreground process group.\n\n`SIGTERM` is the default signal sent by the `kill` command, and is delivered\nonly to its intended victim.\n\nWhen a child process is not running, `just` will exit immediately on receipt of\nany of the above signals.\n\nWhen a child process *is* running, `just` will wait until it terminates, to\navoid leaving it behind.\n\nAdditionally, on receipt of `SIGTERM`, `just` will forward `SIGTERM` to any\nrunning children<sup>1.41.0</sup>, since unlike other fatal signals, `SIGTERM`,\nwas likely sent to `just` alone.\n\nRegardless of whether a child process terminates successfully after `just`\nreceives a fatal signal, `just` halts execution.\n\n#### `SIGINFO`\n\n`SIGINFO` is sent to all processes in the foreground process group when the\nuser types `ctrl-t` on\n[BSD](https://en.wikipedia.org/wiki/Berkeley_Software_Distribution)-derived\noperating systems, including MacOS, but not Linux.\n\n`just` responds by printing a list of all child process IDs and\ncommands<sup>1.41.0</sup>.\n\n#### Windows\n\nOn Windows, `just` behaves as if it had received `SIGINT` when the user types\n`ctrl-c`. Other signals are unsupported.\n\nChangelog\n---------\n\nA changelog for the latest release is available in\n[CHANGELOG.md](https://raw.githubusercontent.com/casey/just/master/CHANGELOG.md).\nChangelogs for previous releases are available on\n[the releases page](https://github.com/casey/just/releases). `just --changelog`\ncan also be used to make a `just` binary print its changelog.\n\nMiscellanea\n-----------\n\n### Re-running recipes when files change\n\n[`watchexec`](https://github.com/mattgreen/watchexec) can re-run any command\nwhen files change.\n\nTo re-run the recipe `foo` when any file changes:\n\n```console\nwatchexec just foo\n```\n\nSee `watchexec --help` for more info, including how to specify which files\nshould be watched for changes.\n\n### Parallelism\n\nDependencies may be run in parallel with the `[parallel]` attribute.\n\nIn this `justfile`, `foo`, `bar`, and `baz` will execute in parallel when\n`main` is run:\n\n```just\n[parallel]\nmain: foo bar baz\n\nfoo:\n  sleep 1\n\nbar:\n  sleep 1\n\nbaz:\n  sleep 1\n```\n\nGNU `parallel` may be used to run recipe lines concurrently:\n\n```just\nparallel:\n  #!/usr/bin/env -S parallel --shebang --ungroup --jobs {{ num_cpus() }}\n  echo task 1 start; sleep 3; echo task 1 done\n  echo task 2 start; sleep 3; echo task 2 done\n  echo task 3 start; sleep 3; echo task 3 done\n  echo task 4 start; sleep 3; echo task 4 done\n```\n\n### Shell Alias\n\nFor lightning-fast command running, put `alias j=just` in your shell's\nconfiguration file.\n\nIn `bash`, the aliased command may not keep the shell completion functionality\ndescribed in the next section. Add the following line to your `.bashrc` to use\nthe same completion function as `just` for your aliased command:\n\n```console\ncomplete -F _just -o bashdefault -o default j\n```\n\n### Shell Completion Scripts\n\nShell completion scripts for Bash, Elvish, Fish, Nushell, PowerShell, and Zsh\nare available [release archives](https://github.com/casey/just/releases).\n\nThe `just` binary can also generate the same completion scripts at runtime\nusing `just --completions SHELL`:\n\n```console\n$ just --completions zsh > just.zsh\n```\n\nPlease refer to your shell's documentation for how to install them.\n\n*macOS Note:* Recent versions of macOS use zsh as the default shell. If you use\nHomebrew to install `just`, it will automatically install the most recent copy\nof the zsh completion script in the Homebrew zsh directory, which the built-in\nversion of zsh doesn't know about by default. It's best to use this copy of the\nscript if possible, since it will be updated whenever you update `just` via\nHomebrew. Also, many other Homebrew packages use the same location for\ncompletion scripts, and the built-in zsh doesn't know about those either. To\ntake advantage of `just` completion in zsh in this scenario, you can set\n`fpath` to the Homebrew location before calling `compinit`. Note also that Oh\nMy Zsh runs `compinit` by default. So your `.zshrc` file could look like this:\n\n```zsh\n# Init Homebrew, which adds environment variables\neval \"$(brew shellenv)\"\n\nfpath=($HOMEBREW_PREFIX/share/zsh/site-functions $fpath)\n\n# Then choose one of these options:\n# 1. If you're using Oh My Zsh, you can initialize it here\n# source $ZSH/oh-my-zsh.sh\n\n# 2. Otherwise, run compinit yourself\n# autoload -U compinit\n# compinit\n```\n\n### Man Page\n\n`just` can print its own man page with `just --man`. Man pages are written in\n[`roff`](https://en.wikipedia.org/wiki/Roff_%28software%29), a venerable markup\nlanguage and one of the first practical applications of Unix. If you have\n[`groff`](https://www.gnu.org/software/groff/) installed you can view the man\npage with  `just --man | groff -mandoc -Tascii | less`.\n\n### Grammar\n\nA non-normative grammar of `justfile`s can be found in\n[GRAMMAR.md](https://github.com/casey/just/blob/master/GRAMMAR.md).\n\n### just.sh\n\nBefore `just` was a fancy Rust program it was a tiny shell script that called\n`make`. You can find the old version in\n[contrib/just.sh](https://github.com/casey/just/blob/master/contrib/just.sh).\n\n### Global and User `justfile`s\n\nIf you want some recipes to be available everywhere, you have a few options.\n\n#### Global Justfile\n\n`just --global-justfile`, or `just -g` for short, searches the following paths,\nin-order, for a justfile:\n\n- `$XDG_CONFIG_HOME/just/justfile`\n- `$HOME/.config/just/justfile`\n- `$HOME/justfile`\n- `$HOME/.justfile`\n\nYou can put recipes that are used across many projects in a global justfile to\neasily invoke them from any directory.\n\n#### User justfile tips\n\nYou can also adopt some of the following workflows. These tips assume you've\ncreated a `justfile` at `~/.user.justfile`, but you can put this `justfile`\nat any convenient path on your system.\n\n##### Recipe Aliases\n\nIf you want to call the recipes in `~/.user.justfile` by name, and don't mind\ncreating an alias for every recipe, add the following to your shell's\ninitialization script:\n\n```console\nfor recipe in `just --justfile ~/.user.justfile --summary`; do\n  alias $recipe=\"just --justfile ~/.user.justfile --working-directory . $recipe\"\ndone\n```\n\nNow, if you have a recipe called `foo` in `~/.user.justfile`, you can just type\n`foo` at the command line to run it.\n\nIt took me way too long to realize that you could create recipe aliases like\nthis. Notwithstanding my tardiness, I am very pleased to bring you this major\nadvance in `justfile` technology.\n\n##### Forwarding Alias\n\nIf you'd rather not create aliases for every recipe, you can create a single alias:\n\n```console\nalias .j='just --justfile ~/.user.justfile --working-directory .'\n```\n\nNow, if you have a recipe called `foo` in `~/.user.justfile`, you can just type\n`.j foo` at the command line to run it.\n\nI'm pretty sure that nobody actually uses this feature, but it's there.\n\n¯\\\\\\_(ツ)\\_/¯\n\n##### Customization\n\nYou can customize the above aliases with additional options. For example, if\nyou'd prefer to have the recipes in your `justfile` run in your home directory,\ninstead of the current directory:\n\n```console\nalias .j='just --justfile ~/.user.justfile --working-directory ~'\n```\n\n### Node.js `package.json` Script Compatibility\n\nThe following export statement gives `just` recipes access to local Node module\nbinaries, and makes `just` recipe commands behave more like `script` entries in\nNode.js `package.json` files:\n\n```just\nexport PATH := \"./node_modules/.bin:\" + env_var('PATH')\n```\n\n### Paths on Windows\n\nOn Windows, all functions that return paths, except `invocation_directory()`\nwill return `\\`-separated paths. When not using PowerShell or `cmd.exe` these\npaths should be quoted to prevent the `\\`s from being interpreted as character\nescapes:\n\n```just\nls:\n    echo '{{absolute_path(\".\")}}'\n```\n\n`cygpath.exe` is an executable included in some distributions of Unix userlands\nfor Windows, including [Cygwin](https://www.cygwin.com/) and\n[Git](https://git-scm.com/downloads) for Windows.\n\n`just` uses `cygpath.exe` in two places:\n\nFor backwards compatibility, `invocation_directory()`, uses `cygpath.exe` to\nconvert the invocation directory into a unix-style `/`-separated path. Use\n`invocation_directory_native()` to get the native, Windows-style path. On unix,\n`invocation_directory()` and `invocation_directory_native()` both return the\nsame unix-style path.\n\n`cygpath.exe` is used also used to convert Unix-style shebang lines into\nWindows paths. As an alternative, the `[script]` attribute, currently unstable,\ncan be used, which does not depend on `cygpath.exe`.\n\nIf `cygpath.exe` is available, you can use it to convert between path styles:\n\n```just\nfoo_unix := '/hello/world'\nfoo_windows := shell('cygpath --windows $1', foo_unix)\n\nbar_windows := 'C:\\hello\\world'\nbar_unix := shell('cygpath --unix $1', bar_windows)\n```\n\n### Remote Justfiles\n\nIf you wish to include a `mod` or `import` source file in many `justfiles`\nwithout needing to duplicate it, you can use an optional `mod` or `import`,\nalong with a recipe to fetch the module source:\n\n```just\nimport? 'foo.just'\n\nfetch:\n  curl https://raw.githubusercontent.com/casey/just/master/justfile > foo.just\n```\n\nGiven the above `justfile`, after running `just fetch`, the recipes in\n`foo.just` will be available.\n\n### Printing Complex Strings\n\n`echo` can be used to print strings, but because it processes escape sequences,\nlike `\\n`, and different implementations of `echo` recognize different escape\nsequences, using `printf` is often a better choice.\n\n`printf` takes a C-style format string and any number of arguments, which are\ninterpolated into the format string.\n\nThis can be combined with indented, triple quoted strings to emulate shell\nheredocs.\n\nSubstitution complex strings into recipe bodies with `{…}` can also lead to\ntrouble as it may be split by the shell into multiple arguments depending on\nthe presence of whitespace and quotes. Exporting complex strings as environment\nvariables and referring to them with `\"$NAME\"`, note the double quotes, can\nalso help.\n\nPutting all this together, to print a string verbatim to standard output, with\nall its various escape sequences and quotes undisturbed:\n\n```just\nexport FOO := '''\n  a complicated string with\n  some dis\\tur\\bi\\ng escape sequences\n  and \"quotes\" of 'different' kinds\n'''\n\nbar:\n  printf %s \"$FOO\"\n```\n\n### Alternatives and Prior Art\n\nThere is no shortage of command runners! Some more or less similar alternatives\nto `just` include:\n\n- [make](https://en.wikipedia.org/wiki/Make_(software)): The Unix build tool\n  that inspired `just`. There are a few different modern day descendents of the\n  original `make`, including\n  [FreeBSD Make](https://www.freebsd.org/cgi/man.cgi?make(1)) and\n  [GNU Make](https://www.gnu.org/software/make/).\n- [task](https://github.com/go-task/task): A YAML-based command runner written\n  in Go.\n- [maid](https://github.com/egoist/maid): A Markdown-based command runner\n  written in JavaScript.\n- [microsoft/just](https://github.com/microsoft/just): A JavaScript-based\n  command runner written in JavaScript.\n- [cargo-make](https://github.com/sagiegurari/cargo-make): A command runner for\n  Rust projects.\n- [mmake](https://github.com/tj/mmake): A wrapper around `make` with a number\n  of improvements, including remote includes.\n- [robo](https://github.com/tj/robo): A YAML-based command runner written in\n  Go.\n- [mask](https://github.com/jakedeichert/mask): A Markdown-based command runner\n  written in Rust.\n- [makesure](https://github.com/xonixx/makesure): A simple and portable command\n  runner written in AWK and shell.\n- [haku](https://github.com/VladimirMarkelov/haku): A make-like command runner\n  written in Rust.\n- [mise](https://mise.jdx.dev/): A development environment tool manager written\n  in Rust supporting tasks in TOML files and standalone scripts.\n\nContributing\n------------\n\n`just` welcomes your contributions! `just` is released under the maximally\npermissive\n[CC0](https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt) public\ndomain dedication and fallback license, so your changes must also be released\nunder this license.\n\n### Getting Started\n\n`just` is written in Rust. Use\n[rustup](https://www.rust-lang.org/tools/install) to install a Rust toolchain.\n\n`just` is extensively tested. All new features must be covered by unit or\nintegration tests. Unit tests are under\n[src](https://github.com/casey/just/blob/master/src), live alongside the code\nbeing tested, and test code in isolation. Integration tests are in the [tests\ndirectory](https://github.com/casey/just/blob/master/tests) and test the `just`\nbinary from the outside by invoking `just` on a given `justfile` and set of\ncommand-line arguments, and checking the output.\n\nYou should write whichever type of tests are easiest to write for your feature\nwhile still providing good test coverage.\n\nUnit tests are useful for testing new Rust functions that are used internally\nand as an aid for development. A good example are the unit tests which cover\nthe\n[`unindent()` function](https://github.com/casey/just/blob/master/src/unindent.rs),\nused to unindent triple-quoted strings and backticks. `unindent()` has a bunch\nof tricky edge cases which are easy to exercise with unit tests that call\n`unindent()` directly.\n\nIntegration tests are useful for making sure that the final behavior of the\n`just` binary is correct. `unindent()` is also covered by integration tests\nwhich make sure that evaluating a triple-quoted string produces the correct\nunindented value. However, there are not integration tests for all possible\ncases. These are covered by faster, more concise unit tests that call\n`unindent()` directly.\n\nIntegration tests use the `Test` struct, a builder which allows for easily\ninvoking `just` with a given `justfile`, arguments, and environment variables,\nand checking the program's stdout, stderr, and exit code .\n\n### Contribution Workflow\n\n1. Make sure the feature is wanted. There should be an open issue about the\n   feature with a comment from [@casey](https://github.com/casey) saying that\n   it's a good idea or seems reasonable. If there isn't, open a new issue and\n   ask for feedback.\n\n   There are lots of good features which can't be merged, either because they\n   aren't backwards compatible, have an implementation which would\n   overcomplicate the codebase, or go against `just`'s design philosophy.\n\n2. Settle on the design of the feature. If the feature has multiple possible\n   implementations or syntaxes, make sure to nail down the details in the\n   issue.\n\n3. Clone `just` and start hacking. The best workflow is to have the code you're\n   working on in an editor alongside a job that re-runs tests whenever a file\n   changes. You can run such a job by installing\n   [cargo-watch](https://github.com/watchexec/cargo-watch) with `cargo install\n   cargo-watch` and running `just watch test`.\n\n4. Add a failing test for your feature. Most of the time this will be an\n   integration test which exercises the feature end-to-end. Look for an\n   appropriate file to put the test in in\n   [tests](https://github.com/casey/just/blob/master/tests), or add a new file\n   in [tests](https://github.com/casey/just/blob/master/tests) and add a `mod`\n   statement importing that file in\n   [tests/lib.rs](https://github.com/casey/just/blob/master/tests/lib.rs).\n\n5. Implement the feature.\n\n6. Run `just ci` to make sure that all tests, lints, and checks pass. Requires\n   [mdBook](https://github.com/rust-lang/mdBook) and\n   [mdbook-linkcheck](https://github.com/Michael-F-Bryan/mdbook-linkcheck).\n\n7. Open a PR with the new code that is editable by maintainers. PRs often\n   require rebasing and minor tweaks. If the PR is not editable by maintainers,\n   each rebase and tweak will require a round trip of code review. Your PR may\n   be summarily closed if it is not editable by maintainers.\n\n8. Incorporate feedback.\n\n9. Enjoy the sweet feeling of your PR getting merged!\n\nFeel free to open a draft PR at any time for discussion and feedback.\n\n### Hints\n\nHere are some hints to get you started with specific kinds of new features,\nwhich you can use in addition to the contribution workflow above.\n\n#### Adding a New Attribute\n\n1. Write a new integration test in\n   [tests/attributes.rs](https://github.com/casey/just/blob/master/tests/attributes.rs).\n\n2. Add a new variant to the\n   [`Attribute`](https://github.com/casey/just/blob/master/src/attribute.rs)\n   enum.\n\n3. Implement the functionality of the new attribute.\n\n4. Run `just ci` to make sure that all tests pass.\n\n### Janus\n\n[Janus](https://github.com/casey/janus) is a tool for checking whether a change\nto `just` breaks or changes the interpretation of existing `justfile`s. It\ncollects and analyzes public `justfile`s on GitHub.\n\nBefore merging a particularly large or gruesome change, Janus should be run to\nmake sure that nothing breaks. Don't worry about running Janus yourself, Casey\nwill happily run it for you on changes that need it.\n\n### Minimum Supported Rust Version\n\nThe minimum supported Rust version, or MSRV, is current stable Rust. It may\nbuild on older versions of Rust, but this is not guaranteed.\n\n### New Releases\n\nNew releases of `just` are made frequently so that users quickly get access to\nnew features.\n\nRelease commit messages use the following template:\n\n```\nRelease x.y.z\n\n- Bump version: x.y.z → x.y.z\n- Update changelog\n- Update changelog contributor credits\n- Update dependencies\n- Update version references in readme\n```\n\nFrequently Asked Questions\n--------------------------\n\n### What are the idiosyncrasies of Make that Just avoids?\n\n`make` has some behaviors which are confusing, complicated, or make it\nunsuitable for use as a general command runner.\n\nOne example is that under some circumstances, `make` won't actually run the\ncommands in a recipe. For example, if you have a file called `test` and the\nfollowing makefile:\n\n```just\ntest:\n  ./test\n```\n\n`make` will refuse to run your tests:\n\n```console\n$ make test\nmake: `test' is up to date.\n```\n\n`make` assumes that the `test` recipe produces a file called `test`. Since this\nfile exists and the recipe has no other dependencies, `make` thinks that it\ndoesn't have anything to do and exits.\n\nTo be fair, this behavior is desirable when using `make` as a build system, but\nnot when using it as a command runner. You can disable this behavior for\nspecific targets using `make`'s built-in\n[`.PHONY` target name](https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html),\nbut the syntax is verbose and can be hard to remember. The explicit list of\nphony targets, written separately from the recipe definitions, also introduces\nthe risk of accidentally defining a new non-phony target. In `just`, all\nrecipes are treated as if they were phony.\n\nOther examples of `make`'s idiosyncrasies include the difference between `=`\nand `:=` in assignments, the confusing error messages that are produced if you\nmess up your makefile, needing `$$` to use environment variables in recipes,\nand incompatibilities between different flavors of `make`.\n\n### What's the relationship between Just and Cargo build scripts?\n\n[`cargo` build scripts](http://doc.crates.io/build-script.html) have a pretty\nspecific use, which is to control how `cargo` builds your Rust project. This\nmight include adding flags to `rustc` invocations, building an external\ndependency, or running some kind of codegen step.\n\n`just`, on the other hand, is for all the other miscellaneous commands you\nmight run as part of development. Things like running tests in different\nconfigurations, linting your code, pushing build artifacts to a server,\nremoving temporary files, and the like.\n\nAlso, although `just` is written in Rust, it can be used regardless of the\nlanguage or build system your project uses.\n\nFurther Ramblings\n-----------------\n\nI personally find it very useful to write a `justfile` for almost every\nproject, big or small.\n\nOn a big project with multiple contributors, it's very useful to have a file\nwith all the commands needed to work on the project close at hand.\n\nThere are probably different commands to test, build, lint, deploy, and the\nlike, and having them all in one place is useful and cuts down on the time you\nhave to spend telling people which commands to run and how to type them.\n\nAnd, with an easy place to put commands, it's likely that you'll come up with\nother useful things which are part of the project's collective wisdom, but\nwhich aren't written down anywhere, like the arcane commands needed for some\npart of your revision control workflow, to install all your project's\ndependencies, or all the random flags you might need to pass to the build\nsystem.\n\nSome ideas for recipes:\n\n- Deploying/publishing the project\n\n- Building in release mode vs debug mode\n\n- Running in debug mode or with logging enabled\n\n- Complex git workflows\n\n- Updating dependencies\n\n- Running different sets of tests, for example fast tests vs slow tests, or\n  running them with verbose output\n\n- Any complex set of commands that you really should write down somewhere, if\n  only to be able to remember them\n\nEven for small, personal projects it's nice to be able to remember commands by\nname instead of ^Reverse searching your shell history, and it's a huge boon to\nbe able to go into an old project written in a random language with a\nmysterious build system and know that all the commands you need to do whatever\nyou need to do are in the `justfile`, and that if you type `just` something\nuseful (or at least interesting!) will probably happen.\n\nFor ideas for recipes, check out\n[this project's `justfile`](https://github.com/casey/just/blob/master/justfile),\nor some of the\n`justfile`s\n[out in the wild](https://github.com/search?q=path%3A**%2Fjustfile&type=code).\n\nAnyways, I think that's about it for this incredibly long-winded README.\n\nI hope you enjoy using `just` and find great success and satisfaction in all\nyour computational endeavors!\n\n😸\n\n[🔼 Back to the top!](#just)\n"
  },
  {
    "path": "README.中文.md",
    "content": "↖️ 目录\n\n<h1 align=\"center\"><code>just</code></h1>\n\n<div align=\"center\">\n  <a href=\"https://crates.io/crates/just\">\n    <img src=\"https://img.shields.io/crates/v/just.svg\" alt=\"crates.io version\">\n  </a>\n  <a href=\"https://github.com/casey/just/actions\">\n    <img src=\"https://github.com/casey/just/actions/workflows/ci.yaml/badge.svg\" alt=\"build status\">\n  </a>\n  <a href=\"https://github.com/casey/just/releases\">\n    <img src=\"https://img.shields.io/github/downloads/casey/just/total.svg\" alt=\"downloads\">\n  </a>\n  <a href=\"https://discord.gg/ezYScXR\">\n    <img src=\"https://img.shields.io/discord/695580069837406228?logo=discord\" alt=\"chat on discord\">\n  </a>\n  <a href=\"mailto:casey@rodarmor.com?subject=Thanks%20for%20Just!\">\n    <img src=\"https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg\" alt=\"say thanks\">\n  </a>\n</div>\n<br>\n\n`just` 为您提供一种保存和运行项目特有命令的便捷方式。\n\n本指南同时也可以以 [书](https://just.systems/man/zh/) 的形式提供在线阅读。\n\n命令，在此也称为配方，存储在一个名为 `justfile` 的文件中，其语法受 `make` 启发：\n\n![screenshot](https://raw.githubusercontent.com/casey/just/master/screenshot.png)\n\n然后你可以用 `just RECIPE` 运行它们：\n\n```sh\n$ just test-all\ncc *.c -o main\n./test --all\nYay, all your tests passed!\n```\n\n`just` 有很多很棒的特性，而且相比 `make` 有很多改进：\n\n- `just` 是一个命令运行器，而不是一个构建系统，所以它避免了许多 [`make` 的复杂性和特异性](#just-避免了-make-的哪些特异性)。不需要 `.PHONY` 配方!\n\n- 支持 Linux、MacOS 和 Windows，而且无需额外的依赖。(尽管如果你的系统没有 `sh`，你需要 [选择一个不同的 Shell](#shell))。\n\n- 错误具体且富有参考价值，语法错误将会与产生它们的上下文一起被报告。\n\n- 配方可以接受 [命令行参数](#配方参数)。\n\n- 错误会尽可能被静态地解决。未知的配方和循环依赖关系会在运行之前被报告。\n\n- `just` 可以 [加载`.env`文件](#环境变量加载)，简化环境变量注入。\n\n- 配方可以在 [命令行中列出](#列出可用的配方)。\n\n- 命令行自动补全脚本 [支持大多数流行的 Shell](#shell-自动补全脚本)。\n\n- 配方可以用 [任意语言](#用其他语言书写配方) 编写，如 Python 或 NodeJS。\n\n- `just` 可以从任何子目录中调用，而不仅仅是包含 `justfile` 的目录。\n\n- 不仅如此，还有 [更多](https://just.systems/man/zh/)！\n\n如果你在使用 `just` 方面需要帮助，请随时创建一个 Issue 或在 [Discord](https://discord.gg/ezYScXR) 上与我联系。我们随时欢迎功能请求和错误报告！\n\n安装\n------------\n\n### 预备知识\n\n`just` 应该可以在任何有合适的 `sh` 的系统上运行，包括 Linux、MacOS 和 BSD。\n\n在 Windows 上，`just` 可以使用 [Git for Windows](https://git-scm.com)、[GitHub Desktop](https://desktop.github.com) 或 [Cygwin](http://www.cygwin.com) 所提供的 `sh`。\n\n如果你不愿意安装 `sh`，也可以使用 `shell` 设置来指定你要使用的 Shell。\n\n比如 PowerShell：\n\n```just\n# 使用 PowerShell 替代 sh:\nset shell := [\"powershell.exe\", \"-c\"]\n\nhello:\n  Write-Host \"Hello, world!\"\n```\n\n…或者 `cmd.exe`:\n\n```just\n# 使用 cmd.exe 替代 sh:\nset shell := [\"cmd.exe\", \"/c\"]\n\nlist:\n  dir\n```\n\n你也可以使用命令行参数来设置 Shell。例如，若要使用 PowerShell 也可以用 `--shell powershell.exe --shell-arg -c` 启动`just`。\n\n(PowerShell 默认安装在 Windows 7 SP1 和 Windows Server 2008 R2 S1 及更高版本上，而 `cmd.exe` 相当麻烦，所以 PowerShell 被推荐给大多数 Windows 用户)\n\n### 安装包\n\n<table>\n  <thead>\n    <tr>\n      <th>操作系统</th>\n      <th>包管理器</th>\n      <th>安装包</th>\n      <th>命令</th>\n    </tr>\n  </thead>\n  <tbody>\n  <tr>\n    <td><a href=\"https://forge.rust-lang.org/release/platform-support.html\">Various</a></td>\n    <td><a href=\"https://www.rust-lang.org\">Cargo</a></td>\n    <td><a href=\"https://crates.io/crates/just\">just</a></td>\n    <td><code>cargo install just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://en.wikipedia.org/wiki/Microsoft_Windows\">Microsoft Windows</a></td>\n    <td><a href=\"https://scoop.sh\">Scoop</a></td>\n    <td><a href=\"https://github.com/ScoopInstaller/Main/blob/master/bucket/just.json\">just</a></td>\n    <td><code>scoop install just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://docs.brew.sh/Installation\">Various</a></td>\n    <td><a href=\"https://brew.sh\">Homebrew</a></td>\n    <td><a href=\"https://formulae.brew.sh/formula/just\">just</a></td>\n    <td><code>brew install just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://en.wikipedia.org/wiki/MacOS\">macOS</a></td>\n    <td><a href=\"https://www.macports.org\">MacPorts</a></td>\n    <td><a href=\"https://ports.macports.org/port/just/summary\">just</a></td>\n    <td><code>port install just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://www.archlinux.org\">Arch Linux</a></td>\n    <td><a href=\"https://wiki.archlinux.org/title/Pacman\">pacman</a></td>\n    <td><a href=\"https://archlinux.org/packages/community/x86_64/just/\">just</a></td>\n    <td><code>pacman -S just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://nixos.org/download.html#download-nix\">Various</a></td>\n    <td><a href=\"https://nixos.org/nix/\">Nix</a></td>\n    <td><a href=\"https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/just/package.nix\">just</a></td>\n    <td><code>nix-env -iA nixpkgs.just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://nixos.org/nixos/\">NixOS</a></td>\n    <td><a href=\"https://nixos.org/nix/\">Nix</a></td>\n    <td><a href=\"https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/just/package.nix\">just</a></td>\n    <td><code>nix-env -iA nixos.just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://getsol.us\">Solus</a></td>\n    <td><a href=\"https://getsol.us/articles/package-management/basics/en\">eopkg</a></td>\n    <td><a href=\"https://dev.getsol.us/source/just/\">just</a></td>\n    <td><code>eopkg install just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://voidlinux.org\">Void Linux</a></td>\n    <td><a href=\"https://wiki.voidlinux.org/XBPS\">XBPS</a></td>\n    <td><a href=\"https://github.com/void-linux/void-packages/blob/master/srcpkgs/just/template\">just</a></td>\n    <td><code>xbps-install -S just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://www.freebsd.org\">FreeBSD</a></td>\n    <td><a href=\"https://www.freebsd.org/doc/handbook/pkgng-intro.html\">pkg</a></td>\n    <td><a href=\"https://www.freshports.org/deskutils/just/\">just</a></td>\n    <td><code>pkg install just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://alpinelinux.org\">Alpine Linux</a></td>\n    <td><a href=\"https://wiki.alpinelinux.org/wiki/Alpine_Linux_package_management\">apk-tools</a></td>\n    <td><a href=\"https://pkgs.alpinelinux.org/package/edge/community/x86_64/just\">just</a></td>\n    <td><code>apk add just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://getfedora.org\">Fedora Linux</a></td>\n    <td><a href=\"https://dnf.readthedocs.io/en/latest/\">DNF</a></td>\n    <td><a href=\"https://src.fedoraproject.org/rpms/rust-just\">just</a></td>\n    <td><code>dnf install just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://www.gentoo.org\">Gentoo Linux</a></td>\n    <td><a href=\"https://wiki.gentoo.org/wiki/Portage\">Portage</a></td>\n    <td><a href=\"https://github.com/gentoo-mirror/guru/tree/master/sys-devel/just\">guru/sys-devel/just</a></td>\n    <td>\n      <code>eselect repository enable guru</code><br>\n      <code>emerge --sync guru</code><br>\n      <code>emerge sys-devel/just</code>\n    </td>\n  </tr>\n  <tr>\n    <td><a href=\"https://docs.conda.io/en/latest/miniconda.html#system-requirements\">Various</a></td>\n    <td><a href=\"https://docs.conda.io/projects/conda/en/latest/index.html\">Conda</a></td>\n    <td><a href=\"https://anaconda.org/conda-forge/just\">just</a></td>\n    <td><code>conda install -c conda-forge just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://en.wikipedia.org/wiki/Microsoft_Windows\">Microsoft Windows</a></td>\n    <td><a href=\"https://chocolatey.org\">Chocolatey</a></td>\n    <td><a href=\"https://github.com/michidk/just-choco\">just</a></td>\n    <td><code>choco install just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://snapcraft.io/docs/installing-snapd\">Various</a></td>\n    <td><a href=\"https://snapcraft.io\">Snap</a></td>\n    <td><a href=\"https://snapcraft.io/just\">just</a></td>\n    <td><code>snap install --edge --classic just</code></td>\n  </tr>\n  <tr>\n    <td><a href=\"https://github.com/casey/just/releases\">Various</a></td>\n    <td><a href=\"https://asdf-vm.com\">asdf</a></td>\n    <td><a href=\"https://github.com/olofvndrhr/asdf-just\">just</a></td>\n    <td>\n      <code>asdf plugin add just</code><br>\n      <code>asdf install just &lt;version&gt;</code>\n    </td>\n  </tr>\n  <tr>\n    <td><a href=\"https://packaging.python.org/tutorials/installing-packages\">Various</a></td>\n    <td><a href=\"https://pypi.org\">PyPI</a></td>\n    <td><a href=\"https://pypi.org/project/rust-just\">rust-just</a></td>\n    <td>\n      <code>pipx install rust-just</code><br>\n    </td>\n  </tr>\n  <tr>\n    <td><a href=\"https://docs.npmjs.com/packages-and-modules/getting-packages-from-the-registry\">Various</a></td>\n    <td><a href=\"https://www.npmjs.com\">npm</a></td>\n    <td><a href=\"https://www.npmjs.com/package/rust-just\">rust-just</a></td>\n    <td>\n      <code>npm install -g rust-just</code><br>\n    </td>\n  </tr>\n  <tr>\n    <td><a href=\"https://debian.org\">Debian</a> and <a href=\"https://ubuntu.com\">Ubuntu</a> derivatives</td>\n    <td><a href=\"https://mpr.makedeb.org\">MPR</a></td>\n    <td><a href=\"https://mpr.makedeb.org/packages/just\">just</a></td>\n    <td>\n      <code>git clone 'https://mpr.makedeb.org/just'</code><br>\n      <code>cd just</code><br>\n      <code>makedeb -si</code>\n    </td>\n  </tr>\n  <tr>\n    <td><a href=\"https://debian.org\">Debian</a> and <a href=\"https://ubuntu.com\">Ubuntu</a> derivatives</td>\n    <td><a href=\"https://docs.makedeb.org/prebuilt-mpr\">Prebuilt-MPR</a></td>\n    <td><a href=\"https://mpr.makedeb.org/packages/just\">just</a></td>\n    <td>\n      <sup><b>You must have the <a href=\"https://docs.makedeb.org/prebuilt-mpr/getting-started/#setting-up-the-repository\">Prebuilt-MPR set up</a> on your system in order to run this command.</b></sup><br>\n      <code>sudo apt install just</code>\n    </td>\n  </tr>\n  </tbody>\n</table>\n\n![package version table](https://repology.org/badge/vertical-allrepos/just.svg)\n\n### 预制二进制文件\n\nLinux、MacOS 和 Windows 的预制二进制文件可以在 [发布页](https://github.com/casey/just/releases) 上找到。\n\n你也可以在 Linux、MacOS 或 Windows 上使用下面的命令来下载最新的版本，只需将 `DEST` 替换为你想安装 `just` 的目录即可：\n\n```sh\ncurl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to DEST\n```\n\n例如，安装 `just` 到 `~/bin` 目录：\n\n```sh\n# 创建 ~/bin\nmkdir -p ~/bin\n\n# 下载并解压 just 到 ~/bin/just\ncurl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/bin\n\n# 在 Shell 搜索可执行文件的路径中添加`~/bin`\n# 这一行应该被添加到你的 Shell 初始化文件中，e.g. `~/.bashrc` 或者 `~/.zshrc`：\nexport PATH=\"$PATH:$HOME/bin\"\n\n# 现在 just 应该就可以执行了\njust --help\n```\n\n### GitHub Actions\n\n使用 [extractions/setup-just](https://github.com/extractions/setup-just):\n\n```yaml\n- uses: extractions/setup-just@v1\n  with:\n    just-version: 0.8 # optional semver specification, otherwise latest\n```\n\n使用 [taiki-e/install-action](https://github.com/taiki-e/install-action):\n\n```yaml\n- uses: taiki-e/install-action@just\n```\n\n### 发布 RSS 订阅\n\n`just` 的发布 [RSS 订阅](https://en.wikipedia.org/wiki/RSS) 可以在 [这里](https://github.com/casey/just/releases.atom) 找到。\n\n### Node.js 安装\n\n[just-install](https://npmjs.com/package/just-install) 可用于在 Node.js 应用程序中自动安装 `just`。\n\n`just` 是一个很赞的比 npm 脚本更强大的替代品。如果你想在 Node.js 应用程序的依赖中包含 `just`，可以通过 `just-install`，它将在本机安装一个针对特定平台的二进制文件作为 `npm install` 安装结果的一部分。这样就不需要每个开发者使用上述提到的步骤独立安装 `just`。安装后，`just` 命令将在 npm 脚本或 npx 中工作。这对那些想让项目的设置过程尽可能简单的团队来说是很有用的。\n\n想了解更多信息, 请查看 [just-install 说明文件](https://github.com/brombal/just-install#readme)。\n\n向后兼容性\n-----------------------\n\n随着 1.0 版本的发布，`just` 突出对向后兼容性和稳定性的强烈承诺。\n\n未来的版本将不会引入向后不兼容的变化，不会使现有的 `justfile` 停止工作，或破坏命令行界面的正常调用。\n\n然而，这并不排除修复全面的错误，即使这样做可能会破坏依赖其行为的 `justfiles`。\n\n永远不会有一个 `just` 2.0。任何理想的向后兼容的变化都是在每个 `justfile` 的基础上选择性加入的，所以用户可以在他们的闲暇时间进行迁移。\n\n还没有准备好稳定化的功能将在 `--unstable` 标志后被选择性启用。由 `--unstable` 启用的功能可能会在任何时候以不兼容的方式发生变化。\n\n编辑器支持\n--------------\n\n`justfile` 的语法与 `make` 非常接近，你可以让你的编辑器对 `just` 使用 `make` 语法高亮。\n\n### Vim 和 Neovim\n\n#### `vim-just`\n\n[vim-just](https://github.com/NoahTheDuke/vim-just) 插件可以为 vim 提供 `justfile` 语法高亮显示。\n\n你可以用你喜欢的软件包管理器安装它，如 [Plug](https://github.com/junegunn/vim-plug)：\n\n```vim\ncall plug#begin()\n\nPlug 'NoahTheDuke/vim-just'\n\ncall plug#end()\n```\n\n或者使用 Vim 的内置包支持：\n\n```sh\nmkdir -p ~/.vim/pack/vendor/start\ncd ~/.vim/pack/vendor/start\ngit clone https://github.com/NoahTheDuke/vim-just.git\n```\n\n#### `tree-sitter-just`\n\n[tree-sitter-just](https://github.com/IndianBoy42/tree-sitter-just) 是一个针对 Neovim 的 [Nvim Treesitter](https://github.com/nvim-treesitter/nvim-treesitter) 插件。\n\n#### Makefile 语法高亮\n\nVim 内置的 makefile 语法高亮对 `justfile` 来说并不完美，但总比没有好。你可以把以下内容放在 `~/.vim/filetype.vim` 中：\n\n```vimscript\nif exists(\"did_load_filetypes\")\n  finish\nendif\n\naugroup filetypedetect\n  au BufNewFile,BufRead justfile setf make\naugroup END\n```\n或者在单个 `justfile` 中添加以下内容，以在每个文件的基础上启用 `make` 模式：\n\n```text\n# vim: set ft=make :\n```\n\n### Emacs\n\n[just-mode](https://github.com/leon-barrett/just-mode.el) 可以为 `justfile` 提供语法高亮和自动缩进。它可以在 [MELPA](https://melpa.org/) 上通过 [just-mode](https://melpa.org/#/just-mode) 获得。\n\n[justl](https://github.com/psibi/justl.el) 提供了执行和列出配方的命令。\n\n你可以在一个单独的 `justfile` 中添加以下内容，以便对每个文件启用 `make` 模式：\n\n```text\n# Local Variables:\n# mode: makefile\n# End:\n```\n\n### Visual Studio Code\n\nVS Code 的一个插件可以在 [这里](https://github.com/nefrob/vscode-just) 找到。\n  \n不再维护的 VS Code 插件有 [skellock/vscode-just](https://github.com/skellock/vscode-just) 和 [sclu1034/vscode-just](https://github.com/sclu1034/vscode-just)。\n\n### JetBrains IDEs\n\n由 [linux_china](https://github.com/linux-china) 为 JetBrains IDEs 提供的插件可 [由此获得](https://plugins.jetbrains.com/plugin/18658-just)。\n\n### Kakoune\n\nKakoune 已经内置支持 `justfile` 语法高亮，这要感谢 TeddyDD。\n\n### Sublime Text\n\n由 [nk9](https://github.com/nk9) 提供的 [Just 包](https://github.com/nk9/just_sublime) 支持 `just` 语法高亮，同时还有其它工具，这些可以在 [PackageControl](https://packagecontrol.io/packages/Just) 上找到。\n\n### 其它编辑器\n\n欢迎给我发送必要的命令，以便在你选择的编辑器中实现语法高亮，这样我就可以把它们放在这里。\n\n快速开始\n-----------\n\n参见 [安装部分](#安装) 了解如何在你的电脑上安装 `just`。试着运行 `just --version` 以确保它被正确安装。\n\n关于语法的概述，请查看这个 [速查表](https://cheatography.com/linux-china/cheat-sheets/justfile/)。\n\n一旦 `just` 安装完毕并开始工作，在你的项目根目录创建一个名为 `justfile` 的文件，内容如下：\n\n```just\nrecipe-name:\n  echo 'This is a recipe!'\n\n# 这是一行注释\nanother-recipe:\n  @echo 'This is another recipe.'\n```\n\n当你调用 `just` 时，它会在当前目录和父目录寻找文件 `justfile`，所以你可以从你项目的任何子目录中调用它。\n\n搜索 `justfile` 是不分大小写的，所以任何大小写，如 `Justfile`、`JUSTFILE` 或 `JuStFiLe` 都可以工作。`just` 也会寻找名字为 `.justfile` 的文件，以便你打算隐藏一个 `justfile`。\n\n运行 `just` 时未传参数，则运行 `justfile` 中的第一个配方：\n\n```sh\n$ just\necho 'This is a recipe!'\nThis is a recipe!\n```\n\n通过一个或多个参数指定要运行的配方：\n\n```sh\n$ just another-recipe\nThis is another recipe.\n```\n\n`just` 在运行每条命令前都会将其打印到标准错误中，这就是为什么 `echo 'This is a recipe!'` 被打印出来。对于以 `@` 开头的行，这将被抑制，这就是为什么 `echo 'This is another recipe.'` 没有被打印。\n\n如果一个命令失败，配方就会停止运行。这里 `cargo publish` 只有在 `cargo test` 成功后才会运行：\n\n```just\npublish:\n  cargo test\n  # 前面的测试通过才会执行 publish!\n  cargo publish\n```\n\n配方可以依赖其他配方。在这里，`test` 配方依赖于 `build` 配方，所以 `build` 将在 `test` 之前运行：\n\n```just\nbuild:\n  cc main.c foo.c bar.c -o main\n\ntest: build\n  ./test\n\nsloc:\n  @echo \"`wc -l *.c` lines of code\"\n```\n\n```sh\n$ just test\ncc main.c foo.c bar.c -o main\n./test\ntesting… all tests passed!\n```\n\n没有依赖关系的配方将按照命令行上给出的顺序运行：\n\n```sh\n$ just build sloc\ncc main.c foo.c bar.c -o main\n1337 lines of code\n```\n\n依赖项总是先运行，即使它们被放在依赖它们的配方之后：\n\n```sh\n$ just test build\ncc main.c foo.c bar.c -o main\n./test\ntesting… all tests passed!\n```\n\n示例\n--------\n\n在 [Examples 目录](https://github.com/casey/just/tree/master/examples) 中可以找到各种 `justfile` 的例子。\n\n特性介绍\n--------\n\n### 默认配方\n\n当 `just` 被调用而没有传入任何配方时，它会运行 `justfile` 中的第一个配方。这个配方可能是项目中最常运行的命令，比如运行测试：\n\n```just\ntest:\n  cargo test\n```\n\n你也可以使用依赖关系来默认运行多个配方：\n\n```just\ndefault: lint build test\n\nbuild:\n  echo Building…\n\ntest:\n  echo Testing…\n\nlint:\n  echo Linting…\n```\n\n在没有合适配方作为默认配方的情况下，你也可以在 `justfile` 的开头添加一个配方，用于列出可用的配方：\n\n```just\ndefault:\n  just --list\n```\n\n### 列出可用的配方\n\n可以用 `just --list` 按字母顺序列出配方：\n\n```sh\n$ just --list\nAvailable recipes:\n    build\n    test\n    deploy\n    lint\n```\n\n`just --summary` 以更简洁的形式列出配方：\n\n```sh\n$ just --summary\nbuild test deploy lint\n```\n\n传入 `--unsorted` 选项可以按照它们在 `justfile` 中出现的顺序打印配方：\n\n```just\ntest:\n  echo 'Testing!'\n\nbuild:\n  echo 'Building!'\n```\n\n```sh\n$ just --list --unsorted\nAvailable recipes:\n    test\n    build\n```\n\n```sh\n$ just --summary --unsorted\ntest build\n```\n\n如果你想让 `just` 默认列出 `justfile` 中的配方，你可以使用这个作为默认配方：\n\n```just\ndefault:\n  @just --list\n```\n\n请注意，你可能需要在上面这一行中添加 `--justfile {{justfile()}}`。没有它，如果你执行 `just -f /some/distant/justfile -d .` 或 `just -f ./non-standard-justfile` 配方中的普通 `just --list` 就不一定会使用你提供的文件，它将试图在你的当前路径中找到一个 `justfile`，甚至可能导致 `No justfile found` 的错误。\n\n标题文本可以用 `--list-heading` 来定制：\n\n```sh\n$ just --list --list-heading $'Cool stuff…\\n'\nCool stuff…\n    test\n    build\n```\n\n而缩进可以用 `--list-prefix` 来定制：\n\n```sh\n$ just --list --list-prefix ····\nAvailable recipes:\n····test\n····build\n```\n\n`--list-heading` 参数同时替换了标题和后面的换行，所以如果不是空的，应该包含一个换行。这样做是为了允许你通过传递空字符串来完全抑制标题行：\n\n```sh\n$ just --list --list-heading ''\n    test\n    build\n```\n\n### 别名\n\n别名允许你用其他名称来调用配方：\n\n```just\nalias b := build\n\nbuild:\n  echo 'Building!'\n```\n\n```sh\n$ just b\nbuild\necho 'Building!'\nBuilding!\n```\n\n### 设置\n\n设置控制解释和执行。每个设置最多可以指定一次，可以出现在 `justfile` 的任何地方。\n\n例如：\n\n```just\nset shell := [\"zsh\", \"-cu\"]\n\nfoo:\n  # this line will be run as `zsh -cu 'ls **/*.txt'`\n  ls **/*.txt\n```\n\n#### 设置一览表\n\n| 名称                        | 值                 | 默认  | 描述                                                                                    |\n| --------------------------- | ------------------ | ----- | --------------------------------------------------------------------------------------- |\n| `allow-duplicate-recipes`   | boolean            | False | 允许在 `justfile` 后面出现的配方覆盖之前的同名配方                                      |\n| `allow-duplicate-variables` | boolean            | False | 允许在 `justfile` 后面出现的变量覆盖之前的同名变量                                      |\n| `dotenv-filename`           | string             | -     | 如果有自定义名称的 `.env` 环境变量文件的话，则将其加载                                  |\n| `dotenv-load`               | boolean            | False | 如果有`.env` 环境变量文件的话，则将其加载                                               |\n| `dotenv-path`               | string             | -     | 从自定义路径中加载 `.env` 环境变量文件， 文件不存在将会报错。可以覆盖 `dotenv-filename` |\n| `dotenv-required`           | boolean            | False | 如果 `.env` 环境变量文件不存在的话，需要报错                                                         |\n| `export`                    | boolean            | False | 将所有变量导出为环境变量                                                                |\n| `fallback`                  | boolean            | False | 如果命令行中的第一个配方没有找到，则在父目录中搜索 `justfile`                           |\n| `ignore-comments`           | boolean            | False | 忽略以`#`开头的配方行                                                                   |\n| `positional-arguments`      | boolean            | False | 传递位置参数                                                                            |\n| `shell`                     | `[COMMAND, ARGS…]` | -     | 设置用于调用配方和评估反引号内包裹内容的命令                                            |\n| `tempdir`                   | string             | -     | 在 `tempdir` 位置创建临时目录，而不是系统默认的临时目录                                 |\n| `windows-powershell`        | boolean            | False | 在 Windows 上使用 PowerShell 作为默认 Shell(废弃，建议使用 `windows-shell`)             |\n| `windows-shell`             | `[COMMAND, ARGS…]` | -     | 设置用于调用配方和评估反引号内包裹内容的命令                                            |\n\nBool 类型设置可以写成：\n\n```justfile\nset NAME\n```\n\n这就相当于：\n\n```justfile\nset NAME := true\n```\n\n#### 允许重复的配方\n\n如果 `allow-duplicate-recipes` 被设置为 `true`，那么定义多个同名的配方就不会出错，而会使用最后的定义。默认为 `false`。\n\n```just\nset allow-duplicate-recipes\n\n@foo:\n  echo foo\n\n@foo:\n  echo bar\n```\n\n```sh\n$ just foo\nbar\n```\n\n#### 允许重复的变量\n如果 `allow-duplicate-variables` 被设置为 `true`，那么定义多个同名的变量将不会报错。默认为 `false`。\n\n```just\nset allow-duplicate-variables\n\na := \"foo\"\na := \"bar\"\n\n@foo:\n  echo $a\n```\n\n```sh\n$ just foo\nbar\n```\n\n#### 环境变量加载\n\n如果 `dotenv-load`, `dotenv-filename`, `dotenv-path`, or `dotenv-required`\n中任意一项被设置, `just` 会尝试从文件中加载环境变量\n\n如果设置了 `dotenv-path`, `just` 会在指定的路径下搜索文件，该路径可以是绝对路径，\n也可以是基于当前工作路径的相对路径\n\n如果设置了 `dotenv-filename`，`just` 会在指定的相对路径，以及其所有的上层目录中，搜索指定文件\n\n如果没有设置 `dotenv-filename`，但是设置了 `dotenv-load` 或 `dotenv-required`，\n`just` 会在当前工作路径，以及其所有的上层目录中，寻找名为 `.env` 的文件。\n\n`dotenv-filename` 和 `dotenv-path` 很相似，但是 `dotenv-path` 只会检查指定的目录\n而 `dotenv-filename` 会检查指定目录以及其所有的上层目录。\n\n如果没有找到环境变量文件也不会报错，除非设置了 `dotenv-required`。\n\n从文件中加载的变量是环境变量，而非 `just` 变量，所以在配方和反引号中需要必须通过 `$VARIABLE_NAME` 来调用。\n\n比如，如果你的 `.env` 文件包含以下内容：\n\n```sh\n# a comment, will be ignored\nDATABASE_ADDRESS=localhost:6379\nSERVER_PORT=1337\n```\n\n并且你的 `justfile` 包含：\n\n```just\nset dotenv-load\n\nserve:\n  @echo \"Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…\"\n  ./server --database $DATABASE_ADDRESS --port $SERVER_PORT\n```\n\n`just serve` 将会输出：\n\n```sh\n$ just serve\nStarting server with database localhost:6379 on port 1337…\n./server --database $DATABASE_ADDRESS --port $SERVER_PORT\n```\n\n#### 导出\n\n`export` 设置使所有 `just` 变量作为环境变量被导出。默认值为 `false`。\n\n```just\nset export\n\na := \"hello\"\n\n@foo b:\n  echo $a\n  echo $b\n```\n\n```sh\n$ just foo goodbye\nhello\ngoodbye\n```\n\n#### 位置参数\n\n如果 `positional-arguments` 为 `true`，配方参数将作为位置参数传递给命令。对于行式配方，参数 `$0` 将是配方的名称。\n\n例如，运行这个配方：\n\n```just\nset positional-arguments\n\n@foo bar:\n  echo $0\n  echo $1\n```\n\n将产生以下输出：\n\n```sh\n$ just foo hello\nfoo\nhello\n```\n\n当使用 `sh` 兼容的 Shell，如 `bash` 或 `zsh` 时，`$@` 会展开为传给配方的位置参数，从1开始。当在双引号内使用 `\"$@\"` 时，包括空白的参数将被传递，就像它们是双引号一样。也就是说，`\"$@\"` 相当于 `\"$1\" \"$2\"`......当没有位置参数时，`\"$@\"` 和 `$@` 将展开为空（即，它们被删除）。\n\n这个例子的配方将逐行打印参数：\n\n```just\nset positional-arguments\n\n@test *args='':\n  bash -c 'while (( \"$#\" )); do echo - $1; shift; done' -- \"$@\"\n```\n\n用 _两个_ 参数运行：\n\n```sh\n$ just test foo \"bar baz\"\n- foo\n- bar baz\n```\n\n#### Shell\n\n`shell` 设置控制用于调用执行配方代码行和反引号内指令的命令。Shebang 配方不受影响。\n\n```just\n# use python3 to execute recipe lines and backticks\nset shell := [\"python3\", \"-c\"]\n\n# use print to capture result of evaluation\nfoos := `print(\"foo\" * 4)`\n\nfoo:\n  print(\"Snake snake snake snake.\")\n  print(\"{{foos}}\")\n```\n\n`just` 把要执行的命令作为一个参数进行传递。许多 Shell 需要一个额外的标志，通常是 `-c`，以使它们评估执行第一个参数。\n\n##### Windows Shell\n\n`just` 在 Windows 上默认使用 `sh`。要在 Windows 上使用不同的 Shell，请使用`windows-shell`：\n\n```just\nset windows-shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"]\n\nhello:\n  Write-Host \"Hello, world!\"\n```\n\n参考 [powershell.just](https://github.com/casey/just/blob/master/examples/powershell.just) ，了解在所有平台上使用 PowerShell 的 justfile。\n\n##### Windows PowerShell\n\n*`set windows-powershell` 使用遗留的 `powershell.exe` 二进制文件，不再推荐。请参阅上面的 `windows-shell` 设置，以通过更灵活的方式来控制在 Windows 上使用哪个 Shell。*\n\n`just` 在 Windows 上默认使用 `sh`。要使用 `powershell.exe` 作为替代，请将 `windows-powershell` 设置为 `true`。\n\n```just\nset windows-powershell := true\n\nhello:\n  Write-Host \"Hello, world!\"\n```\n\n##### Python 3\n\n```just\nset shell := [\"python3\", \"-c\"]\n```\n\n##### Bash\n\n```just\nset shell := [\"bash\", \"-uc\"]\n```\n\n##### Z Shell\n\n```just\nset shell := [\"zsh\", \"-uc\"]\n```\n\n##### Fish\n\n```just\nset shell := [\"fish\", \"-c\"]\n```\n\n##### Nushell\n\n```just\nset shell := [\"nu\", \"-c\"]\n```\n\n如果你想设置默认的表格显示模式为 `light`:\n\n```just\nset shell := ['nu', '-m', 'light', '-c']\n```\n\n*[Nushell](https://github.com/nushell/nushell) 使用 Rust 开发并且具备良好的跨平台能力，**支持 Windows / macOS 和各种 Linux 发行版***\n\n### 文档注释\n\n紧接着配方前面的注释将出现在 `just --list` 中：\n\n```just\n# build stuff\nbuild:\n  ./bin/build\n\n# test stuff\ntest:\n  ./bin/test\n```\n\n```sh\n$ just --list\nAvailable recipes:\n    build # build stuff\n    test # test stuff\n```\n\n### 变量和替换\n\n支持在变量、字符串、拼接、路径连接和替换中使用 `{{…}}` ：\n\n```just\ntmpdir  := `mktemp -d`\nversion := \"0.2.7\"\ntardir  := tmpdir / \"awesomesauce-\" + version\ntarball := tardir + \".tar.gz\"\n\npublish:\n  rm -f {{tarball}}\n  mkdir {{tardir}}\n  cp README.md *.c {{tardir}}\n  tar zcvf {{tarball}} {{tardir}}\n  scp {{tarball}} me@server.com:release/\n  rm -rf {{tarball}} {{tardir}}\n```\n\n#### 路径拼接\n\n`/` 操作符可用于通过斜线连接两个字符串：\n\n```just\nfoo := \"a\" / \"b\"\n```\n\n```\n$ just --evaluate foo\na/b\n```\n\n请注意，即使已经有一个 `/`，也会添加一个 `/`：\n\n```just\nfoo := \"a/\"\nbar := foo / \"b\"\n```\n\n```\n$ just --evaluate bar\na//b\n```\n\n也可以构建绝对路径<sup>1.5.0</sup>:\n\n```just\nfoo := / \"b\"\n```\n\n```\n$ just --evaluate foo\n/b\n```\n\n`/` 操作符使用 `/` 字符，即使在 Windows 上也是如此。因此，在使用通用命名规则（UNC）的路径中应避免使用 `/` 操作符，即那些以 `\\?` 开头的路径，因为 UNC 路径不支持正斜线。\n\n#### 转义 `{{`\n\n想要写一个包含  `{{` 的配方，可以使用 `{{{{`：\n\n```just\nbraces:\n  echo 'I {{{{LOVE}} curly braces!'\n```\n\n(未匹配的 `}}` 会被忽略，所以不需要转义)\n\n另一个选择是把所有你想转义的文本都放在插值里面：\n\n```just\nbraces:\n  echo '{{'I {{LOVE}} curly braces!'}}'\n```\n\n然而，另一个选择是使用  `{{ \"{{\" }}`：\n\n```just\nbraces:\n  echo 'I {{ \"{{\" }}LOVE}} curly braces!'\n```\n\n### 字符串\n\n双引号字符串支持转义序列：\n\n```just\nstring-with-tab             := \"\\t\"\nstring-with-newline         := \"\\n\"\nstring-with-carriage-return := \"\\r\"\nstring-with-double-quote    := \"\\\"\"\nstring-with-slash           := \"\\\\\"\nstring-with-no-newline      := \"\\\n\"\n```\n\n```sh\n$ just --evaluate\n\"tring-with-carriage-return := \"\nstring-with-double-quote    := \"\"\"\nstring-with-newline         := \"\n\"\nstring-with-no-newline      := \"\"\nstring-with-slash           := \"\\\"\nstring-with-tab             := \"     \"\n```\n\n字符串可以包含换行符：\n\n```just\nsingle := '\nhello\n'\n\ndouble := \"\ngoodbye\n\"\n```\n\n单引号字符串不支持转义序列：\n\n```just\nescapes := '\\t\\n\\r\\\"\\\\'\n```\n\n```sh\n$ just --evaluate\nescapes := \"\\t\\n\\r\\\"\\\\\"\n```\n\n支持单引号和双引号字符串的缩进版本，以三个单引号或三个双引号为界。缩进的字符串行被删除了所有非空行所共有的前导空白：\n\n```just\n# 这个字符串执行结果为 `foo\\nbar\\n`\nx := '''\n  foo\n  bar\n'''\n\n# 这个字符串执行结果为 `abc\\n  wuv\\nbar\\n`\ny := \"\"\"\n  abc\n    wuv\n  xyz\n\"\"\"\n```\n\n与未缩进的字符串类似，缩进的双引号字符串处理转义序列，而缩进的单引号字符串则忽略转义序列。转义序列的处理是在取消缩进后进行的。取消缩进的算法不考虑转义序列产生的空白或换行。\n\n### 错误忽略\n\n通常情况下，如果一个命令返回一个非零的退出状态，将停止执行。要想在一个命令之后继续执行，即使它失败了，需要在命令前加上 `-`：\n\n```just\nfoo:\n  -cat foo\n  echo 'Done!'\n```\n\n```sh\n$ just foo\ncat foo\ncat: foo: No such file or directory\necho 'Done!'\nDone!\n```\n\n### 函数\n\n`just` 提供了一些内置函数，在编写配方时可能很有用。\n\n#### 系统信息\n\n- `arch()` — 指令集结构。可能的值是：`\"aarch64\"`, `\"arm\"`, `\"asmjs\"`, `\"hexagon\"`, `\"mips\"`, `\"msp430\"`, `\"powerpc\"`, `\"powerpc64\"`, `\"s390x\"`, `\"sparc\"`, `\"wasm32\"`, `\"x86\"`, `\"x86_64\"`, 和 `\"xcore\"`。\n- `os()` — 操作系统，可能的值是: `\"android\"`, `\"bitrig\"`, `\"dragonfly\"`, `\"emscripten\"`, `\"freebsd\"`, `\"haiku\"`, `\"ios\"`, `\"linux\"`, `\"macos\"`, `\"netbsd\"`, `\"openbsd\"`, `\"solaris\"`, 和 `\"windows\"`。\n- `os_family()` — 操作系统系列；可能的值是：`\"unix\"` 和 `\"windows\"`。\n\n例如：\n\n```just\nsystem-info:\n  @echo \"This is an {{arch()}} machine\".\n```\n\n```sh\n$ just system-info\nThis is an x86_64 machine\n```\n\n`os_family()` 函数可以用来创建跨平台的 `justfile`，使其可以在不同的操作系统上工作。一个例子，见 [cross-platform.just](https://github.com/casey/just/blob/master/examples/cross-platform.just) 文件。\n\n#### 环境变量\n\n- `env_var(key)` — 获取名称为 `key` 的环境变量，如果不存在则终止。\n\n```just\nhome_dir := env_var('HOME')\n\ntest:\n  echo \"{{home_dir}}\"\n```\n\n```sh\n$ just\n/home/user1\n```\n\n- `env_var_or_default(key, default)` — 获取名称为 `key` 的环境变量，如果不存在则返回 `default`。\n\n#### 调用目录\n\n- `invocation_directory()` - 获取 `just` 被调用时当前目录所对应的绝对路径，在 `just` 改变路径并执行相应命令前。\n\n例如，要对 \"当前目录\" 下的文件调用 `rustfmt`（从用户/调用者的角度看），使用以下规则：\n\n```just\nrustfmt:\n  find {{invocation_directory()}} -name \\*.rs -exec rustfmt {} \\;\n```\n\n另外，如果你的命令需要从当前目录运行，你可以使用如下方式：\n\n```just\nbuild:\n  cd {{invocation_directory()}}; ./some_script_that_needs_to_be_run_from_here\n```\n\n#### Justfile 和 Justfile 目录\n\n- `justfile()` - 取得当前 `justfile` 的路径。\n\n- `justfile_directory()` - 取得当前 `justfile` 文件父目录的路径。\n\n例如，运行一个相对于当前 `justfile` 位置的命令：\n\n```just\nscript:\n  ./{{justfile_directory()}}/scripts/some_script\n```\n\n#### Just 可执行程序\n\n- `just_executable()` - `just` 可执行文件的绝对路径。\n\n例如：\n\n```just\nexecutable:\n  @echo The executable is at: {{just_executable()}}\n```\n\n```sh\n$ just\nThe executable is at: /bin/just\n```\n\n#### 字符串处理\n\n- `quote(s)` - 用 `'\\''` 替换所有的单引号，并在 `s` 的首尾添加单引号。这足以为许多 Shell 转义特殊字符，包括大多数 Bourne Shell 的后代。\n- `replace(s, from, to)` - 将 `s` 中的所有 `from` 替换为 `to`。\n- `replace_regex(s, regex, replacement)` - 将 `s` 中所有的 `regex` 替换为 `replacement`。正则表达式由 [Rust `regex` 包](https://docs.rs/regex/latest/regex/) 提供。参见 [语法文档](https://docs.rs/regex/latest/regex/#syntax) 以了解使用示例。\n- `trim(s)` - 去掉 `s` 的首尾空格。\n- `trim_end(s)` - 去掉 `s` 的尾部空格。\n- `trim_end_match(s, substr)` - 删除与 `substr` 匹配的 `s` 的后缀。\n- `trim_end_matches(s, substr)` - 反复删除与 `substr` 匹配的 `s` 的后缀。\n- `trim_start(s)` - 去掉 `s` 的首部空格。\n- `trim_start_match(s, substr)` - 删除与 `substr` 匹配的 `s` 的前缀。\n- `trim_start_matches(s, substr)` - 反复删除与 `substr` 匹配的 `s` 的前缀。\n\n#### 大小写转换\n\n- `capitalize(s)`<sup>1.7.0</sup> - 将 `s` 的第一个字符转换成大写字母，其余的转换成小写字母。\n- `kebabcase(s)`<sup>1.7.0</sup> - 将 `s` 转换为 `kebab-case`。\n- `lowercamelcase(s)`<sup>1.7.0</sup> - 将 `s` 转换为小驼峰形式：`lowerCamelCase`。\n- `lowercase(s)` - 将 `s` 转换为全小写形式。\n- `shoutykebabcase(s)`<sup>1.7.0</sup> - 将 `s` 转换为 `SHOUTY-KEBAB-CASE`。\n- `shoutysnakecase(s)`<sup>1.7.0</sup> - 将 `s` 转换为 `SHOUTY_SNAKE_CASE`。\n- `snakecase(s)`<sup>1.7.0</sup> - 将 `s` 转换为 `snake_case`。\n- `titlecase(s)`<sup>1.7.0</sup> - 将 `s` 转换为 `Title Case`。\n- `uppercamelcase(s)`<sup>1.7.0</sup> - 将 `s` 转换为 `UpperCamelCase`。\n- `uppercase(s)` - 将 `s` 转换为大写形式。\n\n#### 路径操作\n\n##### 非可靠的\n\n- `absolute_path(path)` - 将当前工作目录中到相对路径 `path` 的路径转换为绝对路径。在 `/foo` 目录通过 `absolute_path(\"./bar.txt\")` 可以得到 `/foo/bar.txt`。\n- `extension(path)` - 获取 `path` 的扩展名。`extension(\"/foo/bar.txt\")` 结果为 `txt`。\n- `file_name(path)` - 获取 `path` 的文件名，去掉任何前面的目录部分。`file_name(\"/foo/bar.txt\")` 的结果为 `bar.txt`。\n- `file_stem(path)` - 获取 `path` 的文件名，不含扩展名。`file_stem(\"/foo/bar.txt\")` 的结果为 `bar`。\n- `parent_directory(path)` - 获取 `path` 的父目录。`parent_directory(\"/foo/bar.txt\")` 的结果为 `/foo`。\n- `without_extension(path)` - 获取 `path` 不含扩展名部分。`without_extension(\"/foo/bar.txt\")` 的结果为 `/foo/bar`。\n\n这些函数可能会失败，例如，如果一个路径没有扩展名，则将停止执行。\n\n##### 可靠的\n\n- `clean(path)` - 通过删除多余的路径分隔符、中间的 `.` 和 `..` 来简化 `path`。`clean(\"foo//bar\")` 结果为 `foo/bar`，`clean(\"foo/..\")` 为 `.`，`clean(\"foo/./bar\")` 结果为 `foo/bar`。\n- `join(a, b…)` - *这个函数在 Unix 上使用 `/`，在 Windows 上使用 `\\`，这可能会导致非预期的行为。`/` 操作符，例如，`a / b`，总是使用 `/`，应该被考虑作为替代，除非在 Windows 上特别指定需要 `\\`。* 将路径 `a` 和 路径 `b` 拼接在一起。`join(\"foo/bar\", \"baz\")` 结果为 `foo/bar/baz`。它接受两个或多个参数。\n\n#### 文件系统访问\n\n- `path_exists(path)` - 如果路径指向一个存在的文件或目录，则返回 `true`，否则返回 `false`。也会遍历符号链接，如果路径无法访问或指向一个无效的符号链接，则返回 `false`。\n\n##### 错误报告\n\n- `error(message)` - 终止执行并向用户报告错误 `message`。\n\n#### UUID 和哈希值生成\n\n- `sha256(string)` - 以十六进制字符串形式返回 `string` 的 SHA-256 哈希值。\n- `sha256_file(path)` - 以十六进制字符串形式返回 `path` 处的文件的 SHA-256 哈希值。\n- `uuid()` - 返回一个随机生成的 UUID。\n\n### 配方属性\n\n配方可以通过添加属性注释来改变其行为。\n\n\n| 名称                                | 描述                                   |\n| ----------------------------------- | -------------------------------------- |\n| `[no-cd]`<sup>1.9.0</sup>           | 在执行配方之前不要改变目录。           |\n| `[no-exit-message]`<sup>1.7.0</sup> | 如果配方执行失败，不要打印错误信息。   |\n| `[linux]`<sup>1.8.0</sup>           | 在Linux上启用配方。                    |\n| `[macos]`<sup>1.8.0</sup>           | 在MacOS上启用配方。                    |\n| `[unix]`<sup>1.8.0</sup>            | 在Unixes上启用配方。                   |\n| `[windows]`<sup>1.8.0</sup>         | 在Windows上启用配方。                  |\n| `[private]`<sup>1.10.0</sup>        | 参见 [私有配方](#私有配方). |\n\n#### 启用和禁用配方<sup>1.8.0</sup>\n\n`[linux]`, `[macos]`, `[unix]` 和 `[windows]` 属性是配置属性。默认情况下，配方总是被启用。一个带有一个或多个配置属性的配方只有在其中一个或多个配置处于激活状态时才会被启用。\n\n这可以用来编写因运行的操作系统不同，其行为也不同的 `justfile`。以下 `justfile` 中的 `run` 配方将编译和运行 `main.c`，并且根据操作系统的不同而使用不同的C编译器，同时使用正确的二进制产物名称：\n\n```just\n[unix]\nrun:\n  cc main.c\n  ./a.out\n\n[windows]\nrun:\n  cl main.c\n  main.exe\n```\n\n#### 禁用变更目录<sup>1.9.0</sup>\n\n`just` 通常在执行配方时将当前目录设置为包含 `justfile` 的目录，你可以通过 `[no-cd]` 属性来禁用此行为。这可以用来创建使用调用目录相对路径或者对当前目录进行操作的配方。\n\n例如这个 `commit` 配方：\n\n```just\n[no-cd]\ncommit file:\n  git add {{file}}\n  git commit\n```\n\n可以使用相对于当前目录的路径，因为 `[no-cd]` 可以防止 `just` 在执行 `commit` 配方时改变当前目录。\n\n### 使用反引号的命令求值\n\n反引号可以用来存储命令的求值结果：\n\n```just\nlocalhost := `dumpinterfaces | cut -d: -f2 | sed 's/\\/.*//' | sed 's/ //g'`\n\nserve:\n  ./serve {{localhost}} 8080\n```\n\n缩进的反引号，以三个反引号为界，与字符串缩进的方式一样，会被去掉缩进：\n\n````just\n# This backtick evaluates the command `echo foo\\necho bar\\n`, which produces the value `foo\\nbar\\n`.\nstuff := ```\n    echo foo\n    echo bar\n  ```\n````\n\n参见 [字符串](#字符串) 部分，了解去除缩进的细节。\n\n反引号内不能以 `#!` 开头。这种语法是为将来的升级而保留的。\n\n### 条件表达式\n\n`if` / `else` 表达式评估不同的分支，取决于两个表达式是否评估为相同的值：\n\n```just\nfoo := if \"2\" == \"2\" { \"Good!\" } else { \"1984\" }\n\nbar:\n  @echo \"{{foo}}\"\n```\n\n```sh\n$ just bar\nGood!\n```\n\n也可以用于测试不相等：\n\n```just\nfoo := if \"hello\" != \"goodbye\" { \"xyz\" } else { \"abc\" }\n\nbar:\n  @echo {{foo}}\n```\n\n```sh\n$ just bar\nxyz\n```\n\n还支持与正则表达式进行匹配：\n\n```just\nfoo := if \"hello\" =~ 'hel+o' { \"match\" } else { \"mismatch\" }\n\nbar:\n  @echo {{foo}}\n```\n\n```sh\n$ just bar\nmatch\n```\n\n正则表达式由 [Regex 包](https://github.com/rust-lang/regex) 提供，其语法在 [docs.rs](https://docs.rs/regex/1.5.4/regex/#syntax) 上有对应文档。由于正则表达式通常使用反斜线转义序列，请考虑使用单引号的字符串字面值，这将使斜线不受干扰地传递给正则分析器。\n\n条件表达式是短路的，这意味着它们只评估其中的一个分支。这可以用来确保反引号内的表达式在不应该运行的时候不会运行。\n\n```just\nfoo := if env_var(\"RELEASE\") == \"true\" { `get-something-from-release-database` } else { \"dummy-value\" }\n```\n\n条件语句也可以在配方中使用：\n\n```just\nbar foo:\n  echo {{ if foo == \"bar\" { \"hello\" } else { \"goodbye\" } }}\n```\n\n注意最后的 `}` 后面的空格! 没有这个空格，插值将被提前结束。\n\n多个条件语句可以被连起来：\n\n```just\nfoo := if \"hello\" == \"goodbye\" {\n  \"xyz\"\n} else if \"a\" == \"a\" {\n  \"abc\"\n} else {\n  \"123\"\n}\n\nbar:\n  @echo {{foo}}\n```\n\n```sh\n$ just bar\nabc\n```\n\n### 出现错误停止执行\n\n可以用 `error` 函数停止执行。比如：\n\n```just\nfoo := if \"hello\" == \"goodbye\" {\n  \"xyz\"\n} else if \"a\" == \"b\" {\n  \"abc\"\n} else {\n  error(\"123\")\n}\n```\n\n在运行时产生以下错误：\n\n```\nerror: Call to function `error` failed: 123\n   |\n16 |   error(\"123\")\n```\n\n### 从命令行设置变量\n\n变量可以从命令行进行覆盖。\n\n```just\nos := \"linux\"\n\ntest: build\n  ./test --test {{os}}\n\nbuild:\n  ./build {{os}}\n```\n\n```sh\n$ just\n./build linux\n./test --test linux\n```\n\n任何数量的 `NAME=VALUE` 形式的参数都可以在配方前传递：\n\n```sh\n$ just os=plan9\n./build plan9\n./test --test plan9\n```\n\n或者你可以使用 `--set` 标志：\n\n```sh\n$ just --set os bsd\n./build bsd\n./test --test bsd\n```\n\n### 获取和设置环境变量\n\n#### 导出 `just` 变量\n\n以 `export` 关键字为前缀的赋值将作为环境变量导出到配方中：\n\n```just\nexport RUST_BACKTRACE := \"1\"\n\ntest:\n  # 如果它崩溃了，将打印一个堆栈追踪\n  cargo test\n```\n\n以 `$` 为前缀的参数将被作为环境变量导出：\n\n```just\ntest $RUST_BACKTRACE=\"1\":\n  # 如果它崩溃了，将打印一个堆栈追踪\n  cargo test\n```\n\n导出的变量和参数不会被导出到同一作用域内反引号包裹的表达式里。\n\n```just\nexport WORLD := \"world\"\n# This backtick will fail with \"WORLD: unbound variable\"\nBAR := `echo hello $WORLD`\n```\n\n```just\n# Running `just a foo` will fail with \"A: unbound variable\"\na $A $B=`echo $A`:\n  echo $A $B\n```\n\n当 [export](#导出) 被设置时，所有的 `just` 变量都将作为环境变量被导出。\n\n#### 从环境中获取环境变量\n\n来自环境的环境变量会自动传递给配方：\n\n```just\nprint_home_folder:\n  echo \"HOME is: '${HOME}'\"\n```\n\n```sh\n$ just\nHOME is '/home/myuser'\n```\n\n#### 从 `.env` 文件加载环境变量\n\n如果 [dotenv-load](#环境变量加载) 被设置，`just` 将从 `.env` 文件中加载环境变量。该文件中的变量将作为环境变量提供给配方。参见 [环境变量集成](#环境变量加载) 以获得更多信息。\n\n#### 从环境变量中设置 `just` 变量\n\n环境变量可以通过函数 `env_var()` 和 `env_var_or_default()` 传入到 `just` 变量。\n参见 [environment-variables](#环境变量)。\n\n### 配方参数\n\n配方可以有参数。这里的配方 `build` 有一个参数叫 `target`:\n\n```just\nbuild target:\n  @echo 'Building {{target}}…'\n  cd {{target}} && make\n```\n\n要在命令行上传递参数，请把它们放在配方名称后面：\n\n```sh\n$ just build my-awesome-project\nBuilding my-awesome-project…\ncd my-awesome-project && make\n```\n\n要向依赖配方传递参数，请将依赖配方和参数一起放在括号里：\n\n```just\ndefault: (build \"main\")\n\nbuild target:\n  @echo 'Building {{target}}…'\n  cd {{target}} && make\n```\n\n变量也可以作为参数传递给依赖：\n\n```just\ntarget := \"main\"\n\n_build version:\n  @echo 'Building {{version}}…'\n  cd {{version}} && make\n\nbuild: (_build target)\n```\n\n命令的参数可以通过将依赖与参数一起放在括号中的方式传递给依赖：\n\n```just\nbuild target:\n  @echo \"Building {{target}}…\"\n\npush target: (build target)\n  @echo 'Pushing {{target}}…'\n```\n\n参数可以有默认值：\n\n```just\ndefault := 'all'\n\ntest target tests=default:\n  @echo 'Testing {{target}}:{{tests}}…'\n  ./test --tests {{tests}} {{target}}\n```\n\n有默认值的参数可以省略：\n\n```sh\n$ just test server\nTesting server:all…\n./test --tests all server\n```\n\n或者提供：\n\n```sh\n$ just test server unit\nTesting server:unit…\n./test --tests unit server\n```\n\n默认值可以是任意的表达式，但字符串或路径拼接必须放在括号内：\n\n```just\narch := \"wasm\"\n\ntest triple=(arch + \"-unknown-unknown\") input=(arch / \"input.dat\"):\n  ./test {{triple}}\n```\n\n配方的最后一个参数可以是变长的，在参数名称前用 `+` 或 `*` 表示：\n\n```just\nbackup +FILES:\n  scp {{FILES}} me@server.com:\n```\n\n以 `+` 为前缀的变长参数接受 _一个或多个_ 参数，并展开为一个包含这些参数的字符串，以空格分隔：\n\n```sh\n$ just backup FAQ.md GRAMMAR.md\nscp FAQ.md GRAMMAR.md me@server.com:\nFAQ.md                  100% 1831     1.8KB/s   00:00\nGRAMMAR.md              100% 1666     1.6KB/s   00:00\n```\n\n以 `*` 为前缀的变长参数接受 _0个或更多_ 参数，并展开为一个包含这些参数的字符串，以空格分隔，如果没有参数，则为空字符串：\n\n```just\ncommit MESSAGE *FLAGS:\n  git commit {{FLAGS}} -m \"{{MESSAGE}}\"\n```\n\n变长参数可以被分配默认值。这些参数被命令行上传递的参数所覆盖：\n\n```just\ntest +FLAGS='-q':\n  cargo test {{FLAGS}}\n```\n\n`{{…}}` 的替换可能需要加引号，如果它们包含空格。例如，如果你有以下配方：\n\n```just\nsearch QUERY:\n  lynx https://www.google.com/?q={{QUERY}}\n```\n\n然后你输入：\n\n```sh\n$ just search \"cat toupee\"\n```\n\n`just` 将运行 `lynx https://www.google.com/?q=cat toupee` 命令，这将被 `sh` 解析为`lynx`、`https://www.google.com/?q=cat` 和 `toupee`，而不是原来的 `lynx` 和 `https://www.google.com/?q=cat toupee`。\n\n你可以通过添加引号来解决这个问题：\n\n```just\nsearch QUERY:\n  lynx 'https://www.google.com/?q={{QUERY}}'\n```\n\n以 `$` 为前缀的参数将被作为环境变量导出：\n\n```just\nfoo $bar:\n  echo $bar\n```\n\n### 在配方的末尾运行配方\n\n一个配方的正常依赖总是在配方开始之前运行。也就是说，被依赖方总是在依赖方之前运行。这些依赖被称为 \"前期依赖\"。\n\n一个配方也可以有后续的依赖，它们在配方之后运行，用 `&&` 表示：\n\n```just\na:\n  echo 'A!'\n\nb: a && c d\n  echo 'B!'\n\nc:\n  echo 'C!'\n\nd:\n  echo 'D!'\n```\n\n…运行 _b_ 输出：\n\n```sh\n$ just b\necho 'A!'\nA!\necho 'B!'\nB!\necho 'C!'\nC!\necho 'D!'\nD!\n```\n\n### 在配方中间运行配方\n\n`just` 不支持在配方的中间运行另一个配方，但你可以在一个配方的中间递归调用 `just`。例如以下 `justfile`：\n\n```just\na:\n  echo 'A!'\n\nb: a\n  echo 'B start!'\n  just c\n  echo 'B end!'\n\nc:\n  echo 'C!'\n```\n\n…运行 _b_ 输出：\n\n```sh\n$ just b\necho 'A!'\nA!\necho 'B start!'\nB start!\necho 'C!'\nC!\necho 'B end!'\nB end!\n```\n\n这有局限性，因为配方 `c` 是以一个全新的 `just` 调用来运行的，赋值将被重新计算，依赖可能会运行两次，命令行参数不会被传入到子 `just` 进程。\n\n### 用其他语言书写配方\n\n以 `#!` 开头的配方被称为 Shebang 配方，它通过将配方主体保存到文件中并运行它来执行。这让你可以用不同的语言来编写配方：\n\n```just\npolyglot: python js perl sh ruby nu\n\npython:\n  #!/usr/bin/env python3\n  print('Hello from python!')\n\njs:\n  #!/usr/bin/env node\n  console.log('Greetings from JavaScript!')\n\nperl:\n  #!/usr/bin/env perl\n  print \"Larry Wall says Hi!\\n\";\n\nsh:\n  #!/usr/bin/env sh\n  hello='Yo'\n  echo \"$hello from a shell script!\"\n\nnu:\n  #!/usr/bin/env nu\n  let hello = 'Hola'\n  echo $\"($hello) from a nushell script!\"\n\nruby:\n  #!/usr/bin/env ruby\n  puts \"Hello from ruby!\"\n```\n\n```sh\n$ just polyglot\nHello from python!\nGreetings from JavaScript!\nLarry Wall says Hi!\nYo from a shell script!\nHola from a nushell script!\nHello from ruby!\n```\n\n在类似 Unix 的操作系统中，包括 Linux 和 MacOS，Shebang 配方的执行方式是将配方主体保存到临时目录下的一个文件中，将该文件标记为可执行文件，然后执行它。操作系统将 Shebang 行解析为一个命令行并调用它，包括文件的路径。例如，如果一个配方以 `#!/usr/bin/env bash` 开头，操作系统运行的最终命令将是 `/usr/bin/env bash /tmp/PATH_TO_SAVED_RECIPE_BODY` 之类。请记住，不同的操作系统对 Shebang 行的分割方式不同。\n\nWindows 不支持 Shebang 行。在 Windows 上，`just` 将 Shebang 行分割成命令和参数，将配方主体保存到一个文件中，并调用分割后的命令和参数，同时将保存的配方主体的路径作为最后一个参数。\n\n### 更加安全的 Bash Shebang 配方\n\n如果你正在写一个 `bash` Shebang 配方，考虑加入 `set -euxo pipefail`：\n\n```just\nfoo:\n  #!/usr/bin/env bash\n  set -euxo pipefail\n  hello='Yo'\n  echo \"$hello from Bash!\"\n```\n\n严格意义上说这不是必须的，但是 `set -euxo pipefail` 开启了一些有用的功能，使 `bash` Shebang 配方的行为更像正常的、行式的 `just` 配方:\n\n- `set -e` 使 `bash` 在命令失败时退出。\n\n- `set -u` 使 `bash` 在变量未定义时退出。\n\n- `set -x` 使 `bash` 在运行前打印每一行脚本。\n\n- `set -o pipefail` 使 `bash` 在管道中的一个命令失败时退出。这是 `bash` 特有的，所以在普通的行式 `just` 配方中没有开启。\n\n这些措施共同避免了很多 Shell 脚本的问题。\n\n#### 在 Windows 上执行 Shebang 配方\n\n在 Windows 上，包含 `/` 的 Shebang 解释器路径通过 `cygpath` 从 Unix 风格的路径转换为 Windows 风格的路径，该工具随 [Cygwin](http://www.cygwin.com) 一起提供。\n\n例如，要在 Windows 上执行这个配方：\n\n```just\necho:\n  #!/bin/sh\n  echo \"Hello!\"\n```\n\n解释器路径 `/bin/sh` 在执行前将被 `cygpath` 翻译成 Windows 风格的路径。\n\n如果解释器路径不包含 `/`，它将被执行而不被翻译。这主要用于 `cygpath` 不可用或者你希望向解释器传递一个 Windows 风格的路径的情况下。\n\n### 在配方中设置变量\n\n配方代码行是由 Shell 解释的，而不是 `just`，所以不可能在配方中设置 `just` 变量：\n\n```mf\nfoo:\n  x := \"hello\" # This doesn't work!\n  echo {{x}}\n```\n\n使用 Shell 变量是可能的，但还有一个问题：每一行配方都由一个新的 Shell 实例运行，所以在一行中设置的变量不会在下一行中生效：\n\n```just\nfoo:\n  x=hello && echo $x # 这个没问题！\n  y=bye\n  echo $y            # 这个是有问题的, `y` 在此处未定义!\n```\n\n解决这个问题的最好方法是使用 Shebang 配方。Shebang 配方体被提取出来并作为脚本运行，所以一个 Shell 实例就可以运行整个配方体：\n\n```just\nfoo:\n  #!/usr/bin/env bash\n  set -euxo pipefail\n  x=hello\n  echo $x\n```\n\n### 在配方之间共享环境变量\n\n每个配方的每一行都由一个新的shell执行，所以不可能在配方之间共享环境变量。\n\n#### 使用 Python 虚拟环境\n\n一些工具，像 [Python 的 venv](https://docs.python.org/3/library/venv.html)，需要加载环境变量才能工作，这使得它们在使用 `just` 时具有挑战性。作为一种变通方法，你可以直接执行虚拟环境二进制文件：\n\n```just\nvenv:\n  [ -d foo ] || python3 -m venv foo\n\nrun: venv\n  ./foo/bin/python3 main.py\n```\n\n### 改变配方中的工作目录\n\n每一行配方都由一个新的 Shell 执行，所以如果你在某一行改变了工作目录，对后面的行不会有影响：\n\n```just\nfoo:\n  pwd    # This `pwd` will print the same directory…\n  cd bar\n  pwd    # …as this `pwd`!\n```\n\n有几个方法可以解决这个问题。一个是在你想运行的命令的同一行调用 `cd`：\n\n```just\nfoo:\n  cd bar && pwd\n```\n\n另一种方法是使用 Shebang 配方。Shebang 配方体被提取并作为脚本运行，因此一个 Shell 实例将运行整个配方体，所以一行的 `pwd` 改变将影响后面的行，就像一个 Shell 脚本：\n\n```just\nfoo:\n  #!/usr/bin/env bash\n  set -euxo pipefail\n  cd bar\n  pwd\n```\n\n### 缩进\n\n配方代码行可以用空格或制表符缩进，但不能两者混合使用。一个配方的所有行必须有相同的缩进，但同一 `justfile` 中的不同配方可以使用不同的缩进。\n\n### 多行结构\n\n没有初始 Shebang 的配方会被逐行评估和运行，这意味着多行结构可能不会像你预期的那样工作。\n\n例如对于下面的 `justfile`：\n\n```mf\nconditional:\n  if true; then\n    echo 'True!'\n  fi\n```\n\n在 `conditional` 配方的第二行前有额外的前导空格，会产生一个解析错误：\n\n```sh\n$ just conditional\nerror: Recipe line has extra leading whitespace\n  |\n3 |         echo 'True!'\n  |     ^^^^^^^^^^^^^^^^\n```\n\n为了解决这个问题，你可以在一行上写条件，用斜线转义换行，或者在你的配方中添加一个 Shebang。我们提供了一些多行结构的例子可供参考。\n\n#### `if` 语句\n\n```just\nconditional:\n  if true; then echo 'True!'; fi\n```\n\n```just\nconditional:\n  if true; then \\\n    echo 'True!'; \\\n  fi\n```\n\n```just\nconditional:\n  #!/usr/bin/env sh\n  if true; then\n    echo 'True!'\n  fi\n```\n\n#### `for` 循环\n\n```just\nfor:\n  for file in `ls .`; do echo $file; done\n```\n\n```just\nfor:\n  for file in `ls .`; do \\\n    echo $file; \\\n  done\n```\n\n```just\nfor:\n  #!/usr/bin/env sh\n  for file in `ls .`; do\n    echo $file\n  done\n```\n\n#### `while` 循环\n\n```just\nwhile:\n  while `server-is-dead`; do ping -c 1 server; done\n```\n\n```just\nwhile:\n  while `server-is-dead`; do \\\n    ping -c 1 server; \\\n  done\n```\n\n```just\nwhile:\n  #!/usr/bin/env sh\n  while `server-is-dead`; do\n    ping -c 1 server\n  done\n```\n\n### 命令行选项\n\n`just` 提供了一些有用的命令行选项，用于列出、Dump 和调试配方以及变量：\n\n```sh\n$ just --list\nAvailable recipes:\n  js\n  perl\n  polyglot\n  python\n  ruby\n$ just --show perl\nperl:\n  #!/usr/bin/env perl\n  print \"Larry Wall says Hi!\\n\";\n$ just --show polyglot\npolyglot: python js perl sh ruby\n```\n\n可以通过 `just --help` 命令查看所有选项。\n\n### 私有配方\n\n名字以 `_` 开头的配方和别名将在 `just --list` 中被忽略：\n\n```just\ntest: _test-helper\n  ./bin/test\n\n_test-helper:\n  ./bin/super-secret-test-helper-stuff\n```\n\n```sh\n$ just --list\nAvailable recipes:\n    test\n```\n\n`just --summary` 亦然：\n\n```sh\n$ just --summary\ntest\n```\n\n`[private]` 属性<sup>1.10.0</sup>也可用于隐藏配方，而不需要改变名称：\n\n```just\n[private]\nfoo:\n\n[private]\nalias b := bar\n\nbar:\n```\n\n```sh\n$ just --list\nAvailable recipes:\n    bar\n```\n\n这对那些只作为其他配方的依赖使用的辅助配方很有用。\n\n### 安静配方\n\n配方名称可在前面加上 `@`，可以在每行反转行首 `@` 的含义：\n\n```just\n@quiet:\n  echo hello\n  echo goodbye\n  @# all done!\n```\n\n现在只有以 `@` 开头的行才会被回显：\n\n```sh\n$ j quiet\nhello\ngoodbye\n# all done!\n```\n\nShebang 配方默认是安静的：\n\n```just\nfoo:\n  #!/usr/bin/env bash\n  echo 'Foo!'\n```\n\n```sh\n$ just foo\nFoo!\n```\n\n在 Shebang 配方名称前面添加 `@`，使 `just` 在执行配方前打印该配方：\n\n```just\n@bar:\n  #!/usr/bin/env bash\n  echo 'Bar!'\n```\n\n```sh\n$ just bar\n#!/usr/bin/env bash\necho 'Bar!'\nBar!\n```\n\n`just` 在配方行失败时通常会打印错误信息，这些错误信息可以通过 `[no-exit-message]`<sup>1.7.0</sup> 属性来抑制。你可能会发现这在包装工具的配方中特别有用：\n\n```just\ngit *args:\n    @git {{args}}\n```\n\n```sh\n$ just git status\nfatal: not a git repository (or any of the parent directories): .git\nerror: Recipe `git` failed on line 2 with exit code 128\n```\n\n添加属性，当工具以非零代码退出时抑制退出错误信息：\n\n```just\n[no-exit-message]\ngit *args:\n    @git {{args}}\n```\n\n```sh\n$ just git status\nfatal: not a git repository (or any of the parent directories): .git\n```\n\n### 通过交互式选择器选择要运行的配方\n\n`--choose` 子命令可以使 `just` 唤起一个选择器来让您选择要运行的配方。选择器应该从标准输入中读取包含配方名称的行，并将其中一个或多个用空格分隔的名称打印到标准输出。\n\n因为目前没有办法通过 `--choose` 运行一个需要传入参数的配方，所以这样的配方将不会在选择器中列出。另外，私有配方和别名也会被忽略。\n\n选择器可以用 `--chooser` 标志来覆写。如果 `--chooser` 没有给出，那么 `just` 首先检查 `$JUST_CHOOSER` 是否被设置。如果没有，那么将使用默认选择器 `fzf`，这是一个流行的模糊查找器。\n\n参数可以包含在选择器中，例如：`fzf --exact`。\n\n选择器的调用方式与配方行的调用方式相同。例如，如果选择器是 `fzf`，它将被通过 `sh -cu 'fzf'` 调用，如果 Shell 或 Shell 参数被覆写，选择器的调用将尊重这些覆写。\n\n如果你希望 `just` 默认用选择器来选择配方，你可以用这个作为你的默认配方：\n\n```just\ndefault:\n  @just --choose\n```\n\n### 在其他目录下调用 `justfile`\n\n如果传递给 `just` 的第一个参数包含 `/`，那么就会发生以下情况：\n\n1.  参数在最后的 `/` 处被分割；\n\n2.  最后一个 `/` 之前的部分将被视为一个目录。`just` 将从这里开始搜索 `justfile`，而不是在当前目录下；\n\n3.  最后一个斜线之后的部分被视为正常参数，如果是空的，则被忽略；\n\n这可能看起来有点奇怪，但如果你想在一个子目录下的 `justfile` 中运行一个命令，这很有用。\n\n例如，如果你在一个目录中，该目录包含一个名为 `foo` 的子目录，该目录包含一个 `justfile`，其配方为 `build`，也是默认的配方，以下都是等同的：\n\n```sh\n$ (cd foo && just build)\n$ just foo/build\n$ just foo/\n```\n\n### 隐藏 `justfile`\n\n`just` 会寻找名为 `justfile` 和 `.justfile` 的 `justfile`，因此你也可以使用隐藏的 `justfile`（即 `.justfile`）。\n\n### Just 脚本\n\n通过在 `justfile` 的顶部添加 Shebang 行并使其可执行，`just` 可以作为脚本的解释器使用：\n\n```sh\n$ cat > script <<EOF\n#!/usr/bin/env just --justfile\n\nfoo:\n  echo foo\nEOF\n$ chmod +x script\n$ ./script foo\necho foo\nfoo\n```\n\n当一个带有 Shebang 的脚本被执行时，系统会提供该脚本的路径作为 Shebang 中命令的参数。因此，如果 Shebang 是 `#!/usr/bin/env just --justfile`，对应的命令将是 `/usr/bin/env just --justfile PATH_TO_SCRIPT`。\n\n对于上面的命令，`just` 会把它的工作目录改为脚本的位置。如果你想让工作目录保持不变，可以使用 `#!/usr/bin/env just --working-directory . --justfile`。\n\n注意：Shebang 的行分隔在不同的操作系统中并不一致。前面的例子只在 macOS 上进行了测试。在 Linux 上，你可能需要向 `env` 传递 `-S` 标志：\n\n```just\n#!/usr/bin/env -S just --justfile\n\ndefault:\n  echo foo\n```\n\n### 将 `justfile` 转为JSON文件\n\n`--dump` 命令可以和 `--dump-format json` 一起使用，以打印一个 `justfile` 的JSON表示。JSON格式目前还不稳定，所以需要添加 `--unstable` 标志。\n\n### 回退到父 `justfile`\n\n如果在 `justfile` 中没有找到配方，并且设置了 `fallback`，`just` 将在父目录及其上级目录寻找`justfile`，直到到达根目录。`just` 在找到其中的 `fallback` 设置为`false` 或未设置的 `justfile` 时将停止。\n\n举个例子，假设当前目录包含这个 `justfile`：\n\n```just\nset fallback\nfoo:\n  echo foo\n```\n\n而父目录包含这个 `justfile`：\n\n```just\nbar:\n  echo bar\n```\n\n```sh\n$ just --unstable bar\nTrying ../justfile\necho bar\nbar\n```\n\n### 避免参数分割\n\n考虑这个 `justfile`:\n\n```just\nfoo argument:\n  touch {{argument}}\n```\n\n下面的命令将创建两个文件，`some` 和 `argument.txt`：\n\n```sh\n$ just foo \"some argument.txt\"\n```\n\n用户 Shell 会把 `\"some argument.txt\"` 解析为一个参数，但当 `just` 把 `touch {{argument}}` 替换为`touch some argument.txt` 时，引号没有被保留，`touch` 会收到两个参数。\n\n有几种方法可以避免这种情况：引号包裹、位置参数和导出参数。\n\n#### 引号包裹\n\n可以在 `{{argument}}` 的周围加上引号，进行插值：\n\n```just\nfoo argument:\n  touch '{{argument}}'\n```\n\n这保留了 `just` 在运行前捕捉变量名称拼写错误的能力，例如，如果你写成了 `{{argument}}`，但如果 `argument` 的值包含单引号，则不会如你的预期那样工作。\n\n#### 位置参数\n\n设置 `positional-arguments` 使所有参数作为位置参数传递，允许用 `$1`, `$2`, …, 和 `$@` 访问这些参数，然后可以用双引号避免被 Shell 进一步分割：\n\n```just\nset positional-arguments\n\nfoo argument:\n  touch \"$1\"\n```\n\n这就破坏了 `just` 捕捉拼写错误的能力，例如你输入了 `$2`，这对 `argument` 的所有可能的值都有效，包括那些带双引号的值。\n\n#### 导出参数\n\n当设置 `export` 时，所有参数都被导出：\n\n```just\nset export\n\nfoo argument:\n  touch \"$argument\"\n```\n\n或者可以通过在参数前加上 `$` 来导出单个参数：\n\n```just\nfoo $argument:\n  touch \"$argument\"\n```\n\n这就破坏了 `just` 捕捉拼写错误的能力，例如你输入 `$argumant`，但对 `argument` 的所有可能的值都有效，包括那些带双引号的。\n\n### 配置 Shell\n\n有许多方法可以为行式配方配置 Shell，当配方不以 `#！` Shebang 开头时，这些配方的 Shell 为默认的。它们的优先级，从高到低为：\n\n1. `--shell` 和 `--shell-arg` 命令行选项。传入这两个选项中的任何一个，都会使 `just` 忽略当前 justfile 中的任何设置\n2. `set windows-shell := [...]`\n3. `set windows-powershell` (废弃)\n4. `set shell := [...]`\n\n由于 `set windows-shell` 比 `set shell` 有更高的优先级，你可以用 `set windows-shell` 在 Windows 上选择一个 Shell，而 `set shell` 则为所有其他平台选择一个 Shell。\n\n更新日志\n---------\n\n最新版本的更新日志可以在 [CHANGELOG.md](https://raw.githubusercontent.com/casey/just/master/CHANGELOG.md) 中找到。以前版本的更新日志可在 [发布页](https://github.com/casey/just/releases) 找到。`just --changelog` 也可以用来使 `just` 二进制文件打印其更新日志。\n\n杂项\n-----------\n\n### 配套工具\n\n与 `just` 搭配得很好的工具包括：\n\n- [`watchexec`](https://github.com/mattgreen/watchexec) — 一个简单的工具，它监控一个路径，并在检测到修改时运行一个命令。\n\n### 并行运行任务\n\nGNU parallel 可以用来同时运行多个任务：\n\n```just\nparallel:\n  #!/usr/bin/env -S parallel --shebang --ungroup --jobs {{ num_cpus() }}\n  echo task 1 start; sleep 3; echo task 1 done\n  echo task 2 start; sleep 3; echo task 2 done\n  echo task 3 start; sleep 3; echo task 3 done\n  echo task 4 start; sleep 3; echo task 4 done\n```\n\n### Shell 别名\n\n为了快速运行命令, 可以把 `alias j=just` 放在你的 Shell 配置文件中。\n\n在 `bash` 中，别名的命令可能不会保留下一节中描述的 Shell 自动补全功能。可以在你的 `.bashrc` 中添加以下一行，以便在你的别名命令中使用与 `just` 相同的自动补全功能：\n\n```sh\ncomplete -F _just -o bashdefault -o default j\n```\n\n### Shell 自动补全脚本\n\nBash、Zsh、Fish、PowerShell 和 Elvish 的 Shell 自动补全脚本可以在 [自动补全](https://github.com/casey/just/tree/master/completions) 目录下找到。关于如何安装它们，请参考你的 Shell 文档。\n\n`just` 二进制文件也可以在运行时生成相同的自动补全脚本，使用 `--completions` 命令即可，如下：\n\n```sh\n$ just --completions zsh > just.zsh\n```\n\n*macOS 注意:* 最近版本的 macOS 使用 zsh 作为默认的 Shell。如果你使用 Homebrew 安装 `just`，它会自动安装 zsh 补全脚本的最新副本到 Homebrew zsh 目录下，而内置默认版本的 zsh 是不知道的。如果可能的话，最好使用这个脚本副本，因为当你通过 Homebrew 更新 `just` 时，它也会被更新。另外，许多其他的 Homebrew 软件包也使用相同位置的补全脚本，而内置的 zsh 也不知道这些。为了在这种情况下在 zsh 中使用 `just` 的补全，你可以在调用 `compinit` 之前将 `fpath` 设置为 Homebrew 的位置。还要注意，Oh My Zsh 默认会运行 `compinit`，所以你的 `.zshrc` 文件看起来像这样：\n\n```zsh\n# 启动Homebrew，添加环境变量\neval \"$(brew shellenv)\"\n\nfpath=($HOMEBREW_PREFIX/share/zsh/site-functions $fpath)\n\n# 然后从这些选项中选择一个:\n# 1. 如果你使用的是 Oh My Zsh，你可以在这里初始化它\n# source $ZSH/oh-my-zsh.sh\n\n# 2. 否则就自己运行 compinit\n# autoload -U compinit\n# compinit\n```\n\n### 语法\n\n在 [GRAMMAR.md](https://github.com/casey/just/blob/master/GRAMMAR.md) 中可以找到一个非正式的 `justfile` 语法说明。\n\n### just.sh\n\n在 `just` 成为一个精致的 Rust 程序之前，它是一个很小的 Shell 脚本，叫 `make`。你可以在 [contrib/just.sh](https://github.com/casey/just/blob/master/contrib/just.sh) 中找到旧版本。\n\n### 用户 `justfile`\n\n如果你想让一些配方在任何地方都能使用，你有几个选择。\n\n首先，在 `~/.user.justfile` 中创建一个带有一些配方的 `justfile`。\n\n#### 配方别名\n\n如果你想通过名称来调用 `~/.user.justfile` 中的配方，并且不介意为每个配方创建一个别名，可以在你的 Shell 初始化脚本中加入以下内容：\n\n```sh\nfor recipe in `just --justfile ~/.user.justfile --summary`; do\n  alias $recipe=\"just --justfile ~/.user.justfile --working-directory . $recipe\"\ndone\n```\n\n现在，如果你在 `~/.user.justfile` 里有一个叫 `foo` 的配方，你可以在命令行输入 `foo` 来运行它。\n\n我花了很长时间才意识到你可以像这样创建配方别名。尽管有点迟，但我很高兴给你带来这个 `justfile` 技术的重大进步。\n\n#### 别名转发\n\n如果你不想为每个配方创建别名，你可以创建一个别名：\n\n```sh\nalias .j='just --justfile ~/.user.justfile --working-directory .'\n```\n\n现在，如果你在 `~/.user.justfile` 里有一个叫 `foo` 的配方，你可以在命令行输入 `.j foo` 来运行它。\n\n我很确定没有人真正使用这个功能，但它确实存在。\n\n¯\\\\\\_(ツ)\\_/¯\n\n#### 定制化\n\n你可以用额外的选项来定制上述别名。例如，如果你想让你的 `justfile` 中的配方在你的主目录中运行，而不是在当前目录中运行：\n\n```sh\nalias .j='just --justfile ~/.user.justfile --working-directory ~'\n```\n\n### Node.js `package.json` 脚本兼容性\n\n下面的导出语句使 `just` 配方能够访问本地 Node 模块二进制文件，并使 `just` 配方命令的行为更像 Node.js `package.json` 文件中的 `script` 条目：\n\n```just\nexport PATH := \"./node_modules/.bin:\" + env_var('PATH')\n```\n\n### 替代方案\n\n现在并不缺少命令运行器！在这里，有一些或多或少比较类似于 `just` 的替代方案，包括：\n\n- [make](https://en.wikipedia.org/wiki/Make_(software)): 启发了 `just` 的 Unix 构建工具。最初的 `make` 有几个不同的现代后裔, 包括 [FreeBSD Make](https://www.freebsd.org/cgi/man.cgi?make(1)) 和 [GNU Make](https://www.gnu.org/software/make/)。\n- [task](https://github.com/go-task/task): 一个用 Go 编写的基于 YAML 的命令运行器。\n- [maid](https://github.com/egoist/maid): 一个用 JavaScript 编写的基于 Markdown 的命令运行器。\n- [microsoft/just](https://github.com/microsoft/just): 一个用 JavaScript 编写的基于 JavasScript 的命令运行器。\n- [cargo-make](https://github.com/sagiegurari/cargo-make): 一个用于 Rust 项目的命令运行器。\n- [mmake](https://github.com/tj/mmake): 一个针对 `make` 的包装器，有很多改进，包括远程包含。\n- [robo](https://github.com/tj/robo): 一个用 Go 编写的基于 YAML 的命令运行器。\n- [mask](https://github.com/jakedeichert/mask): 一个用 Rust 编写的基于 Markdown 的命令运行器。\n- [makesure](https://github.com/xonixx/makesure): 一个用 AWK 和 Shell 编写的简单而便携的命令运行器。\n- [haku](https://github.com/VladimirMarkelov/haku): 一个用 Rust 编写的类似 make 的命令运行器。\n\n贡献\n------------\n\n`just` 欢迎你的贡献! `just` 是在最大许可的 [CC0](https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt) 公共领域奉献和后备许可下发布的，所以你的修改也必须在这个许可下发布。\n\n### Janus\n\n[Janus](https://github.com/casey/janus) 是一个收集和分析 `justfile` 的工具，可以确定新版本的 `just` 是否会破坏或改变现有 `justfile` 的解析。\n\n在合并一个特别大的或可怕的变化之前，应该运行 `Janus` 以确保没有任何破坏。不要担心自己运行 `Janus`，Casey 会很乐意在需要时为你运行它。\n\n### 最小支持的 Rust 版本\n\n最低支持的 Rust 版本，或 MSRV，是当前稳定的(current stable) Rust。它可能可以在旧版本的 Rust 上构建，但这并不保证。\n\n### 新版本\n\n`just` 会经常发布新版本，以便用户快速获得新功能。\n\n发布的提交信息使用如下模板：\n\n```\nRelease x.y.z\n\n- Bump version: x.y.z → x.y.z\n- Update changelog\n- Update changelog contributor credits\n- Update dependencies\n- Update man page\n- Update version references in readme\n```\n\n常见问题\n--------------------------\n\n### Just 避免了 Make 的哪些特异性？\n\n`make` 有一些行为令人感到困惑、复杂，或者使它不适合作为通用的命令运行器。\n\n一个例子是，在某些情况下，`make` 不会实际运行配方中的命令。例如，如果你有一个名为 `test` 的文件和以下 makefile：\n\n```just\ntest:\n  ./test\n```\n\n`make` 将会拒绝运行你的测试：\n\n```sh\n$ make test\nmake: `test' is up to date.\n```\n\n`make` 假定 `test` 配方产生一个名为 `test` 的文件。由于这个文件已经存在，而且由于配方没有其他依赖，`make` 认为它没有任何事情可做并退出。\n\n公平地说，当把 `make` 作为一个构建系统时，这种行为是可取的，但当把它作为一个命令运行器时就不可取了。你可以使用 `make` 内置的 [`.PHONY` 目标名称](https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html) 为特定的目标禁用这种行为，但其语法很冗长，而且很难记住。明确的虚假目标列表与配方定义分开写，也带来了意外定义新的非虚假目标的风险。在 `just` 中，所有的配方都被当作是虚假的。\n\n其他 `make` 特异行为的例子包括赋值中 `=` 和 `:=` 的区别；如果你弄乱了你的 makefile，将会产生混乱的错误信息；需要 `$$` 在配方中使用环境变量；以及不同口味的 `make` 之间的不相容性。\n\n### Just 和 Cargo 构建脚本之间有什么关系？\n\n[`cargo` 构建脚本](http://doc.crates.io/build-script.html) 有一个相当特定的用途，就是控制 `cargo` 如何构建你的 Rust 项目。这可能包括给 `rustc` 调用添加标志，构建外部依赖，或运行某种 codegen 步骤。\n\n另一方面，`just` 是用于你可能在开发中会运行的所有其他的杂项命令。比如在不同的配置下运行测试，对代码进行检查，将构建的产出推送到服务器，删除临时文件，等等。\n\n另外，尽管 `just` 是用 Rust 编写的，但它可以被用于任何语言或项目使用的构建系统。\n\n进一步漫谈\n-----------------\n\n我个人认为为几乎每个项目写一个 `justfile` 非常有用，无论大小。\n\n在一个有多个贡献者的大项目中，有一个包含项目工作所需的所有命令的文件是非常有用的，这样所有命令唾手可得。\n\n可能有不同的命令来测试、构建、检查、部署等等，把它们都放在一个地方是很方便的，可以减少你花在告诉人们要运行哪些命令和如何输入这些命令的时间。\n\n而且，有了一个容易放置命令的地方，你很可能会想出其他有用的东西，这些东西是项目集体智慧的一部分，但没有写在任何地方，比如修订控制工作流程的某些部分需要的神秘命令，安装你项目的所有依赖，或者所有你可能需要传递给构建系统的任意标志等。\n\n一些关于配方的想法：\n\n- 部署/发布项目\n\n- 在发布模式与调试模式下进行构建\n\n- 在调试模式下运行或启用日志记录功能\n\n- 复杂的 git 工作流程\n\n- 更新依赖\n\n- 运行不同的测试集，例如快速测试与慢速测试，或以更多输出模式运行它们\n\n- 任何复杂的命令集，你真的应该写下来，如果只是为了能够记住它们的话\n\n即使是小型的个人项目，能够通过名字记住命令，而不是通过 ^Reverse 搜索你的 Shell 历史，这也是一个巨大的福音，能够进入一个用任意语言编写的旧项目，并知道你需要用到的所有命令都在 `justfile` 中，如果你输入 `just`，就可能会输出一些有用的（或至少是有趣的！）信息。\n\n关于配方的想法，请查看 [这个项目的 `justfile`](https://github.com/casey/just/blob/master/justfile)，或一些 [在其他项目里](https://github.com/search?q=path%3A**%2Fjustfile&type=code) 的 `justfile`。\n\n总之，我想这个令人难以置信地啰嗦的 README 就到此为止了。\n\n我希望你喜欢使用 `just`，并在你所有的计算工作中找到巨大的成功和满足！\n\n😸\n"
  },
  {
    "path": "Vagrantfile",
    "content": "Vagrant.configure(2) do |config|\n  config.vm.box = 'debian/jessie64'\n\n  config.vm.provision \"shell\", inline: <<-EOS\n    apt-get -y update\n    apt-get install -y clang git vim curl\n  EOS\n\n  config.vm.provision \"shell\", privileged: false, inline: <<-EOS\n    curl https://sh.rustup.rs -sSf > install-rustup\n    chmod +x install-rustup\n    ./install-rustup -y\n    source ~/.cargo/env\n    rustup target add x86_64-unknown-linux-musl\n    cargo install -f just\n    git clone https://github.com/casey/just.git\n  EOS\nend\n"
  },
  {
    "path": "bin/forbid",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nif ! which rg > /dev/null; then\n  echo 'error: `rg` not found, please install ripgrep: https://github.com/BurntSushi/ripgrep/'\n  exit 1\nfi\n\n! rg \\\n  --glob !CHANGELOG.md \\\n  --glob !bin/forbid \\\n  --ignore-case \\\n  'dbg!|fixme|todo|xxx' \\\n  .\n"
  },
  {
    "path": "bin/package",
    "content": "#!/usr/bin/env bash\n\nset -euxo pipefail\n\nVERSION=${REF#\"refs/tags/\"}\nDIST=`pwd`/dist\n\necho \"Packaging just $VERSION for $TARGET...\"\n\ntest -f Cargo.lock || cargo generate-lockfile\n\necho \"Installing rust toolchain for $TARGET...\"\nrustup target add $TARGET\n\nif [[ $TARGET == aarch64-unknown-linux-musl ]]; then\n  export CC=aarch64-linux-gnu-gcc\nfi\n\necho \"Building just...\"\nRUSTFLAGS=\"--deny warnings --codegen target-feature=+crt-static $TARGET_RUSTFLAGS\" \\\n  cargo build --bin just --target $TARGET --release\nEXECUTABLE=target/$TARGET/release/just\n\nif [[ $OS == windows-latest ]]; then\n  EXECUTABLE=$EXECUTABLE.exe\nfi\n\necho \"Copying release files...\"\nmkdir dist\ncp -r \\\n  $EXECUTABLE \\\n  Cargo.lock \\\n  Cargo.toml \\\n  GRAMMAR.md \\\n  LICENSE \\\n  README.md \\\n  completions \\\n  man/just.1 \\\n  $DIST\n\ncd $DIST\necho \"Creating release archive...\"\ncase $OS in\n  ubuntu-latest | macos-latest)\n    ARCHIVE=just-$VERSION-$TARGET.tar.gz\n    tar czf $ARCHIVE *\n    echo \"archive=$DIST/$ARCHIVE\" >> $GITHUB_OUTPUT\n    ;;\n  windows-latest)\n    ARCHIVE=just-$VERSION-$TARGET.zip\n    7z a $ARCHIVE *\n    echo \"archive=`pwd -W`/$ARCHIVE\" >> $GITHUB_OUTPUT\n    ;;\nesac\n"
  },
  {
    "path": "book/en/book.toml",
    "content": "[book]\nlanguage = \"en\"\nsrc = \"src\"\ntitle = \"Just Programmer's Manual\"\n\n[build]\nbuild-dir = \"build\"\n\n[output.html]\ngit-repository-url = \"https://github.com/casey/just\"\nsite-url = \"/man/en/\"\n\n[output.linkcheck]\n"
  },
  {
    "path": "book/zh/book.toml",
    "content": "[book]\nlanguage = \"zh\"\nsrc = \"src\"\ntitle = \"Just 用户指南\"\n\n[build]\nbuild-dir = \"build\"\n\n[output.html]\ngit-repository-url = \"https://github.com/casey/just\"\nsite-url = \"/man/zh/\"\n\n[output.linkcheck]\n"
  },
  {
    "path": "build.rs",
    "content": "fn main() {\n  let os = std::env::var(\"CARGO_CFG_TARGET_OS\").unwrap();\n  let env = std::env::var(\"CARGO_CFG_TARGET_ENV\").unwrap();\n  if os == \"windows\" {\n    if env == \"msvc\" {\n      println!(\"cargo::rustc-link-arg=/STACK:2097152\");\n    } else if env == \"gnu\" {\n      println!(\"cargo::rustc-link-arg=-Wl,--stack,2097152\");\n    }\n  }\n}\n"
  },
  {
    "path": "clippy.toml",
    "content": "cognitive-complexity-threshold = 1337\nsource-item-ordering = ['enum', 'struct', 'trait']\n"
  },
  {
    "path": "completions/just.bash",
    "content": "_just() {\n    local i cur prev words cword opts cmd\n    COMPREPLY=()\n\n    # Modules use \"::\" as the separator, which is considered a wordbreak character in bash.\n    # The _get_comp_words_by_ref function is a hack to allow for exceptions to this rule without\n    # modifying the global COMP_WORDBREAKS environment variable.\n    if type _get_comp_words_by_ref &>/dev/null; then\n        _get_comp_words_by_ref -n : cur prev words cword\n    else\n        cur=\"${COMP_WORDS[COMP_CWORD]}\"\n        prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n        words=$COMP_WORDS\n        cword=$COMP_CWORD\n    fi\n\n    cmd=\"\"\n    opts=\"\"\n\n    for i in ${words[@]}\n    do\n        case \"${cmd},${i}\" in\n            \",$1\")\n                cmd=\"just\"\n                ;;\n            *)\n                ;;\n        esac\n    done\n\n    case \"${cmd}\" in\n        just)\n            opts=\"-E -n -g -f -q -u -v -d -c -e -l -s -h -V --alias-style --ceiling --check --chooser --clear-shell-args --color --command-color --cygpath --dotenv-filename --dotenv-path --dry-run --dump-format --explain --global-justfile --highlight --justfile --list-heading --list-prefix --list-submodules --group --no-aliases --no-deps --no-dotenv --no-highlight --one --quiet --allow-missing --set --shell --shell-arg --shell-command --tempdir --timestamp --timestamp-format --unsorted --unstable --verbose --working-directory --yes --changelog --choose --command --completions --dump --edit --evaluate --fmt --groups --init --json --list --man --request --show --summary --usage --variables --help --version [ARGUMENTS]...\"\n                if [[ ${cur} == -* ]] ; then\n                    COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )\n                    return 0\n                else\n                    local recipes=$(just --summary 2> /dev/null)\n\n                    if echo \"${cur}\" | \\grep -qF '/'; then\n                        local path_prefix=$(echo \"${cur}\" | sed 's/[/][^/]*$/\\//')\n                        local recipes=$(just --summary 2> /dev/null -- \"${path_prefix}\")\n                        local recipes=$(printf \"${path_prefix}%s\\t\" $recipes)\n                    fi\n\n                    if [[ $? -eq 0 ]]; then\n                        COMPREPLY=( $(compgen -W \"${recipes}\" -- \"${cur}\") )\n                        if type __ltrim_colon_completions &>/dev/null; then\n                            __ltrim_colon_completions \"$cur\"\n                        fi\n                        return 0\n                    fi\n                fi\n            case \"${prev}\" in\n                --alias-style)\n                    COMPREPLY=($(compgen -W \"left right separate\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --ceiling)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --chooser)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --color)\n                    COMPREPLY=($(compgen -W \"always auto never\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --command-color)\n                    COMPREPLY=($(compgen -W \"black blue cyan green purple red yellow\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --cygpath)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --dotenv-filename)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --dotenv-path)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -E)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --dump-format)\n                    COMPREPLY=($(compgen -W \"json just\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --justfile)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -f)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --list-heading)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --list-prefix)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --group)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --set)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --shell)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --shell-arg)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --tempdir)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --timestamp-format)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --working-directory)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -d)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --command)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -c)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --completions)\n                    COMPREPLY=($(compgen -W \"bash elvish fish nushell powershell zsh\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --list)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -l)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --request)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --show)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -s)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --usage)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                *)\n                    COMPREPLY=()\n                    ;;\n            esac\n            COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )\n            return 0\n            ;;\n    esac\n}\n\nif [[ \"${BASH_VERSINFO[0]}\" -eq 4 && \"${BASH_VERSINFO[1]}\" -ge 4 || \"${BASH_VERSINFO[0]}\" -gt 4 ]]; then\n    complete -F _just -o nosort -o bashdefault -o default just\nelse\n    complete -F _just -o bashdefault -o default just\nfi\n"
  },
  {
    "path": "completions/just.elvish",
    "content": "use builtin;\nuse str;\n\nset edit:completion:arg-completer[just] = {|@words|\n    fn spaces {|n|\n        builtin:repeat $n ' ' | str:join ''\n    }\n    fn cand {|text desc|\n        edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc\n    }\n    var command = 'just'\n    for word $words[1..-1] {\n        if (str:has-prefix $word '-') {\n            break\n        }\n        set command = $command';'$word\n    }\n    var completions = [\n        &'just'= {\n            cand --alias-style 'Set list command alias display style'\n            cand --ceiling 'Do not ascend above <CEILING> directory when searching for a justfile.'\n            cand --chooser 'Override binary invoked by `--choose`'\n            cand --color 'Print colorful output'\n            cand --command-color 'Echo recipe lines in <COMMAND-COLOR>'\n            cand --cygpath 'Use binary at <CYGPATH> to convert between unix and Windows paths.'\n            cand --dotenv-filename 'Search for environment file named <DOTENV-FILENAME> instead of `.env`'\n            cand -E 'Load <DOTENV-PATH> as environment file instead of searching for one'\n            cand --dotenv-path 'Load <DOTENV-PATH> as environment file instead of searching for one'\n            cand --dump-format 'Dump justfile as <FORMAT>'\n            cand -f 'Use <JUSTFILE> as justfile'\n            cand --justfile 'Use <JUSTFILE> as justfile'\n            cand --list-heading 'Print <TEXT> before list'\n            cand --list-prefix 'Print <TEXT> before each list item'\n            cand --group 'Only list recipes in <GROUP>'\n            cand --set 'Override <VARIABLE> with <VALUE>'\n            cand --shell 'Invoke <SHELL> to run recipes'\n            cand --shell-arg 'Invoke shell with <SHELL-ARG> as an argument'\n            cand --tempdir 'Save temporary files to <TEMPDIR>.'\n            cand --timestamp-format 'Timestamp format string'\n            cand -d 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set'\n            cand --working-directory 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set'\n            cand -c 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set'\n            cand --command 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set'\n            cand --completions 'Print shell completion script for <SHELL>'\n            cand -l 'List available recipes in <MODULE> or root if omitted'\n            cand --list 'List available recipes in <MODULE> or root if omitted'\n            cand --request 'Execute <REQUEST>. For internal testing purposes only. May be changed or removed at any time.'\n            cand -s 'Show recipe at <PATH>'\n            cand --show 'Show recipe at <PATH>'\n            cand --usage 'Print recipe usage information'\n            cand --check 'Run `--fmt` in ''check'' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.'\n            cand --clear-shell-args 'Clear shell arguments'\n            cand -n 'Print what just would do without doing it'\n            cand --dry-run 'Print what just would do without doing it'\n            cand --explain 'Print recipe doc comment before running it'\n            cand -g 'Use global justfile'\n            cand --global-justfile 'Use global justfile'\n            cand --highlight 'Highlight echoed recipe lines in bold'\n            cand --list-submodules 'List recipes in submodules'\n            cand --no-aliases 'Don''t show aliases in list'\n            cand --no-deps 'Don''t run recipe dependencies'\n            cand --no-dotenv 'Don''t load `.env` file'\n            cand --no-highlight 'Don''t highlight echoed recipe lines in bold'\n            cand --one 'Forbid multiple recipes from being invoked on the command line'\n            cand -q 'Suppress all output'\n            cand --quiet 'Suppress all output'\n            cand --allow-missing 'Ignore missing recipe and module errors'\n            cand --shell-command 'Invoke <COMMAND> with the shell used to run recipe lines and backticks'\n            cand --timestamp 'Print recipe command timestamps'\n            cand -u 'Return list and summary entries in source order'\n            cand --unsorted 'Return list and summary entries in source order'\n            cand --unstable 'Enable unstable features'\n            cand -v 'Use verbose output'\n            cand --verbose 'Use verbose output'\n            cand --yes 'Automatically confirm all recipes.'\n            cand --changelog 'Print changelog'\n            cand --choose 'Select one or more recipes to run using a binary chooser. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`'\n            cand --dump 'Print justfile'\n            cand -e 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'\n            cand --edit 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'\n            cand --evaluate 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable''s value.'\n            cand --fmt 'Format and overwrite justfile'\n            cand --groups 'List recipe groups'\n            cand --init 'Initialize new justfile in project root'\n            cand --json 'Print justfile as JSON'\n            cand --man 'Print man page'\n            cand --summary 'List names of available recipes'\n            cand --variables 'List names of variables'\n            cand -h 'Print help'\n            cand --help 'Print help'\n            cand -V 'Print version'\n            cand --version 'Print version'\n        }\n    ]\n    $completions[$command]\n}\n"
  },
  {
    "path": "completions/just.fish",
    "content": "function __fish_just_complete_recipes\n        if string match -rq '(-f|--justfile)\\s*=?(?<justfile>[^\\s]+)' -- (string split -- ' -- ' (commandline -pc))[1]\n          set -fx JUST_JUSTFILE \"$justfile\"\n        end\n        printf \"%s\\n\" (string split \" \" (just --summary))\nend\n\n# don't suggest files right off\ncomplete -c just -n \"__fish_is_first_arg\" --no-files\n\n# complete recipes\ncomplete -c just -a '(__fish_just_complete_recipes)'\n\n# autogenerated completions\ncomplete -c just -l alias-style -d 'Set list command alias display style' -r -f -a \"left\\t''\nright\\t''\nseparate\\t''\"\ncomplete -c just -l ceiling -d 'Do not ascend above <CEILING> directory when searching for a justfile.' -r -F\ncomplete -c just -l chooser -d 'Override binary invoked by `--choose`' -r\ncomplete -c just -l color -d 'Print colorful output' -r -f -a \"always\\t''\nauto\\t''\nnever\\t''\"\ncomplete -c just -l command-color -d 'Echo recipe lines in <COMMAND-COLOR>' -r -f -a \"black\\t''\nblue\\t''\ncyan\\t''\ngreen\\t''\npurple\\t''\nred\\t''\nyellow\\t''\"\ncomplete -c just -l cygpath -d 'Use binary at <CYGPATH> to convert between unix and Windows paths.' -r -F\ncomplete -c just -l dotenv-filename -d 'Search for environment file named <DOTENV-FILENAME> instead of `.env`' -r\ncomplete -c just -s E -l dotenv-path -d 'Load <DOTENV-PATH> as environment file instead of searching for one' -r -F\ncomplete -c just -l dump-format -d 'Dump justfile as <FORMAT>' -r -f -a \"json\\t''\njust\\t''\"\ncomplete -c just -s f -l justfile -d 'Use <JUSTFILE> as justfile' -r -F\ncomplete -c just -l list-heading -d 'Print <TEXT> before list' -r\ncomplete -c just -l list-prefix -d 'Print <TEXT> before each list item' -r\ncomplete -c just -l group -d 'Only list recipes in <GROUP>' -r\ncomplete -c just -l set -d 'Override <VARIABLE> with <VALUE>' -r\ncomplete -c just -l shell -d 'Invoke <SHELL> to run recipes' -r\ncomplete -c just -l shell-arg -d 'Invoke shell with <SHELL-ARG> as an argument' -r\ncomplete -c just -l tempdir -d 'Save temporary files to <TEMPDIR>.' -r -F\ncomplete -c just -l timestamp-format -d 'Timestamp format string' -r\ncomplete -c just -s d -l working-directory -d 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set' -r -F\ncomplete -c just -s c -l command -d 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set' -r\ncomplete -c just -l completions -d 'Print shell completion script for <SHELL>' -r -f -a \"bash\\t''\nelvish\\t''\nfish\\t''\nnushell\\t''\npowershell\\t''\nzsh\\t''\"\ncomplete -c just -s l -l list -d 'List available recipes in <MODULE> or root if omitted' -r\ncomplete -c just -l request -d 'Execute <REQUEST>. For internal testing purposes only. May be changed or removed at any time.' -r\ncomplete -c just -s s -l show -d 'Show recipe at <PATH>' -r\ncomplete -c just -l usage -d 'Print recipe usage information' -r\ncomplete -c just -l check -d 'Run `--fmt` in \\'check\\' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.'\ncomplete -c just -l clear-shell-args -d 'Clear shell arguments'\ncomplete -c just -s n -l dry-run -d 'Print what just would do without doing it'\ncomplete -c just -l explain -d 'Print recipe doc comment before running it'\ncomplete -c just -s g -l global-justfile -d 'Use global justfile'\ncomplete -c just -l highlight -d 'Highlight echoed recipe lines in bold'\ncomplete -c just -l list-submodules -d 'List recipes in submodules'\ncomplete -c just -l no-aliases -d 'Don\\'t show aliases in list'\ncomplete -c just -l no-deps -d 'Don\\'t run recipe dependencies'\ncomplete -c just -l no-dotenv -d 'Don\\'t load `.env` file'\ncomplete -c just -l no-highlight -d 'Don\\'t highlight echoed recipe lines in bold'\ncomplete -c just -l one -d 'Forbid multiple recipes from being invoked on the command line'\ncomplete -c just -s q -l quiet -d 'Suppress all output'\ncomplete -c just -l allow-missing -d 'Ignore missing recipe and module errors'\ncomplete -c just -l shell-command -d 'Invoke <COMMAND> with the shell used to run recipe lines and backticks'\ncomplete -c just -l timestamp -d 'Print recipe command timestamps'\ncomplete -c just -s u -l unsorted -d 'Return list and summary entries in source order'\ncomplete -c just -l unstable -d 'Enable unstable features'\ncomplete -c just -s v -l verbose -d 'Use verbose output'\ncomplete -c just -l yes -d 'Automatically confirm all recipes.'\ncomplete -c just -l changelog -d 'Print changelog'\ncomplete -c just -l choose -d 'Select one or more recipes to run using a binary chooser. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`'\ncomplete -c just -l dump -d 'Print justfile'\ncomplete -c just -s e -l edit -d 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'\ncomplete -c just -l evaluate -d 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable\\'s value.'\ncomplete -c just -l fmt -d 'Format and overwrite justfile'\ncomplete -c just -l groups -d 'List recipe groups'\ncomplete -c just -l init -d 'Initialize new justfile in project root'\ncomplete -c just -l json -d 'Print justfile as JSON'\ncomplete -c just -l man -d 'Print man page'\ncomplete -c just -l summary -d 'List names of available recipes'\ncomplete -c just -l variables -d 'List names of variables'\ncomplete -c just -s h -l help -d 'Print help'\ncomplete -c just -s V -l version -d 'Print version'\n"
  },
  {
    "path": "completions/just.nu",
    "content": "def \"nu-complete just\" [] {\n    (^just --dump --unstable --dump-format json | from json).recipes | transpose recipe data | flatten | where {|row| $row.private == false } | select recipe doc parameters | rename value description\n}\n\n# Just: A Command Runner\nexport extern \"just\" [\n    ...recipe: string@\"nu-complete just\", # Recipe(s) to run, may be with argument(s)\n]\n"
  },
  {
    "path": "completions/just.powershell",
    "content": "using namespace System.Management.Automation\nusing namespace System.Management.Automation.Language\n\nRegister-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {\n    param($wordToComplete, $commandAst, $cursorPosition)\n\n    $commandElements = $commandAst.CommandElements\n    $command = @(\n        'just'\n        for ($i = 1; $i -lt $commandElements.Count; $i++) {\n            $element = $commandElements[$i]\n            if ($element -isnot [StringConstantExpressionAst] -or\n                $element.StringConstantType -ne [StringConstantType]::BareWord -or\n                $element.Value.StartsWith('-') -or\n                $element.Value -eq $wordToComplete) {\n                break\n        }\n        $element.Value\n    }) -join ';'\n\n    $completions = @(switch ($command) {\n        'just' {\n            [CompletionResult]::new('--alias-style', '--alias-style', [CompletionResultType]::ParameterName, 'Set list command alias display style')\n            [CompletionResult]::new('--ceiling', '--ceiling', [CompletionResultType]::ParameterName, 'Do not ascend above <CEILING> directory when searching for a justfile.')\n            [CompletionResult]::new('--chooser', '--chooser', [CompletionResultType]::ParameterName, 'Override binary invoked by `--choose`')\n            [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'Print colorful output')\n            [CompletionResult]::new('--command-color', '--command-color', [CompletionResultType]::ParameterName, 'Echo recipe lines in <COMMAND-COLOR>')\n            [CompletionResult]::new('--cygpath', '--cygpath', [CompletionResultType]::ParameterName, 'Use binary at <CYGPATH> to convert between unix and Windows paths.')\n            [CompletionResult]::new('--dotenv-filename', '--dotenv-filename', [CompletionResultType]::ParameterName, 'Search for environment file named <DOTENV-FILENAME> instead of `.env`')\n            [CompletionResult]::new('-E', '-E ', [CompletionResultType]::ParameterName, 'Load <DOTENV-PATH> as environment file instead of searching for one')\n            [CompletionResult]::new('--dotenv-path', '--dotenv-path', [CompletionResultType]::ParameterName, 'Load <DOTENV-PATH> as environment file instead of searching for one')\n            [CompletionResult]::new('--dump-format', '--dump-format', [CompletionResultType]::ParameterName, 'Dump justfile as <FORMAT>')\n            [CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Use <JUSTFILE> as justfile')\n            [CompletionResult]::new('--justfile', '--justfile', [CompletionResultType]::ParameterName, 'Use <JUSTFILE> as justfile')\n            [CompletionResult]::new('--list-heading', '--list-heading', [CompletionResultType]::ParameterName, 'Print <TEXT> before list')\n            [CompletionResult]::new('--list-prefix', '--list-prefix', [CompletionResultType]::ParameterName, 'Print <TEXT> before each list item')\n            [CompletionResult]::new('--group', '--group', [CompletionResultType]::ParameterName, 'Only list recipes in <GROUP>')\n            [CompletionResult]::new('--set', '--set', [CompletionResultType]::ParameterName, 'Override <VARIABLE> with <VALUE>')\n            [CompletionResult]::new('--shell', '--shell', [CompletionResultType]::ParameterName, 'Invoke <SHELL> to run recipes')\n            [CompletionResult]::new('--shell-arg', '--shell-arg', [CompletionResultType]::ParameterName, 'Invoke shell with <SHELL-ARG> as an argument')\n            [CompletionResult]::new('--tempdir', '--tempdir', [CompletionResultType]::ParameterName, 'Save temporary files to <TEMPDIR>.')\n            [CompletionResult]::new('--timestamp-format', '--timestamp-format', [CompletionResultType]::ParameterName, 'Timestamp format string')\n            [CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set')\n            [CompletionResult]::new('--working-directory', '--working-directory', [CompletionResultType]::ParameterName, 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set')\n            [CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set')\n            [CompletionResult]::new('--command', '--command', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set')\n            [CompletionResult]::new('--completions', '--completions', [CompletionResultType]::ParameterName, 'Print shell completion script for <SHELL>')\n            [CompletionResult]::new('-l', '-l', [CompletionResultType]::ParameterName, 'List available recipes in <MODULE> or root if omitted')\n            [CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'List available recipes in <MODULE> or root if omitted')\n            [CompletionResult]::new('--request', '--request', [CompletionResultType]::ParameterName, 'Execute <REQUEST>. For internal testing purposes only. May be changed or removed at any time.')\n            [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Show recipe at <PATH>')\n            [CompletionResult]::new('--show', '--show', [CompletionResultType]::ParameterName, 'Show recipe at <PATH>')\n            [CompletionResult]::new('--usage', '--usage', [CompletionResultType]::ParameterName, 'Print recipe usage information')\n            [CompletionResult]::new('--check', '--check', [CompletionResultType]::ParameterName, 'Run `--fmt` in ''check'' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.')\n            [CompletionResult]::new('--clear-shell-args', '--clear-shell-args', [CompletionResultType]::ParameterName, 'Clear shell arguments')\n            [CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'Print what just would do without doing it')\n            [CompletionResult]::new('--dry-run', '--dry-run', [CompletionResultType]::ParameterName, 'Print what just would do without doing it')\n            [CompletionResult]::new('--explain', '--explain', [CompletionResultType]::ParameterName, 'Print recipe doc comment before running it')\n            [CompletionResult]::new('-g', '-g', [CompletionResultType]::ParameterName, 'Use global justfile')\n            [CompletionResult]::new('--global-justfile', '--global-justfile', [CompletionResultType]::ParameterName, 'Use global justfile')\n            [CompletionResult]::new('--highlight', '--highlight', [CompletionResultType]::ParameterName, 'Highlight echoed recipe lines in bold')\n            [CompletionResult]::new('--list-submodules', '--list-submodules', [CompletionResultType]::ParameterName, 'List recipes in submodules')\n            [CompletionResult]::new('--no-aliases', '--no-aliases', [CompletionResultType]::ParameterName, 'Don''t show aliases in list')\n            [CompletionResult]::new('--no-deps', '--no-deps', [CompletionResultType]::ParameterName, 'Don''t run recipe dependencies')\n            [CompletionResult]::new('--no-dotenv', '--no-dotenv', [CompletionResultType]::ParameterName, 'Don''t load `.env` file')\n            [CompletionResult]::new('--no-highlight', '--no-highlight', [CompletionResultType]::ParameterName, 'Don''t highlight echoed recipe lines in bold')\n            [CompletionResult]::new('--one', '--one', [CompletionResultType]::ParameterName, 'Forbid multiple recipes from being invoked on the command line')\n            [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Suppress all output')\n            [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Suppress all output')\n            [CompletionResult]::new('--allow-missing', '--allow-missing', [CompletionResultType]::ParameterName, 'Ignore missing recipe and module errors')\n            [CompletionResult]::new('--shell-command', '--shell-command', [CompletionResultType]::ParameterName, 'Invoke <COMMAND> with the shell used to run recipe lines and backticks')\n            [CompletionResult]::new('--timestamp', '--timestamp', [CompletionResultType]::ParameterName, 'Print recipe command timestamps')\n            [CompletionResult]::new('-u', '-u', [CompletionResultType]::ParameterName, 'Return list and summary entries in source order')\n            [CompletionResult]::new('--unsorted', '--unsorted', [CompletionResultType]::ParameterName, 'Return list and summary entries in source order')\n            [CompletionResult]::new('--unstable', '--unstable', [CompletionResultType]::ParameterName, 'Enable unstable features')\n            [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Use verbose output')\n            [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Use verbose output')\n            [CompletionResult]::new('--yes', '--yes', [CompletionResultType]::ParameterName, 'Automatically confirm all recipes.')\n            [CompletionResult]::new('--changelog', '--changelog', [CompletionResultType]::ParameterName, 'Print changelog')\n            [CompletionResult]::new('--choose', '--choose', [CompletionResultType]::ParameterName, 'Select one or more recipes to run using a binary chooser. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`')\n            [CompletionResult]::new('--dump', '--dump', [CompletionResultType]::ParameterName, 'Print justfile')\n            [CompletionResult]::new('-e', '-e', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`')\n            [CompletionResult]::new('--edit', '--edit', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`')\n            [CompletionResult]::new('--evaluate', '--evaluate', [CompletionResultType]::ParameterName, 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable''s value.')\n            [CompletionResult]::new('--fmt', '--fmt', [CompletionResultType]::ParameterName, 'Format and overwrite justfile')\n            [CompletionResult]::new('--groups', '--groups', [CompletionResultType]::ParameterName, 'List recipe groups')\n            [CompletionResult]::new('--init', '--init', [CompletionResultType]::ParameterName, 'Initialize new justfile in project root')\n            [CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, 'Print justfile as JSON')\n            [CompletionResult]::new('--man', '--man', [CompletionResultType]::ParameterName, 'Print man page')\n            [CompletionResult]::new('--summary', '--summary', [CompletionResultType]::ParameterName, 'List names of available recipes')\n            [CompletionResult]::new('--variables', '--variables', [CompletionResultType]::ParameterName, 'List names of variables')\n            [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help')\n            [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help')\n            [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version')\n            [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version')\n            break\n        }\n    })\n\n    function Get-JustFileRecipes([string[]]$CommandElements) {\n        $justFileIndex = $commandElements.IndexOf(\"--justfile\");\n\n        if ($justFileIndex -ne -1 -and $justFileIndex + 1 -le $commandElements.Length) {\n            $justFileLocation = $commandElements[$justFileIndex + 1]\n        }\n\n        $justArgs = @(\"--summary\")\n\n        if (Test-Path $justFileLocation) {\n            $justArgs += @(\"--justfile\", $justFileLocation)\n        }\n\n        $recipes = $(just @justArgs) -split ' '\n        return $recipes | ForEach-Object { [CompletionResult]::new($_) }\n    }\n\n    $elementValues = $commandElements | Select-Object -ExpandProperty Value\n    $recipes = Get-JustFileRecipes -CommandElements $elementValues\n    $completions += $recipes\n    $completions.Where{ $_.CompletionText -like \"$wordToComplete*\" } |\n        Sort-Object -Property ListItemText\n}\n"
  },
  {
    "path": "completions/just.zsh",
    "content": "#compdef just\n\nautoload -U is-at-least\n\n_just() {\n    typeset -A opt_args\n    typeset -a _arguments_options\n    local ret=1\n\n    if is-at-least 5.2; then\n        _arguments_options=(-s -S -C)\n    else\n        _arguments_options=(-s -C)\n    fi\n\n    local context curcontext=\"$curcontext\" state line\n    local common=(\n'(--no-aliases)--alias-style=[Set list command alias display style]: :(left right separate)' \\\n'--ceiling=[Do not ascend above <CEILING> directory when searching for a justfile.]: :_files' \\\n'--chooser=[Override binary invoked by \\`--choose\\`]: :_default' \\\n'--color=[Print colorful output]: :(always auto never)' \\\n'--command-color=[Echo recipe lines in <COMMAND-COLOR>]: :(black blue cyan green purple red yellow)' \\\n'--cygpath=[Use binary at <CYGPATH> to convert between unix and Windows paths.]: :_files' \\\n'(-E --dotenv-path)--dotenv-filename=[Search for environment file named <DOTENV-FILENAME> instead of \\`.env\\`]: :_default' \\\n'-E+[Load <DOTENV-PATH> as environment file instead of searching for one]: :_files' \\\n'--dotenv-path=[Load <DOTENV-PATH> as environment file instead of searching for one]: :_files' \\\n'--dump-format=[Dump justfile as <FORMAT>]:FORMAT:(json just)' \\\n'-f+[Use <JUSTFILE> as justfile]: :_files' \\\n'--justfile=[Use <JUSTFILE> as justfile]: :_files' \\\n'--list-heading=[Print <TEXT> before list]:TEXT:_default' \\\n'--list-prefix=[Print <TEXT> before each list item]:TEXT:_default' \\\n'*--group=[Only list recipes in <GROUP>]: :_default' \\\n'*--set=[Override <VARIABLE> with <VALUE>]: :(_just_variables)' \\\n'--shell=[Invoke <SHELL> to run recipes]: :_default' \\\n'*--shell-arg=[Invoke shell with <SHELL-ARG> as an argument]: :_default' \\\n'--tempdir=[Save temporary files to <TEMPDIR>.]: :_files' \\\n'--timestamp-format=[Timestamp format string]: :_default' \\\n'-d+[Use <WORKING-DIRECTORY> as working directory. --justfile must also be set]: :_files' \\\n'--working-directory=[Use <WORKING-DIRECTORY> as working directory. --justfile must also be set]: :_files' \\\n'*-c+[Run an arbitrary command with the working directory, \\`.env\\`, overrides, and exports set]: :_default' \\\n'*--command=[Run an arbitrary command with the working directory, \\`.env\\`, overrides, and exports set]: :_default' \\\n'--completions=[Print shell completion script for <SHELL>]:SHELL:(bash elvish fish nushell powershell zsh)' \\\n'()-l+[List available recipes in <MODULE> or root if omitted]' \\\n'()--list=[List available recipes in <MODULE> or root if omitted]' \\\n'--request=[Execute <REQUEST>. For internal testing purposes only. May be changed or removed at any time.]: :_default' \\\n'-s+[Show recipe at <PATH>]: :(_just_commands)' \\\n'--show=[Show recipe at <PATH>]: :(_just_commands)' \\\n'()--usage=[Print recipe usage information]:PATH:_default' \\\n'--check[Run \\`--fmt\\` in '\\''check'\\'' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.]' \\\n'--clear-shell-args[Clear shell arguments]' \\\n'(-q --quiet)-n[Print what just would do without doing it]' \\\n'(-q --quiet)--dry-run[Print what just would do without doing it]' \\\n'--explain[Print recipe doc comment before running it]' \\\n'(-f --justfile -d --working-directory)-g[Use global justfile]' \\\n'(-f --justfile -d --working-directory)--global-justfile[Use global justfile]' \\\n'--highlight[Highlight echoed recipe lines in bold]' \\\n'--list-submodules[List recipes in submodules]' \\\n'--no-aliases[Don'\\''t show aliases in list]' \\\n'--no-deps[Don'\\''t run recipe dependencies]' \\\n'--no-dotenv[Don'\\''t load \\`.env\\` file]' \\\n'--no-highlight[Don'\\''t highlight echoed recipe lines in bold]' \\\n'--one[Forbid multiple recipes from being invoked on the command line]' \\\n'(-n --dry-run)-q[Suppress all output]' \\\n'(-n --dry-run)--quiet[Suppress all output]' \\\n'--allow-missing[Ignore missing recipe and module errors]' \\\n'--shell-command[Invoke <COMMAND> with the shell used to run recipe lines and backticks]' \\\n'--timestamp[Print recipe command timestamps]' \\\n'-u[Return list and summary entries in source order]' \\\n'--unsorted[Return list and summary entries in source order]' \\\n'--unstable[Enable unstable features]' \\\n'*-v[Use verbose output]' \\\n'*--verbose[Use verbose output]' \\\n'--yes[Automatically confirm all recipes.]' \\\n'--changelog[Print changelog]' \\\n'--choose[Select one or more recipes to run using a binary chooser. If \\`--chooser\\` is not passed the chooser defaults to the value of \\$JUST_CHOOSER, falling back to \\`fzf\\`]' \\\n'--dump[Print justfile]' \\\n'-e[Edit justfile with editor given by \\$VISUAL or \\$EDITOR, falling back to \\`vim\\`]' \\\n'--edit[Edit justfile with editor given by \\$VISUAL or \\$EDITOR, falling back to \\`vim\\`]' \\\n'--evaluate[Evaluate and print all variables. If a variable name is given as an argument, only print that variable'\\''s value.]' \\\n'--fmt[Format and overwrite justfile]' \\\n'--groups[List recipe groups]' \\\n'--init[Initialize new justfile in project root]' \\\n'(--dump-format)--json[Print justfile as JSON]' \\\n'--man[Print man page]' \\\n'--summary[List names of available recipes]' \\\n'--variables[List names of variables]' \\\n'-h[Print help]' \\\n'--help[Print help]' \\\n'-V[Print version]' \\\n'--version[Print version]' \\\n)\n\n    _arguments \"${_arguments_options[@]}\" $common \\\n        '1: :_just_commands' \\\n        '*: :->args' \\\n        && ret=0\n\n    case $state in\n        args)\n            curcontext=\"${curcontext%:*}-${words[2]}:\"\n\n            local lastarg=${words[${#words}]}\n            local recipe\n\n            local cmds; cmds=(\n                ${(s: :)$(_call_program commands just --summary)}\n            )\n\n            # Find first recipe name\n            for ((i = 2; i < $#words; i++ )) do\n                if [[ ${cmds[(I)${words[i]}]} -gt 0 ]]; then\n                    recipe=${words[i]}\n                    break\n                fi\n            done\n\n            if [[ $lastarg = */* ]]; then\n                # Arguments contain slash would be recognised as a file\n                _arguments -s -S $common '*:: :_files'\n            elif [[ $lastarg = *=* ]]; then\n                # Arguments contain equal would be recognised as a variable\n                _message \"value\"\n            elif [[ $recipe ]]; then\n                # Show usage message\n                _message \"`just --show $recipe`\"\n                # Or complete with other commands\n                #_arguments -s -S $common '*:: :_just_commands'\n            else\n                _arguments -s -S $common '*:: :_just_commands'\n            fi\n        ;;\n    esac\n\n    return ret\n\n}\n\n(( $+functions[_just_commands] )) ||\n_just_commands() {\n    [[ $PREFIX = -* ]] && return 1\n    integer ret=1\n    local variables; variables=(\n        ${(s: :)$(_call_program commands just --variables)}\n    )\n    local commands; commands=(\n        ${${${(M)\"${(f)$(_call_program commands just --list)}\":#    *}/ ##/}/ ##/:Args: }\n    )\n\n    if compset -P '*='; then\n        case \"${${words[-1]%=*}#*=}\" in\n            *) _message 'value' && ret=0 ;;\n        esac\n    else\n        _describe -t variables 'variables' variables -qS \"=\" && ret=0\n        _describe -t commands 'just commands' commands \"$@\"\n    fi\n\n}\n\nif [ \"$funcstack[1]\" = \"_just\" ]; then\n    (( $+functions[_just_variables] )) ||\n_just_variables() {\n    [[ $PREFIX = -* ]] && return 1\n    integer ret=1\n    local variables; variables=(\n        ${(s: :)$(_call_program commands just --variables)}\n    )\n\n    if compset -P '*='; then\n        case \"${${words[-1]%=*}#*=}\" in\n            *) _message 'value' && ret=0 ;;\n        esac\n    else\n        _describe -t variables 'variables' variables && ret=0\n    fi\n\n    return ret\n}\n\n_just \"$@\"\nelse\n    compdef _just just\nfi\n"
  },
  {
    "path": "contrib/just.sh",
    "content": "#!/usr/bin/env bash\n\n# cd upwards to the justfile\nwhile [[ ! -e justfile ]]; do\n  if [[ $PWD = / ]] || [[ $PWD = $JUSTSTOP ]] || [[ -e juststop ]]; then\n    echo 'No justfile found.'\n    exit 1\n  fi\n  cd ..\ndone\n\n# prefer gmake if it exists\nif command -v gmake > /dev/null; then\n  MAKE=gmake\nelse\n  MAKE=make\nfi\n\ndeclare -a RECIPES\nfor ARG in \"$@\"; do\n  test $ARG =  '--'  && shift && break\n  RECIPES+=($ARG) && shift\ndone\n\n# export arguments after '--' so they can be used in recipes\nI=0\nfor ARG in \"$@\"; do\n    export ARG$I=$ARG\n    I=$((I + 1))\ndone\n\n# go!\nexec $MAKE MAKEFLAGS='' --always-make --no-print-directory -f justfile ${RECIPES[*]}\n"
  },
  {
    "path": "crates/generate-book/Cargo.toml",
    "content": "[package]\nname = \"generate-book\"\nversion = \"0.0.0\"\nedition = \"2018\"\npublish = false\n\n[dependencies]\npulldown-cmark = \"0.9.1\"\npulldown-cmark-to-cmark = \"10.0.1\"\n"
  },
  {
    "path": "crates/generate-book/src/main.rs",
    "content": "use {\n  pulldown_cmark::{CowStr, Event, HeadingLevel, Options, Parser, Tag},\n  pulldown_cmark_to_cmark::cmark,\n  std::{\n    collections::{BTreeMap, BTreeSet},\n    error::Error,\n    fmt::Write,\n    fs,\n    ops::Deref,\n  },\n};\n\ntype Result<T = ()> = std::result::Result<T, Box<dyn Error>>;\n\n#[derive(Copy, Clone, Debug)]\nenum Language {\n  English,\n  Chinese,\n}\n\nimpl Language {\n  fn code(&self) -> &'static str {\n    match self {\n      Self::English => \"en\",\n      Self::Chinese => \"zh\",\n    }\n  }\n\n  fn suffix(&self) -> &'static str {\n    match self {\n      Self::English => \"\",\n      Self::Chinese => \".中文\",\n    }\n  }\n\n  fn introduction(&self) -> &'static str {\n    match self {\n      Self::Chinese => \"说明\",\n      Self::English => \"Introduction\",\n    }\n  }\n}\n\n#[derive(Debug)]\nstruct Chapter<'a> {\n  level: HeadingLevel,\n  events: Vec<Event<'a>>,\n  index: usize,\n  language: Language,\n}\n\nimpl Chapter<'_> {\n  fn title(&self) -> String {\n    if self.index == 0 {\n      return self.language.introduction().into();\n    }\n\n    self\n      .events\n      .iter()\n      .skip_while(|event| !matches!(event, Event::Start(Tag::Heading(..))))\n      .skip(1)\n      .take_while(|event| !matches!(event, Event::End(Tag::Heading(..))))\n      .filter_map(|event| match event {\n        Event::Code(content) | Event::Text(content) => Some(content.deref()),\n        _ => None,\n      })\n      .collect()\n  }\n\n  fn filename(&self) -> String {\n    slug(&self.title())\n  }\n\n  fn markdown(&self) -> Result<String> {\n    let mut markdown = String::new();\n    cmark(self.events.iter(), &mut markdown)?;\n    if self.index == 0 {\n      markdown = markdown.split_inclusive('\\n').skip(1).collect::<String>();\n    }\n    Ok(markdown)\n  }\n}\n\nfn slug(s: &str) -> String {\n  let mut slug = String::new();\n  for c in s.chars() {\n    match c {\n      'A'..='Z' => slug.extend(c.to_lowercase()),\n      ' ' => slug.push('-'),\n      '?' | '.' | '？' | '’' => {}\n      _ => slug.push(c),\n    }\n  }\n  slug\n}\n\nfn main() -> Result {\n  for language in [Language::English, Language::Chinese] {\n    let src = format!(\"book/{}/src\", language.code());\n    fs::remove_dir_all(&src).ok();\n    fs::create_dir(&src)?;\n\n    let txt = fs::read_to_string(format!(\"README{}.md\", language.suffix()))?;\n\n    let mut chapters = vec![Chapter {\n      level: HeadingLevel::H1,\n      events: Vec::new(),\n      index: 0,\n      language,\n    }];\n\n    for event in Parser::new_ext(&txt, Options::all()) {\n      if let Event::Start(Tag::Heading(level @ (HeadingLevel::H2 | HeadingLevel::H3), ..)) = event {\n        let index = chapters.last().unwrap().index + 1;\n        chapters.push(Chapter {\n          level,\n          events: Vec::new(),\n          index,\n          language,\n        });\n      }\n      chapters.last_mut().unwrap().events.push(event);\n    }\n\n    let mut links = BTreeMap::new();\n\n    for chapter in &chapters {\n      let mut current = None;\n      for event in &chapter.events {\n        match event {\n          Event::Start(Tag::Heading(..)) => current = Some(Vec::new()),\n          Event::End(Tag::Heading(level, ..)) => {\n            let events = current.unwrap();\n            let title = events\n              .iter()\n              .filter_map(|event| match event {\n                Event::Code(content) | Event::Text(content) => Some(content.deref()),\n                _ => None,\n              })\n              .collect::<String>();\n            let slug = slug(&title);\n            let link = if let HeadingLevel::H1 | HeadingLevel::H2 | HeadingLevel::H3 = level {\n              format!(\"{}.html\", chapter.filename())\n            } else {\n              format!(\"{}.html#{}\", chapter.filename(), slug)\n            };\n            links.insert(slug, link);\n            current = None;\n          }\n          _ => {\n            if let Some(events) = &mut current {\n              events.push(event.clone());\n            }\n          }\n        }\n      }\n    }\n\n    for chapter in &mut chapters {\n      for event in &mut chapter.events {\n        if let Event::Start(Tag::Link(_, dest, _)) | Event::End(Tag::Link(_, dest, _)) = event {\n          if let Some(anchor) = dest.clone().strip_prefix('#') {\n            if anchor != \"just\" {\n              *dest = CowStr::Borrowed(&links[anchor]);\n            }\n          }\n        }\n      }\n    }\n\n    let mut summary = String::new();\n\n    let mut filenames = BTreeSet::new();\n\n    for chapter in chapters {\n      let filename = chapter.filename();\n      assert!(!filenames.contains(&filename));\n\n      let path = format!(\"{src}/{filename}.md\");\n      fs::write(path, chapter.markdown()?)?;\n      let indent = match chapter.level {\n        HeadingLevel::H1 => 0,\n        HeadingLevel::H2 => 1,\n        HeadingLevel::H3 => 2,\n        HeadingLevel::H4 => 3,\n        HeadingLevel::H5 => 4,\n        HeadingLevel::H6 => 5,\n      };\n\n      writeln!(\n        summary,\n        \"{}- [{}]({filename}.md)\",\n        \" \".repeat(indent * 4),\n        chapter.title(),\n      )?;\n\n      filenames.insert(filename);\n    }\n\n    fs::write(format!(\"{src}/SUMMARY.md\"), summary).unwrap();\n  }\n\n  Ok(())\n}\n"
  },
  {
    "path": "crates/update-contributors/Cargo.toml",
    "content": "[package]\nname = \"update-contributors\"\nversion = \"0.0.0\"\nedition = \"2021\"\npublish = false\n\n[dependencies]\nregex = \"1.5.4\"\n"
  },
  {
    "path": "crates/update-contributors/src/main.rs",
    "content": "use {\n  regex::{Captures, Regex},\n  std::{fs, process::Command, str},\n};\n\nfn author(pr: u64) -> String {\n  eprintln!(\"#{pr}\");\n  let output = Command::new(\"sh\")\n    .args([\n      \"-c\",\n      &format!(\"gh pr view {pr} --json author | jq -r .author.login\"),\n    ])\n    .output()\n    .unwrap();\n\n  assert!(\n    output.status.success(),\n    \"{}\",\n    String::from_utf8_lossy(&output.stderr)\n  );\n\n  str::from_utf8(&output.stdout).unwrap().trim().to_owned()\n}\n\nfn main() {\n  fs::write(\n    \"CHANGELOG.md\",\n    &*Regex::new(r\"\\(#(\\d+)( by @[a-z]+)?\\)\")\n      .unwrap()\n      .replace_all(\n        &fs::read_to_string(\"CHANGELOG.md\").unwrap(),\n        |captures: &Captures| {\n          let pr = captures[1].parse().unwrap();\n          let contributor = author(pr);\n          format!(\"([#{pr}](https://github.com/casey/just/pull/{pr}) by [{contributor}](https://github.com/{contributor}))\")\n        },\n      ),\n  )\n  .unwrap();\n}\n"
  },
  {
    "path": "crates-io-readme.md",
    "content": "`just` is a handy way to save and run project-specific commands.\n\nCommands are stored in a file called `justfile` or `Justfile` with syntax\ninspired by `make`:\n\n```make\nbuild:\n    cc *.c -o main\n\n# test everything\ntest-all: build\n    ./test --all\n\n# run a specific test\ntest TEST: build\n    ./test --test {{TEST}}\n```\n\n`just` produces detailed error messages and avoids `make`'s idiosyncrasies, so\ndebugging a justfile is easier and less surprising than debugging a makefile.\n\nIt works on all operating systems supported by Rust.\n\nRead more on [GitHub](https://github.com/casey/just).\n"
  },
  {
    "path": "examples/cross-platform.just",
    "content": "# use with https://github.com/casey/just\n#\n# Example cross-platform Python project\n#\n\npython_dir := if os_family() == \"windows\" { \"./.venv/Scripts\" } else { \"./.venv/bin\" }\npython := python_dir + if os_family() == \"windows\" { \"/python.exe\" } else { \"/python3\" }\nsystem_python := if os_family() == \"windows\" { \"py.exe -3.9\" } else { \"python3.9\" }\n\n# Set up development environment\nbootstrap:\n    if test ! -e .venv; then {{ system_python }} -m venv .venv; fi\n    {{ python }} -m pip install --upgrade pip wheel pip-tools\n    {{ python_dir }}/pip-sync\n\n# Upgrade Python dependencies\nupgrade-deps: && bootstrap\n    {{ python_dir }}/pip-compile --upgrade\n\n# Sample project script 1\nscript1:\n    {{ python }} script1.py\n\n# Sample project script 2\nscript2 *ARGS:\n    {{ python }} script2.py {{ ARGS }}\n"
  },
  {
    "path": "examples/keybase.just",
    "content": "# use with https://github.com/casey/just\n\n# Be inspired to use just to notify a chat\n# channel, this examples shows use with keybase\n# since it - practically - authenticates at the\n# device level and needs no additional secrets\n\n# notify update in keybase\nnotify m=\"\":\n\tkeybase chat send --topic-type \"chat\" --channel <channel> <team> \"upd(<repo>): {{m}}\"\n"
  },
  {
    "path": "examples/kitchen-sink.just",
    "content": "set shell := [\"sh\", \"-c\"]\nset windows-shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"]\nset allow-duplicate-recipes\nset positional-arguments\nset dotenv-load\nset export\n\nalias s := serve\n\nbt := '0'\n\nexport RUST_BACKTRACE_1 := bt\n\nlog := \"warn\"\n\nexport JUST_LOG := (log + \"ing\" + `grep loop /etc/networks | cut -f2`)\n\ntmpdir  := `mktemp`\nversion := \"0.2.7\"\ntardir  := tmpdir / \"awesomesauce-\" + version\nfoo1    := / \"tmp\"\nfoo2_3  := \"a/\"\ntarball := tardir + \".tar.gz\"\n\nexport RUST_BACKTRACE_2 := \"1\"\nstring-with-tab             := \"\\t\"\nstring-with-newline         := \"\\n\"\nstring-with-carriage-return := \"\\r\"\nstring-with-double-quote    := \"\\\"\"\nstring-with-slash           := \"\\\\\"\nstring-with-no-newline      := \"\\\n\"\n\n# Newlines in variables\nsingle := '\nhello\n'\n\ndouble := \"\ngoodbye\n\"\nescapes := '\\t\\n\\r\\\"\\\\'\n\n# this string will evaluate to `foo\\nbar\\n`\nx := '''\n  foo\n  bar\n'''\n\n# this string will evaluate to `abc\\n  wuv\\nxyz\\n`\ny := \"\"\"\n  abc\n    wuv\n  xyz\n\"\"\"\n\nfor:\n  for file in `ls .`; do \\\n    echo $file; \\\n  done\n\nserve:\n  touch {{tmpdir}}/file\n\n# This backtick evaluates the command `echo foo\\necho bar\\n`, which produces the value `foo\\nbar\\n`.\nstuff := ```\n    echo foo\n    echo bar\n  ```\n\n\nan_arch := trim(lowercase(justfile())) + arch()\ntrim_end := trim_end(\"99.99954%   \")\nhome_dir := replace(env_var('HOME') / \"yep\", 'yep', '')\nquoted := quote(\"some things beyond\\\"$()^%#@!|-+=_*&'`\")\nsmartphone := trim_end_match('blah.txt', 'txt')\nmuseum := trim_start_match(trim_start(trim_end_matches('   yep_blah.txt.txt', '.txt')), 'yep_')\nwater := trim_start_matches('ssssssoup.txt', 's')\ncongress := uppercase(os())\nfam := os_family()\npath_1 := absolute_path('test')\npath_2 := '/tmp/subcommittee.txt'\next_z := extension(path_2)\nexe_name := file_name(just_executable())\na_stem := file_stem(path_2)\na_parent := parent_directory(path_2)\nsans_ext := without_extension(path_2)\ncamera := join('tmp', 'dir1', 'dir2', path_2)\ncleaned := clean('/tmp/blah/..///thing.txt')\nid__path := '/tmp' / sha256('blah') / sha256_file(justfile())\n_another_var := env_var_or_default(\"HOME\", justfile_directory())\npython := `which python`\n\nexists := if path_exists(just_executable()) =~ '^/User' { uuid() } else { 'yeah' }\n\nfoo   := if env_var(\"_\") == \"/usr/bin/env\" { `touch /tmp/a_file` } else { \"dummy-value\" }\nfoo_b := if \"hello\" == \"goodbye\" { \"xyz\" } else { if \"no\" == \"no\" { \"yep\"} else { error(\"123\") } }\nfoo_c := if \"hello\" == \"goodbye\" {\n  \"xyz\"\n} else if \"a\" == \"a\" {\n  \"abc\"\n} else {\n  \"123\"\n}\n\nbar:\n  @echo {{foo}}\n\n\nbar2 foo_stuff:\n  echo {{ if foo_stuff == \"bar\" { \"hello\" } else { \"goodbye\" } }}\n\nexecutable:\n  @echo The executable is at: {{just_executable()}}\n\n\nrustfmt:\n  find {{invocation_directory()}} -name \\*.rs -exec rustfmt {} \\;\n\ntest:\n  echo \"{{home_dir}}\"\n\n\nlinewise:\n  Write-Host \"Hello, world!\"\n\nserve2:\n  @echo \"Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…\"\n\n\nshebang := if os() == 'windows' {\n  'powershell.exe'\n} else {\n  '/usr/bin/env pwsh'\n}\n\nshebang:\n\t#!{{shebang}}\n\t$PSV = $PSVersionTable.PSVersion | % {\"$_\" -split \"\\.\" }\n\t$psver = $PSV[0] + \".\" + $PSV[1]\n\tif ($PSV[2].Length -lt 4) {\n\t\t$psver += \".\" + $PSV[2] + \" Core\"\n\t} else {\n\t\t$psver += \" Desktop\"\n\t}\n\techo \"PowerShell $psver\"\n\n@foo:\n  echo bar\n\n@test5 *args='':\n  bash -c 'while (( \"$#\" )); do echo - $1; shift; done' -- \"$@\"\n\ntest2 $RUST_BACKTRACE=\"1\":\n  # will print a stack trace if it crashes\n  cargo test\n\n\nnotify m=\"\":\n\tkeybase chat send --topic-type \"chat\" --channel <channel> <team> \"upd(<repo>): {{m}}\"\n\n# Sample project script 2\nscript2 *ARGS:\n    {{ python }} script2.py {{ ARGS }}\n\nbraces:\n  echo 'I {{{{LOVE}} curly braces!'\n\n_braces2:\n  echo '{{'I {{LOVE}} curly braces!'}}'\n\n_braces3:\n  echo 'I {{ \"{{\" }}LOVE}} curly braces!'\n\nfoo2:\n  -@cat foo\n  echo 'Done!'\n\ntest3 target tests=path_1:\n  @echo 'Testing {{target}}:{{tests}}…'\n  ./test --tests {{tests}} {{target}}\n\ntest4 triple=(an_arch + \"-unknown-unknown\") input=(an_arch / \"input.dat\"):\n  ./test {{triple}}\n\nvariadic $VAR1_1 VAR2 VAR3 VAR4=(\"a\") +$FLAGS='-q': foo2 braces\n  cargo test {{FLAGS}}\n\ntime:\n  @-date +\"%H:%S\"\n  -cat /tmp/nonexistent_file.txt\n  @echo \"finished\"\n\njustwords:\n  grep just \\\n    --text /usr/share/dict/words \\\n    > /tmp/justwords\n\n# Subsequent dependencies\n# https://just.systems/man/en/chapter_37.html\n# To test, run `$ just -f test-suite.just b`\na:\n  echo 'A!'\n\nb: a && d\n  echo 'B start!'\n  just -f {{justfile()}} c\n  echo 'B end!'\n\nc:\n  echo 'C!'\n\nd:\n  echo 'D!'\n"
  },
  {
    "path": "examples/powershell.just",
    "content": "# Cross platform shebang:\nshebang := if os() == 'windows' {\n  'powershell.exe'\n} else {\n  '/usr/bin/env pwsh'\n}\n\n# Set shell for non-Windows OSs:\nset shell := [\"powershell\", \"-c\"]\n\n# Set shell for Windows OSs:\nset windows-shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"]\n\n# If you have PowerShell Core installed and want to use it,\n# use `pwsh.exe` instead of `powershell.exe`\n\nlinewise:\n  Write-Host \"Hello, world!\"\n\nshebang:\n\t#!{{shebang}}\n\t$PSV = $PSVersionTable.PSVersion | % {\"$_\" -split \"\\.\" }\n\t$psver = $PSV[0] + \".\" + $PSV[1]\n\tif ($PSV[2].Length -lt 4) {\n\t\t$psver += \".\" + $PSV[2] + \" Core\"\n\t} else {\n\t\t$psver += \" Desktop\"\n\t}\n\techo \"PowerShell $psver\"\n"
  },
  {
    "path": "examples/pre-commit.just",
    "content": "# use with https://github.com/casey/just\n\n# Example combining just + pre-commit\n# pre-commit: https://pre-commit.com/\n# > A framework for managing and maintaining\n# > multi-language pre-commit hooks.\n\n# pre-commit brings about encapsulation of your\n# most common repo scripting tasks. It is perfectly\n# usable without actually setting up precommit hooks.\n# If you chose to, this justfiles includes shorthands\n# for git commit and amend to keep pre-commit out of\n# the way when in flow on a feature branch.\n\n# uses: https://github.com/tekwizely/pre-commit-golang\n# uses: https://github.com/prettier/prettier (pre-commit hook)\n# configures: https://www.git-town.com/ (setup receipt)\n\n# fix auto-fixable lint issues in staged files\nfix:\n\tpre-commit run go-returns  # fixes all Go lint issues\n\tpre-commit run prettier    # fixes all Markdown (& other) lint issues\n\n# lint most common issues in - or due - to staged files\nlint:\n\tpre-commit run go-vet-mod || true  # runs go vet\n\tpre-commit run go-lint    || true  # runs golint\n\tpre-commit run go-critic  || true  # runs gocritic\n\n# lint all issues in - or due - to staged files:\nlint-all:\n\tpre-commit run golangci-lint-mod || true  # runs golangci-lint\n\n# run tests in - or due - to staged files\ntest:\n\tpre-commit run go-test-mod || true  # runs go test\n\n# commit skipping pre-commit hooks\ncommit m:\n\tgit commit --no-verify -m \"{{m}}\"\n\n# amend skipping pre-commit hooks\namend:\n\tgit commit --amend --no-verify\n\n# install/update code automation (prettier, pre-commit, goreturns, lintpack, gocritic, golangci-lint)\ninstall:\n\tnpm i -g prettier\n\tcurl https://pre-commit.com/install-local.py | python3 -\n\tgo get github.com/sqs/goreturns\n\tgo get github.com/go-lintpack/lintpack/...\n\tgo get github.com/go-critic/go-critic/...\n\tcurl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.27.0\n\n# setup/update pre-commit hooks (optional)\nsetup:\n\tpre-commit install --install-hooks # uninstall: `pre-commit uninstall`\n\tgit config git-town.code-hosting-driver gitea  # setup git-town with gitea\n\tgit config git-town.code-hosting-origin-hostname gitea.example.org  # setup git-town origin hostname\n"
  },
  {
    "path": "examples/screenshot.just",
    "content": "alias b := build\n\nhost := `uname -a`\n\n# build main\nbuild:\n    cc *.c -o main\n\n# test everything\ntest-all: build\n    ./test --all\n\n# run a specific test\ntest TEST: build\n    ./test --test {{TEST}}\n"
  },
  {
    "path": "fuzz/Cargo.toml",
    "content": "[package]\nname = \"just-fuzz\"\nversion = \"0.0.0\"\nauthors = [\"Automatically generated\"]\npublish = false\nedition = \"2018\"\n\n[package.metadata]\ncargo-fuzz = true\n\n[dependencies]\nlibfuzzer-sys = \"0.4\"\n\n[dependencies.just]\npath = \"..\"\n\n# Prevent this from interfering with workspaces\n[workspace]\nmembers = [\".\"]\n\n[[bin]]\nname = \"compile\"\npath = \"fuzz_targets/compile.rs\"\ntest = false\ndoc = false\n\n[profile.release]\ndebug = true\n"
  },
  {
    "path": "fuzz/fuzz_targets/compile.rs",
    "content": "#![no_main]\nuse libfuzzer_sys::fuzz_target;\n\nfuzz_target!(|src: &str| {\n  let _ = just::fuzzing::compile(src);\n});\n"
  },
  {
    "path": "justfile",
    "content": "#!/usr/bin/env -S just --justfile\n# ^ A shebang isn't required, but allows a justfile to be executed\n#   like a script, with `./justfile test`, for example.\n\nalias t := test\n\nlog := \"warn\"\n\nexport JUST_LOG := log\n\n[group: 'dev']\nwatch +args='test':\n  cargo watch --clear --exec '{{ args }}'\n\n[group: 'test']\ntest:\n  cargo test --all\n\n[group: 'check']\nci: test clippy build-book forbid\n  cargo fmt --all -- --check\n  cargo update --locked --package just\n\n[group: 'check']\nfuzz:\n  cargo +nightly fuzz run fuzz-compiler\n\n[group: 'misc']\nrun:\n  cargo run\n\n# only run tests matching `PATTERN`\n[group: 'test']\nfilter PATTERN:\n  cargo test {{PATTERN}}\n\n[group: 'misc']\nbuild:\n  cargo build\n\n[group: 'misc']\nfmt:\n  cargo fmt --all\n\n[group: 'check']\nshellcheck:\n  shellcheck www/install.sh\n\n[group: 'doc']\nman:\n  mkdir -p man\n  cargo run -- --man > man/just.1\n\n[group: 'doc']\nview-man: man\n  man man/just.1\n\n# add git log messages to changelog\n[group: 'release']\nupdate-changelog:\n  echo >> CHANGELOG.md\n  git log --pretty='format:- %s' >> CHANGELOG.md\n\n[group: 'release']\nupdate-contributors:\n  cargo run --release --package update-contributors\n\n[group: 'check']\noutdated:\n  cargo outdated -R\n\n[group: 'check']\nunused:\n  cargo +nightly udeps --workspace\n\n# publish current GitHub master branch\n[group: 'release']\npublish:\n  #!/usr/bin/env bash\n  set -euxo pipefail\n  rm -rf tmp/release\n  git clone --depth 1 git@github.com:casey/just.git tmp/release\n  cd tmp/release\n  ! grep '<sup>master</sup>' README.md\n  VERSION=`sed -En 's/version[[:space:]]*=[[:space:]]*\"([^\"]+)\"/\\1/p' Cargo.toml | head -1`\n  git tag -a $VERSION -m \"Release $VERSION\"\n  git push origin $VERSION\n  cargo publish\n  cd ../..\n  rm -rf tmp/release\n\n[group: 'release']\nreadme-version-notes:\n  grep '<sup>master</sup>' README.md\n\n# clean up feature branch BRANCH\n[group: 'dev']\ndone BRANCH=`git rev-parse --abbrev-ref HEAD`:\n  git checkout master\n  git diff --no-ext-diff --quiet --exit-code\n  git pull --rebase github master\n  git diff --no-ext-diff --quiet --exit-code {{BRANCH}}\n  git branch -D {{BRANCH}}\n\n# install just from crates.io\n[group: 'misc']\ninstall:\n  cargo install -f just\n\n# install development dependencies\n[group: 'dev']\ninstall-dev-deps:\n  rustup install nightly\n  rustup update nightly\n  cargo +nightly install cargo-fuzz\n  cargo install cargo-check\n  cargo install cargo-watch\n  cargo install mdbook mdbook-linkcheck\n\n# everyone's favorite animate paper clip\n[group: 'check']\nclippy:\n  cargo clippy --all --all-targets --all-features -- --deny warnings\n\n[group: 'check']\nforbid:\n  ./bin/forbid\n\n[group: 'dev']\nreplace FROM TO:\n  sd '{{FROM}}' '{{TO}}' src/*.rs\n\n[group: 'demo']\ntest-quine:\n  cargo run -- quine\n\n# make a quine, compile it, and verify it\n[group: 'demo']\nquine:\n  mkdir -p tmp\n  @echo '{{quine-text}}' > tmp/gen0.c\n  cc tmp/gen0.c -o tmp/gen0\n  ./tmp/gen0 > tmp/gen1.c\n  cc tmp/gen1.c -o tmp/gen1\n  ./tmp/gen1 > tmp/gen2.c\n  diff tmp/gen1.c tmp/gen2.c\n  rm -r tmp\n  @echo 'It was a quine!'\n\nquine-text := '\n  int printf(const char*, ...);\n\n  int main() {\n    char *s =\n      \"int printf(const char*, ...);\"\n      \"int main() {\"\n      \"   char *s = %c%s%c;\"\n      \"  printf(s, 34, s, 34);\"\n      \"  return 0;\"\n      \"}\";\n    printf(s, 34, s, 34);\n    return 0;\n  }\n'\n\n[group: 'test']\ntest-completions:\n  ./tests/completions/just.bash\n\n[group: 'check']\nbuild-book:\n  cargo run --package generate-book\n  mdbook build book/en\n  mdbook build book/zh\n\n[group: 'dev']\nprint-readme-constants-table:\n  cargo test constants::tests::readme_table -- --nocapture\n\n# run all polyglot recipes\n[group: 'demo']\npolyglot: _python _js _perl _sh _ruby\n\n_python:\n  #!/usr/bin/env python3\n  print('Hello from python!')\n\n_js:\n  #!/usr/bin/env node\n  console.log('Greetings from JavaScript!')\n\n_perl:\n  #!/usr/bin/env perl\n  print \"Larry Wall says Hi!\\n\";\n\n_sh:\n  #!/usr/bin/env sh\n  hello='Yo'\n  echo \"$hello from a shell script!\"\n\n_nu:\n  #!/usr/bin/env nu\n  let hellos = [\"Greetings\", \"Yo\", \"Howdy\"]\n  $hellos | each {|el| print $\"($el) from a nushell script!\" }\n\n_ruby:\n  #!/usr/bin/env ruby\n  puts \"Hello from ruby!\"\n\n# Print working directory, for demonstration purposes!\n[group: 'demo']\npwd:\n  echo {{invocation_directory()}}\n\n[group: 'test']\ntest-bash-completions:\n  rm -rf tmp\n  mkdir -p tmp/bin\n  cargo build\n  cp target/debug/just tmp/bin\n  ./tmp/bin/just --completions bash > tmp/just.bash\n  echo 'mod foo' > tmp/justfile\n  echo 'bar:' > tmp/foo.just\n  cd tmp && PATH=\"`realpath bin`:$PATH\" bash --init-file just.bash\n\n[group: 'test']\ntest-release-workflow:\n  -git tag -d test-release\n  -git push origin :test-release\n  git tag test-release\n  git push origin test-release\n\n# Local Variables:\n# mode: makefile\n# End:\n# vim: set ft=make :\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "edition = \"2024\"\nmax_width = 100\nnewline_style = \"Unix\"\ntab_spaces = 2\nuse_field_init_shorthand = true\nuse_try_shorthand = true\n"
  },
  {
    "path": "src/alias.rs",
    "content": "use super::*;\n\n/// An alias, e.g. `alias name := target`\n#[derive(Debug, PartialEq, Clone, Serialize)]\npub(crate) struct Alias<'src, T = Arc<Recipe<'src>>> {\n  pub(crate) attributes: AttributeSet<'src>,\n  pub(crate) name: Name<'src>,\n  #[serde(\n    bound(serialize = \"T: Keyed<'src>\"),\n    serialize_with = \"keyed::serialize\"\n  )]\n  pub(crate) target: T,\n}\n\nimpl<'src> Alias<'src, Namepath<'src>> {\n  pub(crate) fn resolve(self, target: Arc<Recipe<'src>>) -> Alias<'src> {\n    assert!(self.target.last().lexeme() == target.name());\n\n    Alias {\n      attributes: self.attributes,\n      name: self.name,\n      target,\n    }\n  }\n}\n\nimpl Alias<'_> {\n  pub(crate) fn is_public(&self) -> bool {\n    !self.name.lexeme().starts_with('_')\n      && !self.attributes.contains(AttributeDiscriminant::Private)\n  }\n}\n\nimpl<'src, T> Keyed<'src> for Alias<'src, T> {\n  fn key(&self) -> &'src str {\n    self.name.lexeme()\n  }\n}\n\nimpl<'src> Display for Alias<'src, Namepath<'src>> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    write!(f, \"alias {} := {}\", self.name.lexeme(), self.target)\n  }\n}\n\nimpl Display for Alias<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    write!(\n      f,\n      \"alias {} := {}\",\n      self.name.lexeme(),\n      self.target.name.lexeme()\n    )\n  }\n}\n"
  },
  {
    "path": "src/alias_style.rs",
    "content": "use super::*;\n\n#[derive(Debug, Default, PartialEq, Clone, ValueEnum)]\npub(crate) enum AliasStyle {\n  Left,\n  #[default]\n  Right,\n  Separate,\n}\n"
  },
  {
    "path": "src/analyzer.rs",
    "content": "use {super::*, CompileErrorKind::*};\n\n#[derive(Default)]\npub(crate) struct Analyzer<'run, 'src> {\n  aliases: Table<'src, Alias<'src, Namepath<'src>>>,\n  assignments: Vec<&'run Binding<'src, Expression<'src>>>,\n  modules: Table<'src, Justfile<'src>>,\n  recipes: Vec<&'run Recipe<'src, UnresolvedDependency<'src>>>,\n  sets: Table<'src, Set<'src>>,\n  unexports: HashSet<String>,\n  warnings: Vec<Warning>,\n}\n\nimpl<'run, 'src> Analyzer<'run, 'src> {\n  pub(crate) fn analyze(\n    asts: &'run HashMap<PathBuf, Ast<'src>>,\n    config: &Config,\n    doc: Option<String>,\n    groups: &[StringLiteral<'src>],\n    loaded: &[PathBuf],\n    name: Option<Name<'src>>,\n    overrides: &mut HashMap<Number, String>,\n    paths: &HashMap<PathBuf, PathBuf>,\n    private: bool,\n    root: &Path,\n  ) -> RunResult<'src, Justfile<'src>> {\n    Self::default().justfile(\n      asts, config, doc, groups, loaded, name, overrides, paths, private, root,\n    )\n  }\n\n  fn justfile(\n    mut self,\n    asts: &'run HashMap<PathBuf, Ast<'src>>,\n    config: &Config,\n    doc: Option<String>,\n    groups: &[StringLiteral<'src>],\n    loaded: &[PathBuf],\n    name: Option<Name<'src>>,\n    overrides: &mut HashMap<Number, String>,\n    paths: &HashMap<PathBuf, PathBuf>,\n    private: bool,\n    root: &Path,\n  ) -> RunResult<'src, Justfile<'src>> {\n    let mut definitions = HashMap::new();\n    let mut imports = HashSet::new();\n    let mut unstable_features = BTreeSet::new();\n\n    let mut stack = Vec::new();\n    let ast = asts.get(root).unwrap();\n    stack.push(ast);\n\n    while let Some(ast) = stack.pop() {\n      unstable_features.extend(&ast.unstable_features);\n\n      for item in &ast.items {\n        match item {\n          Item::Alias(alias) => {\n            Self::define(&mut definitions, alias.name, \"alias\", false)?;\n            self.aliases.insert(alias.clone());\n          }\n          Item::Assignment(assignment) => {\n            self.assignments.push(assignment);\n          }\n          Item::Comment(_) => (),\n          Item::Import { absolute, .. } => {\n            if let Some(absolute) = absolute {\n              if imports.insert(absolute) {\n                stack.push(asts.get(absolute).unwrap());\n              }\n            }\n          }\n          Item::Module {\n            absolute,\n            doc,\n            groups,\n            name,\n            private,\n            ..\n          } => {\n            if let Some(absolute) = absolute {\n              Self::define(&mut definitions, *name, \"module\", false)?;\n              self.modules.insert(Self::analyze(\n                asts,\n                config,\n                doc.clone(),\n                groups.as_slice(),\n                loaded,\n                Some(*name),\n                overrides,\n                paths,\n                *private,\n                absolute,\n              )?);\n            }\n          }\n          Item::Recipe(recipe) => {\n            if recipe.enabled() {\n              Self::analyze_recipe(recipe)?;\n              self.recipes.push(recipe);\n            }\n          }\n          Item::Set(set) => {\n            self.analyze_set(set)?;\n            self.sets.insert(set.clone());\n          }\n          Item::Unexport { name } => {\n            if !self.unexports.insert(name.lexeme().to_string()) {\n              return Err(\n                name\n                  .error(DuplicateUnexport {\n                    variable: name.lexeme(),\n                  })\n                  .into(),\n              );\n            }\n          }\n        }\n      }\n\n      self.warnings.extend(ast.warnings.iter().cloned());\n    }\n\n    let mut allow_duplicate_variables = false;\n\n    for (_name, set) in &self.sets {\n      if let Setting::AllowDuplicateVariables(value) = set.value {\n        allow_duplicate_variables = value;\n      }\n    }\n\n    let mut assignments: Table<'src, Assignment<'src>> = Table::default();\n    for assignment in self.assignments {\n      let variable = assignment.name.lexeme();\n\n      if !allow_duplicate_variables && assignments.contains_key(variable) {\n        return Err(assignment.name.error(DuplicateVariable { variable }).into());\n      }\n\n      if assignments\n        .get(variable)\n        .is_none_or(|original| assignment.file_depth <= original.file_depth)\n      {\n        assignments.insert(assignment.clone());\n      }\n\n      if self.unexports.contains(variable) {\n        return Err(assignment.name.error(ExportUnexported { variable }).into());\n      }\n    }\n\n    AssignmentResolver::resolve_assignments(&assignments)?;\n\n    for set in self.sets.values() {\n      for expression in set.value.expressions() {\n        for variable in expression.variables() {\n          let name = variable.lexeme();\n          if !assignments.contains_key(name) && !constants().contains_key(name) {\n            return Err(variable.error(UndefinedVariable { variable: name }).into());\n          }\n        }\n      }\n    }\n\n    let mut unknown_overrides = Vec::new();\n\n    for ((path, name), value) in &config.overrides {\n      if *path == ast.modulepath {\n        if let Some(assignment) = assignments.get(name) {\n          overrides.insert(assignment.number, value.clone());\n        } else {\n          unknown_overrides.push(if path.is_empty() {\n            name.into()\n          } else {\n            format!(\"{path}::{name}\")\n          });\n        }\n      } else if path.starts_with(&ast.modulepath)\n        && !self\n          .modules\n          .contains_key(&path.path[ast.modulepath.path.len()])\n      {\n        unknown_overrides.push(format!(\"{path}::{name}\"));\n      }\n    }\n\n    if !unknown_overrides.is_empty() {\n      return Err(Error::UnknownOverrides {\n        overrides: unknown_overrides,\n      });\n    }\n\n    let settings =\n      Evaluator::evaluate_settings(&assignments, overrides, &Scope::root(), self.sets)?;\n\n    let mut deduplicated_recipes = Table::<'src, UnresolvedRecipe<'src>>::default();\n    for recipe in self.recipes {\n      Self::define(\n        &mut definitions,\n        recipe.name,\n        \"recipe\",\n        settings.allow_duplicate_recipes,\n      )?;\n\n      if deduplicated_recipes\n        .get(recipe.name.lexeme())\n        .is_none_or(|original| recipe.file_depth <= original.file_depth)\n      {\n        deduplicated_recipes.insert(recipe.clone());\n      }\n\n      if !recipe.is_script() {\n        for line in &recipe.body {\n          let sigils = line.sigils(&settings);\n\n          if sigils.contains(&Sigil::Guard) && sigils.contains(&Sigil::Infallible) {\n            let Fragment::Text { token } = line.fragments.first().unwrap() else {\n              unreachable!();\n            };\n            return Err(\n              token\n                .error(CompileErrorKind::GuardAndInfallibleSigil)\n                .into(),\n            );\n          }\n        }\n      }\n    }\n\n    let recipes = RecipeResolver::resolve_recipes(\n      &assignments,\n      &ast.modulepath,\n      &self.modules,\n      &settings,\n      deduplicated_recipes,\n    )?;\n\n    let mut aliases = Table::new();\n    while let Some(alias) = self.aliases.pop() {\n      aliases.insert(Self::resolve_alias(&self.modules, &recipes, alias)?);\n    }\n\n    let source = root.to_owned();\n    let root = paths.get(root).unwrap();\n\n    let mut default = None;\n    for recipe in recipes.values() {\n      if recipe.attributes.contains(AttributeDiscriminant::Default) {\n        if default.is_some() {\n          return Err(\n            recipe\n              .name\n              .error(CompileErrorKind::DuplicateDefault {\n                recipe: recipe.name.lexeme(),\n              })\n              .into(),\n          );\n        }\n\n        default = Some(Arc::clone(recipe));\n      }\n    }\n\n    let default = default.or_else(|| {\n      recipes\n        .values()\n        .filter(|recipe| recipe.name.path == root)\n        .fold(None, |accumulator, next| match accumulator {\n          None => Some(Arc::clone(next)),\n          Some(previous) => Some(if previous.line_number() < next.line_number() {\n            previous\n          } else {\n            Arc::clone(next)\n          }),\n        })\n    });\n\n    Ok(Justfile {\n      aliases,\n      assignments,\n      default,\n      doc: doc.filter(|doc| !doc.is_empty()),\n      groups: groups.into(),\n      loaded: loaded.into(),\n      modulepath: ast.modulepath.clone(),\n      modules: self.modules,\n      name,\n      private,\n      recipes,\n      settings,\n      source,\n      unexports: self.unexports,\n      unstable_features,\n      warnings: self.warnings,\n      working_directory: ast.working_directory.clone(),\n    })\n  }\n\n  fn define(\n    definitions: &mut HashMap<&'src str, (&'static str, Name<'src>)>,\n    name: Name<'src>,\n    second_type: &'static str,\n    duplicates_allowed: bool,\n  ) -> CompileResult<'src> {\n    if let Some((first_type, original)) = definitions.get(name.lexeme()) {\n      if !(*first_type == second_type && duplicates_allowed) {\n        let ((first_type, second_type), (original, redefinition)) = if name.line < original.line {\n          ((second_type, *first_type), (name, *original))\n        } else {\n          ((*first_type, second_type), (*original, name))\n        };\n\n        return Err(redefinition.token.error(Redefinition {\n          first_type,\n          second_type,\n          name: name.lexeme(),\n          first: original.line,\n        }));\n      }\n    }\n\n    definitions.insert(name.lexeme(), (second_type, name));\n\n    Ok(())\n  }\n\n  fn analyze_recipe(recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src> {\n    let mut parameters = BTreeSet::new();\n    let mut passed_default = false;\n\n    for parameter in &recipe.parameters {\n      if parameters.contains(parameter.name.lexeme()) {\n        return Err(parameter.name.error(DuplicateParameter {\n          recipe: recipe.name.lexeme(),\n          parameter: parameter.name.lexeme(),\n        }));\n      }\n\n      parameters.insert(parameter.name.lexeme());\n\n      if parameter.default.is_some() {\n        passed_default = true;\n      } else if passed_default && parameter.is_required() && !parameter.is_option() {\n        return Err(\n          parameter\n            .name\n            .token\n            .error(RequiredParameterFollowsDefaultParameter {\n              parameter: parameter.name.lexeme(),\n            }),\n        );\n      }\n    }\n\n    let mut continued = false;\n    for line in &recipe.body {\n      if !recipe.is_script() && !continued {\n        if let Some(Fragment::Text { token }) = line.fragments.first() {\n          let text = token.lexeme();\n\n          if text.starts_with(' ') || text.starts_with('\\t') {\n            return Err(token.error(ExtraLeadingWhitespace));\n          }\n        }\n      }\n\n      continued = line.is_continuation();\n    }\n\n    if !recipe.is_script() {\n      if let Some(attribute) = recipe.attributes.get(AttributeDiscriminant::Extension) {\n        return Err(recipe.name.error(InvalidAttribute {\n          item_kind: \"Recipe\",\n          item_name: recipe.name.lexeme(),\n          attribute: Box::new(attribute.clone()),\n        }));\n      }\n    }\n\n    Ok(())\n  }\n\n  fn analyze_set(&self, set: &Set<'src>) -> CompileResult<'src> {\n    if let Some(original) = self.sets.get(set.name.lexeme()) {\n      return Err(set.name.error(DuplicateSet {\n        setting: original.name.lexeme(),\n        first: original.name.line,\n      }));\n    }\n\n    Ok(())\n  }\n\n  fn resolve_alias<'a>(\n    modules: &'a Table<'src, Justfile<'src>>,\n    recipes: &'a Table<'src, Arc<Recipe<'src>>>,\n    alias: Alias<'src, Namepath<'src>>,\n  ) -> CompileResult<'src, Alias<'src>> {\n    match Self::resolve_recipe(&alias.target, modules, recipes) {\n      Some(target) => Ok(alias.resolve(target)),\n      None => Err(alias.name.error(UnknownAliasTarget {\n        alias: alias.name.lexeme(),\n        target: alias.target,\n      })),\n    }\n  }\n\n  pub(crate) fn resolve_recipe<'a>(\n    path: &Namepath<'src>,\n    mut modules: &'a Table<'src, Justfile<'src>>,\n    mut recipes: &'a Table<'src, Arc<Recipe<'src>>>,\n  ) -> Option<Arc<Recipe<'src>>> {\n    let (name, path) = path.split_last();\n\n    for name in path {\n      let module = modules.get(name.lexeme())?;\n      modules = &module.modules;\n      recipes = &module.recipes;\n    }\n\n    recipes.get(name.lexeme()).cloned()\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  analysis_error! {\n    name: duplicate_alias,\n    input: \"alias foo := bar\\nalias foo := baz\",\n    offset: 23,\n    line: 1,\n    column: 6,\n    width: 3,\n    kind: Redefinition {\n      first_type: \"alias\",\n      second_type: \"alias\",\n      name: \"foo\",\n      first: 0,\n    },\n  }\n\n  analysis_error! {\n    name: unknown_alias_target,\n    input: \"alias foo := bar\\n\",\n    offset: 6,\n    line: 0,\n    column: 6,\n    width: 3,\n    kind: UnknownAliasTarget {\n      alias: \"foo\",\n      target: Namepath::from(Name::from_identifier(\n        Token{\n          column: 13,\n          kind: TokenKind::Identifier,\n          length: 3,\n          line: 0,\n          offset: 13,\n          path: Path::new(\"justfile\"),\n          src: \"alias foo := bar\\n\",\n        }\n      ))\n    },\n  }\n\n  analysis_error! {\n    name: alias_shadows_recipe_before,\n    input: \"bar: \\n  echo bar\\nalias foo := bar\\nfoo:\\n  echo foo\",\n    offset: 34,\n    line: 3,\n    column: 0,\n    width: 3,\n    kind: Redefinition {\n      first_type: \"alias\",\n      second_type: \"recipe\",\n      name: \"foo\",\n      first: 2,\n    },\n  }\n\n  analysis_error! {\n    name: alias_shadows_recipe_after,\n    input: \"foo:\\n  echo foo\\nalias foo := bar\\nbar:\\n  echo bar\",\n    offset: 22,\n    line: 2,\n    column: 6,\n    width: 3,\n    kind: Redefinition {\n      first_type: \"recipe\",\n      second_type: \"alias\",\n      name: \"foo\",\n      first: 0,\n    },\n  }\n\n  analysis_error! {\n    name:   required_after_default,\n    input:  \"hello arg='foo' bar:\",\n    offset: 16,\n    line:   0,\n    column: 16,\n    width:  3,\n    kind:   RequiredParameterFollowsDefaultParameter { parameter: \"bar\" },\n  }\n\n  analysis_error! {\n    name:   duplicate_parameter,\n    input:  \"a b b:\",\n    offset: 4,\n    line:   0,\n    column: 4,\n    width:  1,\n    kind:   DuplicateParameter{ recipe: \"a\", parameter: \"b\" },\n  }\n\n  analysis_error! {\n    name:   duplicate_variadic_parameter,\n    input:  \"a b +b:\",\n    offset: 5,\n    line:   0,\n    column: 5,\n    width:  1,\n    kind:   DuplicateParameter{ recipe: \"a\", parameter: \"b\" },\n  }\n\n  analysis_error! {\n    name:   duplicate_recipe,\n    input:  \"a:\\nb:\\na:\",\n    offset:  6,\n    line:   2,\n    column: 0,\n    width:  1,\n    kind:   Redefinition { first_type: \"recipe\", second_type: \"recipe\", name: \"a\", first: 0 },\n  }\n\n  analysis_error! {\n    name:   duplicate_variable,\n    input:  \"a := \\\"0\\\"\\na := \\\"0\\\"\",\n    offset: 9,\n    line:   1,\n    column: 0,\n    width:  1,\n    kind:   DuplicateVariable{variable: \"a\"},\n  }\n\n  analysis_error! {\n    name:   extra_whitespace,\n    input:  \"a:\\n blah\\n  blarg\",\n    offset:  10,\n    line:   2,\n    column: 1,\n    width:  6,\n    kind:   ExtraLeadingWhitespace,\n  }\n}\n"
  },
  {
    "path": "src/arg_attribute.rs",
    "content": "use super::*;\n\npub(crate) struct ArgAttribute<'src> {\n  pub(crate) help: Option<String>,\n  pub(crate) long: Option<String>,\n  pub(crate) name: Token<'src>,\n  pub(crate) pattern: Option<Pattern<'src>>,\n  pub(crate) short: Option<char>,\n  pub(crate) value: Option<String>,\n}\n"
  },
  {
    "path": "src/assignment.rs",
    "content": "use super::*;\n\n/// An assignment, e.g `foo := bar`\npub(crate) type Assignment<'src> = Binding<'src, Expression<'src>>;\n\nimpl Display for Assignment<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    if self.private {\n      writeln!(f, \"[private]\")?;\n    }\n\n    if self.export {\n      write!(f, \"export \")?;\n    }\n\n    write!(f, \"{} := {}\", self.name, self.value)\n  }\n}\n"
  },
  {
    "path": "src/assignment_resolver.rs",
    "content": "use {super::*, CompileErrorKind::*};\n\npub(crate) struct AssignmentResolver<'src: 'run, 'run> {\n  assignments: &'run Table<'src, Assignment<'src>>,\n  evaluated: BTreeSet<&'src str>,\n  stack: Vec<&'src str>,\n}\n\nimpl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {\n  pub(crate) fn resolve_assignments(\n    assignments: &'run Table<'src, Assignment<'src>>,\n  ) -> CompileResult<'src> {\n    let mut resolver = Self {\n      stack: Vec::new(),\n      evaluated: BTreeSet::new(),\n      assignments,\n    };\n\n    for assignment in assignments.values() {\n      resolver.resolve_assignment(assignment)?;\n    }\n\n    Ok(())\n  }\n\n  fn resolve_assignment(&mut self, assignment: &Assignment<'src>) -> CompileResult<'src> {\n    let name = assignment.name.lexeme();\n\n    if self.evaluated.contains(name) {\n      return Ok(());\n    }\n\n    self.stack.push(name);\n\n    for variable in assignment.value.variables() {\n      let name = variable.lexeme();\n\n      if self.evaluated.contains(name) || constants().contains_key(name) {\n        continue;\n      }\n\n      if self.stack.contains(&name) {\n        self.stack.push(name);\n        return Err(\n          self.assignments[name]\n            .name\n            .error(CircularVariableDependency {\n              variable: name,\n              circle: self.stack.clone(),\n            }),\n        );\n      } else if let Some(assignment) = self.assignments.get(name) {\n        self.resolve_assignment(assignment)?;\n      } else {\n        return Err(variable.error(UndefinedVariable { variable: name }));\n      }\n    }\n    self.evaluated.insert(name);\n\n    self.stack.pop();\n\n    Ok(())\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  analysis_error! {\n    name:   circular_variable_dependency,\n    input:   \"a := b\\nb := a\",\n    offset:  0,\n    line:   0,\n    column: 0,\n    width:  1,\n    kind:   CircularVariableDependency{variable: \"a\", circle: vec![\"a\", \"b\", \"a\"]},\n  }\n\n  analysis_error! {\n    name:   self_variable_dependency,\n    input:  \"a := a\",\n    offset: 0,\n    line:   0,\n    column: 0,\n    width:  1,\n    kind:   CircularVariableDependency{variable: \"a\", circle: vec![\"a\", \"a\"]},\n  }\n\n  analysis_error! {\n    name:   unknown_expression_variable,\n    input:  \"x := yy\",\n    offset: 5,\n    line:   0,\n    column: 5,\n    width:  2,\n    kind:   UndefinedVariable{variable: \"yy\"},\n  }\n\n  analysis_error! {\n    name:   unknown_function_parameter,\n    input:  \"x := env_var(yy)\",\n    offset:  13,\n    line:   0,\n    column: 13,\n    width:  2,\n    kind:   UndefinedVariable{variable: \"yy\"},\n  }\n\n  analysis_error! {\n    name:   unknown_function_parameter_binary_first,\n    input:  \"x := env_var_or_default(yy, 'foo')\",\n    offset:  24,\n    line:   0,\n    column: 24,\n    width:  2,\n    kind:   UndefinedVariable{variable: \"yy\"},\n  }\n\n  analysis_error! {\n    name:   unknown_function_parameter_binary_second,\n    input:  \"x := env_var_or_default('foo', yy)\",\n    offset:  31,\n    line:   0,\n    column: 31,\n    width:  2,\n    kind:   UndefinedVariable{variable: \"yy\"},\n  }\n}\n"
  },
  {
    "path": "src/ast.rs",
    "content": "use super::*;\n\n/// The top-level type produced by the parser. Not all successful parses result\n/// in valid justfiles, so additional consistency checks and name resolution\n/// are performed by the `Analyzer`, which produces a `Justfile` from an `Ast`.\n#[derive(Debug, Clone)]\npub(crate) struct Ast<'src> {\n  pub(crate) items: Vec<Item<'src>>,\n  pub(crate) modulepath: Modulepath,\n  pub(crate) unstable_features: BTreeSet<UnstableFeature>,\n  pub(crate) warnings: Vec<Warning>,\n  pub(crate) working_directory: PathBuf,\n}\n\nimpl Display for Ast<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    let mut iter = self.items.iter().peekable();\n\n    while let Some(item) = iter.next() {\n      writeln!(f, \"{item}\")?;\n\n      if let Some(next_item) = iter.peek() {\n        if matches!(item, Item::Recipe(_))\n          || mem::discriminant(item) != mem::discriminant(next_item)\n        {\n          writeln!(f)?;\n        }\n      }\n    }\n\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "src/attribute.rs",
    "content": "use super::*;\n\n#[allow(clippy::large_enum_variant)]\n#[derive(\n  EnumDiscriminants, PartialEq, Debug, Clone, Serialize, Ord, PartialOrd, Eq, IntoStaticStr,\n)]\n#[strum(serialize_all = \"kebab-case\")]\n#[serde(rename_all = \"kebab-case\")]\n#[strum_discriminants(name(AttributeDiscriminant))]\n#[strum_discriminants(derive(EnumString, Ord, PartialOrd))]\n#[strum_discriminants(strum(serialize_all = \"kebab-case\"))]\npub(crate) enum Attribute<'src> {\n  Arg {\n    help: Option<StringLiteral<'src>>,\n    long: Option<StringLiteral<'src>>,\n    #[serde(skip)]\n    long_key: Option<Token<'src>>,\n    name: StringLiteral<'src>,\n    pattern: Option<Pattern<'src>>,\n    short: Option<StringLiteral<'src>>,\n    value: Option<StringLiteral<'src>>,\n  },\n  Confirm(Option<StringLiteral<'src>>),\n  Default,\n  Doc(Option<StringLiteral<'src>>),\n  Dragonfly,\n  Env(StringLiteral<'src>, StringLiteral<'src>),\n  ExitMessage,\n  Extension(StringLiteral<'src>),\n  Freebsd,\n  Group(StringLiteral<'src>),\n  Linux,\n  Macos,\n  Metadata(Vec<StringLiteral<'src>>),\n  Netbsd,\n  NoCd,\n  NoExitMessage,\n  NoQuiet,\n  Openbsd,\n  Parallel,\n  PositionalArguments,\n  Private,\n  Script(Option<Interpreter<StringLiteral<'src>>>),\n  Unix,\n  Windows,\n  WorkingDirectory(StringLiteral<'src>),\n}\n\nimpl AttributeDiscriminant {\n  fn argument_range(self) -> RangeInclusive<usize> {\n    match self {\n      Self::Default\n      | Self::Dragonfly\n      | Self::ExitMessage\n      | Self::Freebsd\n      | Self::Linux\n      | Self::Macos\n      | Self::Netbsd\n      | Self::NoCd\n      | Self::NoExitMessage\n      | Self::NoQuiet\n      | Self::Openbsd\n      | Self::Parallel\n      | Self::PositionalArguments\n      | Self::Private\n      | Self::Unix\n      | Self::Windows => 0..=0,\n      Self::Confirm | Self::Doc => 0..=1,\n      Self::Script => 0..=usize::MAX,\n      Self::Arg | Self::Extension | Self::Group | Self::WorkingDirectory => 1..=1,\n      Self::Env => 2..=2,\n      Self::Metadata => 1..=usize::MAX,\n    }\n  }\n}\n\nimpl<'src> Attribute<'src> {\n  fn check_option_name(\n    parameter: &StringLiteral<'src>,\n    literal: &StringLiteral<'src>,\n  ) -> CompileResult<'src> {\n    if literal.cooked.contains('=') {\n      return Err(\n        literal\n          .token\n          .error(CompileErrorKind::OptionNameContainsEqualSign {\n            parameter: parameter.cooked.clone(),\n          }),\n      );\n    }\n\n    if literal.cooked.is_empty() {\n      return Err(literal.token.error(CompileErrorKind::OptionNameEmpty {\n        parameter: parameter.cooked.clone(),\n      }));\n    }\n\n    Ok(())\n  }\n\n  pub(crate) fn new(\n    name: Name<'src>,\n    arguments: Vec<StringLiteral<'src>>,\n    mut keyword_arguments: BTreeMap<&'src str, (Name<'src>, Option<StringLiteral<'src>>)>,\n  ) -> CompileResult<'src, Self> {\n    let discriminant = name\n      .lexeme()\n      .parse::<AttributeDiscriminant>()\n      .map_err(|_| {\n        name.error(CompileErrorKind::UnknownAttribute {\n          attribute: name.lexeme(),\n        })\n      })?;\n\n    let found = arguments.len();\n    let range = discriminant.argument_range();\n    if !range.contains(&found) {\n      return Err(\n        name.error(CompileErrorKind::AttributeArgumentCountMismatch {\n          attribute: name,\n          found,\n          min: *range.start(),\n          max: *range.end(),\n        }),\n      );\n    }\n\n    let attribute = match discriminant {\n      AttributeDiscriminant::Arg => {\n        let arg = arguments.into_iter().next().unwrap();\n\n        let (long, long_key) = keyword_arguments\n          .remove(\"long\")\n          .map(|(name, literal)| {\n            if let Some(literal) = literal {\n              Self::check_option_name(&arg, &literal)?;\n              Ok((Some(literal), None))\n            } else {\n              Ok((Some(arg.clone()), Some(*name)))\n            }\n          })\n          .transpose()?\n          .unwrap_or((None, None));\n\n        let short = Self::remove_required(&mut keyword_arguments, \"short\")?\n          .map(|(_key, literal)| {\n            Self::check_option_name(&arg, &literal)?;\n\n            if literal.cooked.chars().count() != 1 {\n              return Err(literal.token.error(\n                CompileErrorKind::ShortOptionWithMultipleCharacters {\n                  parameter: arg.cooked.clone(),\n                },\n              ));\n            }\n\n            Ok(literal)\n          })\n          .transpose()?;\n\n        let pattern = Self::remove_required(&mut keyword_arguments, \"pattern\")?\n          .map(|(_key, literal)| Pattern::new(&literal))\n          .transpose()?;\n\n        let value = Self::remove_required(&mut keyword_arguments, \"value\")?\n          .map(|(key, literal)| {\n            if long.is_none() && short.is_none() {\n              return Err(key.error(CompileErrorKind::ArgAttributeValueRequiresOption));\n            }\n            Ok(literal)\n          })\n          .transpose()?;\n\n        let help =\n          Self::remove_required(&mut keyword_arguments, \"help\")?.map(|(_key, literal)| literal);\n\n        Self::Arg {\n          help,\n          long,\n          long_key,\n          name: arg,\n          pattern,\n          short,\n          value,\n        }\n      }\n      AttributeDiscriminant::Confirm => Self::Confirm(arguments.into_iter().next()),\n      AttributeDiscriminant::Default => Self::Default,\n      AttributeDiscriminant::Doc => Self::Doc(arguments.into_iter().next()),\n      AttributeDiscriminant::Dragonfly => Self::Dragonfly,\n      AttributeDiscriminant::Env => {\n        let [key, value]: [StringLiteral; 2] = arguments.try_into().unwrap();\n        Self::Env(key, value)\n      }\n      AttributeDiscriminant::ExitMessage => Self::ExitMessage,\n      AttributeDiscriminant::Extension => Self::Extension(arguments.into_iter().next().unwrap()),\n      AttributeDiscriminant::Freebsd => Self::Freebsd,\n      AttributeDiscriminant::Group => Self::Group(arguments.into_iter().next().unwrap()),\n      AttributeDiscriminant::Linux => Self::Linux,\n      AttributeDiscriminant::Macos => Self::Macos,\n      AttributeDiscriminant::Metadata => Self::Metadata(arguments),\n      AttributeDiscriminant::Netbsd => Self::Netbsd,\n      AttributeDiscriminant::NoCd => Self::NoCd,\n      AttributeDiscriminant::NoExitMessage => Self::NoExitMessage,\n      AttributeDiscriminant::NoQuiet => Self::NoQuiet,\n      AttributeDiscriminant::Openbsd => Self::Openbsd,\n      AttributeDiscriminant::Parallel => Self::Parallel,\n      AttributeDiscriminant::PositionalArguments => Self::PositionalArguments,\n      AttributeDiscriminant::Private => Self::Private,\n      AttributeDiscriminant::Script => Self::Script({\n        let mut arguments = arguments.into_iter();\n        arguments.next().map(|command| Interpreter {\n          command,\n          arguments: arguments.collect(),\n        })\n      }),\n      AttributeDiscriminant::Unix => Self::Unix,\n      AttributeDiscriminant::Windows => Self::Windows,\n      AttributeDiscriminant::WorkingDirectory => {\n        Self::WorkingDirectory(arguments.into_iter().next().unwrap())\n      }\n    };\n\n    if let Some((_name, (keyword_name, _literal))) = keyword_arguments.into_iter().next() {\n      return Err(\n        keyword_name.error(CompileErrorKind::UnknownAttributeKeyword {\n          attribute: name.lexeme(),\n          keyword: keyword_name.lexeme(),\n        }),\n      );\n    }\n\n    Ok(attribute)\n  }\n\n  fn remove_required(\n    keyword_arguments: &mut BTreeMap<&'src str, (Name<'src>, Option<StringLiteral<'src>>)>,\n    key: &'src str,\n  ) -> CompileResult<'src, Option<(Name<'src>, StringLiteral<'src>)>> {\n    let Some((key, literal)) = keyword_arguments.remove(key) else {\n      return Ok(None);\n    };\n\n    let literal =\n      literal.ok_or_else(|| key.error(CompileErrorKind::AttributeKeyMissingValue { key }))?;\n\n    Ok(Some((key, literal)))\n  }\n\n  pub(crate) fn discriminant(&self) -> AttributeDiscriminant {\n    self.into()\n  }\n\n  pub(crate) fn name(&self) -> &'static str {\n    self.into()\n  }\n\n  pub(crate) fn repeatable(&self) -> bool {\n    matches!(\n      self,\n      Attribute::Arg { .. } | Attribute::Env(_, _) | Attribute::Group(_) | Attribute::Metadata(_),\n    )\n  }\n}\n\nimpl Display for Attribute<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    write!(f, \"{}\", self.name())?;\n\n    match self {\n      Self::Arg {\n        help,\n        long,\n        long_key: _,\n        name,\n        pattern,\n        short,\n        value,\n      } => {\n        write!(f, \"({name}\")?;\n\n        if let Some(long) = long {\n          write!(f, \", long={long}\")?;\n        }\n\n        if let Some(short) = short {\n          write!(f, \", short={short}\")?;\n        }\n\n        if let Some(pattern) = pattern {\n          write!(f, \", pattern={}\", pattern.token.lexeme())?;\n        }\n\n        if let Some(value) = value {\n          write!(f, \", value={value}\")?;\n        }\n\n        if let Some(help) = help {\n          write!(f, \", help={help}\")?;\n        }\n\n        write!(f, \")\")?;\n      }\n      Self::Confirm(None)\n      | Self::Default\n      | Self::Doc(None)\n      | Self::Dragonfly\n      | Self::ExitMessage\n      | Self::Freebsd\n      | Self::Linux\n      | Self::Macos\n      | Self::Netbsd\n      | Self::NoCd\n      | Self::NoExitMessage\n      | Self::NoQuiet\n      | Self::Openbsd\n      | Self::Parallel\n      | Self::PositionalArguments\n      | Self::Private\n      | Self::Script(None)\n      | Self::Unix\n      | Self::Windows => {}\n      Self::Confirm(Some(argument))\n      | Self::Doc(Some(argument))\n      | Self::Extension(argument)\n      | Self::Group(argument)\n      | Self::WorkingDirectory(argument) => write!(f, \"({argument})\")?,\n      Self::Env(key, value) => write!(f, \"({key}, {value})\")?,\n      Self::Metadata(arguments) => {\n        write!(f, \"(\")?;\n        for (i, argument) in arguments.iter().enumerate() {\n          if i > 0 {\n            write!(f, \", \")?;\n          }\n          write!(f, \"{argument}\")?;\n        }\n        write!(f, \")\")?;\n      }\n      Self::Script(Some(shell)) => write!(f, \"({shell})\")?,\n    }\n\n    Ok(())\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn name() {\n    assert_eq!(Attribute::NoExitMessage.name(), \"no-exit-message\");\n  }\n}\n"
  },
  {
    "path": "src/attribute_set.rs",
    "content": "use {super::*, std::collections};\n\n#[derive(Default, Debug, Clone, PartialEq, Serialize)]\npub(crate) struct AttributeSet<'src>(BTreeSet<Attribute<'src>>);\n\nimpl<'src> AttributeSet<'src> {\n  pub(crate) fn len(&self) -> usize {\n    self.0.len()\n  }\n\n  pub(crate) fn contains(&self, target: AttributeDiscriminant) -> bool {\n    self.0.iter().any(|attr| attr.discriminant() == target)\n  }\n\n  pub(crate) fn get(&self, discriminant: AttributeDiscriminant) -> Option<&Attribute<'src>> {\n    self\n      .0\n      .iter()\n      .find(|attr| discriminant == attr.discriminant())\n  }\n\n  pub(crate) fn iter<'a>(&'a self) -> collections::btree_set::Iter<'a, Attribute<'src>> {\n    self.0.iter()\n  }\n\n  pub(crate) fn ensure_valid_attributes(\n    &self,\n    item_kind: &'static str,\n    item_token: Token<'src>,\n    valid: &[AttributeDiscriminant],\n  ) -> Result<(), CompileError<'src>> {\n    for attribute in &self.0 {\n      let discriminant = attribute.discriminant();\n      if !valid.contains(&discriminant) {\n        return Err(item_token.error(CompileErrorKind::InvalidAttribute {\n          item_kind,\n          item_name: item_token.lexeme(),\n          attribute: Box::new(attribute.clone()),\n        }));\n      }\n    }\n    Ok(())\n  }\n}\n\nimpl<'src> FromIterator<Attribute<'src>> for AttributeSet<'src> {\n  fn from_iter<T: IntoIterator<Item = attribute::Attribute<'src>>>(iter: T) -> Self {\n    Self(iter.into_iter().collect())\n  }\n}\n\nimpl<'src, 'a> IntoIterator for &'a AttributeSet<'src> {\n  type Item = &'a Attribute<'src>;\n\n  type IntoIter = collections::btree_set::Iter<'a, Attribute<'src>>;\n\n  fn into_iter(self) -> Self::IntoIter {\n    self.0.iter()\n  }\n}\n\nimpl<'src> IntoIterator for AttributeSet<'src> {\n  type Item = Attribute<'src>;\n\n  type IntoIter = collections::btree_set::IntoIter<Attribute<'src>>;\n\n  fn into_iter(self) -> Self::IntoIter {\n    self.0.into_iter()\n  }\n}\n"
  },
  {
    "path": "src/binding.rs",
    "content": "use super::*;\n\n/// A binding of `name` to `value`\n#[derive(Debug, Clone, PartialEq, Serialize)]\npub(crate) struct Binding<'src, V = String> {\n  pub(crate) eager: bool,\n  pub(crate) export: bool,\n  #[serde(skip)]\n  pub(crate) file_depth: u32,\n  pub(crate) name: Name<'src>,\n  #[serde(skip)]\n  pub(crate) number: Number,\n  #[serde(skip)]\n  pub(crate) prelude: bool,\n  pub(crate) private: bool,\n  pub(crate) value: V,\n}\n\nimpl<'src, V> Keyed<'src> for Binding<'src, V> {\n  fn key(&self) -> &'src str {\n    self.name.lexeme()\n  }\n}\n"
  },
  {
    "path": "src/color.rs",
    "content": "use {\n  super::*,\n  ansi_term::{ANSIGenericString, Color::*, Prefix, Style, Suffix},\n  std::io::{self, IsTerminal},\n};\n\n#[derive(Copy, Clone, Debug, PartialEq)]\npub(crate) struct Color {\n  is_terminal: bool,\n  style: Style,\n  use_color: UseColor,\n}\n\nimpl Color {\n  pub(crate) fn active(&self) -> bool {\n    match self.use_color {\n      UseColor::Always => true,\n      UseColor::Never => false,\n      UseColor::Auto => self.is_terminal,\n    }\n  }\n\n  pub(crate) fn alias(self) -> Self {\n    self.restyle(Style::new().fg(Purple))\n  }\n\n  pub(crate) fn always() -> Self {\n    Self {\n      use_color: UseColor::Always,\n      ..Self::default()\n    }\n  }\n\n  pub(crate) fn annotation(self) -> Self {\n    self.restyle(Style::new().fg(Purple))\n  }\n\n  pub(crate) fn auto() -> Self {\n    Self::default()\n  }\n\n  pub(crate) fn banner(self) -> Self {\n    self.restyle(Style::new().fg(Cyan).bold())\n  }\n\n  pub(crate) fn command(self, foreground: Option<ansi_term::Color>) -> Self {\n    self.restyle(Style {\n      foreground,\n      is_bold: true,\n      ..Style::default()\n    })\n  }\n\n  pub(crate) fn context(self) -> Self {\n    self.restyle(Style::new().fg(Blue).bold())\n  }\n\n  pub(crate) fn diff_added(self) -> Self {\n    self.restyle(Style::new().fg(Green))\n  }\n\n  pub(crate) fn diff_deleted(self) -> Self {\n    self.restyle(Style::new().fg(Red))\n  }\n\n  pub(crate) fn doc(self) -> Self {\n    self.restyle(Style::new().fg(Blue))\n  }\n\n  pub(crate) fn doc_backtick(self) -> Self {\n    self.restyle(Style::new().fg(Cyan))\n  }\n\n  fn effective_style(&self) -> Style {\n    if self.active() {\n      self.style\n    } else {\n      Style::new()\n    }\n  }\n\n  pub(crate) fn error(self) -> Self {\n    self.restyle(Style::new().fg(Red).bold())\n  }\n\n  pub(crate) fn group(self) -> Self {\n    self.restyle(Style::new().fg(Yellow).bold())\n  }\n\n  pub(crate) fn message(self) -> Self {\n    self.restyle(Style::new().bold())\n  }\n\n  pub(crate) fn never() -> Self {\n    Self {\n      use_color: UseColor::Never,\n      ..Self::default()\n    }\n  }\n\n  pub(crate) fn paint<'a>(&self, text: &'a str) -> ANSIGenericString<'a, str> {\n    self.effective_style().paint(text)\n  }\n\n  pub(crate) fn parameter(self) -> Self {\n    self.restyle(Style::new().fg(Cyan))\n  }\n\n  pub(crate) fn prefix(&self) -> Prefix {\n    self.effective_style().prefix()\n  }\n\n  fn redirect(self, stream: impl IsTerminal) -> Self {\n    Self {\n      is_terminal: stream.is_terminal(),\n      ..self\n    }\n  }\n\n  fn restyle(self, style: Style) -> Self {\n    Self { style, ..self }\n  }\n\n  pub(crate) fn stderr(self) -> Self {\n    self.redirect(io::stderr())\n  }\n\n  pub(crate) fn stdout(self) -> Self {\n    self.redirect(io::stdout())\n  }\n\n  pub(crate) fn string(self) -> Self {\n    self.restyle(Style::new().fg(Green))\n  }\n\n  pub(crate) fn suffix(&self) -> Suffix {\n    self.effective_style().suffix()\n  }\n\n  pub(crate) fn warning(self) -> Self {\n    self.restyle(Style::new().fg(Yellow).bold())\n  }\n\n  pub(crate) fn heading(self) -> Self {\n    self.restyle(Style::new().fg(Yellow).bold())\n  }\n\n  pub(crate) fn option(self) -> Self {\n    self.restyle(Style::new().fg(Green))\n  }\n\n  pub(crate) fn argument(self) -> Self {\n    self.restyle(Style::new().fg(Cyan))\n  }\n}\n\nimpl From<UseColor> for Color {\n  fn from(use_color: UseColor) -> Self {\n    Self {\n      use_color,\n      ..Default::default()\n    }\n  }\n}\n\nimpl Default for Color {\n  fn default() -> Self {\n    Self {\n      is_terminal: false,\n      style: Style::new(),\n      use_color: UseColor::Auto,\n    }\n  }\n}\n"
  },
  {
    "path": "src/color_display.rs",
    "content": "use super::*;\n\npub(crate) trait ColorDisplay {\n  fn color_display(&self, color: Color) -> Wrapper\n  where\n    Self: Sized,\n  {\n    Wrapper(self, color)\n  }\n\n  fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result;\n}\n\npub(crate) struct Wrapper<'a>(&'a dyn ColorDisplay, Color);\n\nimpl Display for Wrapper<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    self.0.fmt(f, self.1)\n  }\n}\n"
  },
  {
    "path": "src/command_color.rs",
    "content": "use super::*;\n\n#[derive(Copy, Clone, ValueEnum)]\npub(crate) enum CommandColor {\n  Black,\n  Blue,\n  Cyan,\n  Green,\n  Purple,\n  Red,\n  Yellow,\n}\n\nimpl From<CommandColor> for ansi_term::Color {\n  fn from(command_color: CommandColor) -> Self {\n    match command_color {\n      CommandColor::Black => Self::Black,\n      CommandColor::Blue => Self::Blue,\n      CommandColor::Cyan => Self::Cyan,\n      CommandColor::Green => Self::Green,\n      CommandColor::Purple => Self::Purple,\n      CommandColor::Red => Self::Red,\n      CommandColor::Yellow => Self::Yellow,\n    }\n  }\n}\n"
  },
  {
    "path": "src/command_ext.rs",
    "content": "use super::*;\n\npub(crate) trait CommandExt {\n  fn export(\n    &mut self,\n    settings: &Settings,\n    dotenv: &BTreeMap<String, String>,\n    scope: &Scope,\n    unexports: &HashSet<String>,\n  ) -> &mut Command;\n\n  fn export_scope(&mut self, settings: &Settings, scope: &Scope, unexports: &HashSet<String>);\n\n  fn output_guard(self) -> (io::Result<process::Output>, Option<Signal>);\n\n  fn output_guard_stdout(self) -> Result<String, OutputError>;\n\n  fn status_guard(self) -> (io::Result<ExitStatus>, Option<Signal>);\n}\n\nimpl CommandExt for Command {\n  fn export(\n    &mut self,\n    settings: &Settings,\n    dotenv: &BTreeMap<String, String>,\n    scope: &Scope,\n    unexports: &HashSet<String>,\n  ) -> &mut Command {\n    for (name, value) in dotenv {\n      self.env(name, value);\n    }\n\n    if let Some(parent) = scope.parent() {\n      self.export_scope(settings, parent, unexports);\n    }\n\n    self\n  }\n\n  fn export_scope(&mut self, settings: &Settings, scope: &Scope, unexports: &HashSet<String>) {\n    if let Some(parent) = scope.parent() {\n      self.export_scope(settings, parent, unexports);\n    }\n\n    for unexport in unexports {\n      self.env_remove(unexport);\n    }\n\n    for binding in scope.bindings() {\n      if binding.export || (settings.export && !binding.prelude) {\n        self.env(binding.name.lexeme(), &binding.value);\n      }\n    }\n  }\n\n  fn output_guard(self) -> (io::Result<process::Output>, Option<Signal>) {\n    SignalHandler::spawn(self, process::Child::wait_with_output)\n  }\n\n  fn output_guard_stdout(self) -> Result<String, OutputError> {\n    let (result, caught) = self.output_guard();\n\n    let output = result.map_err(OutputError::Io)?;\n\n    OutputError::result_from_exit_status(output.status)?;\n\n    let output = str::from_utf8(&output.stdout).map_err(OutputError::Utf8)?;\n\n    if let Some(signal) = caught {\n      return Err(OutputError::Interrupted(signal));\n    }\n\n    Ok(\n      output\n        .strip_suffix(\"\\r\\n\")\n        .or_else(|| output.strip_suffix(\"\\n\"))\n        .unwrap_or(output)\n        .into(),\n    )\n  }\n\n  fn status_guard(self) -> (io::Result<ExitStatus>, Option<Signal>) {\n    SignalHandler::spawn(self, |mut child| child.wait())\n  }\n}\n"
  },
  {
    "path": "src/compilation.rs",
    "content": "use super::*;\n\n#[derive(Debug)]\npub(crate) struct Compilation<'src> {\n  pub(crate) asts: HashMap<PathBuf, Ast<'src>>,\n  pub(crate) justfile: Justfile<'src>,\n  pub(crate) overrides: HashMap<Number, String>,\n  pub(crate) root: PathBuf,\n}\n\nimpl<'src> Compilation<'src> {\n  pub(crate) fn root_ast(&self) -> &Ast<'src> {\n    self.asts.get(&self.root).unwrap()\n  }\n}\n"
  },
  {
    "path": "src/compile_error.rs",
    "content": "use super::*;\n\n#[derive(Debug, PartialEq)]\npub(crate) struct CompileError<'src> {\n  pub(crate) kind: Box<CompileErrorKind<'src>>,\n  pub(crate) token: Token<'src>,\n}\n\nimpl<'src> CompileError<'src> {\n  pub(crate) fn context(&self) -> Token<'src> {\n    self.token\n  }\n\n  pub(crate) fn new(token: Token<'src>, kind: CompileErrorKind<'src>) -> Self {\n    Self {\n      token,\n      kind: kind.into(),\n    }\n  }\n\n  pub(crate) fn source(&self) -> Option<&dyn std::error::Error> {\n    match &*self.kind {\n      CompileErrorKind::ArgumentPatternRegex { source } => Some(source),\n      _ => None,\n    }\n  }\n}\n\nfn capitalize(s: &str) -> String {\n  let mut chars = s.chars();\n  match chars.next() {\n    None => String::new(),\n    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),\n  }\n}\n\nimpl Display for CompileError<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    use CompileErrorKind::*;\n\n    match &*self.kind {\n      ArgAttributeValueRequiresOption => {\n        write!(\n          f,\n          \"Argument attribute `value` only valid with `long` or `short`\"\n        )\n      }\n      ArgumentPatternRegex { .. } => {\n        write!(f, \"Failed to parse argument pattern\")\n      }\n      AttributeArgumentCountMismatch {\n        attribute,\n        found,\n        min,\n        max,\n      } => {\n        write!(\n          f,\n          \"Attribute `{attribute}` got {found} {} but takes \",\n          Count(\"argument\", *found),\n        )?;\n\n        if min == max {\n          let expected = min;\n          write!(f, \"{expected} {}\", Count(\"argument\", *expected))\n        } else if found < min {\n          write!(f, \"at least {min} {}\", Count(\"argument\", *min))\n        } else {\n          write!(f, \"at most {max} {}\", Count(\"argument\", *max))\n        }\n      }\n      AttributePositionalFollowsKeyword => {\n        write!(\n          f,\n          \"Positional attribute arguments cannot follow keyword attribute arguments\"\n        )\n      }\n      BacktickShebang => write!(f, \"Backticks may not start with `#!`\"),\n      CircularRecipeDependency { recipe, circle } => {\n        if circle.len() == 2 {\n          write!(f, \"Recipe `{recipe}` depends on itself\")\n        } else {\n          write!(\n            f,\n            \"Recipe `{recipe}` has circular dependency `{}`\",\n            circle.join(\" -> \")\n          )\n        }\n      }\n      CircularVariableDependency { variable, circle } => {\n        if circle.len() == 2 {\n          write!(f, \"Variable `{variable}` is defined in terms of itself\")\n        } else {\n          write!(\n            f,\n            \"Variable `{variable}` depends on its own value: `{}`\",\n            circle.join(\" -> \"),\n          )\n        }\n      }\n      DependencyArgumentCountMismatch {\n        dependency,\n        found,\n        min,\n        max,\n      } => {\n        write!(\n          f,\n          \"Dependency `{dependency}` got {found} {} but takes \",\n          Count(\"argument\", *found),\n        )?;\n\n        if min == max {\n          let expected = min;\n          write!(f, \"{expected} {}\", Count(\"argument\", *expected))\n        } else if found < min {\n          write!(f, \"at least {min} {}\", Count(\"argument\", *min))\n        } else {\n          write!(f, \"at most {max} {}\", Count(\"argument\", *max))\n        }\n      }\n      DuplicateArgAttribute { arg, first } => write!(\n        f,\n        \"Recipe attribute for argument `{arg}` first used on line {} is duplicated on line {}\",\n        first.ordinal(),\n        self.token.line.ordinal(),\n      ),\n      DuplicateAttribute { attribute, first } => write!(\n        f,\n        \"Recipe attribute `{attribute}` first used on line {} is duplicated on line {}\",\n        first.ordinal(),\n        self.token.line.ordinal(),\n      ),\n      DuplicateEnvAttribute { variable, first } => write!(\n        f,\n        \"Environment variable `{variable}` first set on line {} is set again on line {}\",\n        first.ordinal(),\n        self.token.line.ordinal(),\n      ),\n      DuplicateDefault { recipe } => write!(\n        f,\n        \"Recipe `{recipe}` has duplicate `[default]` attribute, which may only appear once per module\",\n      ),\n      DuplicateOption { recipe, option } => {\n        write!(\n          f,\n          \"Recipe `{recipe}` defines option `{option}` multiple times\"\n        )\n      }\n      DuplicateParameter { recipe, parameter } => {\n        write!(f, \"Recipe `{recipe}` has duplicate parameter `{parameter}`\")\n      }\n      DuplicateSet { setting, first } => write!(\n        f,\n        \"Setting `{setting}` first set on line {} is redefined on line {}\",\n        first.ordinal(),\n        self.token.line.ordinal(),\n      ),\n      DuplicateVariable { variable } => {\n        write!(f, \"Variable `{variable}` has multiple definitions\")\n      }\n      DuplicateUnexport { variable } => {\n        write!(f, \"Variable `{variable}` is unexported multiple times\")\n      }\n      ExitMessageAndNoExitMessageAttribute { recipe } => write!(\n        f,\n        \"Recipe `{recipe}` has both `[exit-message]` and `[no-exit-message]` attributes\"\n      ),\n      ExpectedKeyword { expected, found } => {\n        let expected = List::or_ticked(expected);\n        if found.kind == TokenKind::Identifier {\n          write!(\n            f,\n            \"Expected keyword {expected} but found identifier `{}`\",\n            found.lexeme()\n          )\n        } else {\n          write!(f, \"Expected keyword {expected} but found `{}`\", found.kind)\n        }\n      }\n      ExportUnexported { variable } => {\n        write!(f, \"Variable {variable} is both exported and unexported\")\n      }\n      ExtraLeadingWhitespace => write!(f, \"Recipe line has extra leading whitespace\"),\n      ExtraneousAttributes { count } => {\n        write!(f, \"Extraneous {}\", Count(\"attribute\", *count))\n      }\n      FunctionArgumentCountMismatch {\n        function,\n        found,\n        expected,\n      } => write!(\n        f,\n        \"Function `{function}` called with {found} {} but takes {}\",\n        Count(\"argument\", *found),\n        expected.display(),\n      ),\n      GuardAndInfallibleSigil => write!(\n        f,\n        \"The guard `?` and infallible `-` sigils may not be used together\"\n      ),\n      Include => write!(\n        f,\n        \"The `!include` directive has been stabilized as `import`\"\n      ),\n      InconsistentLeadingWhitespace { expected, found } => write!(\n        f,\n        \"Recipe line has inconsistent leading whitespace. Recipe started with `{}` but found \\\n           line with `{}`\",\n        ShowWhitespace(expected),\n        ShowWhitespace(found)\n      ),\n      Internal { message } => write!(\n        f,\n        \"Internal error, this may indicate a bug in just: {message}\\n\\\n           consider filing an issue: https://github.com/casey/just/issues/new\"\n      ),\n      InvalidAttribute {\n        item_name,\n        item_kind,\n        attribute,\n      } => write!(\n        f,\n        \"{item_kind} `{item_name}` has invalid attribute `{}`\",\n        attribute.name(),\n      ),\n      InvalidEscapeSequence { character } => write!(\n        f,\n        \"`\\\\{}` is not a valid escape sequence\",\n        match character {\n          '`' => r\"\\`\".to_owned(),\n          '\\\\' => r\"\\\".to_owned(),\n          '\\'' => r\"'\".to_owned(),\n          '\"' => r#\"\"\"#.to_owned(),\n          _ => character.escape_default().collect(),\n        }\n      ),\n      MismatchedClosingDelimiter {\n        open,\n        open_line,\n        close,\n      } => write!(\n        f,\n        \"Mismatched closing delimiter `{}`. (Did you mean to close the `{}` on line {}?)\",\n        close.close(),\n        open.open(),\n        open_line.ordinal(),\n      ),\n      MixedLeadingWhitespace { whitespace } => write!(\n        f,\n        \"Found a mix of tabs and spaces in leading whitespace: `{}`\\nLeading whitespace may \\\n           consist of tabs or spaces, but not both\",\n        ShowWhitespace(whitespace)\n      ),\n      NoCdAndWorkingDirectoryAttribute { recipe } => write!(\n        f,\n        \"Recipe `{recipe}` has both `[no-cd]` and `[working-directory]` attributes\"\n      ),\n      OptionNameContainsEqualSign { parameter } => {\n        write!(\n          f,\n          \"Option name for parameter `{parameter}` contains equal sign\"\n        )\n      }\n      OptionNameEmpty { parameter } => {\n        write!(f, \"Option name for parameter `{parameter}` is empty\")\n      }\n      ParameterFollowsVariadicParameter { parameter } => {\n        write!(f, \"Parameter `{parameter}` follows variadic parameter\")\n      }\n      ParsingRecursionDepthExceeded => write!(f, \"Parsing recursion depth exceeded\"),\n      Redefinition {\n        first,\n        first_type,\n        name,\n        second_type,\n      } => {\n        if first_type == second_type {\n          write!(\n            f,\n            \"{} `{name}` first defined on line {} is redefined on line {}\",\n            capitalize(first_type),\n            first.ordinal(),\n            self.token.line.ordinal(),\n          )\n        } else {\n          write!(\n            f,\n            \"{} `{name}` defined on line {} is redefined as {} {second_type} on line {}\",\n            capitalize(first_type),\n            first.ordinal(),\n            if *second_type == \"alias\" { \"an\" } else { \"a\" },\n            self.token.line.ordinal(),\n          )\n        }\n      }\n      ShellExpansion { err } => write!(f, \"Shell expansion failed: {err}\"),\n      ShortOptionWithMultipleCharacters { parameter } => {\n        write!(\n          f,\n          \"Short option name for parameter `{parameter}` contains multiple characters\"\n        )\n      }\n      RequiredParameterFollowsDefaultParameter { parameter } => write!(\n        f,\n        \"Non-default parameter `{parameter}` follows default parameter\"\n      ),\n      UndefinedArgAttribute { argument } => {\n        write!(f, \"Argument attribute for undefined argument `{argument}`\")\n      }\n      UndefinedVariable { variable } => write!(f, \"Variable `{variable}` not defined\"),\n      UnexpectedCharacter { expected } => {\n        write!(f, \"Expected character {}\", List::or_ticked(expected))\n      }\n      UnexpectedClosingDelimiter { close } => {\n        write!(f, \"Unexpected closing delimiter `{}`\", close.close())\n      }\n      UnexpectedEndOfToken { expected } => {\n        write!(\n          f,\n          \"Expected character {} but found end-of-file\",\n          List::or_ticked(expected),\n        )\n      }\n      UnexpectedToken { expected, found } => {\n        write!(f, \"Expected {}, but found {found}\", List::or(expected))\n      }\n      UnicodeEscapeCharacter { character } => {\n        write!(f, \"expected hex digit [0-9A-Fa-f] but found `{character}`\")\n      }\n      UnicodeEscapeDelimiter { character } => write!(\n        f,\n        \"expected unicode escape sequence delimiter `{{` but found `{character}`\"\n      ),\n      UnicodeEscapeEmpty => write!(f, \"unicode escape sequences must not be empty\"),\n      UnicodeEscapeLength { hex } => write!(\n        f,\n        \"unicode escape sequence starting with `\\\\u{{{hex}` longer than six hex digits\"\n      ),\n      UnicodeEscapeRange { hex } => {\n        write!(\n          f,\n          \"unicode escape sequence value `{hex}` greater than maximum valid code point `10FFFF`\",\n        )\n      }\n      UnicodeEscapeUnterminated => write!(f, \"unterminated unicode escape sequence\"),\n      UnknownAliasTarget { alias, target } => {\n        write!(f, \"Alias `{alias}` has an unknown target `{target}`\")\n      }\n      AttributeKeyMissingValue { key } => {\n        write!(f, \"Attribute key `{key}` requires value\")\n      }\n      UnknownAttributeKeyword { attribute, keyword } => {\n        write!(f, \"Unknown keyword `{keyword}` for `{attribute}` attribute\")\n      }\n      UnknownAttribute { attribute } => write!(f, \"Unknown attribute `{attribute}`\"),\n      UnknownDependency { recipe, unknown } => {\n        write!(f, \"Recipe `{recipe}` has unknown dependency `{unknown}`\")\n      }\n      UnknownFunction { function } => write!(f, \"Call to unknown function `{function}`\"),\n      UnknownSetting { setting } => write!(f, \"Unknown setting `{setting}`\"),\n      UnknownStartOfToken { start } => {\n        write!(f, \"Unknown start of token '{start}'\")?;\n        if !start.is_ascii_graphic() {\n          write!(f, \" (U+{:04X})\", *start as u32)?;\n        }\n        Ok(())\n      }\n      UnpairedCarriageReturn => write!(f, \"Unpaired carriage return\"),\n      UnterminatedBacktick => write!(f, \"Unterminated backtick\"),\n      UnterminatedInterpolation => write!(f, \"Unterminated interpolation\"),\n      UnterminatedString => write!(f, \"Unterminated string\"),\n      VariadicParameterWithOption => write!(f, \"Variadic parameters may not be options\"),\n    }\n  }\n}\n"
  },
  {
    "path": "src/compile_error_kind.rs",
    "content": "use super::*;\n\n#[derive(Debug, PartialEq)]\npub(crate) enum CompileErrorKind<'src> {\n  ArgAttributeValueRequiresOption,\n  ArgumentPatternRegex {\n    source: regex::Error,\n  },\n  AttributeArgumentCountMismatch {\n    attribute: Name<'src>,\n    found: usize,\n    min: usize,\n    max: usize,\n  },\n  AttributeKeyMissingValue {\n    key: Name<'src>,\n  },\n  AttributePositionalFollowsKeyword,\n  BacktickShebang,\n  CircularRecipeDependency {\n    recipe: &'src str,\n    circle: Vec<&'src str>,\n  },\n  CircularVariableDependency {\n    variable: &'src str,\n    circle: Vec<&'src str>,\n  },\n  DependencyArgumentCountMismatch {\n    dependency: Namepath<'src>,\n    found: usize,\n    min: usize,\n    max: usize,\n  },\n  DuplicateArgAttribute {\n    arg: String,\n    first: usize,\n  },\n  DuplicateAttribute {\n    attribute: &'src str,\n    first: usize,\n  },\n  DuplicateDefault {\n    recipe: &'src str,\n  },\n  DuplicateEnvAttribute {\n    variable: String,\n    first: usize,\n  },\n  DuplicateOption {\n    recipe: &'src str,\n    option: Switch,\n  },\n  DuplicateParameter {\n    recipe: &'src str,\n    parameter: &'src str,\n  },\n  DuplicateSet {\n    setting: &'src str,\n    first: usize,\n  },\n  DuplicateUnexport {\n    variable: &'src str,\n  },\n  DuplicateVariable {\n    variable: &'src str,\n  },\n  ExitMessageAndNoExitMessageAttribute {\n    recipe: &'src str,\n  },\n  ExpectedKeyword {\n    expected: Vec<Keyword>,\n    found: Token<'src>,\n  },\n  ExportUnexported {\n    variable: &'src str,\n  },\n  ExtraLeadingWhitespace,\n  ExtraneousAttributes {\n    count: usize,\n  },\n  FunctionArgumentCountMismatch {\n    function: &'src str,\n    found: usize,\n    expected: RangeInclusive<usize>,\n  },\n  GuardAndInfallibleSigil,\n  Include,\n  InconsistentLeadingWhitespace {\n    expected: &'src str,\n    found: &'src str,\n  },\n  Internal {\n    message: String,\n  },\n  InvalidAttribute {\n    item_kind: &'static str,\n    item_name: &'src str,\n    attribute: Box<Attribute<'src>>,\n  },\n  InvalidEscapeSequence {\n    character: char,\n  },\n  MismatchedClosingDelimiter {\n    close: Delimiter,\n    open: Delimiter,\n    open_line: usize,\n  },\n  MixedLeadingWhitespace {\n    whitespace: &'src str,\n  },\n  NoCdAndWorkingDirectoryAttribute {\n    recipe: &'src str,\n  },\n  OptionNameContainsEqualSign {\n    parameter: String,\n  },\n  OptionNameEmpty {\n    parameter: String,\n  },\n  ParameterFollowsVariadicParameter {\n    parameter: &'src str,\n  },\n  ParsingRecursionDepthExceeded,\n  Redefinition {\n    first: usize,\n    first_type: &'static str,\n    name: &'src str,\n    second_type: &'static str,\n  },\n  RequiredParameterFollowsDefaultParameter {\n    parameter: &'src str,\n  },\n  ShellExpansion {\n    err: shellexpand::LookupError<env::VarError>,\n  },\n  ShortOptionWithMultipleCharacters {\n    parameter: String,\n  },\n  UndefinedArgAttribute {\n    argument: String,\n  },\n  UndefinedVariable {\n    variable: &'src str,\n  },\n  UnexpectedCharacter {\n    expected: Vec<char>,\n  },\n  UnexpectedClosingDelimiter {\n    close: Delimiter,\n  },\n  UnexpectedEndOfToken {\n    expected: Vec<char>,\n  },\n  UnexpectedToken {\n    expected: Vec<TokenKind>,\n    found: TokenKind,\n  },\n  UnicodeEscapeCharacter {\n    character: char,\n  },\n  UnicodeEscapeDelimiter {\n    character: char,\n  },\n  UnicodeEscapeEmpty,\n  UnicodeEscapeLength {\n    hex: String,\n  },\n  UnicodeEscapeRange {\n    hex: String,\n  },\n  UnicodeEscapeUnterminated,\n  UnknownAliasTarget {\n    alias: &'src str,\n    target: Namepath<'src>,\n  },\n  UnknownAttribute {\n    attribute: &'src str,\n  },\n  UnknownAttributeKeyword {\n    attribute: &'src str,\n    keyword: &'src str,\n  },\n  UnknownDependency {\n    recipe: &'src str,\n    unknown: Namepath<'src>,\n  },\n  UnknownFunction {\n    function: &'src str,\n  },\n  UnknownSetting {\n    setting: &'src str,\n  },\n  UnknownStartOfToken {\n    start: char,\n  },\n  UnpairedCarriageReturn,\n  UnterminatedBacktick,\n  UnterminatedInterpolation,\n  UnterminatedString,\n  VariadicParameterWithOption,\n}\n"
  },
  {
    "path": "src/compiler.rs",
    "content": "use super::*;\n\npub(crate) struct Compiler;\n\nimpl Compiler {\n  pub(crate) fn compile<'src>(\n    config: &Config,\n    loader: &'src Loader,\n    root: &Path,\n  ) -> RunResult<'src, Compilation<'src>> {\n    let mut asts = HashMap::<PathBuf, Ast>::new();\n    let mut loaded = Vec::new();\n    let mut numerator = Numerator::new();\n    let mut paths = HashMap::<PathBuf, PathBuf>::new();\n    let mut stack = Vec::new();\n    stack.push(Source::root(root));\n\n    while let Some(current) = stack.pop() {\n      if paths.contains_key(&current.path) {\n        continue;\n      }\n\n      let (relative, src) = loader.load(root, &current.path)?;\n      loaded.push(relative.into());\n      let mut ast = Parser::parse_source(&mut numerator, relative, &current, src)?;\n\n      paths.insert(current.path.clone(), relative.into());\n\n      for item in &mut ast.items {\n        match item {\n          Item::Module {\n            absolute,\n            name,\n            optional,\n            relative,\n            ..\n          } => {\n            let parent = current.path.parent().unwrap();\n\n            let relative = relative\n              .as_ref()\n              .map(|relative| Self::expand_tilde(&relative.cooked))\n              .transpose()?;\n\n            let import = Self::find_module_file(parent, *name, relative.as_deref())?;\n\n            if let Some(import) = import {\n              if current.file_path.contains(&import) {\n                return Err(Error::CircularImport {\n                  current: current.path,\n                  import,\n                });\n              }\n              *absolute = Some(import.clone());\n              stack.push(current.module(*name, import));\n            } else if !*optional {\n              return Err(Error::MissingModuleFile { module: *name });\n            }\n          }\n          Item::Import {\n            relative,\n            absolute,\n            optional,\n          } => {\n            let import = current\n              .path\n              .parent()\n              .unwrap()\n              .join(Self::expand_tilde(&relative.cooked)?)\n              .lexiclean();\n\n            if filesystem::is_file(&import)? {\n              if current.file_path.contains(&import) {\n                return Err(Error::CircularImport {\n                  current: current.path,\n                  import,\n                });\n              }\n              *absolute = Some(import.clone());\n              stack.push(current.import(import, relative.token.offset));\n            } else if !*optional {\n              return Err(Error::MissingImportFile {\n                path: relative.token,\n              });\n            }\n          }\n          _ => {}\n        }\n      }\n\n      asts.insert(current.path, ast.clone());\n    }\n\n    let mut overrides = HashMap::new();\n\n    let justfile = Analyzer::analyze(\n      &asts,\n      config,\n      None,\n      &[],\n      &loaded,\n      None,\n      &mut overrides,\n      &paths,\n      false,\n      root,\n    )?;\n\n    Ok(Compilation {\n      asts,\n      justfile,\n      overrides,\n      root: root.into(),\n    })\n  }\n\n  fn find_module_file<'src>(\n    parent: &Path,\n    module: Name<'src>,\n    path: Option<&Path>,\n  ) -> RunResult<'src, Option<PathBuf>> {\n    let mut candidates = Vec::new();\n\n    if let Some(path) = path {\n      let full = parent.join(path);\n\n      if filesystem::is_file(&full)? {\n        return Ok(Some(full));\n      }\n\n      candidates.push((path.join(\"mod.just\"), true));\n\n      for name in search::JUSTFILE_NAMES {\n        candidates.push((path.join(name), false));\n      }\n    } else {\n      candidates.push((format!(\"{module}.just\").into(), true));\n      candidates.push((format!(\"{module}/mod.just\").into(), true));\n\n      for name in search::JUSTFILE_NAMES {\n        candidates.push((format!(\"{module}/{name}\").into(), false));\n      }\n    }\n\n    let mut grouped = BTreeMap::<PathBuf, Vec<(PathBuf, bool)>>::new();\n\n    for (candidate, case_sensitive) in candidates {\n      let candidate = parent.join(candidate).lexiclean();\n      grouped\n        .entry(candidate.parent().unwrap().into())\n        .or_default()\n        .push((candidate, case_sensitive));\n    }\n\n    let mut found = Vec::new();\n\n    for (directory, candidates) in grouped {\n      let entries = match fs::read_dir(&directory) {\n        Ok(entries) => entries,\n        Err(io_error) => {\n          if io_error.kind() == io::ErrorKind::NotFound {\n            continue;\n          }\n\n          return Err(\n            SearchError::Io {\n              io_error,\n              directory,\n            }\n            .into(),\n          );\n        }\n      };\n\n      for entry in entries {\n        let entry = entry.map_err(|io_error| SearchError::Io {\n          io_error,\n          directory: directory.clone(),\n        })?;\n\n        if let Some(name) = entry.file_name().to_str() {\n          for (candidate, case_sensitive) in &candidates {\n            let candidate_name = candidate.file_name().unwrap().to_str().unwrap();\n\n            let eq = if *case_sensitive {\n              name == candidate_name\n            } else {\n              name.eq_ignore_ascii_case(candidate_name)\n            };\n\n            if eq {\n              found.push(candidate.parent().unwrap().join(name));\n            }\n          }\n        }\n      }\n    }\n\n    if found.len() > 1 {\n      found.sort();\n      Err(Error::AmbiguousModuleFile {\n        found: found\n          .into_iter()\n          .map(|found| found.strip_prefix(parent).unwrap().into())\n          .collect(),\n        module,\n      })\n    } else {\n      Ok(found.into_iter().next())\n    }\n  }\n\n  fn expand_tilde(path: &str) -> RunResult<'static, PathBuf> {\n    Ok(if let Some(path) = path.strip_prefix(\"~/\") {\n      dirs::home_dir()\n        .ok_or(Error::Homedir)?\n        .join(path.trim_start_matches('/'))\n    } else {\n      PathBuf::from(path)\n    })\n  }\n\n  #[cfg(test)]\n  pub(crate) fn test_compile(src: &str) -> RunResult<Justfile> {\n    let tokens = Lexer::test_lex(src)?;\n    let ast = Parser::parse_tokens(&mut Numerator::new(), &tokens)?;\n    let root = PathBuf::from(\"justfile\");\n    let mut asts: HashMap<PathBuf, Ast> = HashMap::new();\n    asts.insert(root.clone(), ast);\n    let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();\n    paths.insert(root.clone(), root.clone());\n    Analyzer::analyze(\n      &asts,\n      &Config::default(),\n      None,\n      &[],\n      &[],\n      None,\n      &mut HashMap::new(),\n      &paths,\n      false,\n      &root,\n    )\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use {super::*, temptree::temptree};\n\n  #[test]\n  fn recursive_includes_fail() {\n    let tmp = temptree! {\n      justfile: \"import './subdir/b'\\na: b\",\n      subdir: {\n        b: \"import '../justfile'\\nb:\"\n      }\n    };\n\n    let loader = Loader::new();\n\n    let justfile_a_path = tmp.path().join(\"justfile\");\n    let loader_output =\n      Compiler::compile(&Config::default(), &loader, &justfile_a_path).unwrap_err();\n\n    assert_matches!(loader_output, Error::CircularImport { current, import }\n      if current == tmp.path().join(\"subdir\").join(\"b\").lexiclean() &&\n      import == tmp.path().join(\"justfile\").lexiclean()\n    );\n  }\n\n  #[test]\n  fn find_module_file() {\n    #[track_caller]\n    fn case(path: Option<&str>, files: &[&str], expected: Result<Option<&str>, &[&str]>) {\n      let module = Name {\n        token: Token {\n          column: 0,\n          kind: TokenKind::Identifier,\n          length: 3,\n          line: 0,\n          offset: 0,\n          path: Path::new(\"\"),\n          src: \"foo\",\n        },\n      };\n\n      let tempdir = tempfile::tempdir().unwrap();\n\n      for file in files {\n        if let Some(parent) = Path::new(file).parent() {\n          fs::create_dir_all(tempdir.path().join(parent)).unwrap();\n        }\n\n        fs::write(tempdir.path().join(file), \"\").unwrap();\n      }\n\n      let actual = Compiler::find_module_file(tempdir.path(), module, path.map(Path::new));\n\n      match expected {\n        Err(expected) => match actual.unwrap_err() {\n          Error::AmbiguousModuleFile { found, .. } => {\n            assert_eq!(\n              found,\n              expected\n                .iter()\n                .map(|expected| expected.replace('/', std::path::MAIN_SEPARATOR_STR).into())\n                .collect::<Vec<PathBuf>>()\n            );\n          }\n          _ => panic!(\"unexpected error\"),\n        },\n        Ok(Some(expected)) => assert_eq!(\n          actual.unwrap().unwrap(),\n          tempdir\n            .path()\n            .join(expected.replace('/', std::path::MAIN_SEPARATOR_STR))\n        ),\n        Ok(None) => assert_eq!(actual.unwrap(), None),\n      }\n    }\n\n    case(None, &[\"foo.just\"], Ok(Some(\"foo.just\")));\n    case(None, &[\"FOO.just\"], Ok(None));\n    case(None, &[\"foo/mod.just\"], Ok(Some(\"foo/mod.just\")));\n    case(None, &[\"foo/MOD.just\"], Ok(None));\n    case(None, &[\"foo/justfile\"], Ok(Some(\"foo/justfile\")));\n    case(None, &[\"foo/JUSTFILE\"], Ok(Some(\"foo/JUSTFILE\")));\n    case(None, &[\"foo/.justfile\"], Ok(Some(\"foo/.justfile\")));\n    case(None, &[\"foo/.JUSTFILE\"], Ok(Some(\"foo/.JUSTFILE\")));\n    case(\n      None,\n      &[\"foo/.justfile\", \"foo/justfile\"],\n      Err(&[\"foo/.justfile\", \"foo/justfile\"]),\n    );\n    case(None, &[\"foo/JUSTFILE\"], Ok(Some(\"foo/JUSTFILE\")));\n\n    case(Some(\"bar\"), &[\"bar\"], Ok(Some(\"bar\")));\n    case(Some(\"bar\"), &[\"bar/mod.just\"], Ok(Some(\"bar/mod.just\")));\n    case(Some(\"bar\"), &[\"bar/justfile\"], Ok(Some(\"bar/justfile\")));\n    case(Some(\"bar\"), &[\"bar/JUSTFILE\"], Ok(Some(\"bar/JUSTFILE\")));\n    case(Some(\"bar\"), &[\"bar/.justfile\"], Ok(Some(\"bar/.justfile\")));\n    case(Some(\"bar\"), &[\"bar/.JUSTFILE\"], Ok(Some(\"bar/.JUSTFILE\")));\n\n    case(\n      Some(\"bar\"),\n      &[\"bar/justfile\", \"bar/mod.just\"],\n      Err(&[\"bar/justfile\", \"bar/mod.just\"]),\n    );\n  }\n}\n"
  },
  {
    "path": "src/completions.rs",
    "content": "use super::*;\n\n#[derive(ValueEnum, Debug, Clone, Copy, PartialEq)]\npub(crate) enum Shell {\n  Bash,\n  Elvish,\n  Fish,\n  #[value(alias = \"nu\")]\n  Nushell,\n  Powershell,\n  Zsh,\n}\n\nimpl Shell {\n  pub(crate) fn script(self) -> &'static str {\n    match self {\n      Self::Bash => include_str!(\"../completions/just.bash\"),\n      Self::Elvish => include_str!(\"../completions/just.elvish\"),\n      Self::Fish => include_str!(\"../completions/just.fish\"),\n      Self::Nushell => include_str!(\"../completions/just.nu\"),\n      Self::Powershell => include_str!(\"../completions/just.powershell\"),\n      Self::Zsh => include_str!(\"../completions/just.zsh\"),\n    }\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use {\n    super::*,\n    pretty_assertions::assert_eq,\n    std::io::{Read, Seek},\n    tempfile::tempfile,\n  };\n\n  #[test]\n  fn scripts() {\n    fs::create_dir_all(\"tmp/completions\").unwrap();\n\n    let bash = clap(clap_complete::Shell::Bash);\n    fs::write(\"tmp/completions/just.bash\", &bash).unwrap();\n\n    let elvish = clap(clap_complete::Shell::Elvish);\n    fs::write(\"tmp/completions/just.elvish\", &elvish).unwrap();\n\n    let fish = clap(clap_complete::Shell::Fish);\n    fs::write(\"tmp/completions/just.fish\", &fish).unwrap();\n\n    let powershell = clap(clap_complete::Shell::PowerShell);\n    fs::write(\"tmp/completions/just.powershell\", &powershell).unwrap();\n\n    let zsh = clap(clap_complete::Shell::Zsh);\n    fs::write(\"tmp/completions/just.zsh\", &zsh).unwrap();\n\n    assert_eq!(Shell::Bash.script(), bash);\n    assert_eq!(Shell::Elvish.script(), elvish);\n    assert_eq!(Shell::Fish.script(), fish);\n    assert_eq!(Shell::Powershell.script(), powershell);\n    assert_eq!(Shell::Zsh.script(), zsh);\n  }\n\n  fn clap(shell: clap_complete::Shell) -> String {\n    fn replace(haystack: &mut String, needle: &str, replacement: &str) {\n      if let Some(index) = haystack.find(needle) {\n        haystack.replace_range(index..index + needle.len(), replacement);\n      } else {\n        panic!(\"Failed to find text:\\n{needle}\\n…in completion script:\\n{haystack}\")\n      }\n    }\n\n    let mut script = {\n      let mut tempfile = tempfile().unwrap();\n\n      clap_complete::generate(\n        shell,\n        &mut crate::config::Config::app(),\n        env!(\"CARGO_PKG_NAME\"),\n        &mut tempfile,\n      );\n\n      tempfile.rewind().unwrap();\n\n      let mut buffer = String::new();\n\n      tempfile.read_to_string(&mut buffer).unwrap();\n\n      buffer\n    };\n\n    match shell {\n      clap_complete::Shell::Bash => {\n        for (needle, replacement) in BASH_COMPLETION_REPLACEMENTS {\n          replace(&mut script, needle, replacement);\n        }\n      }\n      clap_complete::Shell::Fish => {\n        script.insert_str(0, FISH_RECIPE_COMPLETIONS);\n      }\n      clap_complete::Shell::PowerShell => {\n        for (needle, replacement) in POWERSHELL_COMPLETION_REPLACEMENTS {\n          replace(&mut script, needle, replacement);\n        }\n      }\n      clap_complete::Shell::Zsh => {\n        for (needle, replacement) in ZSH_COMPLETION_REPLACEMENTS {\n          replace(&mut script, needle, replacement);\n        }\n      }\n      _ => {}\n    }\n\n    let mut script = script.trim().to_string();\n    script.push('\\n');\n    script\n  }\n\n  const FISH_RECIPE_COMPLETIONS: &str = r#\"function __fish_just_complete_recipes\n        if string match -rq '(-f|--justfile)\\s*=?(?<justfile>[^\\s]+)' -- (string split -- ' -- ' (commandline -pc))[1]\n          set -fx JUST_JUSTFILE \"$justfile\"\n        end\n        printf \"%s\\n\" (string split \" \" (just --summary))\nend\n\n# don't suggest files right off\ncomplete -c just -n \"__fish_is_first_arg\" --no-files\n\n# complete recipes\ncomplete -c just -a '(__fish_just_complete_recipes)'\n\n# autogenerated completions\n\"#;\n\n  const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[\n    (\n      r#\"    _arguments \"${_arguments_options[@]}\" : \\\"#,\n      r\"    local common=(\",\n    ),\n    (\n      r\"'*--set=[Override <VARIABLE> with <VALUE>]:VARIABLE:_default:VARIABLE:_default' \\\",\n      r\"'*--set=[Override <VARIABLE> with <VALUE>]: :(_just_variables)' \\\",\n    ),\n    (\n      r\"'()-s+[Show recipe at <PATH>]:PATH:_default' \\\n'()--show=[Show recipe at <PATH>]:PATH:_default' \\\",\n      r\"'-s+[Show recipe at <PATH>]: :(_just_commands)' \\\n'--show=[Show recipe at <PATH>]: :(_just_commands)' \\\",\n    ),\n    (\n      \"'*::ARGUMENTS -- Overrides and recipe(s) to run, defaulting to the first recipe in the \\\n     justfile:_default' \\\\\n&& ret=0\",\n      r#\")\n\n    _arguments \"${_arguments_options[@]}\" $common \\\n        '1: :_just_commands' \\\n        '*: :->args' \\\n        && ret=0\n\n    case $state in\n        args)\n            curcontext=\"${curcontext%:*}-${words[2]}:\"\n\n            local lastarg=${words[${#words}]}\n            local recipe\n\n            local cmds; cmds=(\n                ${(s: :)$(_call_program commands just --summary)}\n            )\n\n            # Find first recipe name\n            for ((i = 2; i < $#words; i++ )) do\n                if [[ ${cmds[(I)${words[i]}]} -gt 0 ]]; then\n                    recipe=${words[i]}\n                    break\n                fi\n            done\n\n            if [[ $lastarg = */* ]]; then\n                # Arguments contain slash would be recognised as a file\n                _arguments -s -S $common '*:: :_files'\n            elif [[ $lastarg = *=* ]]; then\n                # Arguments contain equal would be recognised as a variable\n                _message \"value\"\n            elif [[ $recipe ]]; then\n                # Show usage message\n                _message \"`just --show $recipe`\"\n                # Or complete with other commands\n                #_arguments -s -S $common '*:: :_just_commands'\n            else\n                _arguments -s -S $common '*:: :_just_commands'\n            fi\n        ;;\n    esac\n\n    return ret\n\"#,\n    ),\n    (\n      \"    local commands; commands=()\",\n      r#\"    [[ $PREFIX = -* ]] && return 1\n    integer ret=1\n    local variables; variables=(\n        ${(s: :)$(_call_program commands just --variables)}\n    )\n    local commands; commands=(\n        ${${${(M)\"${(f)$(_call_program commands just --list)}\":#    *}/ ##/}/ ##/:Args: }\n    )\n\"#,\n    ),\n    (\n      r#\"    _describe -t commands 'just commands' commands \"$@\"\"#,\n      r#\"    if compset -P '*='; then\n        case \"${${words[-1]%=*}#*=}\" in\n            *) _message 'value' && ret=0 ;;\n        esac\n    else\n        _describe -t variables 'variables' variables -qS \"=\" && ret=0\n        _describe -t commands 'just commands' commands \"$@\"\n    fi\n\"#,\n    ),\n    (\n      r#\"_just \"$@\"\"#,\n      r#\"(( $+functions[_just_variables] )) ||\n_just_variables() {\n    [[ $PREFIX = -* ]] && return 1\n    integer ret=1\n    local variables; variables=(\n        ${(s: :)$(_call_program commands just --variables)}\n    )\n\n    if compset -P '*='; then\n        case \"${${words[-1]%=*}#*=}\" in\n            *) _message 'value' && ret=0 ;;\n        esac\n    else\n        _describe -t variables 'variables' variables && ret=0\n    fi\n\n    return ret\n}\n\n_just \"$@\"\"#,\n    ),\n  ];\n\n  const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(\n    r#\"$completions.Where{ $_.CompletionText -like \"$wordToComplete*\" } |\n        Sort-Object -Property ListItemText\"#,\n    r#\"function Get-JustFileRecipes([string[]]$CommandElements) {\n        $justFileIndex = $commandElements.IndexOf(\"--justfile\");\n\n        if ($justFileIndex -ne -1 -and $justFileIndex + 1 -le $commandElements.Length) {\n            $justFileLocation = $commandElements[$justFileIndex + 1]\n        }\n\n        $justArgs = @(\"--summary\")\n\n        if (Test-Path $justFileLocation) {\n            $justArgs += @(\"--justfile\", $justFileLocation)\n        }\n\n        $recipes = $(just @justArgs) -split ' '\n        return $recipes | ForEach-Object { [CompletionResult]::new($_) }\n    }\n\n    $elementValues = $commandElements | Select-Object -ExpandProperty Value\n    $recipes = Get-JustFileRecipes -CommandElements $elementValues\n    $completions += $recipes\n    $completions.Where{ $_.CompletionText -like \"$wordToComplete*\" } |\n        Sort-Object -Property ListItemText\"#,\n  )];\n\n  const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[\n    (\n      r#\"            if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then\n                COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )\n                return 0\n            fi\"#,\n      r#\"                if [[ ${cur} == -* ]] ; then\n                    COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )\n                    return 0\n                elif [[ ${COMP_CWORD} -eq 1 ]]; then\n                    local recipes=$(just --summary 2> /dev/null)\n\n                    if echo \"${cur}\" | \\grep -qF '/'; then\n                        local path_prefix=$(echo \"${cur}\" | sed 's/[/][^/]*$/\\//')\n                        local recipes=$(just --summary 2> /dev/null -- \"${path_prefix}\")\n                        local recipes=$(printf \"${path_prefix}%s\\t\" $recipes)\n                    fi\n\n                    if [[ $? -eq 0 ]]; then\n                        COMPREPLY=( $(compgen -W \"${recipes}\" -- \"${cur}\") )\n                        return 0\n                    fi\n                fi\"#,\n    ),\n    (\n      r\"local i cur prev opts cmd\",\n      r\"local i cur prev words cword opts cmd\",\n    ),\n    (\n      r#\"    cur=\"${COMP_WORDS[COMP_CWORD]}\"\n    prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\"#,\n      r#\"\n    # Modules use \"::\" as the separator, which is considered a wordbreak character in bash.\n    # The _get_comp_words_by_ref function is a hack to allow for exceptions to this rule without\n    # modifying the global COMP_WORDBREAKS environment variable.\n    if type _get_comp_words_by_ref &>/dev/null; then\n        _get_comp_words_by_ref -n : cur prev words cword\n    else\n        cur=\"${COMP_WORDS[COMP_CWORD]}\"\n        prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n        words=$COMP_WORDS\n        cword=$COMP_CWORD\n    fi\n\"#,\n    ),\n    (r\"for i in ${COMP_WORDS[@]}\", r\"for i in ${words[@]}\"),\n    (r\"elif [[ ${COMP_CWORD} -eq 1 ]]; then\", r\"else\"),\n    (\n      r#\"COMPREPLY=( $(compgen -W \"${recipes}\" -- \"${cur}\") )\"#,\n      r#\"COMPREPLY=( $(compgen -W \"${recipes}\" -- \"${cur}\") )\n                        if type __ltrim_colon_completions &>/dev/null; then\n                            __ltrim_colon_completions \"$cur\"\n                        fi\"#,\n    ),\n  ];\n}\n"
  },
  {
    "path": "src/condition.rs",
    "content": "use super::*;\n\n#[derive(PartialEq, Debug, Clone)]\npub(crate) struct Condition<'src> {\n  pub(crate) lhs: Box<Expression<'src>>,\n  pub(crate) operator: ConditionalOperator,\n  pub(crate) rhs: Box<Expression<'src>>,\n}\n\nimpl Display for Condition<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    write!(f, \"{} {} {}\", self.lhs, self.operator, self.rhs)\n  }\n}\n\nimpl Serialize for Condition<'_> {\n  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n  where\n    S: Serializer,\n  {\n    let mut seq = serializer.serialize_seq(None)?;\n    seq.serialize_element(&self.operator.to_string())?;\n    seq.serialize_element(&self.lhs)?;\n    seq.serialize_element(&self.rhs)?;\n    seq.end()\n  }\n}\n"
  },
  {
    "path": "src/conditional_operator.rs",
    "content": "use super::*;\n\n/// A conditional expression operator.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub(crate) enum ConditionalOperator {\n  /// `==`\n  Equality,\n  /// `!=`\n  Inequality,\n  /// `=~`\n  RegexMatch,\n  /// `!~`\n  RegexMismatch,\n}\n\nimpl Display for ConditionalOperator {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    match self {\n      Self::Equality => write!(f, \"==\"),\n      Self::Inequality => write!(f, \"!=\"),\n      Self::RegexMatch => write!(f, \"=~\"),\n      Self::RegexMismatch => write!(f, \"!~\"),\n    }\n  }\n}\n"
  },
  {
    "path": "src/config.rs",
    "content": "use {\n  super::*,\n  clap::{\n    Arg, ArgAction, ArgGroup, ArgMatches, Command,\n    builder::{\n      FalseyValueParser, Styles,\n      styling::{AnsiColor, Effects},\n    },\n    parser::ValuesRef,\n    value_parser,\n  },\n};\n\n#[derive(Debug, Default, PartialEq)]\npub(crate) struct Config {\n  pub(crate) alias_style: AliasStyle,\n  pub(crate) allow_missing: bool,\n  pub(crate) ceiling: Option<PathBuf>,\n  pub(crate) check: bool,\n  pub(crate) color: Color,\n  pub(crate) command_color: Option<ansi_term::Color>,\n  pub(crate) cygpath: PathBuf,\n  pub(crate) dotenv_filename: Option<String>,\n  pub(crate) dotenv_path: Option<PathBuf>,\n  pub(crate) dry_run: bool,\n  pub(crate) explain: bool,\n  pub(crate) groups: Vec<String>,\n  pub(crate) highlight: bool,\n  pub(crate) invocation_directory: PathBuf,\n  pub(crate) list_heading: String,\n  pub(crate) list_prefix: String,\n  pub(crate) list_submodules: bool,\n  pub(crate) load_dotenv: bool,\n  pub(crate) no_aliases: bool,\n  pub(crate) no_dependencies: bool,\n  pub(crate) one: bool,\n  pub(crate) overrides: BTreeMap<(Modulepath, String), String>,\n  pub(crate) search_config: SearchConfig,\n  pub(crate) shell: Option<String>,\n  pub(crate) shell_args: Option<Vec<String>>,\n  pub(crate) shell_command: bool,\n  pub(crate) subcommand: Subcommand,\n  pub(crate) tempdir: Option<PathBuf>,\n  pub(crate) timestamp: bool,\n  pub(crate) timestamp_format: String,\n  pub(crate) unsorted: bool,\n  pub(crate) unstable: bool,\n  pub(crate) verbosity: Verbosity,\n  pub(crate) yes: bool,\n}\n\nmod cmd {\n  pub(crate) const CHANGELOG: &str = \"CHANGELOG\";\n  pub(crate) const CHOOSE: &str = \"CHOOSE\";\n  pub(crate) const COMMAND: &str = \"COMMAND\";\n  pub(crate) const COMPLETIONS: &str = \"COMPLETIONS\";\n  pub(crate) const DUMP: &str = \"DUMP\";\n  pub(crate) const EDIT: &str = \"EDIT\";\n  pub(crate) const EVALUATE: &str = \"EVALUATE\";\n  pub(crate) const FORMAT: &str = \"FORMAT\";\n  pub(crate) const GROUPS: &str = \"GROUPS\";\n  pub(crate) const INIT: &str = \"INIT\";\n  pub(crate) const JSON: &str = \"JSON\";\n  pub(crate) const LIST: &str = \"LIST\";\n  pub(crate) const MAN: &str = \"MAN\";\n  pub(crate) const REQUEST: &str = \"REQUEST\";\n  pub(crate) const SHOW: &str = \"SHOW\";\n  pub(crate) const SUMMARY: &str = \"SUMMARY\";\n  pub(crate) const USAGE: &str = \"USAGE\";\n  pub(crate) const VARIABLES: &str = \"VARIABLES\";\n\n  pub(crate) const ALL: &[&str] = &[\n    CHANGELOG,\n    CHOOSE,\n    COMMAND,\n    COMPLETIONS,\n    DUMP,\n    EDIT,\n    EVALUATE,\n    FORMAT,\n    INIT,\n    JSON,\n    LIST,\n    MAN,\n    REQUEST,\n    SHOW,\n    SUMMARY,\n    VARIABLES,\n  ];\n\n  pub(crate) const ARGLESS: &[&str] = &[\n    CHANGELOG, DUMP, EDIT, FORMAT, JSON, INIT, MAN, SUMMARY, VARIABLES,\n  ];\n\n  pub(crate) const HEADING: &str = \"Commands\";\n}\n\nmod arg {\n  pub(crate) const ALIAS_STYLE: &str = \"ALIAS_STYLE\";\n  pub(crate) const ALLOW_MISSING: &str = \"ALLOW-MISSING\";\n  pub(crate) const ARGUMENTS: &str = \"ARGUMENTS\";\n  pub(crate) const CEILING: &str = \"CEILING\";\n  pub(crate) const CHECK: &str = \"CHECK\";\n  pub(crate) const CHOOSER: &str = \"CHOOSER\";\n  pub(crate) const CLEAR_SHELL_ARGS: &str = \"CLEAR-SHELL-ARGS\";\n  pub(crate) const COLOR: &str = \"COLOR\";\n  pub(crate) const COMMAND_COLOR: &str = \"COMMAND-COLOR\";\n  pub(crate) const CYGPATH: &str = \"CYGPATH\";\n  pub(crate) const DOTENV_FILENAME: &str = \"DOTENV-FILENAME\";\n  pub(crate) const DOTENV_PATH: &str = \"DOTENV-PATH\";\n  pub(crate) const DRY_RUN: &str = \"DRY-RUN\";\n  pub(crate) const DUMP_FORMAT: &str = \"DUMP-FORMAT\";\n  pub(crate) const EXPLAIN: &str = \"EXPLAIN\";\n  pub(crate) const GLOBAL_JUSTFILE: &str = \"GLOBAL-JUSTFILE\";\n  pub(crate) const HIGHLIGHT: &str = \"HIGHLIGHT\";\n  pub(crate) const JUSTFILE: &str = \"JUSTFILE\";\n  pub(crate) const GROUP: &str = \"GROUP\";\n  pub(crate) const LIST_HEADING: &str = \"LIST-HEADING\";\n  pub(crate) const LIST_PREFIX: &str = \"LIST-PREFIX\";\n  pub(crate) const LIST_SUBMODULES: &str = \"LIST-SUBMODULES\";\n  pub(crate) const NO_ALIASES: &str = \"NO-ALIASES\";\n  pub(crate) const NO_DEPS: &str = \"NO-DEPS\";\n  pub(crate) const NO_DOTENV: &str = \"NO-DOTENV\";\n  pub(crate) const NO_HIGHLIGHT: &str = \"NO-HIGHLIGHT\";\n  pub(crate) const ONE: &str = \"ONE\";\n  pub(crate) const QUIET: &str = \"QUIET\";\n  pub(crate) const SET: &str = \"SET\";\n  pub(crate) const SHELL: &str = \"SHELL\";\n  pub(crate) const SHELL_ARG: &str = \"SHELL-ARG\";\n  pub(crate) const SHELL_COMMAND: &str = \"SHELL-COMMAND\";\n  pub(crate) const TEMPDIR: &str = \"TEMPDIR\";\n  pub(crate) const TIMESTAMP: &str = \"TIMESTAMP\";\n  pub(crate) const TIMESTAMP_FORMAT: &str = \"TIMESTAMP-FORMAT\";\n  pub(crate) const UNSORTED: &str = \"UNSORTED\";\n  pub(crate) const UNSTABLE: &str = \"UNSTABLE\";\n  pub(crate) const VERBOSE: &str = \"VERBOSE\";\n  pub(crate) const WORKING_DIRECTORY: &str = \"WORKING-DIRECTORY\";\n  pub(crate) const YES: &str = \"YES\";\n}\n\nimpl Config {\n  pub(crate) fn app() -> Command {\n    Command::new(env!(\"CARGO_PKG_NAME\"))\n      .bin_name(env!(\"CARGO_PKG_NAME\"))\n      .version(env!(\"CARGO_PKG_VERSION\"))\n      .author(env!(\"CARGO_PKG_AUTHORS\"))\n      .about(concat!(\n        env!(\"CARGO_PKG_DESCRIPTION\"),\n        \" - \",\n        env!(\"CARGO_PKG_HOMEPAGE\")\n      ))\n      .trailing_var_arg(true)\n      .styles(\n        Styles::styled()\n          .error(AnsiColor::Red.on_default() | Effects::BOLD)\n          .header(AnsiColor::Yellow.on_default() | Effects::BOLD)\n          .invalid(AnsiColor::Red.on_default())\n          .literal(AnsiColor::Green.on_default())\n          .placeholder(AnsiColor::Cyan.on_default())\n          .usage(AnsiColor::Yellow.on_default() | Effects::BOLD)\n          .valid(AnsiColor::Green.on_default()),\n      )\n      .arg(\n        Arg::new(arg::ALIAS_STYLE)\n          .long(\"alias-style\")\n          .env(\"JUST_ALIAS_STYLE\")\n          .action(ArgAction::Set)\n          .value_parser(clap::value_parser!(AliasStyle))\n          .default_value(\"right\")\n          .help(\"Set list command alias display style\")\n          .conflicts_with(arg::NO_ALIASES),\n      )\n      .arg(\n        Arg::new(arg::CEILING)\n          .long(\"ceiling\")\n          .env(\"JUST_CEILING\")\n          .action(ArgAction::Set)\n          .value_parser(value_parser!(PathBuf))\n          .help(\"Do not ascend above <CEILING> directory when searching for a justfile.\"),\n      )\n      .arg(\n        Arg::new(arg::CHECK)\n          .long(\"check\")\n          .action(ArgAction::SetTrue)\n          .requires(cmd::FORMAT)\n          .help(\n            \"Run `--fmt` in 'check' mode. Exits with 0 if justfile is formatted correctly. \\\n                 Exits with 1 and prints a diff if formatting is required.\",\n          ),\n      )\n      .arg(\n        Arg::new(arg::CHOOSER)\n          .long(\"chooser\")\n          .env(\"JUST_CHOOSER\")\n          .action(ArgAction::Set)\n          .help(\"Override binary invoked by `--choose`\"),\n      )\n      .arg(\n        Arg::new(arg::CLEAR_SHELL_ARGS)\n          .long(\"clear-shell-args\")\n          .action(ArgAction::SetTrue)\n          .overrides_with(arg::SHELL_ARG)\n          .help(\"Clear shell arguments\"),\n      )\n      .arg(\n        Arg::new(arg::COLOR)\n          .long(\"color\")\n          .env(\"JUST_COLOR\")\n          .action(ArgAction::Set)\n          .value_parser(clap::value_parser!(UseColor))\n          .default_value(\"auto\")\n          .help(\"Print colorful output\"),\n      )\n      .arg(\n        Arg::new(arg::COMMAND_COLOR)\n          .long(\"command-color\")\n          .env(\"JUST_COMMAND_COLOR\")\n          .action(ArgAction::Set)\n          .value_parser(clap::value_parser!(CommandColor))\n          .help(\"Echo recipe lines in <COMMAND-COLOR>\"),\n      )\n      .arg(\n        Arg::new(arg::CYGPATH)\n          .long(\"cygpath\")\n          .env(\"JUST_CYGPATH\")\n          .action(ArgAction::Set)\n          .value_parser(value_parser!(PathBuf))\n          .default_value(\"cygpath\")\n          .help(\"Use binary at <CYGPATH> to convert between unix and Windows paths.\"),\n      )\n      .arg(\n        Arg::new(arg::DOTENV_FILENAME)\n          .long(\"dotenv-filename\")\n          .action(ArgAction::Set)\n          .help(\"Search for environment file named <DOTENV-FILENAME> instead of `.env`\")\n          .conflicts_with(arg::DOTENV_PATH),\n      )\n      .arg(\n        Arg::new(arg::DOTENV_PATH)\n          .short('E')\n          .long(\"dotenv-path\")\n          .action(ArgAction::Set)\n          .value_parser(value_parser!(PathBuf))\n          .help(\"Load <DOTENV-PATH> as environment file instead of searching for one\"),\n      )\n      .arg(\n        Arg::new(arg::DRY_RUN)\n          .short('n')\n          .long(\"dry-run\")\n          .env(\"JUST_DRY_RUN\")\n          .action(ArgAction::SetTrue)\n          .help(\"Print what just would do without doing it\")\n          .conflicts_with(arg::QUIET),\n      )\n      .arg(\n        Arg::new(arg::DUMP_FORMAT)\n          .long(\"dump-format\")\n          .env(\"JUST_DUMP_FORMAT\")\n          .action(ArgAction::Set)\n          .value_parser(clap::value_parser!(DumpFormat))\n          .default_value(\"just\")\n          .value_name(\"FORMAT\")\n          .help(\"Dump justfile as <FORMAT>\"),\n      )\n      .arg(\n        Arg::new(arg::EXPLAIN)\n          .action(ArgAction::SetTrue)\n          .long(\"explain\")\n          .env(\"JUST_EXPLAIN\")\n          .help(\"Print recipe doc comment before running it\"),\n      )\n      .arg(\n        Arg::new(arg::GLOBAL_JUSTFILE)\n          .action(ArgAction::SetTrue)\n          .long(\"global-justfile\")\n          .short('g')\n          .conflicts_with(arg::JUSTFILE)\n          .conflicts_with(arg::WORKING_DIRECTORY)\n          .help(\"Use global justfile\"),\n      )\n      .arg(\n        Arg::new(arg::HIGHLIGHT)\n          .long(\"highlight\")\n          .env(\"JUST_HIGHLIGHT\")\n          .action(ArgAction::SetTrue)\n          .help(\"Highlight echoed recipe lines in bold\")\n          .overrides_with(arg::NO_HIGHLIGHT),\n      )\n      .arg(\n        Arg::new(arg::JUSTFILE)\n          .short('f')\n          .long(\"justfile\")\n          .env(\"JUST_JUSTFILE\")\n          .action(ArgAction::Set)\n          .value_parser(value_parser!(PathBuf))\n          .help(\"Use <JUSTFILE> as justfile\"),\n      )\n      .arg(\n        Arg::new(arg::LIST_HEADING)\n          .long(\"list-heading\")\n          .env(\"JUST_LIST_HEADING\")\n          .help(\"Print <TEXT> before list\")\n          .value_name(\"TEXT\")\n          .default_value(\"Available recipes:\\n\")\n          .action(ArgAction::Set),\n      )\n      .arg(\n        Arg::new(arg::LIST_PREFIX)\n          .long(\"list-prefix\")\n          .env(\"JUST_LIST_PREFIX\")\n          .help(\"Print <TEXT> before each list item\")\n          .value_name(\"TEXT\")\n          .default_value(\"    \")\n          .action(ArgAction::Set),\n      )\n      .arg(\n        Arg::new(arg::LIST_SUBMODULES)\n          .long(\"list-submodules\")\n          .env(\"JUST_LIST_SUBMODULES\")\n          .help(\"List recipes in submodules\")\n          .action(ArgAction::SetTrue)\n          .requires(cmd::LIST),\n      )\n      .arg(\n        Arg::new(arg::GROUP)\n          .long(\"group\")\n          .env(\"JUST_GROUP\")\n          .help(\"Only list recipes in <GROUP>\")\n          .action(ArgAction::Append)\n          .requires(cmd::LIST),\n      )\n      .arg(\n        Arg::new(arg::NO_ALIASES)\n          .long(\"no-aliases\")\n          .env(\"JUST_NO_ALIASES\")\n          .action(ArgAction::SetTrue)\n          .help(\"Don't show aliases in list\"),\n      )\n      .arg(\n        Arg::new(arg::NO_DEPS)\n          .long(\"no-deps\")\n          .env(\"JUST_NO_DEPS\")\n          .alias(\"no-dependencies\")\n          .action(ArgAction::SetTrue)\n          .help(\"Don't run recipe dependencies\"),\n      )\n      .arg(\n        Arg::new(arg::NO_DOTENV)\n          .long(\"no-dotenv\")\n          .env(\"JUST_NO_DOTENV\")\n          .action(ArgAction::SetTrue)\n          .help(\"Don't load `.env` file\"),\n      )\n      .arg(\n        Arg::new(arg::NO_HIGHLIGHT)\n          .long(\"no-highlight\")\n          .env(\"JUST_NO_HIGHLIGHT\")\n          .action(ArgAction::SetTrue)\n          .help(\"Don't highlight echoed recipe lines in bold\")\n          .overrides_with(arg::HIGHLIGHT),\n      )\n      .arg(\n        Arg::new(arg::ONE)\n          .long(\"one\")\n          .env(\"JUST_ONE\")\n          .action(ArgAction::SetTrue)\n          .help(\"Forbid multiple recipes from being invoked on the command line\"),\n      )\n      .arg(\n        Arg::new(arg::QUIET)\n          .short('q')\n          .long(\"quiet\")\n          .env(\"JUST_QUIET\")\n          .action(ArgAction::SetTrue)\n          .help(\"Suppress all output\")\n          .conflicts_with(arg::DRY_RUN),\n      )\n      .arg(\n        Arg::new(arg::ALLOW_MISSING)\n          .long(\"allow-missing\")\n          .env(\"JUST_ALLOW_MISSING\")\n          .action(ArgAction::SetTrue)\n          .help(\"Ignore missing recipe and module errors\"),\n      )\n      .arg(\n        Arg::new(arg::SET)\n          .long(\"set\")\n          .action(ArgAction::Append)\n          .number_of_values(2)\n          .value_names([\"VARIABLE\", \"VALUE\"])\n          .help(\"Override <VARIABLE> with <VALUE>\"),\n      )\n      .arg(\n        Arg::new(arg::SHELL)\n          .long(\"shell\")\n          .action(ArgAction::Set)\n          .help(\"Invoke <SHELL> to run recipes\"),\n      )\n      .arg(\n        Arg::new(arg::SHELL_ARG)\n          .long(\"shell-arg\")\n          .action(ArgAction::Append)\n          .allow_hyphen_values(true)\n          .overrides_with(arg::CLEAR_SHELL_ARGS)\n          .help(\"Invoke shell with <SHELL-ARG> as an argument\"),\n      )\n      .arg(\n        Arg::new(arg::SHELL_COMMAND)\n          .long(\"shell-command\")\n          .requires(cmd::COMMAND)\n          .action(ArgAction::SetTrue)\n          .help(\"Invoke <COMMAND> with the shell used to run recipe lines and backticks\"),\n      )\n      .arg(\n        Arg::new(arg::TEMPDIR)\n          .action(ArgAction::Set)\n          .env(\"JUST_TEMPDIR\")\n          .long(\"tempdir\")\n          .value_parser(value_parser!(PathBuf))\n          .help(\"Save temporary files to <TEMPDIR>.\"),\n      )\n      .arg(\n        Arg::new(arg::TIMESTAMP)\n          .action(ArgAction::SetTrue)\n          .long(\"timestamp\")\n          .env(\"JUST_TIMESTAMP\")\n          .help(\"Print recipe command timestamps\"),\n      )\n      .arg(\n        Arg::new(arg::TIMESTAMP_FORMAT)\n          .action(ArgAction::Set)\n          .long(\"timestamp-format\")\n          .env(\"JUST_TIMESTAMP_FORMAT\")\n          .default_value(\"%H:%M:%S\")\n          .help(\"Timestamp format string\"),\n      )\n      .arg(\n        Arg::new(arg::UNSORTED)\n          .long(\"unsorted\")\n          .env(\"JUST_UNSORTED\")\n          .short('u')\n          .action(ArgAction::SetTrue)\n          .help(\"Return list and summary entries in source order\"),\n      )\n      .arg(\n        Arg::new(arg::UNSTABLE)\n          .long(\"unstable\")\n          .env(\"JUST_UNSTABLE\")\n          .action(ArgAction::SetTrue)\n          .value_parser(FalseyValueParser::new())\n          .help(\"Enable unstable features\"),\n      )\n      .arg(\n        Arg::new(arg::VERBOSE)\n          .short('v')\n          .long(\"verbose\")\n          .env(\"JUST_VERBOSE\")\n          .action(ArgAction::Count)\n          .help(\"Use verbose output\"),\n      )\n      .arg(\n        Arg::new(arg::WORKING_DIRECTORY)\n          .short('d')\n          .long(\"working-directory\")\n          .env(\"JUST_WORKING_DIRECTORY\")\n          .action(ArgAction::Set)\n          .value_parser(value_parser!(PathBuf))\n          .help(\"Use <WORKING-DIRECTORY> as working directory. --justfile must also be set\")\n          .requires(arg::JUSTFILE),\n      )\n      .arg(\n        Arg::new(arg::YES)\n          .long(\"yes\")\n          .env(\"JUST_YES\")\n          .action(ArgAction::SetTrue)\n          .help(\"Automatically confirm all recipes.\"),\n      )\n      .arg(\n        Arg::new(cmd::CHANGELOG)\n          .long(\"changelog\")\n          .action(ArgAction::SetTrue)\n          .help(\"Print changelog\")\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::CHOOSE)\n          .long(\"choose\")\n          .action(ArgAction::SetTrue)\n          .help(\n            \"Select one or more recipes to run using a binary chooser. If `--chooser` is not \\\n             passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`\",\n          )\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::COMMAND)\n          .long(\"command\")\n          .short('c')\n          .num_args(1..)\n          .allow_hyphen_values(true)\n          .action(ArgAction::Append)\n          .value_parser(value_parser!(std::ffi::OsString))\n          .help(\n            \"Run an arbitrary command with the working directory, `.env`, overrides, and exports \\\n             set\",\n          )\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::COMPLETIONS)\n          .long(\"completions\")\n          .action(ArgAction::Set)\n          .value_name(\"SHELL\")\n          .value_parser(value_parser!(completions::Shell))\n          .ignore_case(true)\n          .help(\"Print shell completion script for <SHELL>\")\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::DUMP)\n          .long(\"dump\")\n          .action(ArgAction::SetTrue)\n          .help(\"Print justfile\")\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::EDIT)\n          .short('e')\n          .long(\"edit\")\n          .action(ArgAction::SetTrue)\n          .help(\"Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`\")\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::EVALUATE)\n          .long(\"evaluate\")\n          .alias(\"eval\")\n          .action(ArgAction::SetTrue)\n          .help(\n            \"Evaluate and print all variables. If a variable name is given as an argument, only \\\n             print that variable's value.\",\n          )\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::FORMAT)\n          .long(\"fmt\")\n          .alias(\"format\")\n          .action(ArgAction::SetTrue)\n          .help(\"Format and overwrite justfile\")\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::GROUPS)\n          .long(\"groups\")\n          .action(ArgAction::SetTrue)\n          .help(\"List recipe groups\")\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::INIT)\n          .long(\"init\")\n          .alias(\"initialize\")\n          .action(ArgAction::SetTrue)\n          .help(\"Initialize new justfile in project root\")\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::JSON)\n          .long(\"json\")\n          .action(ArgAction::SetTrue)\n          .conflicts_with(arg::DUMP_FORMAT)\n          .help(\"Print justfile as JSON\")\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::LIST)\n          .short('l')\n          .long(\"list\")\n          .num_args(0..)\n          .value_name(\"MODULE\")\n          .action(ArgAction::Set)\n          .conflicts_with(arg::ARGUMENTS)\n          .help(\"List available recipes in <MODULE> or root if omitted\")\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::MAN)\n          .long(\"man\")\n          .action(ArgAction::SetTrue)\n          .help(\"Print man page\")\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::REQUEST)\n          .long(\"request\")\n          .action(ArgAction::Set)\n          .hide(true)\n          .help(\n            \"Execute <REQUEST>. For internal testing purposes only. May be changed or removed at \\\n            any time.\",\n          )\n          .help_heading(cmd::REQUEST),\n      )\n      .arg(\n        Arg::new(cmd::SHOW)\n          .short('s')\n          .long(\"show\")\n          .num_args(1..)\n          .action(ArgAction::Set)\n          .value_name(\"PATH\")\n          .conflicts_with(arg::ARGUMENTS)\n          .help(\"Show recipe at <PATH>\")\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::SUMMARY)\n          .long(\"summary\")\n          .action(ArgAction::SetTrue)\n          .help(\"List names of available recipes\")\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::USAGE)\n          .long(\"usage\")\n          .num_args(1..)\n          .value_name(\"PATH\")\n          .action(ArgAction::Set)\n          .conflicts_with(arg::ARGUMENTS)\n          .help(\"Print recipe usage information\")\n          .help_heading(cmd::HEADING),\n      )\n      .arg(\n        Arg::new(cmd::VARIABLES)\n          .long(\"variables\")\n          .action(ArgAction::SetTrue)\n          .help(\"List names of variables\")\n          .help_heading(cmd::HEADING),\n      )\n      .group(ArgGroup::new(\"SUBCOMMAND\").args(cmd::ALL))\n      .arg(\n        Arg::new(arg::ARGUMENTS)\n          .num_args(1..)\n          .action(ArgAction::Append)\n          .help(\"Overrides and recipe(s) to run, defaulting to the first recipe in the justfile\"),\n      )\n  }\n\n  fn parse_modulepath(values: ValuesRef<String>) -> ConfigResult<Modulepath> {\n    let path = values.clone().map(|s| (*s).as_str()).collect::<Vec<&str>>();\n\n    let path = if path.len() == 1 && path[0].contains(' ') {\n      path[0].split_whitespace().collect::<Vec<&str>>()\n    } else {\n      path\n    };\n\n    path\n      .as_slice()\n      .try_into()\n      .map_err(|()| ConfigError::ModulePath {\n        path: values.cloned().collect(),\n      })\n  }\n\n  fn search_config(matches: &ArgMatches, positional: &Positional) -> ConfigResult<SearchConfig> {\n    if matches.get_flag(arg::GLOBAL_JUSTFILE) {\n      return Ok(SearchConfig::GlobalJustfile);\n    }\n\n    let justfile = matches.get_one::<PathBuf>(arg::JUSTFILE).map(Into::into);\n\n    let working_directory = matches\n      .get_one::<PathBuf>(arg::WORKING_DIRECTORY)\n      .map(Into::into);\n\n    if let Some(search_directory) = positional.search_directory.as_ref().map(PathBuf::from) {\n      if justfile.is_some() || working_directory.is_some() {\n        return Err(ConfigError::SearchDirConflict);\n      }\n      Ok(SearchConfig::FromSearchDirectory { search_directory })\n    } else {\n      match (justfile, working_directory) {\n        (None, None) => Ok(SearchConfig::FromInvocationDirectory),\n        (Some(justfile), None) => Ok(SearchConfig::WithJustfile { justfile }),\n        (Some(justfile), Some(working_directory)) => {\n          Ok(SearchConfig::WithJustfileAndWorkingDirectory {\n            justfile,\n            working_directory,\n          })\n        }\n        (None, Some(_)) => Err(ConfigError::internal(\n          \"--working-directory set without --justfile\",\n        )),\n      }\n    }\n  }\n\n  pub(crate) fn timestamp(&self) -> Option<String> {\n    self.timestamp.then(|| {\n      chrono::Local::now()\n        .format(&self.timestamp_format)\n        .to_string()\n    })\n  }\n\n  fn parse_override(path: &str) -> ConfigResult<(Modulepath, String)> {\n    let mut path = Modulepath::try_from([path].as_slice())\n      .map_err(|()| ConfigError::OverridePath { path: path.into() })?;\n\n    let name = path.path.pop().unwrap();\n\n    Ok((path, name))\n  }\n\n  pub(crate) fn from_matches(matches: &ArgMatches) -> ConfigResult<Self> {\n    let mut overrides = BTreeMap::new();\n    if let Some(mut values) = matches.get_many::<String>(arg::SET) {\n      while let Some(path) = values.next() {\n        overrides.insert(Self::parse_override(path)?, values.next().unwrap().into());\n      }\n    }\n\n    let positional = Positional::from_values(\n      matches\n        .get_many::<String>(arg::ARGUMENTS)\n        .map(|s| s.map(String::as_str)),\n    );\n\n    for (path, value) in &positional.overrides {\n      overrides.insert(Self::parse_override(path)?, value.into());\n    }\n\n    let search_config = Self::search_config(matches, &positional)?;\n\n    let format_overrides = || {\n      overrides\n        .iter()\n        .map(|((path, key), value)| {\n          if path.is_empty() {\n            format!(\"{key}={value}\")\n          } else {\n            format!(\"{path}::{key}={value}\")\n          }\n        })\n        .collect()\n    };\n\n    for subcommand in cmd::ARGLESS {\n      if matches.get_flag(subcommand) {\n        match (!overrides.is_empty(), !positional.arguments.is_empty()) {\n          (false, false) => {}\n          (true, false) => {\n            return Err(ConfigError::SubcommandOverrides {\n              subcommand,\n              overrides: format_overrides(),\n            });\n          }\n          (false, true) => {\n            return Err(ConfigError::SubcommandArguments {\n              arguments: positional.arguments,\n              subcommand,\n            });\n          }\n          (true, true) => {\n            return Err(ConfigError::SubcommandOverridesAndArguments {\n              arguments: positional.arguments,\n              subcommand,\n              overrides: format_overrides(),\n            });\n          }\n        }\n      }\n    }\n\n    let subcommand = if matches.get_flag(cmd::CHANGELOG) {\n      Subcommand::Changelog\n    } else if matches.get_flag(cmd::CHOOSE) {\n      Subcommand::Choose {\n        chooser: matches.get_one::<String>(arg::CHOOSER).map(Into::into),\n      }\n    } else if let Some(values) = matches.get_many::<OsString>(cmd::COMMAND) {\n      let mut arguments = values.map(Into::into).collect::<Vec<OsString>>();\n      Subcommand::Command {\n        binary: arguments.remove(0),\n        arguments,\n      }\n    } else if let Some(&shell) = matches.get_one::<completions::Shell>(cmd::COMPLETIONS) {\n      Subcommand::Completions { shell }\n    } else if matches.get_flag(cmd::DUMP) {\n      Subcommand::Dump {\n        format: *matches.get_one::<DumpFormat>(arg::DUMP_FORMAT).unwrap(),\n      }\n    } else if matches.get_flag(cmd::EDIT) {\n      Subcommand::Edit\n    } else if matches.get_flag(cmd::EVALUATE) {\n      if positional.arguments.len() > 1 {\n        return Err(ConfigError::SubcommandArguments {\n          subcommand: cmd::EVALUATE,\n          arguments: positional\n            .arguments\n            .into_iter()\n            .skip(1)\n            .collect::<Vec<String>>(),\n        });\n      }\n\n      Subcommand::Evaluate {\n        variable: positional.arguments.into_iter().next(),\n      }\n    } else if matches.get_flag(cmd::FORMAT) {\n      Subcommand::Format\n    } else if matches.get_flag(cmd::GROUPS) {\n      Subcommand::Groups\n    } else if matches.get_flag(cmd::INIT) {\n      Subcommand::Init\n    } else if matches.get_flag(cmd::JSON) {\n      Subcommand::Dump {\n        format: DumpFormat::Json,\n      }\n    } else if let Some(path) = matches.get_many::<String>(cmd::LIST) {\n      Subcommand::List {\n        path: Self::parse_modulepath(path)?,\n      }\n    } else if matches.get_flag(cmd::MAN) {\n      Subcommand::Man\n    } else if let Some(request) = matches.get_one::<String>(cmd::REQUEST) {\n      Subcommand::Request {\n        request: serde_json::from_str(request)\n          .map_err(|source| ConfigError::RequestParse { source })?,\n      }\n    } else if let Some(path) = matches.get_many::<String>(cmd::SHOW) {\n      Subcommand::Show {\n        path: Self::parse_modulepath(path)?,\n      }\n    } else if matches.get_flag(cmd::SUMMARY) {\n      Subcommand::Summary\n    } else if let Some(path) = matches.get_many::<String>(cmd::USAGE) {\n      Subcommand::Usage {\n        path: Self::parse_modulepath(path)?,\n      }\n    } else if matches.get_flag(cmd::VARIABLES) {\n      Subcommand::Variables\n    } else {\n      Subcommand::Run {\n        arguments: positional.arguments,\n      }\n    };\n\n    let unstable = matches.get_flag(arg::UNSTABLE) || subcommand == Subcommand::Summary;\n    let explain = matches.get_flag(arg::EXPLAIN);\n\n    Ok(Self {\n      alias_style: matches\n        .get_one::<AliasStyle>(arg::ALIAS_STYLE)\n        .unwrap()\n        .clone(),\n      allow_missing: matches.get_flag(arg::ALLOW_MISSING),\n      ceiling: matches.get_one::<PathBuf>(arg::CEILING).cloned(),\n      check: matches.get_flag(arg::CHECK),\n      color: (*matches.get_one::<UseColor>(arg::COLOR).unwrap()).into(),\n      command_color: matches\n        .get_one::<CommandColor>(arg::COMMAND_COLOR)\n        .copied()\n        .map(CommandColor::into),\n      cygpath: matches.get_one::<PathBuf>(arg::CYGPATH).unwrap().clone(),\n      dotenv_filename: matches\n        .get_one::<String>(arg::DOTENV_FILENAME)\n        .map(Into::into),\n      dotenv_path: matches.get_one::<PathBuf>(arg::DOTENV_PATH).map(Into::into),\n      dry_run: matches.get_flag(arg::DRY_RUN),\n      explain,\n      highlight: !matches.get_flag(arg::NO_HIGHLIGHT),\n      invocation_directory: env::current_dir().context(config_error::CurrentDirContext)?,\n      groups: matches\n        .get_many::<String>(arg::GROUP)\n        .map(|s| s.map(Into::into).collect())\n        .unwrap_or_default(),\n      list_heading: matches.get_one::<String>(arg::LIST_HEADING).unwrap().into(),\n      list_prefix: matches.get_one::<String>(arg::LIST_PREFIX).unwrap().into(),\n      list_submodules: matches.get_flag(arg::LIST_SUBMODULES),\n      load_dotenv: !matches.get_flag(arg::NO_DOTENV),\n      no_aliases: matches.get_flag(arg::NO_ALIASES),\n      no_dependencies: matches.get_flag(arg::NO_DEPS),\n      one: matches.get_flag(arg::ONE),\n      overrides,\n      search_config,\n      shell: matches.get_one::<String>(arg::SHELL).map(Into::into),\n      shell_args: if matches.get_flag(arg::CLEAR_SHELL_ARGS) {\n        Some(Vec::new())\n      } else {\n        matches\n          .get_many::<String>(arg::SHELL_ARG)\n          .map(|s| s.map(Into::into).collect())\n      },\n      shell_command: matches.get_flag(arg::SHELL_COMMAND),\n      subcommand,\n      tempdir: matches.get_one::<PathBuf>(arg::TEMPDIR).map(Into::into),\n      timestamp: matches.get_flag(arg::TIMESTAMP),\n      timestamp_format: matches\n        .get_one::<String>(arg::TIMESTAMP_FORMAT)\n        .unwrap()\n        .into(),\n      unsorted: matches.get_flag(arg::UNSORTED),\n      unstable,\n      verbosity: if matches.get_flag(arg::QUIET) {\n        Verbosity::Quiet\n      } else {\n        Verbosity::from_flag_occurrences(matches.get_count(arg::VERBOSE))\n      },\n      yes: matches.get_flag(arg::YES),\n    })\n  }\n\n  pub(crate) fn require_unstable(\n    &self,\n    justfile: &Justfile,\n    unstable_feature: UnstableFeature,\n  ) -> RunResult<'static> {\n    if self.unstable || justfile.settings.unstable {\n      Ok(())\n    } else {\n      Err(Error::UnstableFeature { unstable_feature })\n    }\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use {\n    super::*,\n    clap::error::{ContextKind, ContextValue},\n    pretty_assertions::assert_eq,\n  };\n\n  macro_rules! test {\n    {\n      name: $name:ident,\n      args: [$($arg:expr),*],\n      $(color: $color:expr,)?\n      $(dry_run: $dry_run:expr,)?\n      $(dump_format: $dump_format:expr,)?\n      $(highlight: $highlight:expr,)?\n      $(no_dependencies: $no_dependencies:expr,)?\n      $(overrides: $overrides:expr,)?\n      $(search_config: $search_config:expr,)?\n      $(shell: $shell:expr,)?\n      $(shell_args: $shell_args:expr,)?\n      $(subcommand: $subcommand:expr,)?\n      $(unsorted: $unsorted:expr,)?\n      $(unstable: $unstable:expr,)?\n      $(verbosity: $verbosity:expr,)?\n    } => {\n      #[test]\n      fn $name() {\n        let arguments = &[\n          \"just\",\n          $($arg,)*\n        ];\n\n        let want = Config {\n          $(color: $color,)?\n          $(dry_run: $dry_run,)?\n          $(dump_format: $dump_format,)?\n          $(highlight: $highlight,)?\n          $(no_dependencies: $no_dependencies,)?\n          $(overrides: $overrides,)?\n          $(search_config: $search_config,)?\n          $(shell: $shell,)?\n          $(shell_args: $shell_args,)?\n          $(subcommand: $subcommand,)?\n          $(unsorted: $unsorted,)?\n          $(unstable: $unstable,)?\n          $(verbosity: $verbosity,)?\n          ..testing::config(&[])\n        };\n\n        test(arguments, want);\n      }\n    }\n  }\n\n  #[track_caller]\n  fn test(arguments: &[&str], want: Config) {\n    let app = Config::app();\n    let matches = app\n      .try_get_matches_from(arguments)\n      .expect(\"argument parsing failed\");\n    let have = Config::from_matches(&matches).expect(\"config parsing failed\");\n    assert_eq!(have, want);\n  }\n\n  macro_rules! error {\n    {\n      name: $name:ident,\n      args: [$($arg:expr),*],\n    } => {\n      #[test]\n      fn $name() {\n        let arguments = &[\n          \"just\",\n          $($arg,)*\n        ];\n\n        let app = Config::app();\n\n        app.try_get_matches_from(arguments).expect_err(\"Expected clap error\");\n      }\n    };\n    {\n      name: $name:ident,\n      args: [$($arg:expr),*],\n      error: $error:pat,\n      $(check: $check:block,)?\n    } => {\n      #[test]\n      fn $name() {\n        let arguments = &[\n          \"just\",\n          $($arg,)*\n        ];\n\n        let app = Config::app();\n\n        let matches = app.try_get_matches_from(arguments).expect(\"Matching fails\");\n\n        match Config::from_matches(&matches).expect_err(\"config parsing succeeded\") {\n          $error => { $($check)? }\n          other => panic!(\"Unexpected config error: {other}\"),\n        }\n      }\n    }\n  }\n\n  macro_rules! error_matches {\n    (\n      name: $name:ident,\n      args: [$($arg:expr),*],\n      error: $error:pat,\n      $(check: $check:block,)?\n    ) => {\n      #[test]\n      fn $name() {\n        let arguments = &[\n          \"just\",\n          $($arg,)*\n        ];\n\n        let app = Config::app();\n\n        match app.try_get_matches_from(arguments) {\n          Err($error) => { $($check)? }\n          other => panic!(\"Unexpected result from get matches: {other:?}\")\n        }\n      }\n    };\n  }\n\n  macro_rules! map {\n    {} => {\n      BTreeMap::new()\n    };\n    {\n      $($key:literal : $value:literal),* $(,)?\n    } => {\n      {\n        let mut map: BTreeMap<(Modulepath, String), String> = BTreeMap::new();\n        $(\n          map.insert((Modulepath::default(), $key.to_owned()), $value.to_owned());\n        )*\n        map\n      }\n    }\n  }\n\n  test! {\n    name: default_config,\n    args: [],\n  }\n\n  test! {\n    name: color_default,\n    args: [],\n    color: Color::auto(),\n  }\n\n  test! {\n    name: color_never,\n    args: [\"--color\", \"never\"],\n    color: Color::never(),\n  }\n\n  test! {\n    name: color_always,\n    args: [\"--color\", \"always\"],\n    color: Color::always(),\n  }\n\n  test! {\n    name: color_auto,\n    args: [\"--color\", \"auto\"],\n    color: Color::auto(),\n  }\n\n  error! {\n    name: color_bad_value,\n    args: [\"--color\", \"foo\"],\n  }\n\n  test! {\n    name: dry_run_default,\n    args: [],\n    dry_run: false,\n  }\n\n  test! {\n    name: dry_run_long,\n    args: [\"--dry-run\"],\n    dry_run: true,\n  }\n\n  test! {\n    name: dry_run_short,\n    args: [\"-n\"],\n    dry_run: true,\n  }\n\n  error! {\n    name: dry_run_quiet,\n    args: [\"--dry-run\", \"--quiet\"],\n  }\n\n  test! {\n    name: highlight_default,\n    args: [],\n    highlight: true,\n  }\n\n  test! {\n    name: highlight_yes,\n    args: [\"--highlight\"],\n    highlight: true,\n  }\n\n  test! {\n    name: highlight_no,\n    args: [\"--no-highlight\"],\n    highlight: false,\n  }\n\n  test! {\n    name: highlight_no_yes,\n    args: [\"--no-highlight\", \"--highlight\"],\n    highlight: true,\n  }\n\n  test! {\n    name: highlight_no_yes_no,\n    args: [\"--no-highlight\", \"--highlight\", \"--no-highlight\"],\n    highlight: false,\n  }\n\n  test! {\n    name: highlight_yes_no,\n    args: [\"--highlight\", \"--no-highlight\"],\n    highlight: false,\n  }\n\n  test! {\n    name: no_deps,\n    args: [\"--no-deps\"],\n    no_dependencies: true,\n  }\n\n  test! {\n    name: no_dependencies,\n    args: [\"--no-dependencies\"],\n    no_dependencies: true,\n  }\n\n  test! {\n    name: unsorted_default,\n    args: [],\n    unsorted: false,\n  }\n\n  test! {\n    name: unsorted_long,\n    args: [\"--unsorted\"],\n    unsorted: true,\n  }\n\n  test! {\n    name: unsorted_short,\n    args: [\"-u\"],\n    unsorted: true,\n  }\n\n  test! {\n    name: quiet_default,\n    args: [],\n    verbosity: Verbosity::Taciturn,\n  }\n\n  test! {\n    name: quiet_long,\n    args: [\"--quiet\"],\n    verbosity: Verbosity::Quiet,\n  }\n\n  test! {\n    name: quiet_short,\n    args: [\"-q\"],\n    verbosity: Verbosity::Quiet,\n  }\n\n  error! {\n    name: dotenv_both_filename_and_path,\n    args: [\"--dotenv-filename\", \"foo\", \"--dotenv-path\", \"bar\"],\n  }\n\n  test! {\n    name: set_default,\n    args: [],\n    overrides: map!(),\n    subcommand: Subcommand::Run {\n      arguments: Vec::new(),\n    },\n  }\n\n  test! {\n    name: set_one,\n    args: [\"--set\", \"foo\", \"bar\"],\n    overrides: map!{\"foo\": \"bar\"},\n    subcommand: Subcommand::Run {\n      arguments: Vec::new(),\n    },\n  }\n\n  test! {\n    name: set_empty,\n    args: [\"--set\", \"foo\", \"\"],\n    overrides: map!{\"foo\": \"\"},\n    subcommand: Subcommand::Run {\n      arguments: Vec::new(),\n    },\n  }\n\n  test! {\n    name: set_two,\n    args: [\"--set\", \"foo\", \"bar\", \"--set\", \"bar\", \"baz\"],\n    overrides: map!{\"foo\": \"bar\", \"bar\": \"baz\"},\n    subcommand: Subcommand::Run {\n      arguments: Vec::new(),\n    },\n  }\n\n  test! {\n    name: set_override,\n    args: [\"--set\", \"foo\", \"bar\", \"--set\", \"foo\", \"baz\"],\n    overrides: map!{\"foo\": \"baz\"},\n    subcommand: Subcommand::Run {\n      arguments: Vec::new(),\n    },\n  }\n\n  error! {\n    name: set_bad,\n    args: [\"--set\", \"foo\"],\n  }\n\n  test! {\n    name: shell_default,\n    args: [],\n    shell: None,\n    shell_args: None,\n  }\n\n  test! {\n    name: shell_set,\n    args: [\"--shell\", \"tclsh\"],\n    shell: Some(\"tclsh\".to_owned()),\n  }\n\n  test! {\n    name: shell_args_set,\n    args: [\"--shell-arg\", \"hello\"],\n    shell: None,\n    shell_args: Some(vec![\"hello\".into()]),\n  }\n\n  test! {\n    name: verbosity_default,\n    args: [],\n    verbosity: Verbosity::Taciturn,\n  }\n\n  test! {\n    name: verbosity_long,\n    args: [\"--verbose\"],\n    verbosity: Verbosity::Loquacious,\n  }\n\n  test! {\n    name: verbosity_loquacious,\n    args: [\"-v\"],\n    verbosity: Verbosity::Loquacious,\n  }\n\n  test! {\n    name: verbosity_grandiloquent,\n    args: [\"-v\", \"-v\"],\n    verbosity: Verbosity::Grandiloquent,\n  }\n\n  test! {\n    name: verbosity_great_grandiloquent,\n    args: [\"-v\", \"-v\", \"-v\"],\n    verbosity: Verbosity::Grandiloquent,\n  }\n\n  test! {\n    name: subcommand_default,\n    args: [],\n    overrides: map!{},\n    subcommand: Subcommand::Run {\n      arguments: Vec::new(),\n    },\n  }\n\n  error! {\n    name: subcommand_conflict_changelog,\n    args: [\"--list\", \"--changelog\"],\n  }\n\n  error! {\n    name: subcommand_conflict_summary,\n    args: [\"--list\", \"--summary\"],\n  }\n\n  error! {\n    name: subcommand_conflict_dump,\n    args: [\"--list\", \"--dump\"],\n  }\n\n  error! {\n    name: subcommand_conflict_fmt,\n    args: [\"--list\", \"--fmt\"],\n  }\n\n  error! {\n    name: subcommand_conflict_init,\n    args: [\"--list\", \"--init\"],\n  }\n\n  error! {\n    name: subcommand_conflict_evaluate,\n    args: [\"--list\", \"--evaluate\"],\n  }\n\n  error! {\n    name: subcommand_conflict_show,\n    args: [\"--list\", \"--show\"],\n  }\n\n  error! {\n    name: subcommand_conflict_completions,\n    args: [\"--list\", \"--completions\"],\n  }\n\n  error! {\n    name: subcommand_conflict_variables,\n    args: [\"--list\", \"--variables\"],\n  }\n\n  error! {\n    name: subcommand_conflict_choose,\n    args: [\"--list\", \"--choose\"],\n  }\n\n  test! {\n    name: subcommand_completions,\n    args: [\"--completions\", \"bash\"],\n    subcommand: Subcommand::Completions{ shell: completions::Shell::Bash },\n  }\n\n  test! {\n    name: subcommand_completions_uppercase,\n    args: [\"--completions\", \"BASH\"],\n    subcommand: Subcommand::Completions{ shell: completions::Shell::Bash },\n  }\n\n  error! {\n    name: subcommand_completions_invalid,\n    args: [\"--completions\", \"monstersh\"],\n  }\n\n  test! {\n    name: subcommand_dump,\n    args: [\"--dump\"],\n    subcommand: Subcommand::Dump { format: DumpFormat::Just },\n  }\n\n  test! {\n    name: subcommand_json,\n    args: [\"--json\"],\n    subcommand: Subcommand::Dump { format: DumpFormat::Json },\n  }\n\n  test! {\n    name: subcommand_edit,\n    args: [\"--edit\"],\n    subcommand: Subcommand::Edit,\n  }\n\n  test! {\n    name: subcommand_evaluate,\n    args: [\"--evaluate\"],\n    overrides: map!{},\n    subcommand: Subcommand::Evaluate {\n      variable: None,\n    },\n  }\n\n  test! {\n    name: subcommand_evaluate_overrides,\n    args: [\"--evaluate\", \"x=y\"],\n    overrides: map!{\"x\": \"y\"},\n    subcommand: Subcommand::Evaluate {\n      variable: None,\n    },\n  }\n\n  test! {\n    name: subcommand_evaluate_overrides_with_argument,\n    args: [\"--evaluate\", \"x=y\", \"foo\"],\n    overrides: map!{\"x\": \"y\"},\n    subcommand: Subcommand::Evaluate {\n      variable: Some(\"foo\".to_owned()),\n    },\n  }\n\n  test! {\n    name: subcommand_list_long,\n    args: [\"--list\"],\n    subcommand: Subcommand::List{ path: Modulepath { path: Vec::new(), spaced: false } },\n  }\n\n  test! {\n    name: subcommand_list_short,\n    args: [\"-l\"],\n    subcommand: Subcommand::List{ path: Modulepath { path: Vec::new(), spaced: false } },\n  }\n\n  test! {\n    name: subcommand_list_arguments,\n    args: [\"--list\", \"bar\"],\n    subcommand: Subcommand::List{ path: Modulepath { path: vec![\"bar\".into()], spaced: false } },\n  }\n\n  test! {\n    name: subcommand_show_long,\n    args: [\"--show\", \"build\"],\n    subcommand: Subcommand::Show { path: Modulepath { path: vec![\"build\".into()], spaced: false } },\n  }\n\n  test! {\n    name: subcommand_show_short,\n    args: [\"-s\", \"build\"],\n    subcommand: Subcommand::Show { path: Modulepath { path: vec![\"build\".into()], spaced: false } },\n  }\n\n  test! {\n    name: subcommand_show_multiple_args,\n    args: [\"--show\", \"foo\", \"bar\"],\n    subcommand: Subcommand::Show {\n      path: Modulepath {\n        path: vec![\"foo\".into(), \"bar\".into()],\n        spaced: true,\n      },\n    },\n  }\n\n  test! {\n    name: subcommand_summary,\n    args: [\"--summary\"],\n    subcommand: Subcommand::Summary,\n    unstable: true,\n  }\n\n  test! {\n    name: arguments,\n    args: [\"foo\", \"bar\"],\n    overrides: map!{},\n    subcommand: Subcommand::Run {\n      arguments: vec![String::from(\"foo\"), String::from(\"bar\")],\n    },\n  }\n\n  test! {\n    name: overrides,\n    args: [\"foo=bar\", \"bar=baz\"],\n    overrides: map!{\"foo\": \"bar\", \"bar\": \"baz\"},\n    subcommand: Subcommand::Run {\n      arguments: Vec::new(),\n    },\n  }\n\n  test! {\n    name: overrides_empty,\n    args: [\"foo=\", \"bar=\"],\n    overrides: map!{\"foo\": \"\", \"bar\": \"\"},\n    subcommand: Subcommand::Run {\n      arguments: Vec::new(),\n    },\n  }\n\n  test! {\n    name: overrides_override_sets,\n    args: [\"--set\", \"foo\", \"0\", \"--set\", \"bar\", \"1\", \"foo=bar\", \"bar=baz\"],\n    overrides: map!{\"foo\": \"bar\", \"bar\": \"baz\"},\n    subcommand: Subcommand::Run {\n      arguments: Vec::new(),\n    },\n  }\n\n  test! {\n    name: shell_args_default,\n    args: [],\n  }\n\n  test! {\n    name: shell_args_set_hyphen,\n    args: [\"--shell-arg\", \"--foo\"],\n    shell_args: Some(vec![\"--foo\".to_owned()]),\n  }\n\n  test! {\n    name: shell_args_set_word,\n    args: [\"--shell-arg\", \"foo\"],\n    shell_args: Some(vec![\"foo\".to_owned()]),\n  }\n\n  test! {\n    name: shell_args_set_multiple,\n    args: [\"--shell-arg\", \"foo\", \"--shell-arg\", \"bar\"],\n    shell_args: Some(vec![\"foo\".to_owned(), \"bar\".to_owned()]),\n\n  }\n\n  test! {\n    name: shell_args_clear,\n    args: [\"--clear-shell-args\"],\n    shell_args: Some(Vec::new()),\n\n  }\n\n  test! {\n    name: shell_args_clear_and_set,\n    args: [\"--clear-shell-args\", \"--shell-arg\", \"bar\"],\n    shell_args: Some(vec![\"bar\".to_owned()]),\n\n  }\n\n  test! {\n    name: shell_args_set_and_clear,\n    args: [\"--shell-arg\", \"bar\", \"--clear-shell-args\"],\n    shell_args: Some(Vec::new()),\n\n  }\n\n  test! {\n    name: shell_args_set_multiple_and_clear,\n    args: [\"--shell-arg\", \"bar\", \"--shell-arg\", \"baz\", \"--clear-shell-args\"],\n    shell_args: Some(Vec::new()),\n\n  }\n\n  test! {\n    name: search_config_default,\n    args: [],\n    search_config: SearchConfig::FromInvocationDirectory,\n  }\n\n  test! {\n    name: search_config_from_working_directory_and_justfile,\n    args: [\"--working-directory\", \"foo\", \"--justfile\", \"bar\"],\n    search_config: SearchConfig::WithJustfileAndWorkingDirectory {\n      justfile: PathBuf::from(\"bar\"),\n      working_directory: PathBuf::from(\"foo\"),\n    },\n  }\n\n  test! {\n    name: search_config_justfile_long,\n    args: [\"--justfile\", \"foo\"],\n    search_config: SearchConfig::WithJustfile {\n      justfile: PathBuf::from(\"foo\"),\n    },\n  }\n\n  test! {\n    name: search_config_justfile_short,\n    args: [\"-f\", \"foo\"],\n    search_config: SearchConfig::WithJustfile {\n      justfile: PathBuf::from(\"foo\"),\n    },\n  }\n\n  test! {\n    name: search_directory_parent,\n    args: [\"../\"],\n    search_config: SearchConfig::FromSearchDirectory {\n      search_directory: PathBuf::from(\"..\"),\n    },\n  }\n\n  test! {\n    name: search_directory_parent_with_recipe,\n    args: [\"../build\"],\n    search_config: SearchConfig::FromSearchDirectory {\n      search_directory: PathBuf::from(\"..\"),\n    },\n    subcommand: Subcommand::Run { arguments: vec![\"build\".to_owned()] },\n  }\n\n  test! {\n    name: search_directory_child,\n    args: [\"foo/\"],\n    search_config: SearchConfig::FromSearchDirectory {\n      search_directory: PathBuf::from(\"foo\"),\n    },\n  }\n\n  test! {\n    name: search_directory_deep,\n    args: [\"foo/bar/\"],\n    search_config: SearchConfig::FromSearchDirectory {\n      search_directory: PathBuf::from(\"foo/bar\"),\n    },\n  }\n\n  test! {\n    name: search_directory_child_with_recipe,\n    args: [\"foo/build\"],\n    search_config: SearchConfig::FromSearchDirectory {\n      search_directory: PathBuf::from(\"foo\"),\n    },\n    subcommand: Subcommand::Run { arguments: vec![\"build\".to_owned()] },\n  }\n\n  error! {\n    name: search_directory_conflict_justfile,\n    args: [\"--justfile\", \"bar\", \"foo/build\"],\n    error: ConfigError::SearchDirConflict,\n  }\n\n  error! {\n    name: search_directory_conflict_working_directory,\n    args: [\"--justfile\", \"bar\", \"--working-directory\", \"baz\", \"foo/build\"],\n    error: ConfigError::SearchDirConflict,\n  }\n\n  error_matches! {\n    name: completions_argument,\n    args: [\"--completions\", \"foo\"],\n    error: error,\n    check: {\n      assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue);\n      assert_eq!(error.context().collect::<Vec<_>>(), vec![\n        (\n          ContextKind::InvalidArg,\n          &ContextValue::String(\"--completions <SHELL>\".into())),\n        (\n          ContextKind::InvalidValue,\n          &ContextValue::String(\"foo\".into()),\n        ),\n        (\n          ContextKind::ValidValue,\n          &ContextValue::Strings([\n            \"bash\".into(),\n            \"elvish\".into(),\n            \"fish\".into(),\n            \"nushell\".into(),\n            \"powershell\".into(),\n            \"zsh\".into()].into()\n          ),\n        ),\n      ]);\n    },\n  }\n\n  error! {\n    name: changelog_arguments,\n    args: [\"--changelog\", \"bar\"],\n    error: ConfigError::SubcommandArguments { subcommand, arguments },\n    check: {\n      assert_eq!(subcommand, cmd::CHANGELOG);\n      assert_eq!(arguments, &[\"bar\"]);\n    },\n  }\n\n  error! {\n    name: dump_arguments,\n    args: [\"--dump\", \"bar\"],\n    error: ConfigError::SubcommandArguments { subcommand, arguments },\n    check: {\n      assert_eq!(subcommand, cmd::DUMP);\n      assert_eq!(arguments, &[\"bar\"]);\n    },\n  }\n\n  error! {\n    name: edit_arguments,\n    args: [\"--edit\", \"bar\"],\n    error: ConfigError::SubcommandArguments { subcommand, arguments },\n    check: {\n      assert_eq!(subcommand, cmd::EDIT);\n      assert_eq!(arguments, &[\"bar\"]);\n    },\n  }\n\n  error! {\n    name: fmt_arguments,\n    args: [\"--fmt\", \"bar\"],\n    error: ConfigError::SubcommandArguments { subcommand, arguments },\n    check: {\n      assert_eq!(subcommand, cmd::FORMAT);\n      assert_eq!(arguments, &[\"bar\"]);\n    },\n  }\n\n  error! {\n    name: fmt_alias,\n    args: [\"--format\", \"bar\"],\n    error: ConfigError::SubcommandArguments { subcommand, arguments },\n    check: {\n      assert_eq!(subcommand, cmd::FORMAT);\n      assert_eq!(arguments, &[\"bar\"]);\n    },\n  }\n\n  error! {\n    name: init_arguments,\n    args: [\"--init\", \"bar\"],\n    error: ConfigError::SubcommandArguments { subcommand, arguments },\n    check: {\n      assert_eq!(subcommand, cmd::INIT);\n      assert_eq!(arguments, &[\"bar\"]);\n    },\n  }\n\n  error! {\n    name: init_alias,\n    args: [\"--initialize\", \"bar\"],\n    error: ConfigError::SubcommandArguments { subcommand, arguments },\n    check: {\n      assert_eq!(subcommand, cmd::INIT);\n      assert_eq!(arguments, &[\"bar\"]);\n    },\n  }\n\n  error! {\n    name: summary_arguments,\n    args: [\"--summary\", \"bar\"],\n    error: ConfigError::SubcommandArguments { subcommand, arguments },\n    check: {\n      assert_eq!(subcommand, cmd::SUMMARY);\n      assert_eq!(arguments, &[\"bar\"]);\n    },\n  }\n\n  error! {\n    name: subcommand_overrides_and_arguments,\n    args: [\"--summary\", \"bar=baz\", \"bar\"],\n    error: ConfigError::SubcommandOverridesAndArguments { subcommand, arguments, overrides },\n    check: {\n      assert_eq!(subcommand, cmd::SUMMARY);\n      assert_eq!(overrides, vec![\"bar=baz\"]);\n      assert_eq!(arguments, &[\"bar\"]);\n    },\n  }\n\n  error! {\n    name: summary_overrides,\n    args: [\"--summary\", \"bar=baz\"],\n    error: ConfigError::SubcommandOverrides { subcommand, overrides },\n    check: {\n      assert_eq!(subcommand, cmd::SUMMARY);\n      assert_eq!(overrides, vec![\"bar=baz\"]);\n    },\n  }\n}\n"
  },
  {
    "path": "src/config_error.rs",
    "content": "use super::*;\n\n#[derive(Debug, Snafu)]\n#[snafu(visibility(pub(crate)), context(suffix(Context)))]\npub(crate) enum ConfigError {\n  #[snafu(display(\"Failed to get current directory: {}\", source))]\n  CurrentDir { source: io::Error },\n  #[snafu(display(\n    \"Internal config error, this may indicate a bug in just: {message} \\\n     consider filing an issue: https://github.com/casey/just/issues/new\",\n  ))]\n  Internal { message: String },\n  #[snafu(display(\"Invalid module path `{}`\", path.join(\" \")))]\n  ModulePath { path: Vec<String> },\n  #[snafu(display(\"Invalid override path `{path}`\"))]\n  OverridePath { path: String },\n  #[snafu(display(\"Failed to parse request: {source}\"))]\n  RequestParse { source: serde_json::Error },\n  #[snafu(display(\n    \"Path-prefixed recipes may not be used with `--working-directory` or `--justfile`.\"\n  ))]\n  SearchDirConflict,\n  #[snafu(display(\n    \"`--{}` used with unexpected {}: {}\",\n    subcommand.to_lowercase(),\n    Count(\"argument\", arguments.len()),\n    List::and_ticked(arguments)\n  ))]\n  SubcommandArguments {\n    subcommand: &'static str,\n    arguments: Vec<String>,\n  },\n  #[snafu(display(\n      \"`--{}` used with unexpected overrides: {}\",\n      subcommand.to_lowercase(),\n      List::and_ticked(overrides.iter()),\n  ))]\n  SubcommandOverrides {\n    subcommand: &'static str,\n    overrides: Vec<String>,\n  },\n  #[snafu(display(\n      \"`--{}` used with unexpected overrides: {}; and arguments: {}\",\n      subcommand.to_lowercase(),\n      List::and_ticked(overrides.iter()),\n      List::and_ticked(arguments)))\n  ]\n  SubcommandOverridesAndArguments {\n    subcommand: &'static str,\n    overrides: Vec<String>,\n    arguments: Vec<String>,\n  },\n}\n\nimpl ConfigError {\n  pub(crate) fn internal(message: impl Into<String>) -> Self {\n    Self::Internal {\n      message: message.into(),\n    }\n  }\n}\n"
  },
  {
    "path": "src/const_error.rs",
    "content": "use super::*;\n\n#[derive(Clone, Copy, Debug)]\npub(crate) enum ConstError<'src> {\n  Backtick(Token<'src>),\n  FunctionCall(Name<'src>),\n  Variable(Name<'src>),\n}\n\nimpl<'src> ConstError<'src> {\n  pub(crate) fn context(self) -> Token<'src> {\n    match self {\n      Self::Backtick(token) => token,\n      Self::FunctionCall(name) | Self::Variable(name) => name.token,\n    }\n  }\n}\n\nimpl Display for ConstError<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    match self {\n      Self::Backtick(_) => write!(f, \"Cannot call backticks in const context\"),\n      Self::FunctionCall(_) => write!(f, \"Cannot call functions in const context\"),\n      Self::Variable(name) => write!(\n        f,\n        \"Cannot access non-const variable `{name}` in const context\"\n      ),\n    }\n  }\n}\n"
  },
  {
    "path": "src/constants.rs",
    "content": "use super::*;\n\nconst CONSTANTS: &[(&str, &str, Option<&str>, &str)] = &[\n  (\"HEX\", \"0123456789abcdef\", None, \"1.27.0\"),\n  (\"HEXLOWER\", \"0123456789abcdef\", None, \"1.27.0\"),\n  (\"HEXUPPER\", \"0123456789ABCDEF\", None, \"1.27.0\"),\n  (\"PATH_SEP\", \"/\", Some(\"\\\\\"), \"1.41.0\"),\n  (\"PATH_VAR_SEP\", \":\", Some(\";\"), \"1.41.0\"),\n  (\"CLEAR\", \"\\x1bc\", None, \"1.37.0\"),\n  (\"NORMAL\", \"\\x1b[0m\", None, \"1.37.0\"),\n  (\"BOLD\", \"\\x1b[1m\", None, \"1.37.0\"),\n  (\"ITALIC\", \"\\x1b[3m\", None, \"1.37.0\"),\n  (\"UNDERLINE\", \"\\x1b[4m\", None, \"1.37.0\"),\n  (\"INVERT\", \"\\x1b[7m\", None, \"1.37.0\"),\n  (\"HIDE\", \"\\x1b[8m\", None, \"1.37.0\"),\n  (\"STRIKETHROUGH\", \"\\x1b[9m\", None, \"1.37.0\"),\n  (\"BLACK\", \"\\x1b[30m\", None, \"1.37.0\"),\n  (\"RED\", \"\\x1b[31m\", None, \"1.37.0\"),\n  (\"GREEN\", \"\\x1b[32m\", None, \"1.37.0\"),\n  (\"YELLOW\", \"\\x1b[33m\", None, \"1.37.0\"),\n  (\"BLUE\", \"\\x1b[34m\", None, \"1.37.0\"),\n  (\"MAGENTA\", \"\\x1b[35m\", None, \"1.37.0\"),\n  (\"CYAN\", \"\\x1b[36m\", None, \"1.37.0\"),\n  (\"WHITE\", \"\\x1b[37m\", None, \"1.37.0\"),\n  (\"BG_BLACK\", \"\\x1b[40m\", None, \"1.37.0\"),\n  (\"BG_RED\", \"\\x1b[41m\", None, \"1.37.0\"),\n  (\"BG_GREEN\", \"\\x1b[42m\", None, \"1.37.0\"),\n  (\"BG_YELLOW\", \"\\x1b[43m\", None, \"1.37.0\"),\n  (\"BG_BLUE\", \"\\x1b[44m\", None, \"1.37.0\"),\n  (\"BG_MAGENTA\", \"\\x1b[45m\", None, \"1.37.0\"),\n  (\"BG_CYAN\", \"\\x1b[46m\", None, \"1.37.0\"),\n  (\"BG_WHITE\", \"\\x1b[47m\", None, \"1.37.0\"),\n];\n\npub(crate) fn constants() -> &'static BTreeMap<&'static str, &'static str> {\n  static MAP: LazyLock<BTreeMap<&str, &str>> = LazyLock::new(|| {\n    CONSTANTS\n      .iter()\n      .copied()\n      .map(|(name, unix, windows, _version)| {\n        (\n          name,\n          if cfg!(windows) {\n            windows.unwrap_or(unix)\n          } else {\n            unix\n          },\n        )\n      })\n      .collect()\n  });\n\n  &MAP\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn readme_table() {\n    let mut table = Vec::<String>::new();\n    table.push(\"| Name | Value | Value on Windows |\".into());\n    table.push(\"|---|---|---|\".into());\n    for (name, unix, windows, version) in CONSTANTS {\n      table.push(format!(\n        \"| `{name}`<sup>{version}</sup> | `\\\"{}\\\"` | {} |\",\n        unix.replace('\\x1b', \"\\\\e\"),\n        windows\n          .map(|value| format!(\"`\\\"{value}\\\"`\"))\n          .unwrap_or_default(),\n      ));\n    }\n\n    let table = table.join(\"\\n\");\n\n    let readme = fs::read_to_string(\"README.md\").unwrap();\n\n    assert!(\n      readme.contains(&table),\n      \"could not find table in readme:\\n{table}\",\n    );\n  }\n}\n"
  },
  {
    "path": "src/count.rs",
    "content": "use super::*;\n\npub(crate) struct Count<T: Display>(pub T, pub usize);\n\nimpl<T: Display> Display for Count<T> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    if self.1 == 1 {\n      write!(f, \"{}\", self.0)\n    } else {\n      write!(f, \"{}s\", self.0)\n    }\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn count() {\n    assert_eq!(Count(\"dog\", 0).to_string(), \"dogs\");\n    assert_eq!(Count(\"dog\", 1).to_string(), \"dog\");\n    assert_eq!(Count(\"dog\", 2).to_string(), \"dogs\");\n  }\n}\n"
  },
  {
    "path": "src/delimiter.rs",
    "content": "use super::*;\n\n#[derive(Clone, Copy, Debug, Eq, PartialEq)]\npub(crate) enum Delimiter {\n  Brace,\n  Bracket,\n  FormatString(StringKind),\n  Paren,\n}\n\nimpl Delimiter {\n  pub(crate) fn open(self) -> char {\n    match self {\n      Self::Brace | Self::FormatString(_) => '{',\n      Self::Bracket => '[',\n      Self::Paren => '(',\n    }\n  }\n\n  pub(crate) fn close(self) -> char {\n    match self {\n      Self::Brace | Self::FormatString(_) => '}',\n      Self::Bracket => ']',\n      Self::Paren => ')',\n    }\n  }\n}\n"
  },
  {
    "path": "src/dependency.rs",
    "content": "use super::*;\n\n#[derive(Clone, PartialEq, Debug, Serialize)]\npub(crate) struct Dependency<'src> {\n  #[serde(serialize_with = \"flatten_arguments\")]\n  pub(crate) arguments: Vec<Vec<Expression<'src>>>,\n  #[serde(serialize_with = \"keyed::serialize\")]\n  pub(crate) recipe: Arc<Recipe<'src>>,\n}\n\nfn flatten_arguments<S: Serializer>(\n  arguments: &[Vec<Expression<'_>>],\n  serializer: S,\n) -> Result<S::Ok, S::Error> {\n  let len = arguments.iter().map(Vec::len).sum();\n  let mut seq = serializer.serialize_seq(Some(len))?;\n  for group in arguments {\n    for argument in group {\n      seq.serialize_element(argument)?;\n    }\n  }\n  seq.end()\n}\n\nimpl Display for Dependency<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    if self.arguments.is_empty() {\n      write!(f, \"{}\", self.recipe.name())\n    } else {\n      write!(f, \"({}\", self.recipe.name())?;\n\n      for group in &self.arguments {\n        for argument in group {\n          write!(f, \" {argument}\")?;\n        }\n      }\n\n      write!(f, \")\")\n    }\n  }\n}\n"
  },
  {
    "path": "src/dump_format.rs",
    "content": "use super::*;\n\n#[derive(Clone, Copy, Debug, Default, PartialEq, ValueEnum)]\npub(crate) enum DumpFormat {\n  Json,\n  #[default]\n  Just,\n}\n"
  },
  {
    "path": "src/enclosure.rs",
    "content": "use super::*;\n\npub(crate) struct Enclosure<T: Display> {\n  enclosure: &'static str,\n  value: T,\n}\n\nimpl<T: Display> Enclosure<T> {\n  pub(crate) fn tick(value: T) -> Self {\n    Self {\n      enclosure: \"`\",\n      value,\n    }\n  }\n}\n\nimpl<T: Display> Display for Enclosure<T> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    write!(f, \"{}{}{}\", self.enclosure, self.value, self.enclosure)\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn tick() {\n    assert_eq!(Enclosure::tick(\"foo\").to_string(), \"`foo`\");\n  }\n}\n"
  },
  {
    "path": "src/error.rs",
    "content": "use super::*;\n\n#[derive(Debug)]\npub(crate) enum Error<'src> {\n  AmbiguousModuleFile {\n    module: Name<'src>,\n    found: Vec<PathBuf>,\n  },\n  ArgumentPatternMismatch {\n    argument: String,\n    parameter: &'src str,\n    pattern: Box<Pattern<'src>>,\n    recipe: &'src str,\n  },\n  Assert {\n    message: String,\n    name: Name<'src>,\n  },\n  Backtick {\n    token: Token<'src>,\n    output_error: OutputError,\n  },\n  ChooserInvoke {\n    shell_binary: String,\n    shell_arguments: String,\n    chooser: OsString,\n    io_error: io::Error,\n  },\n  ChooserRead {\n    chooser: OsString,\n    io_error: io::Error,\n  },\n  ChooserStatus {\n    chooser: OsString,\n    status: ExitStatus,\n  },\n  ChooserWrite {\n    chooser: OsString,\n    io_error: io::Error,\n  },\n  CircularImport {\n    current: PathBuf,\n    import: PathBuf,\n  },\n  Code {\n    recipe: &'src str,\n    line_number: Option<usize>,\n    code: i32,\n    print_message: bool,\n  },\n  CommandInvoke {\n    binary: OsString,\n    arguments: Vec<OsString>,\n    io_error: io::Error,\n  },\n  CommandStatus {\n    binary: OsString,\n    arguments: Vec<OsString>,\n    status: ExitStatus,\n  },\n  Compile {\n    compile_error: CompileError<'src>,\n  },\n  Config {\n    config_error: ConfigError,\n  },\n  Const {\n    const_error: ConstError<'src>,\n  },\n  Cygpath {\n    recipe: &'src str,\n    output_error: OutputError,\n  },\n  DefaultRecipeRequiresArguments {\n    recipe: &'src str,\n    min_arguments: usize,\n  },\n  Dotenv {\n    dotenv_error: dotenvy::Error,\n    path: PathBuf,\n  },\n  DotenvRequired,\n  DumpJson {\n    source: serde_json::Error,\n  },\n  DuplicateOption {\n    recipe: &'src str,\n    option: Switch,\n  },\n  EditorInvoke {\n    editor: OsString,\n    io_error: io::Error,\n  },\n  EditorStatus {\n    editor: OsString,\n    status: ExitStatus,\n  },\n  EvalUnknownVariable {\n    variable: String,\n    suggestion: Option<Suggestion<'src>>,\n  },\n  ExcessInvocations {\n    invocations: usize,\n  },\n  ExpectedSubmoduleButFoundRecipe {\n    path: String,\n  },\n  FilesystemIo {\n    io_error: io::Error,\n    path: PathBuf,\n  },\n  FlagWithValue {\n    recipe: &'src str,\n    option: Switch,\n  },\n  FormatCheckFoundDiff,\n  FunctionCall {\n    function: Name<'src>,\n    message: String,\n  },\n  GetConfirmation {\n    io_error: io::Error,\n  },\n  GuardCode {\n    recipe: &'src str,\n    line_number: usize,\n    code: i32,\n  },\n  Homedir,\n  InitExists {\n    justfile: PathBuf,\n  },\n  Internal {\n    message: String,\n  },\n  Interrupted {\n    signal: Signal,\n  },\n  Io {\n    recipe: &'src str,\n    io_error: io::Error,\n  },\n  Load {\n    path: PathBuf,\n    io_error: io::Error,\n  },\n  MissingImportFile {\n    path: Token<'src>,\n  },\n  MissingModuleFile {\n    module: Name<'src>,\n  },\n  MissingOption {\n    recipe: &'src str,\n    option: Switch,\n  },\n  MultipleShortOptions {\n    options: String,\n  },\n  NoChoosableRecipes,\n  NoDefaultRecipe,\n  NoRecipes,\n  NotConfirmed {\n    recipe: &'src str,\n  },\n  OptionMissingValue {\n    recipe: &'src str,\n    option: Switch,\n  },\n  PositionalArgumentCountMismatch {\n    recipe: Box<Recipe<'src>>,\n    found: usize,\n    min: usize,\n    max: usize,\n  },\n  RegexCompile {\n    source: regex::Error,\n  },\n  RuntimeDirIo {\n    io_error: io::Error,\n    path: PathBuf,\n  },\n  Script {\n    command: String,\n    io_error: io::Error,\n    recipe: &'src str,\n  },\n  Search {\n    search_error: SearchError,\n  },\n  Shebang {\n    argument: Option<String>,\n    command: String,\n    io_error: io::Error,\n    recipe: &'src str,\n  },\n  Signal {\n    recipe: &'src str,\n    line_number: Option<usize>,\n    signal: i32,\n  },\n  #[cfg(windows)]\n  SignalHandlerInstall {\n    source: ctrlc::Error,\n  },\n  #[cfg(unix)]\n  SignalHandlerPipeCloexec {\n    io_error: io::Error,\n  },\n  #[cfg(unix)]\n  SignalHandlerPipeOpen {\n    io_error: io::Error,\n  },\n  #[cfg(unix)]\n  SignalHandlerSigaction {\n    signal: Signal,\n    io_error: io::Error,\n  },\n  #[cfg(unix)]\n  SignalHandlerSpawnThread {\n    io_error: io::Error,\n  },\n  StdoutIo {\n    io_error: io::Error,\n  },\n  TempdirIo {\n    recipe: &'src str,\n    io_error: io::Error,\n  },\n  Unknown {\n    recipe: &'src str,\n    line_number: Option<usize>,\n  },\n  UnknownGroup {\n    group: String,\n  },\n  UnknownOption {\n    recipe: &'src str,\n    option: Switch,\n  },\n  UnknownOverrides {\n    overrides: Vec<String>,\n  },\n  UnknownRecipe {\n    recipe: String,\n    suggestion: Option<Suggestion<'src>>,\n  },\n  UnknownSubmodule {\n    path: String,\n  },\n  UnstableFeature {\n    unstable_feature: UnstableFeature,\n  },\n  WriteJustfile {\n    justfile: PathBuf,\n    io_error: io::Error,\n  },\n}\n\nimpl<'src> Error<'src> {\n  pub(crate) fn code(&self) -> Option<i32> {\n    match self {\n      Self::Backtick {\n        output_error: OutputError::Code(code),\n        ..\n      }\n      | Self::Code { code, .. } => Some(*code),\n\n      Self::ChooserStatus { status, .. } | Self::EditorStatus { status, .. } => status.code(),\n      Self::Backtick {\n        output_error: OutputError::Signal(signal),\n        ..\n      }\n      | Self::Signal { signal, .. } => 128i32.checked_add(*signal),\n      Self::Backtick {\n        output_error: OutputError::Interrupted(signal),\n        ..\n      }\n      | Self::Interrupted { signal } => Some(signal.code()),\n      _ => None,\n    }\n  }\n\n  fn context(&self) -> Option<Token<'src>> {\n    match self {\n      Self::AmbiguousModuleFile { module, .. } | Self::MissingModuleFile { module, .. } => {\n        Some(module.token)\n      }\n      Self::Assert { name, .. } => Some(**name),\n      Self::Backtick { token, .. } => Some(*token),\n      Self::Compile { compile_error } => Some(compile_error.context()),\n      Self::Const { const_error } => Some(const_error.context()),\n      Self::FunctionCall { function, .. } => Some(function.token),\n      Self::MissingImportFile { path } => Some(*path),\n      _ => None,\n    }\n  }\n\n  pub(crate) fn internal(message: impl Into<String>) -> Self {\n    Self::Internal {\n      message: message.into(),\n    }\n  }\n\n  pub(crate) fn print_message(&self) -> bool {\n    !matches!(\n      self,\n      Error::Code {\n        print_message: false,\n        ..\n      }\n    )\n  }\n\n  fn source(&self) -> Option<&dyn std::error::Error> {\n    match self {\n      Self::Compile { compile_error } => compile_error.source(),\n      _ => None,\n    }\n  }\n}\n\nimpl<'src> From<CompileError<'src>> for Error<'src> {\n  fn from(compile_error: CompileError<'src>) -> Self {\n    Self::Compile { compile_error }\n  }\n}\n\nimpl From<ConfigError> for Error<'_> {\n  fn from(config_error: ConfigError) -> Self {\n    Self::Config { config_error }\n  }\n}\n\nimpl<'src> From<ConstError<'src>> for Error<'src> {\n  fn from(const_error: ConstError<'src>) -> Self {\n    Self::Const { const_error }\n  }\n}\n\nimpl From<SearchError> for Error<'_> {\n  fn from(search_error: SearchError) -> Self {\n    Self::Search { search_error }\n  }\n}\n\nimpl ColorDisplay for Error<'_> {\n  fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {\n    use Error::*;\n\n    let error = color.error().paint(\"error\");\n    let message = color.message().prefix();\n    write!(f, \"{error}: {message}\")?;\n\n    match self {\n      AmbiguousModuleFile { module, found } => write!(\n        f,\n        \"Found multiple source files for module `{module}`: {}\",\n        List::and_ticked(found.iter().map(|path| path.display())),\n      )?,\n      ArgumentPatternMismatch {\n        argument,\n        parameter,\n        pattern,\n        recipe,\n      } => {\n        write!(\n          f,\n          \"Argument `{argument}` passed to recipe `{recipe}` parameter `{parameter}` does not match pattern '{}'\",\n          pattern.original(),\n        )?;\n      }\n      Assert { message, .. } => {\n        write!(f, \"Assert failed: {message}\")?;\n      }\n      Backtick { output_error, .. } => match output_error {\n        OutputError::Code(code) => write!(f, \"Backtick failed with exit code {code}\")?,\n        OutputError::Signal(signal) => write!(f, \"Backtick was terminated by signal {signal}\")?,\n        OutputError::Unknown => write!(f, \"Backtick failed for an unknown reason\")?,\n        OutputError::Interrupted(signal) => write!(\n          f,\n          \"Backtick succeeded but `just` was interrupted by signal {signal}\",\n        )?,\n        OutputError::Io(io_error) => match io_error.kind() {\n          io::ErrorKind::NotFound => write!(\n            f,\n            \"Backtick could not be run because just could not find the shell:\\n{io_error}\",\n          ),\n          io::ErrorKind::PermissionDenied => write!(\n            f,\n            \"Backtick could not be run because just could not run the shell:\\n{io_error}\",\n          ),\n          _ => write!(\n            f,\n            \"Backtick could not be run because of an IO error while launching the shell:\\n{io_error}\",\n          ),\n        }?,\n        OutputError::Utf8(utf8_error) => write!(\n          f,\n          \"Backtick succeeded but stdout was not utf8: {utf8_error}\",\n        )?,\n      },\n      ChooserInvoke {\n        shell_binary,\n        shell_arguments,\n        chooser,\n        io_error,\n      } => {\n        let chooser = chooser.to_string_lossy();\n        write!(\n          f,\n          \"Chooser `{shell_binary} {shell_arguments} {chooser}` invocation failed: {io_error}\",\n        )?;\n      }\n      ChooserRead { chooser, io_error } => {\n        let chooser = chooser.to_string_lossy();\n        write!(\n          f,\n          \"Failed to read output from chooser `{chooser}`: {io_error}\",\n        )?;\n      }\n      ChooserStatus { chooser, status } => {\n        let chooser = chooser.to_string_lossy();\n        write!(f, \"Chooser `{chooser}` failed: {status}\")?;\n      }\n      ChooserWrite { chooser, io_error } => {\n        let chooser = chooser.to_string_lossy();\n        write!(f, \"Failed to write to chooser `{chooser}`: {io_error}\")?;\n      }\n      CircularImport { current, import } => {\n        let import = import.display();\n        let current = current.display();\n        write!(f, \"Import `{import}` in `{current}` is circular\")?;\n      }\n      Code {\n        recipe,\n        line_number,\n        code,\n        ..\n      } => {\n        if let Some(n) = line_number {\n          write!(\n            f,\n            \"Recipe `{recipe}` failed on line {n} with exit code {code}\",\n          )?;\n        } else {\n          write!(f, \"Recipe `{recipe}` failed with exit code {code}\")?;\n        }\n      }\n      CommandInvoke {\n        binary,\n        arguments,\n        io_error,\n      } => {\n        let cmd = format_cmd(binary, arguments);\n        write!(f, \"Failed to invoke {cmd}: {io_error}\")?;\n      }\n      CommandStatus {\n        binary,\n        arguments,\n        status,\n      } => {\n        let cmd = format_cmd(binary, arguments);\n        write!(f, \"Command {cmd} failed: {status}\")?;\n      }\n      Compile { compile_error } => Display::fmt(compile_error, f)?,\n      Config { config_error } => Display::fmt(config_error, f)?,\n      Const { const_error } => write!(f, \"{const_error}\")?,\n      Cygpath {\n        recipe,\n        output_error,\n      } => match output_error {\n        OutputError::Code(code) => write!(\n          f,\n          \"Cygpath failed with exit code {code} while translating recipe `{recipe}` shebang interpreter path\",\n        )?,\n        OutputError::Signal(signal) => write!(\n          f,\n          \"Cygpath terminated by signal {signal} while translating recipe `{recipe}` shebang interpreter path\",\n        )?,\n        OutputError::Unknown => write!(\n          f,\n          \"Cygpath experienced an unknown failure while translating recipe `{recipe}` shebang interpreter path\",\n        )?,\n        OutputError::Interrupted(signal) => write!(\n          f,\n          \"Cygpath succeeded but `just` was interrupted by {signal}\",\n        )?,\n        OutputError::Io(io_error) => {\n          match io_error.kind() {\n            io::ErrorKind::NotFound => write!(\n              f,\n              \"Could not find `cygpath` executable to translate recipe `{recipe}` shebang interpreter path:\\n{io_error}\",\n            ),\n            io::ErrorKind::PermissionDenied => write!(\n              f,\n              \"Could not run `cygpath` executable to translate recipe `{recipe}` shebang interpreter path:\\n{io_error}\",\n            ),\n            _ => write!(f, \"Could not run `cygpath` executable:\\n{io_error}\"),\n          }?;\n        }\n        OutputError::Utf8(utf8_error) => write!(\n          f,\n          \"Cygpath successfully translated recipe `{recipe}` shebang interpreter path, but output was not utf8: {utf8_error}\",\n        )?,\n      },\n      DefaultRecipeRequiresArguments {\n        recipe,\n        min_arguments,\n      } => {\n        let count = Count(\"argument\", *min_arguments);\n        write!(\n          f,\n          \"Recipe `{recipe}` cannot be used as default recipe since it requires at least {min_arguments} {count}.\",\n        )?;\n      }\n      Dotenv { dotenv_error, path } => {\n        write!(\n          f,\n          \"Failed to load environment file from `{}`: {dotenv_error}\",\n          path.display(),\n        )?;\n      }\n      DotenvRequired => {\n        write!(f, \"Dotenv file not found\")?;\n      }\n      DumpJson { source } => {\n        write!(f, \"Failed to dump JSON to stdout: {source}\")?;\n      }\n      DuplicateOption { recipe, option } => {\n        write!(\n          f,\n          \"Recipe `{recipe}` option `{option}` cannot be passed more than once\",\n        )?;\n      }\n      EditorInvoke { editor, io_error } => {\n        let editor = editor.to_string_lossy();\n        write!(f, \"Editor `{editor}` invocation failed: {io_error}\")?;\n      }\n      EditorStatus { editor, status } => {\n        let editor = editor.to_string_lossy();\n        write!(f, \"Editor `{editor}` failed: {status}\")?;\n      }\n      EvalUnknownVariable {\n        variable,\n        suggestion,\n      } => {\n        write!(f, \"Justfile does not contain variable `{variable}`.\")?;\n        if let Some(suggestion) = suggestion {\n          write!(f, \"\\n{suggestion}\")?;\n        }\n      }\n      ExcessInvocations { invocations } => {\n        write!(\n          f,\n          \"Expected 1 command-line recipe invocation but found {invocations}.\",\n        )?;\n      }\n      ExpectedSubmoduleButFoundRecipe { path } => {\n        write!(f, \"Expected submodule at `{path}` but found recipe.\")?;\n      }\n      FilesystemIo { io_error, path } => {\n        write!(f, \"I/O error at `{}`: {io_error}\", path.display())?;\n      }\n      FlagWithValue { recipe, option } => {\n        write!(f, \"Recipe `{recipe}` flag `{option}` does not take value\",)?;\n      }\n      FormatCheckFoundDiff => {\n        write!(f, \"Formatted justfile differs from original.\")?;\n      }\n      FunctionCall { function, message } => {\n        let function = function.lexeme();\n        write!(f, \"Call to function `{function}` failed: {message}\")?;\n      }\n      GetConfirmation { io_error } => {\n        write!(f, \"Failed to read confirmation from stdin: {io_error}\")?;\n      }\n      GuardCode {\n        recipe,\n        line_number,\n        code,\n      } => {\n        write!(\n          f,\n          \"Guard line in recipe `{recipe}` on line {line_number} returned reserved exit code {code}\",\n        )?;\n      }\n      Homedir => {\n        write!(f, \"Failed to get homedir\")?;\n      }\n      InitExists { justfile } => {\n        write!(f, \"Justfile `{}` already exists\", justfile.display())?;\n      }\n      Internal { message } => {\n        write!(\n          f,\n          \"Internal runtime error, this may indicate a bug in just: {message} \\\n          consider filing an issue: https://github.com/casey/just/issues/new\",\n        )?;\n      }\n      Interrupted { signal } => {\n        write!(f, \"Interrupted by {signal}\")?;\n      }\n      Io { recipe, io_error } => {\n        match io_error.kind() {\n          io::ErrorKind::NotFound => write!(\n            f,\n            \"Recipe `{recipe}` could not be run because just could not find the shell: {io_error}\",\n          ),\n          io::ErrorKind::PermissionDenied => write!(\n            f,\n            \"Recipe `{recipe}` could not be run because just could not run the shell: {io_error}\",\n          ),\n          _ => write!(\n            f,\n            \"Recipe `{recipe}` could not be run because of an IO error while launching the shell: {io_error}\",\n          ),\n        }?;\n      }\n      Load { io_error, path } => {\n        write!(\n          f,\n          \"Failed to read justfile at `{}`: {io_error}\",\n          path.display()\n        )?;\n      }\n      MissingImportFile { .. } => write!(f, \"Could not find source file for import.\")?,\n      MissingModuleFile { module } => {\n        write!(f, \"Could not find source file for module `{module}`.\")?;\n      }\n      MissingOption { recipe, option } => {\n        write!(f, \"Recipe `{recipe}` requires option `{option}`\")?;\n      }\n      MultipleShortOptions { options } => {\n        write!(\n          f,\n          \"Passing multiple short options (`-{options}`) in one argument is not supported\"\n        )?;\n      }\n      NoChoosableRecipes => write!(f, \"Justfile contains no choosable recipes.\")?,\n      NoDefaultRecipe => write!(f, \"Justfile contains no default recipe.\")?,\n      NoRecipes => write!(f, \"Justfile contains no recipes.\")?,\n      NotConfirmed { recipe } => {\n        write!(f, \"Recipe `{recipe}` was not confirmed\")?;\n      }\n      OptionMissingValue { recipe, option } => {\n        write!(f, \"Recipe `{recipe}` option `{option}` missing value\")?;\n      }\n      PositionalArgumentCountMismatch {\n        recipe,\n        found,\n        min,\n        max,\n        ..\n      } => {\n        let count = Count(\"argument\", *found);\n        if min == max {\n          let expected = min;\n          let only = if expected < found { \"only \" } else { \"\" };\n          write!(\n            f,\n            \"Recipe `{}` got {found} positional {count} but {only}takes {expected}\",\n            recipe.name(),\n          )?;\n        } else if found < min {\n          write!(\n            f,\n            \"Recipe `{}` got {found} positional {count} but takes at least {min}\",\n            recipe.name(),\n          )?;\n        } else if found > max {\n          write!(\n            f,\n            \"Recipe `{}` got {found} positional {count} but takes at most {max}\",\n            recipe.name(),\n          )?;\n        }\n      }\n      RegexCompile { source } => write!(f, \"{source}\")?,\n      RuntimeDirIo { io_error, path } => {\n        write!(\n          f,\n          \"I/O error in runtime dir `{}`: {io_error}\",\n          path.display(),\n        )?;\n      }\n      Script {\n        command,\n        io_error,\n        recipe,\n      } => {\n        write!(\n          f,\n          \"Recipe `{recipe}` with command `{command}` execution error: {io_error}\",\n        )?;\n      }\n      Search { search_error } => Display::fmt(search_error, f)?,\n      Shebang {\n        recipe,\n        command,\n        argument,\n        io_error,\n      } => {\n        if let Some(argument) = argument {\n          write!(\n            f,\n            \"Recipe `{recipe}` with shebang `#!{command} {argument}` execution error: {io_error}\",\n          )?;\n        } else {\n          write!(\n            f,\n            \"Recipe `{recipe}` with shebang `#!{command}` execution error: {io_error}\",\n          )?;\n        }\n      }\n      Signal {\n        recipe,\n        line_number,\n        signal,\n      } => {\n        if let Some(n) = line_number {\n          write!(\n            f,\n            \"Recipe `{recipe}` was terminated on line {n} by signal {signal}\",\n          )?;\n        } else {\n          write!(f, \"Recipe `{recipe}` was terminated by signal {signal}\")?;\n        }\n      }\n      #[cfg(windows)]\n      SignalHandlerInstall { source } => {\n        write!(f, \"Could not install signal handler: {source}\")?;\n      }\n      #[cfg(unix)]\n      SignalHandlerPipeCloexec { io_error } => {\n        write!(\n          f,\n          \"I/O error setting O_CLOEXEC on signal handler pipe: {io_error}\",\n        )?;\n      }\n      #[cfg(unix)]\n      SignalHandlerPipeOpen { io_error } => {\n        write!(f, \"I/O error opening signal handler pipe: {io_error}\")?;\n      }\n      #[cfg(unix)]\n      SignalHandlerSigaction { io_error, signal } => {\n        write!(f, \"I/O error setting sigaction for {signal}: {io_error}\")?;\n      }\n      #[cfg(unix)]\n      SignalHandlerSpawnThread { io_error } => {\n        write!(\n          f,\n          \"I/O error spawning thread for signal handler: {io_error}\",\n        )?;\n      }\n      StdoutIo { io_error } => {\n        write!(f, \"I/O error writing to stdout: {io_error}\")?;\n      }\n      TempdirIo { recipe, io_error } => {\n        write!(\n          f,\n          \"Recipe `{recipe}` could not be run because of an IO error while trying to create a temporary \\\n          directory or write a file to that directory: {io_error}\",\n        )?;\n      }\n      Unknown {\n        recipe,\n        line_number,\n      } => {\n        if let Some(n) = line_number {\n          write!(\n            f,\n            \"Recipe `{recipe}` failed on line {n} for an unknown reason\",\n          )?;\n        } else {\n          write!(f, \"Recipe `{recipe}` failed for an unknown reason\")?;\n        }\n      }\n      UnknownOption { recipe, option } => {\n        write!(f, \"Recipe `{recipe}` does not have option `{option}`\")?;\n      }\n      UnknownOverrides { overrides } => {\n        let count = Count(\"Variable\", overrides.len());\n        let overrides = List::and_ticked(overrides);\n        write!(\n          f,\n          \"{count} {overrides} overridden on the command line but not present in justfile\",\n        )?;\n      }\n      UnknownGroup { group } => {\n        write!(f, \"Justfile does not contain group `{group}`\")?;\n      }\n      UnknownRecipe { recipe, suggestion } => {\n        write!(f, \"Justfile does not contain recipe `{recipe}`\")?;\n        if let Some(suggestion) = suggestion {\n          write!(f, \"\\n{suggestion}\")?;\n        }\n      }\n      UnknownSubmodule { path } => {\n        write!(f, \"Justfile does not contain submodule `{path}`\")?;\n      }\n      UnstableFeature { unstable_feature } => {\n        write!(\n          f,\n          \"{unstable_feature} Invoke `just` with `--unstable`, set the `JUST_UNSTABLE` environment variable, or add `set unstable` to your `justfile` to enable unstable features.\",\n        )?;\n      }\n      WriteJustfile { justfile, io_error } => {\n        let justfile = justfile.display();\n        write!(f, \"Failed to write justfile to `{justfile}`: {io_error}\")?;\n      }\n    }\n\n    write!(f, \"{}\", color.message().suffix())?;\n\n    if let PositionalArgumentCountMismatch { recipe, .. } = self {\n      writeln!(f)?;\n      let path = Modulepath::try_from([recipe.name()].as_slice()).unwrap();\n      write!(\n        f,\n        \"{}\",\n        Usage {\n          long: false,\n          path: &path,\n          recipe,\n        }\n        .color_display(color)\n      )?;\n    }\n\n    if let Some(token) = self.context() {\n      writeln!(f)?;\n      write!(f, \"{}\", token.color_display(color.error()))?;\n    }\n\n    if let Some(source) = self.source() {\n      writeln!(f)?;\n      write!(f, \"caused by: {source}\")?;\n    }\n\n    Ok(())\n  }\n}\n\nfn format_cmd(binary: &OsString, arguments: &Vec<OsString>) -> String {\n  iter::once(binary)\n    .chain(arguments)\n    .map(|value| Enclosure::tick(value.to_string_lossy()).to_string())\n    .collect::<Vec<String>>()\n    .join(\" \")\n}\n"
  },
  {
    "path": "src/evaluator.rs",
    "content": "use super::*;\n\npub(crate) struct Evaluator<'src: 'run, 'run> {\n  assignments: Option<&'run Table<'src, Assignment<'src>>>,\n  context: Option<ExecutionContext<'src, 'run>>,\n  env: BTreeMap<String, String>,\n  is_dependency: bool,\n  non_const_assignments: Table<'src, Name<'src>>,\n  overrides: &'run HashMap<Number, String>,\n  scope: Scope<'src, 'run>,\n}\n\nimpl<'src, 'run> Evaluator<'src, 'run> {\n  fn context(\n    &self,\n    const_error: ConstError<'src>,\n  ) -> Result<&ExecutionContext<'src, 'run>, ConstError<'src>> {\n    self.context.as_ref().ok_or(const_error)\n  }\n\n  pub(crate) fn evaluate_settings(\n    assignments: &'run Table<'src, Assignment<'src>>,\n    overrides: &'run HashMap<Number, String>,\n    scope: &'run Scope<'src, 'run>,\n    sets: Table<'src, Set<'src>>,\n  ) -> RunResult<'src, Settings> {\n    let mut evaluator = Self {\n      assignments: Some(assignments),\n      context: None,\n      env: BTreeMap::new(),\n      is_dependency: false,\n      non_const_assignments: Table::new(),\n      overrides,\n      scope: scope.child(),\n    };\n\n    for assignment in assignments.values() {\n      match evaluator.evaluate_assignment(assignment) {\n        Err(Error::Const { .. }) => evaluator.non_const_assignments.insert(assignment.name),\n        Err(err) => return Err(err),\n        Ok(_) => {}\n      }\n    }\n\n    evaluator.evaluate_sets(sets)\n  }\n\n  fn evaluate_sets(&mut self, sets: Table<'src, Set<'src>>) -> RunResult<'src, Settings> {\n    let mut settings = Settings::default();\n\n    for (_name, set) in sets {\n      match set.value {\n        Setting::AllowDuplicateRecipes(value) => {\n          settings.allow_duplicate_recipes = value;\n        }\n        Setting::AllowDuplicateVariables(value) => {\n          settings.allow_duplicate_variables = value;\n        }\n        Setting::DotenvFilename(value) => {\n          settings.dotenv_filename = Some(self.evaluate_expression(&value)?);\n        }\n        Setting::DotenvLoad(value) => {\n          settings.dotenv_load = value;\n        }\n        Setting::DotenvPath(value) => {\n          settings.dotenv_path = Some(self.evaluate_expression(&value)?.into());\n        }\n        Setting::DotenvOverride(value) => {\n          settings.dotenv_override = value;\n        }\n        Setting::DotenvRequired(value) => {\n          settings.dotenv_required = value;\n        }\n        Setting::Export(value) => {\n          settings.export = value;\n        }\n        Setting::Fallback(value) => {\n          settings.fallback = value;\n        }\n        Setting::Guards(guards) => {\n          settings.guards = guards;\n        }\n        Setting::IgnoreComments(value) => {\n          settings.ignore_comments = value;\n        }\n        Setting::Lazy(value) => {\n          settings.lazy = value;\n        }\n        Setting::NoExitMessage(value) => {\n          settings.no_exit_message = value;\n        }\n        Setting::PositionalArguments(value) => {\n          settings.positional_arguments = value;\n        }\n        Setting::Quiet(value) => {\n          settings.quiet = value;\n        }\n        Setting::ScriptInterpreter(value) => {\n          settings.script_interpreter = Some(self.evaluate_interpreter(&value)?);\n        }\n        Setting::Shell(value) => {\n          settings.shell = Some(self.evaluate_interpreter(&value)?);\n        }\n        Setting::Unstable(value) => {\n          settings.unstable = value;\n        }\n        Setting::WindowsPowerShell(value) => {\n          settings.windows_powershell = value;\n        }\n        Setting::WindowsShell(value) => {\n          settings.windows_shell = Some(self.evaluate_interpreter(&value)?);\n        }\n        Setting::Tempdir(value) => {\n          settings.tempdir = Some(self.evaluate_expression(&value)?);\n        }\n        Setting::WorkingDirectory(value) => {\n          settings.working_directory = Some(self.evaluate_expression(&value)?.into());\n        }\n      }\n    }\n\n    Ok(settings)\n  }\n\n  pub(crate) fn evaluate_interpreter(\n    &mut self,\n    interpreter: &Interpreter<Expression<'src>>,\n  ) -> RunResult<'src, Interpreter<String>> {\n    Ok(Interpreter {\n      command: self.evaluate_expression(&interpreter.command)?,\n      arguments: interpreter\n        .arguments\n        .iter()\n        .map(|argument| self.evaluate_expression(argument))\n        .collect::<RunResult<Vec<String>>>()?,\n    })\n  }\n\n  pub(crate) fn evaluate_assignments(\n    config: &'run Config,\n    dotenv: &'run BTreeMap<String, String>,\n    module: &'run Justfile<'src>,\n    overrides: &'run HashMap<Number, String>,\n    parent: &'run Scope<'src, 'run>,\n    search: &'run Search,\n    variable_references: Option<&HashSet<Number>>,\n  ) -> RunResult<'src, Scope<'src, 'run>>\n  where\n    'src: 'run,\n  {\n    let context = ExecutionContext {\n      config,\n      dotenv,\n      module,\n      search,\n    };\n\n    let mut evaluator = Self {\n      assignments: Some(&module.assignments),\n      context: Some(context),\n      env: BTreeMap::new(),\n      is_dependency: false,\n      non_const_assignments: Table::new(),\n      overrides,\n      scope: parent.child(),\n    };\n\n    for assignment in module.assignments.values() {\n      if assignment.eager\n        || assignment.export\n        || module.settings.export\n        || variable_references\n          .is_none_or(|variable_references| variable_references.contains(&assignment.number))\n      {\n        evaluator.evaluate_assignment(assignment)?;\n      }\n    }\n\n    Ok(evaluator.scope)\n  }\n\n  fn evaluate_assignment(&mut self, assignment: &Assignment<'src>) -> RunResult<'src, &str> {\n    let name = assignment.name.lexeme();\n\n    if !self.scope.bound(name) {\n      let value = if let Some(value) = self.overrides.get(&assignment.number) {\n        value.clone()\n      } else {\n        self.evaluate_expression(&assignment.value)?\n      };\n\n      self.scope.bind(Binding {\n        eager: assignment.eager,\n        export: assignment.export\n          || self\n            .context\n            .is_some_and(|context| context.module.settings.export),\n        file_depth: 0,\n        name: assignment.name,\n        number: assignment.number,\n        prelude: false,\n        private: assignment.private,\n        value,\n      });\n    }\n\n    Ok(self.scope.value(name).unwrap())\n  }\n\n  fn function_context(&self, thunk: &Thunk<'src>) -> RunResult<'src, function::Context> {\n    Ok(function::Context {\n      execution_context: self.context(ConstError::FunctionCall(thunk.name()))?,\n      is_dependency: self.is_dependency,\n      name: thunk.name(),\n      scope: &self.scope,\n    })\n  }\n\n  pub(crate) fn evaluate_expression(\n    &mut self,\n    expression: &Expression<'src>,\n  ) -> RunResult<'src, String> {\n    match expression {\n      Expression::And { lhs, rhs } => {\n        let lhs = self.evaluate_expression(lhs)?;\n        if lhs.is_empty() {\n          return Ok(String::new());\n        }\n        self.evaluate_expression(rhs)\n      }\n      Expression::Assert {\n        condition,\n        error,\n        name,\n      } => {\n        if self.evaluate_condition(condition)? {\n          Ok(String::new())\n        } else {\n          Err(Error::Assert {\n            message: self.evaluate_expression(error)?,\n            name: *name,\n          })\n        }\n      }\n      Expression::Backtick { contents, token } => {\n        let context = self.context(ConstError::Backtick(*token))?;\n\n        if context.config.dry_run {\n          return Ok(format!(\"`{contents}`\"));\n        }\n\n        Self::run_command(context, &self.env, &self.scope, contents, &[]).map_err(|output_error| {\n          Error::Backtick {\n            token: *token,\n            output_error,\n          }\n        })\n      }\n      Expression::Call { thunk } => {\n        use Thunk::*;\n        match thunk {\n          Nullary { function, .. } => function(self.function_context(thunk)?),\n          Unary { function, arg, .. } => {\n            let arg = self.evaluate_expression(arg)?;\n            function(self.function_context(thunk)?, &arg)\n          }\n          UnaryOpt {\n            function,\n            args: (a, b),\n            ..\n          } => {\n            let a = self.evaluate_expression(a)?;\n            let b = match b.as_ref() {\n              Some(b) => Some(self.evaluate_expression(b)?),\n              None => None,\n            };\n            function(self.function_context(thunk)?, &a, b.as_deref())\n          }\n          UnaryPlus {\n            function,\n            args: (a, rest),\n            ..\n          } => {\n            let a = self.evaluate_expression(a)?;\n            let mut rest_evaluated = Vec::new();\n            for arg in rest {\n              rest_evaluated.push(self.evaluate_expression(arg)?);\n            }\n            function(self.function_context(thunk)?, &a, &rest_evaluated)\n          }\n          Binary {\n            function,\n            args: [a, b],\n            ..\n          } => {\n            let a = self.evaluate_expression(a)?;\n            let b = self.evaluate_expression(b)?;\n            function(self.function_context(thunk)?, &a, &b)\n          }\n          BinaryPlus {\n            function,\n            args: ([a, b], rest),\n            ..\n          } => {\n            let a = self.evaluate_expression(a)?;\n            let b = self.evaluate_expression(b)?;\n            let mut rest_evaluated = Vec::new();\n            for arg in rest {\n              rest_evaluated.push(self.evaluate_expression(arg)?);\n            }\n            function(self.function_context(thunk)?, &a, &b, &rest_evaluated)\n          }\n          Ternary {\n            function,\n            args: [a, b, c],\n            ..\n          } => {\n            let a = self.evaluate_expression(a)?;\n            let b = self.evaluate_expression(b)?;\n            let c = self.evaluate_expression(c)?;\n            function(self.function_context(thunk)?, &a, &b, &c)\n          }\n        }\n        .map_err(|message| Error::FunctionCall {\n          function: thunk.name(),\n          message,\n        })\n      }\n      Expression::Concatenation { lhs, rhs } => {\n        let lhs = self.evaluate_expression(lhs)?;\n        let rhs = self.evaluate_expression(rhs)?;\n        Ok(lhs + &rhs)\n      }\n      Expression::Conditional {\n        condition,\n        then,\n        otherwise,\n      } => {\n        if self.evaluate_condition(condition)? {\n          self.evaluate_expression(then)\n        } else {\n          self.evaluate_expression(otherwise)\n        }\n      }\n      Expression::FormatString { start, expressions } => {\n        let mut value = start.cooked.clone();\n\n        for (expression, string) in expressions {\n          value.push_str(&self.evaluate_expression(expression)?);\n          value.push_str(&string.cooked);\n        }\n\n        if start.kind.indented {\n          Ok(unindent(&value))\n        } else {\n          Ok(value)\n        }\n      }\n      Expression::Group { contents } => self.evaluate_expression(contents),\n      Expression::Join { lhs: None, rhs } => Ok(\"/\".to_string() + &self.evaluate_expression(rhs)?),\n      Expression::Join {\n        lhs: Some(lhs),\n        rhs,\n      } => {\n        let lhs = self.evaluate_expression(lhs)?;\n        let rhs = self.evaluate_expression(rhs)?;\n        Ok(lhs + \"/\" + &rhs)\n      }\n      Expression::Or { lhs, rhs } => {\n        let lhs = self.evaluate_expression(lhs)?;\n        if !lhs.is_empty() {\n          return Ok(lhs);\n        }\n        self.evaluate_expression(rhs)\n      }\n      Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()),\n      Expression::Variable { name, .. } => {\n        let variable = name.lexeme();\n        if let Some(value) = self.scope.value(variable) {\n          Ok(value.to_owned())\n        } else if self.non_const_assignments.contains_key(name.lexeme()) {\n          Err(ConstError::Variable(*name).into())\n        } else if let Some(assignment) = self\n          .assignments\n          .and_then(|assignments| assignments.get(variable))\n        {\n          Ok(self.evaluate_assignment(assignment)?.to_owned())\n        } else {\n          Err(Error::internal(format!(\n            \"attempted to evaluate undefined variable `{variable}`\"\n          )))\n        }\n      }\n    }\n  }\n\n  fn evaluate_condition(&mut self, condition: &Condition<'src>) -> RunResult<'src, bool> {\n    let lhs_value = self.evaluate_expression(&condition.lhs)?;\n    let rhs_value = self.evaluate_expression(&condition.rhs)?;\n    let condition = match condition.operator {\n      ConditionalOperator::Equality => lhs_value == rhs_value,\n      ConditionalOperator::Inequality => lhs_value != rhs_value,\n      ConditionalOperator::RegexMatch => Regex::new(&rhs_value)\n        .map_err(|source| Error::RegexCompile { source })?\n        .is_match(&lhs_value),\n      ConditionalOperator::RegexMismatch => !Regex::new(&rhs_value)\n        .map_err(|source| Error::RegexCompile { source })?\n        .is_match(&lhs_value),\n    };\n    Ok(condition)\n  }\n\n  pub(crate) fn run_command(\n    context: &ExecutionContext,\n    env: &BTreeMap<String, String>,\n    scope: &Scope,\n    command: &str,\n    args: &[&str],\n  ) -> Result<String, OutputError> {\n    let mut cmd = context.module.settings.shell_command(context.config);\n\n    cmd\n      .arg(command)\n      .args(args)\n      .current_dir(context.working_directory())\n      .export(\n        &context.module.settings,\n        context.dotenv,\n        scope,\n        &context.module.unexports,\n      )\n      .stdin(Stdio::inherit())\n      .stderr(if context.config.verbosity.quiet() {\n        Stdio::null()\n      } else {\n        Stdio::inherit()\n      })\n      .stdout(Stdio::piped());\n\n    for (key, value) in env {\n      cmd.env(key, value);\n    }\n\n    cmd.output_guard_stdout()\n  }\n\n  pub(crate) fn evaluate_line(\n    &mut self,\n    line: &Line<'src>,\n    continued: bool,\n  ) -> RunResult<'src, String> {\n    let mut evaluated = String::new();\n    for (i, fragment) in line.fragments.iter().enumerate() {\n      match fragment {\n        Fragment::Text { token } => {\n          let lexeme = token\n            .lexeme()\n            .replace(Lexer::INTERPOLATION_ESCAPE, Lexer::INTERPOLATION_START);\n\n          if i == 0 && continued {\n            evaluated += lexeme.trim_start();\n          } else {\n            evaluated += &lexeme;\n          }\n        }\n        Fragment::Interpolation { expression } => {\n          evaluated += &self.evaluate_expression(expression)?;\n        }\n      }\n    }\n    Ok(evaluated)\n  }\n\n  pub(crate) fn evaluate_parameters(\n    arguments: &[Vec<String>],\n    context: &ExecutionContext<'src, 'run>,\n    is_dependency: bool,\n    parameters: &[Parameter<'src>],\n    recipe: &Recipe<'src>,\n    scope: &'run Scope<'src, 'run>,\n  ) -> RunResult<'src, (Scope<'src, 'run>, Vec<String>)> {\n    let env = recipe\n      .attributes\n      .iter()\n      .filter_map(|attribute| {\n        if let Attribute::Env(key, value) = attribute {\n          Some((key.cooked.clone(), value.cooked.clone()))\n        } else {\n          None\n        }\n      })\n      .collect();\n\n    let mut evaluator = Self::new(context, env, is_dependency, scope);\n\n    let mut positional = Vec::new();\n\n    if arguments.len() != parameters.len() {\n      return Err(Error::internal(\"arguments do not match parameter count\"));\n    }\n\n    for (parameter, group) in parameters.iter().zip(arguments) {\n      let values = if group.is_empty() {\n        if let Some(ref default) = parameter.default {\n          let value = evaluator.evaluate_expression(default)?;\n          positional.push(value.clone());\n          vec![value]\n        } else if parameter.kind == ParameterKind::Star {\n          Vec::new()\n        } else {\n          return Err(Error::internal(\"missing parameter without default\"));\n        }\n      } else if parameter.kind.is_variadic() {\n        positional.extend_from_slice(group);\n        group.clone()\n      } else {\n        if group.len() != 1 {\n          return Err(Error::internal(\n            \"multiple values for non-variadic parameter\",\n          ));\n        }\n        let value = group[0].clone();\n        positional.push(value.clone());\n        vec![value]\n      };\n\n      for value in &values {\n        parameter.check_pattern_match(recipe, value)?;\n      }\n\n      evaluator.scope.bind(Binding {\n        eager: false,\n        export: parameter.export,\n        file_depth: 0,\n        name: parameter.name,\n        number: parameter.number,\n        prelude: false,\n        private: false,\n        value: values.join(\" \"),\n      });\n    }\n\n    Ok((evaluator.scope, positional))\n  }\n\n  pub(crate) fn new(\n    context: &ExecutionContext<'src, 'run>,\n    env: BTreeMap<String, String>,\n    is_dependency: bool,\n    scope: &'run Scope<'src, 'run>,\n  ) -> Self {\n    static OVERRIDES: LazyLock<HashMap<Number, String>> = LazyLock::new(HashMap::new);\n    Self {\n      assignments: None,\n      context: Some(*context),\n      env,\n      is_dependency,\n      non_const_assignments: Table::new(),\n      overrides: &OVERRIDES,\n      scope: scope.child(),\n    }\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  run_error! {\n    name: backtick_code,\n    src: \"\n      a:\n       echo {{`f() { return 100; }; f`}}\n    \",\n    args: [\"a\"],\n    error: Error::Backtick {\n      token,\n      output_error: OutputError::Code(code),\n    },\n    check: {\n      assert_eq!(code, 100);\n      assert_eq!(token.lexeme(), \"`f() { return 100; }; f`\");\n    }\n  }\n\n  run_error! {\n    name: export_assignment_backtick,\n    src: r#\"\n      export exported_variable := \"A\"\n      b := `echo $exported_variable`\n\n      recipe:\n        echo {{b}}\n    \"#,\n    args: [\"--quiet\", \"recipe\"],\n    error: Error::Backtick {\n        token,\n        output_error: OutputError::Code(_),\n    },\n    check: {\n      assert_eq!(token.lexeme(), \"`echo $exported_variable`\");\n    }\n  }\n}\n"
  },
  {
    "path": "src/execution_context.rs",
    "content": "use super::*;\n\n#[derive(Copy, Clone)]\npub(crate) struct ExecutionContext<'src: 'run, 'run> {\n  pub(crate) config: &'run Config,\n  pub(crate) dotenv: &'run BTreeMap<String, String>,\n  pub(crate) module: &'run Justfile<'src>,\n  pub(crate) search: &'run Search,\n}\n\nimpl<'src: 'run, 'run> ExecutionContext<'src, 'run> {\n  pub(crate) fn tempdir<D>(&self, recipe: &Recipe<'src, D>) -> RunResult<'src, TempDir> {\n    let mut tempdir_builder = tempfile::Builder::new();\n\n    tempdir_builder.prefix(\"just-\");\n\n    if let Some(tempdir) = &self.config.tempdir {\n      tempdir_builder.tempdir_in(self.search.working_directory.join(tempdir))\n    } else {\n      match &self.module.settings.tempdir {\n        Some(tempdir) => tempdir_builder.tempdir_in(self.search.working_directory.join(tempdir)),\n        None => {\n          if let Some(runtime_dir) = dirs::runtime_dir() {\n            let path = runtime_dir.join(\"just\");\n            fs::create_dir_all(&path).map_err(|io_error| Error::RuntimeDirIo {\n              io_error,\n              path: path.clone(),\n            })?;\n            tempdir_builder.tempdir_in(path)\n          } else {\n            tempdir_builder.tempdir()\n          }\n        }\n      }\n    }\n    .map_err(|error| Error::TempdirIo {\n      recipe: recipe.name(),\n      io_error: error,\n    })\n  }\n\n  pub(crate) fn working_directory(&self) -> PathBuf {\n    let base = if self.module.is_submodule() {\n      &self.module.working_directory\n    } else {\n      &self.search.working_directory\n    };\n\n    if let Some(setting) = &self.module.settings.working_directory {\n      base.join(setting)\n    } else {\n      base.into()\n    }\n  }\n}\n"
  },
  {
    "path": "src/executor.rs",
    "content": "use super::*;\n\npub(crate) enum Executor<'a> {\n  Command(Interpreter<String>),\n  Shebang(Shebang<'a>),\n}\n\nimpl Executor<'_> {\n  pub(crate) fn command<'src>(\n    &self,\n    config: &Config,\n    path: &Path,\n    recipe: &'src str,\n    working_directory: Option<&Path>,\n  ) -> RunResult<'src, Command> {\n    match self {\n      Self::Command(interpreter) => {\n        let mut command = Command::new(&interpreter.command);\n\n        if let Some(working_directory) = working_directory {\n          command.current_dir(working_directory);\n        }\n\n        for arg in &interpreter.arguments {\n          command.arg(arg);\n        }\n\n        command.arg(path);\n\n        Ok(command)\n      }\n      Self::Shebang(shebang) => {\n        // make script executable\n        Platform::set_execute_permission(path).map_err(|error| Error::TempdirIo {\n          recipe,\n          io_error: error,\n        })?;\n\n        // create command to run script\n        Platform::make_shebang_command(config, path, *shebang, working_directory).map_err(\n          |output_error| Error::Cygpath {\n            recipe,\n            output_error,\n          },\n        )\n      }\n    }\n  }\n\n  pub(crate) fn script_filename(&self, recipe: &str, extension: Option<&str>) -> String {\n    let extension = extension.unwrap_or_else(|| {\n      let interpreter = match self {\n        Self::Command(interpreter) => &interpreter.command,\n        Self::Shebang(shebang) => shebang.interpreter_filename(),\n      };\n\n      match interpreter {\n        \"cmd\" | \"cmd.exe\" => \".bat\",\n        \"powershell\" | \"powershell.exe\" | \"pwsh\" | \"pwsh.exe\" => \".ps1\",\n        _ => \"\",\n      }\n    });\n\n    format!(\"{recipe}{extension}\")\n  }\n\n  pub(crate) fn error<'src>(&self, io_error: io::Error, recipe: &'src str) -> Error<'src> {\n    match self {\n      Self::Command(Interpreter { command, arguments }) => {\n        let mut command = command.clone();\n\n        for arg in arguments {\n          command.push(' ');\n          command.push_str(arg);\n        }\n\n        Error::Script {\n          command,\n          io_error,\n          recipe,\n        }\n      }\n      Self::Shebang(shebang) => Error::Shebang {\n        argument: shebang.argument.map(String::from),\n        command: shebang.interpreter.to_owned(),\n        io_error,\n        recipe,\n      },\n    }\n  }\n\n  // Script text for `recipe` given evaluated `lines` including blanks so line\n  // numbers in errors from generated script match justfile source lines.\n  pub(crate) fn script<D>(&self, recipe: &Recipe<D>, lines: &[String]) -> String {\n    let mut script = String::new();\n    let mut n = 0;\n    let shebangs = recipe\n      .body\n      .iter()\n      .take_while(|line| line.is_shebang())\n      .count();\n\n    if let Self::Shebang(shebang) = self {\n      for shebang_line in &lines[..shebangs] {\n        if shebang.include_shebang_line() {\n          script.push_str(shebang_line);\n        }\n        script.push('\\n');\n        n += 1;\n      }\n    }\n\n    for (line, text) in recipe.body.iter().zip(lines).skip(n) {\n      while n < line.number {\n        script.push('\\n');\n        n += 1;\n      }\n\n      script.push_str(text);\n      script.push('\\n');\n      n += 1;\n    }\n\n    script\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn shebang_script_filename() {\n    #[track_caller]\n    fn case(interpreter: &str, recipe: &str, extension: Option<&str>, expected: &str) {\n      assert_eq!(\n        Executor::Shebang(Shebang::new(&format!(\"#!{interpreter}\")).unwrap())\n          .script_filename(recipe, extension),\n        expected\n      );\n      assert_eq!(\n        Executor::Command(Interpreter {\n          command: interpreter.into(),\n          arguments: Vec::new()\n        })\n        .script_filename(recipe, extension),\n        expected\n      );\n    }\n\n    case(\"bar\", \"foo\", Some(\".sh\"), \"foo.sh\");\n    case(\"pwsh.exe\", \"foo\", Some(\".sh\"), \"foo.sh\");\n    case(\"cmd.exe\", \"foo\", Some(\".sh\"), \"foo.sh\");\n    case(\"powershell\", \"foo\", None, \"foo.ps1\");\n    case(\"pwsh\", \"foo\", None, \"foo.ps1\");\n    case(\"powershell.exe\", \"foo\", None, \"foo.ps1\");\n    case(\"pwsh.exe\", \"foo\", None, \"foo.ps1\");\n    case(\"cmd\", \"foo\", None, \"foo.bat\");\n    case(\"cmd.exe\", \"foo\", None, \"foo.bat\");\n    case(\"bar\", \"foo\", None, \"foo\");\n  }\n}\n"
  },
  {
    "path": "src/expression.rs",
    "content": "use super::*;\n\n/// An expression. Note that the Just language grammar has both an `expression`\n/// production of additions (`a + b`) and values, and a `value` production of\n/// all other value types (for example strings, function calls, and\n/// parenthetical groups).\n///\n/// The parser parses both values and expressions into `Expression`s.\n#[derive(PartialEq, Debug, Clone)]\npub(crate) enum Expression<'src> {\n  /// `lhs && rhs`\n  And { lhs: Box<Self>, rhs: Box<Self> },\n  /// `assert(condition, error)`\n  Assert {\n    name: Name<'src>,\n    condition: Condition<'src>,\n    error: Box<Self>,\n  },\n  /// `contents`\n  Backtick {\n    contents: String,\n    token: Token<'src>,\n  },\n  /// `name(arguments)`\n  Call { thunk: Thunk<'src> },\n  /// `lhs + rhs`\n  Concatenation { lhs: Box<Self>, rhs: Box<Self> },\n  /// `if condition { then } else { otherwise }`\n  Conditional {\n    condition: Condition<'src>,\n    then: Box<Self>,\n    otherwise: Box<Self>,\n  },\n  // `f\"format string\"`\n  FormatString {\n    start: StringLiteral<'src>,\n    expressions: Vec<(Self, StringLiteral<'src>)>,\n  },\n  /// `(contents)`\n  Group { contents: Box<Self> },\n  /// `lhs / rhs`\n  Join {\n    lhs: Option<Box<Self>>,\n    rhs: Box<Self>,\n  },\n  /// `lhs || rhs`\n  Or { lhs: Box<Self>, rhs: Box<Self> },\n  /// `\"string_literal\"` or `'string_literal'`\n  StringLiteral { string_literal: StringLiteral<'src> },\n  /// `variable`\n  Variable { name: Name<'src> },\n}\n\nimpl<'src> Expression<'src> {\n  pub(crate) fn variables<'expression>(&'expression self) -> Variables<'expression, 'src> {\n    Variables::new(self)\n  }\n}\n\nimpl Display for Expression<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    match self {\n      Self::And { lhs, rhs } => write!(f, \"{lhs} && {rhs}\"),\n      Self::Assert {\n        condition, error, ..\n      } => write!(f, \"assert({condition}, {error})\"),\n      Self::Backtick { token, .. } => write!(f, \"{}\", token.lexeme()),\n      Self::Call { thunk } => write!(f, \"{thunk}\"),\n      Self::Concatenation { lhs, rhs } => write!(f, \"{lhs} + {rhs}\"),\n      Self::Conditional {\n        condition,\n        then,\n        otherwise,\n      } => {\n        if let Self::Conditional { .. } = **otherwise {\n          write!(f, \"if {condition} {{ {then} }} else {otherwise}\")\n        } else {\n          write!(f, \"if {condition} {{ {then} }} else {{ {otherwise} }}\")\n        }\n      }\n      Self::FormatString { start, expressions } => {\n        write!(f, \"{start}\")?;\n\n        for (expression, string) in expressions {\n          write!(f, \"{expression}{string}\")?;\n        }\n\n        Ok(())\n      }\n      Self::Group { contents } => write!(f, \"({contents})\"),\n      Self::Join { lhs: None, rhs } => write!(f, \"/ {rhs}\"),\n      Self::Join {\n        lhs: Some(lhs),\n        rhs,\n      } => write!(f, \"{lhs} / {rhs}\"),\n      Self::Or { lhs, rhs } => write!(f, \"{lhs} || {rhs}\"),\n      Self::StringLiteral { string_literal } => write!(f, \"{string_literal}\"),\n      Self::Variable { name } => write!(f, \"{}\", name.lexeme()),\n    }\n  }\n}\n\nimpl Serialize for Expression<'_> {\n  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n  where\n    S: Serializer,\n  {\n    match self {\n      Self::And { lhs, rhs } => {\n        let mut seq = serializer.serialize_seq(None)?;\n        seq.serialize_element(\"and\")?;\n        seq.serialize_element(lhs)?;\n        seq.serialize_element(rhs)?;\n        seq.end()\n      }\n      Self::Assert {\n        condition, error, ..\n      } => {\n        let mut seq: <S as Serializer>::SerializeSeq = serializer.serialize_seq(None)?;\n        seq.serialize_element(\"assert\")?;\n        seq.serialize_element(condition)?;\n        seq.serialize_element(error)?;\n        seq.end()\n      }\n      Self::Backtick { contents, .. } => {\n        let mut seq = serializer.serialize_seq(None)?;\n        seq.serialize_element(\"evaluate\")?;\n        seq.serialize_element(contents)?;\n        seq.end()\n      }\n      Self::Call { thunk } => thunk.serialize(serializer),\n      Self::Concatenation { lhs, rhs } => {\n        let mut seq = serializer.serialize_seq(None)?;\n        seq.serialize_element(\"concatenate\")?;\n        seq.serialize_element(lhs)?;\n        seq.serialize_element(rhs)?;\n        seq.end()\n      }\n      Self::Conditional {\n        condition,\n        then,\n        otherwise,\n      } => {\n        let mut seq = serializer.serialize_seq(None)?;\n        seq.serialize_element(\"if\")?;\n        seq.serialize_element(condition)?;\n        seq.serialize_element(then)?;\n        seq.serialize_element(otherwise)?;\n        seq.end()\n      }\n      Self::FormatString { start, expressions } => {\n        let mut seq = serializer.serialize_seq(None)?;\n        seq.serialize_element(\"format\")?;\n        seq.serialize_element(start)?;\n        for (expression, string) in expressions {\n          seq.serialize_element(expression)?;\n          seq.serialize_element(string)?;\n        }\n        seq.end()\n      }\n      Self::Group { contents } => contents.serialize(serializer),\n      Self::Join { lhs, rhs } => {\n        let mut seq = serializer.serialize_seq(None)?;\n        seq.serialize_element(\"join\")?;\n        seq.serialize_element(lhs)?;\n        seq.serialize_element(rhs)?;\n        seq.end()\n      }\n      Self::Or { lhs, rhs } => {\n        let mut seq = serializer.serialize_seq(None)?;\n        seq.serialize_element(\"or\")?;\n        seq.serialize_element(lhs)?;\n        seq.serialize_element(rhs)?;\n        seq.end()\n      }\n      Self::StringLiteral { string_literal } => string_literal.serialize(serializer),\n      Self::Variable { name } => {\n        let mut seq = serializer.serialize_seq(None)?;\n        seq.serialize_element(\"variable\")?;\n        seq.serialize_element(name)?;\n        seq.end()\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/filesystem.rs",
    "content": "use super::*;\n\npub(crate) fn is_file(path: &Path) -> RunResult<'static, bool> {\n  match path.metadata() {\n    Ok(metadata) => Ok(metadata.is_file()),\n    Err(io_error) => {\n      if io_error.kind() == io::ErrorKind::NotFound {\n        Ok(false)\n      } else {\n        Err(Error::FilesystemIo {\n          path: path.into(),\n          io_error,\n        })\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/format_string_part.rs",
    "content": "#[derive(PartialEq, Debug, Clone, Ord, Eq, PartialOrd)]\npub(crate) enum FormatStringPart {\n  Continue,\n  End,\n  Single,\n  Start,\n}\n"
  },
  {
    "path": "src/fragment.rs",
    "content": "use super::*;\n\n/// A line fragment consisting either of…\n#[derive(PartialEq, Debug, Clone)]\npub(crate) enum Fragment<'src> {\n  /// …an interpolation containing `expression`.\n  Interpolation { expression: Expression<'src> },\n  /// …raw text…\n  Text { token: Token<'src> },\n}\n\nimpl Serialize for Fragment<'_> {\n  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n  where\n    S: Serializer,\n  {\n    match self {\n      Self::Text { token } => serializer.serialize_str(token.lexeme()),\n      Self::Interpolation { expression } => {\n        let mut seq = serializer.serialize_seq(None)?;\n        seq.serialize_element(expression)?;\n        seq.end()\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/function.rs",
    "content": "use {\n  super::*,\n  Function::*,\n  heck::{\n    ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase,\n    ToUpperCamelCase,\n  },\n  semver::{Version, VersionReq},\n  std::collections::HashSet,\n};\n\n#[allow(clippy::arbitrary_source_item_ordering)]\npub(crate) enum Function {\n  Nullary(fn(Context) -> FunctionResult),\n  Unary(fn(Context, &str) -> FunctionResult),\n  UnaryOpt(fn(Context, &str, Option<&str>) -> FunctionResult),\n  UnaryPlus(fn(Context, &str, &[String]) -> FunctionResult),\n  Binary(fn(Context, &str, &str) -> FunctionResult),\n  BinaryPlus(fn(Context, &str, &str, &[String]) -> FunctionResult),\n  Ternary(fn(Context, &str, &str, &str) -> FunctionResult),\n}\n\npub(crate) struct Context<'src: 'run, 'run> {\n  pub(crate) execution_context: &'run ExecutionContext<'src, 'run>,\n  pub(crate) is_dependency: bool,\n  pub(crate) name: Name<'src>,\n  pub(crate) scope: &'run Scope<'src, 'run>,\n}\n\npub(crate) fn get(name: &str) -> Option<Function> {\n  let name = if let Some(prefix) = name.strip_suffix(\"_dir\") {\n    format!(\"{prefix}_directory\")\n  } else if let Some(prefix) = name.strip_suffix(\"_dir_native\") {\n    format!(\"{prefix}_directory_native\")\n  } else {\n    name.into()\n  };\n\n  let function = match name.as_str() {\n    \"absolute_path\" => Unary(absolute_path),\n    \"append\" => Binary(append),\n    \"arch\" => Nullary(arch),\n    \"blake3\" => Unary(blake3),\n    \"blake3_file\" => Unary(blake3_file),\n    \"cache_directory\" => Nullary(|_| dir(\"cache\", dirs::cache_dir)),\n    \"canonicalize\" => Unary(canonicalize),\n    \"capitalize\" => Unary(capitalize),\n    \"choose\" => Binary(choose),\n    \"clean\" => Unary(clean),\n    \"config_directory\" => Nullary(|_| dir(\"config\", dirs::config_dir)),\n    \"config_local_directory\" => Nullary(|_| dir(\"local config\", dirs::config_local_dir)),\n    \"data_directory\" => Nullary(|_| dir(\"data\", dirs::data_dir)),\n    \"data_local_directory\" => Nullary(|_| dir(\"local data\", dirs::data_local_dir)),\n    \"datetime\" => Unary(datetime),\n    \"datetime_utc\" => Unary(datetime_utc),\n    \"encode_uri_component\" => Unary(encode_uri_component),\n    \"env\" => UnaryOpt(env),\n    \"env_var\" => Unary(env_var),\n    \"env_var_or_default\" => Binary(env_var_or_default),\n    \"error\" => Unary(error),\n    \"executable_directory\" => Nullary(|_| dir(\"executable\", dirs::executable_dir)),\n    \"extension\" => Unary(extension),\n    \"file_name\" => Unary(file_name),\n    \"file_stem\" => Unary(file_stem),\n    \"home_directory\" => Nullary(|_| dir(\"home\", dirs::home_dir)),\n    \"invocation_directory\" => Nullary(invocation_directory),\n    \"invocation_directory_native\" => Nullary(invocation_directory_native),\n    \"is_dependency\" => Nullary(is_dependency),\n    \"join\" => BinaryPlus(join),\n    \"just_executable\" => Nullary(just_executable),\n    \"just_pid\" => Nullary(just_pid),\n    \"justfile\" => Nullary(justfile),\n    \"justfile_directory\" => Nullary(justfile_directory),\n    \"kebabcase\" => Unary(kebabcase),\n    \"lowercamelcase\" => Unary(lowercamelcase),\n    \"lowercase\" => Unary(lowercase),\n    \"module_directory\" => Nullary(module_directory),\n    \"module_file\" => Nullary(module_file),\n    \"num_cpus\" => Nullary(num_cpus),\n    \"os\" => Nullary(os),\n    \"os_family\" => Nullary(os_family),\n    \"parent_directory\" => Unary(parent_directory),\n    \"path_exists\" => Unary(path_exists),\n    \"prepend\" => Binary(prepend),\n    \"quote\" => Unary(quote),\n    \"read\" => Unary(read),\n    \"replace\" => Ternary(replace),\n    \"replace_regex\" => Ternary(replace_regex),\n    \"require\" => Unary(require),\n    \"semver_matches\" => Binary(semver_matches),\n    \"sha256\" => Unary(sha256),\n    \"sha256_file\" => Unary(sha256_file),\n    \"shell\" => UnaryPlus(shell),\n    \"shoutykebabcase\" => Unary(shoutykebabcase),\n    \"shoutysnakecase\" => Unary(shoutysnakecase),\n    \"snakecase\" => Unary(snakecase),\n    \"source_directory\" => Nullary(source_directory),\n    \"source_file\" => Nullary(source_file),\n    \"style\" => Unary(style),\n    \"titlecase\" => Unary(titlecase),\n    \"trim\" => Unary(trim),\n    \"trim_end\" => Unary(trim_end),\n    \"trim_end_match\" => Binary(trim_end_match),\n    \"trim_end_matches\" => Binary(trim_end_matches),\n    \"trim_start\" => Unary(trim_start),\n    \"trim_start_match\" => Binary(trim_start_match),\n    \"trim_start_matches\" => Binary(trim_start_matches),\n    \"uppercamelcase\" => Unary(uppercamelcase),\n    \"uppercase\" => Unary(uppercase),\n    \"uuid\" => Nullary(uuid),\n    \"which\" => Unary(which),\n    \"without_extension\" => Unary(without_extension),\n    _ => return None,\n  };\n  Some(function)\n}\n\nimpl Function {\n  pub(crate) fn argc(&self) -> RangeInclusive<usize> {\n    match *self {\n      Nullary(_) => 0..=0,\n      Unary(_) => 1..=1,\n      UnaryOpt(_) => 1..=2,\n      UnaryPlus(_) => 1..=usize::MAX,\n      Binary(_) => 2..=2,\n      BinaryPlus(_) => 2..=usize::MAX,\n      Ternary(_) => 3..=3,\n    }\n  }\n}\n\nfn absolute_path(context: Context, path: &str) -> FunctionResult {\n  let abs_path_unchecked = context\n    .execution_context\n    .working_directory()\n    .join(path)\n    .lexiclean();\n  match abs_path_unchecked.to_str() {\n    Some(absolute_path) => Ok(absolute_path.to_owned()),\n    None => Err(format!(\n      \"Working directory is not valid unicode: {}\",\n      context.execution_context.search.working_directory.display()\n    )),\n  }\n}\n\nfn append(_context: Context, suffix: &str, s: &str) -> FunctionResult {\n  Ok(\n    s.split_whitespace()\n      .map(|s| format!(\"{s}{suffix}\"))\n      .collect::<Vec<String>>()\n      .join(\" \"),\n  )\n}\n\nfn arch(_context: Context) -> FunctionResult {\n  Ok(target::arch().to_owned())\n}\n\nfn blake3(_context: Context, s: &str) -> FunctionResult {\n  Ok(blake3::hash(s.as_bytes()).to_string())\n}\n\nfn blake3_file(context: Context, path: &str) -> FunctionResult {\n  let path = context.execution_context.working_directory().join(path);\n  let mut hasher = blake3::Hasher::new();\n  hasher\n    .update_mmap_rayon(&path)\n    .map_err(|err| format!(\"Failed to hash `{}`: {err}\", path.display()))?;\n  Ok(hasher.finalize().to_string())\n}\n\nfn canonicalize(context: Context, path: &str) -> FunctionResult {\n  let canonical = std::fs::canonicalize(context.execution_context.working_directory().join(path))\n    .map_err(|err| format!(\"I/O error canonicalizing path: {err}\"))?;\n\n  canonical.to_str().map(str::to_string).ok_or_else(|| {\n    format!(\n      \"Canonical path is not valid unicode: {}\",\n      canonical.display(),\n    )\n  })\n}\n\nfn capitalize(_context: Context, s: &str) -> FunctionResult {\n  let mut capitalized = String::new();\n  for (i, c) in s.chars().enumerate() {\n    if i == 0 {\n      capitalized.extend(c.to_uppercase());\n    } else {\n      capitalized.extend(c.to_lowercase());\n    }\n  }\n  Ok(capitalized)\n}\n\nfn choose(_context: Context, n: &str, alphabet: &str) -> FunctionResult {\n  let mut chars = HashSet::<char>::with_capacity(alphabet.len());\n\n  for c in alphabet.chars() {\n    if !chars.insert(c) {\n      return Err(format!(\"alphabet contains repeated character `{c}`\"));\n    }\n  }\n\n  let alphabet = alphabet.chars().collect::<Vec<char>>();\n\n  let n = n\n    .parse::<usize>()\n    .map_err(|err| format!(\"failed to parse `{n}` as positive integer: {err}\"))?;\n\n  let mut rng = rand::rng();\n\n  (0..n)\n    .map(|_| {\n      alphabet\n        .choose(&mut rng)\n        .ok_or_else(|| \"empty alphabet\".to_string())\n    })\n    .collect()\n}\n\nfn clean(_context: Context, path: &str) -> FunctionResult {\n  Ok(Path::new(path).lexiclean().to_str().unwrap().to_owned())\n}\n\nfn dir(name: &'static str, f: fn() -> Option<PathBuf>) -> FunctionResult {\n  match f() {\n    Some(path) => path\n      .as_os_str()\n      .to_str()\n      .map(str::to_string)\n      .ok_or_else(|| {\n        format!(\n          \"unable to convert {name} directory path to string: {}\",\n          path.display(),\n        )\n      }),\n    None => Err(format!(\"{name} directory not found\")),\n  }\n}\n\nfn datetime(_context: Context, format: &str) -> FunctionResult {\n  Ok(chrono::Local::now().format(format).to_string())\n}\n\nfn datetime_utc(_context: Context, format: &str) -> FunctionResult {\n  Ok(chrono::Utc::now().format(format).to_string())\n}\n\nfn encode_uri_component(_context: Context, s: &str) -> FunctionResult {\n  static PERCENT_ENCODE: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC\n    .remove(b'-')\n    .remove(b'_')\n    .remove(b'.')\n    .remove(b'!')\n    .remove(b'~')\n    .remove(b'*')\n    .remove(b'\\'')\n    .remove(b'(')\n    .remove(b')');\n  Ok(percent_encoding::utf8_percent_encode(s, &PERCENT_ENCODE).to_string())\n}\n\nfn env(context: Context, key: &str, default: Option<&str>) -> FunctionResult {\n  match default {\n    Some(val) => env_var_or_default(context, key, val),\n    None => env_var(context, key),\n  }\n}\n\nfn env_var(context: Context, key: &str) -> FunctionResult {\n  use std::env::VarError::*;\n\n  if let Some(value) = context.execution_context.dotenv.get(key) {\n    return Ok(value.clone());\n  }\n\n  match env::var(key) {\n    Err(NotPresent) => Err(format!(\"environment variable `{key}` not present\")),\n    Err(NotUnicode(os_string)) => Err(format!(\n      \"environment variable `{key}` not unicode: {os_string:?}\"\n    )),\n    Ok(value) => Ok(value),\n  }\n}\n\nfn env_var_or_default(context: Context, key: &str, default: &str) -> FunctionResult {\n  use std::env::VarError::*;\n\n  if let Some(value) = context.execution_context.dotenv.get(key) {\n    return Ok(value.clone());\n  }\n\n  match env::var(key) {\n    Err(NotPresent) => Ok(default.to_owned()),\n    Err(NotUnicode(os_string)) => Err(format!(\n      \"environment variable `{key}` not unicode: {os_string:?}\"\n    )),\n    Ok(value) => Ok(value),\n  }\n}\n\nfn error(_context: Context, message: &str) -> FunctionResult {\n  Err(message.to_owned())\n}\n\nfn extension(_context: Context, path: &str) -> FunctionResult {\n  Utf8Path::new(path)\n    .extension()\n    .map(str::to_owned)\n    .ok_or_else(|| format!(\"Could not extract extension from `{path}`\"))\n}\n\nfn file_name(_context: Context, path: &str) -> FunctionResult {\n  Utf8Path::new(path)\n    .file_name()\n    .map(str::to_owned)\n    .ok_or_else(|| format!(\"Could not extract file name from `{path}`\"))\n}\n\nfn file_stem(_context: Context, path: &str) -> FunctionResult {\n  Utf8Path::new(path)\n    .file_stem()\n    .map(str::to_owned)\n    .ok_or_else(|| format!(\"Could not extract file stem from `{path}`\"))\n}\n\nfn invocation_directory(context: Context) -> FunctionResult {\n  Platform::convert_native_path(\n    context.execution_context.config,\n    &context.execution_context.search.working_directory,\n    &context.execution_context.config.invocation_directory,\n  )\n  .map_err(|e| format!(\"Error getting shell path: {e}\"))\n}\n\nfn invocation_directory_native(context: Context) -> FunctionResult {\n  context\n    .execution_context\n    .config\n    .invocation_directory\n    .to_str()\n    .map(str::to_owned)\n    .ok_or_else(|| {\n      format!(\n        \"Invocation directory is not valid unicode: {}\",\n        context\n          .execution_context\n          .config\n          .invocation_directory\n          .display()\n      )\n    })\n}\n\nfn is_dependency(context: Context) -> FunctionResult {\n  Ok(context.is_dependency.to_string())\n}\n\nfn prepend(_context: Context, prefix: &str, s: &str) -> FunctionResult {\n  Ok(\n    s.split_whitespace()\n      .map(|s| format!(\"{prefix}{s}\"))\n      .collect::<Vec<String>>()\n      .join(\" \"),\n  )\n}\n\nfn join(_context: Context, base: &str, with: &str, and: &[String]) -> FunctionResult {\n  let mut result = Utf8Path::new(base).join(with);\n  for arg in and {\n    result.push(arg);\n  }\n  Ok(result.to_string())\n}\n\nfn just_executable(_context: Context) -> FunctionResult {\n  let exe_path =\n    env::current_exe().map_err(|e| format!(\"Error getting current executable: {e}\"))?;\n\n  exe_path.to_str().map(str::to_owned).ok_or_else(|| {\n    format!(\n      \"Executable path is not valid unicode: {}\",\n      exe_path.display()\n    )\n  })\n}\n\nfn just_pid(_context: Context) -> FunctionResult {\n  Ok(std::process::id().to_string())\n}\n\nfn justfile(context: Context) -> FunctionResult {\n  context\n    .execution_context\n    .search\n    .justfile\n    .to_str()\n    .map(str::to_owned)\n    .ok_or_else(|| {\n      format!(\n        \"Justfile path is not valid unicode: {}\",\n        context.execution_context.search.justfile.display()\n      )\n    })\n}\n\nfn justfile_directory(context: Context) -> FunctionResult {\n  let justfile_directory = context\n    .execution_context\n    .search\n    .justfile\n    .parent()\n    .ok_or_else(|| {\n      format!(\n        \"Could not resolve justfile directory. Justfile `{}` had no parent.\",\n        context.execution_context.search.justfile.display()\n      )\n    })?;\n\n  justfile_directory\n    .to_str()\n    .map(str::to_owned)\n    .ok_or_else(|| {\n      format!(\n        \"Justfile directory is not valid unicode: {}\",\n        justfile_directory.display()\n      )\n    })\n}\n\nfn kebabcase(_context: Context, s: &str) -> FunctionResult {\n  Ok(s.to_kebab_case())\n}\n\nfn lowercamelcase(_context: Context, s: &str) -> FunctionResult {\n  Ok(s.to_lower_camel_case())\n}\n\nfn lowercase(_context: Context, s: &str) -> FunctionResult {\n  Ok(s.to_lowercase())\n}\n\nfn module_directory(context: Context) -> FunctionResult {\n  let module_directory = context.execution_context.module.source.parent().unwrap();\n  module_directory.to_str().map(str::to_owned).ok_or_else(|| {\n    format!(\n      \"Module directory is not valid unicode: {}\",\n      module_directory.display(),\n    )\n  })\n}\n\nfn module_file(context: Context) -> FunctionResult {\n  let module_file = &context.execution_context.module.source;\n  module_file.to_str().map(str::to_owned).ok_or_else(|| {\n    format!(\n      \"Module file path is not valid unicode: {}\",\n      module_file.display(),\n    )\n  })\n}\n\nfn num_cpus(_context: Context) -> FunctionResult {\n  let num = num_cpus::get();\n  Ok(num.to_string())\n}\n\nfn os(_context: Context) -> FunctionResult {\n  Ok(target::os().to_owned())\n}\n\nfn os_family(_context: Context) -> FunctionResult {\n  Ok(target::family().to_owned())\n}\n\nfn parent_directory(_context: Context, path: &str) -> FunctionResult {\n  Utf8Path::new(path)\n    .parent()\n    .map(Utf8Path::to_string)\n    .ok_or_else(|| format!(\"Could not extract parent directory from `{path}`\"))\n}\n\nfn path_exists(context: Context, path: &str) -> FunctionResult {\n  Ok(\n    context\n      .execution_context\n      .working_directory()\n      .join(path)\n      .exists()\n      .to_string(),\n  )\n}\n\nfn quote(_context: Context, s: &str) -> FunctionResult {\n  Ok(format!(\"'{}'\", s.replace('\\'', \"'\\\\''\")))\n}\n\nfn read(context: Context, filename: &str) -> FunctionResult {\n  fs::read_to_string(context.execution_context.working_directory().join(filename))\n    .map_err(|err| format!(\"I/O error reading `{filename}`: {err}\"))\n}\n\nfn replace(_context: Context, s: &str, from: &str, to: &str) -> FunctionResult {\n  Ok(s.replace(from, to))\n}\n\nfn require(context: Context, name: &str) -> FunctionResult {\n  crate::which(context, name)?.ok_or_else(|| format!(\"could not find executable `{name}`\"))\n}\n\nfn replace_regex(_context: Context, s: &str, regex: &str, replacement: &str) -> FunctionResult {\n  Ok(\n    Regex::new(regex)\n      .map_err(|err| err.to_string())?\n      .replace_all(s, replacement)\n      .to_string(),\n  )\n}\n\nfn sha256(_context: Context, s: &str) -> FunctionResult {\n  use sha2::{Digest, Sha256};\n  let mut hasher = Sha256::new();\n  hasher.update(s);\n  let hash = hasher.finalize();\n  Ok(format!(\"{hash:x}\"))\n}\n\nfn sha256_file(context: Context, path: &str) -> FunctionResult {\n  use sha2::{Digest, Sha256};\n  let path = context.execution_context.working_directory().join(path);\n  let mut hasher = Sha256::new();\n  let mut file =\n    fs::File::open(&path).map_err(|err| format!(\"Failed to open `{}`: {err}\", path.display()))?;\n  std::io::copy(&mut file, &mut hasher)\n    .map_err(|err| format!(\"Failed to read `{}`: {err}\", path.display()))?;\n  let hash = hasher.finalize();\n  Ok(format!(\"{hash:x}\"))\n}\n\nfn shell(context: Context, command: &str, args: &[String]) -> FunctionResult {\n  let args = iter::once(command)\n    .chain(args.iter().map(String::as_str))\n    .collect::<Vec<&str>>();\n\n  Evaluator::run_command(\n    context.execution_context,\n    &BTreeMap::new(),\n    context.scope,\n    command,\n    &args,\n  )\n  .map_err(|output_error| output_error.to_string())\n}\n\nfn shoutykebabcase(_context: Context, s: &str) -> FunctionResult {\n  Ok(s.to_shouty_kebab_case())\n}\n\nfn shoutysnakecase(_context: Context, s: &str) -> FunctionResult {\n  Ok(s.to_shouty_snake_case())\n}\n\nfn snakecase(_context: Context, s: &str) -> FunctionResult {\n  Ok(s.to_snake_case())\n}\n\nfn source_directory(context: Context) -> FunctionResult {\n  context\n    .execution_context\n    .search\n    .justfile\n    .parent()\n    .unwrap()\n    .join(context.name.token.path)\n    .parent()\n    .unwrap()\n    .to_str()\n    .map(str::to_owned)\n    .ok_or_else(|| {\n      format!(\n        \"Source file path not valid unicode: {}\",\n        context.name.token.path.display(),\n      )\n    })\n}\n\nfn source_file(context: Context) -> FunctionResult {\n  context\n    .execution_context\n    .search\n    .justfile\n    .parent()\n    .unwrap()\n    .join(context.name.token.path)\n    .to_str()\n    .map(str::to_owned)\n    .ok_or_else(|| {\n      format!(\n        \"Source file path not valid unicode: {}\",\n        context.name.token.path.display(),\n      )\n    })\n}\n\nfn style(context: Context, s: &str) -> FunctionResult {\n  match s {\n    \"command\" => Ok(\n      Color::always()\n        .command(context.execution_context.config.command_color)\n        .prefix()\n        .to_string(),\n    ),\n    \"error\" => Ok(Color::always().error().prefix().to_string()),\n    \"warning\" => Ok(Color::always().warning().prefix().to_string()),\n    _ => Err(format!(\"unknown style: `{s}`\")),\n  }\n}\n\nfn titlecase(_context: Context, s: &str) -> FunctionResult {\n  Ok(s.to_title_case())\n}\n\nfn trim(_context: Context, s: &str) -> FunctionResult {\n  Ok(s.trim().to_owned())\n}\n\nfn trim_end(_context: Context, s: &str) -> FunctionResult {\n  Ok(s.trim_end().to_owned())\n}\n\nfn trim_end_match(_context: Context, s: &str, pat: &str) -> FunctionResult {\n  Ok(s.strip_suffix(pat).unwrap_or(s).to_owned())\n}\n\nfn trim_end_matches(_context: Context, s: &str, pat: &str) -> FunctionResult {\n  Ok(s.trim_end_matches(pat).to_owned())\n}\n\nfn trim_start(_context: Context, s: &str) -> FunctionResult {\n  Ok(s.trim_start().to_owned())\n}\n\nfn trim_start_match(_context: Context, s: &str, pat: &str) -> FunctionResult {\n  Ok(s.strip_prefix(pat).unwrap_or(s).to_owned())\n}\n\nfn trim_start_matches(_context: Context, s: &str, pat: &str) -> FunctionResult {\n  Ok(s.trim_start_matches(pat).to_owned())\n}\n\nfn uppercamelcase(_context: Context, s: &str) -> FunctionResult {\n  Ok(s.to_upper_camel_case())\n}\n\nfn uppercase(_context: Context, s: &str) -> FunctionResult {\n  Ok(s.to_uppercase())\n}\n\nfn uuid(_context: Context) -> FunctionResult {\n  Ok(uuid::Uuid::new_v4().to_string())\n}\n\nfn which(context: Context, name: &str) -> FunctionResult {\n  Ok(crate::which(context, name)?.unwrap_or_default())\n}\n\nfn without_extension(_context: Context, path: &str) -> FunctionResult {\n  let parent = Utf8Path::new(path)\n    .parent()\n    .ok_or_else(|| format!(\"Could not extract parent from `{path}`\"))?;\n\n  let file_stem = Utf8Path::new(path)\n    .file_stem()\n    .ok_or_else(|| format!(\"Could not extract file stem from `{path}`\"))?;\n\n  Ok(parent.join(file_stem).to_string())\n}\n\n/// Check whether a string processes properly as semver (e.x. \"0.1.0\")\n/// and matches a given semver requirement (e.x. \">=0.1.0\")\nfn semver_matches(_context: Context, version: &str, requirement: &str) -> FunctionResult {\n  Ok(\n    requirement\n      .parse::<VersionReq>()\n      .map_err(|err| format!(\"invalid semver requirement: {err}\"))?\n      .matches(\n        &version\n          .parse::<Version>()\n          .map_err(|err| format!(\"invalid semver version: {err}\"))?,\n      )\n      .to_string(),\n  )\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn dir_not_found() {\n    assert_eq!(dir(\"foo\", || None).unwrap_err(), \"foo directory not found\");\n  }\n\n  #[cfg(unix)]\n  #[test]\n  fn dir_not_unicode() {\n    use std::os::unix::ffi::OsStrExt;\n    assert_eq!(\n      dir(\"foo\", || Some(\n        std::ffi::OsStr::from_bytes(b\"\\xe0\\x80\\x80\").into()\n      ))\n      .unwrap_err(),\n      \"unable to convert foo directory path to string: ���\",\n    );\n  }\n}\n"
  },
  {
    "path": "src/fuzzing.rs",
    "content": "use super::*;\n\npub fn compile(text: &str) {\n  let _ = testing::compile(text);\n}\n"
  },
  {
    "path": "src/interpreter.rs",
    "content": "use super::*;\n\n#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]\npub(crate) struct Interpreter<T> {\n  pub(crate) arguments: Vec<T>,\n  pub(crate) command: T,\n}\n\nimpl Interpreter<String> {\n  pub(crate) fn default_script_interpreter() -> &'static Self {\n    static INSTANCE: LazyLock<Interpreter<String>> = LazyLock::new(|| Interpreter::<String> {\n      arguments: vec![\"-eu\".into()],\n      command: \"sh\".into(),\n    });\n    &INSTANCE\n  }\n}\n\nimpl<T: Display> Display for Interpreter<T> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    write!(f, \"{}\", self.command)?;\n\n    for argument in &self.arguments {\n      write!(f, \", {argument}\")?;\n    }\n\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "src/invocation.rs",
    "content": "use super::*;\n\n#[derive(Debug, PartialEq)]\npub(crate) struct Invocation<'src, 'run> {\n  pub(crate) arguments: Vec<Vec<String>>,\n  pub(crate) recipe: &'run Recipe<'src>,\n}\n"
  },
  {
    "path": "src/invocation_parser.rs",
    "content": "use super::*;\n\n#[allow(clippy::doc_markdown)]\n/// The invocation parser is responsible for grouping command-line positional\n/// arguments into invocations, which consist of a recipe and its arguments.\n///\n/// Invocation parsing is substantially complicated by the fact that recipe\n/// paths can be given on the command line as multiple arguments, i.e., \"foo\"\n/// \"bar\" baz\", or as a single \"::\"-separated argument.\n///\n/// Error messages produced by the invocation parser should use the format of\n/// the recipe path as passed on the command line.\n///\n/// Additionally, if a recipe is specified with a \"::\"-separated path, extra\n/// components of that path after a valid recipe must not be used as arguments,\n/// whereas arguments after multiple argument path may be used as arguments. As\n/// an example, `foo bar baz` may refer to recipe `foo::bar` with argument\n/// `baz`, but `foo::bar::baz` is an error, since `bar` is a recipe, not a\n/// module.\npub(crate) struct InvocationParser<'src: 'run, 'run> {\n  arguments: &'run [&'run str],\n  next: usize,\n  root: &'run Justfile<'src>,\n}\n\nimpl<'src: 'run, 'run> InvocationParser<'src, 'run> {\n  pub(crate) fn parse_invocations(\n    root: &'run Justfile<'src>,\n    arguments: &'run [&'run str],\n  ) -> RunResult<'src, Vec<Invocation<'src, 'run>>> {\n    let mut invocations = Vec::new();\n\n    let mut invocation_parser = Self {\n      arguments,\n      next: 0,\n      root,\n    };\n\n    loop {\n      invocations.push(invocation_parser.parse_invocation()?);\n\n      if invocation_parser.next == arguments.len() {\n        break;\n      }\n    }\n\n    Ok(invocations)\n  }\n\n  fn parse_invocation(&mut self) -> RunResult<'src, Invocation<'src, 'run>> {\n    let recipe = if let Some(next) = self.next() {\n      if next.contains(':') {\n        let modulepath =\n          Modulepath::try_from([next].as_slice()).map_err(|()| Error::UnknownRecipe {\n            recipe: next.into(),\n            suggestion: None,\n          })?;\n        let (recipe, _) = self.resolve_recipe(true, &modulepath.path)?;\n        self.next += 1;\n        recipe\n      } else {\n        let (recipe, consumed) = self.resolve_recipe(false, self.rest())?;\n        self.next += consumed;\n        recipe\n      }\n    } else {\n      let (recipe, consumed) = self.resolve_recipe(false, self.rest())?;\n      assert_eq!(consumed, 0);\n      recipe\n    };\n\n    let mut arguments = vec![Vec::<String>::new(); recipe.parameters.len()];\n\n    let long = recipe\n      .parameters\n      .iter()\n      .enumerate()\n      .filter_map(|(i, parameter)| parameter.long.as_ref().map(|name| (name.as_str(), i)))\n      .collect::<BTreeMap<&str, usize>>();\n\n    let short = recipe\n      .parameters\n      .iter()\n      .enumerate()\n      .filter_map(|(i, parameter)| parameter.short.map(|name| (name, i)))\n      .collect::<BTreeMap<char, usize>>();\n\n    let positional = recipe\n      .parameters\n      .iter()\n      .enumerate()\n      .filter_map(|(i, parameter)| (!parameter.is_option()).then_some(i))\n      .collect::<Vec<usize>>();\n\n    let mut end_of_options = long.is_empty() && short.is_empty();\n\n    let rest = self.rest();\n\n    let mut i = 0;\n    let mut positional_index = 0;\n    let mut positional_accepted = 0;\n    loop {\n      let Some(argument) = rest.get(i) else {\n        break;\n      };\n\n      if !end_of_options && *argument == \"--\" {\n        end_of_options = true;\n        i += 1;\n      } else if !end_of_options && argument.starts_with('-') && *argument != \"-\" {\n        let mut name = argument\n          .strip_prefix(\"--\")\n          .or_else(|| argument.strip_prefix('-'))\n          .unwrap();\n\n        let value = if let Some((left, right)) = name.split_once('=') {\n          name = left;\n          Some(right)\n        } else {\n          None\n        };\n\n        let switch = if argument.starts_with(\"--\") {\n          Switch::Long(name.into())\n        } else {\n          if name.chars().count() != 1 {\n            return Err(Error::MultipleShortOptions {\n              options: name.into(),\n            });\n          }\n          Switch::Short(name.chars().next().unwrap())\n        };\n\n        let index = match &switch {\n          Switch::Long(name) => long.get(name.as_str()),\n          Switch::Short(name) => short.get(name),\n        };\n\n        let Some(&index) = index else {\n          return Err(Error::UnknownOption {\n            recipe: recipe.name(),\n            option: switch,\n          });\n        };\n\n        let value = if let Some(flag_value) = &recipe.parameters[index].value {\n          if value.is_some() {\n            return Err(Error::FlagWithValue {\n              recipe: recipe.name(),\n              option: switch,\n            });\n          }\n          i += 1;\n          flag_value\n        } else if let Some(value) = value {\n          i += 1;\n          value\n        } else {\n          let Some(&value) = rest.get(i + 1) else {\n            return Err(Error::OptionMissingValue {\n              recipe: recipe.name(),\n              option: switch,\n            });\n          };\n          i += 2;\n          value\n        };\n\n        let group = &mut arguments[index];\n\n        if !group.is_empty() {\n          return Err(Error::DuplicateOption {\n            recipe: recipe.name(),\n            option: switch,\n          });\n        }\n\n        group.push((*value).into());\n      } else {\n        let Some(&index) = positional.get(positional_index) else {\n          break;\n        };\n        let group = &mut arguments[index];\n        group.push((*argument).into());\n        if !recipe.parameters[index].kind.is_variadic() {\n          positional_index += 1;\n        }\n        positional_accepted += 1;\n        i += 1;\n      }\n    }\n\n    let mut missing_positional = 0;\n\n    for (parameter, group) in recipe.parameters.iter().zip(&arguments) {\n      if !group.is_empty() {\n        continue;\n      }\n\n      if parameter.default.is_some() || parameter.kind == ParameterKind::Star {\n        continue;\n      }\n\n      if let Some(name) = &parameter.long {\n        return Err(Error::MissingOption {\n          recipe: recipe.name(),\n          option: Switch::Long(name.into()),\n        });\n      }\n\n      if let Some(name) = &parameter.short {\n        return Err(Error::MissingOption {\n          recipe: recipe.name(),\n          option: Switch::Short(*name),\n        });\n      }\n\n      missing_positional += 1;\n    }\n\n    if missing_positional > 0 {\n      return Err(Error::PositionalArgumentCountMismatch {\n        recipe: Box::new(recipe.clone()),\n        found: positional_accepted,\n        min: recipe\n          .parameters\n          .iter()\n          .filter(|p| p.is_required() && !p.is_option())\n          .count(),\n        max: if recipe.parameters.iter().any(|p| p.kind.is_variadic()) {\n          usize::MAX - 1\n        } else {\n          recipe.parameters.iter().filter(|p| !p.is_option()).count()\n        },\n      });\n    }\n\n    for (group, parameter) in arguments.iter().zip(&recipe.parameters) {\n      for argument in group {\n        parameter.check_pattern_match(recipe, argument)?;\n      }\n    }\n\n    self.next += i;\n\n    Ok(Invocation { arguments, recipe })\n  }\n\n  fn resolve_recipe(\n    &self,\n    modulepath: bool,\n    args: &[impl AsRef<str>],\n  ) -> RunResult<'src, (&'run Recipe<'src>, usize)> {\n    let mut current = self.root;\n    let mut path = Vec::new();\n\n    for (i, arg) in args.iter().enumerate() {\n      let arg = arg.as_ref();\n\n      path.push(arg.to_string());\n\n      if let Some(module) = current.modules.get(arg) {\n        current = module;\n      } else if let Some(recipe) = current.get_recipe(arg) {\n        if modulepath && i + 1 < args.len() {\n          return Err(Error::ExpectedSubmoduleButFoundRecipe {\n            path: path.join(\"::\"),\n          });\n        }\n        return Ok((recipe, i + 1));\n      } else {\n        if modulepath && i + 1 < args.len() {\n          return Err(Error::UnknownSubmodule {\n            path: path.join(\"::\"),\n          });\n        }\n\n        return Err(Error::UnknownRecipe {\n          recipe: if modulepath {\n            path.join(\"::\")\n          } else {\n            path.join(\" \")\n          },\n          suggestion: current.suggest_recipe(arg),\n        });\n      }\n    }\n\n    if let Some(recipe) = &current.default {\n      recipe.check_can_be_default_recipe()?;\n      Ok((recipe, args.len()))\n    } else if current.recipes.is_empty() {\n      Err(Error::NoRecipes)\n    } else {\n      Err(Error::NoDefaultRecipe)\n    }\n  }\n\n  fn next(&self) -> Option<&'run str> {\n    self.arguments.get(self.next).copied()\n  }\n\n  fn rest(&self) -> &[&'run str] {\n    &self.arguments[self.next..]\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use {super::*, tempfile::TempDir};\n\n  trait TempDirExt {\n    fn write(&self, path: &str, content: &str);\n  }\n\n  impl TempDirExt for TempDir {\n    fn write(&self, path: &str, content: &str) {\n      let path = self.path().join(path);\n      fs::create_dir_all(path.parent().unwrap()).unwrap();\n      fs::write(path, content).unwrap();\n    }\n  }\n\n  #[test]\n  fn single_no_arguments() {\n    let justfile = testing::compile(\"foo:\");\n\n    let invocations = InvocationParser::parse_invocations(&justfile, &[\"foo\"]).unwrap();\n\n    assert_eq!(invocations.len(), 1);\n    assert_eq!(invocations[0].recipe.recipe_path().to_string(), \"foo\");\n    assert!(invocations[0].arguments.is_empty());\n  }\n\n  #[test]\n  fn single_with_argument() {\n    let justfile = testing::compile(\"foo bar:\");\n\n    let invocations = InvocationParser::parse_invocations(&justfile, &[\"foo\", \"baz\"]).unwrap();\n\n    assert_eq!(invocations.len(), 1);\n    assert_eq!(invocations[0].recipe.recipe_path().to_string(), \"foo\");\n    assert_eq!(invocations[0].arguments, vec![vec![String::from(\"baz\")]]);\n  }\n\n  #[test]\n  fn single_argument_count_mismatch() {\n    let justfile = testing::compile(\"foo bar:\");\n\n    assert_matches!(\n      InvocationParser::parse_invocations(&justfile, &[\"foo\"]).unwrap_err(),\n      Error::PositionalArgumentCountMismatch {\n        recipe: _,\n        found: 0,\n        min: 1,\n        max: 1,\n        ..\n      },\n    );\n  }\n\n  #[test]\n  fn single_unknown() {\n    let justfile = testing::compile(\"foo:\");\n\n    assert_matches!(\n      InvocationParser::parse_invocations(&justfile, &[\"bar\"]).unwrap_err(),\n      Error::UnknownRecipe {\n        recipe,\n        suggestion: None\n      } if recipe == \"bar\",\n    );\n  }\n\n  #[test]\n  fn multiple_unknown() {\n    let justfile = testing::compile(\"foo:\");\n\n    assert_matches!(\n      InvocationParser::parse_invocations(&justfile, &[\"bar\", \"baz\"]).unwrap_err(),\n      Error::UnknownRecipe {\n        recipe,\n        suggestion: None\n      } if recipe == \"bar\",\n    );\n  }\n\n  #[test]\n  fn recipe_in_submodule() {\n    let loader = Loader::new();\n    let tempdir = tempfile::tempdir().unwrap();\n    let path = tempdir.path().join(\"justfile\");\n    fs::write(&path, \"mod foo\").unwrap();\n    fs::create_dir(tempdir.path().join(\"foo\")).unwrap();\n    fs::write(tempdir.path().join(\"foo/mod.just\"), \"bar:\").unwrap();\n    let compilation = Compiler::compile(&Config::default(), &loader, &path).unwrap();\n\n    let invocations =\n      InvocationParser::parse_invocations(&compilation.justfile, &[\"foo\", \"bar\"]).unwrap();\n\n    assert_eq!(invocations.len(), 1);\n    assert_eq!(invocations[0].recipe.recipe_path().to_string(), \"foo::bar\");\n    assert!(invocations[0].arguments.is_empty());\n  }\n\n  #[test]\n  fn recipe_in_submodule_unknown() {\n    let loader = Loader::new();\n    let tempdir = tempfile::tempdir().unwrap();\n    let path = tempdir.path().join(\"justfile\");\n    fs::write(&path, \"mod foo\").unwrap();\n    fs::create_dir(tempdir.path().join(\"foo\")).unwrap();\n    fs::write(tempdir.path().join(\"foo/mod.just\"), \"bar:\").unwrap();\n    let compilation = Compiler::compile(&Config::default(), &loader, &path).unwrap();\n\n    assert_matches!(\n      InvocationParser::parse_invocations(&compilation.justfile, &[\"foo\", \"zzz\"]).unwrap_err(),\n      Error::UnknownRecipe {\n        recipe,\n        suggestion: None\n      } if recipe == \"foo zzz\",\n    );\n  }\n\n  #[test]\n  fn recipe_in_submodule_path_unknown() {\n    let tempdir = tempfile::tempdir().unwrap();\n    tempdir.write(\"justfile\", \"mod foo\");\n    tempdir.write(\"foo.just\", \"bar:\");\n\n    let loader = Loader::new();\n    let compilation = Compiler::compile(\n      &Config::default(),\n      &loader,\n      &tempdir.path().join(\"justfile\"),\n    )\n    .unwrap();\n\n    assert_matches!(\n      InvocationParser::parse_invocations(&compilation.justfile, &[\"foo::zzz\"]).unwrap_err(),\n      Error::UnknownRecipe {\n        recipe,\n        suggestion: None\n      } if recipe == \"foo::zzz\",\n    );\n  }\n\n  #[test]\n  fn module_path_not_consumed() {\n    let tempdir = tempfile::tempdir().unwrap();\n    tempdir.write(\"justfile\", \"mod foo\");\n    tempdir.write(\"foo.just\", \"bar:\");\n\n    let loader = Loader::new();\n    let compilation = Compiler::compile(\n      &Config::default(),\n      &loader,\n      &tempdir.path().join(\"justfile\"),\n    )\n    .unwrap();\n\n    assert_matches!(\n      InvocationParser::parse_invocations(&compilation.justfile, &[\"foo::bar::baz\"]).unwrap_err(),\n      Error::ExpectedSubmoduleButFoundRecipe {\n        path,\n      } if path == \"foo::bar\",\n    );\n  }\n\n  #[test]\n  fn no_recipes() {\n    let tempdir = tempfile::tempdir().unwrap();\n    tempdir.write(\"justfile\", \"\");\n\n    let loader = Loader::new();\n    let compilation = Compiler::compile(\n      &Config::default(),\n      &loader,\n      &tempdir.path().join(\"justfile\"),\n    )\n    .unwrap();\n\n    assert_matches!(\n      InvocationParser::parse_invocations(&compilation.justfile, &[]).unwrap_err(),\n      Error::NoRecipes,\n    );\n  }\n\n  #[test]\n  fn default_recipe_requires_arguments() {\n    let tempdir = tempfile::tempdir().unwrap();\n    tempdir.write(\"justfile\", \"foo bar:\");\n\n    let loader = Loader::new();\n    let compilation = Compiler::compile(\n      &Config::default(),\n      &loader,\n      &tempdir.path().join(\"justfile\"),\n    )\n    .unwrap();\n\n    assert_matches!(\n      InvocationParser::parse_invocations(&compilation.justfile, &[]).unwrap_err(),\n      Error::DefaultRecipeRequiresArguments {\n        recipe: \"foo\",\n        min_arguments: 1,\n      },\n    );\n  }\n\n  #[test]\n  fn no_default_recipe() {\n    let tempdir = tempfile::tempdir().unwrap();\n    tempdir.write(\"justfile\", \"import 'foo.just'\");\n    tempdir.write(\"foo.just\", \"bar:\");\n\n    let loader = Loader::new();\n    let compilation = Compiler::compile(\n      &Config::default(),\n      &loader,\n      &tempdir.path().join(\"justfile\"),\n    )\n    .unwrap();\n\n    assert_matches!(\n      InvocationParser::parse_invocations(&compilation.justfile, &[]).unwrap_err(),\n      Error::NoDefaultRecipe,\n    );\n  }\n\n  #[test]\n  fn complex_grouping() {\n    let justfile = testing::compile(\n      \"\nFOO A B='blarg':\n  echo foo: {{A}} {{B}}\n\nBAR X:\n  echo bar: {{X}}\n\nBAZ +Z:\n  echo baz: {{Z}}\n\",\n    );\n\n    let invocations = InvocationParser::parse_invocations(\n      &justfile,\n      &[\"BAR\", \"0\", \"FOO\", \"1\", \"2\", \"BAZ\", \"3\", \"4\", \"5\"],\n    )\n    .unwrap();\n\n    assert_eq!(invocations.len(), 3);\n    assert_eq!(invocations[0].recipe.recipe_path().to_string(), \"BAR\");\n    assert_eq!(invocations[0].arguments, vec![vec![String::from(\"0\")]]);\n    assert_eq!(invocations[1].recipe.recipe_path().to_string(), \"FOO\");\n    assert_eq!(\n      invocations[1].arguments,\n      vec![vec![String::from(\"1\")], vec![String::from(\"2\")]]\n    );\n    assert_eq!(invocations[2].recipe.recipe_path().to_string(), \"BAZ\");\n    assert_eq!(\n      invocations[2].arguments,\n      vec![vec![\n        String::from(\"3\"),\n        String::from(\"4\"),\n        String::from(\"5\")\n      ]]\n    );\n  }\n\n  #[test]\n  fn long_argument() {\n    let justfile = testing::compile(\n      \"\n[arg('bar', long='bar')]\nfoo bar:\n      \",\n    );\n\n    let invocations =\n      InvocationParser::parse_invocations(&justfile, &[\"foo\", \"--bar\", \"baz\"]).unwrap();\n\n    assert_eq!(invocations.len(), 1);\n    assert_eq!(invocations[0].recipe.recipe_path().to_string(), \"foo\");\n    assert_eq!(invocations[0].arguments, vec![vec![String::from(\"baz\")]]);\n  }\n\n  #[test]\n  fn long_argument_with_positional() {\n    let justfile = testing::compile(\n      \"\n[arg('bar', long='bar')]\nfoo baz bar:\n      \",\n    );\n\n    let invocations =\n      InvocationParser::parse_invocations(&justfile, &[\"foo\", \"qux\", \"--bar\", \"baz\"]).unwrap();\n\n    assert_eq!(invocations.len(), 1);\n    assert_eq!(invocations[0].recipe.recipe_path().to_string(), \"foo\");\n    assert_eq!(\n      invocations[0].arguments,\n      vec![vec![String::from(\"qux\")], vec![String::from(\"baz\")]]\n    );\n  }\n\n  #[test]\n  fn long_argument_terminator() {\n    let justfile = testing::compile(\n      \"\n[arg('bar', long='bar')]\nfoo baz qux='qux' bar='bar':\n      \",\n    );\n\n    let invocations =\n      InvocationParser::parse_invocations(&justfile, &[\"foo\", \"--\", \"--bar\"]).unwrap();\n\n    assert_eq!(invocations.len(), 1);\n    assert_eq!(invocations[0].recipe.recipe_path().to_string(), \"foo\");\n    assert_eq!(\n      invocations[0].arguments,\n      vec![vec![String::from(\"--bar\")], Vec::new(), Vec::new()]\n    );\n  }\n}\n"
  },
  {
    "path": "src/item.rs",
    "content": "use super::*;\n\n/// A single top-level item\n#[derive(Debug, Clone)]\npub(crate) enum Item<'src> {\n  Alias(Alias<'src, Namepath<'src>>),\n  Assignment(Assignment<'src>),\n  Comment(&'src str),\n  Import {\n    absolute: Option<PathBuf>,\n    optional: bool,\n    relative: StringLiteral<'src>,\n  },\n  Module {\n    absolute: Option<PathBuf>,\n    doc: Option<String>,\n    groups: Vec<StringLiteral<'src>>,\n    name: Name<'src>,\n    optional: bool,\n    private: bool,\n    relative: Option<StringLiteral<'src>>,\n  },\n  Recipe(UnresolvedRecipe<'src>),\n  Set(Set<'src>),\n  Unexport {\n    name: Name<'src>,\n  },\n}\n\nimpl Display for Item<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    match self {\n      Self::Alias(alias) => write!(f, \"{alias}\"),\n      Self::Assignment(assignment) => write!(f, \"{assignment}\"),\n      Self::Comment(comment) => write!(f, \"{comment}\"),\n      Self::Import {\n        relative, optional, ..\n      } => {\n        write!(f, \"import\")?;\n\n        if *optional {\n          write!(f, \"?\")?;\n        }\n\n        write!(f, \" {relative}\")\n      }\n      Self::Module {\n        doc,\n        groups,\n        name,\n        optional,\n        relative,\n        ..\n      } => {\n        if let Some(doc) = doc {\n          writeln!(f, \"# {doc}\")?;\n        }\n\n        for group in groups {\n          writeln!(f, \"[group: {group}]\")?;\n        }\n\n        write!(f, \"mod\")?;\n\n        if *optional {\n          write!(f, \"?\")?;\n        }\n\n        write!(f, \" {name}\")?;\n\n        if let Some(path) = relative {\n          write!(f, \" {path}\")?;\n        }\n\n        Ok(())\n      }\n      Self::Recipe(recipe) => write!(f, \"{}\", recipe.color_display(Color::never())),\n      Self::Set(set) => write!(f, \"{set}\"),\n      Self::Unexport { name } => write!(f, \"unexport {name}\"),\n    }\n  }\n}\n"
  },
  {
    "path": "src/justfile.rs",
    "content": "use {super::*, serde::Serialize};\n\n#[derive(Debug, PartialEq, Serialize)]\npub(crate) struct Justfile<'src> {\n  pub(crate) aliases: Table<'src, Alias<'src>>,\n  pub(crate) assignments: Table<'src, Assignment<'src>>,\n  #[serde(rename = \"first\", serialize_with = \"keyed::serialize_option\")]\n  pub(crate) default: Option<Arc<Recipe<'src>>>,\n  pub(crate) doc: Option<String>,\n  pub(crate) groups: Vec<StringLiteral<'src>>,\n  #[serde(skip)]\n  pub(crate) loaded: Vec<PathBuf>,\n  #[serde(skip)]\n  pub(crate) modulepath: Modulepath,\n  pub(crate) modules: Table<'src, Self>,\n  #[serde(skip)]\n  pub(crate) name: Option<Name<'src>>,\n  #[serde(skip)]\n  pub(crate) private: bool,\n  pub(crate) recipes: Table<'src, Arc<Recipe<'src>>>,\n  pub(crate) settings: Settings,\n  pub(crate) source: PathBuf,\n  pub(crate) unexports: HashSet<String>,\n  #[serde(skip)]\n  pub(crate) unstable_features: BTreeSet<UnstableFeature>,\n  pub(crate) warnings: Vec<Warning>,\n  #[serde(skip)]\n  pub(crate) working_directory: PathBuf,\n}\n\nimpl<'src> Justfile<'src> {\n  fn find_suggestion(\n    input: &str,\n    candidates: impl Iterator<Item = Suggestion<'src>>,\n  ) -> Option<Suggestion<'src>> {\n    candidates\n      .map(|suggestion| (edit_distance(input, suggestion.name), suggestion))\n      .filter(|(distance, _suggestion)| *distance < 3)\n      .min_by_key(|(distance, _suggestion)| *distance)\n      .map(|(_distance, suggestion)| suggestion)\n  }\n\n  pub(crate) fn suggest_recipe(&self, input: &str) -> Option<Suggestion<'src>> {\n    Self::find_suggestion(\n      input,\n      self\n        .recipes\n        .values()\n        .filter(|recipe| recipe.is_public())\n        .map(|recipe| Suggestion {\n          name: recipe.name(),\n          target: None,\n        })\n        .chain(\n          self\n            .aliases\n            .values()\n            .filter(|alias| alias.is_public())\n            .map(|alias| Suggestion {\n              name: alias.name.lexeme(),\n              target: Some(alias.target.name.lexeme()),\n            }),\n        ),\n    )\n  }\n\n  pub(crate) fn suggest_variable(&self, input: &str) -> Option<Suggestion<'src>> {\n    Self::find_suggestion(\n      input,\n      self\n        .assignments\n        .keys()\n        .map(|name| Suggestion { name, target: None }),\n    )\n  }\n\n  fn evaluate_scopes<'run>(\n    &'run self,\n    arena: &'run Arena<Scope<'src, 'run>>,\n    config: &'run Config,\n    dotenv: &'run BTreeMap<String, String>,\n    overrides: &'run HashMap<Number, String>,\n    root: &'run Scope<'src, 'run>,\n    scopes: &mut BTreeMap<Modulepath, (&'run Self, &'run Scope<'src, 'run>)>,\n    search: &'run Search,\n    variable_references: Option<&HashSet<Number>>,\n  ) -> RunResult<'src> {\n    let scope = Evaluator::evaluate_assignments(\n      config,\n      dotenv,\n      self,\n      overrides,\n      root,\n      search,\n      variable_references,\n    )?;\n\n    let scope = arena.alloc(scope);\n    scopes.insert(self.modulepath.clone(), (self, scope));\n\n    for module in self.modules.values() {\n      module.evaluate_scopes(\n        arena,\n        config,\n        dotenv,\n        overrides,\n        scope,\n        scopes,\n        search,\n        variable_references,\n      )?;\n    }\n\n    Ok(())\n  }\n\n  pub(crate) fn run(\n    &self,\n    config: &Config,\n    search: &Search,\n    arguments: &[String],\n    overrides: &HashMap<Number, String>,\n  ) -> RunResult<'src> {\n    let dotenv = if config.load_dotenv {\n      load_dotenv(config, &self.settings, &search.working_directory)?\n    } else {\n      BTreeMap::new()\n    };\n\n    let root = Scope::root();\n    let arena = Arena::new();\n    let mut scopes = BTreeMap::new();\n\n    match &config.subcommand {\n      Subcommand::Choose { .. } | Subcommand::Run { .. } => {\n        let arguments = arguments.iter().map(String::as_str).collect::<Vec<&str>>();\n\n        let invocations = InvocationParser::parse_invocations(self, &arguments)?;\n\n        if config.one && invocations.len() > 1 {\n          return Err(Error::ExcessInvocations {\n            invocations: invocations.len(),\n          });\n        }\n\n        let variable_references = if self.settings.lazy {\n          let mut variable_references = HashSet::new();\n\n          let mut stack = Vec::new();\n\n          for invocation in &invocations {\n            stack.push(invocation.recipe);\n          }\n\n          while let Some(recipe) = stack.pop() {\n            variable_references.extend(&recipe.variable_references);\n            for dependency in &recipe.dependencies {\n              stack.push(&dependency.recipe);\n            }\n          }\n\n          Some(variable_references)\n        } else {\n          None\n        };\n\n        self.evaluate_scopes(\n          &arena,\n          config,\n          &dotenv,\n          overrides,\n          &root,\n          &mut scopes,\n          search,\n          variable_references.as_ref(),\n        )?;\n\n        let ran = Ran::default();\n        for invocation in invocations {\n          Self::run_recipe(\n            &invocation.arguments,\n            config,\n            &dotenv,\n            false,\n            &ran,\n            invocation.recipe,\n            &scopes,\n            search,\n          )?;\n        }\n\n        Ok(())\n      }\n      Subcommand::Command {\n        binary, arguments, ..\n      } => {\n        let mut command = if config.shell_command {\n          let mut command = self.settings.shell_command(config);\n          command.arg(binary);\n          command\n        } else {\n          Command::new(binary)\n        };\n\n        command\n          .args(arguments)\n          .current_dir(&search.working_directory);\n\n        self.evaluate_scopes(\n          &arena,\n          config,\n          &dotenv,\n          overrides,\n          &root,\n          &mut scopes,\n          search,\n          Some(HashSet::new()).as_ref(),\n        )?;\n\n        let scope = scopes.get(&self.modulepath).unwrap().1.child();\n\n        command.export(&self.settings, &dotenv, &scope, &self.unexports);\n\n        let (result, caught) = command.status_guard();\n\n        let status = result.map_err(|io_error| Error::CommandInvoke {\n          binary: binary.clone(),\n          arguments: arguments.clone(),\n          io_error,\n        })?;\n\n        if !status.success() {\n          return Err(Error::CommandStatus {\n            binary: binary.clone(),\n            arguments: arguments.clone(),\n            status,\n          });\n        }\n\n        if let Some(signal) = caught {\n          return Err(Error::Interrupted { signal });\n        }\n\n        Ok(())\n      }\n      Subcommand::Evaluate { variable, .. } => {\n        let variable_references = if let Some(variable) = variable {\n          if let Some(assignment) = self.assignments.get(variable) {\n            Some(HashSet::from([assignment.number]))\n          } else {\n            return Err(Error::EvalUnknownVariable {\n              suggestion: self.suggest_variable(variable),\n              variable: variable.clone(),\n            });\n          }\n        } else {\n          Some(\n            self\n              .assignments\n              .values()\n              .filter(|assignment| !assignment.private)\n              .map(|assignment| assignment.number)\n              .collect(),\n          )\n        };\n\n        self.evaluate_scopes(\n          &arena,\n          config,\n          &dotenv,\n          overrides,\n          &root,\n          &mut scopes,\n          search,\n          variable_references.as_ref(),\n        )?;\n\n        let scope = scopes.get(&self.modulepath).unwrap().1;\n\n        if let Some(variable) = variable {\n          print!(\"{}\", scope.value(variable).unwrap());\n        } else {\n          let width = scope.names().fold(0, |max, name| name.len().max(max));\n\n          for binding in scope.bindings() {\n            if !binding.private {\n              println!(\n                \"{0:1$} := \\\"{2}\\\"\",\n                binding.name.lexeme(),\n                width,\n                binding.value\n              );\n            }\n          }\n        }\n\n        Ok(())\n      }\n      _ => unreachable!(),\n    }\n  }\n\n  pub(crate) fn check_unstable(&self, config: &Config) -> RunResult<'src> {\n    if let Some(&unstable_feature) = self.unstable_features.iter().next() {\n      config.require_unstable(self, unstable_feature)?;\n    }\n\n    for module in self.modules.values() {\n      module.check_unstable(config)?;\n    }\n\n    Ok(())\n  }\n\n  pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias<'src>> {\n    self.aliases.get(name)\n  }\n\n  pub(crate) fn get_recipe(&self, name: &str) -> Option<&Recipe<'src>> {\n    self\n      .recipes\n      .get(name)\n      .map(Arc::as_ref)\n      .or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref()))\n  }\n\n  pub(crate) fn is_submodule(&self) -> bool {\n    self.name.is_some()\n  }\n\n  pub(crate) fn name(&self) -> &'src str {\n    self.name.map(|name| name.lexeme()).unwrap_or_default()\n  }\n\n  fn run_recipe(\n    arguments: &[Vec<String>],\n    config: &Config,\n    dotenv: &BTreeMap<String, String>,\n    is_dependency: bool,\n    ran: &Ran,\n    recipe: &Recipe<'src>,\n    scopes: &BTreeMap<Modulepath, (&Self, &Scope<'src, '_>)>,\n    search: &Search,\n  ) -> RunResult<'src> {\n    let mutex = ran.mutex(recipe, arguments);\n\n    let mut guard = mutex.lock().unwrap();\n\n    if *guard {\n      return Ok(());\n    }\n\n    if !config.yes && !recipe.confirm()? {\n      return Err(Error::NotConfirmed {\n        recipe: recipe.name(),\n      });\n    }\n\n    let (module, scope) = scopes\n      .get(recipe.module_path())\n      .expect(\"failed to retrieve scope for module\");\n\n    let context = ExecutionContext {\n      config,\n      dotenv,\n      module,\n      search,\n    };\n\n    let (outer, positional) = Evaluator::evaluate_parameters(\n      arguments,\n      &context,\n      is_dependency,\n      &recipe.parameters,\n      recipe,\n      scope,\n    )?;\n\n    let scope = outer.child();\n\n    let mut evaluator = Evaluator::new(&context, BTreeMap::new(), true, &scope);\n\n    Self::run_dependencies(\n      config,\n      &context,\n      recipe.priors(),\n      dotenv,\n      &mut evaluator,\n      ran,\n      recipe,\n      scopes,\n      search,\n    )?;\n\n    recipe.run(&context, &scope, &positional, is_dependency)?;\n\n    Self::run_dependencies(\n      config,\n      &context,\n      recipe.subsequents(),\n      dotenv,\n      &mut evaluator,\n      &Ran::default(),\n      recipe,\n      scopes,\n      search,\n    )?;\n\n    *guard = true;\n\n    Ok(())\n  }\n\n  fn run_dependencies<'run>(\n    config: &Config,\n    context: &ExecutionContext<'src, 'run>,\n    dependencies: &[Dependency<'src>],\n    dotenv: &BTreeMap<String, String>,\n    evaluator: &mut Evaluator<'src, 'run>,\n    ran: &Ran,\n    recipe: &Recipe<'src>,\n    scopes: &BTreeMap<Modulepath, (&Self, &Scope<'src, 'run>)>,\n    search: &Search,\n  ) -> RunResult<'src> {\n    if context.config.no_dependencies {\n      return Ok(());\n    }\n\n    let mut evaluated = Vec::new();\n    for Dependency { recipe, arguments } in dependencies {\n      let mut grouped = Vec::new();\n      for group in arguments {\n        let evaluated_group = group\n          .iter()\n          .map(|argument| evaluator.evaluate_expression(argument))\n          .collect::<RunResult<Vec<String>>>()?;\n        grouped.push(evaluated_group);\n      }\n      evaluated.push((recipe, grouped));\n    }\n\n    if recipe.is_parallel() {\n      thread::scope::<_, RunResult>(|thread_scope| {\n        let mut handles = Vec::new();\n        for (recipe, arguments) in evaluated {\n          handles.push(thread_scope.spawn(move || {\n            Self::run_recipe(\n              &arguments, config, dotenv, true, ran, recipe, scopes, search,\n            )\n          }));\n        }\n        for handle in handles {\n          handle\n            .join()\n            .map_err(|_| Error::internal(\"parallel dependency thread panicked\"))??;\n        }\n        Ok(())\n      })?;\n    } else {\n      for (recipe, arguments) in evaluated {\n        Self::run_recipe(\n          &arguments, config, dotenv, true, ran, recipe, scopes, search,\n        )?;\n      }\n    }\n\n    Ok(())\n  }\n\n  pub(crate) fn public_modules(&self, config: &Config) -> Vec<&Justfile> {\n    let mut modules = self\n      .modules\n      .values()\n      .filter(|module| !module.private)\n      .collect::<Vec<&Justfile>>();\n\n    if config.unsorted {\n      modules.sort_by_key(|module| {\n        module\n          .name\n          .map(|name| name.token.offset)\n          .unwrap_or_default()\n      });\n    }\n\n    modules\n  }\n\n  pub(crate) fn public_recipes(&self, config: &Config) -> Vec<&Recipe> {\n    let mut recipes = self\n      .recipes\n      .values()\n      .map(AsRef::as_ref)\n      .filter(|recipe| recipe.is_public())\n      .collect::<Vec<&Recipe>>();\n\n    if config.unsorted {\n      recipes.sort_by_key(|recipe| (&recipe.import_offsets, recipe.name.offset));\n    }\n\n    recipes\n  }\n\n  pub(crate) fn groups(&self) -> Vec<&str> {\n    self\n      .groups\n      .iter()\n      .map(|group| group.cooked.as_str())\n      .collect()\n  }\n\n  pub(crate) fn public_groups(&self, config: &Config) -> Vec<String> {\n    let mut groups = Vec::new();\n\n    for recipe in self.recipes.values() {\n      if recipe.is_public() {\n        for group in recipe.groups() {\n          groups.push((recipe.import_offsets.as_slice(), recipe.name.offset, group));\n        }\n      }\n    }\n\n    for submodule in self.public_modules(config) {\n      for group in submodule.groups() {\n        groups.push((&[], submodule.name.unwrap().offset, group.to_string()));\n      }\n    }\n\n    if config.unsorted {\n      groups.sort();\n    } else {\n      groups.sort_by(|(_, _, a), (_, _, b)| a.cmp(b));\n    }\n\n    let mut seen = HashSet::new();\n\n    groups.retain(|(_, _, group)| seen.insert(group.clone()));\n\n    groups.into_iter().map(|(_, _, group)| group).collect()\n  }\n}\n\nimpl ColorDisplay for Justfile<'_> {\n  fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {\n    let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len();\n    for (name, assignment) in &self.assignments {\n      if assignment.export {\n        write!(f, \"export \")?;\n      }\n      write!(f, \"{name} := {}\", assignment.value)?;\n      items -= 1;\n      if items != 0 {\n        write!(f, \"\\n\\n\")?;\n      }\n    }\n    for alias in self.aliases.values() {\n      write!(f, \"{alias}\")?;\n      items -= 1;\n      if items != 0 {\n        write!(f, \"\\n\\n\")?;\n      }\n    }\n    for recipe in self.recipes.values() {\n      write!(f, \"{}\", recipe.color_display(color))?;\n      items -= 1;\n      if items != 0 {\n        write!(f, \"\\n\\n\")?;\n      }\n    }\n    Ok(())\n  }\n}\n\nimpl<'src> Keyed<'src> for Justfile<'src> {\n  fn key(&self) -> &'src str {\n    self.name()\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use {super::*, Error::*, testing::compile};\n\n  run_error! {\n    name: unknown_recipe_no_suggestion,\n    src: \"a:\\nb:\\nc:\",\n    args: [\"a\", \"xyz\", \"y\", \"z\"],\n    error: UnknownRecipe {\n      recipe,\n      suggestion,\n    },\n    check: {\n      assert_eq!(recipe, \"xyz\");\n      assert_eq!(suggestion, None);\n    }\n  }\n\n  run_error! {\n    name: unknown_recipe_with_suggestion,\n    src: \"a:\\nb:\\nc:\",\n    args: [\"a\", \"x\", \"y\", \"z\"],\n    error: UnknownRecipe {\n      recipe,\n      suggestion,\n    },\n    check: {\n      assert_eq!(recipe, \"x\");\n      assert_eq!(suggestion, Some(Suggestion {\n        name: \"a\",\n        target: None,\n      }));\n    }\n  }\n\n  run_error! {\n    name: unknown_recipe_show_alias_suggestion,\n    src: \"\n      foo:\n        echo foo\n\n      alias z := foo\n    \",\n    args: [\"zz\"],\n    error: UnknownRecipe {\n      recipe,\n      suggestion,\n    },\n    check: {\n      assert_eq!(recipe, \"zz\");\n      assert_eq!(suggestion, Some(Suggestion {\n        name: \"z\",\n        target: Some(\"foo\"),\n      }\n    ));\n    }\n  }\n\n  run_error! {\n    name: code_error,\n    src: \"\n      fail:\n        @exit 100\n    \",\n    args: [\"fail\"],\n    error: Code {\n      recipe,\n      line_number,\n      code,\n      print_message,\n    },\n    check: {\n      assert_eq!(recipe, \"fail\");\n      assert_eq!(code, 100);\n      assert_eq!(line_number, Some(2));\n      assert!(print_message);\n    }\n  }\n\n  run_error! {\n    name: run_args,\n    src: r#\"\n      a return code:\n        @x() { {{return}} {{code + \"0\"}}; }; x\n    \"#,\n    args: [\"a\", \"return\", \"15\"],\n    error: Code {\n      recipe,\n      line_number,\n      code,\n      print_message,\n    },\n    check: {\n      assert_eq!(recipe, \"a\");\n      assert_eq!(code, 150);\n      assert_eq!(line_number, Some(2));\n      assert!(print_message);\n    }\n  }\n\n  run_error! {\n    name: missing_some_arguments,\n    src: \"a b c d:\",\n    args: [\"a\", \"b\", \"c\"],\n    error: PositionalArgumentCountMismatch {\n      recipe,\n      found,\n      min,\n      max,\n    },\n    check: {\n      assert_eq!(recipe.name(), \"a\");\n      assert_eq!(found, 2);\n      assert_eq!(min, 3);\n      assert_eq!(max, 3);\n    }\n  }\n\n  run_error! {\n    name: missing_some_arguments_variadic,\n    src: \"a b c +d:\",\n    args: [\"a\", \"B\", \"C\"],\n    error: PositionalArgumentCountMismatch {\n      recipe,\n      found,\n      min,\n      max,\n    },\n    check: {\n      assert_eq!(recipe.name(), \"a\");\n      assert_eq!(found, 2);\n      assert_eq!(min, 3);\n      assert_eq!(max, usize::MAX - 1);\n    }\n  }\n\n  run_error! {\n    name: missing_all_arguments,\n    src: \"a b c d:\\n echo {{b}}{{c}}{{d}}\",\n    args: [\"a\"],\n    error: PositionalArgumentCountMismatch {\n      recipe,\n      found,\n      min,\n      max,\n    },\n    check: {\n      assert_eq!(recipe.name(), \"a\");\n      assert_eq!(found, 0);\n      assert_eq!(min, 3);\n      assert_eq!(max, 3);\n    }\n  }\n\n  run_error! {\n    name: missing_some_defaults,\n    src: \"a b c d='hello':\",\n    args: [\"a\", \"b\"],\n    error: PositionalArgumentCountMismatch {\n      recipe,\n      found,\n      min,\n      max,\n    },\n    check: {\n      assert_eq!(recipe.name(), \"a\");\n      assert_eq!(found, 1);\n      assert_eq!(min, 2);\n      assert_eq!(max, 3);\n    }\n  }\n\n  run_error! {\n    name: missing_all_defaults,\n    src: \"a b c='r' d='h':\",\n    args: [\"a\"],\n    error: PositionalArgumentCountMismatch {\n      recipe,\n      found,\n      min,\n      max,\n    },\n    check: {\n      assert_eq!(recipe.name(), \"a\");\n      assert_eq!(found, 0);\n      assert_eq!(min, 1);\n      assert_eq!(max, 3);\n    }\n  }\n\n  run_error! {\n    name: export_failure,\n    src: r#\"\n      export foo := \"a\"\n      baz := \"c\"\n      export bar := \"b\"\n      export abc := foo + bar + baz\n\n      wut:\n        echo $foo $bar $baz\n    \"#,\n    args: [\"--quiet\", \"wut\"],\n    error: Code {\n      recipe,\n      line_number,\n      print_message,\n      ..\n    },\n    check: {\n      assert_eq!(recipe, \"wut\");\n      assert_eq!(line_number, Some(7));\n      assert!(print_message);\n    }\n  }\n\n  fn case(input: &str, expected: &str) {\n    let justfile = compile(input);\n    let actual = format!(\"{}\", justfile.color_display(Color::never()));\n    assert_eq!(actual, expected);\n    let reparsed = compile(&actual);\n    let redumped = format!(\"{}\", reparsed.color_display(Color::never()));\n    assert_eq!(redumped, actual);\n  }\n\n  #[test]\n  fn parse_empty() {\n    case(\n      \"\n\n# hello\n\n\n    \",\n      \"\",\n    );\n  }\n\n  #[test]\n  fn parse_string_default() {\n    case(\n      r#\"\n\nfoo a=\"b\\t\":\n\n\n  \"#,\n      r#\"foo a=\"b\\t\":\"#,\n    );\n  }\n\n  #[test]\n  fn parse_multiple() {\n    case(\n      r\"\na:\nb:\n\", r\"a:\n\nb:\",\n    );\n  }\n\n  #[test]\n  fn parse_variadic() {\n    case(\n      r\"\n\nfoo +a:\n\n\n  \",\n      r\"foo +a:\",\n    );\n  }\n\n  #[test]\n  fn parse_variadic_string_default() {\n    case(\n      r#\"\n\nfoo +a=\"Hello\":\n\n\n  \"#,\n      r#\"foo +a=\"Hello\":\"#,\n    );\n  }\n\n  #[test]\n  fn parse_raw_string_default() {\n    case(\n      r\"\n\nfoo a='b\\t':\n\n\n  \",\n      r\"foo a='b\\t':\",\n    );\n  }\n\n  #[test]\n  fn parse_export() {\n    case(\n      r#\"\nexport a := \"hello\"\n\n  \"#,\n      r#\"export a := \"hello\"\"#,\n    );\n  }\n\n  #[test]\n  fn parse_alias_after_target() {\n    case(\n      r\"\nfoo:\n  echo a\nalias f := foo\n\",\n      r\"alias f := foo\n\nfoo:\n    echo a\",\n    );\n  }\n\n  #[test]\n  fn parse_alias_before_target() {\n    case(\n      r\"\nalias f := foo\nfoo:\n  echo a\n\",\n      r\"alias f := foo\n\nfoo:\n    echo a\",\n    );\n  }\n\n  #[test]\n  fn parse_alias_with_comment() {\n    case(\n      r\"\nalias f := foo #comment\nfoo:\n  echo a\n\",\n      r\"alias f := foo\n\nfoo:\n    echo a\",\n    );\n  }\n\n  #[test]\n  fn parse_complex() {\n    case(\n      \"\nx:\ny:\nz:\nfoo := \\\"xx\\\"\nbar := foo\ngoodbye := \\\"y\\\"\nhello a b    c   : x y    z #hello\n  #! blah\n  #blarg\n  {{ foo + bar}}abc{{ goodbye\\t  + \\\"x\\\" }}xyz\n  1\n  2\n  3\n\",\n      \"bar := foo\n\nfoo := \\\"xx\\\"\n\ngoodbye := \\\"y\\\"\n\nhello a b c: x y z\n    #! blah\n    #blarg\n    {{ foo + bar }}abc{{ goodbye + \\\"x\\\" }}xyz\n    1\n    2\n    3\n\nx:\n\ny:\n\nz:\",\n    );\n  }\n\n  #[test]\n  fn parse_shebang() {\n    case(\n      \"\npracticum := 'hello'\ninstall:\n\\t#!/bin/sh\n\\tif [[ -f {{practicum}} ]]; then\n\\t\\treturn\n\\tfi\n\",\n      \"practicum := 'hello'\n\ninstall:\n    #!/bin/sh\n    if [[ -f {{ practicum }} ]]; then\n    \\treturn\n    fi\",\n    );\n  }\n\n  #[test]\n  fn parse_simple_shebang() {\n    case(\"a:\\n #!\\n  print(1)\", \"a:\\n    #!\\n     print(1)\");\n  }\n\n  #[test]\n  fn parse_assignments() {\n    case(\n      r#\"a := \"0\"\nc := a + b + a + b\nb := \"1\"\n\"#,\n      r#\"a := \"0\"\n\nb := \"1\"\n\nc := a + b + a + b\"#,\n    );\n  }\n\n  #[test]\n  fn parse_assignment_backticks() {\n    case(\n      \"a := `echo hello`\nc := a + b + a + b\nb := `echo goodbye`\",\n      \"a := `echo hello`\n\nb := `echo goodbye`\n\nc := a + b + a + b\",\n    );\n  }\n\n  #[test]\n  fn parse_interpolation_backticks() {\n    case(\n      r#\"a:\n  echo {{  `echo hello` + \"blarg\"   }} {{   `echo bob`   }}\"#,\n      r#\"a:\n    echo {{ `echo hello` + \"blarg\" }} {{ `echo bob` }}\"#,\n    );\n  }\n\n  #[test]\n  fn eof_test() {\n    case(\"x:\\ny:\\nz:\\na b c: x y z\", \"a b c: x y z\\n\\nx:\\n\\ny:\\n\\nz:\");\n  }\n\n  #[test]\n  fn string_quote_escape() {\n    case(r#\"a := \"hello\\\"\"\"#, r#\"a := \"hello\\\"\"\"#);\n  }\n\n  #[test]\n  fn string_escapes() {\n    case(r#\"a := \"\\n\\t\\r\\\"\\\\\"\"#, r#\"a := \"\\n\\t\\r\\\"\\\\\"\"#);\n  }\n\n  #[test]\n  fn parameters() {\n    case(\n      \"a b c:\n  {{b}} {{c}}\",\n      \"a b c:\n    {{ b }} {{ c }}\",\n    );\n  }\n\n  #[test]\n  fn unary_functions() {\n    case(\n      \"\nx := arch()\n\na:\n  {{os()}} {{os_family()}} {{num_cpus()}}\",\n      \"x := arch()\n\na:\n    {{ os() }} {{ os_family() }} {{ num_cpus() }}\",\n    );\n  }\n\n  #[test]\n  fn env_functions() {\n    case(\n      r#\"\nx := env_var('foo',)\n\na:\n  {{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var(\"baz\"))}}\"#,\n      r#\"x := env_var('foo')\n\na:\n    {{ env_var_or_default('foo' + 'bar', 'baz') }} {{ env_var(env_var(\"baz\")) }}\"#,\n    );\n  }\n\n  #[test]\n  fn parameter_default_string() {\n    case(\n      r#\"\nf x=\"abc\":\n\"#,\n      r#\"f x=\"abc\":\"#,\n    );\n  }\n\n  #[test]\n  fn parameter_default_raw_string() {\n    case(\n      r\"\nf x='abc':\n\",\n      r\"f x='abc':\",\n    );\n  }\n\n  #[test]\n  fn parameter_default_backtick() {\n    case(\n      r\"\nf x=`echo hello`:\n\",\n      r\"f x=`echo hello`:\",\n    );\n  }\n\n  #[test]\n  fn parameter_default_concatenation_string() {\n    case(\n      r#\"\nf x=(`echo hello` + \"foo\"):\n\"#,\n      r#\"f x=(`echo hello` + \"foo\"):\"#,\n    );\n  }\n\n  #[test]\n  fn parameter_default_concatenation_variable() {\n    case(\n      r#\"\nx := \"10\"\nf y=(`echo hello` + x) +z=\"foo\":\n\"#,\n      r#\"x := \"10\"\n\nf y=(`echo hello` + x) +z=\"foo\":\"#,\n    );\n  }\n\n  #[test]\n  fn parameter_default_multiple() {\n    case(\n      r#\"\nx := \"10\"\nf y=(`echo hello` + x) +z=(\"foo\" + \"bar\"):\n\"#,\n      r#\"x := \"10\"\n\nf y=(`echo hello` + x) +z=(\"foo\" + \"bar\"):\"#,\n    );\n  }\n\n  #[test]\n  fn concatenation_in_group() {\n    case(\"x := ('0' + '1')\", \"x := ('0' + '1')\");\n  }\n\n  #[test]\n  fn string_in_group() {\n    case(\"x := ('0'   )\", \"x := ('0')\");\n  }\n\n  #[rustfmt::skip]\n  #[test]\n  fn escaped_dos_newlines() {\n    case(\"@spam:\\r\n\\t{ \\\\\\r\n\\t\\tfiglet test; \\\\\\r\n\\t\\tcargo build --color always 2>&1; \\\\\\r\n\\t\\tcargo test  --color always -- --color always 2>&1; \\\\\\r\n\\t} | less\\r\n\",\n\"@spam:\n    { \\\\\n    \\tfiglet test; \\\\\n    \\tcargo build --color always 2>&1; \\\\\n    \\tcargo test  --color always -- --color always 2>&1; \\\\\n    } | less\");\n  }\n}\n"
  },
  {
    "path": "src/keyed.rs",
    "content": "use super::*;\n\npub(crate) trait Keyed<'key> {\n  fn key(&self) -> &'key str;\n}\n\nimpl<'key, T: Keyed<'key>> Keyed<'key> for Arc<T> {\n  fn key(&self) -> &'key str {\n    self.as_ref().key()\n  }\n}\n\npub(crate) fn serialize<'src, S, K>(keyed: &K, serializer: S) -> Result<S::Ok, S::Error>\nwhere\n  S: Serializer,\n  K: Keyed<'src>,\n{\n  serializer.serialize_str(keyed.key())\n}\n\n#[rustversion::attr(since(1.83), allow(clippy::ref_option))]\npub(crate) fn serialize_option<'src, S, K>(\n  recipe: &Option<K>,\n  serializer: S,\n) -> Result<S::Ok, S::Error>\nwhere\n  S: Serializer,\n  K: Keyed<'src>,\n{\n  match recipe {\n    None => serializer.serialize_none(),\n    Some(keyed) => serialize(keyed, serializer),\n  }\n}\n"
  },
  {
    "path": "src/keyword.rs",
    "content": "use super::*;\n\n#[derive(Debug, Eq, PartialEq, IntoStaticStr, Display, Copy, Clone, EnumString)]\n#[strum(serialize_all = \"kebab_case\")]\npub(crate) enum Keyword {\n  Alias,\n  AllowDuplicateRecipes,\n  AllowDuplicateVariables,\n  Assert,\n  DotenvFilename,\n  DotenvLoad,\n  DotenvOverride,\n  DotenvPath,\n  DotenvRequired,\n  Eager,\n  Else,\n  Export,\n  F,\n  Fallback,\n  False,\n  Guards,\n  If,\n  IgnoreComments,\n  Import,\n  Lazy,\n  Mod,\n  NoExitMessage,\n  PositionalArguments,\n  Quiet,\n  ScriptInterpreter,\n  Set,\n  Shell,\n  Tempdir,\n  True,\n  Unexport,\n  Unstable,\n  WindowsPowershell,\n  WindowsShell,\n  WorkingDirectory,\n  X,\n}\n\nimpl Keyword {\n  pub(crate) fn from_lexeme(lexeme: &str) -> Option<Self> {\n    lexeme.parse().ok()\n  }\n\n  pub(crate) fn lexeme(self) -> &'static str {\n    self.into()\n  }\n}\n\nimpl<'a> PartialEq<&'a str> for Keyword {\n  fn eq(&self, other: &&'a str) -> bool {\n    self.lexeme() == *other\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn keyword_case() {\n    assert_eq!(Keyword::X.lexeme(), \"x\");\n    assert_eq!(Keyword::IgnoreComments.lexeme(), \"ignore-comments\");\n  }\n}\n"
  },
  {
    "path": "src/lexer.rs",
    "content": "use {super::*, CompileErrorKind::*, TokenKind::*};\n\n/// Just language lexer\n///\n/// The lexer proceeds character-by-character, as opposed to using regular\n/// expressions to lex tokens or semi-tokens at a time. As a result, it is\n/// verbose and straightforward. Just used to have a regex-based lexer, which\n/// was slower and generally godawful.  However, this should not be taken as a\n/// slight against regular expressions, the lexer was just idiosyncratically\n/// bad.\npub(crate) struct Lexer<'src> {\n  /// Char iterator\n  chars: Chars<'src>,\n  /// Indentation stack\n  indentation: Vec<&'src str>,\n  /// Interpolation token start stack\n  interpolation_stack: Vec<Token<'src>>,\n  /// Next character to be lexed\n  next: Option<char>,\n  /// Current open delimiters\n  open_delimiters: Vec<(Delimiter, usize)>,\n  /// Path to source file\n  path: &'src Path,\n  /// Inside recipe body\n  recipe_body: bool,\n  /// Next indent will start a recipe body\n  recipe_body_pending: bool,\n  /// Source text\n  src: &'src str,\n  /// Current token end\n  token_end: Position,\n  /// Current token start\n  token_start: Position,\n  /// Tokens\n  tokens: Vec<Token<'src>>,\n}\n\nimpl<'src> Lexer<'src> {\n  pub(crate) const INTERPOLATION_END: &'static str = \"}}\";\n  pub(crate) const INTERPOLATION_ESCAPE: &'static str = \"{{{{\";\n  pub(crate) const INTERPOLATION_START: &'static str = \"{{\";\n\n  /// Lex `src`\n  pub(crate) fn lex(path: &'src Path, src: &'src str) -> CompileResult<'src, Vec<Token<'src>>> {\n    Self::new(path, src).tokenize()\n  }\n\n  #[cfg(test)]\n  pub(crate) fn test_lex(src: &'src str) -> CompileResult<'src, Vec<Token<'src>>> {\n    Self::new(\"justfile\".as_ref(), src).tokenize()\n  }\n\n  /// Create a new Lexer to lex `src`\n  fn new(path: &'src Path, src: &'src str) -> Self {\n    let mut chars = src.chars();\n    let next = chars.next();\n\n    let start = Position {\n      offset: 0,\n      column: 0,\n      line: 0,\n    };\n\n    Self {\n      indentation: vec![\"\"],\n      tokens: Vec::new(),\n      token_start: start,\n      token_end: start,\n      recipe_body_pending: false,\n      recipe_body: false,\n      interpolation_stack: Vec::new(),\n      open_delimiters: Vec::new(),\n      chars,\n      next,\n      src,\n      path,\n    }\n  }\n\n  /// Advance over the character in `self.next`, updating `self.token_end`\n  /// accordingly.\n  fn advance(&mut self) -> CompileResult<'src> {\n    match self.next {\n      Some(c) => {\n        let len_utf8 = c.len_utf8();\n\n        self.token_end.offset += len_utf8;\n        self.token_end.column += len_utf8;\n\n        if c == '\\n' {\n          self.token_end.column = 0;\n          self.token_end.line += 1;\n        }\n\n        self.next = self.chars.next();\n\n        Ok(())\n      }\n      None => Err(self.internal_error(\"Lexer advanced past end of text\")),\n    }\n  }\n\n  /// Lexeme of in-progress token\n  fn lexeme(&self) -> &'src str {\n    &self.src[self.token_start.offset..self.token_end.offset]\n  }\n\n  /// Length of current token\n  fn current_token_length(&self) -> usize {\n    self.token_end.offset - self.token_start.offset\n  }\n\n  fn accepted(&mut self, c: char) -> CompileResult<'src, bool> {\n    if self.next_is(c) {\n      self.advance()?;\n      Ok(true)\n    } else {\n      Ok(false)\n    }\n  }\n\n  fn presume(&mut self, c: char) -> CompileResult<'src> {\n    if !self.next_is(c) {\n      return Err(self.internal_error(format!(\"Lexer presumed character `{c}`\")));\n    }\n\n    self.advance()?;\n\n    Ok(())\n  }\n\n  fn presume_str(&mut self, s: &str) -> CompileResult<'src> {\n    for c in s.chars() {\n      self.presume(c)?;\n    }\n\n    Ok(())\n  }\n\n  /// Is next character c?\n  fn next_is(&self, c: char) -> bool {\n    self.next == Some(c)\n  }\n\n  /// Is next character ' ' or '\\t'?\n  fn next_is_whitespace(&self) -> bool {\n    self.next_is(' ') || self.next_is('\\t')\n  }\n\n  /// Un-lexed text\n  fn rest(&self) -> &'src str {\n    &self.src[self.token_end.offset..]\n  }\n\n  /// Check if unlexed text begins with prefix\n  fn rest_starts_with(&self, prefix: &str) -> bool {\n    self.rest().starts_with(prefix)\n  }\n\n  /// Does rest start with \"\\n\" or \"\\r\\n\"?\n  fn at_eol(&self) -> bool {\n    self.next_is('\\n') || self.rest_starts_with(\"\\r\\n\")\n  }\n\n  /// Are we at end-of-file?\n  fn at_eof(&self) -> bool {\n    self.rest().is_empty()\n  }\n\n  /// Are we at end-of-line or end-of-file?\n  fn at_eol_or_eof(&self) -> bool {\n    self.at_eol() || self.at_eof()\n  }\n\n  /// Get current indentation\n  fn indentation(&self) -> &'src str {\n    self.indentation.last().unwrap()\n  }\n\n  /// Are we currently indented\n  fn indented(&self) -> bool {\n    !self.indentation().is_empty()\n  }\n\n  /// Create a new token with `kind` whose lexeme is between `self.token_start`\n  /// and `self.token_end`\n  fn token(&mut self, kind: TokenKind) {\n    self.tokens.push(Token {\n      column: self.token_start.column,\n      kind,\n      length: self.token_end.offset - self.token_start.offset,\n      line: self.token_start.line,\n      offset: self.token_start.offset,\n      path: self.path,\n      src: self.src,\n    });\n\n    // Set `token_start` to point after the lexed token\n    self.token_start = self.token_end;\n  }\n\n  /// Create an internal error with `message`\n  fn internal_error(&self, message: impl Into<String>) -> CompileError<'src> {\n    // Use `self.token_end` as the location of the error\n    let token = Token {\n      src: self.src,\n      offset: self.token_end.offset,\n      line: self.token_end.line,\n      column: self.token_end.column,\n      length: 0,\n      kind: Unspecified,\n      path: self.path,\n    };\n    CompileError::new(\n      token,\n      Internal {\n        message: message.into(),\n      },\n    )\n  }\n\n  /// Create a compilation error with `kind`\n  fn error(&self, kind: CompileErrorKind<'src>) -> CompileError<'src> {\n    // Use the in-progress token span as the location of the error.\n\n    // The width of the error site to highlight depends on the kind of error:\n    let length = match kind {\n      UnterminatedString | UnterminatedBacktick => {\n        let Some(kind) = StringKind::from_token_start(self.lexeme()) else {\n          return self.internal_error(\"Lexer::error: expected string or backtick token start\");\n        };\n        kind.delimiter().len()\n      }\n      // highlight the full token\n      _ => self.lexeme().len(),\n    };\n\n    let token = Token {\n      kind: Unspecified,\n      src: self.src,\n      offset: self.token_start.offset,\n      line: self.token_start.line,\n      column: self.token_start.column,\n      length,\n      path: self.path,\n    };\n\n    CompileError::new(token, kind)\n  }\n\n  fn unterminated_interpolation_error(interpolation_start: Token<'src>) -> CompileError<'src> {\n    CompileError::new(interpolation_start, UnterminatedInterpolation)\n  }\n\n  /// True if `c` can be the first character of an identifier\n  pub(crate) fn is_identifier_start(c: char) -> bool {\n    matches!(c, 'a'..='z' | 'A'..='Z' | '_')\n  }\n\n  /// True if `c` can be a continuation character of an identifier\n  pub(crate) fn is_identifier_continue(c: char) -> bool {\n    Self::is_identifier_start(c) || matches!(c, '0'..='9' | '-')\n  }\n\n  /// Consume the text and produce a series of tokens\n  fn tokenize(mut self) -> CompileResult<'src, Vec<Token<'src>>> {\n    loop {\n      if self.token_start.column == 0 {\n        self.lex_line_start()?;\n      }\n\n      match self.next {\n        Some(first) => {\n          if let Some(&interpolation_start) = self.interpolation_stack.last() {\n            self.lex_interpolation(interpolation_start, first)?;\n          } else if self.recipe_body {\n            self.lex_body()?;\n          } else {\n            self.lex_normal(first)?;\n          }\n        }\n        None => break,\n      }\n    }\n\n    if let Some(&interpolation_start) = self.interpolation_stack.last() {\n      return Err(Self::unterminated_interpolation_error(interpolation_start));\n    }\n\n    while self.indented() {\n      self.lex_dedent();\n    }\n\n    self.token(Eof);\n\n    assert_eq!(self.token_start.offset, self.token_end.offset);\n    assert_eq!(self.token_start.offset, self.src.len());\n    assert_eq!(self.indentation.len(), 1);\n\n    Ok(self.tokens)\n  }\n\n  /// Handle blank lines and indentation\n  fn lex_line_start(&mut self) -> CompileResult<'src> {\n    enum Indentation<'src> {\n      // Line only contains whitespace\n      Blank,\n      // Indentation continues\n      Continue,\n      // Indentation decreases\n      Decrease,\n      // Indentation isn't consistent\n      Inconsistent,\n      // Indentation increases\n      Increase,\n      // Indentation mixes spaces and tabs\n      Mixed { whitespace: &'src str },\n    }\n\n    use Indentation::*;\n\n    let nonblank_index = self\n      .rest()\n      .char_indices()\n      .skip_while(|&(_, c)| c == ' ' || c == '\\t')\n      .map(|(i, _)| i)\n      .next()\n      .unwrap_or_else(|| self.rest().len());\n\n    let rest = &self.rest()[nonblank_index..];\n\n    let whitespace = &self.rest()[..nonblank_index];\n\n    if self.open_delimiters_or_interpolation() {\n      if !whitespace.is_empty() {\n        while self.next_is_whitespace() {\n          self.advance()?;\n        }\n\n        self.token(Whitespace);\n      }\n\n      return Ok(());\n    }\n\n    let body_whitespace = &whitespace[..whitespace\n      .char_indices()\n      .take(self.indentation().chars().count())\n      .map(|(i, _c)| i)\n      .next()\n      .unwrap_or(0)];\n\n    let spaces = whitespace.chars().any(|c| c == ' ');\n    let tabs = whitespace.chars().any(|c| c == '\\t');\n\n    let body_spaces = body_whitespace.chars().any(|c| c == ' ');\n    let body_tabs = body_whitespace.chars().any(|c| c == '\\t');\n\n    #[allow(clippy::if_same_then_else)]\n    let indentation = if rest.starts_with('\\n') || rest.starts_with(\"\\r\\n\") || rest.is_empty() {\n      Blank\n    } else if whitespace == self.indentation() {\n      Continue\n    } else if self.indentation.contains(&whitespace) {\n      Decrease\n    } else if self.recipe_body && whitespace.starts_with(self.indentation()) {\n      Continue\n    } else if self.recipe_body && body_spaces && body_tabs {\n      Mixed {\n        whitespace: body_whitespace,\n      }\n    } else if !self.recipe_body && spaces && tabs {\n      Mixed { whitespace }\n    } else if whitespace.len() < self.indentation().len() {\n      Inconsistent\n    } else if self.recipe_body\n      && body_whitespace.len() >= self.indentation().len()\n      && !body_whitespace.starts_with(self.indentation())\n    {\n      Inconsistent\n    } else if whitespace.len() >= self.indentation().len()\n      && !whitespace.starts_with(self.indentation())\n    {\n      Inconsistent\n    } else {\n      Increase\n    };\n\n    match indentation {\n      Blank => {\n        if !whitespace.is_empty() {\n          while self.next_is_whitespace() {\n            self.advance()?;\n          }\n\n          self.token(Whitespace);\n        }\n\n        Ok(())\n      }\n      Continue => {\n        if !self.indentation().is_empty() {\n          for _ in self.indentation().chars() {\n            self.advance()?;\n          }\n\n          self.token(Whitespace);\n        }\n\n        Ok(())\n      }\n      Decrease => {\n        while self.indentation() != whitespace {\n          self.lex_dedent();\n        }\n\n        if !whitespace.is_empty() {\n          while self.next_is_whitespace() {\n            self.advance()?;\n          }\n\n          self.token(Whitespace);\n        }\n\n        Ok(())\n      }\n      Mixed { whitespace } => {\n        for _ in whitespace.chars() {\n          self.advance()?;\n        }\n\n        Err(self.error(MixedLeadingWhitespace { whitespace }))\n      }\n      Inconsistent => {\n        for _ in whitespace.chars() {\n          self.advance()?;\n        }\n\n        Err(self.error(InconsistentLeadingWhitespace {\n          expected: self.indentation(),\n          found: whitespace,\n        }))\n      }\n      Increase => {\n        while self.next_is_whitespace() {\n          self.advance()?;\n        }\n\n        let indentation = self.lexeme();\n        self.indentation.push(indentation);\n        self.token(Indent);\n        if self.recipe_body_pending {\n          self.recipe_body = true;\n        }\n\n        Ok(())\n      }\n    }\n  }\n\n  /// Lex token beginning with `start` outside of a recipe body\n  fn lex_normal(&mut self, start: char) -> CompileResult<'src> {\n    match start {\n      ' ' | '\\t' => self.lex_whitespace(),\n      '!' if self.rest().starts_with(\"!include\") => Err(self.error(Include)),\n      '!' => self.lex_choices('!', &[('=', BangEquals), ('~', BangTilde)], None),\n      '#' => self.lex_comment(),\n      '$' => self.lex_single(Dollar),\n      '&' => self.lex_digraph('&', '&', AmpersandAmpersand),\n      '(' => self.lex_delimiter(ParenL),\n      ')' => self.lex_delimiter(ParenR),\n      '*' => self.lex_single(Asterisk),\n      '+' => self.lex_single(Plus),\n      ',' => self.lex_single(Comma),\n      '/' => self.lex_single(Slash),\n      ':' => self.lex_colon(),\n      '=' => self.lex_choices(\n        '=',\n        &[('=', EqualsEquals), ('~', EqualsTilde)],\n        Some(Equals),\n      ),\n      '?' => self.lex_single(QuestionMark),\n      '@' => self.lex_single(At),\n      '[' => self.lex_delimiter(BracketL),\n      '\\\\' => self.lex_escape(),\n      '\\n' | '\\r' => self.lex_eol(),\n      '\\u{feff}' => self.lex_single(ByteOrderMark),\n      ']' => self.lex_delimiter(BracketR),\n      '`' | '\"' | '\\'' => self.lex_string(None),\n      '{' => self.lex_delimiter(BraceL),\n      '|' => self.lex_digraph('|', '|', BarBar),\n      '}' => {\n        let format_string_kind = self.open_delimiters.last().and_then(|(delimiter, _line)| {\n          if !self.rest().starts_with(Self::INTERPOLATION_END) {\n            None\n          } else if let Delimiter::FormatString(kind) = delimiter {\n            Some(kind)\n          } else {\n            None\n          }\n        });\n\n        if let Some(format_string_kind) = format_string_kind {\n          self.lex_string(Some(*format_string_kind))\n        } else {\n          self.lex_delimiter(BraceR)\n        }\n      }\n      _ if Self::is_identifier_start(start) => self.lex_identifier(),\n      _ => {\n        self.advance()?;\n        Err(self.error(UnknownStartOfToken { start }))\n      }\n    }\n  }\n\n  /// Lex token beginning with `start` inside an interpolation\n  fn lex_interpolation(\n    &mut self,\n    interpolation_start: Token<'src>,\n    start: char,\n  ) -> CompileResult<'src> {\n    if self.rest_starts_with(Self::INTERPOLATION_END) && self.open_delimiters.is_empty() {\n      // end current interpolation\n      if self.interpolation_stack.pop().is_none() {\n        self.presume_str(Self::INTERPOLATION_END)?;\n        return Err(self.internal_error(\n          \"Lexer::lex_interpolation found `}}` but was called with empty interpolation stack.\",\n        ));\n      }\n      // Emit interpolation end token\n      self.lex_double(InterpolationEnd)\n    } else if self.at_eof() && self.open_delimiters.is_empty() {\n      // Return unterminated interpolation error that highlights the opening\n      // {{\n      Err(Self::unterminated_interpolation_error(interpolation_start))\n    } else {\n      // Otherwise lex as per normal\n      self.lex_normal(start)\n    }\n  }\n\n  /// Lex token while in recipe body\n  fn lex_body(&mut self) -> CompileResult<'src> {\n    enum Terminator {\n      EndOfFile,\n      Interpolation,\n      Newline,\n      NewlineCarriageReturn,\n    }\n\n    use Terminator::*;\n\n    let terminator = loop {\n      if self.rest_starts_with(Self::INTERPOLATION_ESCAPE) {\n        self.presume_str(Self::INTERPOLATION_ESCAPE)?;\n        continue;\n      }\n\n      if self.rest_starts_with(\"\\n\") {\n        break Newline;\n      }\n\n      if self.rest_starts_with(\"\\r\\n\") {\n        break NewlineCarriageReturn;\n      }\n\n      if self.rest_starts_with(Self::INTERPOLATION_START) {\n        break Interpolation;\n      }\n\n      if self.at_eof() {\n        break EndOfFile;\n      }\n\n      self.advance()?;\n    };\n\n    // emit text token containing text so far\n    if self.current_token_length() > 0 {\n      self.token(Text);\n    }\n\n    match terminator {\n      Newline => self.lex_single(Eol),\n      NewlineCarriageReturn => self.lex_double(Eol),\n      Interpolation => {\n        self.lex_double(InterpolationStart)?;\n        self\n          .interpolation_stack\n          .push(self.tokens[self.tokens.len() - 1]);\n        Ok(())\n      }\n      EndOfFile => Ok(()),\n    }\n  }\n\n  fn lex_dedent(&mut self) {\n    assert_eq!(self.current_token_length(), 0);\n    self.token(Dedent);\n    self.indentation.pop();\n    self.recipe_body_pending = false;\n    self.recipe_body = false;\n  }\n\n  /// Lex a single-character token\n  fn lex_single(&mut self, kind: TokenKind) -> CompileResult<'src> {\n    self.advance()?;\n    self.token(kind);\n    Ok(())\n  }\n\n  /// Lex a double-character token\n  fn lex_double(&mut self, kind: TokenKind) -> CompileResult<'src> {\n    self.advance()?;\n    self.advance()?;\n    self.token(kind);\n    Ok(())\n  }\n\n  /// Lex a double-character token of kind `then` if the second character of\n  /// that token would be `second`, otherwise lex a single-character token of\n  /// kind `otherwise`\n  fn lex_choices(\n    &mut self,\n    first: char,\n    choices: &[(char, TokenKind)],\n    otherwise: Option<TokenKind>,\n  ) -> CompileResult<'src> {\n    self.presume(first)?;\n\n    for (second, then) in choices {\n      if self.accepted(*second)? {\n        self.token(*then);\n        return Ok(());\n      }\n    }\n\n    if let Some(token) = otherwise {\n      self.token(token);\n    } else {\n      // Emit an unspecified token to consume the current character,\n      self.token(Unspecified);\n\n      let expected = choices.iter().map(|choice| choice.0).collect();\n\n      if self.at_eof() {\n        return Err(self.error(UnexpectedEndOfToken { expected }));\n      }\n\n      // …and advance past another character,\n      self.advance()?;\n\n      // …so that the error we produce highlights the unexpected character.\n      return Err(self.error(UnexpectedCharacter { expected }));\n    }\n\n    Ok(())\n  }\n\n  /// Lex an opening or closing delimiter\n  fn lex_delimiter(&mut self, kind: TokenKind) -> CompileResult<'src> {\n    match kind {\n      BraceL => self.open_delimiter(Delimiter::Brace),\n      BraceR => self.close_delimiter(Delimiter::Brace)?,\n      BracketL => self.open_delimiter(Delimiter::Bracket),\n      BracketR => self.close_delimiter(Delimiter::Bracket)?,\n      ParenL => self.open_delimiter(Delimiter::Paren),\n      ParenR => self.close_delimiter(Delimiter::Paren)?,\n      _ => {\n        return Err(self.internal_error(format!(\n          \"Lexer::lex_delimiter called with non-delimiter token: `{kind}`\",\n        )));\n      }\n    }\n\n    // Emit the delimiter token\n    self.lex_single(kind)?;\n\n    Ok(())\n  }\n\n  /// Push a delimiter onto the open delimiter stack\n  fn open_delimiter(&mut self, delimiter: Delimiter) {\n    self\n      .open_delimiters\n      .push((delimiter, self.token_start.line));\n  }\n\n  /// Pop a delimiter from the open delimiter stack and error if incorrect type\n  fn close_delimiter(&mut self, close: Delimiter) -> CompileResult<'src> {\n    match self.open_delimiters.pop() {\n      Some((open, _)) if open == close => Ok(()),\n      Some((open, open_line)) => Err(self.error(MismatchedClosingDelimiter {\n        open,\n        close,\n        open_line,\n      })),\n      None => Err(self.error(UnexpectedClosingDelimiter { close })),\n    }\n  }\n\n  /// Return true if there are any unclosed delimiters\n  fn open_delimiters_or_interpolation(&self) -> bool {\n    !self.open_delimiters.is_empty() || !self.interpolation_stack.is_empty()\n  }\n\n  /// Lex a two-character digraph\n  fn lex_digraph(&mut self, left: char, right: char, token: TokenKind) -> CompileResult<'src> {\n    self.presume(left)?;\n\n    if self.accepted(right)? {\n      self.token(token);\n      Ok(())\n    } else {\n      // Emit an unspecified token to consume the current character,\n      self.token(Unspecified);\n\n      if self.at_eof() {\n        return Err(self.error(UnexpectedEndOfToken {\n          expected: vec![right],\n        }));\n      }\n\n      // …and advance past another character,\n      self.advance()?;\n\n      // …so that the error we produce highlights the unexpected character.\n      Err(self.error(UnexpectedCharacter {\n        expected: vec![right],\n      }))\n    }\n  }\n\n  /// Lex a token starting with ':'\n  fn lex_colon(&mut self) -> CompileResult<'src> {\n    self.presume(':')?;\n\n    if self.accepted('=')? {\n      self.token(ColonEquals);\n    } else if self.accepted(':')? {\n      self.token(ColonColon);\n    } else {\n      self.token(Colon);\n      self.recipe_body_pending = true;\n    }\n\n    Ok(())\n  }\n\n  /// Lex an token starting with '\\' escape\n  fn lex_escape(&mut self) -> CompileResult<'src> {\n    self.presume('\\\\')?;\n\n    // Treat newline escaped with \\ as whitespace\n    if self.accepted('\\n')? {\n      while self.next_is_whitespace() {\n        self.advance()?;\n      }\n      self.token(Whitespace);\n    } else if self.accepted('\\r')? {\n      if !self.accepted('\\n')? {\n        return Err(self.error(UnpairedCarriageReturn));\n      }\n      while self.next_is_whitespace() {\n        self.advance()?;\n      }\n      self.token(Whitespace);\n    } else if let Some(character) = self.next {\n      return Err(self.error(InvalidEscapeSequence { character }));\n    }\n\n    Ok(())\n  }\n\n  /// Lex a carriage return and line feed\n  fn lex_eol(&mut self) -> CompileResult<'src> {\n    if self.accepted('\\r')? {\n      if !self.accepted('\\n')? {\n        return Err(self.error(UnpairedCarriageReturn));\n      }\n    } else {\n      self.presume('\\n')?;\n    }\n\n    // Emit eol if there are no open delimiters, otherwise emit whitespace.\n    if self.open_delimiters_or_interpolation() {\n      self.token(Whitespace);\n    } else {\n      self.token(Eol);\n    }\n\n    Ok(())\n  }\n\n  /// Lex name: [a-zA-Z_][a-zA-Z0-9_]*\n  fn lex_identifier(&mut self) -> CompileResult<'src> {\n    self.advance()?;\n\n    while let Some(c) = self.next {\n      if !Self::is_identifier_continue(c) {\n        break;\n      }\n\n      self.advance()?;\n    }\n\n    self.token(Identifier);\n\n    Ok(())\n  }\n\n  /// Lex comment: #[^\\r\\n]\n  fn lex_comment(&mut self) -> CompileResult<'src> {\n    self.presume('#')?;\n\n    while !self.at_eol_or_eof() {\n      self.advance()?;\n    }\n\n    self.token(Comment);\n\n    Ok(())\n  }\n\n  /// Lex whitespace: [ \\t]+\n  fn lex_whitespace(&mut self) -> CompileResult<'src> {\n    while self.next_is_whitespace() {\n      self.advance()?;\n    }\n\n    self.token(Whitespace);\n\n    Ok(())\n  }\n\n  /// Lex a backtick, cooked string, or raw string.\n  ///\n  /// Backtick:      ``[^`]*``\n  /// Cooked string: \"[^\"]*\" # also processes escape sequences\n  /// Raw string:    '[^']*'\n  fn lex_string(&mut self, format_string_kind: Option<StringKind>) -> CompileResult<'src> {\n    let format = format_string_kind.is_some()\n      || self.tokens.last().is_some_and(|token| {\n        token.kind == TokenKind::Identifier && token.lexeme() == Keyword::F.lexeme()\n      });\n\n    let kind = if let Some(kind) = format_string_kind {\n      self.presume_str(Self::INTERPOLATION_END)?;\n      kind\n    } else {\n      let Some(kind) = StringKind::from_token_start(self.rest()) else {\n        self.advance()?;\n        return Err(self.internal_error(\"Lexer::lex_string: invalid string start\"));\n      };\n      self.presume_str(kind.delimiter())?;\n      kind\n    };\n\n    let mut escape = false;\n\n    loop {\n      if self.next.is_none() {\n        return Err(self.error(kind.unterminated_error_kind()));\n      } else if !escape && kind.processes_escape_sequences() && self.next_is('\\\\') {\n        escape = true;\n      } else if escape && kind.processes_escape_sequences() && self.next_is('u') {\n        escape = false;\n      } else if format && self.rest_starts_with(Self::INTERPOLATION_ESCAPE) {\n        escape = false;\n        self.advance()?;\n        self.advance()?;\n        self.advance()?;\n      } else if !escape\n        && (self.rest_starts_with(kind.delimiter())\n          || format && self.rest_starts_with(Self::INTERPOLATION_START))\n      {\n        break;\n      } else {\n        escape = false;\n      }\n\n      self.advance()?;\n    }\n\n    if format && self.rest_starts_with(Self::INTERPOLATION_START) {\n      self.presume_str(Self::INTERPOLATION_START)?;\n      if format_string_kind.is_some() {\n        self.token(FormatStringContinue);\n      } else {\n        self.token(FormatStringStart);\n        self.open_delimiter(Delimiter::FormatString(kind));\n      }\n    } else {\n      self.presume_str(kind.delimiter())?;\n\n      if let Some(format_string_kind) = format_string_kind {\n        self.close_delimiter(Delimiter::FormatString(format_string_kind))?;\n        self.token(FormatStringEnd);\n      } else {\n        self.token(kind.token_kind());\n      }\n    }\n\n    Ok(())\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  use pretty_assertions::assert_eq;\n\n  macro_rules! test {\n    {\n      name:     $name:ident,\n      text:     $text:expr,\n      tokens:   ($($kind:ident $(: $lexeme:literal)?),* $(,)?)$(,)?\n    } => {\n      #[test]\n      fn $name() {\n        let kinds: &[TokenKind] = &[$($kind,)* Eof];\n\n        let lexemes: &[&str] = &[$(lexeme!($kind $(, $lexeme)?),)* \"\"];\n\n        test($text, true, kinds, lexemes);\n      }\n    };\n    {\n      name:     $name:ident,\n      text:     $text:expr,\n      tokens:   ($($kind:ident $(: $lexeme:literal)?),* $(,)?)$(,)?\n      unindent: $unindent:expr,\n    } => {\n      #[test]\n      fn $name() {\n        let kinds: &[TokenKind] = &[$($kind,)* Eof];\n\n        let lexemes: &[&str] = &[$(lexeme!($kind $(, $lexeme)?),)* \"\"];\n\n        test($text, $unindent, kinds, lexemes);\n      }\n    }\n  }\n\n  macro_rules! lexeme {\n    {\n      $kind:ident, $lexeme:literal\n    } => {\n      $lexeme\n    };\n    {\n      $kind:ident\n    } => {\n      default_lexeme($kind)\n    }\n  }\n\n  #[track_caller]\n  fn test(text: &str, unindent_text: bool, want_kinds: &[TokenKind], want_lexemes: &[&str]) {\n    let text = if unindent_text {\n      unindent(text)\n    } else {\n      text.to_owned()\n    };\n\n    let have = Lexer::test_lex(&text).unwrap();\n\n    let have_kinds = have\n      .iter()\n      .map(|token| token.kind)\n      .collect::<Vec<TokenKind>>();\n\n    let have_lexemes = have.iter().map(Token::lexeme).collect::<Vec<&str>>();\n\n    assert_eq!(have_kinds, want_kinds, \"Token kind mismatch\");\n    assert_eq!(have_lexemes, want_lexemes, \"Token lexeme mismatch\");\n\n    let mut roundtrip = String::new();\n\n    for lexeme in have_lexemes {\n      roundtrip.push_str(lexeme);\n    }\n\n    assert_eq!(roundtrip, text, \"Roundtrip mismatch\");\n\n    let mut offset = 0;\n    let mut line = 0;\n    let mut column = 0;\n\n    for token in have {\n      assert_eq!(token.offset, offset);\n      assert_eq!(token.line, line);\n      assert_eq!(token.lexeme().len(), token.length);\n      assert_eq!(token.column, column);\n\n      for c in token.lexeme().chars() {\n        if c == '\\n' {\n          line += 1;\n          column = 0;\n        } else {\n          column += c.len_utf8();\n        }\n      }\n\n      offset += token.length;\n    }\n  }\n\n  fn default_lexeme(kind: TokenKind) -> &'static str {\n    match kind {\n      // Fixed lexemes\n      AmpersandAmpersand => \"&&\",\n      Asterisk => \"*\",\n      At => \"@\",\n      BangEquals => \"!=\",\n      BangTilde => \"!~\",\n      BarBar => \"||\",\n      BraceL => \"{\",\n      BraceR => \"}\",\n      BracketL => \"[\",\n      BracketR => \"]\",\n      ByteOrderMark => \"\\u{feff}\",\n      Colon => \":\",\n      ColonColon => \"::\",\n      ColonEquals => \":=\",\n      Comma => \",\",\n      Dollar => \"$\",\n      Eol => \"\\n\",\n      Equals => \"=\",\n      EqualsEquals => \"==\",\n      EqualsTilde => \"=~\",\n      Indent => \"  \",\n      InterpolationEnd => \"}}\",\n      InterpolationStart => \"{{\",\n      ParenL => \"(\",\n      ParenR => \")\",\n      Plus => \"+\",\n      QuestionMark => \"?\",\n      Slash => \"/\",\n      Whitespace => \" \",\n\n      // Empty lexemes\n      Dedent | Eof => \"\",\n\n      // Variable lexemes\n      Backtick | Comment | FormatStringContinue | FormatStringEnd | FormatStringStart\n      | Identifier | StringToken | Text | Unspecified => {\n        panic!(\"Token {kind:?} has no default lexeme\")\n      }\n    }\n  }\n\n  macro_rules! error {\n    (\n      name:   $name:ident,\n      input:  $input:expr,\n      offset: $offset:expr,\n      line:   $line:expr,\n      column: $column:expr,\n      width:  $width:expr,\n      kind:   $kind:expr,\n    ) => {\n      #[test]\n      fn $name() {\n        error($input, $offset, $line, $column, $width, $kind);\n      }\n    };\n  }\n\n  #[track_caller]\n  fn error(\n    src: &str,\n    offset: usize,\n    line: usize,\n    column: usize,\n    length: usize,\n    kind: CompileErrorKind,\n  ) {\n    match Lexer::test_lex(src) {\n      Ok(_) => panic!(\"Lexing succeeded but expected\"),\n      Err(have) => {\n        let want = CompileError {\n          token: Token {\n            kind: have.token.kind,\n            src,\n            offset,\n            line,\n            column,\n            length,\n            path: \"justfile\".as_ref(),\n          },\n          kind: kind.into(),\n        };\n        assert_eq!(have, want);\n      }\n    }\n  }\n\n  test! {\n    name:   name_new,\n    text:   \"foo\",\n    tokens: (Identifier:\"foo\"),\n  }\n\n  test! {\n    name:   comment,\n    text:   \"# hello\",\n    tokens: (Comment:\"# hello\"),\n  }\n\n  test! {\n    name:   backtick,\n    text:   \"`echo`\",\n    tokens: (Backtick:\"`echo`\"),\n  }\n\n  test! {\n    name:   backtick_multi_line,\n    text:   \"`echo\\necho`\",\n    tokens: (Backtick:\"`echo\\necho`\"),\n  }\n\n  test! {\n    name:   raw_string,\n    text:   \"'hello'\",\n    tokens: (StringToken:\"'hello'\"),\n  }\n\n  test! {\n    name:   raw_string_multi_line,\n    text:   \"'hello\\ngoodbye'\",\n    tokens: (StringToken:\"'hello\\ngoodbye'\"),\n  }\n\n  test! {\n    name:   cooked_string,\n    text:   \"\\\"hello\\\"\",\n    tokens: (StringToken:\"\\\"hello\\\"\"),\n  }\n\n  test! {\n    name:   cooked_string_multi_line,\n    text:   \"\\\"hello\\ngoodbye\\\"\",\n    tokens: (StringToken:\"\\\"hello\\ngoodbye\\\"\"),\n  }\n\n  test! {\n    name:   cooked_multiline_string,\n    text:   \"\\\"\\\"\\\"hello\\ngoodbye\\\"\\\"\\\"\",\n    tokens: (StringToken:\"\\\"\\\"\\\"hello\\ngoodbye\\\"\\\"\\\"\"),\n  }\n\n  test! {\n    name:   ampersand_ampersand,\n    text:   \"&&\",\n    tokens: (AmpersandAmpersand),\n  }\n\n  test! {\n    name:   equals,\n    text:   \"=\",\n    tokens: (Equals),\n  }\n\n  test! {\n    name:   equals_equals,\n    text:   \"==\",\n    tokens: (EqualsEquals),\n  }\n\n  test! {\n    name:   bang_equals,\n    text:   \"!=\",\n    tokens: (BangEquals),\n  }\n\n  test! {\n    name:   brace_l,\n    text:   \"{\",\n    tokens: (BraceL),\n  }\n\n  test! {\n    name:   brace_r,\n    text:   \"{}\",\n    tokens: (BraceL, BraceR),\n  }\n\n  test! {\n    name:   brace_lll,\n    text:   \"{{{\",\n    tokens: (BraceL, BraceL, BraceL),\n  }\n\n  test! {\n    name:   brace_rrr,\n    text:   \"{{{}}}\",\n    tokens: (BraceL, BraceL, BraceL, BraceR, BraceR, BraceR),\n  }\n\n  test! {\n    name:   dollar,\n    text:   \"$\",\n    tokens: (Dollar),\n  }\n\n  test! {\n    name:   export_concatenation,\n    text:   \"export foo = 'foo' + 'bar'\",\n    tokens: (\n      Identifier:\"export\",\n      Whitespace,\n      Identifier:\"foo\",\n      Whitespace,\n      Equals,\n      Whitespace,\n      StringToken:\"'foo'\",\n      Whitespace,\n      Plus,\n      Whitespace,\n      StringToken:\"'bar'\",\n    )\n  }\n\n  test! {\n    name: export_complex,\n    text: \"export foo = ('foo' + 'bar') + `baz`\",\n    tokens: (\n      Identifier:\"export\",\n      Whitespace,\n      Identifier:\"foo\",\n      Whitespace,\n      Equals,\n      Whitespace,\n      ParenL,\n      StringToken:\"'foo'\",\n      Whitespace,\n      Plus,\n      Whitespace,\n      StringToken:\"'bar'\",\n      ParenR,\n      Whitespace,\n      Plus,\n      Whitespace,\n      Backtick:\"`baz`\",\n    ),\n  }\n\n  test! {\n    name:     eol_linefeed,\n    text:     \"\\n\",\n    tokens:   (Eol),\n    unindent: false,\n  }\n\n  test! {\n    name:     eol_carriage_return_linefeed,\n    text:     \"\\r\\n\",\n    tokens:   (Eol:\"\\r\\n\"),\n    unindent: false,\n  }\n\n  test! {\n    name:   indented_line,\n    text:   \"foo:\\n a\",\n    tokens: (Identifier:\"foo\", Colon, Eol, Indent:\" \", Text:\"a\", Dedent),\n  }\n\n  test! {\n    name:   indented_normal,\n    text:   \"\n      a\n        b\n        c\n    \",\n    tokens: (\n      Identifier:\"a\",\n      Eol,\n      Indent:\"  \",\n      Identifier:\"b\",\n      Eol,\n      Whitespace:\"  \",\n      Identifier:\"c\",\n      Eol,\n      Dedent,\n    ),\n  }\n\n  test! {\n    name:   indented_normal_nonempty_blank,\n    text:   \"a\\n  b\\n\\t\\t\\n  c\\n\",\n    tokens: (\n      Identifier:\"a\",\n      Eol,\n      Indent:\"  \",\n      Identifier:\"b\",\n      Eol,\n      Whitespace:\"\\t\\t\",\n      Eol,\n      Whitespace:\"  \",\n      Identifier:\"c\",\n      Eol,\n      Dedent,\n    ),\n    unindent: false,\n  }\n\n  test! {\n    name:   indented_normal_multiple,\n    text:   \"\n      a\n        b\n          c\n    \",\n    tokens: (\n      Identifier:\"a\",\n      Eol,\n      Indent:\"  \",\n      Identifier:\"b\",\n      Eol,\n      Indent:\"    \",\n      Identifier:\"c\",\n      Eol,\n      Dedent,\n      Dedent,\n    ),\n  }\n\n  test! {\n    name:   indent_indent_dedent_indent,\n    text:   \"\n      a\n        b\n          c\n        d\n          e\n    \",\n    tokens: (\n      Identifier:\"a\",\n      Eol,\n      Indent:\"  \",\n        Identifier:\"b\",\n        Eol,\n        Indent:\"    \",\n          Identifier:\"c\",\n          Eol,\n        Dedent,\n        Whitespace:\"  \",\n        Identifier:\"d\",\n        Eol,\n        Indent:\"    \",\n          Identifier:\"e\",\n          Eol,\n        Dedent,\n      Dedent,\n    ),\n  }\n\n  test! {\n    name:   indent_recipe_dedent_indent,\n    text:   \"\n      a\n        b:\n          c\n        d\n          e\n    \",\n    tokens: (\n      Identifier:\"a\",\n      Eol,\n      Indent:\"  \",\n        Identifier:\"b\",\n        Colon,\n        Eol,\n        Indent:\"    \",\n          Text:\"c\",\n          Eol,\n        Dedent,\n        Whitespace:\"  \",\n        Identifier:\"d\",\n        Eol,\n        Indent:\"    \",\n          Identifier:\"e\",\n          Eol,\n        Dedent,\n      Dedent,\n    ),\n  }\n\n  test! {\n    name: indented_block,\n    text: \"\n      foo:\n        a\n        b\n        c\n    \",\n    tokens: (\n      Identifier:\"foo\",\n      Colon,\n      Eol,\n      Indent,\n      Text:\"a\",\n      Eol,\n      Whitespace:\"  \",\n      Text:\"b\",\n      Eol,\n      Whitespace:\"  \",\n      Text:\"c\",\n      Eol,\n      Dedent,\n    )\n  }\n\n  test! {\n    name: brace_escape,\n    text: \"\n      foo:\n        {{{{\n    \",\n    tokens: (\n      Identifier:\"foo\",\n      Colon,\n      Eol,\n      Indent,\n      Text:\"{{{{\",\n      Eol,\n      Dedent,\n    )\n  }\n\n  test! {\n    name: indented_block_followed_by_item,\n    text: \"\n      foo:\n        a\n      b:\n    \",\n    tokens: (\n      Identifier:\"foo\",\n      Colon,\n      Eol,\n      Indent,\n      Text:\"a\",\n      Eol,\n      Dedent,\n      Identifier:\"b\",\n      Colon,\n      Eol,\n    )\n  }\n\n  test! {\n    name: indented_block_followed_by_blank,\n    text: \"\n      foo:\n          a\n\n      b:\n    \",\n    tokens: (\n      Identifier:\"foo\",\n      Colon,\n      Eol,\n      Indent:\"    \",\n      Text:\"a\",\n      Eol,\n      Eol,\n      Dedent,\n      Identifier:\"b\",\n      Colon,\n      Eol,\n    ),\n  }\n\n  test! {\n    name: indented_line_containing_unpaired_carriage_return,\n    text: \"foo:\\n \\r \\n\",\n    tokens: (\n      Identifier:\"foo\",\n      Colon,\n      Eol,\n      Indent:\" \",\n      Text:\"\\r \",\n      Eol,\n      Dedent,\n    ),\n    unindent: false,\n  }\n\n  test! {\n    name: indented_blocks,\n    text: \"\n      b: a\n        @mv a b\n\n      a:\n        @touch F\n        @touch a\n\n      d: c\n        @rm c\n\n      c: b\n        @mv b c\n    \",\n    tokens: (\n      Identifier:\"b\",\n      Colon,\n      Whitespace,\n      Identifier:\"a\",\n      Eol,\n      Indent,\n      Text:\"@mv a b\",\n      Eol,\n      Eol,\n      Dedent,\n      Identifier:\"a\",\n      Colon,\n      Eol,\n      Indent,\n      Text:\"@touch F\",\n      Eol,\n      Whitespace:\"  \",\n      Text:\"@touch a\",\n      Eol,\n      Eol,\n      Dedent,\n      Identifier:\"d\",\n      Colon,\n      Whitespace,\n      Identifier:\"c\",\n      Eol,\n      Indent,\n      Text:\"@rm c\",\n      Eol,\n      Eol,\n      Dedent,\n      Identifier:\"c\",\n      Colon,\n      Whitespace,\n      Identifier:\"b\",\n      Eol,\n      Indent,\n      Text:\"@mv b c\",\n      Eol,\n      Dedent\n    ),\n  }\n\n  test! {\n    name: interpolation_empty,\n    text: \"hello:\\n echo {{}}\",\n    tokens: (\n      Identifier:\"hello\",\n      Colon,\n      Eol,\n      Indent:\" \",\n      Text:\"echo \",\n      InterpolationStart,\n      InterpolationEnd,\n      Dedent,\n    ),\n  }\n\n  test! {\n    name: interpolation_expression,\n    text: \"hello:\\n echo {{`echo hello` + `echo goodbye`}}\",\n    tokens: (\n      Identifier:\"hello\",\n      Colon,\n      Eol,\n      Indent:\" \",\n      Text:\"echo \",\n      InterpolationStart,\n      Backtick:\"`echo hello`\",\n      Whitespace,\n      Plus,\n      Whitespace,\n      Backtick:\"`echo goodbye`\",\n      InterpolationEnd,\n      Dedent,\n    ),\n  }\n\n  test! {\n    name: interpolation_raw_multiline_string,\n    text: \"hello:\\n echo {{'\\n'}}\",\n    tokens: (\n      Identifier:\"hello\",\n      Colon,\n      Eol,\n      Indent:\" \",\n      Text:\"echo \",\n      InterpolationStart,\n      StringToken:\"'\\n'\",\n      InterpolationEnd,\n      Dedent,\n    ),\n  }\n\n  test! {\n    name: tokenize_names,\n    text: \"\n      foo\n      bar-bob\n      b-bob_asdfAAAA\n      test123\n    \",\n    tokens: (\n      Identifier:\"foo\",\n      Eol,\n      Identifier:\"bar-bob\",\n      Eol,\n      Identifier:\"b-bob_asdfAAAA\",\n      Eol,\n      Identifier:\"test123\",\n      Eol,\n    ),\n  }\n\n  test! {\n    name: tokenize_indented_line,\n    text: \"foo:\\n a\",\n    tokens: (\n      Identifier:\"foo\",\n      Colon,\n      Eol,\n      Indent:\" \",\n      Text:\"a\",\n      Dedent,\n    ),\n  }\n\n  test! {\n    name: tokenize_indented_block,\n    text: \"\n      foo:\n        a\n        b\n        c\n    \",\n    tokens: (\n      Identifier:\"foo\",\n      Colon,\n      Eol,\n      Indent,\n      Text:\"a\",\n      Eol,\n      Whitespace:\"  \",\n      Text:\"b\",\n      Eol,\n      Whitespace:\"  \",\n      Text:\"c\",\n      Eol,\n      Dedent,\n    ),\n  }\n\n  test! {\n    name: tokenize_strings,\n    text: r#\"a = \"'a'\" + '\"b\"' + \"'c'\" + '\"d\"'#echo hello\"#,\n    tokens: (\n      Identifier:\"a\",\n      Whitespace,\n      Equals,\n      Whitespace,\n      StringToken:\"\\\"'a'\\\"\",\n      Whitespace,\n      Plus,\n      Whitespace,\n      StringToken:\"'\\\"b\\\"'\",\n      Whitespace,\n      Plus,\n      Whitespace,\n      StringToken:\"\\\"'c'\\\"\",\n      Whitespace,\n      Plus,\n      Whitespace,\n      StringToken:\"'\\\"d\\\"'\",\n      Comment:\"#echo hello\",\n    )\n  }\n\n  test! {\n    name: tokenize_recipe_interpolation_eol,\n    text: \"\n      foo: # some comment\n       {{hello}}\n    \",\n    tokens: (\n      Identifier:\"foo\",\n      Colon,\n      Whitespace,\n      Comment:\"# some comment\",\n      Eol,\n      Indent:\" \",\n      InterpolationStart,\n      Identifier:\"hello\",\n      InterpolationEnd,\n      Eol,\n      Dedent\n    ),\n  }\n\n  test! {\n    name: tokenize_recipe_interpolation_eof,\n    text: \"foo: # more comments\n {{hello}}\n# another comment\n\",\n    tokens: (\n      Identifier:\"foo\",\n      Colon,\n      Whitespace,\n      Comment:\"# more comments\",\n      Eol,\n      Indent:\" \",\n      InterpolationStart,\n      Identifier:\"hello\",\n      InterpolationEnd,\n      Eol,\n      Dedent,\n      Comment:\"# another comment\",\n      Eol,\n    ),\n  }\n\n  test! {\n    name: tokenize_recipe_complex_interpolation_expression,\n    text: \"foo: #lol\\n {{a + b + \\\"z\\\" + blarg}}\",\n    tokens: (\n      Identifier:\"foo\",\n      Colon,\n      Whitespace:\" \",\n      Comment:\"#lol\",\n      Eol,\n      Indent:\" \",\n      InterpolationStart,\n      Identifier:\"a\",\n      Whitespace,\n      Plus,\n      Whitespace,\n      Identifier:\"b\",\n      Whitespace,\n      Plus,\n      Whitespace,\n      StringToken:\"\\\"z\\\"\",\n      Whitespace,\n      Plus,\n      Whitespace,\n      Identifier:\"blarg\",\n      InterpolationEnd,\n      Dedent,\n    ),\n  }\n\n  test! {\n    name: tokenize_recipe_multiple_interpolations,\n    text: \"foo:,#ok\\n {{a}}0{{b}}1{{c}}\",\n    tokens: (\n      Identifier:\"foo\",\n      Colon,\n      Comma,\n      Comment:\"#ok\",\n      Eol,\n      Indent:\" \",\n      InterpolationStart,\n      Identifier:\"a\",\n      InterpolationEnd,\n      Text:\"0\",\n      InterpolationStart,\n      Identifier:\"b\",\n      InterpolationEnd,\n      Text:\"1\",\n      InterpolationStart,\n      Identifier:\"c\",\n      InterpolationEnd,\n      Dedent,\n\n    ),\n  }\n\n  test! {\n    name: tokenize_junk,\n    text: \"\n      bob\n\n      hello blah blah blah : a b c #whatever\n    \",\n    tokens: (\n      Identifier:\"bob\",\n      Eol,\n      Eol,\n      Identifier:\"hello\",\n      Whitespace,\n      Identifier:\"blah\",\n      Whitespace,\n      Identifier:\"blah\",\n      Whitespace,\n      Identifier:\"blah\",\n      Whitespace,\n      Colon,\n      Whitespace,\n      Identifier:\"a\",\n      Whitespace,\n      Identifier:\"b\",\n      Whitespace,\n      Identifier:\"c\",\n      Whitespace,\n      Comment:\"#whatever\",\n      Eol,\n    )\n  }\n\n  test! {\n    name: tokenize_empty_lines,\n    text: \"\n\n      # this does something\n      hello:\n        asdf\n        bsdf\n\n        csdf\n\n        dsdf # whatever\n\n      # yolo\n    \",\n    tokens: (\n      Eol,\n      Comment:\"# this does something\",\n      Eol,\n      Identifier:\"hello\",\n      Colon,\n      Eol,\n      Indent,\n      Text:\"asdf\",\n      Eol,\n      Whitespace:\"  \",\n      Text:\"bsdf\",\n      Eol,\n      Eol,\n      Whitespace:\"  \",\n      Text:\"csdf\",\n      Eol,\n      Eol,\n      Whitespace:\"  \",\n      Text:\"dsdf # whatever\",\n      Eol,\n      Eol,\n      Dedent,\n      Comment:\"# yolo\",\n      Eol,\n    ),\n  }\n\n  test! {\n    name: tokenize_comment_before_variable,\n    text: \"\n      #\n      A='1'\n      echo:\n        echo {{A}}\n    \",\n    tokens: (\n      Comment:\"#\",\n      Eol,\n      Identifier:\"A\",\n      Equals,\n      StringToken:\"'1'\",\n      Eol,\n      Identifier:\"echo\",\n      Colon,\n      Eol,\n      Indent,\n      Text:\"echo \",\n      InterpolationStart,\n      Identifier:\"A\",\n      InterpolationEnd,\n      Eol,\n      Dedent,\n    ),\n  }\n\n  test! {\n    name: tokenize_interpolation_backticks,\n    text: \"hello:\\n echo {{`echo hello` + `echo goodbye`}}\",\n    tokens: (\n      Identifier:\"hello\",\n      Colon,\n      Eol,\n      Indent:\" \",\n      Text:\"echo \",\n      InterpolationStart,\n      Backtick:\"`echo hello`\",\n      Whitespace,\n      Plus,\n      Whitespace,\n      Backtick:\"`echo goodbye`\",\n      InterpolationEnd,\n      Dedent\n    ),\n  }\n\n  test! {\n    name: tokenize_empty_interpolation,\n    text: \"hello:\\n echo {{}}\",\n    tokens: (\n      Identifier:\"hello\",\n      Colon,\n      Eol,\n      Indent:\" \",\n      Text:\"echo \",\n      InterpolationStart,\n      InterpolationEnd,\n      Dedent,\n    ),\n  }\n\n  test! {\n    name: tokenize_assignment_backticks,\n    text: \"a = `echo hello` + `echo goodbye`\",\n    tokens: (\n      Identifier:\"a\",\n      Whitespace,\n      Equals,\n      Whitespace,\n      Backtick:\"`echo hello`\",\n      Whitespace,\n      Plus,\n      Whitespace,\n      Backtick:\"`echo goodbye`\",\n    ),\n  }\n\n  test! {\n    name: tokenize_multiple,\n    text: \"\n\n      hello:\n        a\n        b\n\n        c\n\n        d\n\n      # hello\n      bob:\n        frank\n       \\t\n    \",\n    tokens: (\n      Eol,\n      Identifier:\"hello\",\n      Colon,\n      Eol,\n      Indent,\n      Text:\"a\",\n      Eol,\n      Whitespace:\"  \",\n      Text:\"b\",\n      Eol,\n      Eol,\n      Whitespace:\"  \",\n      Text:\"c\",\n      Eol,\n      Eol,\n      Whitespace:\"  \",\n      Text:\"d\",\n      Eol,\n      Eol,\n      Dedent,\n      Comment:\"# hello\",\n      Eol,\n      Identifier:\"bob\",\n      Colon,\n      Eol,\n      Indent:\"  \",\n      Text:\"frank\",\n      Eol,\n      Eol,\n      Dedent,\n    ),\n  }\n\n  test! {\n    name: tokenize_comment,\n    text: \"a:=#\",\n    tokens: (\n      Identifier:\"a\",\n      ColonEquals,\n      Comment:\"#\",\n    ),\n  }\n\n  test! {\n    name: tokenize_comment_with_bang,\n    text: \"a:=#foo!\",\n    tokens: (\n      Identifier:\"a\",\n      ColonEquals,\n      Comment:\"#foo!\",\n    ),\n  }\n\n  test! {\n    name: tokenize_order,\n    text: \"\n      b: a\n        @mv a b\n\n      a:\n        @touch F\n        @touch a\n\n      d: c\n        @rm c\n\n      c: b\n        @mv b c\n    \",\n    tokens: (\n      Identifier:\"b\",\n      Colon,\n      Whitespace,\n      Identifier:\"a\",\n      Eol,\n      Indent,\n      Text:\"@mv a b\",\n      Eol,\n      Eol,\n      Dedent,\n      Identifier:\"a\",\n      Colon,\n      Eol,\n      Indent,\n      Text:\"@touch F\",\n      Eol,\n      Whitespace:\"  \",\n      Text:\"@touch a\",\n      Eol,\n      Eol,\n      Dedent,\n      Identifier:\"d\",\n      Colon,\n      Whitespace,\n      Identifier:\"c\",\n      Eol,\n      Indent,\n      Text:\"@rm c\",\n      Eol,\n      Eol,\n      Dedent,\n      Identifier:\"c\",\n      Colon,\n      Whitespace,\n      Identifier:\"b\",\n      Eol,\n      Indent,\n      Text:\"@mv b c\",\n      Eol,\n      Dedent,\n    ),\n  }\n\n  test! {\n    name: tokenize_parens,\n    text: \"((())) ()abc(+\",\n    tokens: (\n      ParenL,\n      ParenL,\n      ParenL,\n      ParenR,\n      ParenR,\n      ParenR,\n      Whitespace,\n      ParenL,\n      ParenR,\n      Identifier:\"abc\",\n      ParenL,\n      Plus,\n    ),\n  }\n\n  test! {\n    name: crlf_newline,\n    text: \"#\\r\\n#asdf\\r\\n\",\n    tokens: (\n      Comment:\"#\",\n      Eol:\"\\r\\n\",\n      Comment:\"#asdf\",\n      Eol:\"\\r\\n\",\n    ),\n  }\n\n  test! {\n    name: multiple_recipes,\n    text: \"a:\\n  foo\\nb:\",\n    tokens: (\n      Identifier:\"a\",\n      Colon,\n      Eol,\n      Indent:\"  \",\n      Text:\"foo\",\n      Eol,\n      Dedent,\n      Identifier:\"b\",\n      Colon,\n    ),\n  }\n\n  test! {\n    name:   brackets,\n    text:   \"[][]\",\n    tokens: (BracketL, BracketR, BracketL, BracketR),\n  }\n\n  test! {\n    name:   open_delimiter_eol,\n    text:   \"[\\n](\\n){\\n}\",\n    tokens: (\n      BracketL, Whitespace:\"\\n\", BracketR,\n      ParenL, Whitespace:\"\\n\", ParenR,\n      BraceL, Whitespace:\"\\n\", BraceR\n    ),\n  }\n\n  test! {\n    name:   format_string_empty,\n    text:   \"f''\",\n    tokens: (\n      Identifier: \"f\",\n      StringToken: \"''\",\n    ),\n  }\n\n  test! {\n    name:   format_string_identifier,\n    text:   \"f'{{foo}}'\",\n    tokens: (\n      Identifier: \"f\",\n      FormatStringStart: \"'{{\",\n      Identifier: \"foo\",\n      FormatStringEnd: \"}}'\",\n    ),\n  }\n\n  test! {\n    name:   format_string_continue,\n    text:   \"f'{{foo}}bar{{baz}}'\",\n    tokens: (\n      Identifier: \"f\",\n      FormatStringStart: \"'{{\",\n      Identifier: \"foo\",\n      FormatStringContinue: \"}}bar{{\",\n      Identifier: \"baz\",\n      FormatStringEnd: \"}}'\",\n    ),\n  }\n\n  test! {\n    name:   format_string_whitespace,\n    text:   \"f '{{foo}}'\",\n    tokens: (\n      Identifier: \"f\",\n      Whitespace,\n      StringToken: \"'{{foo}}'\",\n    ),\n  }\n\n  test! {\n    name:   format_string_wrong_identifier,\n    text:   \"g'{{foo}}'\",\n    tokens: (\n      Identifier: \"g\",\n      StringToken: \"'{{foo}}'\",\n    ),\n  }\n\n  test! {\n    name:   format_string_followed_by_recipe,\n    text:   \"foo := f'{{'foo'}}{{'bar'}}'\\nbar:\",\n    tokens: (\n      Identifier: \"foo\",\n      Whitespace: \" \",\n      ColonEquals: \":=\",\n      Whitespace: \" \",\n      Identifier: \"f\",\n      FormatStringStart: \"'{{\",\n      StringToken: \"'foo'\",\n      FormatStringContinue: \"}}{{\",\n      StringToken: \"'bar'\",\n      FormatStringEnd: \"}}'\",\n      Eol: \"\\n\",\n      Identifier: \"bar\",\n      Colon,\n    ),\n  }\n\n  test! {\n    name:   indented_format_string_followed_by_recipe,\n    text:   \"foo := f'''{{'foo'}}{{'bar'}}'''\\nbar:\",\n    tokens: (\n      Identifier: \"foo\",\n      Whitespace: \" \",\n      ColonEquals: \":=\",\n      Whitespace: \" \",\n      Identifier: \"f\",\n      FormatStringStart: \"'''{{\",\n      StringToken: \"'foo'\",\n      FormatStringContinue: \"}}{{\",\n      StringToken: \"'bar'\",\n      FormatStringEnd: \"}}'''\",\n      Eol: \"\\n\",\n      Identifier: \"bar\",\n      Colon,\n    ),\n  }\n\n  error! {\n    name:  tokenize_space_then_tab,\n    input: \"a:\n 0\n 1\n\\t2\n\",\n    offset: 9,\n    line:   3,\n    column: 0,\n    width:  1,\n    kind:   InconsistentLeadingWhitespace{expected: \" \", found: \"\\t\"},\n  }\n\n  error! {\n    name:  tokenize_tabs_then_tab_space,\n    input: \"a:\n\\t\\t0\n\\t\\t 1\n\\t  2\n\",\n    offset: 12,\n    line:   3,\n    column: 0,\n    width:  3,\n    kind:   InconsistentLeadingWhitespace{expected: \"\\t\\t\", found: \"\\t  \"},\n  }\n\n  error! {\n    name:   tokenize_unknown,\n    input:  \"%\",\n    offset: 0,\n    line:   0,\n    column: 0,\n    width:  1,\n    kind:   UnknownStartOfToken { start: '%'},\n  }\n\n  error! {\n    name:   unterminated_string_with_escapes,\n    input:  r#\"a = \"\\n\\t\\r\\\"\\\\\"#,\n    offset: 4,\n    line:   0,\n    column: 4,\n    width:  1,\n    kind:   UnterminatedString,\n  }\n\n  error! {\n    name:   unterminated_raw_string,\n    input:  \"r a='asdf\",\n    offset: 4,\n    line:   0,\n    column: 4,\n    width:  1,\n    kind:   UnterminatedString,\n  }\n\n  error! {\n    name:   unterminated_interpolation,\n    input:  \"foo:\\n echo {{\n  \",\n    offset: 11,\n    line:   1,\n    column: 6,\n    width:  2,\n    kind:   UnterminatedInterpolation,\n  }\n\n  error! {\n    name:   unterminated_backtick,\n    input:  \"`echo\",\n    offset: 0,\n    line:   0,\n    column: 0,\n    width:  1,\n    kind:   UnterminatedBacktick,\n  }\n\n  error! {\n    name:   unpaired_carriage_return,\n    input:  \"foo\\rbar\",\n    offset: 3,\n    line:   0,\n    column: 3,\n    width:  1,\n    kind:   UnpairedCarriageReturn,\n  }\n\n  error! {\n    name:   invalid_name_start_dash,\n    input:  \"-foo\",\n    offset: 0,\n    line:   0,\n    column: 0,\n    width:  1,\n    kind:   UnknownStartOfToken{ start: '-'},\n  }\n\n  error! {\n    name:   invalid_name_start_digit,\n    input:  \"0foo\",\n    offset: 0,\n    line:   0,\n    column: 0,\n    width:  1,\n    kind:   UnknownStartOfToken { start: '0' },\n  }\n\n  error! {\n    name:   unterminated_string,\n    input:  r#\"a = \"\"#,\n    offset: 4,\n    line:   0,\n    column: 4,\n    width:  1,\n    kind:   UnterminatedString,\n  }\n\n  error! {\n    name:   mixed_leading_whitespace_recipe,\n    input:  \"a:\\n\\t echo hello\",\n    offset: 3,\n    line:   1,\n    column: 0,\n    width:  2,\n    kind:   MixedLeadingWhitespace{whitespace: \"\\t \"},\n  }\n\n  error! {\n    name:   mixed_leading_whitespace_normal,\n    input:  \"a\\n\\t echo hello\",\n    offset: 2,\n    line:   1,\n    column: 0,\n    width:  2,\n    kind:   MixedLeadingWhitespace{whitespace: \"\\t \"},\n  }\n\n  error! {\n    name:   mixed_leading_whitespace_indent,\n    input:  \"a\\n foo\\n \\tbar\",\n    offset: 7,\n    line:   2,\n    column: 0,\n    width:  2,\n    kind:   MixedLeadingWhitespace{whitespace: \" \\t\"},\n  }\n\n  error! {\n    name:   bad_dedent,\n    input:  \"a\\n foo\\n   bar\\n  baz\",\n    offset: 14,\n    line:   3,\n    column: 0,\n    width:  2,\n    kind:   InconsistentLeadingWhitespace{expected: \"   \", found: \"  \"},\n  }\n\n  error! {\n    name:   unclosed_interpolation_delimiter,\n    input:  \"a:\\n echo {{ foo\",\n    offset: 9,\n    line:   1,\n    column: 6,\n    width:  2,\n    kind:   UnterminatedInterpolation,\n  }\n\n  error! {\n    name:   unexpected_character_after_at,\n    input:  \"@%\",\n    offset: 1,\n    line:   0,\n    column: 1,\n    width:  1,\n    kind:   UnknownStartOfToken { start: '%'},\n  }\n\n  error! {\n    name:   mismatched_closing_brace,\n    input:  \"(]\",\n    offset: 1,\n    line:   0,\n    column: 1,\n    width:  0,\n    kind:   MismatchedClosingDelimiter {\n      open:      Delimiter::Paren,\n      close:     Delimiter::Bracket,\n      open_line: 0,\n    },\n  }\n\n  error! {\n    name:   ampersand_eof,\n    input:  \"&\",\n    offset: 1,\n    line:   0,\n    column: 1,\n    width:  0,\n    kind:   UnexpectedEndOfToken {\n      expected: vec!['&'],\n    },\n  }\n\n  error! {\n    name:   ampersand_unexpected,\n    input:  \"&%\",\n    offset: 1,\n    line:   0,\n    column: 1,\n    width:  1,\n    kind:   UnexpectedCharacter {\n      expected: vec!['&'],\n    },\n  }\n\n  error! {\n    name:   bang_eof,\n    input:  \"!\",\n    offset: 1,\n    line:   0,\n    column: 1,\n    width:  0,\n    kind:   UnexpectedEndOfToken {\n      expected: vec!['=', '~'],\n    },\n  }\n\n  error! {\n    name:   unclosed_parenthesis_in_interpolation,\n    input:  \"a:\\n echo {{foo(}}\",\n    offset:  15,\n    line:   1,\n    column: 12,\n    width:  0,\n    kind:   MismatchedClosingDelimiter {\n      close: Delimiter::Brace,\n      open: Delimiter::Paren,\n      open_line: 1,\n    },\n  }\n\n  #[test]\n  fn presume_error() {\n    let compile_error = Lexer::new(\"justfile\".as_ref(), \"!\")\n      .presume('-')\n      .unwrap_err();\n    assert_matches!(\n      compile_error.token,\n      Token {\n        offset: 0,\n        line: 0,\n        column: 0,\n        length: 0,\n        src: \"!\",\n        kind: Unspecified,\n        path: _,\n      }\n    );\n    assert_matches!(&*compile_error.kind,\n        Internal { message }\n        if message == \"Lexer presumed character `-`\"\n    );\n\n    assert_eq!(\n      Error::Compile { compile_error }\n        .color_display(Color::never())\n        .to_string(),\n      \"error: Internal error, this may indicate a bug in just: Lexer presumed character `-`\nconsider filing an issue: https://github.com/casey/just/issues/new\n ——▶ justfile:1:1\n  │\n1 │ !\n  │ ^\"\n    );\n  }\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "//! `just` is primarily used as a command-line binary, but does provide a\n//! limited public library interface.\n//!\n//! Please keep in mind that there are no semantic version guarantees for the\n//! library interface. It may break or change at any time.\n\npub(crate) use {\n  crate::{\n    alias::Alias,\n    alias_style::AliasStyle,\n    analyzer::Analyzer,\n    arg_attribute::ArgAttribute,\n    assignment::Assignment,\n    assignment_resolver::AssignmentResolver,\n    ast::Ast,\n    attribute::{Attribute, AttributeDiscriminant},\n    attribute_set::AttributeSet,\n    binding::Binding,\n    color::Color,\n    color_display::ColorDisplay,\n    command_color::CommandColor,\n    command_ext::CommandExt,\n    compilation::Compilation,\n    compile_error::CompileError,\n    compile_error_kind::CompileErrorKind,\n    compiler::Compiler,\n    condition::Condition,\n    conditional_operator::ConditionalOperator,\n    config::Config,\n    config_error::ConfigError,\n    const_error::ConstError,\n    constants::constants,\n    count::Count,\n    delimiter::Delimiter,\n    dependency::Dependency,\n    dump_format::DumpFormat,\n    enclosure::Enclosure,\n    error::Error,\n    evaluator::Evaluator,\n    execution_context::ExecutionContext,\n    executor::Executor,\n    expression::Expression,\n    format_string_part::FormatStringPart,\n    fragment::Fragment,\n    function::Function,\n    interpreter::Interpreter,\n    invocation::Invocation,\n    invocation_parser::InvocationParser,\n    item::Item,\n    justfile::Justfile,\n    keyed::Keyed,\n    keyword::Keyword,\n    lexer::Lexer,\n    line::Line,\n    list::List,\n    load_dotenv::load_dotenv,\n    loader::Loader,\n    modulepath::Modulepath,\n    name::Name,\n    namepath::Namepath,\n    number::Number,\n    numerator::Numerator,\n    ordinal::Ordinal,\n    output_error::OutputError,\n    parameter::Parameter,\n    parameter_kind::ParameterKind,\n    parser::Parser,\n    pattern::Pattern,\n    platform::Platform,\n    platform_interface::PlatformInterface,\n    position::Position,\n    positional::Positional,\n    ran::Ran,\n    range_ext::RangeExt,\n    recipe::Recipe,\n    recipe_resolver::RecipeResolver,\n    recipe_signature::RecipeSignature,\n    scope::Scope,\n    search::Search,\n    search_config::SearchConfig,\n    search_error::SearchError,\n    set::Set,\n    setting::Setting,\n    settings::Settings,\n    shebang::Shebang,\n    show_whitespace::ShowWhitespace,\n    sigil::Sigil,\n    signal::Signal,\n    signal_handler::SignalHandler,\n    source::Source,\n    string_delimiter::StringDelimiter,\n    string_kind::StringKind,\n    string_literal::StringLiteral,\n    string_state::StringState,\n    subcommand::Subcommand,\n    suggestion::Suggestion,\n    switch::Switch,\n    table::Table,\n    thunk::Thunk,\n    token::Token,\n    token_kind::TokenKind,\n    unresolved_dependency::UnresolvedDependency,\n    unresolved_recipe::UnresolvedRecipe,\n    unstable_feature::UnstableFeature,\n    usage::Usage,\n    use_color::UseColor,\n    variables::Variables,\n    verbosity::Verbosity,\n    warning::Warning,\n    which::which,\n  },\n  camino::Utf8Path,\n  clap::ValueEnum,\n  derive_where::derive_where,\n  edit_distance::edit_distance,\n  lexiclean::Lexiclean,\n  libc::EXIT_FAILURE,\n  rand::seq::IndexedRandom,\n  regex::Regex,\n  serde::{\n    Deserialize, Serialize, Serializer,\n    ser::{SerializeMap, SerializeSeq},\n  },\n  snafu::{ResultExt, Snafu},\n  std::{\n    borrow::Cow,\n    cmp::Ordering,\n    collections::{BTreeMap, BTreeSet, HashMap, HashSet},\n    env,\n    ffi::OsString,\n    fmt::{self, Debug, Display, Formatter},\n    fs,\n    io::{self, Write},\n    iter::{self, FromIterator},\n    mem,\n    ops::Deref,\n    ops::{Index, RangeInclusive},\n    path::{self, Path, PathBuf},\n    process::{self, Command, ExitStatus, Stdio},\n    slice,\n    str::{self, Chars},\n    sync::{Arc, LazyLock, Mutex, MutexGuard},\n    thread, vec,\n  },\n  strum::{Display, EnumDiscriminants, EnumString, IntoStaticStr},\n  tempfile::TempDir,\n  typed_arena::Arena,\n  unicode_width::{UnicodeWidthChar, UnicodeWidthStr},\n};\n\n#[cfg(test)]\npub(crate) use crate::{node::Node, tree::Tree};\n\npub use crate::run::run;\n\n#[doc(hidden)]\nuse request::Request;\n\n// Used in integration tests.\n#[doc(hidden)]\npub use {request::Response, subcommand::INIT_JUSTFILE, unindent::unindent};\n\ntype CompileResult<'a, T = ()> = Result<T, CompileError<'a>>;\ntype ConfigResult<T> = Result<T, ConfigError>;\ntype FunctionResult = Result<String, String>;\ntype RunResult<'a, T = ()> = Result<T, Error<'a>>;\ntype SearchResult<T> = Result<T, SearchError>;\n\n#[cfg(test)]\n#[macro_use]\npub mod testing;\n\n#[cfg(test)]\n#[macro_use]\npub mod tree;\n\n#[cfg(test)]\npub mod node;\n\n#[cfg(fuzzing)]\npub mod fuzzing;\n\n// Used by Janus, https://github.com/casey/janus, a tool\n// that analyses all public justfiles on GitHub to avoid\n// breaking changes.\n#[doc(hidden)]\npub mod summary;\n\n// Used for testing with the `--request` subcommand.\n#[doc(hidden)]\npub mod request;\n\nmod alias;\nmod alias_style;\nmod analyzer;\nmod arg_attribute;\nmod assignment;\nmod assignment_resolver;\nmod ast;\nmod attribute;\nmod attribute_set;\nmod binding;\nmod color;\nmod color_display;\nmod command_color;\nmod command_ext;\nmod compilation;\nmod compile_error;\nmod compile_error_kind;\nmod compiler;\nmod completions;\nmod condition;\nmod conditional_operator;\nmod config;\nmod config_error;\nmod const_error;\nmod constants;\nmod count;\nmod delimiter;\nmod dependency;\nmod dump_format;\nmod enclosure;\nmod error;\nmod evaluator;\nmod execution_context;\nmod executor;\nmod expression;\nmod filesystem;\nmod format_string_part;\nmod fragment;\nmod function;\nmod interpreter;\nmod invocation;\nmod invocation_parser;\nmod item;\nmod justfile;\nmod keyed;\nmod keyword;\nmod lexer;\nmod line;\nmod list;\nmod load_dotenv;\nmod loader;\nmod modulepath;\nmod name;\nmod namepath;\nmod number;\nmod numerator;\nmod ordinal;\nmod output_error;\nmod parameter;\nmod parameter_kind;\nmod parser;\nmod pattern;\nmod platform;\nmod platform_interface;\nmod position;\nmod positional;\nmod ran;\nmod range_ext;\nmod recipe;\nmod recipe_resolver;\nmod recipe_signature;\nmod run;\nmod scope;\nmod search;\nmod search_config;\nmod search_error;\nmod set;\nmod setting;\nmod settings;\nmod shebang;\nmod show_whitespace;\nmod sigil;\nmod signal;\nmod signal_handler;\n#[cfg(unix)]\nmod signals;\nmod source;\nmod string_delimiter;\nmod string_kind;\nmod string_literal;\nmod string_state;\nmod subcommand;\nmod suggestion;\nmod switch;\nmod table;\nmod thunk;\nmod token;\nmod token_kind;\nmod unindent;\nmod unresolved_dependency;\nmod unresolved_recipe;\nmod unstable_feature;\nmod usage;\nmod use_color;\nmod variables;\nmod verbosity;\nmod warning;\nmod which;\n"
  },
  {
    "path": "src/line.rs",
    "content": "use super::*;\n\n/// A single line in a recipe body, consisting of any number of `Fragment`s.\n#[derive(Debug, Clone, PartialEq, Serialize)]\n#[serde(transparent)]\npub(crate) struct Line<'src> {\n  pub(crate) fragments: Vec<Fragment<'src>>,\n  #[serde(skip)]\n  pub(crate) number: usize,\n}\n\nimpl Line<'_> {\n  fn first(&self) -> Option<&str> {\n    if let Fragment::Text { token } = self.fragments.first()? {\n      Some(token.lexeme())\n    } else {\n      None\n    }\n  }\n\n  pub(crate) fn sigils(&self, settings: &Settings) -> BTreeSet<Sigil> {\n    let mut sigils = BTreeSet::new();\n\n    if let Some(first) = self.first() {\n      for c in first.chars() {\n        let sigil = match c {\n          '-' => Sigil::Infallible,\n          '?' if settings.guards => Sigil::Guard,\n          '@' => Sigil::Quiet,\n          _ => break,\n        };\n\n        if !sigils.insert(sigil) {\n          break;\n        }\n      }\n    }\n\n    sigils\n  }\n\n  pub(crate) fn is_comment(&self) -> bool {\n    self.first().is_some_and(|text| text.starts_with('#'))\n  }\n\n  pub(crate) fn is_continuation(&self) -> bool {\n    matches!(\n      self.fragments.last(),\n      Some(Fragment::Text { token }) if token.lexeme().ends_with('\\\\'),\n    )\n  }\n\n  pub(crate) fn is_empty(&self) -> bool {\n    self.fragments.is_empty()\n  }\n\n  pub(crate) fn is_shebang(&self) -> bool {\n    self.first().is_some_and(|text| text.starts_with(\"#!\"))\n  }\n}\n"
  },
  {
    "path": "src/list.rs",
    "content": "use super::*;\n\npub(crate) struct List<T: Display, I: Iterator<Item = T> + Clone> {\n  conjunction: &'static str,\n  values: I,\n}\n\nimpl<T: Display, I: Iterator<Item = T> + Clone> List<T, I> {\n  pub(crate) fn or<II: IntoIterator<Item = T, IntoIter = I>>(values: II) -> Self {\n    Self {\n      conjunction: \"or\",\n      values: values.into_iter(),\n    }\n  }\n\n  pub(crate) fn and<II: IntoIterator<Item = T, IntoIter = I>>(values: II) -> Self {\n    Self {\n      conjunction: \"and\",\n      values: values.into_iter(),\n    }\n  }\n\n  pub(crate) fn or_ticked<II: IntoIterator<Item = T, IntoIter = I>>(\n    values: II,\n  ) -> List<Enclosure<T>, impl Iterator<Item = Enclosure<T>> + Clone> {\n    List::or(values.into_iter().map(Enclosure::tick))\n  }\n\n  pub(crate) fn and_ticked<II: IntoIterator<Item = T, IntoIter = I>>(\n    values: II,\n  ) -> List<Enclosure<T>, impl Iterator<Item = Enclosure<T>> + Clone> {\n    List::and(values.into_iter().map(Enclosure::tick))\n  }\n}\n\nimpl<T: Display, I: Iterator<Item = T> + Clone> Display for List<T, I> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    let mut values = self.values.clone().fuse();\n\n    if let Some(first) = values.next() {\n      write!(f, \"{first}\")?;\n    } else {\n      return Ok(());\n    }\n\n    let second = values.next();\n\n    if second.is_none() {\n      return Ok(());\n    }\n\n    let third = values.next();\n\n    if let (Some(second), None) = (second.as_ref(), third.as_ref()) {\n      write!(f, \" {} {second}\", self.conjunction)?;\n      return Ok(());\n    }\n\n    let mut current = second;\n    let mut next = third;\n\n    loop {\n      match (current, next) {\n        (Some(c), Some(n)) => {\n          write!(f, \", {c}\")?;\n          current = Some(n);\n          next = values.next();\n        }\n        (Some(c), None) => {\n          write!(f, \", {} {c}\", self.conjunction)?;\n          return Ok(());\n        }\n        _ => unreachable!(\"Iterator was fused, but returned Some after None\"),\n      }\n    }\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn or() {\n    assert_eq!(\"1\", List::or(&[1]).to_string());\n    assert_eq!(\"1 or 2\", List::or(&[1, 2]).to_string());\n    assert_eq!(\"1, 2, or 3\", List::or(&[1, 2, 3]).to_string());\n    assert_eq!(\"1, 2, 3, or 4\", List::or(&[1, 2, 3, 4]).to_string());\n  }\n\n  #[test]\n  fn and() {\n    assert_eq!(\"1\", List::and(&[1]).to_string());\n    assert_eq!(\"1 and 2\", List::and(&[1, 2]).to_string());\n    assert_eq!(\"1, 2, and 3\", List::and(&[1, 2, 3]).to_string());\n    assert_eq!(\"1, 2, 3, and 4\", List::and(&[1, 2, 3, 4]).to_string());\n  }\n\n  #[test]\n  fn or_ticked() {\n    assert_eq!(\"`1`\", List::or_ticked(&[1]).to_string());\n    assert_eq!(\"`1` or `2`\", List::or_ticked(&[1, 2]).to_string());\n    assert_eq!(\"`1`, `2`, or `3`\", List::or_ticked(&[1, 2, 3]).to_string());\n    assert_eq!(\n      \"`1`, `2`, `3`, or `4`\",\n      List::or_ticked(&[1, 2, 3, 4]).to_string()\n    );\n  }\n\n  #[test]\n  fn and_ticked() {\n    assert_eq!(\"`1`\", List::and_ticked(&[1]).to_string());\n    assert_eq!(\"`1` and `2`\", List::and_ticked(&[1, 2]).to_string());\n    assert_eq!(\n      \"`1`, `2`, and `3`\",\n      List::and_ticked(&[1, 2, 3]).to_string()\n    );\n    assert_eq!(\n      \"`1`, `2`, `3`, and `4`\",\n      List::and_ticked(&[1, 2, 3, 4]).to_string()\n    );\n  }\n}\n"
  },
  {
    "path": "src/load_dotenv.rs",
    "content": "use super::*;\n\npub(crate) fn load_dotenv(\n  config: &Config,\n  settings: &Settings,\n  working_directory: &Path,\n) -> RunResult<'static, BTreeMap<String, String>> {\n  let dotenv_filename = config\n    .dotenv_filename\n    .as_ref()\n    .or(settings.dotenv_filename.as_ref());\n\n  let dotenv_path = config\n    .dotenv_path\n    .as_ref()\n    .or(settings.dotenv_path.as_ref());\n\n  if !settings.dotenv_load\n    && !settings.dotenv_override\n    && !settings.dotenv_required\n    && dotenv_filename.is_none()\n    && dotenv_path.is_none()\n  {\n    return Ok(BTreeMap::new());\n  }\n\n  if let Some(path) = dotenv_path {\n    let path = working_directory.join(path);\n    if filesystem::is_file(&path)? {\n      return load_from_file(&path, settings);\n    }\n  }\n\n  let filename = dotenv_filename.map_or(\".env\", |s| s.as_str());\n\n  for directory in working_directory.ancestors() {\n    let path = directory.join(filename);\n    if filesystem::is_file(&path)? {\n      return load_from_file(&path, settings);\n    }\n  }\n\n  if settings.dotenv_required {\n    Err(Error::DotenvRequired)\n  } else {\n    Ok(BTreeMap::new())\n  }\n}\n\nfn load_from_file(\n  path: &Path,\n  settings: &Settings,\n) -> RunResult<'static, BTreeMap<String, String>> {\n  let iter = dotenvy::from_path_iter(path).map_err(|dotenv_error| Error::Dotenv {\n    dotenv_error,\n    path: path.into(),\n  })?;\n  let mut dotenv = BTreeMap::new();\n  for result in iter {\n    let (key, value) = result.map_err(|dotenv_error| Error::Dotenv {\n      dotenv_error,\n      path: path.into(),\n    })?;\n    if settings.dotenv_override || env::var_os(&key).is_none() {\n      dotenv.insert(key, value);\n    }\n  }\n  Ok(dotenv)\n}\n"
  },
  {
    "path": "src/loader.rs",
    "content": "use super::*;\n\npub(crate) struct Loader {\n  paths: Arena<PathBuf>,\n  srcs: Arena<String>,\n}\n\nimpl Loader {\n  pub(crate) fn new() -> Self {\n    Self {\n      srcs: Arena::new(),\n      paths: Arena::new(),\n    }\n  }\n\n  pub(crate) fn load<'src>(\n    &'src self,\n    root: &Path,\n    path: &Path,\n  ) -> RunResult<'src, (&'src Path, &'src str)> {\n    let src = fs::read_to_string(path).map_err(|io_error| Error::Load {\n      path: path.into(),\n      io_error,\n    })?;\n\n    let relative = path.strip_prefix(root.parent().unwrap()).unwrap_or(path);\n\n    Ok((self.paths.alloc(relative.into()), self.srcs.alloc(src)))\n  }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "fn main() {\n  if let Err(code) = just::run(std::env::args_os()) {\n    std::process::exit(code);\n  }\n}\n"
  },
  {
    "path": "src/modulepath.rs",
    "content": "use super::*;\n\n#[derive(Debug, Default, Eq, Ord, PartialEq, PartialOrd, Clone)]\npub(crate) struct Modulepath {\n  pub(crate) path: Vec<String>,\n  pub(crate) spaced: bool,\n}\n\nimpl Modulepath {\n  pub(crate) fn is_empty(&self) -> bool {\n    self.path.is_empty()\n  }\n\n  pub(crate) fn starts_with(&self, other: &Modulepath) -> bool {\n    self.path.starts_with(&other.path)\n  }\n}\n\nimpl Serialize for Modulepath {\n  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n  where\n    S: Serializer,\n  {\n    serializer.serialize_str(&self.to_string())\n  }\n}\n\nimpl From<&Namepath<'_>> for Modulepath {\n  fn from(namepath: &Namepath) -> Self {\n    Self {\n      path: namepath.iter().map(|name| name.lexeme().into()).collect(),\n      spaced: false,\n    }\n  }\n}\n\nimpl TryFrom<&[&str]> for Modulepath {\n  type Error = ();\n\n  fn try_from(path: &[&str]) -> Result<Self, Self::Error> {\n    let spaced = path.len() > 1;\n\n    let path = if path.len() == 1 {\n      let first = path[0];\n\n      if first.starts_with(':') || first.ends_with(':') || first.contains(\":::\") {\n        return Err(());\n      }\n\n      first\n        .split(\"::\")\n        .map(str::to_string)\n        .collect::<Vec<String>>()\n    } else {\n      path.iter().map(|s| (*s).to_string()).collect()\n    };\n\n    for name in &path {\n      if name.is_empty() {\n        return Err(());\n      }\n\n      for (i, c) in name.chars().enumerate() {\n        if i == 0 {\n          if !Lexer::is_identifier_start(c) {\n            return Err(());\n          }\n        } else if !Lexer::is_identifier_continue(c) {\n          return Err(());\n        }\n      }\n    }\n\n    Ok(Self { path, spaced })\n  }\n}\n\nimpl Display for Modulepath {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    for (i, name) in self.path.iter().enumerate() {\n      if i > 0 {\n        if self.spaced {\n          write!(f, \" \")?;\n        } else {\n          write!(f, \"::\")?;\n        }\n      }\n      write!(f, \"{name}\")?;\n    }\n    Ok(())\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn try_from_ok() {\n    #[track_caller]\n    fn case(path: &[&str], expected: &[&str], display: &str) {\n      let actual = Modulepath::try_from(path).unwrap();\n      assert_eq!(actual.path, expected);\n      assert_eq!(actual.to_string(), display);\n    }\n\n    case(&[], &[], \"\");\n    case(&[\"foo\"], &[\"foo\"], \"foo\");\n    case(&[\"foo0\"], &[\"foo0\"], \"foo0\");\n    case(&[\"foo\", \"bar\"], &[\"foo\", \"bar\"], \"foo bar\");\n    case(&[\"foo::bar\"], &[\"foo\", \"bar\"], \"foo::bar\");\n  }\n\n  #[test]\n  fn try_from_err() {\n    #[track_caller]\n    fn case(path: &[&str]) {\n      assert!(Modulepath::try_from(path).is_err());\n    }\n\n    case(&[\":foo\"]);\n    case(&[\"foo:\"]);\n    case(&[\"foo:::bar\"]);\n    case(&[\"0foo\"]);\n    case(&[\"f$oo\"]);\n    case(&[\"\"]);\n  }\n}\n"
  },
  {
    "path": "src/name.rs",
    "content": "use super::*;\n\n/// A name. This is just a `Token` of kind `Identifier`, but we give it its own\n/// type for clarity.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]\npub(crate) struct Name<'src> {\n  pub(crate) token: Token<'src>,\n}\n\nimpl<'src> Name<'src> {\n  pub(crate) fn from_identifier(token: Token<'src>) -> Self {\n    assert_eq!(token.kind, TokenKind::Identifier);\n    Self { token }\n  }\n}\n\nimpl<'src> Deref for Name<'src> {\n  type Target = Token<'src>;\n\n  fn deref(&self) -> &Self::Target {\n    &self.token\n  }\n}\n\nimpl Display for Name<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    write!(f, \"{}\", self.lexeme())\n  }\n}\n\nimpl<'src> Keyed<'src> for Name<'src> {\n  fn key(&self) -> &'src str {\n    self.lexeme()\n  }\n}\n\nimpl Serialize for Name<'_> {\n  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n  where\n    S: Serializer,\n  {\n    serializer.serialize_str(self.lexeme())\n  }\n}\n"
  },
  {
    "path": "src/namepath.rs",
    "content": "use super::*;\n\n#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]\npub(crate) struct Namepath<'src>(Vec<Name<'src>>);\n\nimpl<'src> Namepath<'src> {\n  pub(crate) fn join(&self, name: Name<'src>) -> Self {\n    Self(self.0.iter().copied().chain(iter::once(name)).collect())\n  }\n\n  pub(crate) fn push(&mut self, name: Name<'src>) {\n    self.0.push(name);\n  }\n\n  pub(crate) fn last(&self) -> &Name<'src> {\n    self.0.last().unwrap()\n  }\n\n  pub(crate) fn split_last(&self) -> (&Name<'src>, &[Name<'src>]) {\n    self.0.split_last().unwrap()\n  }\n\n  pub(crate) fn iter(&self) -> slice::Iter<'_, Name<'src>> {\n    self.0.iter()\n  }\n\n  pub(crate) fn components(&self) -> usize {\n    self.0.len()\n  }\n}\n\nimpl Display for Namepath<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    for (i, name) in self.0.iter().enumerate() {\n      if i > 0 {\n        write!(f, \"::\")?;\n      }\n      write!(f, \"{name}\")?;\n    }\n    Ok(())\n  }\n}\n\nimpl<'src> From<Name<'src>> for Namepath<'src> {\n  fn from(name: Name<'src>) -> Self {\n    Self(vec![name])\n  }\n}\n\nimpl Serialize for Namepath<'_> {\n  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n  where\n    S: Serializer,\n  {\n    serializer.serialize_str(&format!(\"{self}\"))\n  }\n}\n"
  },
  {
    "path": "src/node.rs",
    "content": "use super::*;\n\n/// Methods common to all AST nodes. Currently only used in parser unit tests.\npub(crate) trait Node<'src> {\n  /// Construct an untyped tree of atoms representing this Node. This function,\n  /// and `Tree` type, are only used in parser unit tests.\n  fn tree(&self) -> Tree<'src>;\n}\n\nimpl<'src> Node<'src> for Ast<'src> {\n  fn tree(&self) -> Tree<'src> {\n    Tree::atom(\"justfile\")\n      .extend(self.items.iter().map(Node::tree))\n      .extend(self.warnings.iter().map(Node::tree))\n  }\n}\n\nimpl<'src> Node<'src> for Item<'src> {\n  fn tree(&self) -> Tree<'src> {\n    match self {\n      Self::Alias(alias) => alias.tree(),\n      Self::Assignment(assignment) => assignment.tree(),\n      Self::Comment(comment) => comment.tree(),\n      Self::Import {\n        relative, optional, ..\n      } => {\n        let mut tree = Tree::atom(\"import\");\n\n        if *optional {\n          tree = tree.push(\"?\");\n        }\n\n        tree.push(format!(\"{relative}\"))\n      }\n      Self::Module {\n        name,\n        optional,\n        relative,\n        ..\n      } => {\n        let mut tree = Tree::atom(\"mod\");\n\n        if *optional {\n          tree = tree.push(\"?\");\n        }\n\n        tree = tree.push(name.lexeme());\n\n        if let Some(relative) = relative {\n          tree = tree.push(format!(\"{relative}\"));\n        }\n\n        tree\n      }\n      Self::Recipe(recipe) => recipe.tree(),\n      Self::Set(set) => set.tree(),\n      Self::Unexport { name } => {\n        let mut unexport = Tree::atom(Keyword::Unexport.lexeme());\n        unexport.push_mut(name.lexeme().replace('-', \"_\"));\n        unexport\n      }\n    }\n  }\n}\n\nimpl<'src> Node<'src> for Namepath<'src> {\n  fn tree(&self) -> Tree<'src> {\n    match self.components() {\n      1 => Tree::atom(self.last().lexeme()),\n      _ => Tree::list(\n        self\n          .iter()\n          .map(|name| Tree::atom(Cow::Borrowed(name.lexeme()))),\n      ),\n    }\n  }\n}\n\nimpl<'src> Node<'src> for Alias<'src, Namepath<'src>> {\n  fn tree(&self) -> Tree<'src> {\n    let target = self.target.tree();\n\n    Tree::atom(Keyword::Alias.lexeme())\n      .push(self.name.lexeme())\n      .push(target)\n  }\n}\n\nimpl<'src> Node<'src> for Assignment<'src> {\n  fn tree(&self) -> Tree<'src> {\n    if self.export {\n      Tree::atom(\"assignment\")\n        .push(\"#\")\n        .push(Keyword::Export.lexeme())\n    } else {\n      Tree::atom(\"assignment\")\n    }\n    .push(self.name.lexeme())\n    .push(self.value.tree())\n  }\n}\n\nimpl<'src> Node<'src> for Expression<'src> {\n  fn tree(&self) -> Tree<'src> {\n    match self {\n      Self::And { lhs, rhs } => Tree::atom(\"&&\").push(lhs.tree()).push(rhs.tree()),\n      Self::Assert {\n        condition: Condition { lhs, rhs, operator },\n        error,\n        ..\n      } => Tree::atom(Keyword::Assert.lexeme())\n        .push(lhs.tree())\n        .push(operator.to_string())\n        .push(rhs.tree())\n        .push(error.tree()),\n      Self::Backtick { contents, .. } => Tree::atom(\"backtick\").push(Tree::string(contents)),\n      Self::Call { thunk } => {\n        use Thunk::*;\n        let mut tree = Tree::atom(\"call\");\n        match thunk {\n          Nullary { name, .. } => tree.push_mut(name.lexeme()),\n          Unary { name, arg, .. } => {\n            tree.push_mut(name.lexeme());\n            tree.push_mut(arg.tree());\n          }\n          UnaryOpt {\n            name, args: (a, b), ..\n          } => {\n            tree.push_mut(name.lexeme());\n            tree.push_mut(a.tree());\n            if let Some(b) = b.as_ref() {\n              tree.push_mut(b.tree());\n            }\n          }\n          UnaryPlus {\n            name,\n            args: (a, rest),\n            ..\n          } => {\n            tree.push_mut(name.lexeme());\n            tree.push_mut(a.tree());\n            for arg in rest {\n              tree.push_mut(arg.tree());\n            }\n          }\n          Binary {\n            name, args: [a, b], ..\n          } => {\n            tree.push_mut(name.lexeme());\n            tree.push_mut(a.tree());\n            tree.push_mut(b.tree());\n          }\n          BinaryPlus {\n            name,\n            args: ([a, b], rest),\n            ..\n          } => {\n            tree.push_mut(name.lexeme());\n            tree.push_mut(a.tree());\n            tree.push_mut(b.tree());\n            for arg in rest {\n              tree.push_mut(arg.tree());\n            }\n          }\n          Ternary {\n            name,\n            args: [a, b, c],\n            ..\n          } => {\n            tree.push_mut(name.lexeme());\n            tree.push_mut(a.tree());\n            tree.push_mut(b.tree());\n            tree.push_mut(c.tree());\n          }\n        }\n        tree\n      }\n      Self::Concatenation { lhs, rhs } => Tree::atom(\"+\").push(lhs.tree()).push(rhs.tree()),\n      Self::Conditional {\n        condition: Condition { lhs, rhs, operator },\n        then,\n        otherwise,\n      } => {\n        let mut tree = Tree::atom(Keyword::If.lexeme());\n        tree.push_mut(lhs.tree());\n        tree.push_mut(operator.to_string());\n        tree.push_mut(rhs.tree());\n        tree.push_mut(then.tree());\n        tree.push_mut(otherwise.tree());\n        tree\n      }\n      Self::FormatString { start, expressions } => {\n        let mut tree = Tree::atom(\"format\");\n        tree.push_mut(Tree::string(&start.cooked));\n        for (expression, string) in expressions {\n          tree.push_mut(expression.tree());\n          tree.push_mut(Tree::string(&string.cooked));\n        }\n        tree\n      }\n      Self::Group { contents } => Tree::List(vec![contents.tree()]),\n      Self::Join { lhs: None, rhs } => Tree::atom(\"/\").push(rhs.tree()),\n      Self::Join {\n        lhs: Some(lhs),\n        rhs,\n      } => Tree::atom(\"/\").push(lhs.tree()).push(rhs.tree()),\n      Self::Or { lhs, rhs } => Tree::atom(\"||\").push(lhs.tree()).push(rhs.tree()),\n      Self::StringLiteral {\n        string_literal: StringLiteral { cooked, .. },\n      } => Tree::string(cooked),\n      Self::Variable { name } => Tree::atom(name.lexeme()),\n    }\n  }\n}\n\nimpl<'src> Node<'src> for UnresolvedRecipe<'src> {\n  fn tree(&self) -> Tree<'src> {\n    let mut t = Tree::atom(\"recipe\");\n\n    if self.quiet {\n      t.push_mut(\"#\");\n      t.push_mut(\"quiet\");\n    }\n\n    if let Some(doc) = &self.doc {\n      t.push_mut(Tree::string(doc));\n    }\n\n    t.push_mut(self.name.lexeme());\n\n    if !self.parameters.is_empty() {\n      let mut params = Tree::atom(\"params\");\n\n      for parameter in &self.parameters {\n        if let Some(prefix) = parameter.kind.prefix() {\n          params.push_mut(prefix);\n        }\n\n        params.push_mut(parameter.tree());\n      }\n\n      t.push_mut(params);\n    }\n\n    if !self.dependencies.is_empty() {\n      let mut dependencies = Tree::atom(\"deps\");\n      let mut subsequents = Tree::atom(\"sups\");\n\n      for (i, dependency) in self.dependencies.iter().enumerate() {\n        let mut d = dependency.recipe.tree();\n\n        for argument in &dependency.arguments {\n          d.push_mut(argument.tree());\n        }\n\n        if i < self.priors {\n          dependencies.push_mut(d);\n        } else {\n          subsequents.push_mut(d);\n        }\n      }\n\n      if let Tree::List(_) = dependencies {\n        t.push_mut(dependencies);\n      }\n\n      if let Tree::List(_) = subsequents {\n        t.push_mut(subsequents);\n      }\n    }\n\n    if !self.body.is_empty() {\n      t.push_mut(Tree::atom(\"body\").extend(self.body.iter().map(Node::tree)));\n    }\n\n    t\n  }\n}\n\nimpl<'src> Node<'src> for Parameter<'src> {\n  fn tree(&self) -> Tree<'src> {\n    let mut children = vec![Tree::atom(self.name.lexeme())];\n\n    if let Some(default) = &self.default {\n      children.push(default.tree());\n    }\n\n    Tree::List(children)\n  }\n}\n\nimpl<'src> Node<'src> for Line<'src> {\n  fn tree(&self) -> Tree<'src> {\n    Tree::list(self.fragments.iter().map(Node::tree))\n  }\n}\n\nimpl<'src> Node<'src> for Fragment<'src> {\n  fn tree(&self) -> Tree<'src> {\n    match self {\n      Self::Text { token } => Tree::string(token.lexeme()),\n      Self::Interpolation { expression } => Tree::List(vec![expression.tree()]),\n    }\n  }\n}\n\nimpl<'src> Node<'src> for Set<'src> {\n  fn tree(&self) -> Tree<'src> {\n    let mut set = Tree::atom(Keyword::Set.lexeme());\n    set.push_mut(self.name.lexeme().replace('-', \"_\"));\n\n    match &self.value {\n      Setting::AllowDuplicateRecipes(value)\n      | Setting::AllowDuplicateVariables(value)\n      | Setting::DotenvLoad(value)\n      | Setting::DotenvOverride(value)\n      | Setting::DotenvRequired(value)\n      | Setting::Export(value)\n      | Setting::Fallback(value)\n      | Setting::Guards(value)\n      | Setting::IgnoreComments(value)\n      | Setting::Lazy(value)\n      | Setting::NoExitMessage(value)\n      | Setting::PositionalArguments(value)\n      | Setting::Quiet(value)\n      | Setting::Unstable(value)\n      | Setting::WindowsPowerShell(value) => {\n        set.push_mut(value.to_string());\n      }\n      Setting::DotenvFilename(value)\n      | Setting::DotenvPath(value)\n      | Setting::Tempdir(value)\n      | Setting::WorkingDirectory(value) => {\n        set.push_mut(value.tree());\n      }\n      Setting::ScriptInterpreter(Interpreter { command, arguments })\n      | Setting::Shell(Interpreter { command, arguments })\n      | Setting::WindowsShell(Interpreter { command, arguments }) => {\n        set.push_mut(command.tree());\n        for argument in arguments {\n          set.push_mut(argument.tree());\n        }\n      }\n    }\n\n    set\n  }\n}\n\nimpl<'src> Node<'src> for Warning {\n  fn tree(&self) -> Tree<'src> {\n    unreachable!()\n  }\n}\n\nimpl<'src> Node<'src> for str {\n  fn tree(&self) -> Tree<'src> {\n    Tree::atom(\"comment\").push([\"\\\"\", self, \"\\\"\"].concat())\n  }\n}\n"
  },
  {
    "path": "src/number.rs",
    "content": "#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]\npub(crate) struct Number(pub(crate) u32);\n"
  },
  {
    "path": "src/numerator.rs",
    "content": "use super::*;\n\npub(crate) struct Numerator(u32);\n\nimpl Numerator {\n  pub(crate) fn new() -> Self {\n    Self(constants().len().try_into().unwrap())\n  }\n\n  pub(crate) fn next(&mut self) -> Number {\n    let id = self.0;\n    self.0 += 1;\n    Number(id)\n  }\n\n  pub(crate) fn constant(i: usize) -> Number {\n    assert!(i < constants().len());\n    Number(i.try_into().unwrap())\n  }\n}\n"
  },
  {
    "path": "src/ordinal.rs",
    "content": "pub(crate) trait Ordinal {\n  /// Convert an index starting at 0 to an ordinal starting at 1\n  fn ordinal(self) -> Self;\n}\n\nimpl Ordinal for usize {\n  fn ordinal(self) -> Self {\n    self + 1\n  }\n}\n"
  },
  {
    "path": "src/output_error.rs",
    "content": "use super::*;\n\n#[derive(Debug)]\npub(crate) enum OutputError {\n  /// Non-zero exit code\n  Code(i32),\n  /// Interrupted by signal\n  Interrupted(Signal),\n  /// IO error\n  Io(io::Error),\n  /// Terminated by signal\n  Signal(i32),\n  /// Unknown failure\n  Unknown,\n  /// Stdout not UTF-8\n  Utf8(str::Utf8Error),\n}\n\nimpl OutputError {\n  pub(crate) fn result_from_exit_status(exit_status: ExitStatus) -> Result<(), Self> {\n    match exit_status.code() {\n      Some(0) => Ok(()),\n      Some(code) => Err(Self::Code(code)),\n      None => match Platform::signal_from_exit_status(exit_status) {\n        Some(signal) => Err(Self::Signal(signal)),\n        None => Err(Self::Unknown),\n      },\n    }\n  }\n}\n\nimpl Display for OutputError {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    match *self {\n      Self::Code(code) => write!(f, \"Process exited with status code {code}\"),\n      Self::Interrupted(signal) => write!(\n        f,\n        \"Process succeeded but `just` was interrupted by signal {signal}\"\n      ),\n      Self::Io(ref io_error) => write!(f, \"Error executing process: {io_error}\"),\n      Self::Signal(signal) => write!(f, \"Process terminated by signal {signal}\"),\n      Self::Unknown => write!(f, \"Process experienced an unknown failure\"),\n      Self::Utf8(ref err) => write!(f, \"Could not convert process stdout to UTF-8: {err}\"),\n    }\n  }\n}\n"
  },
  {
    "path": "src/parameter.rs",
    "content": "use super::*;\n\n#[derive(PartialEq, Debug, Clone, Serialize)]\npub(crate) struct Parameter<'src> {\n  pub(crate) default: Option<Expression<'src>>,\n  pub(crate) export: bool,\n  pub(crate) help: Option<String>,\n  pub(crate) kind: ParameterKind,\n  pub(crate) long: Option<String>,\n  pub(crate) name: Name<'src>,\n  #[serde(skip)]\n  pub(crate) number: Number,\n  pub(crate) pattern: Option<Pattern<'src>>,\n  pub(crate) short: Option<char>,\n  pub(crate) value: Option<String>,\n}\n\nimpl<'src> Parameter<'src> {\n  pub(crate) fn is_option(&self) -> bool {\n    self.long.is_some() || self.short.is_some()\n  }\n\n  pub(crate) fn is_required(&self) -> bool {\n    self.default.is_none() && self.kind != ParameterKind::Star\n  }\n\n  pub(crate) fn check_pattern_match(\n    &self,\n    recipe: &Recipe<'src>,\n    value: &str,\n  ) -> Result<(), Error<'src>> {\n    let Some(pattern) = &self.pattern else {\n      return Ok(());\n    };\n\n    if pattern.is_match(value) {\n      return Ok(());\n    }\n\n    Err(Error::ArgumentPatternMismatch {\n      argument: value.into(),\n      parameter: self.name.lexeme(),\n      pattern: Box::new(pattern.clone()),\n      recipe: recipe.name(),\n    })\n  }\n}\n\nimpl ColorDisplay for Parameter<'_> {\n  fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {\n    if let Some(prefix) = self.kind.prefix() {\n      write!(f, \"{}\", color.annotation().paint(prefix))?;\n    }\n    if self.export {\n      write!(f, \"$\")?;\n    }\n    write!(f, \"{}\", color.parameter().paint(self.name.lexeme()))?;\n    if let Some(ref default) = self.default {\n      write!(f, \"={}\", color.string().paint(&default.to_string()))?;\n    }\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "src/parameter_kind.rs",
    "content": "use super::*;\n\n/// Parameters can either be…\n#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize)]\n#[serde(rename_all = \"snake_case\")]\npub(crate) enum ParameterKind {\n  /// …variadic, accepting one or more arguments\n  Plus,\n  /// …singular, accepting a single argument\n  Singular,\n  /// …variadic, accepting zero or more arguments\n  Star,\n}\n\nimpl ParameterKind {\n  pub(crate) fn prefix(self) -> Option<&'static str> {\n    match self {\n      Self::Singular => None,\n      Self::Plus => Some(\"+\"),\n      Self::Star => Some(\"*\"),\n    }\n  }\n\n  pub(crate) fn is_variadic(self) -> bool {\n    self != Self::Singular\n  }\n}\n"
  },
  {
    "path": "src/parser.rs",
    "content": "use {super::*, TokenKind::*};\n\n/// Just language parser\n///\n/// The parser is a (hopefully) straightforward recursive descent parser.\n///\n/// It uses a few tokens of lookahead to disambiguate different constructs.\n///\n/// The `expect_*` and `presume_`* methods are similar in that they assert the\n/// type of unparsed tokens and consume them. However, upon encountering an\n/// unexpected token, the `expect_*` methods return an unexpected token error,\n/// whereas the `presume_*` tokens return an internal error.\n///\n/// The `presume_*` methods are used when the token stream has been inspected in\n/// some other way, and thus encountering an unexpected token is a bug in Just,\n/// and not a syntax error.\n///\n/// All methods starting with `parse_*` parse and return a language construct.\n///\n/// The parser tracks an expected set of tokens as it parses. This set contains\n/// all tokens which would have been accepted at the current point in the\n/// parse. Whenever the parser tests for a token that would be accepted, but\n/// does not find it, it adds that token to the set. When the parser accepts a\n/// token, the set is cleared. If the parser finds a token which is unexpected,\n/// the elements of the set are printed in the resultant error message.\npub(crate) struct Parser<'run, 'src> {\n  expected_tokens: BTreeSet<TokenKind>,\n  file_depth: u32,\n  import_offsets: Vec<usize>,\n  module_namepath: Option<&'run Namepath<'src>>,\n  next_token: usize,\n  numerator: &'run mut Numerator,\n  recursion_depth: usize,\n  tokens: &'run [Token<'src>],\n  unstable_features: BTreeSet<UnstableFeature>,\n  working_directory: &'run Path,\n}\n\nimpl<'run, 'src> Parser<'run, 'src> {\n  /// Parse `tokens` into an `Ast`\n  pub(crate) fn parse(\n    file_depth: u32,\n    import_offsets: &[usize],\n    module_namepath: Option<&'run Namepath<'src>>,\n    numerator: &'run mut Numerator,\n    tokens: &'run [Token<'src>],\n    working_directory: &'run Path,\n  ) -> CompileResult<'src, Ast<'src>> {\n    Self {\n      expected_tokens: BTreeSet::new(),\n      file_depth,\n      import_offsets: import_offsets.to_vec(),\n      module_namepath,\n      next_token: 0,\n      numerator,\n      recursion_depth: 0,\n      tokens,\n      unstable_features: BTreeSet::new(),\n      working_directory,\n    }\n    .parse_ast()\n  }\n\n  pub(crate) fn parse_source(\n    numerator: &mut Numerator,\n    path: &'src Path,\n    source: &Source<'src>,\n    src: &'src str,\n  ) -> CompileResult<'src, Ast<'src>> {\n    let tokens = Lexer::lex(path, src)?;\n\n    Parser::parse(\n      source.file_depth,\n      &source.import_offsets,\n      source.namepath.as_ref(),\n      numerator,\n      &tokens,\n      &source.working_directory,\n    )\n  }\n\n  #[cfg(test)]\n  pub(crate) fn parse_tokens(\n    numerator: &'run mut Numerator,\n    tokens: &'run [Token<'src>],\n  ) -> CompileResult<'src, Ast<'src>> {\n    Self::parse(0, &[], None, numerator, tokens, \"\".as_ref())\n  }\n\n  fn error(&self, kind: CompileErrorKind<'src>) -> CompileResult<'src, CompileError<'src>> {\n    Ok(self.next()?.error(kind))\n  }\n\n  /// Construct an unexpected token error with the token returned by\n  /// `Parser::next`\n  fn unexpected_token(&self) -> CompileResult<'src, CompileError<'src>> {\n    self.error(CompileErrorKind::UnexpectedToken {\n      expected: self\n        .expected_tokens\n        .iter()\n        .copied()\n        .filter(|kind| *kind != ByteOrderMark)\n        .collect::<Vec<TokenKind>>(),\n      found: self.next()?.kind,\n    })\n  }\n\n  fn internal_error(&self, message: impl Into<String>) -> CompileResult<'src, CompileError<'src>> {\n    self.error(CompileErrorKind::Internal {\n      message: message.into(),\n    })\n  }\n\n  /// An iterator over the remaining significant tokens\n  fn rest(&self) -> impl Iterator<Item = Token<'src>> + 'run {\n    self.tokens[self.next_token..]\n      .iter()\n      .copied()\n      .filter(|token| token.kind != Whitespace)\n  }\n\n  /// The next significant token\n  fn next(&self) -> CompileResult<'src, Token<'src>> {\n    if let Some(token) = self.rest().next() {\n      Ok(token)\n    } else {\n      Err(self.internal_error(\"`Parser::next()` called after end of token stream\")?)\n    }\n  }\n\n  /// Check if the next significant token is of kind `kind`\n  fn next_is(&mut self, kind: TokenKind) -> bool {\n    self.next_are(&[kind])\n  }\n\n  /// Check if the next significant tokens are of kinds `kinds`\n  ///\n  /// The first token in `kinds` will be added to the expected token set.\n  fn next_are(&mut self, kinds: &[TokenKind]) -> bool {\n    if let Some(&kind) = kinds.first() {\n      self.expected_tokens.insert(kind);\n    }\n\n    let mut rest = self.rest();\n    for kind in kinds {\n      match rest.next() {\n        Some(token) => {\n          if token.kind != *kind {\n            return false;\n          }\n        }\n        None => return false,\n      }\n    }\n    true\n  }\n\n  /// Advance past one significant token, clearing the expected token set.\n  fn advance(&mut self) -> CompileResult<'src, Token<'src>> {\n    self.expected_tokens.clear();\n\n    for skipped in &self.tokens[self.next_token..] {\n      self.next_token += 1;\n\n      if skipped.kind != Whitespace {\n        return Ok(*skipped);\n      }\n    }\n\n    Err(self.internal_error(\"`Parser::advance()` advanced past end of token stream\")?)\n  }\n\n  /// Return next token if it is of kind `expected`, otherwise, return an\n  /// unexpected token error\n  fn expect(&mut self, expected: TokenKind) -> CompileResult<'src, Token<'src>> {\n    if let Some(token) = self.accept(expected)? {\n      Ok(token)\n    } else {\n      Err(self.unexpected_token()?)\n    }\n  }\n\n  /// Return the next token if it is any of kinds in `expected`, otherwise,\n  /// return an unexpected token error\n  fn expect_any(&mut self, expected: &[TokenKind]) -> CompileResult<'src, Token<'src>> {\n    for &kind in expected {\n      if let Some(token) = self.accept(kind)? {\n        return Ok(token);\n      }\n    }\n\n    Err(self.unexpected_token()?)\n  }\n\n  /// Return an unexpected token error if the next token is not an EOL\n  fn expect_eol(&mut self) -> CompileResult<'src> {\n    self.accept(Comment)?;\n\n    if self.next_is(Eof) {\n      return Ok(());\n    }\n\n    self.expect(Eol).map(|_| ())\n  }\n\n  fn expect_keyword(&mut self, expected: Keyword) -> CompileResult<'src> {\n    let found = self.advance()?;\n\n    if found.kind == Identifier && expected == found.lexeme() {\n      Ok(())\n    } else {\n      Err(found.error(CompileErrorKind::ExpectedKeyword {\n        expected: vec![expected],\n        found,\n      }))\n    }\n  }\n\n  /// Return an internal error if the next token is not of kind `Identifier`\n  /// with lexeme `lexeme`.\n  fn presume_keyword(&mut self, keyword: Keyword) -> CompileResult<'src> {\n    let next = self.advance()?;\n\n    if next.kind != Identifier {\n      Err(self.internal_error(format!(\n        \"Presumed next token would have kind {Identifier}, but found {}\",\n        next.kind\n      ))?)\n    } else if keyword == next.lexeme() {\n      Ok(())\n    } else {\n      Err(self.internal_error(format!(\n        \"Presumed next token would have lexeme \\\"{keyword}\\\", but found \\\"{}\\\"\",\n        next.lexeme(),\n      ))?)\n    }\n  }\n\n  /// Return an internal error if the next token is not of kind `kind`.\n  fn presume(&mut self, kind: TokenKind) -> CompileResult<'src, Token<'src>> {\n    let next = self.advance()?;\n\n    if next.kind == kind {\n      Ok(next)\n    } else {\n      Err(self.internal_error(format!(\n        \"Presumed next token would have kind {kind:?}, but found {:?}\",\n        next.kind\n      ))?)\n    }\n  }\n\n  /// Return an internal error if the next token is not one of kinds `kinds`.\n  fn presume_any(&mut self, kinds: &[TokenKind]) -> CompileResult<'src, Token<'src>> {\n    let next = self.advance()?;\n    if kinds.contains(&next.kind) {\n      Ok(next)\n    } else {\n      Err(self.internal_error(format!(\n        \"Presumed next token would be {}, but found {}\",\n        List::or(kinds),\n        next.kind\n      ))?)\n    }\n  }\n\n  /// Accept and return a token of kind `kind`\n  fn accept(&mut self, kind: TokenKind) -> CompileResult<'src, Option<Token<'src>>> {\n    if self.next_is(kind) {\n      Ok(Some(self.advance()?))\n    } else {\n      Ok(None)\n    }\n  }\n\n  /// Return an error if the next token is of kind `forbidden`\n  fn forbid<F>(&self, forbidden: TokenKind, error: F) -> CompileResult<'src>\n  where\n    F: FnOnce(Token) -> CompileError,\n  {\n    let next = self.next()?;\n\n    if next.kind == forbidden {\n      Err(error(next))\n    } else {\n      Ok(())\n    }\n  }\n\n  /// Accept a double-colon separated sequence of identifiers\n  fn accept_namepath(&mut self) -> CompileResult<'src, Option<Namepath<'src>>> {\n    if self.next_is(Identifier) {\n      Ok(Some(self.parse_namepath()?))\n    } else {\n      Ok(None)\n    }\n  }\n\n  fn accept_keyword(&mut self, keyword: Keyword) -> CompileResult<'src, Option<Name<'src>>> {\n    let next = self.next()?;\n\n    if next.kind == Identifier && next.lexeme() == keyword.lexeme() {\n      self.advance()?;\n      Ok(Some(Name::from_identifier(next)))\n    } else {\n      Ok(None)\n    }\n  }\n\n  fn accepted_keyword(&mut self, keyword: Keyword) -> CompileResult<'src, bool> {\n    Ok(self.accept_keyword(keyword)?.is_some())\n  }\n\n  /// Accept a dependency\n  fn accept_dependency(&mut self) -> CompileResult<'src, Option<UnresolvedDependency<'src>>> {\n    if let Some(recipe) = self.accept_namepath()? {\n      Ok(Some(UnresolvedDependency {\n        arguments: Vec::new(),\n        recipe,\n      }))\n    } else if self.accepted(ParenL)? {\n      let recipe = self.parse_namepath()?;\n\n      let mut arguments = Vec::new();\n\n      while !self.accepted(ParenR)? {\n        arguments.push(self.parse_expression()?);\n      }\n\n      Ok(Some(UnresolvedDependency { arguments, recipe }))\n    } else {\n      Ok(None)\n    }\n  }\n\n  /// Accept and return `true` if next token is of kind `kind`\n  fn accepted(&mut self, kind: TokenKind) -> CompileResult<'src, bool> {\n    Ok(self.accept(kind)?.is_some())\n  }\n\n  /// Parse a justfile, consumes self\n  fn parse_ast(mut self) -> CompileResult<'src, Ast<'src>> {\n    fn pop_doc_comment<'src>(\n      items: &mut Vec<Item<'src>>,\n      eol_since_last_comment: bool,\n    ) -> Option<&'src str> {\n      if !eol_since_last_comment {\n        if let Some(Item::Comment(contents)) = items.last() {\n          let doc = Some(contents[1..].trim_start());\n          items.pop();\n          return doc;\n        }\n      }\n\n      None\n    }\n\n    let mut items = Vec::new();\n\n    let mut eol_since_last_comment = false;\n\n    self.accept(ByteOrderMark)?;\n\n    loop {\n      let mut attributes = self.parse_attributes()?;\n      let mut take_attributes = || {\n        attributes\n          .take()\n          .map(|(_token, attributes)| attributes)\n          .unwrap_or_default()\n      };\n\n      let next = self.next()?;\n\n      if let Some(comment) = self.accept(Comment)? {\n        items.push(Item::Comment(comment.lexeme().trim_end()));\n        self.expect_eol()?;\n        eol_since_last_comment = false;\n      } else if self.accepted(Eol)? {\n        eol_since_last_comment = true;\n      } else if self.accepted(Eof)? {\n        break;\n      } else if self.next_is(Identifier) {\n        match Keyword::from_lexeme(next.lexeme()) {\n          Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {\n            items.push(Item::Alias(self.parse_alias(take_attributes())?));\n          }\n          Some(Keyword::Eager) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {\n            self\n              .unstable_features\n              .insert(UnstableFeature::EagerAssignments);\n            self.presume_keyword(Keyword::Eager)?;\n            items.push(Item::Assignment(self.parse_assignment(\n              take_attributes(),\n              true,\n              false,\n            )?));\n          }\n          Some(Keyword::Export) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {\n            self.presume_keyword(Keyword::Export)?;\n            items.push(Item::Assignment(self.parse_assignment(\n              take_attributes(),\n              false,\n              true,\n            )?));\n          }\n          Some(Keyword::Unexport)\n            if self.next_are(&[Identifier, Identifier, Eof])\n              || self.next_are(&[Identifier, Identifier, Eol]) =>\n          {\n            self.presume_keyword(Keyword::Unexport)?;\n            let name = self.parse_name()?;\n            self.expect_eol()?;\n            items.push(Item::Unexport { name });\n          }\n          Some(Keyword::Import)\n            if self.next_are(&[Identifier, StringToken])\n              || self.next_are(&[Identifier, Identifier, StringToken])\n              || self.next_are(&[Identifier, QuestionMark]) =>\n          {\n            self.presume_keyword(Keyword::Import)?;\n            let optional = self.accepted(QuestionMark)?;\n            let relative = self.parse_string_literal()?;\n            items.push(Item::Import {\n              absolute: None,\n              optional,\n              relative,\n            });\n          }\n          Some(Keyword::Mod)\n            if self.next_are(&[Identifier, Identifier, Comment])\n              || self.next_are(&[Identifier, Identifier, Eof])\n              || self.next_are(&[Identifier, Identifier, Eol])\n              || self.next_are(&[Identifier, Identifier, Identifier, StringToken])\n              || self.next_are(&[Identifier, Identifier, StringToken])\n              || self.next_are(&[Identifier, QuestionMark]) =>\n          {\n            let doc = pop_doc_comment(&mut items, eol_since_last_comment);\n\n            self.presume_keyword(Keyword::Mod)?;\n\n            let optional = self.accepted(QuestionMark)?;\n\n            let name = self.parse_name()?;\n\n            let relative = if self.next_is(StringToken) || self.next_are(&[Identifier, StringToken])\n            {\n              Some(self.parse_string_literal()?)\n            } else {\n              None\n            };\n\n            let attributes = take_attributes();\n\n            attributes.ensure_valid_attributes(\n              \"Module\",\n              *name,\n              &[\n                AttributeDiscriminant::Doc,\n                AttributeDiscriminant::Group,\n                AttributeDiscriminant::Private,\n              ],\n            )?;\n\n            let doc = match attributes.get(AttributeDiscriminant::Doc) {\n              Some(Attribute::Doc(Some(doc))) => Some(doc.cooked.clone()),\n              Some(Attribute::Doc(None)) => None,\n              None => doc.map(ToOwned::to_owned),\n              _ => unreachable!(),\n            };\n\n            let private = attributes.contains(AttributeDiscriminant::Private);\n\n            let mut groups = Vec::new();\n            for attribute in attributes {\n              if let Attribute::Group(group) = attribute {\n                groups.push(group);\n              }\n            }\n\n            items.push(Item::Module {\n              absolute: None,\n              doc,\n              groups,\n              name,\n              optional,\n              private,\n              relative,\n            });\n          }\n          Some(Keyword::Set)\n            if self.next_are(&[Identifier, Identifier, ColonEquals])\n              || self.next_are(&[Identifier, Identifier, Comment, Eof])\n              || self.next_are(&[Identifier, Identifier, Comment, Eol])\n              || self.next_are(&[Identifier, Identifier, Eof])\n              || self.next_are(&[Identifier, Identifier, Eol]) =>\n          {\n            items.push(Item::Set(self.parse_set()?));\n          }\n          _ => {\n            if self.next_are(&[Identifier, ColonEquals]) {\n              items.push(Item::Assignment(self.parse_assignment(\n                take_attributes(),\n                false,\n                false,\n              )?));\n            } else {\n              let doc = pop_doc_comment(&mut items, eol_since_last_comment);\n              items.push(Item::Recipe(self.parse_recipe(\n                take_attributes(),\n                doc,\n                false,\n              )?));\n            }\n          }\n        }\n      } else if self.accepted(At)? {\n        let doc = pop_doc_comment(&mut items, eol_since_last_comment);\n        items.push(Item::Recipe(self.parse_recipe(\n          take_attributes(),\n          doc,\n          true,\n        )?));\n      } else {\n        return Err(self.unexpected_token()?);\n      }\n\n      if let Some((token, attributes)) = attributes {\n        return Err(token.error(CompileErrorKind::ExtraneousAttributes {\n          count: attributes.len(),\n        }));\n      }\n    }\n\n    if self.next_token != self.tokens.len() {\n      return Err(self.internal_error(format!(\n        \"Parse completed with {} unparsed tokens\",\n        self.tokens.len() - self.next_token,\n      ))?);\n    }\n\n    Ok(Ast {\n      items,\n      modulepath: self.module_namepath.map(Into::into).unwrap_or_default(),\n      unstable_features: self.unstable_features,\n      warnings: Vec::new(),\n      working_directory: self.working_directory.into(),\n    })\n  }\n\n  /// Parse an alias, e.g `alias name := target`\n  fn parse_alias(\n    &mut self,\n    attributes: AttributeSet<'src>,\n  ) -> CompileResult<'src, Alias<'src, Namepath<'src>>> {\n    self.presume_keyword(Keyword::Alias)?;\n    let name = self.parse_name()?;\n    self.presume_any(&[Equals, ColonEquals])?;\n    let target = self.parse_namepath()?;\n    self.expect_eol()?;\n\n    attributes.ensure_valid_attributes(\"Alias\", *name, &[AttributeDiscriminant::Private])?;\n\n    Ok(Alias {\n      attributes,\n      name,\n      target,\n    })\n  }\n\n  /// Parse an assignment, e.g. `foo := bar`\n  fn parse_assignment(\n    &mut self,\n    attributes: AttributeSet<'src>,\n    eager: bool,\n    export: bool,\n  ) -> CompileResult<'src, Assignment<'src>> {\n    let name = self.parse_name()?;\n    self.presume(ColonEquals)?;\n    let value = self.parse_expression()?;\n    self.expect_eol()?;\n\n    let private = attributes.contains(AttributeDiscriminant::Private);\n\n    attributes.ensure_valid_attributes(\"Assignment\", *name, &[AttributeDiscriminant::Private])?;\n\n    Ok(Assignment {\n      eager,\n      export,\n      file_depth: self.file_depth,\n      name,\n      number: self.numerator.next(),\n      prelude: false,\n      private: private || name.lexeme().starts_with('_'),\n      value,\n    })\n  }\n\n  /// Parse an expression, e.g. `1 + 2`\n  fn parse_expression(&mut self) -> CompileResult<'src, Expression<'src>> {\n    if self.recursion_depth == if cfg!(windows) { 48 } else { 256 } {\n      let token = self.next()?;\n      return Err(CompileError::new(\n        token,\n        CompileErrorKind::ParsingRecursionDepthExceeded,\n      ));\n    }\n\n    self.recursion_depth += 1;\n\n    let disjunct = self.parse_disjunct()?;\n\n    let expression = if self.accepted(BarBar)? {\n      self\n        .unstable_features\n        .insert(UnstableFeature::LogicalOperators);\n      let lhs = disjunct.into();\n      let rhs = self.parse_expression()?.into();\n      Expression::Or { lhs, rhs }\n    } else {\n      disjunct\n    };\n\n    self.recursion_depth -= 1;\n\n    Ok(expression)\n  }\n\n  fn parse_disjunct(&mut self) -> CompileResult<'src, Expression<'src>> {\n    let conjunct = self.parse_conjunct()?;\n\n    let disjunct = if self.accepted(AmpersandAmpersand)? {\n      self\n        .unstable_features\n        .insert(UnstableFeature::LogicalOperators);\n      let lhs = conjunct.into();\n      let rhs = self.parse_disjunct()?.into();\n      Expression::And { lhs, rhs }\n    } else {\n      conjunct\n    };\n\n    Ok(disjunct)\n  }\n\n  fn parse_conjunct(&mut self) -> CompileResult<'src, Expression<'src>> {\n    if self.accepted_keyword(Keyword::If)? {\n      self.parse_conditional()\n    } else if self.accepted(Slash)? {\n      let lhs = None;\n      let rhs = self.parse_conjunct()?.into();\n      Ok(Expression::Join { lhs, rhs })\n    } else {\n      let value = self.parse_value()?;\n\n      if self.accepted(Slash)? {\n        let lhs = Some(Box::new(value));\n        let rhs = self.parse_conjunct()?.into();\n        Ok(Expression::Join { lhs, rhs })\n      } else if self.accepted(Plus)? {\n        let lhs = value.into();\n        let rhs = self.parse_conjunct()?.into();\n        Ok(Expression::Concatenation { lhs, rhs })\n      } else {\n        Ok(value)\n      }\n    }\n  }\n\n  /// Parse a conditional, e.g. `if a == b { \"foo\" } else { \"bar\" }`\n  fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> {\n    let condition = self.parse_condition()?;\n\n    self.expect(BraceL)?;\n\n    let then = self.parse_expression()?;\n\n    self.expect(BraceR)?;\n\n    self.expect_keyword(Keyword::Else)?;\n\n    let otherwise = if self.accepted_keyword(Keyword::If)? {\n      self.parse_conditional()?\n    } else {\n      self.expect(BraceL)?;\n      let otherwise = self.parse_expression()?;\n      self.expect(BraceR)?;\n      otherwise\n    };\n\n    Ok(Expression::Conditional {\n      condition,\n      then: then.into(),\n      otherwise: otherwise.into(),\n    })\n  }\n\n  fn parse_condition(&mut self) -> CompileResult<'src, Condition<'src>> {\n    let lhs = self.parse_expression()?;\n    let operator = if self.accepted(BangEquals)? {\n      ConditionalOperator::Inequality\n    } else if self.accepted(EqualsTilde)? {\n      ConditionalOperator::RegexMatch\n    } else if self.accepted(BangTilde)? {\n      ConditionalOperator::RegexMismatch\n    } else {\n      self.expect(EqualsEquals)?;\n      ConditionalOperator::Equality\n    };\n    let rhs = self.parse_expression()?;\n    Ok(Condition {\n      lhs: lhs.into(),\n      rhs: rhs.into(),\n      operator,\n    })\n  }\n\n  fn parse_format_string(&mut self) -> CompileResult<'src, Expression<'src>> {\n    self.expect_keyword(Keyword::F)?;\n\n    let start = self.parse_string_literal_in_state(StringState::FormatStart)?;\n\n    let kind = StringKind::from_string_or_backtick(start.token)?;\n\n    let mut more = start.token.kind == FormatStringStart;\n\n    let mut expressions = Vec::new();\n\n    while more {\n      let expression = self.parse_expression()?;\n      more = self.next_is(FormatStringContinue);\n      expressions.push((\n        expression,\n        self.parse_string_literal_in_state(StringState::FormatContinue(kind))?,\n      ));\n    }\n\n    Ok(Expression::FormatString { start, expressions })\n  }\n\n  // Check if the next tokens are a shell-expanded string, i.e., `x\"foo\"`.\n  //\n  // This function skips initial whitespace tokens, but thereafter is\n  // whitespace-sensitive, so `x\"foo\"` is a shell-expanded string, whereas `x\n  // \"foo\"` is not.\n  fn next_is_shell_expanded_string(&self) -> bool {\n    let mut tokens = self\n      .tokens\n      .iter()\n      .skip(self.next_token)\n      .skip_while(|token| token.kind == Whitespace);\n\n    tokens\n      .next()\n      .is_some_and(|token| token.kind == Identifier && token.lexeme() == Keyword::X.lexeme())\n      && tokens.next().is_some_and(|token| token.kind == StringToken)\n  }\n\n  // Check if the next tokens are a format string, i.e., `f\"foo\"`.\n  //\n  // This function skips initial whitespace tokens, but thereafter is\n  // whitespace-sensitive, so `f\"foo\"` is a format string, whereas `f\n  // \"foo\"` is not.\n  fn next_is_format_string(&self) -> bool {\n    let mut tokens = self\n      .tokens\n      .iter()\n      .skip(self.next_token)\n      .skip_while(|token| token.kind == Whitespace);\n\n    tokens\n      .next()\n      .is_some_and(|token| token.kind == Identifier && token.lexeme() == Keyword::F.lexeme())\n      && tokens\n        .next()\n        .is_some_and(|token| matches!(token.kind, StringToken | FormatStringStart))\n  }\n\n  /// Parse a value, e.g. `(bar)`\n  fn parse_value(&mut self) -> CompileResult<'src, Expression<'src>> {\n    if self.next_is(StringToken) || self.next_is_shell_expanded_string() {\n      Ok(Expression::StringLiteral {\n        string_literal: self.parse_string_literal()?,\n      })\n    } else if self.next_is_format_string() {\n      self.parse_format_string()\n    } else if self.next_is(Backtick) {\n      let next = self.next()?;\n      let kind = StringKind::from_string_or_backtick(next)?;\n      let contents =\n        &next.lexeme()[kind.delimiter_len()..next.lexeme().len() - kind.delimiter_len()];\n      let token = self.advance()?;\n      let contents = if kind.indented() {\n        unindent(contents)\n      } else {\n        contents.to_owned()\n      };\n\n      if contents.starts_with(\"#!\") {\n        return Err(next.error(CompileErrorKind::BacktickShebang));\n      }\n      Ok(Expression::Backtick { contents, token })\n    } else if self.next_is(Identifier) {\n      if let Some(name) = self.accept_keyword(Keyword::Assert)? {\n        self.expect(ParenL)?;\n        let condition = self.parse_condition()?;\n        self.expect(Comma)?;\n        let error = Box::new(self.parse_expression()?);\n        self.expect(ParenR)?;\n        Ok(Expression::Assert {\n          condition,\n          error,\n          name,\n        })\n      } else {\n        let name = self.parse_name()?;\n\n        if self.next_is(ParenL) {\n          let arguments = self.parse_sequence()?;\n          if name.lexeme() == \"which\" {\n            self\n              .unstable_features\n              .insert(UnstableFeature::WhichFunction);\n          }\n          Ok(Expression::Call {\n            thunk: Thunk::resolve(name, arguments)?,\n          })\n        } else {\n          Ok(Expression::Variable { name })\n        }\n      }\n    } else if self.next_is(ParenL) {\n      self.presume(ParenL)?;\n      let contents = self.parse_expression()?.into();\n      self.expect(ParenR)?;\n      Ok(Expression::Group { contents })\n    } else {\n      Err(self.unexpected_token()?)\n    }\n  }\n\n  /// Parse a string literal, e.g. `\"FOO\"`\n  fn parse_string_literal(&mut self) -> CompileResult<'src, StringLiteral<'src>> {\n    self.parse_string_literal_in_state(StringState::Normal)\n  }\n\n  /// Parse a string literal, e.g. `\"FOO\"`\n  fn parse_string_literal_in_state(\n    &mut self,\n    state: StringState,\n  ) -> CompileResult<'src, StringLiteral<'src>> {\n    let expand = if self.next_is(Identifier) {\n      self.expect_keyword(Keyword::X)?;\n      true\n    } else {\n      false\n    };\n\n    let token = match state {\n      StringState::Normal => self.expect(StringToken)?,\n      StringState::FormatStart => self.expect_any(&[StringToken, FormatStringStart])?,\n      StringState::FormatContinue(_) => {\n        self.expect_any(&[FormatStringContinue, FormatStringEnd])?\n      }\n    };\n\n    let kind = match state {\n      StringState::Normal | StringState::FormatStart => StringKind::from_string_or_backtick(token)?,\n      StringState::FormatContinue(kind) => kind,\n    };\n\n    let open = if matches!(token.kind, FormatStringContinue | FormatStringEnd) {\n      Lexer::INTERPOLATION_END.len()\n    } else {\n      kind.delimiter_len()\n    };\n\n    let close = if matches!(token.kind, FormatStringStart | FormatStringContinue) {\n      Lexer::INTERPOLATION_START.len()\n    } else {\n      kind.delimiter_len()\n    };\n\n    let raw = &token.lexeme()[open..token.lexeme().len() - close];\n\n    let unindented = if kind.indented() && matches!(token.kind, StringToken) {\n      unindent(raw)\n    } else {\n      raw.to_owned()\n    };\n\n    let undelimited = if matches!(state, StringState::Normal) {\n      unindented\n    } else {\n      unindented.replace(Lexer::INTERPOLATION_ESCAPE, Lexer::INTERPOLATION_START)\n    };\n\n    let cooked = if kind.processes_escape_sequences() {\n      Self::cook_string(token, &undelimited)?\n    } else {\n      undelimited\n    };\n\n    let cooked = if expand {\n      shellexpand::full(&cooked)\n        .map_err(|err| token.error(CompileErrorKind::ShellExpansion { err }))?\n        .into_owned()\n    } else {\n      cooked\n    };\n\n    Ok(StringLiteral {\n      token,\n      cooked,\n      expand,\n      kind,\n      part: match token.kind {\n        FormatStringStart => Some(FormatStringPart::Start),\n        FormatStringContinue => Some(FormatStringPart::Continue),\n        FormatStringEnd => Some(FormatStringPart::End),\n        StringToken => {\n          if matches!(state, StringState::Normal) {\n            None\n          } else {\n            Some(FormatStringPart::Single)\n          }\n        }\n        _ => {\n          return Err(token.error(CompileErrorKind::Internal {\n            message: \"unexpected token kind while parsing string literal\".into(),\n          }));\n        }\n      },\n    })\n  }\n\n  // Transform escape sequences in from string literal `token` with content `text`\n  fn cook_string(token: Token<'src>, text: &str) -> CompileResult<'src, String> {\n    #[derive(PartialEq, Eq)]\n    enum State {\n      Backslash,\n      Initial,\n      Unicode,\n      UnicodeValue { hex: String },\n    }\n\n    let mut cooked = String::new();\n\n    let mut state = State::Initial;\n\n    for c in text.chars() {\n      match state {\n        State::Initial => {\n          if c == '\\\\' {\n            state = State::Backslash;\n          } else {\n            cooked.push(c);\n          }\n        }\n        State::Backslash if c == 'u' => {\n          state = State::Unicode;\n        }\n        State::Backslash => {\n          match c {\n            'n' => cooked.push('\\n'),\n            'r' => cooked.push('\\r'),\n            't' => cooked.push('\\t'),\n            '\\\\' => cooked.push('\\\\'),\n            '\\n' => {}\n            '\"' => cooked.push('\"'),\n            character => {\n              return Err(token.error(CompileErrorKind::InvalidEscapeSequence { character }));\n            }\n          }\n          state = State::Initial;\n        }\n        State::Unicode => match c {\n          '{' => {\n            state = State::UnicodeValue { hex: String::new() };\n          }\n          character => {\n            return Err(token.error(CompileErrorKind::UnicodeEscapeDelimiter { character }));\n          }\n        },\n        State::UnicodeValue { ref mut hex } => match c {\n          '}' => {\n            if hex.is_empty() {\n              return Err(token.error(CompileErrorKind::UnicodeEscapeEmpty));\n            }\n\n            let codepoint = u32::from_str_radix(hex, 16).unwrap();\n\n            cooked.push(char::from_u32(codepoint).ok_or_else(|| {\n              token.error(CompileErrorKind::UnicodeEscapeRange { hex: hex.clone() })\n            })?);\n\n            state = State::Initial;\n          }\n          '0'..='9' | 'A'..='F' | 'a'..='f' => {\n            hex.push(c);\n            if hex.len() > 6 {\n              return Err(token.error(CompileErrorKind::UnicodeEscapeLength { hex: hex.clone() }));\n            }\n          }\n          _ => {\n            return Err(token.error(CompileErrorKind::UnicodeEscapeCharacter { character: c }));\n          }\n        },\n      }\n    }\n\n    if state != State::Initial {\n      return Err(token.error(CompileErrorKind::UnicodeEscapeUnterminated));\n    }\n\n    Ok(cooked)\n  }\n\n  /// Parse a name from an identifier token\n  fn parse_name(&mut self) -> CompileResult<'src, Name<'src>> {\n    self.expect(Identifier).map(Name::from_identifier)\n  }\n\n  /// Parse a path of `::` separated names\n  fn parse_namepath(&mut self) -> CompileResult<'src, Namepath<'src>> {\n    let first = self.parse_name()?;\n    let mut path = Namepath::from(first);\n\n    while self.accepted(ColonColon)? {\n      let name = self.parse_name()?;\n      path.push(name);\n    }\n\n    Ok(path)\n  }\n\n  /// Parse sequence of comma-separated expressions\n  fn parse_sequence(&mut self) -> CompileResult<'src, Vec<Expression<'src>>> {\n    self.presume(ParenL)?;\n\n    let mut elements = Vec::new();\n\n    while !self.next_is(ParenR) {\n      elements.push(self.parse_expression()?);\n\n      if !self.accepted(Comma)? {\n        break;\n      }\n    }\n\n    self.expect(ParenR)?;\n\n    Ok(elements)\n  }\n\n  /// Parse a recipe\n  fn parse_recipe(\n    &mut self,\n    attributes: AttributeSet<'src>,\n    doc: Option<&'src str>,\n    quiet: bool,\n  ) -> CompileResult<'src, UnresolvedRecipe<'src>> {\n    let name = self.parse_name()?;\n\n    let mut positional = Vec::new();\n\n    let mut longs = HashSet::new();\n    let mut shorts = HashSet::new();\n\n    let mut arg_attributes = BTreeMap::new();\n\n    for attribute in &attributes {\n      let Attribute::Arg {\n        help,\n        long,\n        long_key,\n        name: arg,\n        pattern,\n        short,\n        value,\n        ..\n      } = attribute\n      else {\n        continue;\n      };\n\n      if let Some(option) = long {\n        if !longs.insert(&option.cooked) {\n          return Err(\n            long_key\n              .unwrap_or(option.token)\n              .error(CompileErrorKind::DuplicateOption {\n                option: Switch::Long(option.cooked.clone()),\n                recipe: name.lexeme(),\n              }),\n          );\n        }\n      }\n\n      if let Some(option) = short {\n        if !shorts.insert(&option.cooked) {\n          return Err(option.token.error(CompileErrorKind::DuplicateOption {\n            option: Switch::Short(option.cooked.chars().next().unwrap()),\n            recipe: name.lexeme(),\n          }));\n        }\n      }\n\n      arg_attributes.insert(\n        arg.cooked.clone(),\n        ArgAttribute {\n          help: help.as_ref().map(|literal| literal.cooked.clone()),\n          name: arg.token,\n          pattern: pattern.clone(),\n          long: long.as_ref().map(|long| long.cooked.clone()),\n          short: short\n            .as_ref()\n            .map(|short| short.cooked.chars().next().unwrap()),\n          value: value.as_ref().map(|value| value.cooked.clone()),\n        },\n      );\n    }\n\n    while self.next_is(Identifier) || self.next_is(Dollar) {\n      positional.push(self.parse_parameter(&mut arg_attributes, ParameterKind::Singular)?);\n    }\n\n    let kind = if self.accepted(Plus)? {\n      ParameterKind::Plus\n    } else if self.accepted(Asterisk)? {\n      ParameterKind::Star\n    } else {\n      ParameterKind::Singular\n    };\n\n    let variadic = if kind.is_variadic() {\n      let variadic = self.parse_parameter(&mut arg_attributes, kind)?;\n\n      self.forbid(Identifier, |token| {\n        token.error(CompileErrorKind::ParameterFollowsVariadicParameter {\n          parameter: token.lexeme(),\n        })\n      })?;\n\n      Some(variadic)\n    } else {\n      None\n    };\n\n    self.expect(Colon)?;\n\n    if let Some((argument, ArgAttribute { name, .. })) = arg_attributes.into_iter().next() {\n      return Err(name.error(CompileErrorKind::UndefinedArgAttribute { argument }));\n    }\n\n    let mut dependencies = Vec::new();\n\n    while let Some(dependency) = self.accept_dependency()? {\n      dependencies.push(dependency);\n    }\n\n    let priors = dependencies.len();\n\n    if self.accepted(AmpersandAmpersand)? {\n      let mut subsequents = Vec::new();\n\n      while let Some(subsequent) = self.accept_dependency()? {\n        subsequents.push(subsequent);\n      }\n\n      if subsequents.is_empty() {\n        return Err(self.unexpected_token()?);\n      }\n\n      dependencies.append(&mut subsequents);\n    }\n\n    self.expect_eol()?;\n\n    let body = self.parse_body()?;\n\n    let shebang = body.first().is_some_and(Line::is_shebang);\n\n    let script = attributes.contains(AttributeDiscriminant::Script);\n\n    if attributes.contains(AttributeDiscriminant::WorkingDirectory)\n      && attributes.contains(AttributeDiscriminant::NoCd)\n    {\n      return Err(\n        name.error(CompileErrorKind::NoCdAndWorkingDirectoryAttribute {\n          recipe: name.lexeme(),\n        }),\n      );\n    }\n\n    if attributes.contains(AttributeDiscriminant::ExitMessage)\n      && attributes.contains(AttributeDiscriminant::NoExitMessage)\n    {\n      return Err(\n        name.error(CompileErrorKind::ExitMessageAndNoExitMessageAttribute {\n          recipe: name.lexeme(),\n        }),\n      );\n    }\n\n    let private =\n      name.lexeme().starts_with('_') || attributes.contains(AttributeDiscriminant::Private);\n\n    let mut doc = doc.map(ToOwned::to_owned);\n\n    for attribute in &attributes {\n      if let Attribute::Doc(attribute_doc) = attribute {\n        doc = attribute_doc.as_ref().map(|doc| doc.cooked.clone());\n      }\n    }\n\n    Ok(Recipe {\n      attributes,\n      body,\n      dependencies,\n      doc: doc.filter(|doc| !doc.is_empty()),\n      file_depth: self.file_depth,\n      import_offsets: self.import_offsets.clone(),\n      module_path: None,\n      name,\n      recipe_path: None,\n      parameters: positional.into_iter().chain(variadic).collect(),\n      priors,\n      private,\n      quiet,\n      shebang: shebang || script,\n      variable_references: HashSet::new(),\n    })\n  }\n\n  /// Parse a recipe parameter\n  fn parse_parameter(\n    &mut self,\n    arg_attributes: &mut BTreeMap<String, ArgAttribute<'src>>,\n    kind: ParameterKind,\n  ) -> CompileResult<'src, Parameter<'src>> {\n    let export = self.accepted(Dollar)?;\n\n    let name = self.parse_name()?;\n\n    let default = if self.accepted(Equals)? {\n      Some(self.parse_value()?)\n    } else {\n      None\n    };\n\n    let mut help = None;\n    let mut long = None;\n    let mut pattern = None;\n    let mut short = None;\n    let mut value = None;\n\n    if let Some(arg) = arg_attributes.remove(name.lexeme()) {\n      help = arg.help;\n      long = arg.long;\n      pattern = arg.pattern;\n      short = arg.short;\n      value = arg.value;\n    }\n\n    if kind.is_variadic() && (long.is_some() || short.is_some()) {\n      return Err(name.error(CompileErrorKind::VariadicParameterWithOption));\n    }\n\n    Ok(Parameter {\n      default,\n      export,\n      help,\n      kind,\n      long,\n      name,\n      number: self.numerator.next(),\n      pattern,\n      short,\n      value,\n    })\n  }\n\n  /// Parse the body of a recipe\n  fn parse_body(&mut self) -> CompileResult<'src, Vec<Line<'src>>> {\n    let mut lines = Vec::new();\n\n    if self.accepted(Indent)? {\n      while !self.accepted(Dedent)? {\n        let mut fragments = Vec::new();\n        let number = self\n          .tokens\n          .get(self.next_token)\n          .map(|token| token.line)\n          .unwrap_or_default();\n\n        if !self.accepted(Eol)? {\n          while !(self.accepted(Eol)? || self.next_is(Dedent)) {\n            if let Some(token) = self.accept(Text)? {\n              fragments.push(Fragment::Text { token });\n            } else if self.accepted(InterpolationStart)? {\n              fragments.push(Fragment::Interpolation {\n                expression: self.parse_expression()?,\n              });\n              self.expect(InterpolationEnd)?;\n            } else {\n              return Err(self.unexpected_token()?);\n            }\n          }\n        }\n\n        lines.push(Line { fragments, number });\n      }\n    }\n\n    while lines.last().is_some_and(Line::is_empty) {\n      lines.pop();\n    }\n\n    Ok(lines)\n  }\n\n  /// Parse a boolean setting value\n  fn parse_set_bool(&mut self) -> CompileResult<'src, bool> {\n    if !self.accepted(ColonEquals)? {\n      return Ok(true);\n    }\n\n    let identifier = self.expect(Identifier)?;\n\n    let value = if Keyword::True == identifier.lexeme() {\n      true\n    } else if Keyword::False == identifier.lexeme() {\n      false\n    } else {\n      return Err(identifier.error(CompileErrorKind::ExpectedKeyword {\n        expected: vec![Keyword::True, Keyword::False],\n        found: identifier,\n      }));\n    };\n\n    Ok(value)\n  }\n\n  /// Parse a setting\n  fn parse_set(&mut self) -> CompileResult<'src, Set<'src>> {\n    self.presume_keyword(Keyword::Set)?;\n    let name = Name::from_identifier(self.presume(Identifier)?);\n    let lexeme = name.lexeme();\n    let Some(keyword) = Keyword::from_lexeme(lexeme) else {\n      return Err(name.error(CompileErrorKind::UnknownSetting {\n        setting: name.lexeme(),\n      }));\n    };\n\n    let set_bool = match keyword {\n      Keyword::AllowDuplicateRecipes => {\n        Some(Setting::AllowDuplicateRecipes(self.parse_set_bool()?))\n      }\n      Keyword::AllowDuplicateVariables => {\n        Some(Setting::AllowDuplicateVariables(self.parse_set_bool()?))\n      }\n      Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)),\n      Keyword::DotenvOverride => Some(Setting::DotenvOverride(self.parse_set_bool()?)),\n      Keyword::DotenvRequired => Some(Setting::DotenvRequired(self.parse_set_bool()?)),\n      Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)),\n      Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)),\n      Keyword::Guards => Some(Setting::Guards(self.parse_set_bool()?)),\n      Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),\n      Keyword::Lazy => {\n        self.unstable_features.insert(UnstableFeature::LazySetting);\n        Some(Setting::Lazy(self.parse_set_bool()?))\n      }\n      Keyword::NoExitMessage => Some(Setting::NoExitMessage(self.parse_set_bool()?)),\n      Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),\n      Keyword::Quiet => Some(Setting::Quiet(self.parse_set_bool()?)),\n      Keyword::Unstable => Some(Setting::Unstable(self.parse_set_bool()?)),\n      Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)),\n      _ => None,\n    };\n\n    if let Some(value) = set_bool {\n      return Ok(Set { name, value });\n    }\n\n    self.expect(ColonEquals)?;\n\n    let set_value = match keyword {\n      Keyword::DotenvFilename => Some(Setting::DotenvFilename(self.parse_expression()?)),\n      Keyword::DotenvPath => Some(Setting::DotenvPath(self.parse_expression()?)),\n      Keyword::ScriptInterpreter => Some(Setting::ScriptInterpreter(self.parse_interpreter()?)),\n      Keyword::Shell => Some(Setting::Shell(self.parse_interpreter()?)),\n      Keyword::Tempdir => Some(Setting::Tempdir(self.parse_expression()?)),\n      Keyword::WindowsShell => Some(Setting::WindowsShell(self.parse_interpreter()?)),\n      Keyword::WorkingDirectory => Some(Setting::WorkingDirectory(self.parse_expression()?)),\n      _ => None,\n    };\n\n    if let Some(value) = set_value {\n      return Ok(Set { name, value });\n    }\n\n    Err(name.error(CompileErrorKind::UnknownSetting {\n      setting: name.lexeme(),\n    }))\n  }\n\n  /// Parse interpreter setting value, i.e., `['sh', '-eu']`\n  fn parse_interpreter(&mut self) -> CompileResult<'src, Interpreter<Expression<'src>>> {\n    self.expect(BracketL)?;\n\n    let command = self.parse_expression()?;\n\n    let mut arguments = Vec::new();\n\n    if self.accepted(Comma)? {\n      while !self.next_is(BracketR) {\n        arguments.push(self.parse_expression()?);\n\n        if !self.accepted(Comma)? {\n          break;\n        }\n      }\n    }\n\n    self.expect(BracketR)?;\n\n    Ok(Interpreter { arguments, command })\n  }\n\n  /// Item attributes, i.e., `[macos]` or `[confirm: \"warning!\"]`\n  fn parse_attributes(&mut self) -> CompileResult<'src, Option<(Token<'src>, AttributeSet<'src>)>> {\n    let mut arg_attributes = BTreeMap::new();\n    let mut attributes = Vec::new();\n    let mut discriminants = BTreeMap::new();\n    let mut env_attributes = BTreeMap::new();\n\n    let mut token = None;\n\n    while let Some(bracket) = self.accept(BracketL)? {\n      token.get_or_insert(bracket);\n\n      loop {\n        let name = self.parse_name()?;\n\n        let mut arguments = Vec::new();\n        let mut keyword_arguments = BTreeMap::new();\n\n        if self.accepted(Colon)? {\n          arguments.push(self.parse_string_literal()?);\n        } else if self.accepted(ParenL)? {\n          loop {\n            if self.next_is(Identifier) && !self.next_is_shell_expanded_string() {\n              let key = self.parse_name()?;\n\n              let value = self\n                .accepted(Equals)?\n                .then(|| self.parse_string_literal())\n                .transpose()?;\n\n              keyword_arguments.insert(key.lexeme(), (key, value));\n            } else {\n              let literal = self.parse_string_literal()?;\n\n              if !keyword_arguments.is_empty() {\n                return Err(\n                  literal\n                    .token\n                    .error(CompileErrorKind::AttributePositionalFollowsKeyword),\n                );\n              }\n\n              arguments.push(literal);\n            }\n\n            if !self.accepted(Comma)? || self.next_is(ParenR) {\n              break;\n            }\n          }\n\n          self.expect(ParenR)?;\n        }\n\n        let attribute = Attribute::new(name, arguments, keyword_arguments)?;\n\n        let first = if attribute.repeatable() {\n          None\n        } else {\n          discriminants.get(&attribute.discriminant())\n        };\n\n        if let Some(&first) = first {\n          return Err(name.error(CompileErrorKind::DuplicateAttribute {\n            attribute: name.lexeme(),\n            first,\n          }));\n        }\n\n        if let Attribute::Arg { name: arg, .. } = &attribute {\n          if let Some(&first) = arg_attributes.get(&arg.cooked) {\n            return Err(name.error(CompileErrorKind::DuplicateArgAttribute {\n              arg: arg.cooked.clone(),\n              first,\n            }));\n          }\n\n          arg_attributes.insert(arg.cooked.clone(), name.line);\n        }\n\n        if let Attribute::Env(variable, _) = &attribute {\n          if let Some(&first) = env_attributes.get(&variable.cooked) {\n            return Err(name.error(CompileErrorKind::DuplicateEnvAttribute {\n              variable: variable.cooked.clone(),\n              first,\n            }));\n          }\n\n          env_attributes.insert(variable.cooked.clone(), name.line);\n        }\n\n        discriminants.insert(attribute.discriminant(), name.line);\n\n        attributes.push(attribute);\n\n        if !self.accepted(Comma)? {\n          break;\n        }\n      }\n      self.expect(BracketR)?;\n      self.expect_eol()?;\n    }\n\n    if attributes.is_empty() {\n      Ok(None)\n    } else {\n      Ok(Some((token.unwrap(), attributes.into_iter().collect())))\n    }\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use {super::*, CompileErrorKind::*, pretty_assertions::assert_eq};\n\n  macro_rules! test {\n    {\n      name: $name:ident,\n      text: $text:expr,\n      tree: $tree:tt,\n    } => {\n      #[test]\n      fn $name() {\n        let text: String = $text.into();\n        let want = tree!($tree);\n        test(&text, want);\n      }\n    }\n  }\n\n  fn test(text: &str, want: Tree) {\n    let unindented = unindent(text);\n    let tokens = Lexer::test_lex(&unindented).expect(\"lexing failed\");\n    let have = Parser::parse_tokens(&mut Numerator::new(), &tokens)\n      .expect(\"parsing failed\")\n      .tree();\n    if have != want {\n      println!(\"parsed text: {unindented}\");\n      println!(\"expected:    {want}\");\n      println!(\"but got:     {have}\");\n      println!(\"tokens:      {tokens:?}\");\n      panic!();\n    }\n  }\n\n  macro_rules! error {\n    (\n      name:   $name:ident,\n      input:  $input:expr,\n      offset: $offset:expr,\n      line:   $line:expr,\n      column: $column:expr,\n      width:  $width:expr,\n      kind:   $kind:expr,\n    ) => {\n      #[test]\n      fn $name() {\n        error($input, $offset, $line, $column, $width, $kind);\n      }\n    };\n  }\n\n  #[track_caller]\n  fn error(\n    src: &str,\n    offset: usize,\n    line: usize,\n    column: usize,\n    length: usize,\n    kind: CompileErrorKind,\n  ) {\n    let tokens = Lexer::test_lex(src).expect(\"Lexing failed in parse test...\");\n\n    match Parser::parse_tokens(&mut Numerator::new(), &tokens) {\n      Ok(_) => panic!(\"Parsing unexpectedly succeeded\"),\n      Err(have) => {\n        let want = CompileError {\n          token: Token {\n            kind: have.token.kind,\n            src,\n            offset,\n            line,\n            column,\n            length,\n            path: \"justfile\".as_ref(),\n          },\n          kind: kind.into(),\n        };\n        assert_eq!(have, want);\n      }\n    }\n  }\n\n  test! {\n    name: empty,\n    text: \"\",\n    tree: (justfile),\n  }\n\n  test! {\n    name: empty_multiline,\n    text: \"\n\n\n\n\n\n    \",\n    tree: (justfile),\n  }\n\n  test! {\n    name: whitespace,\n    text: \" \",\n    tree: (justfile),\n  }\n\n  test! {\n    name: alias_single,\n    text: \"alias t := test\",\n    tree: (justfile (alias t test)),\n  }\n\n  test! {\n    name: alias_with_attribute,\n    text: \"[private]\\nalias t := test\",\n    tree: (justfile (alias t test)),\n  }\n\n  test! {\n    name: alias_modulepath,\n    text: \"alias fbb := foo::bar::baz\",\n    tree: (justfile (alias fbb (foo bar baz))),\n  }\n\n  test! {\n    name: single_argument_attribute_shorthand,\n    text: \"[group: 'foo']\\nbar:\",\n    tree: (justfile (recipe bar)),\n  }\n\n  test! {\n    name: single_argument_attribute_shorthand_multiple_same_line,\n    text: \"[group: 'foo', group: 'bar']\\nbaz:\",\n    tree: (justfile (recipe baz)),\n  }\n\n  test! {\n    name: aliases_multiple,\n    text: \"alias t := test\\nalias b := build\",\n    tree: (\n      justfile\n      (alias t test)\n      (alias b build)\n    ),\n  }\n\n  test! {\n    name: alias_equals,\n    text: \"alias t := test\",\n    tree: (justfile\n      (alias t test)\n    ),\n  }\n\n  test! {\n      name: recipe_named_alias,\n      text: r\"\n      [private]\n      alias:\n        echo 'echoing alias'\n          \",\n    tree: (justfile\n      (recipe alias (body (\"echo 'echoing alias'\")))\n    ),\n  }\n\n  test! {\n    name: export,\n    text: r#\"export x := \"hello\"\"#,\n    tree: (justfile (assignment #export x \"hello\")),\n  }\n\n  test! {\n    name: private_export,\n    text: \"\n      [private]\n      export x := 'hello'\n    \",\n    tree: (justfile (assignment #export x \"hello\")),\n  }\n\n  test! {\n    name: export_equals,\n    text: r#\"export x := \"hello\"\"#,\n    tree: (justfile\n      (assignment #export x \"hello\")\n    ),\n  }\n\n  test! {\n    name: assignment,\n    text: r#\"x := \"hello\"\"#,\n    tree: (justfile (assignment x \"hello\")),\n  }\n\n  test! {\n    name: private_assignment,\n    text: \"\n      [private]\n      x := 'hello'\n      \",\n    tree: (justfile (assignment x \"hello\")),\n  }\n\n  test! {\n    name: assignment_equals,\n    text: r#\"x := \"hello\"\"#,\n    tree: (justfile\n      (assignment x \"hello\")\n    ),\n  }\n\n  test! {\n    name: backtick,\n    text: \"x := `hello`\",\n    tree: (justfile (assignment x (backtick \"hello\"))),\n  }\n\n  test! {\n    name: variable,\n    text: \"x := y\",\n    tree: (justfile (assignment x y)),\n  }\n\n  test! {\n    name: group,\n    text: \"x := (y)\",\n    tree: (justfile (assignment x (y))),\n  }\n\n  test! {\n    name: addition_single,\n    text: \"x := a + b\",\n    tree: (justfile (assignment x (+ a b))),\n  }\n\n  test! {\n    name: addition_chained,\n    text: \"x := a + b + c\",\n    tree: (justfile (assignment x (+ a (+ b c)))),\n  }\n\n  test! {\n    name: call_one_arg,\n    text: \"x := env_var(y)\",\n    tree: (justfile (assignment x (call env_var y))),\n  }\n\n  test! {\n    name: call_multiple_args,\n    text: \"x := env_var_or_default(y, z)\",\n    tree: (justfile (assignment x (call env_var_or_default y z))),\n  }\n\n  test! {\n    name: call_trailing_comma,\n    text: \"x := env_var(y,)\",\n    tree: (justfile (assignment x (call env_var y))),\n  }\n\n  test! {\n    name: recipe,\n    text: \"foo:\",\n    tree: (justfile (recipe foo)),\n  }\n\n  test! {\n    name: recipe_multiple,\n    text: \"\n      foo:\n      bar:\n      baz:\n    \",\n    tree: (justfile (recipe foo) (recipe bar) (recipe baz)),\n  }\n\n  test! {\n    name: recipe_quiet,\n    text: \"@foo:\",\n    tree: (justfile (recipe #quiet foo)),\n  }\n\n  test! {\n    name: recipe_parameter_single,\n    text: \"foo bar:\",\n    tree: (justfile (recipe foo (params (bar)))),\n  }\n\n  test! {\n    name: recipe_parameter_multiple,\n    text: \"foo bar baz:\",\n    tree: (justfile (recipe foo (params (bar) (baz)))),\n  }\n\n  test! {\n    name: recipe_default_single,\n    text: r#\"foo bar=\"baz\":\"#,\n    tree: (justfile (recipe foo (params (bar \"baz\")))),\n  }\n\n  test! {\n    name: recipe_default_multiple,\n    text: r#\"foo bar=\"baz\" bob=\"biz\":\"#,\n    tree: (justfile (recipe foo (params (bar \"baz\") (bob \"biz\")))),\n  }\n\n  test! {\n    name: recipe_plus_variadic,\n    text: r\"foo +bar:\",\n    tree: (justfile (recipe foo (params +(bar)))),\n  }\n\n  test! {\n    name: recipe_star_variadic,\n    text: r\"foo *bar:\",\n    tree: (justfile (recipe foo (params *(bar)))),\n  }\n\n  test! {\n    name: recipe_variadic_string_default,\n    text: r#\"foo +bar=\"baz\":\"#,\n    tree: (justfile (recipe foo (params +(bar \"baz\")))),\n  }\n\n  test! {\n    name: recipe_variadic_variable_default,\n    text: r\"foo +bar=baz:\",\n    tree: (justfile (recipe foo (params +(bar baz)))),\n  }\n\n  test! {\n    name: recipe_variadic_addition_group_default,\n    text: r\"foo +bar=(baz + bob):\",\n    tree: (justfile (recipe foo (params +(bar ((+ baz bob)))))),\n  }\n\n  test! {\n    name: recipe_dependency_single,\n    text: \"foo: bar\",\n    tree: (justfile (recipe foo (deps bar))),\n  }\n\n  test! {\n    name: recipe_dependency_multiple,\n    text: \"foo: bar baz\",\n    tree: (justfile (recipe foo (deps bar baz))),\n  }\n\n  test! {\n    name: recipe_dependency_parenthesis,\n    text: \"foo: (bar)\",\n    tree: (justfile (recipe foo (deps bar))),\n  }\n\n  test! {\n    name: recipe_dependency_argument_string,\n    text: \"foo: (bar 'baz')\",\n    tree: (justfile (recipe foo (deps (bar \"baz\")))),\n  }\n\n  test! {\n    name: recipe_dependency_argument_identifier,\n    text: \"foo: (bar baz)\",\n    tree: (justfile (recipe foo (deps (bar baz)))),\n  }\n\n  test! {\n    name: recipe_dependency_argument_concatenation,\n    text: \"foo: (bar 'a' + 'b' 'c' + 'd')\",\n    tree: (justfile (recipe foo (deps (bar (+ 'a' 'b') (+ 'c' 'd'))))),\n  }\n\n  test! {\n    name: recipe_subsequent,\n    text: \"foo: && bar\",\n    tree: (justfile (recipe foo (sups bar))),\n  }\n\n  test! {\n    name: recipe_line_single,\n    text: \"foo:\\n bar\",\n    tree: (justfile (recipe foo (body (\"bar\")))),\n  }\n\n  test! {\n    name: recipe_dependency_module,\n    text: \"foo: bar::baz\",\n    tree: (justfile (recipe foo (deps (bar baz)))),\n  }\n\n  test! {\n    name: recipe_dependency_parenthesis_module,\n    text: \"foo: (bar::baz)\",\n    tree: (justfile (recipe foo (deps (bar baz)))),\n  }\n\n  test! {\n    name: recipe_dependency_module_mixed,\n      text: \"foo: bar::baz qux\",\n    tree: (justfile (recipe foo (deps (bar baz) qux))),\n  }\n\n  test! {\n    name: recipe_line_multiple,\n    text: \"foo:\\n bar\\n baz\\n {{\\\"bob\\\"}}biz\",\n    tree: (justfile (recipe foo (body (\"bar\") (\"baz\") ((\"bob\") \"biz\")))),\n  }\n\n  test! {\n    name: recipe_line_interpolation,\n    text: \"foo:\\n bar{{\\\"bob\\\"}}biz\",\n    tree: (justfile (recipe foo (body (\"bar\" (\"bob\") \"biz\")))),\n  }\n\n  test! {\n    name: comment,\n    text: \"# foo\",\n    tree: (justfile (comment \"# foo\")),\n  }\n\n  test! {\n    name: comment_before_alias,\n    text: \"# foo\\nalias x := y\",\n    tree: (justfile (comment \"# foo\") (alias x y)),\n  }\n\n  test! {\n    name: comment_after_alias,\n    text: \"alias x := y # foo\",\n    tree: (justfile (alias x y)),\n  }\n\n  test! {\n    name: comment_assignment,\n    text: \"x := y # foo\",\n    tree: (justfile (assignment x y)),\n  }\n\n  test! {\n    name: comment_export,\n    text: \"export x := y # foo\",\n    tree: (justfile (assignment #export x y)),\n  }\n\n  test! {\n    name: comment_recipe,\n    text: \"foo: # bar\",\n    tree: (justfile (recipe foo)),\n  }\n\n  test! {\n    name: comment_recipe_dependencies,\n    text: \"foo: bar # baz\",\n    tree: (justfile (recipe foo (deps bar))),\n  }\n\n  test! {\n    name: doc_comment_single,\n    text: \"\n      # foo\n      bar:\n    \",\n    tree: (justfile (recipe \"foo\" bar)),\n  }\n\n  test! {\n    name: doc_comment_recipe_clear,\n    text: \"\n      # foo\n      bar:\n      baz:\n    \",\n    tree: (justfile (recipe \"foo\" bar) (recipe baz)),\n  }\n\n  test! {\n    name: doc_comment_middle,\n    text: \"\n      bar:\n      # foo\n      baz:\n    \",\n    tree: (justfile (recipe bar) (recipe \"foo\" baz)),\n  }\n\n  test! {\n    name: doc_comment_assignment_clear,\n    text: \"\n      # foo\n      x := y\n      bar:\n    \",\n    tree: (justfile (comment \"# foo\") (assignment x y) (recipe bar)),\n  }\n\n  test! {\n    name: doc_comment_empty_line_clear,\n    text: \"\n      # foo\n\n      bar:\n    \",\n    tree: (justfile (comment \"# foo\") (recipe bar)),\n  }\n\n  test! {\n    name: string_escape_tab,\n    text: r#\"x := \"foo\\tbar\"\"#,\n    tree: (justfile (assignment x \"foo\\tbar\")),\n  }\n\n  test! {\n    name: string_escape_newline,\n    text: r#\"x := \"foo\\nbar\"\"#,\n    tree: (justfile (assignment x \"foo\\nbar\")),\n  }\n\n  test! {\n    name: string_escape_suppress_newline,\n    text: r#\"\n      x := \"foo\\\n      bar\"\n    \"#,\n    tree: (justfile (assignment x \"foobar\")),\n  }\n\n  test! {\n    name: string_escape_carriage_return,\n    text: r#\"x := \"foo\\rbar\"\"#,\n    tree: (justfile (assignment x \"foo\\rbar\")),\n  }\n\n  test! {\n    name: string_escape_slash,\n    text: r#\"x := \"foo\\\\bar\"\"#,\n    tree: (justfile (assignment x \"foo\\\\bar\")),\n  }\n\n  test! {\n    name: string_escape_quote,\n    text: r#\"x := \"foo\\\"bar\"\"#,\n    tree: (justfile (assignment x \"foo\\\"bar\")),\n  }\n\n  test! {\n    name: indented_string_raw_with_dedent,\n    text: \"\n      x := '''\n        foo\\\\t\n        bar\\\\n\n      '''\n    \",\n    tree: (justfile (assignment x \"foo\\\\t\\nbar\\\\n\\n\")),\n  }\n\n  test! {\n    name: indented_string_raw_no_dedent,\n    text: \"\n      x := '''\n      foo\\\\t\n        bar\\\\n\n      '''\n    \",\n    tree: (justfile (assignment x \"foo\\\\t\\n  bar\\\\n\\n\")),\n  }\n\n  test! {\n    name: indented_string_cooked,\n    text: r#\"\n      x := \"\"\"\n        \\tfoo\\t\n        \\tbar\\n\n      \"\"\"\n    \"#,\n    tree: (justfile (assignment x \"\\tfoo\\t\\n\\tbar\\n\\n\")),\n  }\n\n  test! {\n    name: indented_string_cooked_no_dedent,\n    text: r#\"\n      x := \"\"\"\n      \\tfoo\\t\n        \\tbar\\n\n      \"\"\"\n    \"#,\n    tree: (justfile (assignment x \"\\tfoo\\t\\n  \\tbar\\n\\n\")),\n  }\n\n  test! {\n    name: indented_backtick,\n    text: r\"\n      x := ```\n        \\tfoo\\t\n        \\tbar\\n\n      ```\n    \",\n    tree: (justfile (assignment x (backtick \"\\\\tfoo\\\\t\\n\\\\tbar\\\\n\\n\"))),\n  }\n\n  test! {\n    name: indented_backtick_no_dedent,\n    text: r\"\n      x := ```\n      \\tfoo\\t\n        \\tbar\\n\n      ```\n    \",\n    tree: (justfile (assignment x (backtick \"\\\\tfoo\\\\t\\n  \\\\tbar\\\\n\\n\"))),\n  }\n\n  test! {\n    name: recipe_variadic_with_default_after_default,\n    text: r\"\n      f a=b +c=d:\n    \",\n    tree: (justfile (recipe f (params (a b) +(c d)))),\n  }\n\n  test! {\n    name: parameter_default_concatenation_variable,\n    text: r#\"\n      x := \"10\"\n\n      f y=(`echo hello` + x) +z=\"foo\":\n    \"#,\n    tree: (justfile\n      (assignment x \"10\")\n      (recipe f (params (y ((+ (backtick \"echo hello\") x))) +(z \"foo\")))\n    ),\n  }\n\n  test! {\n    name: parameter_default_multiple,\n    text: r#\"\n      x := \"10\"\n      f y=(`echo hello` + x) +z=(\"foo\" + \"bar\"):\n    \"#,\n    tree: (justfile\n      (assignment x \"10\")\n      (recipe f (params (y ((+ (backtick \"echo hello\") x))) +(z ((+ \"foo\" \"bar\")))))\n    ),\n  }\n\n  test! {\n    name: parse_raw_string_default,\n    text: r\"\n\n      foo a='b\\t':\n\n\n    \",\n    tree: (justfile (recipe foo (params (a \"b\\\\t\")))),\n  }\n\n  test! {\n    name: parse_alias_after_target,\n    text: r\"\n      foo:\n        echo a\n      alias f := foo\n    \",\n    tree: (justfile\n      (recipe foo (body (\"echo a\")))\n      (alias f foo)\n    ),\n  }\n\n  test! {\n    name: parse_alias_before_target,\n    text: \"\n      alias f := foo\n      foo:\n        echo a\n      \",\n    tree: (justfile\n      (alias f foo)\n      (recipe foo (body (\"echo a\")))\n    ),\n  }\n\n  test! {\n    name: parse_alias_with_comment,\n    text: \"\n      alias f := foo #comment\n      foo:\n        echo a\n    \",\n    tree: (justfile\n      (alias f foo)\n      (recipe foo (body (\"echo a\")))\n    ),\n  }\n\n  test! {\n    name: parse_assignment_with_comment,\n    text: \"\n      f := foo #comment\n      foo:\n        echo a\n    \",\n    tree: (justfile\n      (assignment f foo)\n      (recipe foo (body (\"echo a\")))\n    ),\n  }\n\n  test! {\n    name: parse_complex,\n    text: \"\n      x:\n      y:\n      z:\n      foo := \\\"xx\\\"\n      bar := foo\n      goodbye := \\\"y\\\"\n      hello a b    c   : x y    z #hello\n        #! blah\n        #blarg\n        {{ foo + bar}}abc{{ goodbye\\t  + \\\"x\\\" }}xyz\n        1\n        2\n        3\n    \",\n    tree: (justfile\n      (recipe x)\n      (recipe y)\n      (recipe z)\n      (assignment foo \"xx\")\n      (assignment bar foo)\n      (assignment goodbye \"y\")\n      (recipe hello\n        (params (a) (b) (c))\n        (deps x y z)\n        (body\n          (\"#! blah\")\n          (\"#blarg\")\n          (((+ foo bar)) \"abc\" ((+ goodbye \"x\")) \"xyz\")\n          (\"1\")\n          (\"2\")\n          (\"3\")\n        )\n      )\n    ),\n  }\n\n  test! {\n    name: parse_shebang,\n    text: \"\n      practicum := 'hello'\n      install:\n      \\t#!/bin/sh\n      \\tif [[ -f {{practicum}} ]]; then\n      \\t\\treturn\n      \\tfi\n      \",\n    tree: (justfile\n      (assignment practicum \"hello\")\n      (recipe install\n        (body\n         (\"#!/bin/sh\")\n         (\"if [[ -f \" (practicum) \" ]]; then\")\n         (\"\\treturn\")\n         (\"fi\")\n        )\n      )\n    ),\n  }\n\n  test! {\n    name: parse_simple_shebang,\n    text: \"a:\\n #!\\n  print(1)\",\n    tree: (justfile\n      (recipe a (body (\"#!\") (\" print(1)\")))\n    ),\n  }\n\n  test! {\n    name: parse_assignments,\n    text: r#\"\n      a := \"0\"\n      c := a + b + a + b\n      b := \"1\"\n    \"#,\n    tree: (justfile\n      (assignment a \"0\")\n      (assignment c (+ a (+ b (+ a b))))\n      (assignment b \"1\")\n    ),\n  }\n\n  test! {\n    name: parse_assignment_backticks,\n    text: \"\n      a := `echo hello`\n      c := a + b + a + b\n      b := `echo goodbye`\n    \",\n    tree: (justfile\n      (assignment a (backtick \"echo hello\"))\n      (assignment c (+ a (+ b (+ a b))))\n      (assignment b (backtick \"echo goodbye\"))\n    ),\n  }\n\n  test! {\n    name: parse_interpolation_backticks,\n    text: r#\"\n      a:\n        echo {{  `echo hello` + \"blarg\"   }} {{   `echo bob`   }}\n    \"#,\n    tree: (justfile\n      (recipe a\n        (body (\"echo \" ((+ (backtick \"echo hello\") \"blarg\")) \" \" ((backtick \"echo bob\"))))\n      )\n    ),\n  }\n\n  test! {\n    name: eof_test,\n    text: \"x:\\ny:\\nz:\\na b c: x y z\",\n    tree: (justfile\n      (recipe x)\n      (recipe y)\n      (recipe z)\n      (recipe a (params (b) (c)) (deps x y z))\n    ),\n  }\n\n  test! {\n    name: string_quote_escape,\n    text: r#\"a := \"hello\\\"\"\"#,\n    tree: (justfile\n      (assignment a \"hello\\\"\")\n    ),\n  }\n\n  test! {\n    name: string_escapes,\n    text: r#\"a := \"\\n\\t\\r\\\"\\\\\"\"#,\n    tree: (justfile (assignment a \"\\n\\t\\r\\\"\\\\\")),\n  }\n\n  test! {\n    name: parameters,\n    text: \"\n      a b c:\n        {{b}} {{c}}\n    \",\n    tree: (justfile (recipe a (params (b) (c)) (body ((b) \" \" (c))))),\n  }\n\n  test! {\n    name: unary_functions,\n    text: \"\n      x := arch()\n\n      a:\n        {{os()}} {{os_family()}}\n    \",\n    tree: (justfile\n      (assignment x (call arch))\n      (recipe a (body (((call os)) \" \" ((call os_family)))))\n    ),\n  }\n\n  test! {\n    name: env_functions,\n    text: r#\"\n      x := env_var('foo',)\n\n      a:\n        {{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var(\"baz\"))}}\n    \"#,\n    tree: (justfile\n      (assignment x (call env_var \"foo\"))\n      (recipe a\n        (body\n          (\n            ((call env_var_or_default (+ \"foo\" \"bar\") \"baz\"))\n            \" \"\n            ((call env_var (call env_var \"baz\")))\n          )\n        )\n      )\n    ),\n  }\n\n  test! {\n    name: parameter_default_string,\n    text: r#\"\n      f x=\"abc\":\n    \"#,\n    tree: (justfile (recipe f (params (x \"abc\")))),\n  }\n\n  test! {\n    name: parameter_default_raw_string,\n    text: r\"\n      f x='abc':\n    \",\n    tree: (justfile (recipe f (params (x \"abc\")))),\n  }\n\n  test! {\n    name: parameter_default_backtick,\n    text: \"\n      f x=`echo hello`:\n    \",\n    tree: (justfile\n      (recipe f (params (x (backtick \"echo hello\"))))\n    ),\n  }\n\n  test! {\n    name: parameter_default_concatenation_string,\n    text: r#\"\n      f x=(`echo hello` + \"foo\"):\n    \"#,\n    tree: (justfile (recipe f (params (x ((+ (backtick \"echo hello\") \"foo\")))))),\n  }\n\n  test! {\n    name: concatenation_in_group,\n    text: \"x := ('0' + '1')\",\n    tree: (justfile (assignment x ((+ \"0\" \"1\")))),\n  }\n\n  test! {\n    name: string_in_group,\n    text: \"x := ('0'   )\",\n    tree: (justfile (assignment x (\"0\"))),\n  }\n\n  test! {\n    name: escaped_dos_newlines,\n    text: \"\n      @spam:\\r\n      \\t{ \\\\\\r\n      \\t\\tfiglet test; \\\\\\r\n      \\t\\tcargo build --color always 2>&1; \\\\\\r\n      \\t\\tcargo test  --color always -- --color always 2>&1; \\\\\\r\n      \\t} | less\\r\n    \",\n    tree: (justfile\n      (recipe #quiet spam\n        (body\n         (\"{ \\\\\")\n         (\"\\tfiglet test; \\\\\")\n         (\"\\tcargo build --color always 2>&1; \\\\\")\n         (\"\\tcargo test  --color always -- --color always 2>&1; \\\\\")\n         (\"} | less\")\n        )\n      )\n    ),\n  }\n\n  test! {\n    name: empty_body,\n    text: \"a:\",\n    tree: (justfile (recipe a)),\n  }\n\n  test! {\n    name: single_line_body,\n    text: \"a:\\n foo\",\n    tree: (justfile (recipe a (body (\"foo\")))),\n  }\n\n  test! {\n    name: trimmed_body,\n    text: \"a:\\n foo\\n \\n \\n \\nb:\\n  \",\n    tree: (justfile (recipe a (body (\"foo\"))) (recipe b)),\n  }\n\n  test! {\n    name: set_export_implicit,\n    text: \"set export\",\n    tree: (justfile (set export true)),\n  }\n\n  test! {\n    name: set_export_true,\n    text: \"set export := true\",\n    tree: (justfile (set export true)),\n  }\n\n  test! {\n    name: set_export_false,\n    text: \"set export := false\",\n    tree: (justfile (set export false)),\n  }\n\n  test! {\n    name: set_dotenv_load_implicit,\n    text: \"set dotenv-load\",\n    tree: (justfile (set dotenv_load true)),\n  }\n\n  test! {\n    name: set_allow_duplicate_recipes_implicit,\n    text: \"set allow-duplicate-recipes\",\n    tree: (justfile (set allow_duplicate_recipes true)),\n  }\n\n  test! {\n    name: set_allow_duplicate_variables_implicit,\n    text: \"set allow-duplicate-variables\",\n    tree: (justfile (set allow_duplicate_variables true)),\n  }\n\n  test! {\n    name: set_dotenv_load_true,\n    text: \"set dotenv-load := true\",\n    tree: (justfile (set dotenv_load true)),\n  }\n\n  test! {\n    name: set_dotenv_load_false,\n    text: \"set dotenv-load := false\",\n    tree: (justfile (set dotenv_load false)),\n  }\n\n  test! {\n    name: set_positional_arguments_implicit,\n    text: \"set positional-arguments\",\n    tree: (justfile (set positional_arguments true)),\n  }\n\n  test! {\n    name: set_positional_arguments_true,\n    text: \"set positional-arguments := true\",\n    tree: (justfile (set positional_arguments true)),\n  }\n\n  test! {\n    name: set_quiet_implicit,\n    text: \"set quiet\",\n    tree: (justfile (set quiet true)),\n  }\n\n  test! {\n    name: set_quiet_true,\n    text: \"set quiet := true\",\n    tree: (justfile (set quiet true)),\n  }\n\n  test! {\n    name: set_quiet_false,\n    text: \"set quiet := false\",\n    tree: (justfile (set quiet false)),\n  }\n\n  test! {\n    name: set_positional_arguments_false,\n    text: \"set positional-arguments := false\",\n    tree: (justfile (set positional_arguments false)),\n  }\n\n  test! {\n    name: set_shell_no_arguments,\n    text: \"set shell := ['tclsh']\",\n    tree: (justfile (set shell \"tclsh\")),\n  }\n\n  test! {\n    name: set_shell_no_arguments_cooked,\n    text: \"set shell := [\\\"tclsh\\\"]\",\n    tree: (justfile (set shell \"tclsh\")),\n  }\n\n  test! {\n    name: set_shell_no_arguments_trailing_comma,\n    text: \"set shell := ['tclsh',]\",\n    tree: (justfile (set shell \"tclsh\")),\n  }\n\n  test! {\n    name: set_shell_with_one_argument,\n    text: \"set shell := ['bash', '-cu']\",\n    tree: (justfile (set shell \"bash\" \"-cu\")),\n  }\n\n  test! {\n    name: set_shell_with_one_argument_trailing_comma,\n    text: \"set shell := ['bash', '-cu',]\",\n    tree: (justfile (set shell \"bash\" \"-cu\")),\n  }\n\n  test! {\n    name: set_shell_with_two_arguments,\n    text: \"set shell := ['bash', '-cu', '-l']\",\n    tree: (justfile (set shell \"bash\" \"-cu\" \"-l\")),\n  }\n\n  test! {\n    name: set_windows_powershell_implicit,\n    text: \"set windows-powershell\",\n    tree: (justfile (set windows_powershell true)),\n  }\n\n  test! {\n    name: set_windows_powershell_true,\n    text: \"set windows-powershell := true\",\n    tree: (justfile (set windows_powershell true)),\n  }\n\n  test! {\n    name: set_windows_powershell_false,\n    text: \"set windows-powershell := false\",\n    tree: (justfile (set windows_powershell false)),\n  }\n\n  test! {\n    name: set_working_directory,\n    text: \"set working-directory := 'foo'\",\n    tree: (justfile (set working_directory \"foo\")),\n  }\n\n  test! {\n    name: conditional,\n    text: \"a := if b == c { d } else { e }\",\n    tree: (justfile (assignment a (if b == c d e))),\n  }\n\n  test! {\n    name: conditional_inverted,\n    text: \"a := if b != c { d } else { e }\",\n    tree: (justfile (assignment a (if b != c d e))),\n  }\n\n  test! {\n    name: conditional_concatenations,\n    text: \"a := if b0 + b1 == c0 + c1 { d0 + d1 } else { e0 + e1 }\",\n    tree: (justfile (assignment a (if (+ b0 b1) == (+ c0 c1) (+ d0 d1) (+ e0 e1)))),\n  }\n\n  test! {\n    name: conditional_nested_lhs,\n    text: \"a := if if b == c { d } else { e } == c { d } else { e }\",\n    tree: (justfile (assignment a (if (if b == c d e) == c d e))),\n  }\n\n  test! {\n    name: conditional_nested_rhs,\n    text: \"a := if c == if b == c { d } else { e } { d } else { e }\",\n    tree: (justfile (assignment a (if c == (if b == c d e) d e))),\n  }\n\n  test! {\n    name: conditional_nested_then,\n    text: \"a := if b == c { if b == c { d } else { e } } else { e }\",\n    tree: (justfile (assignment a (if b == c (if b == c d e) e))),\n  }\n\n  test! {\n    name: conditional_nested_otherwise,\n    text: \"a := if b == c { d } else { if b == c { d } else { e } }\",\n    tree: (justfile (assignment a (if b == c d (if b == c d e)))),\n  }\n\n  test! {\n    name: import,\n    text: \"import \\\"some/file/path.txt\\\"     \\n\",\n    tree: (justfile (import \"some/file/path.txt\")),\n  }\n\n  test! {\n    name: optional_import,\n    text: \"import? \\\"some/file/path.txt\\\"     \\n\",\n    tree: (justfile (import ? \"some/file/path.txt\")),\n  }\n\n  test! {\n    name: module_with,\n    text: \"mod foo\",\n    tree: (justfile (mod foo )),\n  }\n\n  test! {\n    name: optional_module,\n    text: \"mod? foo\",\n    tree: (justfile (mod ? foo)),\n  }\n\n  test! {\n    name: module_with_path,\n    text: \"mod foo \\\"some/file/path.txt\\\"     \\n\",\n    tree: (justfile (mod foo \"some/file/path.txt\")),\n  }\n\n  test! {\n    name: optional_module_with_path,\n    text: \"mod? foo \\\"some/file/path.txt\\\"     \\n\",\n    tree: (justfile (mod ? foo \"some/file/path.txt\")),\n  }\n\n  test! {\n    name: assert,\n    text: \"a := assert(foo == \\\"bar\\\", \\\"error\\\")\",\n    tree: (justfile (assignment a (assert foo == \"bar\" \"error\"))),\n  }\n\n  test! {\n    name: assert_conditional_condition,\n    text: \"foo := assert(if a != b { c } else { d } == \\\"abc\\\", \\\"error\\\")\",\n    tree: (justfile (assignment foo (assert (if a != b c d) == \"abc\" \"error\"))),\n  }\n\n  test! {\n    name: format_string_simple,\n    text: \"foo := f'abc'\",\n    tree: (justfile (assignment foo (format \"abc\"))),\n  }\n\n  test! {\n    name: format_string_expression,\n    text: \"foo := f'foo{{ 'abc' + 'xyz' }}bar'\",\n    tree: (justfile (assignment foo (format \"foo\" (+ \"abc\" \"xyz\") \"bar\"))),\n  }\n\n  test! {\n    name: format_string_complex,\n    text: \"foo := f'foo{{ 'abc' + 'xyz' }}bar{{ 'hello' }}goodbye'\",\n    tree: (justfile (assignment foo (format \"foo\" (+ \"abc\" \"xyz\") \"bar\" \"hello\" \"goodbye\"))),\n  }\n\n  error! {\n    name:   alias_syntax_multiple_rhs,\n    input:  \"alias foo := bar baz\",\n    offset: 17,\n    line:   0,\n    column: 17,\n    width:  3,\n    kind:   UnexpectedToken { expected: vec![ColonColon, Comment, Eof, Eol], found: Identifier },\n  }\n\n  error! {\n    name:   alias_syntax_no_rhs,\n    input:  \"alias foo := \\n\",\n    offset: 13,\n    line:   0,\n    column: 13,\n    width:  1,\n    kind:   UnexpectedToken {expected: vec![Identifier], found:Eol},\n  }\n\n  error! {\n    name:   alias_syntax_colon_end,\n    input:  \"alias foo := bar::\\n\",\n    offset: 18,\n    line:   0,\n    column: 18,\n    width:  1,\n    kind:   UnexpectedToken {expected: vec![Identifier], found:Eol},\n  }\n\n  error! {\n    name:   alias_syntax_single_colon,\n    input:  \"alias foo := bar:baz\",\n    offset: 16,\n    line:   0,\n    column: 16,\n    width:  1,\n    kind:   UnexpectedToken {expected: vec![ColonColon, Comment, Eof, Eol], found:Colon},\n  }\n\n  error! {\n    name:   missing_colon,\n    input:  \"a b c\\nd e f\",\n    offset:  5,\n    line:   0,\n    column: 5,\n    width:  1,\n    kind:   UnexpectedToken{\n      expected: vec![Asterisk, Colon, Dollar, Equals, Identifier, Plus],\n      found:    Eol\n    },\n  }\n\n  error! {\n    name:   missing_default_eol,\n    input:  \"hello arg=\\n\",\n    offset:  10,\n    line:   0,\n    column: 10,\n    width:  1,\n    kind:   UnexpectedToken {\n      expected: vec![\n        Backtick,\n        Identifier,\n        ParenL,\n        StringToken,\n      ],\n      found: Eol\n    },\n  }\n\n  error! {\n    name:   missing_default_eof,\n    input:  \"hello arg=\",\n    offset:  10,\n    line:   0,\n    column: 10,\n    width:  0,\n    kind:   UnexpectedToken {\n      expected: vec![\n        Backtick,\n        Identifier,\n        ParenL,\n        StringToken,\n      ],\n      found: Eof,\n    },\n  }\n\n  error! {\n    name:   missing_eol,\n    input:  \"a b c: z =\",\n    offset:  9,\n    line:    0,\n    column:  9,\n    width:   1,\n    kind:    UnexpectedToken{\n      expected: vec![AmpersandAmpersand, ColonColon, Comment, Eof, Eol, Identifier, ParenL],\n      found: Equals\n    },\n  }\n\n  error! {\n    name:   unexpected_brace,\n    input:  \"{{\",\n    offset:  0,\n    line:   0,\n    column: 0,\n    width:  1,\n    kind: UnexpectedToken {\n      expected: vec![At, BracketL, Comment, Eof, Eol, Identifier],\n      found: BraceL,\n    },\n  }\n\n  error! {\n    name:   unclosed_parenthesis_in_expression,\n    input:  \"x := foo(\",\n    offset: 9,\n    line:   0,\n    column: 9,\n    width:  0,\n    kind: UnexpectedToken{\n      expected: vec![\n        Backtick,\n        Identifier,\n        ParenL,\n        ParenR,\n        Slash,\n        StringToken,\n      ],\n      found: Eof,\n    },\n  }\n\n  error! {\n    name:   plus_following_parameter,\n    input:  \"a b c+:\",\n    offset: 6,\n    line:   0,\n    column: 6,\n    width:  1,\n    kind:   UnexpectedToken{expected: vec![Dollar, Identifier], found: Colon},\n  }\n\n  error! {\n    name:   invalid_escape_sequence,\n    input:  r#\"foo := \"\\b\"\"#,\n    offset: 7,\n    line:   0,\n    column: 7,\n    width:  4,\n    kind:   InvalidEscapeSequence{character: 'b'},\n  }\n\n  error! {\n    name:   bad_export,\n    input:  \"export a\",\n    offset:  8,\n    line:   0,\n    column: 8,\n    width:  0,\n    kind:   UnexpectedToken {\n      expected: vec![Asterisk, Colon, Dollar, Equals, Identifier, Plus],\n      found:    Eof\n    },\n  }\n\n  error! {\n    name:   parameter_follows_variadic_parameter,\n    input:  \"foo +a b:\",\n    offset: 7,\n    line:   0,\n    column: 7,\n    width:  1,\n    kind:   ParameterFollowsVariadicParameter{parameter: \"b\"},\n  }\n\n  error! {\n    name:   parameter_after_variadic,\n    input:  \"foo +a bbb:\",\n    offset: 7,\n    line:   0,\n    column: 7,\n    width:  3,\n    kind:   ParameterFollowsVariadicParameter{parameter: \"bbb\"},\n  }\n\n  error! {\n    name:   concatenation_in_default,\n    input:  \"foo a=c+d e:\",\n    offset: 10,\n    line:   0,\n    column: 10,\n    width:  1,\n    kind:   ParameterFollowsVariadicParameter{parameter: \"e\"},\n  }\n\n  error! {\n    name:   set_shell_empty,\n    input:  \"set shell := []\",\n    offset: 14,\n    line:   0,\n    column: 14,\n    width:  1,\n    kind:   UnexpectedToken {\n      expected: vec![\n        Backtick,\n        Identifier,\n        ParenL,\n        Slash,\n        StringToken,\n      ],\n      found: BracketR,\n    },\n  }\n\n  error! {\n    name:   set_shell_bad_comma,\n    input:  \"set shell := ['bash',\",\n    offset: 21,\n    line:   0,\n    column: 21,\n    width:  0,\n    kind:   UnexpectedToken {\n      expected: vec![\n        Backtick,\n        BracketR,\n        Identifier,\n        ParenL,\n        Slash,\n        StringToken,\n      ],\n      found: Eof,\n    },\n  }\n\n  error! {\n    name:   set_shell_bad,\n    input:  \"set shell := ['bash'\",\n    offset: 20,\n    line:   0,\n    column: 20,\n    width:  0,\n    kind:   UnexpectedToken {\n      expected: vec![AmpersandAmpersand, BarBar, BracketR, Comma, Plus, Slash],\n      found: Eof,\n    },\n  }\n\n  error! {\n    name:   empty_attribute,\n    input:  \"[]\\nsome_recipe:\\n @exit 3\",\n    offset: 1,\n    line:   0,\n    column: 1,\n    width:  1,\n    kind:   UnexpectedToken {\n      expected: vec![Identifier],\n      found: BracketR,\n    },\n  }\n\n  error! {\n    name:   unknown_attribute,\n    input:  \"[unknown]\\nsome_recipe:\\n @exit 3\",\n    offset: 1,\n    line:   0,\n    column: 1,\n    width:  7,\n    kind:   UnknownAttribute { attribute: \"unknown\" },\n  }\n\n  error! {\n    name:   set_unknown,\n    input:  \"set shall := []\",\n    offset: 4,\n    line:   0,\n    column: 4,\n    width:  5,\n    kind:   UnknownSetting {\n      setting: \"shall\",\n    },\n  }\n\n  error! {\n    name:   set_shell_non_string,\n    input:  \"set shall := []\",\n    offset: 4,\n    line:   0,\n    column: 4,\n    width:  5,\n    kind:   UnknownSetting {\n      setting: \"shall\",\n    },\n  }\n\n  error! {\n    name:   unknown_function,\n    input:  \"a := foo()\",\n    offset: 5,\n    line:   0,\n    column: 5,\n    width:  3,\n    kind:   UnknownFunction{function: \"foo\"},\n  }\n\n  error! {\n    name:   unknown_function_in_interpolation,\n    input:  \"a:\\n echo {{bar()}}\",\n    offset: 11,\n    line:   1,\n    column: 8,\n    width:  3,\n    kind:   UnknownFunction{function: \"bar\"},\n  }\n\n  error! {\n    name:   unknown_function_in_default,\n    input:  \"a f=baz():\",\n    offset: 4,\n    line:   0,\n    column: 4,\n    width:  3,\n    kind:   UnknownFunction{function: \"baz\"},\n  }\n\n  error! {\n    name: function_argument_count_nullary,\n    input: \"x := arch('foo')\",\n    offset: 5,\n    line: 0,\n    column: 5,\n    width: 4,\n    kind: FunctionArgumentCountMismatch {\n      function: \"arch\",\n      found: 1,\n      expected: 0..=0,\n    },\n  }\n\n  error! {\n    name: function_argument_count_unary,\n    input: \"x := env_var()\",\n    offset: 5,\n    line: 0,\n    column: 5,\n    width: 7,\n    kind: FunctionArgumentCountMismatch {\n      function: \"env_var\",\n      found: 0,\n      expected: 1..=1,\n    },\n  }\n\n  error! {\n    name: function_argument_count_too_high_unary_opt,\n    input: \"x := env('foo', 'foo', 'foo')\",\n    offset: 5,\n    line: 0,\n    column: 5,\n    width: 3,\n    kind: FunctionArgumentCountMismatch {\n      function: \"env\",\n      found: 3,\n      expected: 1..=2,\n    },\n  }\n\n  error! {\n    name: function_argument_count_too_low_unary_opt,\n    input: \"x := env()\",\n    offset: 5,\n    line: 0,\n    column: 5,\n    width: 3,\n    kind: FunctionArgumentCountMismatch {\n      function: \"env\",\n      found: 0,\n      expected: 1..=2,\n    },\n  }\n\n  error! {\n    name: function_argument_count_binary,\n    input: \"x := env_var_or_default('foo')\",\n    offset: 5,\n    line: 0,\n    column: 5,\n    width: 18,\n    kind: FunctionArgumentCountMismatch {\n      function: \"env_var_or_default\",\n      found: 1,\n      expected: 2..=2,\n    },\n  }\n\n  error! {\n    name: function_argument_count_binary_plus,\n    input: \"x := join('foo')\",\n    offset: 5,\n    line: 0,\n    column: 5,\n    width: 4,\n    kind: FunctionArgumentCountMismatch {\n      function: \"join\",\n      found: 1,\n      expected: 2..=usize::MAX,\n    },\n  }\n\n  error! {\n    name: function_argument_count_ternary,\n    input: \"x := replace('foo')\",\n    offset: 5,\n    line: 0,\n    column: 5,\n    width: 7,\n    kind: FunctionArgumentCountMismatch {\n      function: \"replace\",\n      found: 1,\n      expected: 3..=3,\n    },\n  }\n}\n"
  },
  {
    "path": "src/pattern.rs",
    "content": "use super::*;\n\n#[derive(Debug, Clone)]\npub(crate) struct Pattern<'src> {\n  pub(crate) regex: Regex,\n  pub(crate) token: Token<'src>,\n}\n\nimpl<'src> Pattern<'src> {\n  pub(crate) fn is_match(&self, haystack: &str) -> bool {\n    self.regex.is_match(haystack)\n  }\n\n  pub(crate) fn new(literal: &StringLiteral<'src>) -> Result<Self, CompileError<'src>> {\n    literal.cooked.parse::<Regex>().map_err(|source| {\n      literal\n        .token\n        .error(CompileErrorKind::ArgumentPatternRegex { source })\n    })?;\n\n    Ok(Self {\n      regex: format!(\"^(?:{})$\", literal.cooked)\n        .parse::<Regex>()\n        .map_err(|source| {\n          literal\n            .token\n            .error(CompileErrorKind::ArgumentPatternRegex { source })\n        })?,\n      token: literal.token,\n    })\n  }\n\n  pub(crate) fn original(&self) -> &str {\n    &self.regex.as_str()[4..self.regex.as_str().len() - 2]\n  }\n}\n\nimpl Eq for Pattern<'_> {}\n\nimpl Ord for Pattern<'_> {\n  fn cmp(&self, other: &pattern::Pattern) -> Ordering {\n    self.regex.as_str().cmp(other.regex.as_str())\n  }\n}\n\nimpl PartialEq for Pattern<'_> {\n  fn eq(&self, other: &pattern::Pattern) -> bool {\n    self.regex.as_str() == other.regex.as_str()\n  }\n}\n\nimpl PartialOrd for Pattern<'_> {\n  fn partial_cmp(&self, other: &pattern::Pattern) -> Option<Ordering> {\n    Some(self.cmp(other))\n  }\n}\n\nimpl Serialize for Pattern<'_> {\n  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n  where\n    S: Serializer,\n  {\n    serializer.serialize_str(self.original())\n  }\n}\n"
  },
  {
    "path": "src/platform/unix.rs",
    "content": "use super::*;\n\nimpl PlatformInterface for Platform {\n  fn make_shebang_command(\n    _config: &Config,\n    path: &Path,\n    _shebang: Shebang,\n    working_directory: Option<&Path>,\n  ) -> Result<Command, OutputError> {\n    // shebang scripts can be executed directly on unix\n    let mut command = Command::new(path);\n\n    if let Some(working_directory) = working_directory {\n      command.current_dir(working_directory);\n    }\n\n    Ok(command)\n  }\n\n  fn set_execute_permission(path: &Path) -> io::Result<()> {\n    use std::os::unix::fs::PermissionsExt;\n\n    // get current permissions\n    let mut permissions = fs::metadata(path)?.permissions();\n\n    // set the execute bit\n    let current_mode = permissions.mode();\n    permissions.set_mode(current_mode | 0o100);\n\n    // set the new permissions\n    fs::set_permissions(path, permissions)\n  }\n\n  fn signal_from_exit_status(exit_status: ExitStatus) -> Option<i32> {\n    use std::os::unix::process::ExitStatusExt;\n    exit_status.signal()\n  }\n\n  fn convert_native_path(\n    _config: &Config,\n    _working_directory: &Path,\n    path: &Path,\n  ) -> FunctionResult {\n    path\n      .to_str()\n      .map(str::to_string)\n      .ok_or_else(|| String::from(\"Error getting current directory: unicode decode error\"))\n  }\n\n  fn install_signal_handler<T: Fn(Signal) + Send + 'static>(handler: T) -> RunResult<'static> {\n    let signals = crate::signals::Signals::new()?;\n\n    std::thread::Builder::new()\n      .name(\"signal handler\".into())\n      .spawn(move || {\n        for signal in signals {\n          match signal {\n            Ok(signal) => handler(signal),\n            Err(io_error) => eprintln!(\"warning: I/O error reading from signal pipe: {io_error}\"),\n          }\n        }\n      })\n      .map_err(|io_error| Error::SignalHandlerSpawnThread { io_error })?;\n\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "src/platform/windows.rs",
    "content": "use super::*;\n\nimpl PlatformInterface for Platform {\n  fn make_shebang_command(\n    config: &Config,\n    path: &Path,\n    shebang: Shebang,\n    working_directory: Option<&Path>,\n  ) -> Result<Command, OutputError> {\n    use std::borrow::Cow;\n\n    // If the path contains forward slashes…\n    let command = if shebang.interpreter.contains('/') {\n      // …translate path to the interpreter from unix style to windows style.\n      let mut cygpath = Command::new(&config.cygpath);\n\n      if let Some(working_directory) = working_directory {\n        cygpath.current_dir(working_directory);\n      }\n\n      cygpath\n        .arg(\"--windows\")\n        .arg(shebang.interpreter)\n        .stdin(Stdio::null())\n        .stdout(Stdio::piped())\n        .stderr(Stdio::piped());\n\n      Cow::Owned(cygpath.output_guard_stdout()?)\n    } else {\n      // …otherwise use it as-is.\n      Cow::Borrowed(shebang.interpreter)\n    };\n\n    let mut cmd = Command::new(command.as_ref());\n\n    if let Some(working_directory) = working_directory {\n      cmd.current_dir(working_directory);\n    }\n\n    if let Some(argument) = shebang.argument {\n      cmd.arg(argument);\n    }\n\n    cmd.arg(path);\n    Ok(cmd)\n  }\n\n  fn set_execute_permission(_path: &Path) -> io::Result<()> {\n    // it is not necessary to set an execute permission on a script on windows, so\n    // this is a nop\n    Ok(())\n  }\n\n  fn signal_from_exit_status(_exit_status: process::ExitStatus) -> Option<i32> {\n    // The rust standard library does not expose a way to extract a signal from a\n    // windows process exit status, so just return None\n    None\n  }\n\n  fn convert_native_path(config: &Config, working_directory: &Path, path: &Path) -> FunctionResult {\n    // Translate path from windows style to unix style\n    let mut cygpath = Command::new(&config.cygpath);\n\n    cygpath\n      .current_dir(working_directory)\n      .arg(\"--unix\")\n      .arg(path)\n      .stdin(Stdio::null())\n      .stdout(Stdio::piped())\n      .stderr(Stdio::piped());\n\n    match cygpath.output_guard_stdout() {\n      Ok(shell_path) => Ok(shell_path),\n      Err(_) => path\n        .to_str()\n        .map(str::to_string)\n        .ok_or_else(|| String::from(\"Error getting current directory: unicode decode error\")),\n    }\n  }\n\n  fn install_signal_handler<T: Fn(Signal) + Send + 'static>(handler: T) -> RunResult<'static> {\n    ctrlc::set_handler(move || handler(Signal::Interrupt))\n      .map_err(|source| Error::SignalHandlerInstall { source })\n  }\n}\n"
  },
  {
    "path": "src/platform.rs",
    "content": "use super::*;\n\npub(crate) struct Platform;\n\n#[cfg(unix)]\nmod unix;\n\n#[cfg(windows)]\nmod windows;\n"
  },
  {
    "path": "src/platform_interface.rs",
    "content": "use super::*;\n\npub(crate) trait PlatformInterface {\n  /// translate path from \"native\" path to path interpreter expects\n  fn convert_native_path(config: &Config, working_directory: &Path, path: &Path) -> FunctionResult;\n\n  /// install handler, may only be called once\n  fn install_signal_handler<T: Fn(Signal) + Send + 'static>(handler: T) -> RunResult<'static>;\n\n  /// construct command equivalent to running script at `path` with shebang\n  /// line `shebang`\n  fn make_shebang_command(\n    config: &Config,\n    path: &Path,\n    shebang: Shebang,\n    working_directory: Option<&Path>,\n  ) -> Result<Command, OutputError>;\n\n  /// set the execute permission on file pointed to by `path`\n  fn set_execute_permission(path: &Path) -> io::Result<()>;\n\n  /// extract signal from process exit status\n  fn signal_from_exit_status(exit_status: ExitStatus) -> Option<i32>;\n}\n"
  },
  {
    "path": "src/position.rs",
    "content": "/// Source position\n#[derive(Copy, Clone, PartialEq, Debug)]\npub(crate) struct Position {\n  pub(crate) column: usize,\n  pub(crate) line: usize,\n  pub(crate) offset: usize,\n}\n"
  },
  {
    "path": "src/positional.rs",
    "content": "use super::*;\n\n/// A struct containing the parsed representation of positional command-line\n/// arguments, i.e. arguments that are not flags, options, or the subcommand.\n///\n/// The DSL of positional arguments is fairly complex and mostly accidental.\n/// There are three possible components: overrides, a search directory, and the\n/// rest:\n///\n/// - Overrides are of the form `NAME=.*`\n///\n/// - After overrides comes a single optional search directory argument. This is\n///   either '.', '..', or an argument that contains a `/`.\n///\n///   If the argument contains a `/`, everything before and including the slash\n///   is the search directory, and everything after is added to the rest.\n///\n/// - Everything else is an argument.\n///\n/// Overrides set the values of top-level variables in the justfile being\n/// invoked and are a convenient way to override settings.\n///\n/// For modes that do not take other arguments, the search directory argument\n/// determines where to begin searching for the justfile.  This allows command\n/// lines like `just -l ..` and `just ../build` to find the same justfile.\n///\n/// For modes that do take other arguments, the search argument is simply\n/// prepended to rest.\n#[cfg_attr(test, derive(PartialEq, Eq, Debug))]\npub(crate) struct Positional {\n  /// Everything else\n  pub arguments: Vec<String>,\n  /// Overrides from values of the form `[a-zA-Z_][a-zA-Z0-9_-]*=.*`\n  pub overrides: Vec<(String, String)>,\n  /// An argument equal to '.', '..', or ending with `/`\n  pub search_directory: Option<String>,\n}\n\nimpl Positional {\n  pub(crate) fn from_values<'values>(\n    values: Option<impl IntoIterator<Item = &'values str>>,\n  ) -> Self {\n    let mut overrides = Vec::new();\n    let mut search_directory = None;\n    let mut arguments = Vec::new();\n\n    if let Some(values) = values {\n      for value in values {\n        if search_directory.is_none() && arguments.is_empty() {\n          if let Some(o) = Self::override_from_value(value) {\n            overrides.push(o);\n          } else if value == \".\" || value == \"..\" {\n            search_directory = Some(value.to_owned());\n          } else if let Some(i) = value.rfind('/') {\n            let (dir, tail) = value.split_at(i + 1);\n\n            search_directory = Some(dir.to_owned());\n\n            if !tail.is_empty() {\n              arguments.push(tail.to_owned());\n            }\n          } else {\n            arguments.push(value.to_owned());\n          }\n        } else {\n          arguments.push(value.to_owned());\n        }\n      }\n    }\n\n    Self {\n      arguments,\n      overrides,\n      search_directory,\n    }\n  }\n\n  /// Parse an override from a value of the form `NAME=.*`.\n  fn override_from_value(value: &str) -> Option<(String, String)> {\n    let (path, value) = value.split_once('=')?;\n    Some((path.into(), value.into()))\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  use pretty_assertions::assert_eq;\n\n  macro_rules! test {\n    {\n      name: $name:ident,\n      values: $vals:expr,\n      overrides: $overrides:expr,\n      search_directory: $search_directory:expr,\n      arguments: $arguments:expr,\n    } => {\n      #[test]\n      fn $name() {\n        assert_eq! (\n          Positional::from_values(Some($vals.iter().cloned())),\n          Positional {\n            overrides: $overrides\n              .iter()\n              .cloned()\n              .map(|(key, value): (&str, &str)| (key.to_owned(), value.to_owned()))\n              .collect(),\n            search_directory: $search_directory.map(str::to_owned),\n            arguments: $arguments.iter().cloned().map(str::to_owned).collect(),\n          },\n        )\n      }\n    }\n  }\n\n  test! {\n    name: no_values,\n    values: [],\n    overrides: [],\n    search_directory: None,\n    arguments: [],\n  }\n\n  test! {\n    name: arguments_only,\n    values: [\"foo\", \"bar\"],\n    overrides: [],\n    search_directory: None,\n    arguments: [\"foo\", \"bar\"],\n  }\n\n  test! {\n    name: all_overrides,\n    values: [\"foo=bar\", \"bar=foo\"],\n    overrides: [(\"foo\", \"bar\"), (\"bar\", \"foo\")],\n    search_directory: None,\n    arguments: [],\n  }\n\n  test! {\n    name: no_overrides,\n    values: [\"the-dir/\", \"baz\", \"bzzd\"],\n    overrides: [],\n    search_directory: Some(\"the-dir/\"),\n    arguments: [\"baz\", \"bzzd\"],\n  }\n\n  test! {\n    name: no_search_directory,\n    values: [\"foo=bar\", \"bar=foo\", \"baz\", \"bzzd\"],\n    overrides: [(\"foo\", \"bar\"), (\"bar\", \"foo\")],\n    search_directory: None,\n    arguments: [\"baz\", \"bzzd\"],\n  }\n\n  test! {\n    name: no_arguments,\n    values: [\"foo=bar\", \"bar=foo\", \"the-dir/\"],\n    overrides: [(\"foo\", \"bar\"), (\"bar\", \"foo\")],\n    search_directory: Some(\"the-dir/\"),\n    arguments: [],\n  }\n\n  test! {\n    name: all_dot,\n    values: [\"foo=bar\", \"bar=foo\", \".\", \"garnor\"],\n    overrides: [(\"foo\", \"bar\"), (\"bar\", \"foo\")],\n    search_directory: Some(\".\"),\n    arguments: [\"garnor\"],\n  }\n\n  test! {\n    name: all_dot_dot,\n    values: [\"foo=bar\", \"bar=foo\", \"..\", \"garnor\"],\n    overrides: [(\"foo\", \"bar\"), (\"bar\", \"foo\")],\n    search_directory: Some(\"..\"),\n    arguments: [\"garnor\"],\n  }\n\n  test! {\n    name: all_slash,\n    values: [\"foo=bar\", \"bar=foo\", \"/\", \"garnor\"],\n    overrides: [(\"foo\", \"bar\"), (\"bar\", \"foo\")],\n    search_directory: Some(\"/\"),\n    arguments: [\"garnor\"],\n  }\n\n  test! {\n    name: search_directory_after_argument,\n    values: [\"foo=bar\", \"bar=foo\", \"baz\", \"bzzd\", \"bar/\"],\n    overrides: [(\"foo\", \"bar\"), (\"bar\", \"foo\")],\n    search_directory: None,\n    arguments: [\"baz\", \"bzzd\", \"bar/\"],\n  }\n\n  test! {\n    name: override_after_search_directory,\n    values: [\"..\", \"a=b\"],\n    overrides: [],\n    search_directory: Some(\"..\"),\n    arguments: [\"a=b\"],\n  }\n\n  test! {\n    name: override_after_argument,\n    values: [\"a\", \"a=b\"],\n    overrides: [],\n    search_directory: None,\n    arguments: [\"a\", \"a=b\"],\n  }\n}\n"
  },
  {
    "path": "src/ran.rs",
    "content": "use super::*;\n\n#[derive(Default)]\npub(crate) struct Ran(Mutex<BTreeMap<Modulepath, BTreeMap<Vec<Vec<String>>, Arc<Mutex<bool>>>>>);\n\nimpl Ran {\n  pub(crate) fn mutex(&self, recipe: &Recipe, arguments: &[Vec<String>]) -> Arc<Mutex<bool>> {\n    self\n      .0\n      .lock()\n      .unwrap()\n      .entry(recipe.recipe_path().clone())\n      .or_default()\n      .entry(arguments.into())\n      .or_default()\n      .clone()\n  }\n}\n"
  },
  {
    "path": "src/range_ext.rs",
    "content": "use super::*;\n\npub(crate) trait RangeExt<T> {\n  fn display(&self) -> DisplayRange<&Self> {\n    DisplayRange(self)\n  }\n}\n\npub(crate) struct DisplayRange<T>(T);\n\nimpl Display for DisplayRange<&RangeInclusive<usize>> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    if self.0.start() == self.0.end() {\n      write!(f, \"{}\", self.0.start())?;\n    } else if *self.0.end() == usize::MAX {\n      write!(f, \"{} or more\", self.0.start())?;\n    } else {\n      write!(f, \"{} to {}\", self.0.start(), self.0.end())?;\n    }\n    Ok(())\n  }\n}\n\nimpl<T> RangeExt<T> for RangeInclusive<T> where T: PartialOrd {}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn display() {\n    assert!(!(1..1).contains(&1));\n    assert!((1..1).is_empty());\n    assert!((5..5).is_empty());\n    assert_eq!((0..=0).display().to_string(), \"0\");\n    assert_eq!((1..=1).display().to_string(), \"1\");\n    assert_eq!((5..=5).display().to_string(), \"5\");\n    assert_eq!((5..=9).display().to_string(), \"5 to 9\");\n    assert_eq!((1..=usize::MAX).display().to_string(), \"1 or more\");\n  }\n}\n"
  },
  {
    "path": "src/recipe.rs",
    "content": "use super::*;\n\n/// Return a `Error::Signal` if the process was terminated by a signal,\n/// otherwise return an `Error::UnknownFailure`\nfn error_from_signal(recipe: &str, line_number: Option<usize>, exit_status: ExitStatus) -> Error {\n  match Platform::signal_from_exit_status(exit_status) {\n    Some(signal) => Error::Signal {\n      recipe,\n      line_number,\n      signal,\n    },\n    None => Error::Unknown {\n      recipe,\n      line_number,\n    },\n  }\n}\n\n/// A recipe, e.g. `foo: bar baz`\n#[derive(PartialEq, Debug, Clone, Serialize)]\npub(crate) struct Recipe<'src, D = Dependency<'src>> {\n  pub(crate) attributes: AttributeSet<'src>,\n  pub(crate) body: Vec<Line<'src>>,\n  pub(crate) dependencies: Vec<D>,\n  pub(crate) doc: Option<String>,\n  #[serde(skip)]\n  pub(crate) file_depth: u32,\n  #[serde(skip)]\n  pub(crate) import_offsets: Vec<usize>,\n  #[serde(skip)]\n  pub(crate) module_path: Option<Modulepath>,\n  pub(crate) name: Name<'src>,\n  pub(crate) parameters: Vec<Parameter<'src>>,\n  pub(crate) priors: usize,\n  pub(crate) private: bool,\n  pub(crate) quiet: bool,\n  #[serde(rename = \"namepath\")]\n  pub(crate) recipe_path: Option<Modulepath>,\n  pub(crate) shebang: bool,\n  #[serde(skip)]\n  pub(crate) variable_references: HashSet<Number>,\n}\n\nimpl Recipe<'_> {\n  pub(crate) fn module_path(&self) -> &Modulepath {\n    self.module_path.as_ref().unwrap()\n  }\n\n  pub(crate) fn recipe_path(&self) -> &Modulepath {\n    self.recipe_path.as_ref().unwrap()\n  }\n\n  pub(crate) fn spaced_recipe_path(&self) -> String {\n    self.recipe_path().to_string().replace(\"::\", \" \")\n  }\n}\n\nimpl<'src, D> Recipe<'src, D> {\n  pub(crate) fn argument_range(&self) -> RangeInclusive<usize> {\n    self.min_arguments()..=self.max_arguments()\n  }\n\n  pub(crate) fn group_arguments(\n    &self,\n    arguments: &[Expression<'src>],\n  ) -> Vec<Vec<Expression<'src>>> {\n    let mut groups = Vec::new();\n    let mut rest = arguments;\n\n    for parameter in &self.parameters {\n      let group = if parameter.kind.is_variadic() {\n        mem::take(&mut rest).into()\n      } else if let Some(argument) = rest.first() {\n        rest = &rest[1..];\n        vec![argument.clone()]\n      } else {\n        debug_assert!(parameter.default.is_some());\n        Vec::new()\n      };\n\n      groups.push(group);\n    }\n\n    groups\n  }\n\n  pub(crate) fn min_arguments(&self) -> usize {\n    self.parameters.iter().filter(|p| p.is_required()).count()\n  }\n\n  pub(crate) fn max_arguments(&self) -> usize {\n    if self.parameters.iter().any(|p| p.kind.is_variadic()) {\n      usize::MAX - 1\n    } else {\n      self.parameters.len()\n    }\n  }\n\n  pub(crate) fn name(&self) -> &'src str {\n    self.name.lexeme()\n  }\n\n  pub(crate) fn line_number(&self) -> usize {\n    self.name.line\n  }\n\n  pub(crate) fn confirm(&self) -> RunResult<'src, bool> {\n    if let Some(Attribute::Confirm(prompt)) = self.attributes.get(AttributeDiscriminant::Confirm) {\n      if let Some(prompt) = prompt {\n        eprint!(\"{} \", prompt.cooked);\n      } else {\n        eprint!(\"Run recipe `{}`? \", self.name);\n      }\n      let mut line = String::new();\n      std::io::stdin()\n        .read_line(&mut line)\n        .map_err(|io_error| Error::GetConfirmation { io_error })?;\n      let line = line.trim().to_lowercase();\n      Ok(line == \"y\" || line == \"yes\")\n    } else {\n      Ok(true)\n    }\n  }\n\n  pub(crate) fn check_can_be_default_recipe(&self) -> RunResult<'src> {\n    let min_arguments = self.min_arguments();\n    if min_arguments > 0 {\n      return Err(Error::DefaultRecipeRequiresArguments {\n        recipe: self.name.lexeme(),\n        min_arguments,\n      });\n    }\n\n    Ok(())\n  }\n\n  pub(crate) fn is_parallel(&self) -> bool {\n    self.attributes.contains(AttributeDiscriminant::Parallel)\n  }\n\n  pub(crate) fn is_public(&self) -> bool {\n    !self.private && !self.attributes.contains(AttributeDiscriminant::Private)\n  }\n\n  pub(crate) fn is_script(&self) -> bool {\n    self.shebang\n  }\n\n  pub(crate) fn takes_positional_arguments(&self, settings: &Settings) -> bool {\n    settings.positional_arguments\n      || self\n        .attributes\n        .contains(AttributeDiscriminant::PositionalArguments)\n  }\n\n  pub(crate) fn change_directory(&self) -> bool {\n    !self.attributes.contains(AttributeDiscriminant::NoCd)\n  }\n\n  pub(crate) fn enabled(&self) -> bool {\n    let dragonfly = self.attributes.contains(AttributeDiscriminant::Dragonfly);\n    let freebsd = self.attributes.contains(AttributeDiscriminant::Freebsd);\n    let linux = self.attributes.contains(AttributeDiscriminant::Linux);\n    let macos = self.attributes.contains(AttributeDiscriminant::Macos);\n    let netbsd = self.attributes.contains(AttributeDiscriminant::Netbsd);\n    let openbsd = self.attributes.contains(AttributeDiscriminant::Openbsd);\n    let unix = self.attributes.contains(AttributeDiscriminant::Unix);\n    let windows = self.attributes.contains(AttributeDiscriminant::Windows);\n\n    (!windows && !linux && !macos && !openbsd && !freebsd && !dragonfly && !netbsd && !unix)\n      || (cfg!(target_os = \"dragonfly\") && (dragonfly || unix))\n      || (cfg!(target_os = \"freebsd\") && (freebsd || unix))\n      || (cfg!(target_os = \"linux\") && (linux || unix))\n      || (cfg!(target_os = \"macos\") && (macos || unix))\n      || (cfg!(target_os = \"netbsd\") && (netbsd || unix))\n      || (cfg!(target_os = \"openbsd\") && (openbsd || unix))\n      || (cfg!(target_os = \"windows\") && windows)\n      || (cfg!(unix) && unix)\n      || (cfg!(windows) && windows)\n  }\n\n  fn print_exit_message(&self, settings: &Settings) -> bool {\n    if self.attributes.contains(AttributeDiscriminant::ExitMessage) {\n      true\n    } else if settings.no_exit_message {\n      false\n    } else {\n      !self\n        .attributes\n        .contains(AttributeDiscriminant::NoExitMessage)\n    }\n  }\n\n  fn working_directory<'a>(&'a self, context: &'a ExecutionContext) -> Option<PathBuf> {\n    if !self.change_directory() {\n      return None;\n    }\n\n    let working_directory = context.working_directory();\n\n    for attribute in &self.attributes {\n      if let Attribute::WorkingDirectory(dir) = attribute {\n        return Some(working_directory.join(&dir.cooked));\n      }\n    }\n\n    Some(working_directory)\n  }\n\n  fn no_quiet(&self) -> bool {\n    self.attributes.contains(AttributeDiscriminant::NoQuiet)\n  }\n\n  pub(crate) fn run<'run>(\n    &self,\n    context: &ExecutionContext<'src, 'run>,\n    scope: &Scope<'src, 'run>,\n    positional: &[String],\n    is_dependency: bool,\n  ) -> RunResult<'src> {\n    let color = context.config.color.stderr().banner();\n    let prefix = color.prefix();\n    let suffix = color.suffix();\n\n    if context.config.verbosity.loquacious() {\n      eprintln!(\"{prefix}===> Running recipe `{}`...{suffix}\", self.name);\n    }\n\n    if context.config.explain {\n      if let Some(doc) = self.doc() {\n        eprintln!(\"{prefix}#### {doc}{suffix}\");\n      }\n    }\n\n    let evaluator = Evaluator::new(context, BTreeMap::new(), is_dependency, scope);\n\n    if self.is_script() {\n      self.run_script(context, scope, positional, evaluator)\n    } else {\n      self.run_linewise(context, scope, positional, evaluator)\n    }\n  }\n\n  fn run_linewise<'run>(\n    &self,\n    context: &ExecutionContext<'src, 'run>,\n    scope: &Scope<'src, 'run>,\n    positional: &[String],\n    mut evaluator: Evaluator<'src, 'run>,\n  ) -> RunResult<'src> {\n    let config = &context.config;\n    let settings = &context.module.settings;\n\n    let mut lines = self.body.iter().peekable();\n    let mut line_number = self.line_number() + 1;\n    loop {\n      let Some(line) = lines.peek() else {\n        return Ok(());\n      };\n\n      let mut evaluated = String::new();\n      let mut continued = false;\n\n      let comment_line = settings.ignore_comments && line.is_comment();\n      let sigils = line.sigils(settings);\n\n      loop {\n        if lines.peek().is_none() {\n          break;\n        }\n        let line = lines.next().unwrap();\n        line_number += 1;\n        if !comment_line {\n          evaluated += &evaluator.evaluate_line(line, continued)?;\n        }\n        if line.is_continuation() && !comment_line {\n          continued = true;\n          evaluated.pop();\n        } else {\n          break;\n        }\n      }\n\n      if comment_line {\n        continue;\n      }\n\n      let mut command = evaluated.as_str();\n\n      command = &command[sigils.len()..];\n\n      if command.is_empty() {\n        continue;\n      }\n\n      let guard = sigils.contains(&Sigil::Guard);\n      let infallible = sigils.contains(&Sigil::Infallible);\n      let quiet = sigils.contains(&Sigil::Quiet);\n\n      if config.dry_run\n        || config.verbosity.loquacious()\n        || config.timestamp\n        || !((quiet ^ self.quiet)\n          || (settings.quiet && !self.no_quiet())\n          || config.verbosity.quiet())\n      {\n        let color = if config.highlight {\n          config.color.command(config.command_color)\n        } else {\n          config.color\n        }\n        .stderr();\n\n        if let Some(timestamp) = config.timestamp() {\n          eprint!(\"[{}] \", color.paint(&timestamp));\n        }\n\n        eprintln!(\"{}\", color.paint(command));\n      }\n\n      if config.dry_run {\n        continue;\n      }\n\n      let mut cmd = settings.shell_command(config);\n\n      if let Some(working_directory) = self.working_directory(context) {\n        cmd.current_dir(working_directory);\n      }\n\n      cmd.arg(command);\n\n      if self.takes_positional_arguments(settings) {\n        cmd.arg(self.name.lexeme());\n        cmd.args(positional);\n      }\n\n      if config.verbosity.quiet() {\n        cmd.stderr(Stdio::null());\n        cmd.stdout(Stdio::null());\n      }\n\n      for attribute in &self.attributes {\n        if let Attribute::Env(key, value) = attribute {\n          cmd.env(&key.cooked, &value.cooked);\n        }\n      }\n\n      cmd.export(settings, context.dotenv, scope, &context.module.unexports);\n\n      let (result, caught) = cmd.status_guard();\n\n      match result {\n        Ok(exit_status) => {\n          if let Some(code) = exit_status.code() {\n            if code != 0 {\n              if guard {\n                if code == 1 {\n                  return Ok(());\n                }\n\n                return Err(Error::GuardCode {\n                  recipe: self.name(),\n                  line_number,\n                  code,\n                });\n              } else if !infallible {\n                return Err(Error::Code {\n                  recipe: self.name(),\n                  line_number: Some(line_number),\n                  code,\n                  print_message: self.print_exit_message(settings),\n                });\n              }\n            }\n          } else if !infallible {\n            return Err(error_from_signal(\n              self.name(),\n              Some(line_number),\n              exit_status,\n            ));\n          }\n        }\n        Err(io_error) => {\n          return Err(Error::Io {\n            recipe: self.name(),\n            io_error,\n          });\n        }\n      }\n\n      if !infallible {\n        if let Some(signal) = caught {\n          return Err(Error::Interrupted { signal });\n        }\n      }\n    }\n  }\n\n  pub(crate) fn run_script<'run>(\n    &self,\n    context: &ExecutionContext<'src, 'run>,\n    scope: &Scope<'src, 'run>,\n    positional: &[String],\n    mut evaluator: Evaluator<'src, 'run>,\n  ) -> RunResult<'src> {\n    let config = &context.config;\n\n    if let Some(timestamp) = config.timestamp() {\n      let color = if config.highlight {\n        config.color.command(config.command_color)\n      } else {\n        config.color\n      }\n      .stderr();\n\n      eprintln!(\"[{}] {}\", color.paint(&timestamp), self.name);\n    }\n\n    let mut evaluated_lines = Vec::new();\n    for line in &self.body {\n      evaluated_lines.push(evaluator.evaluate_line(line, false)?);\n    }\n\n    if config.verbosity.loud() && (config.dry_run || self.quiet) {\n      for line in &evaluated_lines {\n        eprintln!(\n          \"{}\",\n          config\n            .color\n            .command(config.command_color)\n            .stderr()\n            .paint(line)\n        );\n      }\n    }\n\n    if config.dry_run {\n      return Ok(());\n    }\n\n    let executor = if let Some(Attribute::Script(interpreter)) =\n      self.attributes.get(AttributeDiscriminant::Script)\n    {\n      Executor::Command(\n        interpreter\n          .as_ref()\n          .map(|interpreter| Interpreter {\n            command: interpreter.command.cooked.clone(),\n            arguments: interpreter\n              .arguments\n              .iter()\n              .map(|argument| argument.cooked.clone())\n              .collect(),\n          })\n          .or_else(|| context.module.settings.script_interpreter.clone())\n          .unwrap_or_else(|| Interpreter::default_script_interpreter().clone()),\n      )\n    } else {\n      let line = evaluated_lines\n        .first()\n        .ok_or_else(|| Error::internal(\"evaluated_lines was empty\"))?;\n\n      let shebang =\n        Shebang::new(line).ok_or_else(|| Error::internal(format!(\"bad shebang line: {line}\")))?;\n\n      Executor::Shebang(shebang)\n    };\n\n    let tempdir = context.tempdir(self)?;\n\n    let mut path = tempdir.path().to_path_buf();\n\n    let extension = self.attributes.iter().find_map(|attribute| {\n      if let Attribute::Extension(extension) = attribute {\n        Some(extension.cooked.as_str())\n      } else {\n        None\n      }\n    });\n\n    path.push(executor.script_filename(self.name(), extension));\n\n    let script = executor.script(self, &evaluated_lines);\n\n    if config.verbosity.grandiloquent() {\n      eprintln!(\"{}\", config.color.doc().stderr().paint(&script));\n    }\n\n    fs::write(&path, script).map_err(|error| Error::TempdirIo {\n      recipe: self.name(),\n      io_error: error,\n    })?;\n\n    let mut command = executor.command(\n      config,\n      &path,\n      self.name(),\n      self.working_directory(context).as_deref(),\n    )?;\n\n    if self.takes_positional_arguments(&context.module.settings) {\n      command.args(positional);\n    }\n\n    for attribute in &self.attributes {\n      if let Attribute::Env(key, value) = attribute {\n        command.env(&key.cooked, &value.cooked);\n      }\n    }\n\n    command.export(\n      &context.module.settings,\n      context.dotenv,\n      scope,\n      &context.module.unexports,\n    );\n\n    // run it!\n    let (result, caught) = command.status_guard();\n\n    match result {\n      Ok(exit_status) => exit_status.code().map_or_else(\n        || Err(error_from_signal(self.name(), None, exit_status)),\n        |code| {\n          if code == 0 {\n            Ok(())\n          } else {\n            Err(Error::Code {\n              recipe: self.name(),\n              line_number: None,\n              code,\n              print_message: self.print_exit_message(&context.module.settings),\n            })\n          }\n        },\n      )?,\n      Err(io_error) => return Err(executor.error(io_error, self.name())),\n    }\n\n    if let Some(signal) = caught {\n      return Err(Error::Interrupted { signal });\n    }\n\n    Ok(())\n  }\n\n  pub(crate) fn groups(&self) -> BTreeSet<String> {\n    self\n      .attributes\n      .iter()\n      .filter_map(|attribute| {\n        if let Attribute::Group(group) = attribute {\n          Some(group.cooked.clone())\n        } else {\n          None\n        }\n      })\n      .collect()\n  }\n\n  pub(crate) fn doc(&self) -> Option<&str> {\n    for attribute in &self.attributes {\n      if let Attribute::Doc(doc) = attribute {\n        return doc.as_ref().map(|s| s.cooked.as_ref());\n      }\n    }\n\n    self.doc.as_deref()\n  }\n\n  pub(crate) fn priors(&self) -> &[D] {\n    &self.dependencies[..self.priors]\n  }\n\n  pub(crate) fn subsequents(&self) -> &[D] {\n    &self.dependencies[self.priors..]\n  }\n}\n\nimpl<D: Display> ColorDisplay for Recipe<'_, D> {\n  fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {\n    if !self\n      .attributes\n      .iter()\n      .any(|attribute| matches!(attribute, Attribute::Doc(_)))\n    {\n      if let Some(doc) = &self.doc {\n        writeln!(f, \"# {doc}\")?;\n      }\n    }\n\n    for attribute in &self.attributes {\n      writeln!(f, \"[{attribute}]\")?;\n    }\n\n    if self.quiet {\n      write!(f, \"@{}\", self.name)?;\n    } else {\n      write!(f, \"{}\", self.name)?;\n    }\n\n    for parameter in &self.parameters {\n      write!(f, \" {}\", parameter.color_display(color))?;\n    }\n    write!(f, \":\")?;\n\n    for (i, dependency) in self.dependencies.iter().enumerate() {\n      if i == self.priors {\n        write!(f, \" &&\")?;\n      }\n\n      write!(f, \" {dependency}\")?;\n    }\n\n    for (i, line) in self.body.iter().enumerate() {\n      if i == 0 {\n        writeln!(f)?;\n      }\n      for (j, fragment) in line.fragments.iter().enumerate() {\n        if j == 0 {\n          write!(f, \"    \")?;\n        }\n        match fragment {\n          Fragment::Text { token } => write!(f, \"{}\", token.lexeme())?,\n          Fragment::Interpolation { expression, .. } => write!(f, \"{{{{ {expression} }}}}\")?,\n        }\n      }\n      if i + 1 < self.body.len() {\n        writeln!(f)?;\n      }\n    }\n    Ok(())\n  }\n}\n\nimpl<'src, D> Keyed<'src> for Recipe<'src, D> {\n  fn key(&self) -> &'src str {\n    self.name.lexeme()\n  }\n}\n"
  },
  {
    "path": "src/recipe_resolver.rs",
    "content": "use {super::*, CompileErrorKind::*};\n\npub(crate) struct RecipeResolver<'src: 'run, 'run> {\n  assignments: &'run Table<'src, Assignment<'src>>,\n  modulepath: &'run Modulepath,\n  modules: &'run Table<'src, Justfile<'src>>,\n  resolved_recipes: Table<'src, Arc<Recipe<'src>>>,\n  settings: &'run Settings,\n  unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,\n}\n\nimpl<'src: 'run, 'run> RecipeResolver<'src, 'run> {\n  pub(crate) fn resolve_recipes(\n    assignments: &'run Table<'src, Assignment<'src>>,\n    modulepath: &'run Modulepath,\n    modules: &'run Table<'src, Justfile<'src>>,\n    settings: &'run Settings,\n    unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,\n  ) -> CompileResult<'src, Table<'src, Arc<Recipe<'src>>>> {\n    let mut resolver = Self {\n      assignments,\n      modulepath,\n      modules,\n      resolved_recipes: Table::new(),\n      settings,\n      unresolved_recipes,\n    };\n\n    while let Some(unresolved) = resolver.unresolved_recipes.pop() {\n      resolver.resolve_recipe(&mut Vec::new(), unresolved)?;\n    }\n\n    Ok(resolver.resolved_recipes)\n  }\n\n  fn resolve_recipe(\n    &mut self,\n    stack: &mut Vec<&'src str>,\n    recipe: UnresolvedRecipe<'src>,\n  ) -> CompileResult<'src, Arc<Recipe<'src>>> {\n    if let Some(resolved) = self.resolved_recipes.get(recipe.name()) {\n      return Ok(Arc::clone(resolved));\n    }\n\n    stack.push(recipe.name());\n\n    let dependencies = recipe\n      .dependencies\n      .iter()\n      .map(|dependency| {\n        self\n          .resolve_dependency(dependency, &recipe, stack)?\n          .ok_or_else(|| {\n            dependency.recipe.last().error(UnknownDependency {\n              recipe: recipe.name(),\n              unknown: dependency.recipe.clone(),\n            })\n          })\n      })\n      .collect::<CompileResult<Vec<Arc<Recipe>>>>()?;\n\n    stack.pop();\n\n    let resolved = Arc::new(recipe.resolve(\n      self.assignments,\n      self.modulepath,\n      dependencies,\n      self.settings,\n    )?);\n    self.resolved_recipes.insert(Arc::clone(&resolved));\n    Ok(resolved)\n  }\n\n  fn resolve_dependency(\n    &mut self,\n    dependency: &UnresolvedDependency<'src>,\n    recipe: &UnresolvedRecipe<'src>,\n    stack: &mut Vec<&'src str>,\n  ) -> CompileResult<'src, Option<Arc<Recipe<'src>>>> {\n    let name = dependency.recipe.last().lexeme();\n\n    if dependency.recipe.components() > 1 {\n      // recipe is in a submodule and is thus already resolved\n      Ok(Analyzer::resolve_recipe(\n        &dependency.recipe,\n        self.modules,\n        &self.resolved_recipes,\n      ))\n    } else if let Some(resolved) = self.resolved_recipes.get(name) {\n      // recipe is the current module and has already been resolved\n      Ok(Some(Arc::clone(resolved)))\n    } else if stack.contains(&name) {\n      // recipe depends on itself\n      let first = stack[0];\n      stack.push(first);\n      Err(\n        dependency.recipe.last().error(CircularRecipeDependency {\n          recipe: recipe.name(),\n          circle: stack\n            .iter()\n            .skip_while(|name| **name != dependency.recipe.last().lexeme())\n            .copied()\n            .collect(),\n        }),\n      )\n    } else if let Some(unresolved) = self.unresolved_recipes.remove(name) {\n      // recipe is as of yet unresolved\n      Ok(Some(self.resolve_recipe(stack, unresolved)?))\n    } else {\n      // recipe is unknown\n      Ok(None)\n    }\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  analysis_error! {\n    name:   circular_recipe_dependency,\n    input:  \"a: b\\nb: a\",\n    offset: 8,\n    line:   1,\n    column: 3,\n    width:  1,\n    kind:   CircularRecipeDependency{recipe: \"b\", circle: vec![\"a\", \"b\", \"a\"]},\n  }\n\n  analysis_error! {\n    name:   self_recipe_dependency,\n    input:  \"a: a\",\n    offset: 3,\n    line:   0,\n    column: 3,\n    width:  1,\n    kind:   CircularRecipeDependency{recipe: \"a\", circle: vec![\"a\", \"a\"]},\n  }\n\n  analysis_error! {\n    name:   unknown_dependency,\n    input:  \"a: b\",\n    offset: 3,\n    line:   0,\n    column: 3,\n    width:  1,\n    kind:   UnknownDependency{\n      recipe: \"a\",\n      unknown: Namepath::from(Name::from_identifier(\n        Token{\n          column: 3,\n          kind: TokenKind::Identifier,\n          length: 1,\n          line: 0,\n          offset: 3,\n          path: Path::new(\"justfile\"),\n          src: \"a: b\" }))\n    },\n  }\n\n  analysis_error! {\n    name:   unknown_interpolation_variable,\n    input:  \"x:\\n {{   hello}}\",\n    offset: 9,\n    line:   1,\n    column: 6,\n    width:  5,\n    kind:   UndefinedVariable{variable: \"hello\"},\n  }\n\n  analysis_error! {\n    name:   unknown_second_interpolation_variable,\n    input:  \"wtf:=\\\"x\\\"\\nx:\\n echo\\n foo {{wtf}} {{ lol }}\",\n    offset: 34,\n    line:   3,\n    column: 16,\n    width:  3,\n    kind:   UndefinedVariable{variable: \"lol\"},\n  }\n\n  analysis_error! {\n    name:   unknown_variable_in_default,\n    input:  \"a f=foo:\",\n    offset: 4,\n    line:   0,\n    column: 4,\n    width:  3,\n    kind:   UndefinedVariable{variable: \"foo\"},\n  }\n\n  analysis_error! {\n    name:   unknown_variable_in_dependency_argument,\n    input:  \"bar x:\\nfoo: (bar baz)\",\n    offset: 17,\n    line:   1,\n    column: 10,\n    width:  3,\n    kind:   UndefinedVariable{variable: \"baz\"},\n  }\n}\n"
  },
  {
    "path": "src/recipe_signature.rs",
    "content": "use super::*;\n\npub(crate) struct RecipeSignature<'a> {\n  pub(crate) name: &'a str,\n  pub(crate) recipe: &'a Recipe<'a>,\n}\n\nimpl ColorDisplay for RecipeSignature<'_> {\n  fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {\n    write!(f, \"{}\", self.name)?;\n    for parameter in &self.recipe.parameters {\n      write!(f, \" {}\", parameter.color_display(color))?;\n    }\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "src/request.rs",
    "content": "use super::*;\n\n#[derive(Clone, Debug, Deserialize, PartialEq)]\n#[serde(rename_all = \"kebab-case\")]\npub enum Request {\n  EnvironmentVariable(String),\n  #[cfg(not(windows))]\n  Signal,\n}\n\n#[derive(Debug, Deserialize, PartialEq, Serialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum Response {\n  EnvironmentVariable(Option<OsString>),\n  #[cfg(not(windows))]\n  Signal(String),\n}\n"
  },
  {
    "path": "src/run.rs",
    "content": "use super::*;\n\n/// Main entry point into `just`. Parse arguments from `args` and run.\n#[allow(clippy::missing_errors_doc)]\npub fn run(args: impl Iterator<Item = impl Into<OsString> + Clone>) -> Result<(), i32> {\n  #[cfg(windows)]\n  ansi_term::enable_ansi_support().ok();\n\n  let app = Config::app();\n\n  let matches = app.try_get_matches_from(args).map_err(|err| {\n    err.print().ok();\n    err.exit_code()\n  })?;\n\n  let config = Config::from_matches(&matches).map_err(Error::from);\n\n  let (color, verbosity) = config\n    .as_ref()\n    .map(|config| (config.color, config.verbosity))\n    .unwrap_or_default();\n\n  let loader = Loader::new();\n\n  config\n    .and_then(|config| {\n      SignalHandler::install(config.verbosity)?;\n      config.subcommand.execute(&config, &loader)\n    })\n    .map_err(|error| {\n      if !verbosity.quiet() && error.print_message() {\n        eprintln!(\"{}\", error.color_display(color.stderr()));\n      }\n      error.code().unwrap_or(EXIT_FAILURE)\n    })\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn run_can_be_called_more_than_once() {\n    let tmp = testing::tempdir();\n    fs::write(tmp.path().join(\"justfile\"), \"foo:\").unwrap();\n    let search_directory = format!(\"{}/\", tmp.path().to_str().unwrap());\n    run([\"just\", &search_directory].iter()).unwrap();\n    run([\"just\", &search_directory].iter()).unwrap();\n  }\n}\n"
  },
  {
    "path": "src/scope.rs",
    "content": "use super::*;\n\n#[derive(Debug)]\npub(crate) struct Scope<'src: 'run, 'run> {\n  bindings: Table<'src, Binding<'src, String>>,\n  parent: Option<&'run Self>,\n}\n\nimpl<'src, 'run> Scope<'src, 'run> {\n  pub(crate) fn child(&'run self) -> Self {\n    Self {\n      parent: Some(self),\n      bindings: Table::new(),\n    }\n  }\n\n  pub(crate) fn root() -> Self {\n    let mut root = Self {\n      parent: None,\n      bindings: Table::new(),\n    };\n\n    for (i, (key, value)) in constants().iter().enumerate() {\n      root.bind(Binding {\n        eager: false,\n        export: false,\n        file_depth: 0,\n        name: Name {\n          token: Token {\n            column: 0,\n            kind: TokenKind::Identifier,\n            length: key.len(),\n            line: 0,\n            offset: 0,\n            path: Path::new(\"PRELUDE\"),\n            src: key,\n          },\n        },\n        number: Numerator::constant(i),\n        prelude: true,\n        private: false,\n        value: (*value).into(),\n      });\n    }\n\n    root\n  }\n\n  pub(crate) fn bind(&mut self, binding: Binding<'src>) {\n    self.bindings.insert(binding);\n  }\n\n  pub(crate) fn bound(&self, name: &str) -> bool {\n    self.bindings.contains_key(name)\n  }\n\n  pub(crate) fn value(&self, name: &str) -> Option<&str> {\n    if let Some(binding) = self.bindings.get(name) {\n      Some(binding.value.as_ref())\n    } else {\n      self.parent?.value(name)\n    }\n  }\n\n  pub(crate) fn bindings(&self) -> impl Iterator<Item = &Binding<String>> {\n    self.bindings.values()\n  }\n\n  pub(crate) fn names(&self) -> impl Iterator<Item = &str> {\n    self.bindings.keys().copied()\n  }\n\n  pub(crate) fn parent(&self) -> Option<&'run Self> {\n    self.parent\n  }\n}\n"
  },
  {
    "path": "src/search.rs",
    "content": "use {super::*, std::path::Component};\n\nconst DEFAULT_JUSTFILE_NAME: &str = JUSTFILE_NAMES[0];\npub(crate) const JUSTFILE_NAMES: [&str; 2] = [\"justfile\", \".justfile\"];\nconst PROJECT_ROOT_CHILDREN: &[&str] = &[\".bzr\", \".git\", \".hg\", \".svn\", \"_darcs\"];\n\n#[derive(Debug)]\npub(crate) struct Search {\n  pub(crate) justfile: PathBuf,\n  pub(crate) working_directory: PathBuf,\n}\n\nimpl Search {\n  fn global_justfile_paths() -> Vec<(PathBuf, &'static str)> {\n    let mut paths = Vec::new();\n\n    if let Some(config_dir) = dirs::config_dir() {\n      paths.push((config_dir.join(\"just\"), DEFAULT_JUSTFILE_NAME));\n    }\n\n    if let Some(home_dir) = dirs::home_dir() {\n      paths.push((home_dir.join(\".config\").join(\"just\"), DEFAULT_JUSTFILE_NAME));\n\n      for justfile_name in JUSTFILE_NAMES {\n        paths.push((home_dir.clone(), justfile_name));\n      }\n    }\n\n    paths\n  }\n\n  /// Find justfile given search configuration and invocation directory\n  pub(crate) fn find(\n    ceiling: Option<&Path>,\n    invocation_directory: &Path,\n    search_config: &SearchConfig,\n  ) -> SearchResult<Self> {\n    match search_config {\n      SearchConfig::FromInvocationDirectory => {\n        Self::find_in_directory(ceiling, invocation_directory)\n      }\n      SearchConfig::FromSearchDirectory { search_directory } => {\n        let search_directory = Self::clean(invocation_directory, search_directory);\n        let justfile = Self::justfile(ceiling, &search_directory)?;\n        let working_directory = Self::working_directory_from_justfile(&justfile)?;\n        Ok(Self {\n          justfile,\n          working_directory,\n        })\n      }\n      SearchConfig::GlobalJustfile => Ok(Self {\n        justfile: Self::find_global_justfile()?,\n        working_directory: Self::project_root(ceiling, invocation_directory)?,\n      }),\n      SearchConfig::WithJustfile { justfile } => {\n        let justfile = Self::clean(invocation_directory, justfile);\n        let working_directory = Self::working_directory_from_justfile(&justfile)?;\n        Ok(Self {\n          justfile,\n          working_directory,\n        })\n      }\n      SearchConfig::WithJustfileAndWorkingDirectory {\n        justfile,\n        working_directory,\n      } => Ok(Self {\n        justfile: Self::clean(invocation_directory, justfile),\n        working_directory: Self::clean(invocation_directory, working_directory),\n      }),\n    }\n  }\n\n  fn find_global_justfile() -> SearchResult<PathBuf> {\n    for (directory, filename) in Self::global_justfile_paths() {\n      if let Ok(read_dir) = fs::read_dir(&directory) {\n        for entry in read_dir {\n          let entry = entry.map_err(|io_error| SearchError::Io {\n            io_error,\n            directory: directory.clone(),\n          })?;\n          if let Some(candidate) = entry.file_name().to_str() {\n            if candidate.eq_ignore_ascii_case(filename) {\n              return Ok(entry.path());\n            }\n          }\n        }\n      }\n    }\n\n    Err(SearchError::GlobalJustfileNotFound)\n  }\n\n  /// Find justfile starting from parent directory of current justfile\n  pub(crate) fn search_parent_directory(&self, ceiling: Option<&Path>) -> SearchResult<Self> {\n    let parent = self\n      .justfile\n      .parent()\n      .and_then(|path| path.parent())\n      .ok_or_else(|| SearchError::JustfileHadNoParent {\n        path: self.justfile.clone(),\n      })?;\n    Self::find_in_directory(ceiling, parent)\n  }\n\n  /// Find justfile starting in given directory searching upwards in directory tree\n  fn find_in_directory(ceiling: Option<&Path>, starting_dir: &Path) -> SearchResult<Self> {\n    let justfile = Self::justfile(ceiling, starting_dir)?;\n    let working_directory = Self::working_directory_from_justfile(&justfile)?;\n    Ok(Self {\n      justfile,\n      working_directory,\n    })\n  }\n\n  /// Get working directory and justfile path for newly-initialized justfile\n  pub(crate) fn init(\n    search_config: &SearchConfig,\n    invocation_directory: &Path,\n    ceiling: Option<&Path>,\n  ) -> SearchResult<Self> {\n    match search_config {\n      SearchConfig::FromInvocationDirectory => {\n        let working_directory = Self::project_root(ceiling, invocation_directory)?;\n        let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME);\n        Ok(Self {\n          justfile,\n          working_directory,\n        })\n      }\n      SearchConfig::FromSearchDirectory { search_directory } => {\n        let search_directory = Self::clean(invocation_directory, search_directory);\n        let working_directory = Self::project_root(ceiling, &search_directory)?;\n        let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME);\n        Ok(Self {\n          justfile,\n          working_directory,\n        })\n      }\n      SearchConfig::GlobalJustfile => Err(SearchError::GlobalJustfileInit),\n      SearchConfig::WithJustfile { justfile } => {\n        let justfile = Self::clean(invocation_directory, justfile);\n        let working_directory = Self::working_directory_from_justfile(&justfile)?;\n        Ok(Self {\n          justfile,\n          working_directory,\n        })\n      }\n      SearchConfig::WithJustfileAndWorkingDirectory {\n        justfile,\n        working_directory,\n      } => Ok(Self {\n        justfile: Self::clean(invocation_directory, justfile),\n        working_directory: Self::clean(invocation_directory, working_directory),\n      }),\n    }\n  }\n\n  /// Search upwards from `directory` for a file whose name matches one of\n  /// `JUSTFILE_NAMES`\n  fn justfile(ceiling: Option<&Path>, directory: &Path) -> SearchResult<PathBuf> {\n    for directory in directory.ancestors() {\n      let mut candidates = BTreeSet::new();\n\n      let entries = fs::read_dir(directory).map_err(|io_error| SearchError::Io {\n        io_error,\n        directory: directory.to_owned(),\n      })?;\n\n      for entry in entries {\n        let entry = entry.map_err(|io_error| SearchError::Io {\n          io_error,\n          directory: directory.to_owned(),\n        })?;\n        if let Some(name) = entry.file_name().to_str() {\n          for justfile_name in JUSTFILE_NAMES {\n            if name.eq_ignore_ascii_case(justfile_name) {\n              candidates.insert(entry.path());\n            }\n          }\n        }\n      }\n\n      match candidates.len() {\n        0 => {}\n        1 => return Ok(candidates.into_iter().next().unwrap()),\n        _ => return Err(SearchError::MultipleCandidates { candidates }),\n      }\n\n      if let Some(ceiling) = ceiling {\n        if directory == ceiling {\n          break;\n        }\n      }\n    }\n\n    Err(SearchError::NotFound)\n  }\n\n  fn clean(invocation_directory: &Path, path: &Path) -> PathBuf {\n    let path = invocation_directory.join(path);\n\n    let mut clean = Vec::new();\n\n    for component in path.components() {\n      if component == Component::ParentDir {\n        if let Some(Component::Normal(_)) = clean.last() {\n          clean.pop();\n        }\n      } else {\n        clean.push(component);\n      }\n    }\n\n    clean.into_iter().collect()\n  }\n\n  /// Search upwards from `directory` for the root directory of a software\n  /// project, as determined by the presence of one of the version control\n  /// system directories given in `PROJECT_ROOT_CHILDREN`\n  fn project_root(ceiling: Option<&Path>, directory: &Path) -> SearchResult<PathBuf> {\n    for directory in directory.ancestors() {\n      let entries = fs::read_dir(directory).map_err(|io_error| SearchError::Io {\n        io_error,\n        directory: directory.to_owned(),\n      })?;\n\n      for entry in entries {\n        let entry = entry.map_err(|io_error| SearchError::Io {\n          io_error,\n          directory: directory.to_owned(),\n        })?;\n        for project_root_child in PROJECT_ROOT_CHILDREN.iter().copied() {\n          if entry.file_name() == project_root_child {\n            return Ok(directory.to_owned());\n          }\n        }\n      }\n\n      if let Some(ceiling) = ceiling {\n        if directory == ceiling {\n          break;\n        }\n      }\n    }\n\n    Ok(directory.to_owned())\n  }\n\n  fn working_directory_from_justfile(justfile: &Path) -> SearchResult<PathBuf> {\n    Ok(\n      justfile\n        .parent()\n        .ok_or_else(|| SearchError::JustfileHadNoParent {\n          path: justfile.to_path_buf(),\n        })?\n        .to_owned(),\n    )\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n  use temptree::temptree;\n\n  #[test]\n  fn not_found() {\n    let tmp = testing::tempdir();\n    match Search::justfile(None, tmp.path()) {\n      Err(SearchError::NotFound) => {}\n      _ => panic!(\"No justfile found error was expected\"),\n    }\n  }\n\n  #[test]\n  fn multiple_candidates() {\n    let tmp = testing::tempdir();\n    let mut path = tmp.path().to_path_buf();\n    path.push(DEFAULT_JUSTFILE_NAME);\n    fs::write(&path, \"default:\\n\\techo ok\").unwrap();\n    path.pop();\n    path.push(DEFAULT_JUSTFILE_NAME.to_uppercase());\n    if fs::File::open(path.as_path()).is_ok() {\n      // We are in case-insensitive file system\n      return;\n    }\n    fs::write(&path, \"default:\\n\\techo ok\").unwrap();\n    path.pop();\n    match Search::justfile(None, path.as_path()) {\n      Err(SearchError::MultipleCandidates { .. }) => {}\n      _ => panic!(\"Multiple candidates error was expected\"),\n    }\n  }\n\n  #[test]\n  fn found() {\n    let tmp = testing::tempdir();\n    let mut path = tmp.path().to_path_buf();\n    path.push(DEFAULT_JUSTFILE_NAME);\n    fs::write(&path, \"default:\\n\\techo ok\").unwrap();\n    path.pop();\n    if let Err(err) = Search::justfile(None, path.as_path()) {\n      panic!(\"No errors were expected: {err}\");\n    }\n  }\n\n  #[test]\n  fn found_spongebob_case() {\n    let tmp = testing::tempdir();\n    let mut path = tmp.path().to_path_buf();\n    let spongebob_case = DEFAULT_JUSTFILE_NAME\n      .chars()\n      .enumerate()\n      .map(|(i, c)| {\n        if i % 2 == 0 {\n          c.to_ascii_uppercase()\n        } else {\n          c\n        }\n      })\n      .collect::<String>();\n    path.push(spongebob_case);\n    fs::write(&path, \"default:\\n\\techo ok\").unwrap();\n    path.pop();\n    if let Err(err) = Search::justfile(None, path.as_path()) {\n      panic!(\"No errors were expected: {err}\");\n    }\n  }\n\n  #[test]\n  fn found_from_inner_dir() {\n    let tmp = testing::tempdir();\n    let mut path = tmp.path().to_path_buf();\n    path.push(DEFAULT_JUSTFILE_NAME);\n    fs::write(&path, \"default:\\n\\techo ok\").unwrap();\n    path.pop();\n    path.push(\"a\");\n    fs::create_dir(&path).expect(\"test justfile search: failed to create intermediary directory\");\n    path.push(\"b\");\n    fs::create_dir(&path).expect(\"test justfile search: failed to create intermediary directory\");\n    if let Err(err) = Search::justfile(None, path.as_path()) {\n      panic!(\"No errors were expected: {err}\");\n    }\n  }\n\n  #[test]\n  fn found_and_stopped_at_first_justfile() {\n    let tmp = testing::tempdir();\n    let mut path = tmp.path().to_path_buf();\n    path.push(DEFAULT_JUSTFILE_NAME);\n    fs::write(&path, \"default:\\n\\techo ok\").unwrap();\n    path.pop();\n    path.push(\"a\");\n    fs::create_dir(&path).expect(\"test justfile search: failed to create intermediary directory\");\n    path.push(DEFAULT_JUSTFILE_NAME);\n    fs::write(&path, \"default:\\n\\techo ok\").unwrap();\n    path.pop();\n    path.push(\"b\");\n    fs::create_dir(&path).expect(\"test justfile search: failed to create intermediary directory\");\n    match Search::justfile(None, path.as_path()) {\n      Ok(found_path) => {\n        path.pop();\n        path.push(DEFAULT_JUSTFILE_NAME);\n        assert_eq!(found_path, path);\n      }\n      Err(err) => panic!(\"No errors were expected: {err}\"),\n    }\n  }\n\n  #[test]\n  fn justfile_symlink_parent() {\n    let tmp = temptree! {\n      src: \"\",\n      sub: {},\n    };\n\n    let src = tmp.path().join(\"src\");\n    let sub = tmp.path().join(\"sub\");\n    let justfile = sub.join(\"justfile\");\n\n    #[cfg(unix)]\n    std::os::unix::fs::symlink(src, &justfile).unwrap();\n\n    #[cfg(windows)]\n    std::os::windows::fs::symlink_file(&src, &justfile).unwrap();\n\n    let search_config = SearchConfig::FromInvocationDirectory;\n\n    let search = Search::find(None, &sub, &search_config).unwrap();\n\n    assert_eq!(search.justfile, justfile);\n    assert_eq!(search.working_directory, sub);\n  }\n\n  #[test]\n  fn clean() {\n    let cases = &[\n      (\"/\", \"foo\", \"/foo\"),\n      (\"/bar\", \"/foo\", \"/foo\"),\n      if cfg!(windows) {\n        (\"//foo\", \"bar//baz\", \"//foo\\\\bar\\\\baz\")\n      } else {\n        (\"/\", \"..\", \"/\")\n      },\n      (\"/\", \"/..\", \"/\"),\n      (\"/..\", \"\", \"/\"),\n      (\"/../../../..\", \"../../../\", \"/\"),\n      (\"/.\", \"./\", \"/\"),\n      (\"/foo/../\", \"bar\", \"/bar\"),\n      (\"/foo/bar\", \"..\", \"/foo\"),\n      (\"/foo/bar/\", \"..\", \"/foo\"),\n    ];\n\n    for (prefix, suffix, want) in cases {\n      let have = Search::clean(Path::new(prefix), Path::new(suffix));\n      assert_eq!(have, Path::new(want));\n    }\n  }\n}\n"
  },
  {
    "path": "src/search_config.rs",
    "content": "use super::*;\n\n/// Controls how `just` will search for the justfile.\n#[derive(Debug, Default, PartialEq)]\npub(crate) enum SearchConfig {\n  /// Recursively search for the justfile upwards from the invocation directory\n  /// to the root, setting the working directory to the directory in which the\n  /// justfile is found.\n  #[default]\n  FromInvocationDirectory,\n  /// As in `Invocation`, but start from `search_directory`.\n  FromSearchDirectory { search_directory: PathBuf },\n  /// Search for global justfile\n  GlobalJustfile,\n  /// Use user-specified justfile, with the working directory set to the\n  /// directory that contains it.\n  WithJustfile { justfile: PathBuf },\n  /// Use user-specified justfile and working directory.\n  WithJustfileAndWorkingDirectory {\n    justfile: PathBuf,\n    working_directory: PathBuf,\n  },\n}\n"
  },
  {
    "path": "src/search_error.rs",
    "content": "use super::*;\n\n#[derive(Debug, Snafu)]\n#[snafu(visibility(pub(crate)))]\npub(crate) enum SearchError {\n  #[snafu(display(\"Cannot initialize global justfile\"))]\n  GlobalJustfileInit,\n  #[snafu(display(\"Global justfile not found\"))]\n  GlobalJustfileNotFound,\n  #[snafu(display(\n    \"I/O error reading directory `{}`: {}\",\n    directory.display(),\n    io_error\n  ))]\n  Io {\n    directory: PathBuf,\n    io_error: io::Error,\n  },\n  #[snafu(display(\"Justfile path had no parent: {}\", path.display()))]\n  JustfileHadNoParent { path: PathBuf },\n  #[snafu(display(\n    \"Multiple candidate justfiles found in `{}`: {}\",\n    candidates.iter().next().unwrap().parent().unwrap().display(),\n    List::and_ticked(\n      candidates\n        .iter()\n        .map(|candidate| candidate.file_name().unwrap().to_string_lossy())\n    ),\n  ))]\n  MultipleCandidates { candidates: BTreeSet<PathBuf> },\n  #[snafu(display(\"No justfile found\"))]\n  NotFound,\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn multiple_candidates_formatting() {\n    let error = SearchError::MultipleCandidates {\n      candidates: [Path::new(\"/foo/justfile\"), Path::new(\"/foo/JUSTFILE\")]\n        .iter()\n        .map(|path| path.to_path_buf())\n        .collect(),\n    };\n\n    assert_eq!(\n      error.to_string(),\n      \"Multiple candidate justfiles found in `/foo`: `JUSTFILE` and `justfile`\"\n    );\n  }\n}\n"
  },
  {
    "path": "src/set.rs",
    "content": "use super::*;\n\n#[derive(Debug, Clone)]\npub(crate) struct Set<'src> {\n  pub(crate) name: Name<'src>,\n  pub(crate) value: Setting<'src>,\n}\n\nimpl<'src> Keyed<'src> for Set<'src> {\n  fn key(&self) -> &'src str {\n    self.name.lexeme()\n  }\n}\n\nimpl Display for Set<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    write!(f, \"set {} := {}\", self.name, self.value)\n  }\n}\n"
  },
  {
    "path": "src/setting.rs",
    "content": "use super::*;\n\n#[derive(Debug, Clone)]\npub(crate) enum Setting<'src> {\n  AllowDuplicateRecipes(bool),\n  AllowDuplicateVariables(bool),\n  DotenvFilename(Expression<'src>),\n  DotenvLoad(bool),\n  DotenvOverride(bool),\n  DotenvPath(Expression<'src>),\n  DotenvRequired(bool),\n  Export(bool),\n  Fallback(bool),\n  Guards(bool),\n  IgnoreComments(bool),\n  Lazy(bool),\n  NoExitMessage(bool),\n  PositionalArguments(bool),\n  Quiet(bool),\n  ScriptInterpreter(Interpreter<Expression<'src>>),\n  Shell(Interpreter<Expression<'src>>),\n  Tempdir(Expression<'src>),\n  Unstable(bool),\n  WindowsPowerShell(bool),\n  WindowsShell(Interpreter<Expression<'src>>),\n  WorkingDirectory(Expression<'src>),\n}\n\nimpl<'src> Setting<'src> {\n  pub(crate) fn expressions(&self) -> impl Iterator<Item = &Expression<'src>> {\n    let first = match self {\n      Self::DotenvFilename(value)\n      | Self::DotenvPath(value)\n      | Self::Tempdir(value)\n      | Self::WorkingDirectory(value) => Some(value),\n      Self::ScriptInterpreter(value) | Self::Shell(value) | Self::WindowsShell(value) => {\n        Some(&value.command)\n      }\n      _ => None,\n    };\n\n    let rest = match self {\n      Self::ScriptInterpreter(value) | Self::Shell(value) | Self::WindowsShell(value) => {\n        value.arguments.as_slice()\n      }\n      _ => &[],\n    };\n\n    first.into_iter().chain(rest)\n  }\n}\n\nimpl Display for Setting<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    match self {\n      Self::AllowDuplicateRecipes(value)\n      | Self::AllowDuplicateVariables(value)\n      | Self::DotenvLoad(value)\n      | Self::DotenvOverride(value)\n      | Self::DotenvRequired(value)\n      | Self::Export(value)\n      | Self::Fallback(value)\n      | Self::Guards(value)\n      | Self::IgnoreComments(value)\n      | Self::Lazy(value)\n      | Self::NoExitMessage(value)\n      | Self::PositionalArguments(value)\n      | Self::Quiet(value)\n      | Self::Unstable(value)\n      | Self::WindowsPowerShell(value) => write!(f, \"{value}\"),\n      Self::DotenvFilename(value)\n      | Self::DotenvPath(value)\n      | Self::Tempdir(value)\n      | Self::WorkingDirectory(value) => {\n        write!(f, \"{value}\")\n      }\n      Self::ScriptInterpreter(value) | Self::Shell(value) | Self::WindowsShell(value) => {\n        write!(f, \"[{value}]\")\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/settings.rs",
    "content": "use super::*;\n\npub(crate) const DEFAULT_SHELL: &str = \"sh\";\npub(crate) const DEFAULT_SHELL_ARGS: &[&str] = &[\"-cu\"];\npub(crate) const WINDOWS_POWERSHELL_SHELL: &str = \"powershell.exe\";\npub(crate) const WINDOWS_POWERSHELL_ARGS: &[&str] = &[\"-NoLogo\", \"-Command\"];\n\n#[derive(Debug, PartialEq, Serialize, Default)]\npub(crate) struct Settings {\n  pub(crate) allow_duplicate_recipes: bool,\n  pub(crate) allow_duplicate_variables: bool,\n  pub(crate) dotenv_filename: Option<String>,\n  pub(crate) dotenv_load: bool,\n  pub(crate) dotenv_override: bool,\n  pub(crate) dotenv_path: Option<PathBuf>,\n  pub(crate) dotenv_required: bool,\n  pub(crate) export: bool,\n  pub(crate) fallback: bool,\n  pub(crate) guards: bool,\n  pub(crate) ignore_comments: bool,\n  pub(crate) lazy: bool,\n  pub(crate) no_exit_message: bool,\n  pub(crate) positional_arguments: bool,\n  pub(crate) quiet: bool,\n  #[serde(skip)]\n  pub(crate) script_interpreter: Option<Interpreter<String>>,\n  pub(crate) shell: Option<Interpreter<String>>,\n  pub(crate) tempdir: Option<String>,\n  pub(crate) unstable: bool,\n  pub(crate) windows_powershell: bool,\n  pub(crate) windows_shell: Option<Interpreter<String>>,\n  pub(crate) working_directory: Option<PathBuf>,\n}\n\nimpl Settings {\n  pub(crate) fn shell_command(&self, config: &Config) -> Command {\n    let (command, args) = self.shell(config);\n\n    let mut cmd = Command::new(command);\n\n    cmd.args(args);\n\n    cmd\n  }\n\n  pub(crate) fn shell<'a>(&'a self, config: &'a Config) -> (&'a str, Vec<&'a str>) {\n    match (&config.shell, &config.shell_args) {\n      (Some(shell), Some(shell_args)) => (shell, shell_args.iter().map(String::as_ref).collect()),\n      (Some(shell), None) => (shell, DEFAULT_SHELL_ARGS.to_vec()),\n      (None, Some(shell_args)) => (\n        DEFAULT_SHELL,\n        shell_args.iter().map(String::as_ref).collect(),\n      ),\n      (None, None) => {\n        if let (true, Some(shell)) = (cfg!(windows), &self.windows_shell) {\n          (\n            shell.command.as_ref(),\n            shell.arguments.iter().map(AsRef::as_ref).collect(),\n          )\n        } else if cfg!(windows) && self.windows_powershell {\n          (WINDOWS_POWERSHELL_SHELL, WINDOWS_POWERSHELL_ARGS.to_vec())\n        } else if let Some(shell) = &self.shell {\n          (\n            shell.command.as_ref(),\n            shell.arguments.iter().map(AsRef::as_ref).collect(),\n          )\n        } else {\n          (DEFAULT_SHELL, DEFAULT_SHELL_ARGS.to_vec())\n        }\n      }\n    }\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn default_shell() {\n    let settings = Settings::default();\n\n    let config = Config {\n      shell_command: false,\n      ..testing::config(&[])\n    };\n\n    assert_eq!(settings.shell(&config), (\"sh\", vec![\"-cu\"]));\n  }\n\n  #[test]\n  fn default_shell_powershell() {\n    let settings = Settings {\n      windows_powershell: true,\n      ..Default::default()\n    };\n\n    let config = Config {\n      shell_command: false,\n      ..testing::config(&[])\n    };\n\n    if cfg!(windows) {\n      assert_eq!(\n        settings.shell(&config),\n        (\"powershell.exe\", vec![\"-NoLogo\", \"-Command\"])\n      );\n    } else {\n      assert_eq!(settings.shell(&config), (\"sh\", vec![\"-cu\"]));\n    }\n  }\n\n  #[test]\n  fn overwrite_shell() {\n    let settings = Settings::default();\n\n    let config = Config {\n      shell_command: true,\n      shell: Some(\"lol\".to_string()),\n      shell_args: Some(vec![\"-nice\".to_string()]),\n      ..testing::config(&[])\n    };\n\n    assert_eq!(settings.shell(&config), (\"lol\", vec![\"-nice\"]));\n  }\n\n  #[test]\n  fn overwrite_shell_powershell() {\n    let settings = Settings {\n      windows_powershell: true,\n      ..Default::default()\n    };\n\n    let config = Config {\n      shell_command: true,\n      shell: Some(\"lol\".to_string()),\n      shell_args: Some(vec![\"-nice\".to_string()]),\n      ..testing::config(&[])\n    };\n\n    assert_eq!(settings.shell(&config), (\"lol\", vec![\"-nice\"]));\n  }\n\n  #[test]\n  fn shell_cooked() {\n    let settings = Settings {\n      shell: Some(Interpreter {\n        command: \"asdf.exe\".into(),\n        arguments: vec![\"-nope\".into()],\n      }),\n      ..Default::default()\n    };\n\n    let config = Config {\n      shell_command: false,\n      ..testing::config(&[])\n    };\n\n    assert_eq!(settings.shell(&config), (\"asdf.exe\", vec![\"-nope\"]));\n  }\n\n  #[test]\n  fn shell_present_but_not_shell_args() {\n    let settings = Settings {\n      windows_powershell: true,\n      ..Default::default()\n    };\n\n    let config = Config {\n      shell: Some(\"lol\".to_string()),\n      ..testing::config(&[])\n    };\n\n    assert_eq!(settings.shell(&config).0, \"lol\");\n  }\n\n  #[test]\n  fn shell_args_present_but_not_shell() {\n    let settings = Settings {\n      windows_powershell: true,\n      ..Default::default()\n    };\n\n    let config = Config {\n      shell_command: false,\n      shell_args: Some(vec![\"-nice\".to_string()]),\n      ..testing::config(&[])\n    };\n\n    assert_eq!(settings.shell(&config), (\"sh\", vec![\"-nice\"]));\n  }\n}\n"
  },
  {
    "path": "src/shebang.rs",
    "content": "#[derive(Copy, Clone)]\npub(crate) struct Shebang<'line> {\n  pub(crate) argument: Option<&'line str>,\n  pub(crate) interpreter: &'line str,\n}\n\nimpl<'line> Shebang<'line> {\n  pub(crate) fn new(line: &'line str) -> Option<Self> {\n    if !line.starts_with(\"#!\") {\n      return None;\n    }\n\n    let mut pieces = line[2..]\n      .lines()\n      .next()\n      .unwrap_or(\"\")\n      .trim()\n      .splitn(2, [' ', '\\t']);\n\n    let interpreter = pieces.next().unwrap_or(\"\");\n    let argument = pieces.next();\n\n    if interpreter.is_empty() {\n      return None;\n    }\n\n    Some(Self {\n      argument,\n      interpreter,\n    })\n  }\n\n  pub(crate) fn interpreter_filename(&self) -> &str {\n    self\n      .interpreter\n      .split(['/', '\\\\'])\n      .next_back()\n      .unwrap_or(self.interpreter)\n  }\n\n  pub(crate) fn include_shebang_line(&self) -> bool {\n    !(cfg!(windows) || matches!(self.interpreter_filename(), \"cmd\" | \"cmd.exe\"))\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::Shebang;\n\n  #[test]\n  fn split_shebang() {\n    fn check(text: &str, expected_split: Option<(&str, Option<&str>)>) {\n      let shebang = Shebang::new(text);\n      assert_eq!(\n        shebang.map(|shebang| (shebang.interpreter, shebang.argument)),\n        expected_split\n      );\n    }\n\n    check(\"#!    \", None);\n    check(\"#!\", None);\n    check(\"#!/bin/bash\", Some((\"/bin/bash\", None)));\n    check(\"#!/bin/bash    \", Some((\"/bin/bash\", None)));\n    check(\n      \"#!/usr/bin/env python\",\n      Some((\"/usr/bin/env\", Some(\"python\"))),\n    );\n    check(\n      \"#!/usr/bin/env python   \",\n      Some((\"/usr/bin/env\", Some(\"python\"))),\n    );\n    check(\n      \"#!/usr/bin/env python -x\",\n      Some((\"/usr/bin/env\", Some(\"python -x\"))),\n    );\n    check(\n      \"#!/usr/bin/env python   -x\",\n      Some((\"/usr/bin/env\", Some(\"python   -x\"))),\n    );\n    check(\n      \"#!/usr/bin/env python \\t-x\\t\",\n      Some((\"/usr/bin/env\", Some(\"python \\t-x\"))),\n    );\n    check(\"#/usr/bin/env python \\t-x\\t\", None);\n    check(\"#!  /bin/bash\", Some((\"/bin/bash\", None)));\n    check(\"#!\\t\\t/bin/bash    \", Some((\"/bin/bash\", None)));\n    check(\n      \"#!  \\t\\t/usr/bin/env python\",\n      Some((\"/usr/bin/env\", Some(\"python\"))),\n    );\n    check(\n      \"#!  /usr/bin/env python   \",\n      Some((\"/usr/bin/env\", Some(\"python\"))),\n    );\n    check(\n      \"#!  /usr/bin/env python -x\",\n      Some((\"/usr/bin/env\", Some(\"python -x\"))),\n    );\n    check(\n      \"#!  /usr/bin/env python   -x\",\n      Some((\"/usr/bin/env\", Some(\"python   -x\"))),\n    );\n    check(\n      \"#!  /usr/bin/env python \\t-x\\t\",\n      Some((\"/usr/bin/env\", Some(\"python \\t-x\"))),\n    );\n    check(\"#  /usr/bin/env python \\t-x\\t\", None);\n  }\n\n  #[test]\n  fn interpreter_filename_with_forward_slash() {\n    assert_eq!(\n      Shebang::new(\"#!/foo/bar/baz\")\n        .unwrap()\n        .interpreter_filename(),\n      \"baz\"\n    );\n  }\n\n  #[test]\n  fn interpreter_filename_with_backslash() {\n    assert_eq!(\n      Shebang::new(\"#!\\\\foo\\\\bar\\\\baz\")\n        .unwrap()\n        .interpreter_filename(),\n      \"baz\"\n    );\n  }\n\n  #[test]\n  fn dont_include_shebang_line_cmd() {\n    assert!(!Shebang::new(\"#!cmd\").unwrap().include_shebang_line());\n  }\n\n  #[test]\n  fn dont_include_shebang_line_cmd_exe() {\n    assert!(!Shebang::new(\"#!cmd.exe /C\").unwrap().include_shebang_line());\n  }\n\n  #[test]\n  fn include_shebang_line_other() {\n    assert_eq!(\n      Shebang::new(\"#!foo -c\").unwrap().include_shebang_line(),\n      !cfg!(windows),\n    );\n  }\n}\n"
  },
  {
    "path": "src/show_whitespace.rs",
    "content": "use super::*;\n\n/// String wrapper that uses nonblank characters to display spaces and tabs\npub(crate) struct ShowWhitespace<'str>(pub &'str str);\n\nimpl Display for ShowWhitespace<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    for c in self.0.chars() {\n      match c {\n        '\\t' => write!(f, \"␉\")?,\n        ' ' => write!(f, \"␠\")?,\n        _ => write!(f, \"{c}\")?,\n      }\n    }\n\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "src/sigil.rs",
    "content": "#[derive(Eq, Ord, PartialEq, PartialOrd)]\npub(crate) enum Sigil {\n  Guard,\n  Infallible,\n  Quiet,\n}\n"
  },
  {
    "path": "src/signal.rs",
    "content": "use super::*;\n\n#[derive(Clone, Copy, Debug, PartialEq)]\n#[repr(i32)]\npub(crate) enum Signal {\n  Hangup = 1,\n  #[cfg(any(\n    target_os = \"dragonfly\",\n    target_os = \"freebsd\",\n    target_os = \"ios\",\n    target_os = \"macos\",\n    target_os = \"netbsd\",\n    target_os = \"openbsd\",\n  ))]\n  Info = 29,\n  Interrupt = 2,\n  Quit = 3,\n  Terminate = 15,\n}\n\nimpl Signal {\n  #[cfg(not(windows))]\n  pub(crate) const ALL: &'static [Self] = &[\n    Self::Hangup,\n    #[cfg(any(\n      target_os = \"dragonfly\",\n      target_os = \"freebsd\",\n      target_os = \"ios\",\n      target_os = \"macos\",\n      target_os = \"netbsd\",\n      target_os = \"openbsd\",\n    ))]\n    Self::Info,\n    Self::Interrupt,\n    Self::Quit,\n    Self::Terminate,\n  ];\n\n  pub(crate) fn code(self) -> i32 {\n    128i32.checked_add(self.number()).unwrap()\n  }\n\n  pub(crate) fn is_fatal(self) -> bool {\n    match self {\n      Self::Hangup | Self::Interrupt | Self::Quit | Self::Terminate => true,\n      #[cfg(any(\n        target_os = \"dragonfly\",\n        target_os = \"freebsd\",\n        target_os = \"ios\",\n        target_os = \"macos\",\n        target_os = \"netbsd\",\n        target_os = \"openbsd\",\n      ))]\n      Self::Info => false,\n    }\n  }\n\n  pub(crate) fn number(self) -> i32 {\n    self as libc::c_int\n  }\n}\n\nimpl Display for Signal {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    write!(\n      f,\n      \"{}\",\n      match self {\n        Self::Hangup => \"SIGHUP\",\n        #[cfg(any(\n          target_os = \"dragonfly\",\n          target_os = \"freebsd\",\n          target_os = \"ios\",\n          target_os = \"macos\",\n          target_os = \"netbsd\",\n          target_os = \"openbsd\",\n        ))]\n        Self::Info => \"SIGINFO\",\n        Self::Interrupt => \"SIGINT\",\n        Self::Quit => \"SIGQUIT\",\n        Self::Terminate => \"SIGTERM\",\n      }\n    )\n  }\n}\n\n#[cfg(not(windows))]\nimpl From<Signal> for nix::sys::signal::Signal {\n  fn from(signal: Signal) -> Self {\n    match signal {\n      Signal::Hangup => Self::SIGHUP,\n      #[cfg(any(\n        target_os = \"dragonfly\",\n        target_os = \"freebsd\",\n        target_os = \"ios\",\n        target_os = \"macos\",\n        target_os = \"netbsd\",\n        target_os = \"openbsd\",\n      ))]\n      Signal::Info => Self::SIGINFO,\n      Signal::Interrupt => Self::SIGINT,\n      Signal::Quit => Self::SIGQUIT,\n      Signal::Terminate => Self::SIGTERM,\n    }\n  }\n}\n\nimpl TryFrom<u8> for Signal {\n  type Error = io::Error;\n\n  fn try_from(n: u8) -> Result<Self, Self::Error> {\n    match n {\n      1 => Ok(Self::Hangup),\n      #[cfg(any(\n        target_os = \"dragonfly\",\n        target_os = \"freebsd\",\n        target_os = \"ios\",\n        target_os = \"macos\",\n        target_os = \"netbsd\",\n        target_os = \"openbsd\",\n      ))]\n      29 => Ok(Self::Info),\n      2 => Ok(Self::Interrupt),\n      3 => Ok(Self::Quit),\n      15 => Ok(Self::Terminate),\n      _ => Err(io::Error::other(format!(\"unexpected signal: {n}\"))),\n    }\n  }\n}\n\n#[cfg(test)]\n#[cfg(not(windows))]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn signals_fit_in_u8() {\n    for signal in Signal::ALL {\n      assert!(signal.number() <= i32::from(u8::MAX));\n    }\n  }\n\n  #[test]\n  fn signals_have_valid_exit_codes() {\n    for signal in Signal::ALL {\n      signal.code();\n    }\n  }\n\n  #[test]\n  fn signal_numbers_are_correct() {\n    for &signal in Signal::ALL {\n      let n = match signal {\n        Signal::Hangup => libc::SIGHUP,\n        #[cfg(any(\n          target_os = \"dragonfly\",\n          target_os = \"freebsd\",\n          target_os = \"ios\",\n          target_os = \"macos\",\n          target_os = \"netbsd\",\n          target_os = \"openbsd\",\n        ))]\n        Signal::Info => libc::SIGINFO,\n        Signal::Interrupt => libc::SIGINT,\n        Signal::Quit => libc::SIGQUIT,\n        Signal::Terminate => libc::SIGTERM,\n      };\n\n      assert_eq!(signal as i32, n);\n\n      assert_eq!(Signal::try_from(u8::try_from(n).unwrap()).unwrap(), signal);\n    }\n  }\n}\n"
  },
  {
    "path": "src/signal_handler.rs",
    "content": "use super::*;\n\npub(crate) struct SignalHandler {\n  caught: Option<Signal>,\n  children: BTreeMap<i32, Command>,\n  initialized: bool,\n  verbosity: Verbosity,\n}\n\nimpl SignalHandler {\n  pub(crate) fn install(verbosity: Verbosity) -> RunResult<'static> {\n    let mut instance = Self::instance();\n    instance.verbosity = verbosity;\n    if !instance.initialized {\n      Platform::install_signal_handler(|signal| Self::instance().handle(signal))?;\n      instance.initialized = true;\n    }\n    Ok(())\n  }\n\n  pub(crate) fn instance() -> MutexGuard<'static, Self> {\n    static INSTANCE: Mutex<SignalHandler> = Mutex::new(SignalHandler::new());\n\n    match INSTANCE.lock() {\n      Ok(guard) => guard,\n      Err(poison_error) => {\n        eprintln!(\n          \"{}\",\n          Error::internal(format!(\"signal handler mutex poisoned: {poison_error}\"),)\n            .color_display(Color::auto().stderr())\n        );\n        process::exit(EXIT_FAILURE);\n      }\n    }\n  }\n\n  const fn new() -> Self {\n    Self {\n      caught: None,\n      children: BTreeMap::new(),\n      initialized: false,\n      verbosity: Verbosity::default(),\n    }\n  }\n\n  fn handle(&mut self, signal: Signal) {\n    if signal.is_fatal() {\n      if self.children.is_empty() {\n        process::exit(signal.code());\n      }\n\n      if self.caught.is_none() {\n        self.caught = Some(signal);\n      }\n    }\n\n    match signal {\n      // SIGHUP, SIGINT, and SIGQUIT are normally sent on terminal close,\n      // ctrl-c, and ctrl-\\, respectively, and are sent to all processes in the\n      // foreground process group. this includes child processes, so we ignore\n      // the signal and wait for them to exit\n      Signal::Hangup | Signal::Interrupt | Signal::Quit => {}\n      #[cfg(any(\n        target_os = \"dragonfly\",\n        target_os = \"freebsd\",\n        target_os = \"ios\",\n        target_os = \"macos\",\n        target_os = \"netbsd\",\n        target_os = \"openbsd\",\n      ))]\n      Signal::Info => {\n        let id = process::id();\n        if self.children.is_empty() {\n          eprintln!(\"just {id}: no child processes\");\n        } else {\n          let n = self.children.len();\n\n          let mut message = format!(\n            \"just {id}: {n} child {}:\\n\",\n            if n == 1 { \"process\" } else { \"processes\" }\n          );\n\n          for (&child, command) in &self.children {\n            use std::fmt::Write;\n            writeln!(message, \"{child}: {command:?}\").unwrap();\n          }\n\n          eprint!(\"{message}\");\n        }\n      }\n      // SIGTERM is the default signal sent by kill. forward it to child\n      // processes and wait for them to exit\n      Signal::Terminate =>\n      {\n        #[cfg(not(windows))]\n        for &child in self.children.keys() {\n          if self.verbosity.loquacious() {\n            eprintln!(\"just: sending SIGTERM to child process {child}\");\n          }\n          nix::sys::signal::kill(\n            nix::unistd::Pid::from_raw(child),\n            Some(Signal::Terminate.into()),\n          )\n          .ok();\n        }\n      }\n    }\n  }\n\n  pub(crate) fn spawn<T>(\n    mut command: Command,\n    f: impl Fn(process::Child) -> io::Result<T>,\n  ) -> (io::Result<T>, Option<Signal>) {\n    let mut instance = Self::instance();\n\n    let child = match command.spawn() {\n      Err(err) => return (Err(err), None),\n      Ok(child) => child,\n    };\n\n    let pid = match child.id().try_into() {\n      Err(err) => {\n        return (\n          Err(io::Error::other(format!(\"invalid child PID: {err}\"))),\n          None,\n        );\n      }\n      Ok(pid) => pid,\n    };\n\n    instance.children.insert(pid, command);\n\n    drop(instance);\n\n    let result = f(child);\n\n    let mut instance = Self::instance();\n\n    instance.children.remove(&pid);\n\n    (result, instance.caught)\n  }\n}\n"
  },
  {
    "path": "src/signals.rs",
    "content": "use {\n  super::*,\n  nix::{\n    errno::Errno,\n    fcntl::{FcntlArg, FdFlag},\n    sys::signal::{SaFlags, SigAction, SigHandler, SigSet},\n  },\n  std::{\n    fs::File,\n    io::Read,\n    os::fd::{BorrowedFd, IntoRawFd, OwnedFd},\n    sync::atomic::{self, AtomicI32},\n  },\n};\n\nconst INVALID_FILENO: i32 = -1;\n\nstatic WRITE: AtomicI32 = AtomicI32::new(INVALID_FILENO);\n\nfn die(message: &str) -> ! {\n  // SAFETY:\n  //\n  // Standard error is open for the duration of the program.\n  const STDERR: BorrowedFd = unsafe { BorrowedFd::borrow_raw(libc::STDERR_FILENO) };\n\n  let mut i = 0;\n  let mut buffer = [0; 512];\n\n  let mut append = |s: &str| {\n    let remaining = buffer.len() - i;\n    let n = s.len().min(remaining);\n    let end = i + n;\n    buffer[i..end].copy_from_slice(&s.as_bytes()[0..n]);\n    i = end;\n  };\n\n  append(\"error: \");\n  append(message);\n  append(\"\\n\");\n\n  nix::unistd::write(STDERR, &buffer[0..i]).ok();\n\n  process::abort();\n}\n\nextern \"C\" fn handler(signal: libc::c_int) {\n  let errno = Errno::last();\n\n  let Ok(signal) = u8::try_from(signal) else {\n    die(\"unexpected signal\");\n  };\n\n  // SAFETY:\n  //\n  // `WRITE` is initialized before the signal handler can run and remains open\n  // for the duration of the program.\n  let fd = unsafe { BorrowedFd::borrow_raw(WRITE.load(atomic::Ordering::Relaxed)) };\n\n  if let Err(err) = nix::unistd::write(fd, &[signal]) {\n    die(err.desc());\n  }\n\n  errno.set();\n}\n\nfn fcntl(fd: &OwnedFd, arg: FcntlArg) -> RunResult<'static, libc::c_int> {\n  nix::fcntl::fcntl(fd, arg).map_err(|errno| Error::SignalHandlerPipeCloexec {\n    io_error: errno.into(),\n  })\n}\n\nfn set_cloexec(fd: &OwnedFd) -> RunResult<'static> {\n  // It would be better to use pipe2(O_CLOEXEC) rather than pipe-then-fcntl,\n  // but it isn't supported on all platforms (most notably, not macos) and in\n  // the atomicity guarantees that pipe2 provides aren't needed.\n  let existing_flags = fcntl(fd, FcntlArg::F_GETFD)?;\n  let combined_flags = FdFlag::from_bits_retain(existing_flags) | FdFlag::FD_CLOEXEC;\n  fcntl(fd, FcntlArg::F_SETFD(combined_flags))?;\n  Ok(())\n}\n\npub(crate) struct Signals(File);\n\nimpl Signals {\n  pub(crate) fn new() -> RunResult<'static, Self> {\n    let (read, write) = nix::unistd::pipe().map_err(|errno| Error::SignalHandlerPipeOpen {\n      io_error: errno.into(),\n    })?;\n\n    set_cloexec(&read)?;\n    set_cloexec(&write)?;\n\n    if WRITE\n      .compare_exchange(\n        INVALID_FILENO,\n        write.into_raw_fd(),\n        atomic::Ordering::Relaxed,\n        atomic::Ordering::Relaxed,\n      )\n      .is_err()\n    {\n      panic!(\"signal iterator cannot be initialized twice\");\n    }\n\n    let sa = SigAction::new(\n      SigHandler::Handler(handler),\n      SaFlags::SA_RESTART,\n      SigSet::empty(),\n    );\n\n    for &signal in Signal::ALL {\n      // SAFETY:\n      //\n      // This is the only place we modify signal handlers, and\n      // `nix::sys::signal::sigaction` is unsafe only if an invalid signal\n      // handler has already been installed.\n      unsafe {\n        nix::sys::signal::sigaction(signal.into(), &sa).map_err(|errno| {\n          Error::SignalHandlerSigaction {\n            signal,\n            io_error: errno.into(),\n          }\n        })?;\n      }\n    }\n\n    Ok(Self(File::from(read)))\n  }\n}\n\nimpl Iterator for Signals {\n  type Item = io::Result<Signal>;\n\n  fn next(&mut self) -> Option<Self::Item> {\n    let mut signal = [0];\n    Some(\n      self\n        .0\n        .read_exact(&mut signal)\n        .and_then(|()| Signal::try_from(signal[0])),\n    )\n  }\n}\n"
  },
  {
    "path": "src/source.rs",
    "content": "use super::*;\n\n#[derive(Debug)]\npub(crate) struct Source<'src> {\n  pub(crate) file_depth: u32,\n  pub(crate) file_path: Vec<PathBuf>,\n  pub(crate) import_offsets: Vec<usize>,\n  pub(crate) namepath: Option<Namepath<'src>>,\n  pub(crate) path: PathBuf,\n  pub(crate) working_directory: PathBuf,\n}\n\nimpl<'src> Source<'src> {\n  pub(crate) fn root(path: &Path) -> Self {\n    Self {\n      file_depth: 0,\n      file_path: vec![path.into()],\n      import_offsets: Vec::new(),\n      namepath: None,\n      path: path.into(),\n      working_directory: path.parent().unwrap().into(),\n    }\n  }\n\n  pub(crate) fn import(&self, path: PathBuf, import_offset: usize) -> Self {\n    Self {\n      file_depth: self.file_depth + 1,\n      file_path: self\n        .file_path\n        .clone()\n        .into_iter()\n        .chain(iter::once(path.clone()))\n        .collect(),\n      import_offsets: self\n        .import_offsets\n        .iter()\n        .copied()\n        .chain(iter::once(import_offset))\n        .collect(),\n      namepath: self.namepath.clone(),\n      path,\n      working_directory: self.working_directory.clone(),\n    }\n  }\n\n  pub(crate) fn module(&self, name: Name<'src>, path: PathBuf) -> Self {\n    Self {\n      file_depth: self.file_depth + 1,\n      file_path: self\n        .file_path\n        .clone()\n        .into_iter()\n        .chain(iter::once(path.clone()))\n        .collect(),\n      import_offsets: Vec::new(),\n      namepath: Some(\n        self\n          .namepath\n          .as_ref()\n          .map_or_else(|| name.into(), |namepath| namepath.join(name)),\n      ),\n      path: path.clone(),\n      working_directory: path.parent().unwrap().into(),\n    }\n  }\n}\n"
  },
  {
    "path": "src/string_delimiter.rs",
    "content": "#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]\npub(crate) enum StringDelimiter {\n  Backtick,\n  QuoteDouble,\n  QuoteSingle,\n}\n"
  },
  {
    "path": "src/string_kind.rs",
    "content": "use super::*;\n\n#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]\npub(crate) struct StringKind {\n  pub(crate) delimiter: StringDelimiter,\n  pub(crate) indented: bool,\n}\n\nimpl StringKind {\n  // Indented values must come before un-indented values, or else\n  // `Self::from_token_start` will incorrectly return indented = false\n  // for indented strings.\n  const ALL: &'static [Self] = &[\n    Self::new(StringDelimiter::Backtick, true),\n    Self::new(StringDelimiter::Backtick, false),\n    Self::new(StringDelimiter::QuoteDouble, true),\n    Self::new(StringDelimiter::QuoteDouble, false),\n    Self::new(StringDelimiter::QuoteSingle, true),\n    Self::new(StringDelimiter::QuoteSingle, false),\n  ];\n\n  const fn new(delimiter: StringDelimiter, indented: bool) -> Self {\n    Self {\n      delimiter,\n      indented,\n    }\n  }\n\n  pub(crate) fn delimiter(self) -> &'static str {\n    match (self.delimiter, self.indented) {\n      (StringDelimiter::Backtick, false) => \"`\",\n      (StringDelimiter::Backtick, true) => \"```\",\n      (StringDelimiter::QuoteDouble, false) => \"\\\"\",\n      (StringDelimiter::QuoteDouble, true) => \"\\\"\\\"\\\"\",\n      (StringDelimiter::QuoteSingle, false) => \"'\",\n      (StringDelimiter::QuoteSingle, true) => \"'''\",\n    }\n  }\n\n  pub(crate) fn delimiter_len(self) -> usize {\n    self.delimiter().len()\n  }\n\n  pub(crate) fn token_kind(self) -> TokenKind {\n    match self.delimiter {\n      StringDelimiter::QuoteDouble | StringDelimiter::QuoteSingle => TokenKind::StringToken,\n      StringDelimiter::Backtick => TokenKind::Backtick,\n    }\n  }\n\n  pub(crate) fn unterminated_error_kind(self) -> CompileErrorKind<'static> {\n    match self.delimiter {\n      StringDelimiter::QuoteDouble | StringDelimiter::QuoteSingle => {\n        CompileErrorKind::UnterminatedString\n      }\n      StringDelimiter::Backtick => CompileErrorKind::UnterminatedBacktick,\n    }\n  }\n\n  pub(crate) fn processes_escape_sequences(self) -> bool {\n    match self.delimiter {\n      StringDelimiter::QuoteDouble => true,\n      StringDelimiter::Backtick | StringDelimiter::QuoteSingle => false,\n    }\n  }\n\n  pub(crate) fn indented(self) -> bool {\n    self.indented\n  }\n\n  pub(crate) fn from_string_or_backtick(token: Token) -> CompileResult<Self> {\n    Self::from_token_start(token.lexeme()).ok_or_else(|| {\n      token.error(CompileErrorKind::Internal {\n        message: \"StringKind::from_token: expected string or backtick\".to_owned(),\n      })\n    })\n  }\n\n  pub(crate) fn from_token_start(token_start: &str) -> Option<Self> {\n    Self::ALL\n      .iter()\n      .find(|&&kind| token_start.starts_with(kind.delimiter()))\n      .copied()\n  }\n}\n"
  },
  {
    "path": "src/string_literal.rs",
    "content": "use super::*;\n\n#[derive(PartialEq, Debug, Clone, Ord, Eq, PartialOrd)]\npub(crate) struct StringLiteral<'src> {\n  pub(crate) cooked: String,\n  pub(crate) expand: bool,\n  pub(crate) kind: StringKind,\n  pub(crate) part: Option<FormatStringPart>,\n  pub(crate) token: Token<'src>,\n}\n\nimpl Display for StringLiteral<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    if self.expand {\n      write!(f, \"x\")?;\n    }\n\n    if let Some(FormatStringPart::Start | FormatStringPart::Single) = self.part {\n      write!(f, \"f\")?;\n    }\n\n    write!(f, \"{}\", self.token.lexeme())\n  }\n}\n\nimpl Serialize for StringLiteral<'_> {\n  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n  where\n    S: Serializer,\n  {\n    serializer.serialize_str(&self.cooked)\n  }\n}\n"
  },
  {
    "path": "src/string_state.rs",
    "content": "use super::*;\n\npub(crate) enum StringState {\n  FormatContinue(StringKind),\n  FormatStart,\n  Normal,\n}\n"
  },
  {
    "path": "src/subcommand.rs",
    "content": "use {super::*, clap_mangen::Man};\n\npub const INIT_JUSTFILE: &str = \"\\\n# https://just.systems\n\ndefault:\n    echo 'Hello, world!'\n\";\n\nstatic BACKTICK_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(\"(`.*?`)|(`[^`]*$)\").unwrap());\n\nconst CHOOSER_CANCELLED_EXIT_STATUS: i32 = 130;\n\n#[derive(PartialEq, Clone, Debug)]\npub(crate) enum Subcommand {\n  Changelog,\n  Choose {\n    chooser: Option<String>,\n  },\n  Command {\n    arguments: Vec<OsString>,\n    binary: OsString,\n  },\n  Completions {\n    shell: completions::Shell,\n  },\n  Dump {\n    format: DumpFormat,\n  },\n  Edit,\n  Evaluate {\n    variable: Option<String>,\n  },\n  Format,\n  Groups,\n  Init,\n  List {\n    path: Modulepath,\n  },\n  Man,\n  Request {\n    request: Request,\n  },\n  Run {\n    arguments: Vec<String>,\n  },\n  Show {\n    path: Modulepath,\n  },\n  Summary,\n  Usage {\n    path: Modulepath,\n  },\n  Variables,\n}\n\nimpl Default for Subcommand {\n  fn default() -> Self {\n    Self::Run {\n      arguments: Vec::new(),\n    }\n  }\n}\n\nimpl Subcommand {\n  pub(crate) fn execute<'src>(&self, config: &Config, loader: &'src Loader) -> RunResult<'src> {\n    use Subcommand::*;\n\n    match self {\n      Changelog => {\n        Self::changelog();\n        return Ok(());\n      }\n      Completions { shell } => {\n        Self::completions(*shell);\n        return Ok(());\n      }\n      Init => return Self::init(config),\n      Man => return Self::man(),\n      Request { request } => return Self::request(request),\n      _ => {}\n    }\n\n    let search = Search::find(\n      config.ceiling.as_deref(),\n      &config.invocation_directory,\n      &config.search_config,\n    )?;\n\n    if matches!(self, Edit) {\n      return Self::edit(&search);\n    }\n\n    if matches!(self, Format) {\n      return Self::format(config, loader, &search);\n    }\n\n    let compilation = Self::compile(config, loader, &search)?;\n    let justfile = &compilation.justfile;\n\n    match self {\n      Choose { chooser } => {\n        Self::choose(\n          chooser.as_deref(),\n          config,\n          justfile,\n          &compilation.overrides,\n          &search,\n        )?;\n      }\n      Command { .. } | Evaluate { .. } => {\n        justfile.run(config, &search, &[], &compilation.overrides)?;\n      }\n      Dump { format } => Self::dump(compilation, *format)?,\n      Groups => Self::groups(config, justfile),\n      List { path } => Self::list(config, justfile, path)?,\n      Run { arguments } => Self::run(config, loader, search, compilation, arguments)?,\n      Show { path } => Self::show(config, justfile, path)?,\n      Summary => Self::summary(config, justfile),\n      Usage { path } => Self::usage(config, justfile, path)?,\n      Variables => Self::variables(justfile),\n      Changelog | Completions { .. } | Edit | Format | Init | Man | Request { .. } => {\n        unreachable!()\n      }\n    }\n\n    Ok(())\n  }\n\n  fn groups(config: &Config, justfile: &Justfile) {\n    println!(\"Recipe groups:\");\n    for group in justfile.public_groups(config) {\n      println!(\"{}{group}\", config.list_prefix);\n    }\n  }\n\n  fn run<'src>(\n    config: &Config,\n    loader: &'src Loader,\n    mut search: Search,\n    mut compilation: Compilation<'src>,\n    arguments: &[String],\n  ) -> RunResult<'src> {\n    let starting_parent = search.justfile.parent().as_ref().unwrap().lexiclean();\n\n    loop {\n      let justfile = &compilation.justfile;\n      let fallback = justfile.settings.fallback\n        && matches!(\n          config.search_config,\n          SearchConfig::FromInvocationDirectory | SearchConfig::FromSearchDirectory { .. }\n        );\n\n      let result = justfile.run(config, &search, arguments, &compilation.overrides);\n\n      if fallback {\n        if let Err(err @ (Error::UnknownRecipe { .. } | Error::UnknownSubmodule { .. })) = result {\n          search = search\n            .search_parent_directory(config.ceiling.as_deref())\n            .map_err(|_| err)?;\n\n          if config.verbosity.loquacious() {\n            eprintln!(\n              \"Trying {}\",\n              starting_parent\n                .strip_prefix(search.justfile.parent().unwrap())\n                .unwrap()\n                .components()\n                .map(|_| path::Component::ParentDir)\n                .collect::<PathBuf>()\n                .join(search.justfile.file_name().unwrap())\n                .display()\n            );\n          }\n\n          compilation = Self::compile(config, loader, &search)?;\n\n          continue;\n        }\n      }\n\n      if config.allow_missing\n        && matches!(\n          result,\n          Err(Error::UnknownRecipe { .. } | Error::UnknownSubmodule { .. })\n        )\n      {\n        return Ok(());\n      }\n\n      return result;\n    }\n  }\n\n  fn compile<'src>(\n    config: &Config,\n    loader: &'src Loader,\n    search: &Search,\n  ) -> RunResult<'src, Compilation<'src>> {\n    let compilation = Compiler::compile(config, loader, &search.justfile)?;\n\n    compilation.justfile.check_unstable(config)?;\n\n    if config.verbosity.loud() {\n      for warning in &compilation.justfile.warnings {\n        eprintln!(\"{}\", warning.color_display(config.color.stderr()));\n      }\n    }\n\n    Ok(compilation)\n  }\n\n  fn changelog() {\n    write!(io::stdout(), \"{}\", include_str!(\"../CHANGELOG.md\")).ok();\n  }\n\n  fn choose<'src>(\n    chooser: Option<&str>,\n    config: &Config,\n    justfile: &Justfile<'src>,\n    overrides: &HashMap<Number, String>,\n    search: &Search,\n  ) -> RunResult<'src> {\n    let mut recipes = Vec::<&Recipe>::new();\n    let mut stack = vec![justfile];\n    while let Some(module) = stack.pop() {\n      recipes.extend(\n        module\n          .public_recipes(config)\n          .iter()\n          .filter(|recipe| recipe.min_arguments() == 0),\n      );\n      stack.extend(module.modules.values());\n    }\n\n    if recipes.is_empty() {\n      return Err(Error::NoChoosableRecipes);\n    }\n\n    let chooser = if let Some(chooser) = chooser {\n      OsString::from(chooser)\n    } else {\n      let mut chooser = OsString::new();\n      chooser.push(\"fzf --multi --preview 'just --unstable --color always --justfile \\\"\");\n      chooser.push(&search.justfile);\n      chooser.push(\"\\\" --show {}'\");\n      chooser\n    };\n\n    let result = justfile\n      .settings\n      .shell_command(config)\n      .arg(&chooser)\n      .current_dir(&search.working_directory)\n      .stdin(Stdio::piped())\n      .stdout(Stdio::piped())\n      .spawn();\n\n    let mut child = match result {\n      Ok(child) => child,\n      Err(io_error) => {\n        let (shell_binary, shell_arguments) = justfile.settings.shell(config);\n        return Err(Error::ChooserInvoke {\n          shell_binary: shell_binary.to_owned(),\n          shell_arguments: shell_arguments.join(\" \"),\n          chooser,\n          io_error,\n        });\n      }\n    };\n\n    let stdin = child.stdin.as_mut().unwrap();\n    for recipe in recipes {\n      if let Err(io_error) = writeln!(stdin, \"{}\", recipe.spaced_recipe_path()) {\n        if io_error.kind() != std::io::ErrorKind::BrokenPipe {\n          return Err(Error::ChooserWrite { io_error, chooser });\n        }\n      }\n    }\n\n    let output = match child.wait_with_output() {\n      Ok(output) => output,\n      Err(io_error) => {\n        return Err(Error::ChooserRead { io_error, chooser });\n      }\n    };\n\n    if output.status.code() == Some(CHOOSER_CANCELLED_EXIT_STATUS) {\n      return Ok(());\n    }\n\n    if !output.status.success() {\n      return Err(Error::ChooserStatus {\n        status: output.status,\n        chooser,\n      });\n    }\n\n    let stdout = String::from_utf8_lossy(&output.stdout);\n\n    let recipes = stdout\n      .split_whitespace()\n      .map(str::to_owned)\n      .collect::<Vec<String>>();\n\n    justfile.run(config, search, &recipes, overrides)\n  }\n\n  fn completions(shell: completions::Shell) {\n    print!(\"{}\", shell.script());\n  }\n\n  fn dump(compilation: Compilation, format: DumpFormat) -> RunResult<'static> {\n    match format {\n      DumpFormat::Json => {\n        serde_json::to_writer(io::stdout(), &compilation.justfile)\n          .map_err(|source| Error::DumpJson { source })?;\n        println!();\n      }\n      DumpFormat::Just => print!(\"{}\", compilation.root_ast()),\n    }\n    Ok(())\n  }\n\n  fn edit(search: &Search) -> RunResult<'static> {\n    let editor = env::var_os(\"VISUAL\")\n      .or_else(|| env::var_os(\"EDITOR\"))\n      .unwrap_or_else(|| \"vim\".into());\n\n    let error = Command::new(&editor)\n      .current_dir(&search.working_directory)\n      .arg(&search.justfile)\n      .status();\n\n    let status = match error {\n      Err(io_error) => return Err(Error::EditorInvoke { editor, io_error }),\n      Ok(status) => status,\n    };\n\n    if !status.success() {\n      return Err(Error::EditorStatus { editor, status });\n    }\n\n    Ok(())\n  }\n\n  fn format<'src>(config: &Config, loader: &'src Loader, search: &Search) -> RunResult<'src> {\n    let root = search.justfile.parent().unwrap();\n\n    let (path, src) = loader.load(root, &search.justfile)?;\n\n    let ast = Parser::parse_source(\n      &mut Numerator::new(),\n      path,\n      &Source::root(&search.justfile),\n      src,\n    )?;\n\n    let unstable = config.unstable\n      || ast.items.iter().any(|item| {\n        matches!(\n          item,\n          Item::Set(Set {\n            value: Setting::Unstable(true),\n            ..\n          })\n        )\n      });\n\n    if !unstable {\n      return Err(Error::UnstableFeature {\n        unstable_feature: UnstableFeature::FormatSubcommand,\n      });\n    }\n\n    let formatted = ast.to_string();\n\n    if formatted == src {\n      return Ok(());\n    }\n\n    if config.check {\n      if !config.verbosity.quiet() {\n        use similar::{ChangeTag, TextDiff};\n\n        let diff = TextDiff::configure()\n          .algorithm(similar::Algorithm::Patience)\n          .diff_lines(src, &formatted);\n\n        for op in diff.ops() {\n          for change in diff.iter_changes(op) {\n            let (symbol, color) = match change.tag() {\n              ChangeTag::Delete => (\"-\", config.color.stdout().diff_deleted()),\n              ChangeTag::Equal => (\" \", config.color.stdout()),\n              ChangeTag::Insert => (\"+\", config.color.stdout().diff_added()),\n            };\n\n            print!(\"{}{symbol}{change}{}\", color.prefix(), color.suffix());\n          }\n        }\n      }\n\n      Err(Error::FormatCheckFoundDiff)\n    } else {\n      fs::write(&search.justfile, formatted).map_err(|io_error| Error::WriteJustfile {\n        justfile: search.justfile.clone(),\n        io_error,\n      })?;\n\n      if config.verbosity.loud() {\n        eprintln!(\"Wrote justfile to `{}`\", search.justfile.display());\n      }\n\n      Ok(())\n    }\n  }\n\n  fn init(config: &Config) -> RunResult<'static> {\n    let search = Search::init(\n      &config.search_config,\n      &config.invocation_directory,\n      config.ceiling.as_deref(),\n    )?;\n\n    if filesystem::is_file(&search.justfile)? {\n      return Err(Error::InitExists {\n        justfile: search.justfile,\n      });\n    }\n\n    if let Err(io_error) = fs::write(&search.justfile, INIT_JUSTFILE) {\n      return Err(Error::WriteJustfile {\n        justfile: search.justfile,\n        io_error,\n      });\n    }\n\n    if config.verbosity.loud() {\n      eprintln!(\"Wrote justfile to `{}`\", search.justfile.display());\n    }\n\n    Ok(())\n  }\n\n  fn man() -> RunResult<'static> {\n    let mut buffer = Vec::<u8>::new();\n\n    Man::new(Config::app())\n      .render(&mut buffer)\n      .expect(\"writing to buffer cannot fail\");\n\n    let mut stdout = io::stdout().lock();\n\n    stdout\n      .write_all(&buffer)\n      .map_err(|io_error| Error::StdoutIo { io_error })?;\n\n    stdout\n      .flush()\n      .map_err(|io_error| Error::StdoutIo { io_error })?;\n\n    Ok(())\n  }\n\n  fn request(request: &Request) -> RunResult<'static> {\n    let response = match request {\n      Request::EnvironmentVariable(key) => Response::EnvironmentVariable(env::var_os(key)),\n      #[cfg(not(windows))]\n      Request::Signal => {\n        let sigset = nix::sys::signal::SigSet::all();\n\n        sigset.thread_block().unwrap();\n\n        let received = sigset.wait().unwrap();\n\n        Response::Signal(received.as_str().into())\n      }\n    };\n\n    serde_json::to_writer(io::stdout(), &response).map_err(|source| Error::DumpJson { source })?;\n\n    Ok(())\n  }\n\n  fn list(config: &Config, mut module: &Justfile, path: &Modulepath) -> RunResult<'static> {\n    for name in &path.path {\n      module = module\n        .modules\n        .get(name)\n        .ok_or_else(|| Error::UnknownSubmodule {\n          path: path.to_string(),\n        })?;\n    }\n\n    Self::list_module(config, 0, &config.groups, module)?;\n\n    Ok(())\n  }\n\n  fn list_module(\n    config: &Config,\n    depth: usize,\n    groups: &[String],\n    module: &Justfile,\n  ) -> RunResult<'static> {\n    fn print_doc_and_aliases(\n      config: &Config,\n      name: &str,\n      doc: Option<&str>,\n      aliases: &[&str],\n      max_signature_width: usize,\n      signature_widths: &BTreeMap<&str, usize>,\n    ) {\n      let color = config.color.stdout();\n\n      let inline_aliases = config.alias_style != AliasStyle::Separate && !aliases.is_empty();\n\n      if inline_aliases || doc.is_some() {\n        print!(\n          \"{:padding$}{}\",\n          \"\",\n          color.doc().paint(\"#\"),\n          padding = max_signature_width.saturating_sub(signature_widths[name]) + 1,\n        );\n      }\n\n      let print_aliases = || {\n        print!(\n          \" {}\",\n          color.alias().paint(&format!(\n            \"[alias{}: {}]\",\n            if aliases.len() == 1 { \"\" } else { \"es\" },\n            aliases.join(\", \")\n          ))\n        );\n      };\n\n      if inline_aliases && config.alias_style == AliasStyle::Left {\n        print_aliases();\n      }\n\n      if let Some(doc) = doc {\n        print!(\" \");\n        let mut end = 0;\n        for backtick in BACKTICK_RE.find_iter(doc) {\n          let prefix = &doc[end..backtick.start()];\n          if !prefix.is_empty() {\n            print!(\"{}\", color.doc().paint(prefix));\n          }\n          print!(\"{}\", color.doc_backtick().paint(backtick.as_str()));\n          end = backtick.end();\n        }\n\n        let suffix = &doc[end..];\n        if !suffix.is_empty() {\n          print!(\"{}\", color.doc().paint(suffix));\n        }\n      }\n\n      if inline_aliases && config.alias_style == AliasStyle::Right {\n        print_aliases();\n      }\n\n      println!();\n    }\n\n    let aliases = if config.no_aliases {\n      BTreeMap::new()\n    } else {\n      let mut aliases = BTreeMap::<&str, Vec<&str>>::new();\n      for alias in module.aliases.values().filter(|alias| alias.is_public()) {\n        aliases\n          .entry(alias.target.name.lexeme())\n          .or_default()\n          .push(alias.name.lexeme());\n      }\n      aliases\n    };\n\n    let signature_widths = {\n      let mut signature_widths: BTreeMap<&str, usize> = BTreeMap::new();\n\n      for (name, recipe) in &module.recipes {\n        if !recipe.is_public() {\n          continue;\n        }\n\n        for name in iter::once(name).chain(aliases.get(name).unwrap_or(&Vec::new())) {\n          signature_widths.insert(\n            name,\n            UnicodeWidthStr::width(\n              RecipeSignature { name, recipe }\n                .color_display(Color::never())\n                .to_string()\n                .as_str(),\n            ),\n          );\n        }\n      }\n      if !config.list_submodules {\n        for submodule in module.public_modules(config) {\n          let name = submodule.name();\n          signature_widths.insert(name, UnicodeWidthStr::width(format!(\"{name} ...\").as_str()));\n        }\n      }\n\n      signature_widths\n    };\n\n    let max_signature_width = signature_widths\n      .values()\n      .copied()\n      .filter(|width| *width <= 50)\n      .max()\n      .unwrap_or(0);\n\n    let list_prefix = config.list_prefix.repeat(depth + 1);\n\n    if !groups.is_empty() {\n      let public_groups = module.public_groups(config);\n      for group in groups {\n        if !public_groups.contains(group) {\n          return Err(Error::UnknownGroup {\n            group: group.clone(),\n          });\n        }\n      }\n    }\n\n    if depth == 0 {\n      print!(\"{}\", config.list_heading);\n    }\n\n    let recipe_groups = {\n      let mut recipe_groups = BTreeMap::<Option<String>, Vec<&Recipe>>::new();\n      for recipe in module.public_recipes(config) {\n        let recipe_groups_list = recipe.groups();\n        if recipe_groups_list.is_empty() {\n          recipe_groups.entry(None).or_default().push(recipe);\n        } else {\n          for group in recipe_groups_list {\n            recipe_groups.entry(Some(group)).or_default().push(recipe);\n          }\n        }\n      }\n      recipe_groups\n    };\n\n    let submodule_groups = {\n      let mut submodule_groups = BTreeMap::<Option<String>, Vec<&Justfile>>::new();\n      for submodule in module.public_modules(config) {\n        let submodule_groups_list = submodule.groups();\n        if submodule_groups_list.is_empty() {\n          submodule_groups.entry(None).or_default().push(submodule);\n        } else {\n          for group in submodule_groups_list {\n            submodule_groups\n              .entry(Some(group.to_string()))\n              .or_default()\n              .push(submodule);\n          }\n        }\n      }\n      submodule_groups\n    };\n\n    let mut ordered_groups = if groups.is_empty() {\n      module\n        .public_groups(config)\n        .into_iter()\n        .map(Some)\n        .collect::<Vec<Option<String>>>()\n    } else {\n      groups\n        .iter()\n        .cloned()\n        .map(Some)\n        .collect::<Vec<Option<String>>>()\n    };\n\n    if groups.is_empty()\n      && (recipe_groups.contains_key(&None) || submodule_groups.contains_key(&None))\n    {\n      ordered_groups.insert(0, None);\n    }\n\n    let no_groups =\n      groups.is_empty() && ordered_groups.len() == 1 && ordered_groups.first() == Some(&None);\n\n    let groups_count = if no_groups { 0 } else { ordered_groups.len() };\n\n    for (i, group) in ordered_groups.into_iter().enumerate() {\n      if i > 0 {\n        println!();\n      }\n\n      if !no_groups {\n        if let Some(group) = &group {\n          println!(\n            \"{list_prefix}{}\",\n            config.color.stdout().group().paint(&format!(\"[{group}]\"))\n          );\n        }\n      }\n\n      if let Some(recipes) = recipe_groups.get(&group) {\n        for recipe in recipes {\n          let recipe_alias_entries = if config.alias_style == AliasStyle::Separate {\n            aliases.get(recipe.name())\n          } else {\n            None\n          };\n\n          for (i, name) in iter::once(&recipe.name())\n            .chain(recipe_alias_entries.unwrap_or(&Vec::new()))\n            .enumerate()\n          {\n            let doc = if i == 0 {\n              recipe.doc().map(Cow::Borrowed)\n            } else {\n              Some(Cow::Owned(format!(\"alias for `{}`\", recipe.name)))\n            };\n\n            if let Some(doc) = &doc {\n              if doc.lines().count() > 1 {\n                for line in doc.lines() {\n                  println!(\n                    \"{list_prefix}{} {}\",\n                    config.color.stdout().doc().paint(\"#\"),\n                    config.color.stdout().doc().paint(line),\n                  );\n                }\n              }\n            }\n\n            print!(\n              \"{list_prefix}{}\",\n              RecipeSignature { name, recipe }.color_display(config.color.stdout())\n            );\n\n            print_doc_and_aliases(\n              config,\n              name,\n              doc.filter(|doc| doc.lines().count() <= 1).as_deref(),\n              aliases\n                .get(recipe.name())\n                .map(Vec::as_slice)\n                .unwrap_or_default(),\n              max_signature_width,\n              &signature_widths,\n            );\n          }\n        }\n      }\n\n      if let Some(submodules) = submodule_groups.get(&group) {\n        for (i, submodule) in submodules.iter().enumerate() {\n          if config.list_submodules {\n            if no_groups && (i + groups_count > 0) {\n              println!();\n            }\n            println!(\"{list_prefix}{}:\", submodule.name());\n\n            Self::list_module(config, depth + 1, &[], submodule)?;\n          } else {\n            print!(\"{list_prefix}{} ...\", submodule.name());\n            print_doc_and_aliases(\n              config,\n              submodule.name(),\n              submodule.doc.as_deref(),\n              &[],\n              max_signature_width,\n              &signature_widths,\n            );\n          }\n        }\n      }\n    }\n\n    Ok(())\n  }\n\n  fn show<'src>(config: &Config, module: &Justfile<'src>, path: &Modulepath) -> RunResult<'src> {\n    let (alias, recipe) = Self::resolve_path(module, path)?;\n\n    if let Some(alias) = alias {\n      println!(\"{alias}\");\n    }\n\n    println!(\"{}\", recipe.color_display(config.color.stdout()));\n\n    Ok(())\n  }\n\n  fn summary(config: &Config, justfile: &Justfile) {\n    let mut printed = 0;\n    Self::summary_recursive(config, &mut Vec::new(), &mut printed, justfile);\n    println!();\n\n    if printed == 0 && config.verbosity.loud() {\n      eprintln!(\"Justfile contains no recipes.\");\n    }\n  }\n\n  fn summary_recursive<'a>(\n    config: &Config,\n    components: &mut Vec<&'a str>,\n    printed: &mut usize,\n    justfile: &'a Justfile,\n  ) {\n    let path = components.join(\"::\");\n\n    for recipe in justfile.public_recipes(config) {\n      if *printed > 0 {\n        print!(\" \");\n      }\n      if path.is_empty() {\n        print!(\"{}\", recipe.name());\n      } else {\n        print!(\"{}::{}\", path, recipe.name());\n      }\n      *printed += 1;\n    }\n\n    for module in justfile.public_modules(config) {\n      let name = module.name();\n      components.push(name);\n      Self::summary_recursive(config, components, printed, module);\n      components.pop();\n    }\n  }\n\n  fn usage<'src>(config: &Config, module: &Justfile<'src>, path: &Modulepath) -> RunResult<'src> {\n    let (alias, recipe) = Self::resolve_path(module, path)?;\n\n    if let Some(alias) = alias {\n      println!(\"{alias}\");\n    }\n\n    println!(\n      \"{}\",\n      Usage {\n        long: true,\n        path,\n        recipe,\n      }\n      .color_display(config.color.stdout()),\n    );\n\n    Ok(())\n  }\n\n  fn resolve_path<'src, 'run>(\n    mut module: &'run Justfile<'src>,\n    path: &Modulepath,\n  ) -> RunResult<'src, (Option<&'run Alias<'src>>, &'run Recipe<'src>)> {\n    for name in &path.path[0..path.path.len() - 1] {\n      module = module\n        .modules\n        .get(name)\n        .ok_or_else(|| Error::UnknownSubmodule {\n          path: path.to_string(),\n        })?;\n    }\n\n    let name = path.path.last().unwrap();\n\n    if let Some(alias) = module.get_alias(name) {\n      Ok((Some(alias), &alias.target))\n    } else if let Some(recipe) = module.get_recipe(name) {\n      Ok((None, recipe))\n    } else {\n      Err(Error::UnknownRecipe {\n        recipe: name.to_owned(),\n        suggestion: module.suggest_recipe(name),\n      })\n    }\n  }\n\n  fn variables(justfile: &Justfile) {\n    for (i, (_, assignment)) in justfile\n      .assignments\n      .iter()\n      .filter(|(_, binding)| !binding.private)\n      .enumerate()\n    {\n      if i > 0 {\n        print!(\" \");\n      }\n      print!(\"{}\", assignment.name);\n    }\n    println!();\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn init_justfile() {\n    testing::compile(INIT_JUSTFILE);\n  }\n}\n"
  },
  {
    "path": "src/suggestion.rs",
    "content": "use super::*;\n\n#[derive(Clone, Copy, Debug, PartialEq)]\npub(crate) struct Suggestion<'src> {\n  pub(crate) name: &'src str,\n  pub(crate) target: Option<&'src str>,\n}\n\nimpl Display for Suggestion<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    write!(f, \"Did you mean `{}`\", self.name)?;\n    if let Some(target) = self.target {\n      write!(f, \", an alias for `{target}`\")?;\n    }\n    write!(f, \"?\")\n  }\n}\n"
  },
  {
    "path": "src/summary.rs",
    "content": "//! Justfile summary creation, for testing purposes only.\n//!\n//! The contents of this module are not bound by any stability guarantees.\n//! Breaking changes may be introduced at any time.\n//!\n//! The main entry point into this module is the `summary` function, which\n//! parses a justfile at a given path and produces a `Summary` object, which\n//! broadly captures the functionality of the parsed justfile, or an error\n//! message.\n//!\n//! This functionality is intended to be used with `janus`, a tool for ensuring\n//! that changes to just do not inadvertently break or change the interpretation\n//! of existing justfiles.\n\nuse {\n  crate::{compiler::Compiler, config::Config, error::Error, loader::Loader},\n  std::{collections::BTreeMap, io, path::Path},\n};\n\nmod full {\n  pub(crate) use crate::{\n    assignment::Assignment, condition::Condition, conditional_operator::ConditionalOperator,\n    dependency::Dependency, expression::Expression, fragment::Fragment, justfile::Justfile,\n    line::Line, parameter::Parameter, parameter_kind::ParameterKind, recipe::Recipe, thunk::Thunk,\n  };\n}\n\npub fn summary(path: &Path) -> io::Result<Result<Summary, String>> {\n  let loader = Loader::new();\n\n  match Compiler::compile(&Config::default(), &loader, path) {\n    Ok(compilation) => Ok(Ok(Summary::new(&compilation.justfile))),\n    Err(error) => Ok(Err(if let Error::Compile { compile_error } = error {\n      compile_error.to_string()\n    } else {\n      format!(\"{error:?}\")\n    })),\n  }\n}\n\n#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]\npub struct Summary {\n  pub assignments: BTreeMap<String, Assignment>,\n  pub recipes: BTreeMap<String, Recipe>,\n}\n\nimpl Summary {\n  fn new(justfile: &full::Justfile) -> Self {\n    let mut aliases = BTreeMap::new();\n\n    for alias in justfile.aliases.values() {\n      aliases\n        .entry(alias.target.name())\n        .or_insert_with(Vec::new)\n        .push(alias.name.to_string());\n    }\n\n    Self {\n      recipes: justfile\n        .recipes\n        .iter()\n        .map(|(name, recipe)| {\n          (\n            (*name).to_string(),\n            Recipe::new(recipe, aliases.remove(name).unwrap_or_default()),\n          )\n        })\n        .collect(),\n      assignments: justfile\n        .assignments\n        .iter()\n        .map(|(name, assignment)| ((*name).to_owned(), Assignment::new(assignment)))\n        .collect(),\n    }\n  }\n}\n\n#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]\npub struct Recipe {\n  pub aliases: Vec<String>,\n  pub dependencies: Vec<Dependency>,\n  pub lines: Vec<Line>,\n  pub parameters: Vec<Parameter>,\n  pub private: bool,\n  pub quiet: bool,\n  pub shebang: bool,\n}\n\nimpl Recipe {\n  fn new(recipe: &full::Recipe, aliases: Vec<String>) -> Self {\n    Self {\n      aliases,\n      dependencies: recipe.dependencies.iter().map(Dependency::new).collect(),\n      lines: recipe.body.iter().map(Line::new).collect(),\n      parameters: recipe.parameters.iter().map(Parameter::new).collect(),\n      private: recipe.private,\n      quiet: recipe.quiet,\n      shebang: recipe.shebang,\n    }\n  }\n}\n\n#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]\npub struct Parameter {\n  pub default: Option<Expression>,\n  pub kind: ParameterKind,\n  pub name: String,\n}\n\nimpl Parameter {\n  fn new(parameter: &full::Parameter) -> Self {\n    Self {\n      kind: ParameterKind::new(parameter.kind),\n      name: parameter.name.lexeme().to_owned(),\n      default: parameter.default.as_ref().map(Expression::new),\n    }\n  }\n}\n\n#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]\npub enum ParameterKind {\n  Plus,\n  Singular,\n  Star,\n}\n\nimpl ParameterKind {\n  fn new(parameter_kind: full::ParameterKind) -> Self {\n    match parameter_kind {\n      full::ParameterKind::Singular => Self::Singular,\n      full::ParameterKind::Plus => Self::Plus,\n      full::ParameterKind::Star => Self::Star,\n    }\n  }\n}\n\n#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]\npub struct Line {\n  pub fragments: Vec<Fragment>,\n}\n\nimpl Line {\n  fn new(line: &full::Line) -> Self {\n    Self {\n      fragments: line.fragments.iter().map(Fragment::new).collect(),\n    }\n  }\n}\n\n#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]\npub enum Fragment {\n  Expression { expression: Expression },\n  Text { text: String },\n}\n\nimpl Fragment {\n  fn new(fragment: &full::Fragment) -> Self {\n    match fragment {\n      full::Fragment::Text { token } => Self::Text {\n        text: token.lexeme().to_owned(),\n      },\n      full::Fragment::Interpolation { expression } => Self::Expression {\n        expression: Expression::new(expression),\n      },\n    }\n  }\n}\n\n#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]\npub struct Assignment {\n  pub exported: bool,\n  pub expression: Expression,\n}\n\nimpl Assignment {\n  fn new(assignment: &full::Assignment) -> Self {\n    Self {\n      exported: assignment.export,\n      expression: Expression::new(&assignment.value),\n    }\n  }\n}\n\n#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]\npub enum Expression {\n  And {\n    lhs: Box<Self>,\n    rhs: Box<Self>,\n  },\n  Assert {\n    condition: Condition,\n    error: Box<Self>,\n  },\n  Backtick {\n    command: String,\n  },\n  Call {\n    name: String,\n    arguments: Vec<Self>,\n  },\n  Concatenation {\n    lhs: Box<Self>,\n    rhs: Box<Self>,\n  },\n  Conditional {\n    lhs: Box<Self>,\n    rhs: Box<Self>,\n    then: Box<Self>,\n    otherwise: Box<Self>,\n    operator: ConditionalOperator,\n  },\n  FormatString {\n    start: String,\n    expressions: Vec<(Self, String)>,\n  },\n  Join {\n    lhs: Option<Box<Self>>,\n    rhs: Box<Self>,\n  },\n  Or {\n    lhs: Box<Self>,\n    rhs: Box<Self>,\n  },\n  String {\n    text: String,\n  },\n  Variable {\n    name: String,\n  },\n}\n\nimpl Expression {\n  fn new(expression: &full::Expression) -> Self {\n    use full::Expression::*;\n    match expression {\n      And { lhs, rhs } => Self::And {\n        lhs: Self::new(lhs).into(),\n        rhs: Self::new(rhs).into(),\n      },\n      Assert {\n        condition: full::Condition { lhs, rhs, operator },\n        error,\n        ..\n      } => Self::Assert {\n        condition: Condition {\n          lhs: Box::new(Self::new(lhs)),\n          rhs: Box::new(Self::new(rhs)),\n          operator: ConditionalOperator::new(*operator),\n        },\n        error: Box::new(Self::new(error)),\n      },\n      Backtick { contents, .. } => Self::Backtick {\n        command: (*contents).clone(),\n      },\n      Call { thunk } => match thunk {\n        full::Thunk::Nullary { name, .. } => Self::Call {\n          name: name.lexeme().to_owned(),\n          arguments: Vec::new(),\n        },\n        full::Thunk::Unary { name, arg, .. } => Self::Call {\n          name: name.lexeme().to_owned(),\n          arguments: vec![Self::new(arg)],\n        },\n        full::Thunk::UnaryOpt {\n          name,\n          args: (a, opt_b),\n          ..\n        } => {\n          let mut arguments = Vec::new();\n          if let Some(b) = opt_b.as_ref() {\n            arguments.push(Self::new(b));\n          }\n          arguments.push(Self::new(a));\n          Self::Call {\n            name: name.lexeme().to_owned(),\n            arguments,\n          }\n        }\n        full::Thunk::UnaryPlus {\n          name,\n          args: (a, rest),\n          ..\n        } => {\n          let mut arguments = vec![Self::new(a)];\n          for arg in rest {\n            arguments.push(Self::new(arg));\n          }\n          Self::Call {\n            name: name.lexeme().to_owned(),\n            arguments,\n          }\n        }\n        full::Thunk::Binary {\n          name, args: [a, b], ..\n        } => Self::Call {\n          name: name.lexeme().to_owned(),\n          arguments: vec![Self::new(a), Self::new(b)],\n        },\n        full::Thunk::BinaryPlus {\n          name,\n          args: ([a, b], rest),\n          ..\n        } => {\n          let mut arguments = vec![Self::new(a), Self::new(b)];\n          for arg in rest {\n            arguments.push(Self::new(arg));\n          }\n          Self::Call {\n            name: name.lexeme().to_owned(),\n            arguments,\n          }\n        }\n        full::Thunk::Ternary {\n          name,\n          args: [a, b, c],\n          ..\n        } => Self::Call {\n          name: name.lexeme().to_owned(),\n          arguments: vec![Self::new(a), Self::new(b), Self::new(c)],\n        },\n      },\n      Concatenation { lhs, rhs } => Self::Concatenation {\n        lhs: Self::new(lhs).into(),\n        rhs: Self::new(rhs).into(),\n      },\n      Conditional {\n        condition: full::Condition { lhs, rhs, operator },\n        otherwise,\n        then,\n      } => Self::Conditional {\n        lhs: Self::new(lhs).into(),\n        operator: ConditionalOperator::new(*operator),\n        otherwise: Self::new(otherwise).into(),\n        rhs: Self::new(rhs).into(),\n        then: Self::new(then).into(),\n      },\n      FormatString { start, expressions } => Self::FormatString {\n        start: start.cooked.clone(),\n        expressions: expressions\n          .iter()\n          .map(|(expression, string)| (Self::new(expression), string.cooked.clone()))\n          .collect(),\n      },\n      Group { contents } => Self::new(contents),\n      Join { lhs, rhs } => Self::Join {\n        lhs: lhs.as_ref().map(|lhs| Self::new(lhs).into()),\n        rhs: Self::new(rhs).into(),\n      },\n      Or { lhs, rhs } => Self::Or {\n        lhs: Self::new(lhs).into(),\n        rhs: Self::new(rhs).into(),\n      },\n      StringLiteral { string_literal } => Self::String {\n        text: string_literal.cooked.clone(),\n      },\n      Variable { name, .. } => Self::Variable {\n        name: name.lexeme().to_owned(),\n      },\n    }\n  }\n}\n\n#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]\npub struct Condition {\n  lhs: Box<Expression>,\n  operator: ConditionalOperator,\n  rhs: Box<Expression>,\n}\n\n#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]\npub enum ConditionalOperator {\n  Equality,\n  Inequality,\n  RegexMatch,\n  RegexMismatch,\n}\n\nimpl ConditionalOperator {\n  fn new(operator: full::ConditionalOperator) -> Self {\n    match operator {\n      full::ConditionalOperator::Equality => Self::Equality,\n      full::ConditionalOperator::Inequality => Self::Inequality,\n      full::ConditionalOperator::RegexMatch => Self::RegexMatch,\n      full::ConditionalOperator::RegexMismatch => Self::RegexMismatch,\n    }\n  }\n}\n\n#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]\npub struct Dependency {\n  pub arguments: Vec<Expression>,\n  pub recipe: String,\n}\n\nimpl Dependency {\n  fn new(dependency: &full::Dependency) -> Self {\n    let mut arguments = Vec::new();\n    for group in &dependency.arguments {\n      for argument in group {\n        arguments.push(Expression::new(argument));\n      }\n    }\n\n    Self {\n      recipe: dependency.recipe.name().to_owned(),\n      arguments,\n    }\n  }\n}\n"
  },
  {
    "path": "src/switch.rs",
    "content": "use super::*;\n\n#[derive(Debug, PartialEq)]\npub(crate) enum Switch {\n  Long(String),\n  Short(char),\n}\n\nimpl Display for Switch {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    match &self {\n      Self::Long(long) => write!(f, \"--{long}\"),\n      Self::Short(short) => write!(f, \"-{short}\"),\n    }\n  }\n}\n"
  },
  {
    "path": "src/table.rs",
    "content": "use {super::*, std::collections::btree_map};\n\n#[derive(Debug, PartialEq, Serialize)]\n#[serde(transparent)]\npub(crate) struct Table<'key, V: Keyed<'key>> {\n  map: BTreeMap<&'key str, V>,\n}\n\nimpl<'key, V: Keyed<'key>> Table<'key, V> {\n  pub(crate) fn new() -> Self {\n    Self {\n      map: BTreeMap::new(),\n    }\n  }\n\n  pub(crate) fn insert(&mut self, value: V) {\n    self.map.insert(value.key(), value);\n  }\n\n  pub(crate) fn len(&self) -> usize {\n    self.map.len()\n  }\n\n  pub(crate) fn get(&self, key: &str) -> Option<&V> {\n    self.map.get(key)\n  }\n\n  pub(crate) fn is_empty(&self) -> bool {\n    self.map.is_empty()\n  }\n\n  pub(crate) fn values(&self) -> btree_map::Values<&'key str, V> {\n    self.map.values()\n  }\n\n  pub(crate) fn contains_key(&self, key: &str) -> bool {\n    self.map.contains_key(key)\n  }\n\n  pub(crate) fn keys(&self) -> btree_map::Keys<&'key str, V> {\n    self.map.keys()\n  }\n\n  pub(crate) fn iter(&self) -> btree_map::Iter<&'key str, V> {\n    self.map.iter()\n  }\n\n  pub(crate) fn pop(&mut self) -> Option<V> {\n    let key = self.map.keys().next().copied()?;\n    self.map.remove(key)\n  }\n\n  pub(crate) fn remove(&mut self, key: &str) -> Option<V> {\n    self.map.remove(key)\n  }\n}\n\nimpl<'key, V: Keyed<'key>> Default for Table<'key, V> {\n  fn default() -> Self {\n    Self::new()\n  }\n}\n\nimpl<'key, V: Keyed<'key>> FromIterator<V> for Table<'key, V> {\n  fn from_iter<I: IntoIterator<Item = V>>(iter: I) -> Self {\n    Self {\n      map: iter.into_iter().map(|value| (value.key(), value)).collect(),\n    }\n  }\n}\n\nimpl<'key, V: Keyed<'key>> Index<&'key str> for Table<'key, V> {\n  type Output = V;\n\n  #[inline]\n  fn index(&self, key: &str) -> &V {\n    self.map.get(key).expect(\"no entry found for key\")\n  }\n}\n\nimpl<'key, V: Keyed<'key>> IntoIterator for Table<'key, V> {\n  type IntoIter = btree_map::IntoIter<&'key str, V>;\n  type Item = (&'key str, V);\n\n  fn into_iter(self) -> btree_map::IntoIter<&'key str, V> {\n    self.map.into_iter()\n  }\n}\n\nimpl<'table, V: Keyed<'table> + 'table> IntoIterator for &'table Table<'table, V> {\n  type IntoIter = btree_map::Iter<'table, &'table str, V>;\n  type Item = (&'table &'table str, &'table V);\n\n  fn into_iter(self) -> btree_map::Iter<'table, &'table str, V> {\n    self.map.iter()\n  }\n}\n"
  },
  {
    "path": "src/testing.rs",
    "content": "use {super::*, pretty_assertions::assert_eq};\n\npub(crate) fn compile(src: &str) -> Justfile {\n  Compiler::test_compile(src).expect(\"expected successful compilation\")\n}\n\npub(crate) fn config(args: &[&str]) -> Config {\n  let mut args = Vec::from(args);\n  args.insert(0, \"just\");\n\n  let app = Config::app();\n\n  let matches = app.try_get_matches_from(args).unwrap();\n\n  Config::from_matches(&matches).unwrap()\n}\n\npub(crate) fn search(config: &Config) -> Search {\n  let working_directory = config.invocation_directory.clone();\n  let justfile = working_directory.join(\"justfile\");\n\n  Search {\n    justfile,\n    working_directory,\n  }\n}\n\npub(crate) fn tempdir() -> tempfile::TempDir {\n  tempfile::Builder::new()\n    .prefix(\"just-test-tempdir\")\n    .tempdir()\n    .expect(\"failed to create temporary directory\")\n}\n\nmacro_rules! analysis_error {\n  (\n      name:   $name:ident,\n      input:  $input:expr,\n      offset: $offset:expr,\n      line:   $line:expr,\n      column: $column:expr,\n      width:  $width:expr,\n      kind:   $kind:expr,\n    ) => {\n    #[test]\n    fn $name() {\n      $crate::testing::analysis_error($input, $offset, $line, $column, $width, $kind);\n    }\n  };\n}\n\npub(crate) fn analysis_error(\n  src: &str,\n  offset: usize,\n  line: usize,\n  column: usize,\n  length: usize,\n  kind: CompileErrorKind,\n) {\n  let tokens = Lexer::test_lex(src).expect(\"Lexing failed in parse test...\");\n\n  let ast = Parser::parse_tokens(&mut Numerator::new(), &tokens)\n    .expect(\"Parsing failed in analysis test...\");\n\n  let root = PathBuf::from(\"justfile\");\n  let mut asts: HashMap<PathBuf, Ast> = HashMap::new();\n  asts.insert(root.clone(), ast);\n\n  let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();\n  paths.insert(\"justfile\".into(), \"justfile\".into());\n\n  match Analyzer::analyze(\n    &asts,\n    &Config::default(),\n    None,\n    &[],\n    &[],\n    None,\n    &mut HashMap::new(),\n    &paths,\n    false,\n    &root,\n  ) {\n    Ok(_) => panic!(\"Analysis unexpectedly succeeded\"),\n    Err(have) => {\n      let Error::Compile { compile_error } = have else {\n        panic!(\n          \"unexpected non-compile analysis error: {}\",\n          have.color_display(Color::never()),\n        );\n      };\n\n      let want = CompileError {\n        token: Token {\n          kind: compile_error.token.kind,\n          src,\n          offset,\n          line,\n          column,\n          length,\n          path: \"justfile\".as_ref(),\n        },\n        kind: kind.into(),\n      };\n      assert_eq!(compile_error, want);\n    }\n  }\n}\n\nmacro_rules! run_error {\n  {\n    name: $name:ident,\n    src:  $src:expr,\n    args: $args:expr,\n    error: $error:pat,\n    check: $check:block $(,)?\n  } => {\n    #[test]\n    fn $name() {\n      let config = $crate::testing::config(&$args);\n      let search = $crate::testing::search(&config);\n\n      if let Subcommand::Run{ arguments } = &config.subcommand {\n        match $crate::testing::compile(&$crate::unindent::unindent($src))\n          .run(\n            &config,\n            &search,\n            &arguments,\n            &HashMap::new(),\n          ).expect_err(\"Expected runtime error\") {\n            $error => $check\n            other => {\n              panic!(\"Unexpected run error: {other:?}\");\n            }\n          }\n      } else {\n          panic!(\"Unexpected subcommand: {:?}\", config.subcommand);\n      }\n    }\n  };\n}\n\nmacro_rules! assert_matches {\n  ($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )? $(,)?) => {\n    match $expression {\n      $( $pattern )|+ $( if $guard )? => {}\n      left => panic!(\n        \"assertion failed: (left ~= right)\\n  left: `{:?}`\\n right: `{}`\",\n        left,\n        stringify!($($pattern)|+ $(if $guard)?)\n      ),\n    }\n  }\n}\n"
  },
  {
    "path": "src/thunk.rs",
    "content": "use super::*;\n\n#[allow(clippy::arbitrary_source_item_ordering)]\n#[derive_where(Debug, PartialEq)]\n#[derive(Clone)]\npub(crate) enum Thunk<'src> {\n  Nullary {\n    name: Name<'src>,\n    #[derive_where(skip(Debug, EqHashOrd))]\n    function: fn(function::Context) -> FunctionResult,\n  },\n  Unary {\n    name: Name<'src>,\n    #[derive_where(skip(Debug, EqHashOrd))]\n    function: fn(function::Context, &str) -> FunctionResult,\n    arg: Box<Expression<'src>>,\n  },\n  UnaryOpt {\n    name: Name<'src>,\n    #[derive_where(skip(Debug, EqHashOrd))]\n    function: fn(function::Context, &str, Option<&str>) -> FunctionResult,\n    args: (Box<Expression<'src>>, Box<Option<Expression<'src>>>),\n  },\n  UnaryPlus {\n    name: Name<'src>,\n    #[derive_where(skip(Debug, EqHashOrd))]\n    function: fn(function::Context, &str, &[String]) -> FunctionResult,\n    args: (Box<Expression<'src>>, Vec<Expression<'src>>),\n  },\n  Binary {\n    name: Name<'src>,\n    #[derive_where(skip(Debug, EqHashOrd))]\n    function: fn(function::Context, &str, &str) -> FunctionResult,\n    args: [Box<Expression<'src>>; 2],\n  },\n  BinaryPlus {\n    name: Name<'src>,\n    #[derive_where(skip(Debug, EqHashOrd))]\n    function: fn(function::Context, &str, &str, &[String]) -> FunctionResult,\n    args: ([Box<Expression<'src>>; 2], Vec<Expression<'src>>),\n  },\n  Ternary {\n    name: Name<'src>,\n    #[derive_where(skip(Debug, EqHashOrd))]\n    function: fn(function::Context, &str, &str, &str) -> FunctionResult,\n    args: [Box<Expression<'src>>; 3],\n  },\n}\n\nimpl<'src> Thunk<'src> {\n  pub(crate) fn name(&self) -> Name<'src> {\n    match self {\n      Self::Nullary { name, .. }\n      | Self::Unary { name, .. }\n      | Self::UnaryOpt { name, .. }\n      | Self::UnaryPlus { name, .. }\n      | Self::Binary { name, .. }\n      | Self::BinaryPlus { name, .. }\n      | Self::Ternary { name, .. } => *name,\n    }\n  }\n\n  pub(crate) fn resolve(\n    name: Name<'src>,\n    mut arguments: Vec<Expression<'src>>,\n  ) -> CompileResult<'src, Self> {\n    function::get(name.lexeme()).map_or(\n      Err(name.error(CompileErrorKind::UnknownFunction {\n        function: name.lexeme(),\n      })),\n      |function| match (function, arguments.len()) {\n        (Function::Nullary(function), 0) => Ok(Thunk::Nullary { function, name }),\n        (Function::Unary(function), 1) => Ok(Thunk::Unary {\n          function,\n          arg: arguments.pop().unwrap().into(),\n          name,\n        }),\n        (Function::UnaryOpt(function), 1..=2) => {\n          let a = arguments.remove(0).into();\n          let b = match arguments.pop() {\n            Some(value) => Some(value).into(),\n            None => None.into(),\n          };\n          Ok(Thunk::UnaryOpt {\n            function,\n            args: (a, b),\n            name,\n          })\n        }\n        (Function::UnaryPlus(function), 1..=usize::MAX) => {\n          let rest = arguments.drain(1..).collect();\n          let a = Box::new(arguments.pop().unwrap());\n          Ok(Thunk::UnaryPlus {\n            function,\n            args: (a, rest),\n            name,\n          })\n        }\n        (Function::Binary(function), 2) => {\n          let b = arguments.pop().unwrap().into();\n          let a = arguments.pop().unwrap().into();\n          Ok(Thunk::Binary {\n            function,\n            args: [a, b],\n            name,\n          })\n        }\n        (Function::BinaryPlus(function), 2..=usize::MAX) => {\n          let rest = arguments.drain(2..).collect();\n          let b = arguments.pop().unwrap().into();\n          let a = arguments.pop().unwrap().into();\n          Ok(Thunk::BinaryPlus {\n            function,\n            args: ([a, b], rest),\n            name,\n          })\n        }\n        (Function::Ternary(function), 3) => {\n          let c = arguments.pop().unwrap().into();\n          let b = arguments.pop().unwrap().into();\n          let a = arguments.pop().unwrap().into();\n          Ok(Thunk::Ternary {\n            function,\n            args: [a, b, c],\n            name,\n          })\n        }\n        (function, _) => Err(name.error(CompileErrorKind::FunctionArgumentCountMismatch {\n          function: name.lexeme(),\n          found: arguments.len(),\n          expected: function.argc(),\n        })),\n      },\n    )\n  }\n}\n\nimpl Display for Thunk<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    use Thunk::*;\n    match self {\n      Nullary { name, .. } => write!(f, \"{}()\", name.lexeme()),\n      Unary { name, arg, .. } => write!(f, \"{}({arg})\", name.lexeme()),\n      UnaryOpt {\n        name, args: (a, b), ..\n      } => {\n        if let Some(b) = b.as_ref() {\n          write!(f, \"{}({a}, {b})\", name.lexeme())\n        } else {\n          write!(f, \"{}({a})\", name.lexeme())\n        }\n      }\n      UnaryPlus {\n        name,\n        args: (a, rest),\n        ..\n      } => {\n        write!(f, \"{}({a}\", name.lexeme())?;\n        for arg in rest {\n          write!(f, \", {arg}\")?;\n        }\n        write!(f, \")\")\n      }\n      Binary {\n        name, args: [a, b], ..\n      } => write!(f, \"{}({a}, {b})\", name.lexeme()),\n      BinaryPlus {\n        name,\n        args: ([a, b], rest),\n        ..\n      } => {\n        write!(f, \"{}({a}, {b}\", name.lexeme())?;\n        for arg in rest {\n          write!(f, \", {arg}\")?;\n        }\n        write!(f, \")\")\n      }\n      Ternary {\n        name,\n        args: [a, b, c],\n        ..\n      } => write!(f, \"{}({a}, {b}, {c})\", name.lexeme()),\n    }\n  }\n}\n\nimpl Serialize for Thunk<'_> {\n  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n  where\n    S: Serializer,\n  {\n    let mut seq = serializer.serialize_seq(None)?;\n    seq.serialize_element(\"call\")?;\n    seq.serialize_element(&self.name())?;\n    match self {\n      Self::Nullary { .. } => {}\n      Self::Unary { arg, .. } => seq.serialize_element(&arg)?,\n      Self::UnaryOpt {\n        args: (a, opt_b), ..\n      } => {\n        seq.serialize_element(a)?;\n        if let Some(b) = opt_b.as_ref() {\n          seq.serialize_element(b)?;\n        }\n      }\n      Self::UnaryPlus {\n        args: (a, rest), ..\n      } => {\n        seq.serialize_element(a)?;\n        for arg in rest {\n          seq.serialize_element(arg)?;\n        }\n      }\n      Self::Binary { args, .. } => {\n        for arg in args {\n          seq.serialize_element(arg)?;\n        }\n      }\n      Self::BinaryPlus { args, .. } => {\n        for arg in args.0.iter().map(Box::as_ref).chain(&args.1) {\n          seq.serialize_element(arg)?;\n        }\n      }\n      Self::Ternary { args, .. } => {\n        for arg in args {\n          seq.serialize_element(arg)?;\n        }\n      }\n    }\n    seq.end()\n  }\n}\n"
  },
  {
    "path": "src/token.rs",
    "content": "use super::*;\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]\npub(crate) struct Token<'src> {\n  pub(crate) column: usize,\n  pub(crate) kind: TokenKind,\n  pub(crate) length: usize,\n  pub(crate) line: usize,\n  pub(crate) offset: usize,\n  pub(crate) path: &'src Path,\n  pub(crate) src: &'src str,\n}\n\nimpl<'src> Token<'src> {\n  pub(crate) fn lexeme(&self) -> &'src str {\n    &self.src[self.offset..self.offset + self.length]\n  }\n\n  pub(crate) fn error(&self, kind: CompileErrorKind<'src>) -> CompileError<'src> {\n    CompileError::new(*self, kind)\n  }\n}\n\nimpl ColorDisplay for Token<'_> {\n  fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {\n    let width = if self.length == 0 { 1 } else { self.length };\n\n    let line_number = self.line.ordinal();\n    match self.src.lines().nth(self.line) {\n      Some(line) => {\n        let mut i = 0;\n        let mut space_column = 0;\n        let mut space_line = String::new();\n        let mut space_width = 0;\n        for c in line.chars() {\n          if c == '\\t' {\n            space_line.push_str(\"    \");\n            if i < self.column {\n              space_column += 4;\n            }\n            if i >= self.column && i < self.column + width {\n              space_width += 4;\n            }\n          } else {\n            if i < self.column {\n              space_column += UnicodeWidthChar::width(c).unwrap_or(0);\n            }\n            if i >= self.column && i < self.column + width {\n              space_width += UnicodeWidthChar::width(c).unwrap_or(0);\n            }\n            space_line.push(c);\n          }\n          i += c.len_utf8();\n        }\n        let line_number_width = line_number.to_string().len();\n        writeln!(\n          f,\n          \"{:width$}{} {}:{}:{}\",\n          \"\",\n          color.context().paint(\"——▶\"),\n          self.path.display(),\n          line_number,\n          self.column.ordinal(),\n          width = line_number_width\n        )?;\n        writeln!(\n          f,\n          \"{:width$} {}\",\n          \"\",\n          color.context().paint(\"│\"),\n          width = line_number_width\n        )?;\n        writeln!(\n          f,\n          \"{} {space_line}\",\n          color.context().paint(&format!(\"{line_number} │\"))\n        )?;\n        write!(\n          f,\n          \"{:width$} {}\",\n          \"\",\n          color.context().paint(\"│\"),\n          width = line_number_width\n        )?;\n        write!(\n          f,\n          \" {0:1$}{2}{3:^<4$}{5}\",\n          \"\",\n          space_column,\n          color.prefix(),\n          \"\",\n          space_width.max(1),\n          color.suffix()\n        )?;\n      }\n      None => {\n        if self.offset != self.src.len() {\n          write!(\n            f,\n            \"internal error: Error has invalid line number: {line_number}\"\n          )?;\n        }\n      }\n    }\n\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "src/token_kind.rs",
    "content": "use super::*;\n\n#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]\npub(crate) enum TokenKind {\n  AmpersandAmpersand,\n  Asterisk,\n  At,\n  Backtick,\n  BangEquals,\n  BangTilde,\n  BarBar,\n  BraceL,\n  BraceR,\n  BracketL,\n  BracketR,\n  ByteOrderMark,\n  Colon,\n  ColonColon,\n  ColonEquals,\n  Comma,\n  Comment,\n  Dedent,\n  Dollar,\n  Eof,\n  Eol,\n  Equals,\n  EqualsEquals,\n  EqualsTilde,\n  FormatStringContinue,\n  FormatStringEnd,\n  FormatStringStart,\n  Identifier,\n  Indent,\n  InterpolationEnd,\n  InterpolationStart,\n  ParenL,\n  ParenR,\n  Plus,\n  QuestionMark,\n  Slash,\n  StringToken,\n  Text,\n  Unspecified,\n  Whitespace,\n}\n\nimpl Display for TokenKind {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    use TokenKind::*;\n    write!(\n      f,\n      \"{}\",\n      match *self {\n        AmpersandAmpersand => \"'&&'\",\n        Asterisk => \"'*'\",\n        At => \"'@'\",\n        Backtick => \"backtick\",\n        BangEquals => \"'!='\",\n        BangTilde => \"'!~'\",\n        BarBar => \"'||'\",\n        BraceL => \"'{'\",\n        BraceR => \"'}'\",\n        BracketL => \"'['\",\n        BracketR => \"']'\",\n        ByteOrderMark => \"byte order mark\",\n        Colon => \"':'\",\n        ColonColon => \"'::'\",\n        ColonEquals => \"':='\",\n        Comma => \"','\",\n        Comment => \"comment\",\n        Dedent => \"dedent\",\n        Dollar => \"'$'\",\n        Eof => \"end of file\",\n        Eol => \"end of line\",\n        Equals => \"'='\",\n        EqualsEquals => \"'=='\",\n        EqualsTilde => \"'=~'\",\n        FormatStringContinue | FormatStringEnd | FormatStringStart => \"format string\",\n        Identifier => \"identifier\",\n        Indent => \"indent\",\n        InterpolationEnd => \"'}}'\",\n        InterpolationStart => \"'{{'\",\n        ParenL => \"'('\",\n        ParenR => \"')'\",\n        Plus => \"'+'\",\n        QuestionMark => \"?\",\n        Slash => \"'/'\",\n        StringToken => \"string\",\n        Text => \"command text\",\n        Unspecified => \"unspecified\",\n        Whitespace => \"whitespace\",\n      }\n    )\n  }\n}\n"
  },
  {
    "path": "src/tree.rs",
    "content": "use {super::*, std::borrow::Cow};\n\n/// Construct a `Tree` from a symbolic expression literal. This macro, and the\n/// Tree type, are only used in the Parser unit tests, providing a concise\n/// notation for representing the expected results of parsing a given string.\nmacro_rules! tree {\n  { ($($child:tt)*) } => {\n    $crate::tree::Tree::List(vec![$(tree!($child),)*])\n  };\n\n  { $atom:ident } => {\n    $crate::tree::Tree::atom(stringify!($atom))\n  };\n\n  { $atom:literal } => {\n    $crate::tree::Tree::atom(format!(\"\\\"{}\\\"\", $atom))\n  };\n\n  { # } => {\n    $crate::tree::Tree::atom(\"#\")\n  };\n\n  { ? } => {\n    $crate::tree::Tree::atom(\"?\")\n  };\n\n  { + } => {\n    $crate::tree::Tree::atom(\"+\")\n  };\n\n  { * } => {\n    $crate::tree::Tree::atom(\"*\")\n  };\n\n  { && } => {\n    $crate::tree::Tree::atom(\"&&\")\n  };\n\n  { == } => {\n    $crate::tree::Tree::atom(\"==\")\n  };\n\n  { != } => {\n    $crate::tree::Tree::atom(\"!=\")\n  };\n}\n\n/// A `Tree` is either…\n#[derive(Debug, PartialEq)]\npub(crate) enum Tree<'text> {\n  /// …an atom containing text, or…\n  Atom(Cow<'text, str>),\n  /// …a list containing zero or more `Tree`s.\n  List(Vec<Self>),\n}\n\nimpl<'text> Tree<'text> {\n  /// Construct an Atom from a text scalar\n  pub(crate) fn atom(text: impl Into<Cow<'text, str>>) -> Self {\n    Self::Atom(text.into())\n  }\n\n  /// Construct a List from an iterable of trees\n  pub(crate) fn list(children: impl IntoIterator<Item = Self>) -> Self {\n    Self::List(children.into_iter().collect())\n  }\n\n  /// Convenience function to create an atom containing quoted text\n  pub(crate) fn string(contents: impl AsRef<str>) -> Self {\n    Self::atom(format!(\"\\\"{}\\\"\", contents.as_ref()))\n  }\n\n  /// Push a child node into self, turning it into a List if it was an Atom\n  pub(crate) fn push(self, tree: impl Into<Self>) -> Self {\n    match self {\n      Self::List(mut children) => {\n        children.push(tree.into());\n        Self::List(children)\n      }\n      Self::Atom(text) => Self::List(vec![Self::Atom(text), tree.into()]),\n    }\n  }\n\n  /// Extend a self with a tail of Trees, turning self into a List if it was an\n  /// Atom\n  pub(crate) fn extend<I, T>(self, tail: I) -> Self\n  where\n    I: IntoIterator<Item = T>,\n    T: Into<Self>,\n  {\n    // Tree::List(children.into_iter().collect())\n    let mut head = match self {\n      Self::List(children) => children,\n      Self::Atom(text) => vec![Self::Atom(text)],\n    };\n\n    for child in tail {\n      head.push(child.into());\n    }\n\n    Self::List(head)\n  }\n\n  /// Like `push`, but modify self in-place\n  pub(crate) fn push_mut(&mut self, tree: impl Into<Self>) {\n    *self = mem::replace(self, Self::List(Vec::new())).push(tree.into());\n  }\n}\n\nimpl Display for Tree<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    match self {\n      Self::List(children) => {\n        write!(f, \"(\")?;\n\n        for (i, child) in children.iter().enumerate() {\n          if i > 0 {\n            write!(f, \" \")?;\n          }\n          write!(f, \"{child}\")?;\n        }\n\n        write!(f, \")\")\n      }\n      Self::Atom(text) => write!(f, \"{text}\"),\n    }\n  }\n}\n\nimpl<'text, T> From<T> for Tree<'text>\nwhere\n  T: Into<Cow<'text, str>>,\n{\n  fn from(text: T) -> Self {\n    Self::Atom(text.into())\n  }\n}\n"
  },
  {
    "path": "src/unindent.rs",
    "content": "#[must_use]\npub fn unindent(text: &str) -> String {\n  // find line start and end indices\n  let mut lines = Vec::new();\n  let mut start = 0;\n  for (i, c) in text.char_indices() {\n    if c == '\\n' || i == text.len() - c.len_utf8() {\n      let end = i + c.len_utf8();\n      lines.push(&text[start..end]);\n      start = end;\n    }\n  }\n\n  let common_indentation = lines\n    .iter()\n    .filter(|line| !blank(line))\n    .copied()\n    .map(indentation)\n    .fold(\n      None,\n      |common_indentation, line_indentation| match common_indentation {\n        Some(common_indentation) => Some(common(common_indentation, line_indentation)),\n        None => Some(line_indentation),\n      },\n    )\n    .unwrap_or(\"\");\n\n  let mut replacements = Vec::with_capacity(lines.len());\n\n  for (i, line) in lines.iter().enumerate() {\n    let blank = blank(line);\n    let first = i == 0;\n    let last = i == lines.len() - 1;\n\n    let replacement = match (blank, first, last) {\n      (true, false, false) => \"\\n\",\n      (true, _, _) => \"\",\n      (false, _, _) => &line[common_indentation.len()..],\n    };\n\n    replacements.push(replacement);\n  }\n\n  replacements.into_iter().collect()\n}\n\nfn indentation(line: &str) -> &str {\n  let i = line\n    .char_indices()\n    .take_while(|(_, c)| matches!(c, ' ' | '\\t'))\n    .map(|(i, _)| i + 1)\n    .last()\n    .unwrap_or(0);\n\n  &line[..i]\n}\n\nfn blank(line: &str) -> bool {\n  line.chars().all(|c| matches!(c, ' ' | '\\t' | '\\r' | '\\n'))\n}\n\nfn common<'s>(a: &'s str, b: &'s str) -> &'s str {\n  let i = a\n    .char_indices()\n    .zip(b.chars())\n    .take_while(|((_, ac), bc)| ac == bc)\n    .map(|((i, c), _)| i + c.len_utf8())\n    .last()\n    .unwrap_or(0);\n\n  &a[0..i]\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn unindents() {\n    assert_eq!(unindent(\"foo\"), \"foo\");\n    assert_eq!(unindent(\"foo\\nbar\\nbaz\\n\"), \"foo\\nbar\\nbaz\\n\");\n    assert_eq!(unindent(\"\"), \"\");\n    assert_eq!(unindent(\"  foo\\n  bar\"), \"foo\\nbar\");\n    assert_eq!(unindent(\"  foo\\n  bar\\n\\n\"), \"foo\\nbar\\n\");\n\n    assert_eq!(\n      unindent(\n        \"\n          hello\n          bar\n        \"\n      ),\n      \"hello\\nbar\\n\"\n    );\n\n    assert_eq!(unindent(\"hello\\n  bar\\n  foo\"), \"hello\\n  bar\\n  foo\");\n\n    assert_eq!(\n      unindent(\n        \"\n\n          hello\n          bar\n\n        \"\n      ),\n      \"\\nhello\\nbar\\n\\n\"\n    );\n  }\n\n  #[test]\n  fn indentations() {\n    assert_eq!(indentation(\"\"), \"\");\n    assert_eq!(indentation(\"foo\"), \"\");\n    assert_eq!(indentation(\"   foo\"), \"   \");\n    assert_eq!(indentation(\"\\t\\tfoo\"), \"\\t\\t\");\n    assert_eq!(indentation(\"\\t \\t foo\"), \"\\t \\t \");\n  }\n\n  #[test]\n  fn blanks() {\n    assert!(blank(\"       \\n\"));\n    assert!(!blank(\"       foo\\n\"));\n    assert!(blank(\"\\t\\t\\n\"));\n  }\n\n  #[test]\n  fn commons() {\n    assert_eq!(common(\"foo\", \"foobar\"), \"foo\");\n    assert_eq!(common(\"foo\", \"bar\"), \"\");\n    assert_eq!(common(\"\", \"\"), \"\");\n    assert_eq!(common(\"\", \"bar\"), \"\");\n  }\n}\n"
  },
  {
    "path": "src/unresolved_dependency.rs",
    "content": "use super::*;\n\n#[derive(PartialEq, Debug, Clone)]\npub(crate) struct UnresolvedDependency<'src> {\n  pub(crate) arguments: Vec<Expression<'src>>,\n  pub(crate) recipe: Namepath<'src>,\n}\n\nimpl Display for UnresolvedDependency<'_> {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    if self.arguments.is_empty() {\n      write!(f, \"{}\", self.recipe)\n    } else {\n      write!(f, \"({}\", self.recipe)?;\n\n      for argument in &self.arguments {\n        write!(f, \" {argument}\")?;\n      }\n\n      write!(f, \")\")\n    }\n  }\n}\n"
  },
  {
    "path": "src/unresolved_recipe.rs",
    "content": "use super::*;\n\npub(crate) type UnresolvedRecipe<'src> = Recipe<'src, UnresolvedDependency<'src>>;\n\nimpl<'src> UnresolvedRecipe<'src> {\n  pub(crate) fn resolve(\n    self,\n    assignments: &Table<'src, Assignment<'src>>,\n    modulepath: &Modulepath,\n    resolved: Vec<Arc<Recipe<'src>>>,\n    settings: &Settings,\n  ) -> CompileResult<'src, Recipe<'src>> {\n    assert_eq!(\n      self.dependencies.len(),\n      resolved.len(),\n      \"UnresolvedRecipe::resolve: dependency count not equal to resolved count: {} != {}\",\n      self.dependencies.len(),\n      resolved.len()\n    );\n\n    let mut variable_references = HashSet::new();\n\n    for (i, parameter) in self.parameters.iter().enumerate() {\n      if let Some(expression) = &parameter.default {\n        for variable in expression.variables() {\n          Self::resolve_variable(\n            assignments,\n            &self.parameters[..i],\n            &variable,\n            &mut variable_references,\n          )?;\n        }\n      }\n    }\n\n    for dependency in &self.dependencies {\n      for argument in &dependency.arguments {\n        for variable in argument.variables() {\n          Self::resolve_variable(\n            assignments,\n            &self.parameters,\n            &variable,\n            &mut variable_references,\n          )?;\n        }\n      }\n    }\n\n    for line in &self.body {\n      if line.is_comment() && settings.ignore_comments {\n        continue;\n      }\n\n      for fragment in &line.fragments {\n        if let Fragment::Interpolation { expression, .. } = fragment {\n          for variable in expression.variables() {\n            Self::resolve_variable(\n              assignments,\n              &self.parameters,\n              &variable,\n              &mut variable_references,\n            )?;\n          }\n        }\n      }\n    }\n\n    for (unresolved, resolved) in self.dependencies.iter().zip(&resolved) {\n      assert_eq!(unresolved.recipe.last().lexeme(), resolved.name.lexeme());\n      if !resolved\n        .argument_range()\n        .contains(&unresolved.arguments.len())\n      {\n        return Err(unresolved.recipe.last().error(\n          CompileErrorKind::DependencyArgumentCountMismatch {\n            dependency: unresolved.recipe.clone(),\n            found: unresolved.arguments.len(),\n            min: resolved.min_arguments(),\n            max: resolved.max_arguments(),\n          },\n        ));\n      }\n    }\n\n    let dependencies = self\n      .dependencies\n      .into_iter()\n      .zip(resolved)\n      .map(|(unresolved, resolved)| Dependency {\n        arguments: resolved.group_arguments(&unresolved.arguments),\n        recipe: resolved,\n      })\n      .collect();\n\n    let mut recipe_path = modulepath.clone();\n\n    recipe_path.path.push(self.name.lexeme().into());\n\n    Ok(Recipe {\n      attributes: self.attributes,\n      body: self.body,\n      dependencies,\n      doc: self.doc,\n      file_depth: self.file_depth,\n      import_offsets: self.import_offsets,\n      module_path: Some(modulepath.clone()),\n      name: self.name,\n      parameters: self.parameters,\n      priors: self.priors,\n      private: self.private,\n      quiet: self.quiet,\n      recipe_path: Some(recipe_path),\n      shebang: self.shebang,\n      variable_references,\n    })\n  }\n\n  fn resolve_variable(\n    assignments: &Table<'src, Assignment<'src>>,\n    parameters: &[Parameter],\n    variable: &Token<'src>,\n    variable_references: &mut HashSet<Number>,\n  ) -> CompileResult<'src> {\n    let name = variable.lexeme();\n\n    if parameters.iter().any(|p| p.name.lexeme() == name) {\n      Ok(())\n    } else if let Some(assignment) = assignments.get(name) {\n      variable_references.insert(assignment.number);\n      Ok(())\n    } else if constants().contains_key(name) {\n      Ok(())\n    } else {\n      Err(variable.error(CompileErrorKind::UndefinedVariable { variable: name }))\n    }\n  }\n}\n"
  },
  {
    "path": "src/unstable_feature.rs",
    "content": "use super::*;\n\n#[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd)]\npub(crate) enum UnstableFeature {\n  EagerAssignments,\n  FormatSubcommand,\n  LazySetting,\n  LogicalOperators,\n  WhichFunction,\n}\n\nimpl Display for UnstableFeature {\n  fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n    match self {\n      Self::EagerAssignments => write!(f, \"`eager` assignments are currently unstable.\"),\n      Self::FormatSubcommand => write!(f, \"The `--fmt` command is currently unstable.\"),\n      Self::LazySetting => write!(f, \"The `lazy` setting is currently unstable.\"),\n      Self::LogicalOperators => write!(\n        f,\n        \"The logical operators `&&` and `||` are currently unstable.\"\n      ),\n      Self::WhichFunction => write!(f, \"The `which()` function is currently unstable.\"),\n    }\n  }\n}\n"
  },
  {
    "path": "src/usage.rs",
    "content": "use super::*;\n\npub(crate) struct Usage<'a, D> {\n  pub(crate) long: bool,\n  pub(crate) path: &'a Modulepath,\n  pub(crate) recipe: &'a Recipe<'a, D>,\n}\n\nimpl<D> ColorDisplay for Usage<'_, D> {\n  fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {\n    write!(\n      f,\n      \"{}{}{} {}\",\n      color\n        .heading()\n        .paint(if self.long { \"Usage:\" } else { \"usage:\" }),\n      if self.long { \" \" } else { \"\\n    \" },\n      color.argument().paint(\"just\"),\n      color.argument().paint(&self.path.to_string()),\n    )?;\n\n    let options = self.recipe.parameters.iter().any(Parameter::is_option);\n\n    let arguments = self.recipe.parameters.iter().any(|p| !p.is_option());\n\n    if options {\n      write!(f, \" {}\", color.argument().paint(\"[OPTIONS]\"))?;\n    }\n\n    for parameter in &self.recipe.parameters {\n      if parameter.is_option() {\n        continue;\n      }\n\n      write!(f, \" \")?;\n\n      write!(\n        f,\n        \"{}\",\n        UsageParameter {\n          parameter,\n          long: false,\n        }\n        .color_display(color),\n      )?;\n    }\n\n    if !self.long {\n      return Ok(());\n    }\n\n    if arguments {\n      writeln!(f)?;\n      writeln!(f)?;\n      writeln!(f, \"{}\", color.heading().paint(\"Arguments:\"))?;\n\n      for (i, parameter) in self\n        .recipe\n        .parameters\n        .iter()\n        .filter(|p| !p.is_option())\n        .enumerate()\n      {\n        if i > 0 {\n          writeln!(f)?;\n        }\n\n        write!(f, \"  \")?;\n\n        write!(\n          f,\n          \"{}\",\n          UsageParameter {\n            parameter,\n            long: true,\n          }\n          .color_display(color),\n        )?;\n      }\n    }\n\n    if options {\n      if arguments {\n        writeln!(f)?;\n      }\n\n      writeln!(f)?;\n      writeln!(f, \"{}\", color.heading().paint(\"Options:\"))?;\n      for (i, parameter) in self\n        .recipe\n        .parameters\n        .iter()\n        .filter(|p| p.is_option())\n        .enumerate()\n      {\n        if i > 0 {\n          writeln!(f)?;\n        }\n\n        write!(f, \"  \")?;\n\n        write!(\n          f,\n          \"{}\",\n          UsageParameter {\n            parameter,\n            long: true,\n          }\n          .color_display(color),\n        )?;\n      }\n    }\n\n    Ok(())\n  }\n}\n\nstruct UsageParameter<'a> {\n  long: bool,\n  parameter: &'a Parameter<'a>,\n}\n\nimpl ColorDisplay for UsageParameter<'_> {\n  fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {\n    if self.parameter.is_option() {\n      if let Some(short) = self.parameter.short {\n        write!(f, \"{}\", color.option().paint(&format!(\"-{short}\")))?;\n      } else {\n        write!(f, \"  \")?;\n      }\n\n      if let Some(long) = &self.parameter.long {\n        if self.parameter.short.is_some() {\n          write!(f, \", \")?;\n        } else {\n          write!(f, \"  \")?;\n        }\n\n        write!(f, \"{}\", color.option().paint(&format!(\"--{long}\")))?;\n      }\n\n      if self.parameter.value.is_none() {\n        write!(\n          f,\n          \" {}\",\n          color.argument().paint(self.parameter.name.lexeme()),\n        )?;\n      }\n    } else {\n      if !self.parameter.is_required() {\n        write!(f, \"{}\", color.argument().paint(\"[\"))?;\n      }\n\n      write!(\n        f,\n        \"{}\",\n        color.argument().paint(self.parameter.name.lexeme()),\n      )?;\n\n      if self.parameter.kind.is_variadic() {\n        write!(f, \"{}\", color.argument().paint(\"...\"))?;\n      }\n\n      if !self.parameter.is_required() {\n        write!(f, \"{}\", color.argument().paint(\"]\"))?;\n      }\n    }\n\n    if !self.long {\n      return Ok(());\n    }\n\n    if let Some(help) = &self.parameter.help {\n      write!(f, \" {help}\")?;\n    }\n\n    if let Some(default) = &self.parameter.default {\n      if self.parameter.value.is_none() {\n        write!(f, \" [default: {default}]\")?;\n      }\n    }\n\n    if let Some(pattern) = &self.parameter.pattern {\n      write!(f, \" [pattern: '{}']\", pattern.original())?;\n    }\n\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "src/use_color.rs",
    "content": "use super::*;\n\n#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)]\npub(crate) enum UseColor {\n  Always,\n  Auto,\n  Never,\n}\n"
  },
  {
    "path": "src/variables.rs",
    "content": "use super::*;\n\npub(crate) struct Variables<'expression, 'src> {\n  stack: Vec<&'expression Expression<'src>>,\n}\n\nimpl<'expression, 'src> Variables<'expression, 'src> {\n  pub(crate) fn new(root: &'expression Expression<'src>) -> Self {\n    Self { stack: vec![root] }\n  }\n}\n\nimpl<'src> Iterator for Variables<'_, 'src> {\n  type Item = Name<'src>;\n\n  fn next(&mut self) -> Option<Name<'src>> {\n    loop {\n      match self.stack.pop()? {\n        Expression::And { lhs, rhs } | Expression::Or { lhs, rhs } => {\n          self.stack.push(lhs);\n          self.stack.push(rhs);\n        }\n        Expression::Assert {\n          condition:\n            Condition {\n              lhs,\n              rhs,\n              operator: _,\n            },\n          error,\n          ..\n        } => {\n          self.stack.push(error);\n          self.stack.push(rhs);\n          self.stack.push(lhs);\n        }\n        Expression::Backtick { .. } | Expression::StringLiteral { .. } => {}\n        Expression::Call { thunk } => match thunk {\n          Thunk::Nullary { .. } => {}\n          Thunk::Unary { arg, .. } => self.stack.push(arg),\n          Thunk::UnaryOpt {\n            args: (a, opt_b), ..\n          } => {\n            self.stack.push(a);\n            if let Some(b) = opt_b.as_ref() {\n              self.stack.push(b);\n            }\n          }\n          Thunk::UnaryPlus {\n            args: (a, rest), ..\n          } => {\n            let first: &[&Expression] = &[a];\n            for arg in first.iter().copied().chain(rest).rev() {\n              self.stack.push(arg);\n            }\n          }\n          Thunk::Binary { args, .. } => {\n            for arg in args.iter().rev() {\n              self.stack.push(arg);\n            }\n          }\n          Thunk::BinaryPlus {\n            args: ([a, b], rest),\n            ..\n          } => {\n            let first: &[&Expression] = &[a, b];\n            for arg in first.iter().copied().chain(rest).rev() {\n              self.stack.push(arg);\n            }\n          }\n          Thunk::Ternary { args, .. } => {\n            for arg in args.iter().rev() {\n              self.stack.push(arg);\n            }\n          }\n        },\n        Expression::Concatenation { lhs, rhs } => {\n          self.stack.push(rhs);\n          self.stack.push(lhs);\n        }\n        Expression::Conditional {\n          condition:\n            Condition {\n              lhs,\n              rhs,\n              operator: _,\n            },\n          then,\n          otherwise,\n        } => {\n          self.stack.push(otherwise);\n          self.stack.push(then);\n          self.stack.push(rhs);\n          self.stack.push(lhs);\n        }\n        Expression::FormatString { expressions, .. } => {\n          for (expression, _string) in expressions {\n            self.stack.push(expression);\n          }\n        }\n        Expression::Group { contents } => {\n          self.stack.push(contents);\n        }\n        Expression::Join { lhs, rhs } => {\n          self.stack.push(rhs);\n          if let Some(lhs) = lhs {\n            self.stack.push(lhs);\n          }\n        }\n        Expression::Variable { name, .. } => return Some(*name),\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/verbosity.rs",
    "content": "#[allow(clippy::arbitrary_source_item_ordering)]\n#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]\npub(crate) enum Verbosity {\n  Quiet,\n  Taciturn,\n  Loquacious,\n  Grandiloquent,\n}\n\nimpl Verbosity {\n  pub(crate) fn from_flag_occurrences(flag_occurrences: u8) -> Self {\n    match flag_occurrences {\n      0 => Self::Taciturn,\n      1 => Self::Loquacious,\n      _ => Self::Grandiloquent,\n    }\n  }\n\n  pub(crate) fn quiet(self) -> bool {\n    self == Self::Quiet\n  }\n\n  pub(crate) fn loud(self) -> bool {\n    !self.quiet()\n  }\n\n  pub(crate) fn loquacious(self) -> bool {\n    self >= Self::Loquacious\n  }\n\n  pub(crate) fn grandiloquent(self) -> bool {\n    self >= Self::Grandiloquent\n  }\n\n  pub(crate) const fn default() -> Self {\n    Self::Taciturn\n  }\n}\n\nimpl Default for Verbosity {\n  fn default() -> Self {\n    Self::default()\n  }\n}\n"
  },
  {
    "path": "src/warning.rs",
    "content": "use super::*;\n\n#[derive(Clone, Debug, PartialEq)]\npub(crate) enum Warning {}\n\nimpl Warning {\n  #[allow(clippy::unused_self)]\n  fn context(&self) -> Option<&Token> {\n    None\n  }\n}\n\nimpl ColorDisplay for Warning {\n  fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {\n    let warning = color.warning();\n    let message = color.message();\n\n    write!(f, \"{} {}\", warning.paint(\"warning:\"), message.prefix())?;\n\n    write!(f, \"{}\", message.suffix())?;\n\n    if let Some(token) = self.context() {\n      writeln!(f)?;\n      write!(f, \"{}\", token.color_display(color))?;\n    }\n\n    Ok(())\n  }\n}\n\nimpl Serialize for Warning {\n  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n  where\n    S: Serializer,\n  {\n    let mut map = serializer.serialize_map(None)?;\n\n    map.serialize_entry(\"message\", &self.color_display(Color::never()).to_string())?;\n\n    map.end()\n  }\n}\n"
  },
  {
    "path": "src/which.rs",
    "content": "use super::*;\n\npub(crate) fn which(context: function::Context, name: &str) -> Result<Option<String>, String> {\n  let name = Path::new(name);\n\n  let paths = match name.components().count() {\n    0 => return Err(\"empty command\".into()),\n    1 => {\n      // cmd is a regular command\n      env::split_paths(&env::var_os(\"PATH\").ok_or(\"`PATH` environment variable not set\")?)\n        .map(|path| path.join(name))\n        .collect()\n    }\n    _ => {\n      // cmd contains a path separator, treat it as a path\n      vec![name.into()]\n    }\n  };\n\n  for mut path in paths {\n    if path.is_relative() {\n      // This candidate is a relative path, either because the user invoked `which(\"rel/path\")`,\n      // or because there was a relative path in `PATH`. Resolve it to an absolute path,\n      // relative to the working directory of the just invocation.\n      path = context.execution_context.working_directory().join(path);\n    }\n\n    path = path.lexiclean();\n\n    let mut candidates = vec![path.clone()];\n\n    if cfg!(windows) && path.extension().is_none() {\n      if let Some(pathext) = env::var_os(\"PATHEXT\") {\n        let pathext = pathext.to_str().ok_or_else(|| {\n          format!(\n            \"`PATHEXT` environment variable is not valid unicode: {}\",\n            pathext.to_string_lossy(),\n          )\n        })?;\n\n        for extension in pathext.split(';') {\n          let extension = extension\n            .strip_prefix('.')\n            .ok_or_else(|| format!(\"`PATHEXT` entry `{extension}` does not start with `.`\"))?;\n          candidates.push(path.with_extension(extension));\n        }\n      }\n    }\n\n    for candidate in candidates {\n      if is_executable::is_executable(&candidate) {\n        return candidate\n          .to_str()\n          .map(|candidate| Some(candidate.into()))\n          .ok_or_else(|| {\n            format!(\n              \"Executable path is not valid unicode: {}\",\n              candidate.display()\n            )\n          });\n      }\n    }\n  }\n\n  Ok(None)\n}\n"
  },
  {
    "path": "tests/alias.rs",
    "content": "use super::*;\n\n#[test]\nfn alias_nested_module() {\n  Test::new()\n    .write(\"foo.just\", \"mod bar\\nbaz: \\n @echo FOO\")\n    .write(\"bar.just\", \"baz:\\n @echo BAZ\")\n    .justfile(\n      \"\n      mod foo\n\n      alias b := foo::bar::baz\n\n      baz:\n        @echo 'HERE'\n      \",\n    )\n    .arg(\"b\")\n    .stdout(\"BAZ\\n\")\n    .success();\n}\n\n#[test]\nfn unknown_nested_alias() {\n  Test::new()\n    .write(\"foo.just\", \"baz: \\n @echo FOO\")\n    .justfile(\n      \"\n      mod foo\n\n      alias b := foo::bar::baz\n      \",\n    )\n    .arg(\"b\")\n    .stderr(\n      \"\\\n        error: Alias `b` has an unknown target `foo::bar::baz`\n ——▶ justfile:3:7\n  │\n3 │ alias b := foo::bar::baz\n  │       ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn alias_in_submodule() {\n  Test::new()\n    .write(\n      \"foo.just\",\n      \"\nalias b := bar\n\nbar:\n  @echo BAR\n\",\n    )\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo::b\")\n    .stdout(\"BAR\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/alias_style.rs",
    "content": "use super::*;\n\n#[test]\nfn default() {\n  Test::new()\n    .justfile(\n      \"\n        alias f := foo\n\n        # comment\n        foo:\n\n        bar:\n      \",\n    )\n    .args([\"--list\"])\n    .stdout(\n      \"\n        Available recipes:\n            bar\n            foo # comment [alias: f]\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn multiple() {\n  Test::new()\n    .justfile(\n      \"\n        alias a := foo\n        alias b := foo\n\n        # comment\n        foo:\n\n        bar:\n      \",\n    )\n    .args([\"--list\"])\n    .stdout(\n      \"\n        Available recipes:\n            bar\n            foo # comment [aliases: a, b]\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn right() {\n  Test::new()\n    .justfile(\n      \"\n        alias f := foo\n\n        # comment\n        foo:\n\n        bar:\n      \",\n    )\n    .args([\"--alias-style=right\", \"--list\"])\n    .stdout(\n      \"\n        Available recipes:\n            bar\n            foo # comment [alias: f]\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn left() {\n  Test::new()\n    .justfile(\n      \"\n        alias f := foo\n\n        # comment\n        foo:\n\n        bar:\n      \",\n    )\n    .args([\"--alias-style=left\", \"--list\"])\n    .stdout(\n      \"\n        Available recipes:\n            bar\n            foo # [alias: f] comment\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn separate() {\n  Test::new()\n    .justfile(\n      \"\n        alias f := foo\n\n        # comment\n        foo:\n\n        bar:\n      \",\n    )\n    .args([\"--alias-style=separate\", \"--list\"])\n    .stdout(\n      \"\n        Available recipes:\n            bar\n            foo # comment\n            f   # alias for `foo`\n      \",\n    )\n    .success();\n}\n"
  },
  {
    "path": "tests/allow_duplicate_recipes.rs",
    "content": "use super::*;\n\n#[test]\nfn allow_duplicate_recipes() {\n  Test::new()\n    .justfile(\n      \"\n      b:\n        echo foo\n      b:\n        echo bar\n\n      set allow-duplicate-recipes\n    \",\n    )\n    .stdout(\"bar\\n\")\n    .stderr(\"echo bar\\n\")\n    .success();\n}\n\n#[test]\nfn allow_duplicate_recipes_with_args() {\n  Test::new()\n    .justfile(\n      \"\n      b a:\n        echo foo\n      b c d:\n        echo bar {{c}} {{d}}\n\n      set allow-duplicate-recipes\n    \",\n    )\n    .args([\"b\", \"one\", \"two\"])\n    .stdout(\"bar one two\\n\")\n    .stderr(\"echo bar one two\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/allow_duplicate_variables.rs",
    "content": "use super::*;\n\n#[test]\nfn allow_duplicate_variables() {\n  Test::new()\n    .justfile(\n      \"\n      a := 'foo'\n      a := 'bar'\n\n      set allow-duplicate-variables\n\n      b:\n        echo {{a}}\n      \",\n    )\n    .arg(\"b\")\n    .stdout(\"bar\\n\")\n    .stderr(\"echo bar\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/allow_missing.rs",
    "content": "use super::*;\n\n#[test]\nfn allow_missing_recipes_in_run_invocation() {\n  Test::new()\n    .arg(\"foo\")\n    .stderr(\"error: Justfile does not contain recipe `foo`\\n\")\n    .failure();\n\n  Test::new().args([\"--allow-missing\", \"foo\"]).success();\n}\n\n#[test]\nfn allow_missing_modules_in_run_invocation() {\n  Test::new()\n    .arg(\"foo::bar\")\n    .stderr(\"error: Justfile does not contain submodule `foo`\\n\")\n    .failure();\n\n  Test::new().args([\"--allow-missing\", \"foo::bar\"]).success();\n}\n\n#[test]\nfn allow_missing_does_not_apply_to_compilation_errors() {\n  Test::new()\n    .justfile(\"bar: foo\")\n    .args([\"--allow-missing\", \"foo\"])\n    .stderr(\n      \"\n        error: Recipe `bar` has unknown dependency `foo`\n         ——▶ justfile:1:6\n          │\n        1 │ bar: foo\n          │      ^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn allow_missing_does_not_apply_to_other_subcommands() {\n  Test::new()\n    .args([\"--allow-missing\", \"--show\", \"foo\"])\n    .stderr(\"error: Justfile does not contain recipe `foo`\\n\")\n    .failure();\n}\n"
  },
  {
    "path": "tests/arg_attribute.rs",
    "content": "use super::*;\n\n#[test]\nfn pattern_match() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', pattern='BAR')]\n        foo bar:\n      \",\n    )\n    .args([\"foo\", \"BAR\"])\n    .success();\n}\n\n#[test]\nfn pattern_mismatch() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', pattern='BAR')]\n        foo bar:\n      \",\n    )\n    .args([\"foo\", \"bar\"])\n    .stderr(\n      \"\n        error: Argument `bar` passed to recipe `foo` parameter `bar` does not match pattern 'BAR'\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn patterns_are_regulare_expressions() {\n  Test::new()\n    .justfile(\n      r\"\n        [arg('bar', pattern='\\d+')]\n        foo bar:\n      \",\n    )\n    .args([\"foo\", r\"\\d+\"])\n    .stderr(\n      r\"\n        error: Argument `\\d+` passed to recipe `foo` parameter `bar` does not match pattern '\\d+'\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn pattern_must_match_entire_string() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', pattern='bar')]\n        foo bar:\n      \",\n    )\n    .args([\"foo\", \"xbarx\"])\n    .stderr(\n      \"\n        error: Argument `xbarx` passed to recipe `foo` parameter `bar` does not match pattern 'bar'\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn pattern_invalid_regex_error() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', pattern='{')]\n        foo bar:\n      \",\n    )\n    .stderr(\n      \"\n        error: Failed to parse argument pattern\n         ——▶ justfile:1:21\n          │\n        1 │ [arg('bar', pattern='{')]\n          │                     ^^^\n        caused by: regex parse error:\n            {\n            ^\n        error: repetition operator missing expression\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn dump() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', pattern='BAR')]\n        foo bar:\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\n      \"\n        [arg('bar', pattern='BAR')]\n        foo bar:\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn duplicate_attribute_error() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', pattern='BAR')]\n        [arg('bar', pattern='BAR')]\n        foo bar:\n      \",\n    )\n    .args([\"foo\", \"BAR\"])\n    .stderr(\n      \"\n        error: Recipe attribute for argument `bar` first used on line 1 is duplicated on line 2\n         ——▶ justfile:2:2\n          │\n        2 │ [arg('bar', pattern='BAR')]\n          │  ^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn extra_keyword_error() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', pattern='BAR', foo='foo')]\n        foo bar:\n      \",\n    )\n    .args([\"foo\", \"BAR\"])\n    .stderr(\n      \"\n        error: Unknown keyword `foo` for `arg` attribute\n         ——▶ justfile:1:28\n          │\n        1 │ [arg('bar', pattern='BAR', foo='foo')]\n          │                            ^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_argument_error() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', pattern='BAR')]\n        foo:\n      \",\n    )\n    .arg(\"foo\")\n    .stderr(\n      \"\n        error: Argument attribute for undefined argument `bar`\n         ——▶ justfile:1:6\n          │\n        1 │ [arg('bar', pattern='BAR')]\n          │      ^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn split_across_multiple_lines() {\n  Test::new()\n    .justfile(\n      \"\n        [arg(\n          'bar',\n          pattern='BAR'\n        )]\n        foo bar:\n      \",\n    )\n    .args([\"foo\", \"BAR\"])\n    .success();\n}\n\n#[test]\nfn optional_trailing_comma() {\n  Test::new()\n    .justfile(\n      \"\n        [arg(\n          'bar',\n          pattern='BAR',\n        )]\n        foo bar:\n      \",\n    )\n    .args([\"foo\", \"BAR\"])\n    .success();\n}\n\n#[test]\nfn positional_arguments_cannot_follow_keyword_arguments() {\n  Test::new()\n    .justfile(\n      \"\n        [arg(pattern='BAR', 'bar')]\n        foo bar:\n      \",\n    )\n    .args([\"foo\", \"BAR\"])\n    .stderr(\n      \"\n        error: Positional attribute arguments cannot follow keyword attribute arguments\n         ——▶ justfile:1:21\n          │\n        1 │ [arg(pattern='BAR', 'bar')]\n          │                     ^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn pattern_mismatches_are_caught_before_running_dependencies() {\n  Test::new()\n    .justfile(\n      \"\n        baz:\n          exit 1\n\n        [arg('bar', pattern='BAR')]\n        foo bar: baz\n      \",\n    )\n    .args([\"foo\", \"bar\"])\n    .stderr(\n      \"\n        error: Argument `bar` passed to recipe `foo` parameter `bar` does not match pattern 'BAR'\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn pattern_mismatches_are_caught_before_running_invocation() {\n  Test::new()\n    .justfile(\n      \"\n        baz:\n          exit 1\n\n        [arg('bar', pattern='BAR')]\n        foo bar: baz\n      \",\n    )\n    .args([\"baz\", \"foo\", \"bar\"])\n    .stderr(\n      \"\n        error: Argument `bar` passed to recipe `foo` parameter `bar` does not match pattern 'BAR'\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn pattern_mismatches_are_caught_in_evaluated_arguments() {\n  Test::new()\n    .justfile(\n      \"\n        bar: (foo 'ba' + 'r')\n\n        [arg('bar', pattern='BAR')]\n        foo bar:\n      \",\n    )\n    .stderr(\n      \"\n        error: Argument `bar` passed to recipe `foo` parameter `bar` does not match pattern 'BAR'\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn alternates_do_not_bind_to_anchors() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', pattern='a|b')]\n        foo bar:\n      \",\n    )\n    .args([\"foo\", \"aa\"])\n    .stderr(\n      \"\n        error: Argument `aa` passed to recipe `foo` parameter `bar` does not match pattern 'a|b'\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn pattern_match_variadic() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', pattern='BAR')]\n        foo *bar:\n      \",\n    )\n    .args([\"foo\", \"BAR\", \"BAR\"])\n    .success();\n}\n\n#[test]\nfn pattern_mismatch_variadic() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', pattern='BAR BAR')]\n        foo *bar:\n      \",\n    )\n    .args([\"foo\", \"BAR\", \"BAR\"])\n    .stderr(\n      \"\n        error: Argument `BAR` passed to recipe `foo` parameter `bar` does not match pattern 'BAR BAR'\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn pattern_requires_value() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', pattern)]\n        foo bar:\n      \",\n    )\n    .stderr(\n      \"\n        error: Attribute key `pattern` requires value\n         ——▶ justfile:1:13\n          │\n        1 │ [arg('bar', pattern)]\n          │             ^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn short_requires_value() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', short)]\n        foo bar:\n      \",\n    )\n    .stderr(\n      \"\n        error: Attribute key `short` requires value\n         ——▶ justfile:1:13\n          │\n        1 │ [arg('bar', short)]\n          │             ^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn value_requires_value() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long, value)]\n        foo bar:\n      \",\n    )\n    .stderr(\n      \"\n        error: Attribute key `value` requires value\n         ——▶ justfile:1:19\n          │\n        1 │ [arg('bar', long, value)]\n          │                   ^^^^^\n      \",\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/assert_stdout.rs",
    "content": "use super::*;\n\npub(crate) fn assert_stdout(output: &std::process::Output, stdout: &str) {\n  assert_success(output);\n  assert_eq!(String::from_utf8_lossy(&output.stdout), stdout);\n}\n"
  },
  {
    "path": "tests/assert_success.rs",
    "content": "#[track_caller]\npub(crate) fn assert_success(output: &std::process::Output) {\n  if !output.status.success() {\n    eprintln!(\"stderr: {}\", String::from_utf8_lossy(&output.stderr));\n    eprintln!(\"stdout: {}\", String::from_utf8_lossy(&output.stdout));\n    panic!(\"{}\", output.status);\n  }\n}\n"
  },
  {
    "path": "tests/assertions.rs",
    "content": "use super::*;\n\n#[test]\nfn assert_pass() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          {{ assert('a' == 'a', 'error message') }}\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn assert_fail() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          {{ assert('a' != 'a', 'error message') }}\n      \",\n    )\n    .stderr(\n      \"\n        error: Assert failed: error message\n         ——▶ justfile:2:6\n          │\n        2 │   {{ assert('a' != 'a', 'error message') }}\n          │      ^^^^^^\n      \",\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/assignment.rs",
    "content": "use super::*;\n\n#[test]\nfn set_export_parse_error() {\n  Test::new()\n    .justfile(\n      \"\n    set export := fals\n  \",\n    )\n    .stderr(\n      \"\n    error: Expected keyword `true` or `false` but found identifier `fals`\n     ——▶ justfile:1:15\n      │\n    1 │ set export := fals\n      │               ^^^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn set_export_parse_error_eol() {\n  Test::new()\n    .justfile(\n      \"\n    set export :=\n  \",\n    )\n    .stderr(\n      \"\n    error: Expected identifier, but found end of line\n     ——▶ justfile:1:14\n      │\n    1 │ set export :=\n      │              ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn invalid_attributes_are_an_error() {\n  Test::new()\n    .justfile(\n      \"\n        [group: 'bar']\n        x := 'foo'\n      \",\n    )\n    .args([\"--evaluate\", \"x\"])\n    .stderr(\n      \"\n        error: Assignment `x` has invalid attribute `group`\n         ——▶ justfile:2:1\n          │\n        2 │ x := 'foo'\n          │ ^\n      \",\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/attributes.rs",
    "content": "use super::*;\n\n#[test]\nfn all() {\n  Test::new()\n    .justfile(\n      \"\n      [dragonfly]\n      [freebsd]\n      [linux]\n      [macos]\n      [netbsd]\n      [no-exit-message]\n      [openbsd]\n      [unix]\n      [windows]\n      foo:\n        exit 1\n    \",\n    )\n    .stderr(\"exit 1\\n\")\n    .failure();\n}\n\n#[test]\nfn duplicate_attributes_are_disallowed() {\n  Test::new()\n    .justfile(\n      \"\n      [no-exit-message]\n      [no-exit-message]\n      foo:\n        echo bar\n    \",\n    )\n    .stderr(\n      \"\n      error: Recipe attribute `no-exit-message` first used on line 1 is duplicated on line 2\n       ——▶ justfile:2:2\n        │\n      2 │ [no-exit-message]\n        │  ^^^^^^^^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn multiple_attributes_one_line() {\n  Test::new()\n    .justfile(\n      \"\n      [macos,windows,linux,openbsd,freebsd,dragonfly,netbsd]\n      [no-exit-message]\n      foo:\n        exit 1\n    \",\n    )\n    .stderr(\"exit 1\\n\")\n    .failure();\n}\n\n#[test]\nfn multiple_attributes_one_line_error_message() {\n  Test::new()\n    .justfile(\n      \"\n      [macos,windows linux,openbsd,freebsd,dragonfly,netbsd]\n      [no-exit-message]\n      foo:\n        exit 1\n    \",\n    )\n    .stderr(\n      \"\n        error: Expected ']', ':', ',', or '(', but found identifier\n         ——▶ justfile:1:16\n          │\n        1 │ [macos,windows linux,openbsd,freebsd,dragonfly,netbsd]\n          │                ^^^^^\n          \",\n    )\n    .failure();\n}\n\n#[test]\nfn multiple_attributes_one_line_duplicate_check() {\n  Test::new()\n    .justfile(\n      \"\n      [macos, windows, linux, openbsd, freebsd, dragonfly, netbsd]\n      [linux]\n      foo:\n        exit 1\n    \",\n    )\n    .stderr(\n      \"\n      error: Recipe attribute `linux` first used on line 1 is duplicated on line 2\n       ——▶ justfile:2:2\n        │\n      2 │ [linux]\n        │  ^^^^^\n        \",\n    )\n    .failure();\n}\n\n#[test]\nfn unexpected_attribute_argument() {\n  Test::new()\n    .justfile(\n      \"\n      [private('foo')]\n      foo:\n        exit 1\n    \",\n    )\n    .stderr(\n      \"\n        error: Attribute `private` got 1 argument but takes 0 arguments\n         ——▶ justfile:1:2\n          │\n        1 │ [private('foo')]\n          │  ^^^^^^^\n          \",\n    )\n    .failure();\n}\n\n#[test]\nfn multiple_metadata_attributes() {\n  Test::new()\n    .justfile(\n      \"\n      [metadata('example')]\n      [metadata('sample')]\n      [no-exit-message]\n      foo:\n        exit 1\n    \",\n    )\n    .stderr(\"exit 1\\n\")\n    .failure();\n}\n\n#[test]\nfn multiple_metadata_attributes_with_multiple_args() {\n  Test::new()\n    .justfile(\n      \"\n      [metadata('example', 'arg1')]\n      [metadata('sample', 'argument')]\n      [no-exit-message]\n      foo:\n        exit 1\n    \",\n    )\n    .stderr(\"exit 1\\n\")\n    .failure();\n}\n\n#[test]\nfn expected_metadata_attribute_argument() {\n  Test::new()\n    .justfile(\n      \"\n      [metadata]\n      foo:\n        exit 1\n    \",\n    )\n    .stderr(\n      \"\n        error: Attribute `metadata` got 0 arguments but takes at least 1 argument\n         ——▶ justfile:1:2\n          │\n        1 │ [metadata]\n          │  ^^^^^^^^\n          \",\n    )\n    .failure();\n}\n\n#[test]\nfn doc_attribute() {\n  Test::new()\n    .justfile(\n      \"\n    # Non-document comment\n    [doc('The real docstring')]\n    foo:\n      echo foo\n  \",\n    )\n    .args([\"--list\"])\n    .stdout(\n      \"\n    Available recipes:\n        foo # The real docstring\n        \",\n    )\n    .success();\n}\n\n#[test]\nfn doc_attribute_suppress() {\n  Test::new()\n    .justfile(\n      \"\n        # Non-document comment\n        [doc]\n        foo:\n          echo foo\n      \",\n    )\n    .args([\"--list\"])\n    .stdout(\n      \"\n    Available recipes:\n        foo\n        \",\n    )\n    .success();\n}\n\n#[test]\nfn doc_multiline() {\n  Test::new()\n    .justfile(\n      \"\n        [doc('multiline\n        comment')]\n        foo:\n      \",\n    )\n    .args([\"--list\"])\n    .stdout(\n      \"\n    Available recipes:\n        # multiline\n        # comment\n        foo\n        \",\n    )\n    .success();\n}\n\n#[test]\nfn extension() {\n  Test::new()\n    .justfile(\n      \"\n        [extension: '.txt']\n        baz:\n          #!/bin/sh\n          echo $0\n      \",\n    )\n    .stdout_regex(r\"*baz\\.txt\\n\")\n    .success();\n}\n\n#[test]\nfn extension_on_linewise_error() {\n  Test::new()\n    .justfile(\n      \"\n        [extension: '.txt']\n        baz:\n      \",\n    )\n    .stderr(\n      \"\n  error: Recipe `baz` has invalid attribute `extension`\n   ——▶ justfile:2:1\n    │\n  2 │ baz:\n    │ ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn duplicate_non_repeatable_attributes_are_forbidden() {\n  Test::new()\n    .justfile(\n      \"\n        [confirm: 'yes']\n        [confirm: 'no']\n        baz:\n      \",\n    )\n    .stderr(\n      \"\n  error: Recipe attribute `confirm` first used on line 1 is duplicated on line 2\n   ——▶ justfile:2:2\n    │\n  2 │ [confirm: 'no']\n    │  ^^^^^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn shell_expanded_strings_can_be_used_in_attributes() {\n  Test::new()\n    .justfile(\n      \"\n        [doc(x'foo')]\n        bar:\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn env_attribute_single() {\n  Test::new()\n    .justfile(\n      \"\n        [env('MY_VAR', 'my_value')]\n        foo:\n          @echo $MY_VAR\n      \",\n    )\n    .stdout(\"my_value\\n\")\n    .success();\n}\n\n#[test]\nfn env_attribute_multiple() {\n  Test::new()\n    .justfile(\n      \"\n        [env('VAR1', 'value1')]\n        [env('VAR2', 'value 2')]\n        foo:\n          @echo $VAR1 $VAR2\n      \",\n    )\n    .stdout(\"value1 value 2\\n\")\n    .success();\n}\n\n#[test]\nfn env_attribute_in_recipe_params() {\n  Test::new()\n    .justfile(\n      r#\"\n[env(\"foo\", \"bar\")]\nbaz x=`echo ${foo}.txt`:\n    @echo {{x}}\n\"#,\n    )\n    .stdout(\"bar.txt\\n\")\n    .success();\n}\n\n#[test]\nfn env_attribute_not_in_env_function() {\n  Test::new()\n    .justfile(\n      r#\"\n\n[env(\"foo\", \"bar\")]\nbaz:\n  @echo {{ env(\"foo\") }}.txt\n\n    \"#,\n    )\n    .stderr(\n      r#\"\nerror: Call to function `env` failed: environment variable `foo` not present\n ——▶ justfile:4:12\n  │\n4 │   @echo {{ env(\"foo\") }}.txt\n  │            ^^^\n\n\"#,\n    )\n    .failure();\n}\n\n#[test]\nfn env_attribute_too_few_arguments() {\n  Test::new()\n    .justfile(\n      \"\n        [env('MY_VAR')]\n        foo:\n          echo bar\n      \",\n    )\n    .stderr(\n      \"\n  error: Attribute `env` got 1 argument but takes 2 arguments\n   ——▶ justfile:1:2\n    │\n  1 │ [env('MY_VAR')]\n    │  ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn env_attribute_too_many_arguments() {\n  Test::new()\n    .justfile(\n      \"\n        [env('A', 'B', 'C')]\n        foo:\n          echo bar\n      \",\n    )\n    .stderr(\n      \"\n  error: Attribute `env` got 3 arguments but takes 2 arguments\n   ——▶ justfile:1:2\n    │\n  1 │ [env('A', 'B', 'C')]\n    │  ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn env_attribute_duplicate_error() {\n  Test::new()\n    .justfile(\n      \"\n        [env('VAR1', 'value1')]\n        [env('VAR1', 'value 2')]\n        foo:\n          @echo $VAR1\n      \",\n    )\n    .stderr(\n      \"\n  error: Environment variable `VAR1` first set on line 1 is set again on line 2\n   ——▶ justfile:2:2\n    │\n  2 │ [env('VAR1', 'value 2')]\n    │  ^^^\n\",\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/backticks.rs",
    "content": "use super::*;\n\n#[test]\nfn trailing_newlines_are_stripped() {\n  Test::new()\n    .shell(false)\n    .args([\"--evaluate\", \"foos\"])\n    .justfile(\n      \"\nset shell := ['python3', '-c']\n\nfoos := `print('foo' * 4)`\n      \",\n    )\n    .stdout(\"foofoofoofoo\")\n    .success();\n}\n"
  },
  {
    "path": "tests/byte_order_mark.rs",
    "content": "use super::*;\n\n#[test]\nfn ignore_leading_byte_order_mark() {\n  Test::new()\n    .justfile(\n      \"\n      \\u{feff}foo:\n        echo bar\n    \",\n    )\n    .stderr(\"echo bar\\n\")\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn non_leading_byte_order_mark_produces_error() {\n  Test::new()\n    .justfile(\n      \"\n      foo:\n        echo bar\n      \\u{feff}\n    \",\n    )\n    .stderr(\n      \"\n      error: Expected \\'@\\', \\'[\\', comment, end of file, end of line, or identifier, but found byte order mark\n       ——▶ justfile:3:1\n        │\n      3 │ \\u{feff}\n        │ ^\n      \")\n    .failure();\n}\n\n#[test]\nfn dont_mention_byte_order_mark_in_errors() {\n  Test::new()\n    .justfile(\"{\")\n    .stderr(\n      \"\n      error: Expected '@', '[', comment, end of file, end of line, or identifier, but found '{'\n       ——▶ justfile:1:1\n        │\n      1 │ {\n        │ ^\n      \",\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/ceiling.rs",
    "content": "use super::*;\n\n#[test]\nfn justfile_run_search_stops_at_ceiling_dir() {\n  let tempdir = tempdir();\n\n  let ceiling = tempdir.path().join(\"foo\");\n\n  fs::create_dir(&ceiling).unwrap();\n\n  let ceiling = if cfg!(not(windows)) {\n    ceiling.canonicalize().unwrap()\n  } else {\n    ceiling\n  };\n\n  Test::with_tempdir(tempdir)\n    .justfile(\n      \"\n        foo:\n          echo bar\n      \",\n    )\n    .create_dir(\"foo/bar\")\n    .current_dir(\"foo/bar\")\n    .args([\"--ceiling\", ceiling.to_str().unwrap()])\n    .stderr(\"error: No justfile found\\n\")\n    .failure();\n}\n\n#[test]\nfn ceiling_can_be_passed_as_environment_variable() {\n  let tempdir = tempdir();\n\n  let ceiling = tempdir.path().join(\"foo\");\n\n  fs::create_dir(&ceiling).unwrap();\n\n  let ceiling = if cfg!(not(windows)) {\n    ceiling.canonicalize().unwrap()\n  } else {\n    ceiling\n  };\n\n  Test::with_tempdir(tempdir)\n    .justfile(\n      \"\n        foo:\n          echo bar\n      \",\n    )\n    .create_dir(\"foo/bar\")\n    .current_dir(\"foo/bar\")\n    .env(\"JUST_CEILING\", ceiling.to_str().unwrap())\n    .stderr(\"error: No justfile found\\n\")\n    .failure();\n}\n\n#[test]\nfn justfile_init_search_stops_at_ceiling_dir() {\n  let tempdir = tempdir();\n\n  let ceiling = tempdir.path().join(\"foo\");\n\n  fs::create_dir(&ceiling).unwrap();\n\n  let ceiling = if cfg!(not(windows)) {\n    ceiling.canonicalize().unwrap()\n  } else {\n    ceiling\n  };\n\n  let Output { tempdir, .. } = Test::with_tempdir(tempdir)\n    .no_justfile()\n    .test_round_trip(false)\n    .create_dir(\".git\")\n    .create_dir(\"foo/bar\")\n    .current_dir(\"foo/bar\")\n    .args([\"--init\", \"--ceiling\", ceiling.to_str().unwrap()])\n    .stderr_regex(if cfg!(windows) {\n      r\"Wrote justfile to `.*\\\\foo\\\\bar\\\\justfile`\\n\"\n    } else {\n      \"Wrote justfile to `.*/foo/bar/justfile`\\n\"\n    })\n    .success();\n\n  assert_eq!(\n    fs::read_to_string(tempdir.path().join(\"foo/bar/justfile\")).unwrap(),\n    just::INIT_JUSTFILE\n  );\n}\n"
  },
  {
    "path": "tests/changelog.rs",
    "content": "use super::*;\n\n#[test]\nfn print_changelog() {\n  Test::new()\n    .args([\"--changelog\"])\n    .stdout(fs::read_to_string(\"CHANGELOG.md\").unwrap())\n    .success();\n}\n"
  },
  {
    "path": "tests/choose.rs",
    "content": "use super::*;\n\n#[test]\nfn env() {\n  Test::new()\n    .arg(\"--choose\")\n    .env(\"JUST_CHOOSER\", \"head -n1\")\n    .justfile(\n      \"\n        foo:\n          echo foo\n\n        bar:\n          echo bar\n      \",\n    )\n    .stderr(\"echo bar\\n\")\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn chooser() {\n  Test::new()\n    .arg(\"--choose\")\n    .arg(\"--chooser\")\n    .arg(\"head -n1\")\n    .justfile(\n      \"\n        foo:\n          echo foo\n\n        bar:\n          echo bar\n      \",\n    )\n    .stderr(\"echo bar\\n\")\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn override_variable() {\n  Test::new()\n    .arg(\"--choose\")\n    .arg(\"baz=B\")\n    .env(\"JUST_CHOOSER\", \"head -n1\")\n    .justfile(\n      \"\n        baz := 'A'\n\n        foo:\n          echo foo\n\n        bar:\n          echo {{baz}}\n      \",\n    )\n    .stderr(\"echo B\\n\")\n    .stdout(\"B\\n\")\n    .success();\n}\n\n#[test]\nfn skip_private_recipes() {\n  Test::new()\n    .arg(\"--choose\")\n    .env(\"JUST_CHOOSER\", \"head -n1\")\n    .justfile(\n      \"\n        foo:\n          echo foo\n\n        _bar:\n          echo bar\n      \",\n    )\n    .stderr(\"echo foo\\n\")\n    .stdout(\"foo\\n\")\n    .success();\n}\n\n#[test]\nfn recipes_in_submodules_can_be_chosen() {\n  Test::new()\n    .args([\"--unstable\", \"--choose\"])\n    .env(\"JUST_CHOOSER\", \"head -n10\")\n    .write(\"bar.just\", \"baz:\\n echo BAZ\")\n    .justfile(\n      \"\n        mod bar\n      \",\n    )\n    .stderr(\"echo BAZ\\n\")\n    .stdout(\"BAZ\\n\")\n    .success();\n}\n\n#[test]\nfn skip_recipes_that_require_arguments() {\n  Test::new()\n    .arg(\"--choose\")\n    .env(\"JUST_CHOOSER\", \"head -n1\")\n    .justfile(\n      \"\n        foo:\n          echo foo\n\n        bar BAR:\n          echo {{BAR}}\n      \",\n    )\n    .stderr(\"echo foo\\n\")\n    .stdout(\"foo\\n\")\n    .success();\n}\n\n#[test]\nfn no_choosable_recipes() {\n  Test::new()\n    .arg(\"--choose\")\n    .justfile(\n      \"\n        _foo:\n          echo foo\n\n        bar BAR:\n          echo {{BAR}}\n      \",\n    )\n    .stderr(\"error: Justfile contains no choosable recipes.\\n\")\n    .failure();\n}\n\n#[test]\nfn multiple_recipes() {\n  Test::new()\n    .arg(\"--choose\")\n    .arg(\"--chooser\")\n    .arg(\"echo foo bar\")\n    .justfile(\n      \"\n        foo:\n          echo foo\n\n        bar:\n          echo bar\n      \",\n    )\n    .stderr(\"echo foo\\necho bar\\n\")\n    .stdout(\"foo\\nbar\\n\")\n    .success();\n}\n\n#[test]\nfn invoke_error_function() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          echo foo\n\n        bar:\n          echo bar\n      \",\n    )\n    .stderr_regex(\n      r#\"error: Chooser `/ -cu fzf --multi --preview 'just --unstable --color always --justfile \".*justfile\" --show \\{\\}'` invocation failed: .*\\n\"#,\n    )\n    .shell(false)\n    .args([\"--shell\", \"/\", \"--choose\"])\n    .failure();\n}\n\n#[test]\nfn status_error() {\n  if cfg!(windows) {\n    return;\n  }\n  let tmp = temptree! {\n    justfile: \"foo:\\n echo foo\\nbar:\\n echo bar\\n\",\n    \"exit-2\": \"#!/usr/bin/env bash\\nexit 2\\n\",\n  };\n\n  let output = Command::new(\"chmod\")\n    .arg(\"+x\")\n    .arg(tmp.path().join(\"exit-2\"))\n    .output()\n    .unwrap();\n\n  assert!(output.status.success());\n\n  let path = env::join_paths(\n    iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os(\"PATH\").unwrap())),\n  )\n  .unwrap();\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--choose\")\n    .arg(\"--chooser\")\n    .arg(\"exit-2\")\n    .env(\"PATH\", path)\n    .output()\n    .unwrap();\n\n  assert!(\n    Regex::new(\"^error: Chooser `exit-2` failed: exit (code|status): 2\\n$\")\n      .unwrap()\n      .is_match(str::from_utf8(&output.stderr).unwrap())\n  );\n\n  assert_eq!(output.status.code().unwrap(), 2);\n}\n\n#[test]\nfn cancelled_by_user() {\n  if cfg!(windows) {\n    return;\n  }\n\n  let tmp = temptree! {\n    justfile: \"foo:\\n echo foo\\nbar:\\n echo bar\\n\",\n    chooser: \"#!/usr/bin/env bash\\nexit 130\\n\",\n  };\n\n  let output = Command::new(\"chmod\")\n    .arg(\"+x\")\n    .arg(tmp.path().join(\"chooser\"))\n    .output()\n    .unwrap();\n\n  assert!(output.status.success());\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--choose\")\n    .arg(\"--chooser\")\n    .arg(\"./chooser\")\n    .output()\n    .unwrap();\n\n  assert!(output.stderr.is_empty());\n\n  assert!(output.status.success());\n}\n\n#[test]\nfn default() {\n  let tmp = temptree! {\n    justfile: \"foo:\\n echo foo\\n\",\n  };\n\n  let cat = which(\"cat\").unwrap();\n  let fzf = tmp.path().join(format!(\"fzf{EXE_SUFFIX}\"));\n\n  #[cfg(unix)]\n  std::os::unix::fs::symlink(cat, fzf).unwrap();\n\n  #[cfg(windows)]\n  std::os::windows::fs::symlink_file(cat, fzf).unwrap();\n\n  let path = env::join_paths(\n    iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os(\"PATH\").unwrap())),\n  )\n  .unwrap();\n\n  let output = Command::new(JUST)\n    .arg(\"--choose\")\n    .arg(\"--chooser=fzf\")\n    .current_dir(tmp.path())\n    .env(\"PATH\", path)\n    .output()\n    .unwrap();\n\n  assert_stdout(&output, \"foo\\n\");\n}\n"
  },
  {
    "path": "tests/command.rs",
    "content": "use super::*;\n\n#[test]\nfn long() {\n  Test::new()\n    .arg(\"--command\")\n    .arg(\"printf\")\n    .arg(\"foo\")\n    .justfile(\n      \"\n    x:\n      echo XYZ\n  \",\n    )\n    .stdout(\"foo\")\n    .success();\n}\n\n#[test]\nfn short() {\n  Test::new()\n    .arg(\"-c\")\n    .arg(\"printf\")\n    .arg(\"foo\")\n    .justfile(\n      \"\n    x:\n      echo XYZ\n  \",\n    )\n    .stdout(\"foo\")\n    .success();\n}\n\n#[test]\nfn command_color() {\n  Test::new()\n    .arg(\"--color\")\n    .arg(\"always\")\n    .arg(\"--command-color\")\n    .arg(\"cyan\")\n    .justfile(\n      \"\n    x:\n      echo XYZ\n  \",\n    )\n    .stdout(\"XYZ\\n\")\n    .stderr(\"\\u{1b}[1;36mecho XYZ\\u{1b}[0m\\n\")\n    .success();\n}\n\n#[test]\nfn no_binary() {\n  Test::new()\n    .arg(\"--command\")\n    .justfile(\n      \"\n    x:\n      echo XYZ\n  \",\n    )\n    .stderr(\n      \"\n    error: a value is required for '--command <COMMAND>...' but none was supplied\n\n    For more information, try '--help'.\n  \",\n    )\n    .status(2);\n}\n\n#[test]\nfn env_is_loaded() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-load\n\n        x:\n          echo XYZ\n      \",\n    )\n    .args([\"--command\", \"sh\", \"-c\", \"printf $DOTENV_KEY\"])\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .stdout(\"dotenv-value\")\n    .success();\n}\n\n#[test]\nfn exports_are_available() {\n  Test::new()\n    .arg(\"--command\")\n    .arg(\"sh\")\n    .arg(\"-c\")\n    .arg(\"printf $FOO\")\n    .justfile(\n      \"\n    export FOO := 'bar'\n\n    x:\n      echo XYZ\n  \",\n    )\n    .stdout(\"bar\")\n    .success();\n}\n\n#[test]\nfn set_overrides_work() {\n  Test::new()\n    .arg(\"--set\")\n    .arg(\"FOO\")\n    .arg(\"baz\")\n    .arg(\"--command\")\n    .arg(\"sh\")\n    .arg(\"-c\")\n    .arg(\"printf $FOO\")\n    .justfile(\n      \"\n    export FOO := 'bar'\n\n    x:\n      echo XYZ\n  \",\n    )\n    .stdout(\"baz\")\n    .success();\n}\n\n#[test]\nfn run_in_shell() {\n  Test::new()\n    .arg(\"--shell-command\")\n    .arg(\"--command\")\n    .arg(\"bar baz\")\n    .justfile(\n      \"\n    set shell := ['printf']\n  \",\n    )\n    .stdout(\"bar baz\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn exit_status() {\n  Test::new()\n    .arg(\"--command\")\n    .arg(\"false\")\n    .justfile(\n      \"\n    x:\n      echo XYZ\n  \",\n    )\n    .stderr_regex(\"error: Command `false` failed: exit (code|status): 1\\n\")\n    .failure();\n}\n\n#[test]\nfn working_directory_is_correct() {\n  let tmp = tempdir();\n\n  fs::write(tmp.path().join(\"justfile\"), \"\").unwrap();\n  fs::write(tmp.path().join(\"bar\"), \"baz\").unwrap();\n  fs::create_dir(tmp.path().join(\"foo\")).unwrap();\n\n  let output = Command::new(JUST)\n    .args([\"--command\", \"cat\", \"bar\"])\n    .current_dir(tmp.path().join(\"foo\"))\n    .output()\n    .unwrap();\n\n  assert_eq!(str::from_utf8(&output.stderr).unwrap(), \"\");\n\n  assert!(output.status.success());\n\n  assert_eq!(str::from_utf8(&output.stdout).unwrap(), \"baz\");\n}\n\n#[test]\nfn command_not_found() {\n  let tmp = tempdir();\n\n  fs::write(tmp.path().join(\"justfile\"), \"\").unwrap();\n\n  let output = Command::new(JUST)\n    .args([\"--command\", \"asdfasdfasdfasdfadfsadsfadsf\", \"bar\"])\n    .output()\n    .unwrap();\n\n  assert!(\n    str::from_utf8(&output.stderr)\n      .unwrap()\n      .starts_with(\"error: Failed to invoke `asdfasdfasdfasdfadfsadsfadsf` `bar`:\"),\n  );\n\n  assert!(!output.status.success());\n}\n\n#[test]\nfn dont_evaluate_unnecessary_variables() {\n  Test::new()\n    .justfile(\n      \"\n      export x := 'FOO'\n\n      y := `exit 1`\n    \",\n    )\n    .args([\"--command\", \"bash\", \"-c\", \"echo $x\"])\n    .stdout(\"FOO\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/completions/just.bash",
    "content": "#!/usr/bin/env bash\n\n# --- Shared functions ---\nreply_equals() {\n  local reply=$(declare -p COMPREPLY)\n  local expected=$1\n\n  if [ \"$reply\" = \"$expected\" ]; then\n    echo \"${FUNCNAME[1]}: ok\"\n  else\n    exit_code=1\n    echo >&2 \"${FUNCNAME[1]}: failed! Completion for \\`${COMP_WORDS[*]}\\` does not match.\"\n\n    echo\n    diff -U3 --label expected <(echo \"$expected\") --label actual <(echo \"$reply\") >&2\n    echo\n  fi\n}\n\n# --- Initial Setup ---\nsource \"$1\"\ncd tests/completions\ncargo build\nPATH=\"$(git rev-parse --show-toplevel)/target/debug:$PATH\"\nexit_code=0\n\n# --- Tests ---\ntest_complete_all_recipes() {\n  COMP_WORDS=(just)\n  COMP_CWORD=1 _just just\n  reply_equals 'declare -a COMPREPLY=([0]=\"deploy\" [1]=\"install\" [2]=\"publish\" [3]=\"push\" [4]=\"test\")'\n}\ntest_complete_all_recipes\n\ntest_complete_recipes_starting_with_i() {\n  COMP_WORDS=(just i)\n  COMP_CWORD=1 _just just\n  reply_equals 'declare -a COMPREPLY=([0]=\"install\")'\n}\ntest_complete_recipes_starting_with_i\n\ntest_complete_recipes_starting_with_p() {\n  COMP_WORDS=(just p)\n  COMP_CWORD=1 _just just\n  reply_equals 'declare -a COMPREPLY=([0]=\"publish\" [1]=\"push\")'\n}\ntest_complete_recipes_starting_with_p\n\ntest_complete_recipes_from_subdirs() {\n  COMP_WORDS=(just subdir/)\n  COMP_CWORD=1 _just just\n  reply_equals 'declare -a COMPREPLY=([0]=\"subdir/special\" [1]=\"subdir/surprise\")'\n}\ntest_complete_recipes_from_subdirs\n\n# --- Conclusion ---\nif [ $exit_code = 0 ]; then\n  echo \"All tests passed.\"\nelse\n  echo \"Some test[s] failed.\"\nfi\n\nexit $exit_code\n"
  },
  {
    "path": "tests/completions/justfile",
    "content": "install:\ntest:\ndeploy:\npush:\npublish:\n"
  },
  {
    "path": "tests/completions/subdir/justfile",
    "content": "special:\nsurprise:\n"
  },
  {
    "path": "tests/completions.rs",
    "content": "use super::*;\n\n#[test]\nfn bash() {\n  if cfg!(not(target_os = \"linux\")) {\n    return;\n  }\n  let output = Command::new(JUST)\n    .args([\"--completions\", \"bash\"])\n    .output()\n    .unwrap();\n\n  assert!(output.status.success());\n\n  let script = str::from_utf8(&output.stdout).unwrap();\n\n  let tempdir = tempdir();\n\n  let path = tempdir.path().join(\"just.bash\");\n\n  fs::write(&path, script).unwrap();\n\n  let status = Command::new(\"./tests/completions/just.bash\")\n    .arg(path)\n    .status()\n    .unwrap();\n\n  assert!(status.success());\n}\n\n#[test]\nfn replacements() {\n  for shell in [\"bash\", \"elvish\", \"fish\", \"nushell\", \"powershell\", \"zsh\"] {\n    let output = Command::new(JUST)\n      .args([\"--completions\", shell])\n      .output()\n      .unwrap();\n    assert!(\n      output.status.success(),\n      \"shell completion generation for {shell} failed: {}\\n{}\",\n      output.status,\n      String::from_utf8_lossy(&output.stderr),\n    );\n  }\n}\n"
  },
  {
    "path": "tests/conditional.rs",
    "content": "use super::*;\n\n#[test]\nfn then_branch_unevaluated() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ if 'a' == 'b' { `exit 1` } else { 'otherwise' } }}\n  \",\n    )\n    .stdout(\"otherwise\\n\")\n    .stderr(\"echo otherwise\\n\")\n    .success();\n}\n\n#[test]\nfn otherwise_branch_unevaluated() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ if 'a' == 'a' { 'then' } else { `exit 1` } }}\n  \",\n    )\n    .stdout(\"then\\n\")\n    .stderr(\"echo then\\n\")\n    .success();\n}\n\n#[test]\nfn otherwise_branch_unevaluated_inverted() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ if 'a' != 'b' { 'then' } else { `exit 1` } }}\n  \",\n    )\n    .stdout(\"then\\n\")\n    .stderr(\"echo then\\n\")\n    .success();\n}\n\n#[test]\nfn then_branch_unevaluated_inverted() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ if 'a' != 'a' { `exit 1` } else { 'otherwise' } }}\n  \",\n    )\n    .stdout(\"otherwise\\n\")\n    .stderr(\"echo otherwise\\n\")\n    .success();\n}\n\n#[test]\nfn complex_expressions() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ if 'a' + 'b' == `echo ab` { 'c' + 'd' } else { 'e' + 'f' } }}\n  \",\n    )\n    .stdout(\"cd\\n\")\n    .stderr(\"echo cd\\n\")\n    .success();\n}\n\n#[test]\nfn undefined_lhs() {\n  Test::new()\n    .justfile(\n      \"\n    a := if b == '' { '' } else { '' }\n\n    foo:\n      echo {{ a }}\n  \",\n    )\n    .stderr(\n      \"\n    error: Variable `b` not defined\n     ——▶ justfile:1:9\n      │\n    1 │ a := if b == '' { '' } else { '' }\n      │         ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn undefined_rhs() {\n  Test::new()\n    .justfile(\n      \"\n    a := if '' == b { '' } else { '' }\n\n    foo:\n      echo {{ a }}\n  \",\n    )\n    .stderr(\n      \"\n    error: Variable `b` not defined\n     ——▶ justfile:1:15\n      │\n    1 │ a := if '' == b { '' } else { '' }\n      │               ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn undefined_then() {\n  Test::new()\n    .justfile(\n      \"\n    a := if '' == '' { b } else { '' }\n\n    foo:\n      echo {{ a }}\n  \",\n    )\n    .stderr(\n      \"\n    error: Variable `b` not defined\n     ——▶ justfile:1:20\n      │\n    1 │ a := if '' == '' { b } else { '' }\n      │                    ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn undefined_otherwise() {\n  Test::new()\n    .justfile(\n      \"\n    a := if '' == '' { '' } else { b }\n\n    foo:\n      echo {{ a }}\n  \",\n    )\n    .stderr(\n      \"\n    error: Variable `b` not defined\n     ——▶ justfile:1:32\n      │\n    1 │ a := if '' == '' { '' } else { b }\n      │                                ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn unexpected_op() {\n  Test::new()\n    .justfile(\n      \"\n    a := if '' a '' { '' } else { b }\n\n    foo:\n      echo {{ a }}\n  \",\n    )\n    .stderr(\n      \"\n    error: Expected '&&', '!=', '!~', '||', '==', '=~', '+', or '/', but found identifier\n     ——▶ justfile:1:12\n      │\n    1 │ a := if '' a '' { '' } else { b }\n      │            ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn dump() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    a := if '' == '' { '' } else { '' }\n\n    foo:\n      echo {{ a }}\n  \",\n    )\n    .stdout(\n      \"\n    a := if '' == '' { '' } else { '' }\n\n    foo:\n        echo {{ a }}\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn if_else() {\n  Test::new()\n    .justfile(\n      \"\n    x := if '0' == '1' { 'a' } else if '0' == '0' { 'b' } else { 'c' }\n\n    foo:\n      echo {{ x }}\n  \",\n    )\n    .stdout(\"b\\n\")\n    .stderr(\"echo b\\n\")\n    .success();\n}\n\n#[test]\nfn missing_else() {\n  Test::new()\n    .justfile(\n      \"\n  TEST := if path_exists('/bin/bash') == 'true' {'yes'}\n  \",\n    )\n    .stderr(\n      \"\n    error: Expected keyword `else` but found `end of line`\n     ——▶ justfile:1:54\n      │\n    1 │ TEST := if path_exists('/bin/bash') == 'true' {'yes'}\n      │                                                      ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn incorrect_else_identifier() {\n  Test::new()\n    .justfile(\n      \"\n  TEST := if path_exists('/bin/bash') == 'true' {'yes'} els {'no'}\n  \",\n    )\n    .stderr(\n      \"\n    error: Expected keyword `else` but found identifier `els`\n     ——▶ justfile:1:55\n      │\n    1 │ TEST := if path_exists('/bin/bash') == 'true' {'yes'} els {'no'}\n      │                                                       ^^^\n  \",\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/confirm.rs",
    "content": "use super::*;\n\n#[test]\nfn confirm_recipe_arg() {\n  Test::new()\n    .arg(\"--yes\")\n    .justfile(\n      \"\n        [confirm]\n        requires_confirmation:\n            echo confirmed\n        \",\n    )\n    .stderr(\"echo confirmed\\n\")\n    .stdout(\"confirmed\\n\")\n    .success();\n}\n\n#[test]\nfn recipe_with_confirm_recipe_dependency_arg() {\n  Test::new()\n    .arg(\"--yes\")\n    .justfile(\n      \"\n        dep_confirmation: requires_confirmation\n            echo confirmed2\n\n        [confirm]\n        requires_confirmation:\n            echo confirmed\n        \",\n    )\n    .stderr(\"echo confirmed\\necho confirmed2\\n\")\n    .stdout(\"confirmed\\nconfirmed2\\n\")\n    .success();\n}\n\n#[test]\nfn confirm_recipe() {\n  Test::new()\n    .justfile(\n      \"\n        [confirm]\n        requires_confirmation:\n            echo confirmed\n        \",\n    )\n    .stderr(\"Run recipe `requires_confirmation`? echo confirmed\\n\")\n    .stdout(\"confirmed\\n\")\n    .stdin(\"y\")\n    .success();\n}\n\n#[test]\nfn recipe_with_confirm_recipe_dependency() {\n  Test::new()\n    .justfile(\n      \"\n        dep_confirmation: requires_confirmation\n            echo confirmed2\n\n        [confirm]\n        requires_confirmation:\n            echo confirmed\n        \",\n    )\n    .stderr(\"Run recipe `requires_confirmation`? echo confirmed\\necho confirmed2\\n\")\n    .stdout(\"confirmed\\nconfirmed2\\n\")\n    .stdin(\"y\")\n    .success();\n}\n\n#[test]\nfn do_not_confirm_recipe() {\n  Test::new()\n    .justfile(\n      \"\n        [confirm]\n        requires_confirmation:\n            echo confirmed\n        \",\n    )\n    .stderr(\"Run recipe `requires_confirmation`? error: Recipe `requires_confirmation` was not confirmed\\n\")\n    .failure();\n}\n\n#[test]\nfn do_not_confirm_recipe_with_confirm_recipe_dependency() {\n  Test::new()\n    .justfile(\n      \"\n        dep_confirmation: requires_confirmation\n            echo mistake\n\n        [confirm]\n        requires_confirmation:\n            echo confirmed\n        \",\n    )\n    .stderr(\"Run recipe `requires_confirmation`? error: Recipe `requires_confirmation` was not confirmed\\n\")\n    .failure();\n}\n\n#[test]\nfn confirm_recipe_with_prompt() {\n  Test::new()\n    .justfile(\n      \"\n        [confirm(\\\"This is dangerous - are you sure you want to run it?\\\")]\n        requires_confirmation:\n            echo confirmed\n        \",\n    )\n    .stderr(\"This is dangerous - are you sure you want to run it? echo confirmed\\n\")\n    .stdout(\"confirmed\\n\")\n    .stdin(\"y\")\n    .success();\n}\n\n#[test]\nfn confirm_recipe_with_prompt_too_many_args() {\n  Test::new()\n    .justfile(\n      r#\"\n        [confirm(\"PROMPT\",\"EXTRA\")]\n        requires_confirmation:\n            echo confirmed\n      \"#,\n    )\n    .stderr(\n      r#\"\n        error: Attribute `confirm` got 2 arguments but takes at most 1 argument\n         ——▶ justfile:1:2\n          │\n        1 │ [confirm(\"PROMPT\",\"EXTRA\")]\n          │  ^^^^^^^\n      \"#,\n    )\n    .failure();\n}\n\n#[test]\nfn confirm_attribute_is_formatted_correctly() {\n  Test::new()\n    .justfile(\n      \"\n        [confirm('prompt')]\n        foo:\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\"[confirm('prompt')]\\nfoo:\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/constants.rs",
    "content": "use super::*;\n\n#[test]\nfn constants_are_defined() {\n  assert_eval_eq(\"HEX\", \"0123456789abcdef\");\n}\n\n#[test]\nfn constants_can_have_different_values_on_windows() {\n  assert_eval_eq(\"PATH_SEP\", if cfg!(windows) { \"\\\\\" } else { \"/\" });\n  assert_eval_eq(\"PATH_VAR_SEP\", if cfg!(windows) { \";\" } else { \":\" });\n}\n\n#[test]\nfn constants_are_defined_in_recipe_bodies() {\n  Test::new()\n    .justfile(\n      \"\n        @foo:\n          echo {{HEX}}\n      \",\n    )\n    .stdout(\"0123456789abcdef\\n\")\n    .success();\n}\n\n#[test]\nfn constants_are_defined_in_recipe_parameters() {\n  Test::new()\n    .justfile(\n      \"\n        @foo hex=HEX:\n          echo {{hex}}\n      \",\n    )\n    .stdout(\"0123456789abcdef\\n\")\n    .success();\n}\n\n#[test]\nfn constants_can_be_redefined() {\n  Test::new()\n    .justfile(\n      \"\n        HEX := 'foo'\n      \",\n    )\n    .args([\"--evaluate\", \"HEX\"])\n    .stdout(\"foo\")\n    .success();\n}\n\n#[test]\nfn constants_are_not_exported() {\n  Test::new()\n    .justfile(\n      r#\"\n        set export\n\n        foo:\n          @'{{just_executable()}}' --request '{\"environment-variable\": \"HEXUPPER\"}'\n      \"#,\n    )\n    .response(Response::EnvironmentVariable(None))\n    .success();\n}\n"
  },
  {
    "path": "tests/datetime.rs",
    "content": "use super::*;\n\n#[test]\nfn datetime() {\n  Test::new()\n    .justfile(\n      \"\n        x := datetime('%Y-%m-%d %z')\n      \",\n    )\n    .args([\"--eval\", \"x\"])\n    .stdout_regex(r\"\\d\\d\\d\\d-\\d\\d-\\d\\d [+-]\\d\\d\\d\\d\")\n    .success();\n}\n\n#[test]\nfn datetime_utc() {\n  Test::new()\n    .justfile(\n      \"\n        x := datetime_utc('%Y-%m-%d %Z')\n      \",\n    )\n    .args([\"--eval\", \"x\"])\n    .stdout_regex(r\"\\d\\d\\d\\d-\\d\\d-\\d\\d UTC\")\n    .success();\n}\n"
  },
  {
    "path": "tests/default.rs",
    "content": "use super::*;\n\n#[test]\nfn default_attribute_overrides_first_recipe() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          @echo FOO\n\n        [default]\n        bar:\n          @echo BAR\n      \",\n    )\n    .stdout(\"BAR\\n\")\n    .success();\n}\n\n#[test]\nfn default_attribute_may_only_appear_once_per_justfile() {\n  Test::new()\n    .justfile(\n      \"\n        [default]\n        foo:\n\n        [default]\n        bar:\n      \",\n    )\n    .stderr(\n      \"\n        error: Recipe `foo` has duplicate `[default]` attribute, which may only appear once per module\n         ——▶ justfile:2:1\n          │\n        2 │ foo:\n          │ ^^^\n      \"\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/delimiters.rs",
    "content": "use super::*;\n\n#[test]\nfn mismatched_delimiter() {\n  Test::new()\n    .justfile(\"(]\")\n    .stderr(\n      \"\n    error: Mismatched closing delimiter `]`. (Did you mean to close the `(` on line 1?)\n     ——▶ justfile:1:2\n      │\n    1 │ (]\n      │  ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn unexpected_delimiter() {\n  Test::new()\n    .justfile(\"]\")\n    .stderr(\n      \"\n    error: Unexpected closing delimiter `]`\n     ——▶ justfile:1:1\n      │\n    1 │ ]\n      │ ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn paren_continuation() {\n  Test::new()\n    .justfile(\n      \"\n    x := (\n          'a'\n              +\n      'b'\n    )\n\n    foo:\n      echo {{x}}\n  \",\n    )\n    .stdout(\"ab\\n\")\n    .stderr(\"echo ab\\n\")\n    .success();\n}\n\n#[test]\nfn brace_continuation() {\n  Test::new()\n    .justfile(\n      \"\n    x := if '' == '' {\n      'a'\n    } else {\n      'b'\n    }\n\n    foo:\n      echo {{x}}\n  \",\n    )\n    .stdout(\"a\\n\")\n    .stderr(\"echo a\\n\")\n    .success();\n}\n\n#[test]\nfn bracket_continuation() {\n  Test::new()\n    .justfile(\n      \"\n    set shell := [\n      'sh',\n      '-cu',\n    ]\n\n    foo:\n      echo foo\n  \",\n    )\n    .stdout(\"foo\\n\")\n    .stderr(\"echo foo\\n\")\n    .success();\n}\n\n#[test]\nfn dependency_continuation() {\n  Test::new()\n    .justfile(\n      \"\n    foo: (\n    bar 'bar'\n    )\n      echo foo\n\n    bar x:\n      echo {{x}}\n  \",\n    )\n    .stdout(\"bar\\nfoo\\n\")\n    .stderr(\"echo bar\\necho foo\\n\")\n    .success();\n}\n\n#[test]\nfn interpolation_continuation() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ (\n        'a' + 'b')}}\n  \",\n    )\n    .stderr(\"echo ab\\n\")\n    .stdout(\"ab\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/dependencies.rs",
    "content": "use super::*;\n\n#[test]\nfn recipe_doubly_nested_module_dependencies() {\n  Test::new()\n    .write(\"foo.just\", \"mod bar\\nbaz: \\n @echo FOO\")\n    .write(\"bar.just\", \"baz:\\n @echo BAZ\")\n    .justfile(\n      \"\n      mod foo\n\n      baz: foo::bar::baz\n      \",\n    )\n    .arg(\"baz\")\n    .stdout(\"BAZ\\n\")\n    .success();\n}\n\n#[test]\nfn recipe_singly_nested_module_dependencies() {\n  Test::new()\n    .write(\"foo.just\", \"mod bar\\nbaz: \\n @echo BAR\")\n    .write(\"bar.just\", \"baz:\\n @echo BAZ\")\n    .justfile(\n      \"\n      mod foo\n      baz: foo::baz\n      \",\n    )\n    .arg(\"baz\")\n    .stdout(\"BAR\\n\")\n    .success();\n}\n\n#[test]\nfn dependency_not_in_submodule() {\n  Test::new()\n    .write(\"foo.just\", \"qux: \\n @echo QUX\")\n    .justfile(\n      \"\n      mod foo\n      baz: foo::baz\n      \",\n    )\n    .arg(\"baz\")\n    .stderr(\n      \"error: Recipe `baz` has unknown dependency `foo::baz`\n ——▶ justfile:2:11\n  │\n2 │ baz: foo::baz\n  │           ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn dependency_submodule_missing() {\n  Test::new()\n    .justfile(\n      \"\n      foo:\n        @echo FOO\n      bar:\n        @echo BAR\n      baz: foo::bar\n      \",\n    )\n    .arg(\"baz\")\n    .stderr(\n      \"error: Recipe `baz` has unknown dependency `foo::bar`\n ——▶ justfile:5:11\n  │\n5 │ baz: foo::bar\n  │           ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn recipe_dependency_on_module_fails() {\n  Test::new()\n    .write(\"foo.just\", \"mod bar\\nbaz: \\n @echo BAR\")\n    .write(\"bar.just\", \"baz:\\n @echo BAZ\")\n    .justfile(\n      \"\n      mod foo\n      baz: foo::bar\n      \",\n    )\n    .arg(\"baz\")\n    .stderr(\n      \"error: Recipe `baz` has unknown dependency `foo::bar`\n ——▶ justfile:2:11\n  │\n2 │ baz: foo::bar\n  │           ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn recipe_module_dependency_subsequent_mix() {\n  Test::new()\n    .write(\"foo.just\", \"bar: \\n @echo BAR\")\n    .justfile(\n      \"\n      mod foo\n      baz:\n        @echo BAZ\n      quux: foo::bar && baz\n        @echo QUUX\n      \",\n    )\n    .arg(\"quux\")\n    .stdout(\"BAR\\nQUUX\\nBAZ\\n\")\n    .success();\n}\n\n#[test]\nfn recipe_module_dependency_only_runs_once() {\n  Test::new()\n    .write(\"foo.just\", \"bar: baz \\n  \\nbaz: \\n @echo BAZ\")\n    .justfile(\n      \"\n      mod foo\n      qux: foo::bar foo::baz\n      \",\n    )\n    .arg(\"qux\")\n    .stdout(\"BAZ\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/directories.rs",
    "content": "use super::*;\n\n#[test]\nfn cache_directory() {\n  Test::new()\n    .justfile(\"x := cache_directory()\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(dirs::cache_dir().unwrap_or_default().to_string_lossy())\n    .success();\n}\n\n#[test]\nfn config_directory() {\n  Test::new()\n    .justfile(\"x := config_directory()\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(dirs::config_dir().unwrap_or_default().to_string_lossy())\n    .success();\n}\n\n#[test]\nfn config_local_directory() {\n  Test::new()\n    .justfile(\"x := config_local_directory()\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\n      dirs::config_local_dir()\n        .unwrap_or_default()\n        .to_string_lossy(),\n    )\n    .success();\n}\n\n#[test]\nfn data_directory() {\n  Test::new()\n    .justfile(\"x := data_directory()\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(dirs::data_dir().unwrap_or_default().to_string_lossy())\n    .success();\n}\n\n#[test]\nfn data_local_directory() {\n  Test::new()\n    .justfile(\"x := data_local_directory()\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(dirs::data_local_dir().unwrap_or_default().to_string_lossy())\n    .success();\n}\n\n#[test]\nfn executable_directory() {\n  if let Some(executable_dir) = dirs::executable_dir() {\n    Test::new()\n      .justfile(\"x := executable_directory()\")\n      .args([\"--evaluate\", \"x\"])\n      .stdout(executable_dir.to_string_lossy())\n      .success();\n  } else {\n    Test::new()\n      .justfile(\"x := executable_directory()\")\n      .args([\"--evaluate\", \"x\"])\n      .stderr(\n        \"\n          error: Call to function `executable_directory` failed: executable directory not found\n           ——▶ justfile:1:6\n            │\n          1 │ x := executable_directory()\n            │      ^^^^^^^^^^^^^^^^^^^^\n        \",\n      )\n      .failure();\n  }\n}\n\n#[test]\nfn home_directory() {\n  Test::new()\n    .justfile(\"x := home_directory()\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(dirs::home_dir().unwrap_or_default().to_string_lossy())\n    .success();\n}\n"
  },
  {
    "path": "tests/dotenv.rs",
    "content": "use super::*;\n\n#[test]\nfn dotenv() {\n  Test::new()\n    .write(\".env\", \"KEY=ROOT\")\n    .write(\"sub/.env\", \"KEY=SUB\")\n    .write(\"sub/justfile\", \"default:\\n\\techo KEY=${KEY:-unset}\")\n    .args([\"sub/default\"])\n    .stdout(\"KEY=unset\\n\")\n    .stderr(\"echo KEY=${KEY:-unset}\\n\")\n    .success();\n}\n\n#[test]\nfn set_false() {\n  Test::new()\n    .justfile(\n      r#\"\n      set dotenv-load := false\n\n      @foo:\n        if [ -n \"${DOTENV_KEY+1}\" ]; then echo defined; else echo undefined; fi\n    \"#,\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .stdout(\"undefined\\n\")\n    .success();\n}\n\n#[test]\nfn set_implicit() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-load\n\n        foo:\n          echo $DOTENV_KEY\n      \",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .stdout(\"dotenv-value\\n\")\n    .stderr(\"echo $DOTENV_KEY\\n\")\n    .success();\n}\n\n#[test]\nfn set_true() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-load := true\n\n        foo:\n          echo $DOTENV_KEY\n      \",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .stdout(\"dotenv-value\\n\")\n    .stderr(\"echo $DOTENV_KEY\\n\")\n    .success();\n}\n\n#[test]\nfn no_warning() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          echo ${DOTENV_KEY:-unset}\n      \",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .stdout(\"unset\\n\")\n    .stderr(\"echo ${DOTENV_KEY:-unset}\\n\")\n    .success();\n}\n\n#[test]\nfn dotenv_required() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-required\n\n        foo:\n      \",\n    )\n    .stderr(\"error: Dotenv file not found\\n\")\n    .failure();\n}\n\n#[test]\nfn path_resolves() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          @echo $JUST_TEST_VARIABLE\n      \",\n    )\n    .tree(tree! {\n      subdir: {\n        \".env\": \"JUST_TEST_VARIABLE=bar\"\n      }\n    })\n    .args([\"--dotenv-path\", \"subdir/.env\"])\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn filename_resolves() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          @echo $JUST_TEST_VARIABLE\n      \",\n    )\n    .tree(tree! {\n      \".env.special\": \"JUST_TEST_VARIABLE=bar\"\n    })\n    .args([\"--dotenv-filename\", \".env.special\"])\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn filename_flag_overwrites_no_load() {\n  Test::new()\n    .justfile(\n      \"\n      set dotenv-load := false\n\n      foo:\n        @echo $JUST_TEST_VARIABLE\n    \",\n    )\n    .tree(tree! {\n      \".env.special\": \"JUST_TEST_VARIABLE=bar\"\n    })\n    .args([\"--dotenv-filename\", \".env.special\"])\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn path_flag_overwrites_no_load() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-load := false\n\n        foo:\n          @echo $JUST_TEST_VARIABLE\n      \",\n    )\n    .tree(tree! {\n      subdir: {\n        \".env\": \"JUST_TEST_VARIABLE=bar\"\n      }\n    })\n    .args([\"--dotenv-path\", \"subdir/.env\"])\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn can_set_dotenv_filename_from_justfile() {\n  Test::new()\n    .justfile(\n      r#\"\n        set dotenv-filename := \".env.special\"\n\n        foo:\n          @echo $JUST_TEST_VARIABLE\n      \"#,\n    )\n    .tree(tree! {\n      \".env.special\": \"JUST_TEST_VARIABLE=bar\"\n    })\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn can_set_dotenv_path_from_justfile() {\n  Test::new()\n    .justfile(\n      r#\"\n        set dotenv-path := \"subdir/.env\"\n\n        foo:\n          @echo $JUST_TEST_VARIABLE\n      \"#,\n    )\n    .tree(tree! {\n      subdir: {\n        \".env\": \"JUST_TEST_VARIABLE=bar\"\n      }\n    })\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn program_argument_has_priority_for_dotenv_filename() {\n  Test::new()\n    .justfile(\n      r#\"\n        set dotenv-filename := \".env.special\"\n\n        foo:\n          @echo $JUST_TEST_VARIABLE\n      \"#,\n    )\n    .tree(tree! {\n      \".env.special\": \"JUST_TEST_VARIABLE=bar\",\n      \".env.superspecial\": \"JUST_TEST_VARIABLE=baz\"\n    })\n    .args([\"--dotenv-filename\", \".env.superspecial\"])\n    .stdout(\"baz\\n\")\n    .success();\n}\n\n#[test]\nfn program_argument_has_priority_for_dotenv_path() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-path := 'subdir/.env'\n\n        foo:\n          @echo $JUST_TEST_VARIABLE\n      \",\n    )\n    .tree(tree! {\n      subdir: {\n        \".env\": \"JUST_TEST_VARIABLE=bar\",\n        \".env.special\": \"JUST_TEST_VARIABLE=baz\"\n      }\n    })\n    .args([\"--dotenv-path\", \"subdir/.env.special\"])\n    .stdout(\"baz\\n\")\n    .success();\n}\n\n#[test]\nfn dotenv_path_is_relative_to_working_directory() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-path := '.env'\n\n        foo:\n          @echo $DOTENV_KEY\n      \",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .tree(tree! { subdir: { } })\n    .current_dir(\"subdir\")\n    .stdout(\"dotenv-value\\n\")\n    .success();\n}\n\n#[test]\nfn dotenv_variable_in_recipe() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-load\n\n        echo:\n          echo $DOTENV_KEY\n      \",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .stdout(\"dotenv-value\\n\")\n    .stderr(\"echo $DOTENV_KEY\\n\")\n    .success();\n}\n\n#[test]\nfn dotenv_variable_in_backtick() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-load\n        X:=`echo $DOTENV_KEY`\n        echo:\n          echo {{X}}\n      \",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .stdout(\"dotenv-value\\n\")\n    .stderr(\"echo dotenv-value\\n\")\n    .success();\n}\n\n#[test]\nfn dotenv_variable_in_function_in_recipe() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-load\n        echo:\n          echo {{env_var_or_default('DOTENV_KEY', 'foo')}}\n          echo {{env_var('DOTENV_KEY')}}\n      \",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .stdout(\"dotenv-value\\ndotenv-value\\n\")\n    .stderr(\"echo dotenv-value\\necho dotenv-value\\n\")\n    .success();\n}\n\n#[test]\nfn dotenv_variable_in_function_in_backtick() {\n  Test::new()\n    .justfile(\n      \"\n  set dotenv-load\n  X:=env_var_or_default('DOTENV_KEY', 'foo')\n  Y:=env_var('DOTENV_KEY')\n  echo:\n    echo {{X}}\n    echo {{Y}}\n\",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .stdout(\"dotenv-value\\ndotenv-value\\n\")\n    .stderr(\"echo dotenv-value\\necho dotenv-value\\n\")\n    .success();\n}\n\n#[test]\nfn no_dotenv() {\n  Test::new()\n    .justfile(\n      \"\n        X:=env_var_or_default('DOTENV_KEY', 'DEFAULT')\n        echo:\n          echo {{X}}\n      \",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .arg(\"--no-dotenv\")\n    .stdout(\"DEFAULT\\n\")\n    .stderr(\"echo DEFAULT\\n\")\n    .success();\n}\n\n#[test]\nfn dotenv_env_var_default_no_override() {\n  Test::new()\n    .justfile(\n      \"\n        echo:\n          echo $DOTENV_KEY\n      \",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .env(\"DOTENV_KEY\", \"not-the-dotenv-value\")\n    .stdout(\"not-the-dotenv-value\\n\")\n    .stderr(\"echo $DOTENV_KEY\\n\")\n    .success();\n}\n\n#[test]\nfn dotenv_env_var_override() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-load\n        set dotenv-override := true\n        echo:\n          echo $DOTENV_KEY\n      \",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .env(\"DOTENV_KEY\", \"not-the-dotenv-value\")\n    .stdout(\"dotenv-value\\n\")\n    .stderr(\"echo $DOTENV_KEY\\n\")\n    .success();\n}\n\n#[test]\nfn dotenv_env_var_override_no_load() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-override := true\n        echo:\n          echo $DOTENV_KEY\n      \",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .env(\"DOTENV_KEY\", \"not-the-dotenv-value\")\n    .stdout(\"dotenv-value\\n\")\n    .stderr(\"echo $DOTENV_KEY\\n\")\n    .success();\n}\n\n#[test]\nfn dotenv_path_usable_from_subdir() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-path := '.custom-env'\n\n        @echo:\n          echo $DOTENV_KEY\n      \",\n    )\n    .create_dir(\"sub\")\n    .current_dir(\"sub\")\n    .write(\".custom-env\", \"DOTENV_KEY=dotenv-value\")\n    .stdout(\"dotenv-value\\n\")\n    .success();\n}\n\n#[test]\nfn dotenv_path_does_not_override_dotenv_file() {\n  Test::new()\n    .write(\".env\", \"KEY=ROOT\")\n    .write(\n      \"sub/justfile\",\n      \"set dotenv-path := '.'\\n@foo:\\n echo ${KEY}\",\n    )\n    .current_dir(\"sub\")\n    .stdout(\"ROOT\\n\")\n    .success();\n}\n\n#[test]\nfn error_message() {\n  Test::new()\n    .write(\".env\", \"FOO=bar baz\")\n    .justfile(\n      \"\n        set dotenv-load\n\n        foo:\n      \",\n    )\n    .stderr_regex(r\"error: Failed to load environment file from `.*\\.env`: .*\")\n    .failure();\n}\n"
  },
  {
    "path": "tests/dump.rs",
    "content": "use super::*;\n\n#[test]\nfn dump() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n        # this recipe does something\n        recipe a b +d:\n          @exit 100\n      \",\n    )\n    .stdout(\n      \"\n        # this recipe does something\n        recipe a b +d:\n            @exit 100\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn json() {\n  Test::new()\n    .arg(\"--json\")\n    .justfile(\n      \"\n        foo:\n      \",\n    )\n    .stdout_regex(r\"\\{.*\\}\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/edit.rs",
    "content": "use super::*;\n\nconst JUSTFILE: &str = \"Yooooooo, hopefully this never becomes valid syntax.\";\n\n/// Test that --edit doesn't require a valid justfile\n#[test]\nfn invalid_justfile() {\n  let tmp = temptree! {\n    justfile: JUSTFILE,\n  };\n\n  let output = Command::new(JUST).current_dir(tmp.path()).output().unwrap();\n\n  assert!(!output.status.success());\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--edit\")\n    .env(\"VISUAL\", \"cat\")\n    .output()\n    .unwrap();\n\n  assert_stdout(&output, JUSTFILE);\n}\n\n#[test]\nfn invoke_error() {\n  let tmp = temptree! {\n    justfile: JUSTFILE,\n  };\n\n  let output = Command::new(JUST).current_dir(tmp.path()).output().unwrap();\n\n  assert!(!output.status.success());\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--edit\")\n    .env(\"VISUAL\", \"/\")\n    .output()\n    .unwrap();\n\n  assert_eq!(\n    String::from_utf8_lossy(&output.stderr),\n    if cfg!(windows) {\n      \"error: Editor `/` invocation failed: program path has no file name\\n\"\n    } else {\n      \"error: Editor `/` invocation failed: Permission denied (os error 13)\\n\"\n    }\n  );\n}\n\n#[test]\nfn status_error() {\n  if cfg!(windows) {\n    return;\n  }\n  let tmp = temptree! {\n    justfile: JUSTFILE,\n    \"exit-2\": \"#!/usr/bin/env bash\\nexit 2\\n\",\n  };\n\n  let output = Command::new(\"chmod\")\n    .arg(\"+x\")\n    .arg(tmp.path().join(\"exit-2\"))\n    .output()\n    .unwrap();\n\n  assert!(output.status.success());\n\n  let path = env::join_paths(\n    iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os(\"PATH\").unwrap())),\n  )\n  .unwrap();\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--edit\")\n    .env(\"PATH\", path)\n    .env(\"VISUAL\", \"exit-2\")\n    .output()\n    .unwrap();\n\n  assert!(\n    Regex::new(\"^error: Editor `exit-2` failed: exit (code|status): 2\\n$\")\n      .unwrap()\n      .is_match(str::from_utf8(&output.stderr).unwrap())\n  );\n\n  assert_eq!(output.status.code().unwrap(), 2);\n}\n\n/// Test that editor is $VISUAL, $EDITOR, or \"vim\" in that order\n#[test]\nfn editor_precedence() {\n  let tmp = temptree! {\n    justfile: JUSTFILE,\n  };\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--edit\")\n    .env(\"VISUAL\", \"cat\")\n    .env(\"EDITOR\", \"this-command-doesnt-exist\")\n    .output()\n    .unwrap();\n\n  assert_stdout(&output, JUSTFILE);\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--edit\")\n    .env_remove(\"VISUAL\")\n    .env(\"EDITOR\", \"cat\")\n    .output()\n    .unwrap();\n\n  assert_stdout(&output, JUSTFILE);\n\n  let cat = which(\"cat\").unwrap();\n  let vim = tmp.path().join(format!(\"vim{EXE_SUFFIX}\"));\n\n  #[cfg(unix)]\n  std::os::unix::fs::symlink(cat, vim).unwrap();\n\n  #[cfg(windows)]\n  std::os::windows::fs::symlink_file(cat, vim).unwrap();\n\n  let path = env::join_paths(\n    iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os(\"PATH\").unwrap())),\n  )\n  .unwrap();\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--edit\")\n    .env(\"PATH\", path)\n    .env_remove(\"VISUAL\")\n    .env_remove(\"EDITOR\")\n    .output()\n    .unwrap();\n\n  assert_stdout(&output, JUSTFILE);\n}\n\n/// Test that editor working directory is the same as edited justfile\n#[cfg(unix)]\n#[test]\nfn editor_working_directory() {\n  let tmp = temptree! {\n    justfile: JUSTFILE,\n    child: {},\n    editor: \"#!/usr/bin/env sh\\ncat $1\\npwd\",\n  };\n\n  let editor = tmp.path().join(\"editor\");\n\n  let permissions = std::os::unix::fs::PermissionsExt::from_mode(0o700);\n  fs::set_permissions(&editor, permissions).unwrap();\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path().join(\"child\"))\n    .arg(\"--edit\")\n    .env(\"VISUAL\", &editor)\n    .output()\n    .unwrap();\n\n  let want = format!(\n    \"{JUSTFILE}{}\\n\",\n    tmp.path().canonicalize().unwrap().display()\n  );\n\n  assert_stdout(&output, &want);\n}\n"
  },
  {
    "path": "tests/equals.rs",
    "content": "use super::*;\n\n#[test]\nfn export_recipe() {\n  Test::new()\n    .justfile(\n      \"\n      export foo='bar':\n        echo {{foo}}\n    \",\n    )\n    .stdout(\"bar\\n\")\n    .stderr(\"echo bar\\n\")\n    .success();\n}\n\n#[test]\nfn alias_recipe() {\n  Test::new()\n    .justfile(\n      \"\n      alias foo='bar':\n        echo {{foo}}\n    \",\n    )\n    .stdout(\"bar\\n\")\n    .stderr(\"echo bar\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/error_messages.rs",
    "content": "use super::*;\n\n#[test]\nfn invalid_alias_attribute() {\n  Test::new()\n    .justfile(\"[private]\\n[linux]\\nalias t := test\\n\\ntest:\\n\")\n    .stderr(\n      \"\n    error: Alias `t` has invalid attribute `linux`\n     ——▶ justfile:3:7\n      │\n    3 │ alias t := test\n      │       ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn expected_keyword() {\n  Test::new()\n    .justfile(\"foo := if '' == '' { '' } arlo { '' }\")\n    .stderr(\n      \"\n    error: Expected keyword `else` but found identifier `arlo`\n     ——▶ justfile:1:27\n      │\n    1 │ foo := if '' == '' { '' } arlo { '' }\n      │                           ^^^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn unexpected_character() {\n  Test::new()\n    .justfile(\"&~\")\n    .stderr(\n      \"\n    error: Expected character `&`\n     ——▶ justfile:1:2\n      │\n    1 │ &~\n      │  ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn argument_count_mismatch() {\n  Test::new()\n    .justfile(\"foo a b:\")\n    .args([\"foo\"])\n    .stderr(\n      \"\n      error: Recipe `foo` got 0 positional arguments but takes 2\n      usage:\n          just foo a b\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn file_path_is_indented_if_justfile_is_long() {\n  Test::new()\n    .justfile(\"\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nfoo\")\n    .stderr(\n      \"\nerror: Expected '*', ':', '$', identifier, or '+', but found end of file\n  ——▶ justfile:20:4\n   │\n20 │ foo\n   │    ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn file_paths_are_relative() {\n  Test::new()\n    .justfile(\"import 'foo/bar.just'\")\n    .write(\"foo/bar.just\", \"baz\")\n    .stderr(format!(\n      \"\nerror: Expected '*', ':', '$', identifier, or '+', but found end of file\n ——▶ foo{MAIN_SEPARATOR}bar.just:1:4\n  │\n1 │ baz\n  │    ^\n\",\n    ))\n    .failure();\n}\n\n#[test]\nfn file_paths_not_in_subdir_are_absolute() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .write(\"foo/justfile\", \"import '../bar.just'\")\n    .write(\"bar.just\", \"baz\")\n    .no_justfile()\n    .args([\"--justfile\", \"foo/justfile\"])\n    .stderr_regex(\n      r\"error: Expected '\\*', ':', '\\$', identifier, or '\\+', but found end of file\n ——▶ /.*/bar.just:1:4\n  │\n1 │ baz\n  │    \\^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn redefinition_errors_properly_swap_types() {\n  Test::new()\n    .write(\"foo.just\", \"foo:\")\n    .justfile(\"foo:\\n echo foo\\n\\nmod foo 'foo.just'\")\n    .stderr(\n      \"\nerror: Recipe `foo` defined on line 1 is redefined as a module on line 4\n ——▶ justfile:4:5\n  │\n4 │ mod foo 'foo.just'\n  │     ^^^\n\",\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/evaluate.rs",
    "content": "use super::*;\n\n#[test]\nfn evaluate() {\n  Test::new()\n    .arg(\"--evaluate\")\n    .justfile(\n      r#\"\nfoo := \"a\\t\"\nhello := \"c\"\nbar := \"b\\t\"\nab := foo + bar + hello\n\nwut:\n  touch /this/is/not/a/file\n\"#,\n    )\n    .stdout(\n      r#\"ab    := \"a\tb\tc\"\nbar   := \"b\t\"\nfoo   := \"a\t\"\nhello := \"c\"\n\"#,\n    )\n    .success();\n}\n\n#[test]\nfn evaluate_empty() {\n  Test::new()\n    .arg(\"--evaluate\")\n    .justfile(\n      \"\n    a := 'foo'\n  \",\n    )\n    .stdout(\n      r#\"\n    a := \"foo\"\n  \"#,\n    )\n    .success();\n}\n\n#[test]\nfn evaluate_multiple() {\n  Test::new()\n    .arg(\"--evaluate\")\n    .arg(\"a\")\n    .arg(\"c\")\n    .justfile(\n      \"\n    a := 'x'\n    b := 'y'\n    c := 'z'\n  \",\n    )\n    .stderr(\"error: `--evaluate` used with unexpected argument: `c`\\n\")\n    .failure();\n}\n\n#[test]\nfn evaluate_single_free() {\n  Test::new()\n    .arg(\"--evaluate\")\n    .arg(\"b\")\n    .justfile(\n      \"\n    a := 'x'\n    b := 'y'\n    c := 'z'\n  \",\n    )\n    .stdout(\"y\")\n    .success();\n}\n\n#[test]\nfn evaluate_no_suggestion() {\n  Test::new()\n    .arg(\"--evaluate\")\n    .arg(\"aby\")\n    .justfile(\n      \"\n    abc := 'x'\n  \",\n    )\n    .stderr(\n      \"\n    error: Justfile does not contain variable `aby`.\n    Did you mean `abc`?\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn evaluate_suggestion() {\n  Test::new()\n    .arg(\"--evaluate\")\n    .arg(\"goodbye\")\n    .justfile(\n      \"\n    hello := 'x'\n  \",\n    )\n    .stderr(\n      \"\n    error: Justfile does not contain variable `goodbye`.\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn evaluate_private() {\n  Test::new()\n    .arg(\"--evaluate\")\n    .justfile(\n      \"\n    [private]\n    foo := 'one'\n    bar := 'two'\n    _baz := 'three'\n  \",\n    )\n    .stdout(\"bar := \\\"two\\\"\\n\")\n    .success();\n}\n\n#[test]\nfn evaluate_single_private() {\n  Test::new()\n    .arg(\"--evaluate\")\n    .arg(\"foo\")\n    .justfile(\n      \"\n    [private]\n    foo := 'one'\n    bar := 'two'\n    _baz := 'three'\n  \",\n    )\n    .stdout(\"one\")\n    .success();\n}\n\n#[test]\nfn dont_evaluate_unnecessary_variables() {\n  Test::new()\n    .justfile(\n      \"\n      x := 'FOO'\n\n      y := `exit 1`\n    \",\n    )\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"FOO\")\n    .success();\n}\n"
  },
  {
    "path": "tests/examples.rs",
    "content": "use super::*;\n\n#[test]\nfn examples() {\n  for result in fs::read_dir(\"examples\").unwrap() {\n    let entry = result.unwrap();\n    let path = entry.path();\n\n    println!(\"Parsing `{}`…\", path.display());\n\n    let output = Command::new(JUST)\n      .arg(\"--justfile\")\n      .arg(&path)\n      .arg(\"--dump\")\n      .output()\n      .unwrap();\n\n    assert_success(&output);\n  }\n}\n"
  },
  {
    "path": "tests/explain.rs",
    "content": "use super::*;\n\n#[test]\nfn explain_recipe() {\n  Test::new()\n    .justfile(\n      \"\n      # List some fruits\n      fruits:\n        echo 'apple peach dragonfruit'\n    \",\n    )\n    .args([\"--explain\", \"fruits\"])\n    .stdout(\"apple peach dragonfruit\\n\")\n    .stderr(\n      \"\n      #### List some fruits\n      echo 'apple peach dragonfruit'\n    \",\n    )\n    .success();\n}\n"
  },
  {
    "path": "tests/export.rs",
    "content": "use super::*;\n\n#[test]\nfn success() {\n  Test::new()\n    .justfile(\n      r#\"\nexport FOO := \"a\"\nbaz := \"c\"\nexport BAR := \"b\"\nexport ABC := FOO + BAR + baz\n\nwut:\n  echo $FOO $BAR $ABC\n\"#,\n    )\n    .stdout(\"a b abc\\n\")\n    .stderr(\"echo $FOO $BAR $ABC\\n\")\n    .success();\n}\n\n#[test]\nfn parameter() {\n  Test::new()\n    .justfile(\n      r#\"\n    wut $FOO='a' BAR='b':\n      echo $FOO\n      echo {{BAR}}\n      if [ -n \"${BAR+1}\" ]; then echo defined; else echo undefined; fi\n  \"#,\n    )\n    .stdout(\"a\\nb\\nundefined\\n\")\n    .stderr(\n      \"echo $FOO\\necho b\\nif [ -n \\\"${BAR+1}\\\" ]; then echo defined; else echo undefined; fi\\n\",\n    )\n    .success();\n}\n\n#[test]\nfn parameter_not_visible_to_backtick() {\n  Test::new()\n    .arg(\"wut\")\n    .arg(\"bar\")\n    .justfile(\n      r#\"\n    wut $FOO BAR=`if [ -n \"${FOO+1}\" ]; then echo defined; else echo undefined; fi`:\n      echo $FOO\n      echo {{BAR}}\n  \"#,\n    )\n    .stdout(\"bar\\nundefined\\n\")\n    .stderr(\"echo $FOO\\necho undefined\\n\")\n    .success();\n}\n\n#[test]\nfn override_variable() {\n  Test::new()\n    .arg(\"--set\")\n    .arg(\"BAR\")\n    .arg(\"bye\")\n    .arg(\"FOO=hello\")\n    .justfile(\n      r#\"\nexport FOO := \"a\"\nbaz := \"c\"\nexport BAR := \"b\"\nexport ABC := FOO + \"-\" + BAR + \"-\" + baz\n\nwut:\n  echo $FOO $BAR $ABC\n\"#,\n    )\n    .stdout(\"hello bye hello-bye-c\\n\")\n    .stderr(\"echo $FOO $BAR $ABC\\n\")\n    .success();\n}\n\n#[test]\nfn shebang() {\n  Test::new()\n    .justfile(\n      r#\"\nexport FOO := \"a\"\nbaz := \"c\"\nexport BAR := \"b\"\nexport ABC := FOO + BAR + baz\n\nwut:\n  #!/bin/sh\n  echo $FOO $BAR $ABC\n\"#,\n    )\n    .stdout(\"a b abc\\n\")\n    .success();\n}\n\n#[test]\nfn recipe_backtick() {\n  Test::new()\n    .justfile(\n      r#\"\nexport EXPORTED_VARIABLE := \"A-IS-A\"\n\nrecipe:\n  echo {{`echo recipe $EXPORTED_VARIABLE`}}\n\"#,\n    )\n    .stdout(\"recipe A-IS-A\\n\")\n    .stderr(\"echo recipe A-IS-A\\n\")\n    .success();\n}\n\n#[test]\nfn setting_implicit() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"goodbye\")\n    .justfile(\n      \"\n    set export\n\n    A := 'hello'\n\n    foo B C=`echo $A`:\n      echo $A\n      echo $B\n      echo $C\n  \",\n    )\n    .stdout(\"hello\\ngoodbye\\nhello\\n\")\n    .stderr(\"echo $A\\necho $B\\necho $C\\n\")\n    .success();\n}\n\n#[test]\nfn setting_true() {\n  Test::new()\n    .justfile(\n      \"\n    set export := true\n\n    A := 'hello'\n\n    foo B C=`echo $A`:\n      echo $A\n      echo $B\n      echo $C\n  \",\n    )\n    .arg(\"foo\")\n    .arg(\"goodbye\")\n    .stdout(\"hello\\ngoodbye\\nhello\\n\")\n    .stderr(\"echo $A\\necho $B\\necho $C\\n\")\n    .success();\n}\n\n#[test]\nfn setting_false() {\n  Test::new()\n    .justfile(\n      r#\"\n    set export := false\n\n    A := 'hello'\n\n    foo:\n      if [ -n \"${A+1}\" ]; then echo defined; else echo undefined; fi\n  \"#,\n    )\n    .stdout(\"undefined\\n\")\n    .stderr(\"if [ -n \\\"${A+1}\\\" ]; then echo defined; else echo undefined; fi\\n\")\n    .success();\n}\n\n#[test]\nfn setting_shebang() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"goodbye\")\n    .justfile(\n      \"\n    set export\n\n    A := 'hello'\n\n    foo B:\n      #!/bin/sh\n      echo $A\n      echo $B\n  \",\n    )\n    .stdout(\"hello\\ngoodbye\\n\")\n    .success();\n}\n\n#[test]\nfn setting_override_undefined() {\n  Test::new()\n    .arg(\"A=zzz\")\n    .arg(\"foo\")\n    .justfile(\n      r#\"\n    set export\n\n    A := 'hello'\n    B := `if [ -n \"${A+1}\" ]; then echo defined; else echo undefined; fi`\n\n    foo C='goodbye' D=`if [ -n \"${C+1}\" ]; then echo defined; else echo undefined; fi`:\n      echo $B\n      echo $D\n  \"#,\n    )\n    .stdout(\"undefined\\nundefined\\n\")\n    .stderr(\"echo $B\\necho $D\\n\")\n    .success();\n}\n\n#[test]\nfn setting_variable_not_visible() {\n  Test::new()\n    .arg(\"A=zzz\")\n    .justfile(\n      r#\"\n    export A := 'hello'\n    export B := `if [ -n \"${A+1}\" ]; then echo defined; else echo undefined; fi`\n\n    foo:\n      echo $B\n  \"#,\n    )\n    .stdout(\"undefined\\n\")\n    .stderr(\"echo $B\\n\")\n    .success();\n}\n\n#[test]\nfn variables_exported_with_setting_are_visible_in_child() {\n  Test::new()\n    .write(\"foo.just\", \"bar:\\n @echo $x\")\n    .justfile(\n      \"\n        set export\n\n        x := 'FOO'\n\n        mod foo\n      \",\n    )\n    .arg(\"foo::bar\")\n    .stdout(\"FOO\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/fallback.rs",
    "content": "use super::*;\n\n#[test]\nfn fallback_from_subdir_bugfix() {\n  Test::new()\n    .write(\n      \"sub/justfile\",\n      unindent(\n        \"\n        set fallback\n\n        @default:\n          echo foo\n      \",\n      ),\n    )\n    .args([\"sub/default\"])\n    .stdout(\"foo\\n\")\n    .success();\n}\n\n#[test]\nfn fallback_from_subdir_message() {\n  Test::new()\n    .justfile(\"bar:\\n echo bar\")\n    .write(\n      \"sub/justfile\",\n      unindent(\n        \"\n        set fallback\n\n        @foo:\n          echo foo\n      \",\n      ),\n    )\n    .args([\"sub/bar\"])\n    .stderr(path(\"echo bar\\n\"))\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn fallback_from_subdir_verbose_message() {\n  Test::new()\n    .justfile(\"bar:\\n echo bar\")\n    .write(\n      \"sub/justfile\",\n      unindent(\n        \"\n        set fallback\n\n        @foo:\n          echo foo\n      \",\n      ),\n    )\n    .args([\"--verbose\", \"sub/bar\"])\n    .stderr(path(\n      \"\n      Trying ../justfile\n      ===> Running recipe `bar`...\n      echo bar\n      \",\n    ))\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn runs_recipe_in_parent_if_not_found_in_current() {\n  Test::new()\n    .tree(tree! {\n      bar: {\n        justfile: \"\n          set fallback := true\n\n          baz:\n            echo subdir\n        \"\n      }\n    })\n    .justfile(\n      \"\n      foo:\n        echo root\n    \",\n    )\n    .args([\"foo\"])\n    .current_dir(\"bar\")\n    .stderr(\n      \"\n      echo root\n    \",\n    )\n    .stdout(\"root\\n\")\n    .success();\n}\n\n#[test]\nfn setting_accepts_value() {\n  Test::new()\n    .tree(tree! {\n      bar: {\n        justfile: \"\n          set fallback := true\n\n          baz:\n            echo subdir\n        \"\n      }\n    })\n    .justfile(\n      \"\n      foo:\n        echo root\n    \",\n    )\n    .args([\"foo\"])\n    .current_dir(\"bar\")\n    .stderr(\n      \"\n      echo root\n    \",\n    )\n    .stdout(\"root\\n\")\n    .success();\n}\n\n#[test]\nfn print_error_from_parent_if_recipe_not_found_in_current() {\n  Test::new()\n    .tree(tree! {\n      bar: {\n        justfile: \"\n          set fallback := true\n\n          baz:\n            echo subdir\n        \"\n      }\n    })\n    .justfile(\"foo:\\n echo {{bar}}\")\n    .args([\"foo\"])\n    .current_dir(\"bar\")\n    .stderr(\n      \"\n      error: Variable `bar` not defined\n       ——▶ justfile:2:9\n        │\n      2 │  echo {{bar}}\n        │         ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn requires_setting() {\n  Test::new()\n    .tree(tree! {\n      bar: {\n        justfile: \"\n          baz:\n            echo subdir\n        \"\n      }\n    })\n    .justfile(\n      \"\n      foo:\n        echo root\n    \",\n    )\n    .args([\"foo\"])\n    .current_dir(\"bar\")\n    .stderr(\"error: Justfile does not contain recipe `foo`\\n\")\n    .failure();\n}\n\n#[test]\nfn works_with_provided_search_directory() {\n  Test::new()\n    .tree(tree! {\n      bar: {\n        justfile: \"\n          set fallback := true\n\n          baz:\n            echo subdir\n        \"\n      }\n    })\n    .justfile(\n      \"\n      foo:\n        echo root\n    \",\n    )\n    .args([\"./foo\"])\n    .stdout(\"root\\n\")\n    .stderr(\n      \"\n      echo root\n    \",\n    )\n    .current_dir(\"bar\")\n    .success();\n}\n\n#[test]\nfn doesnt_work_with_justfile() {\n  Test::new()\n    .tree(tree! {\n      bar: {\n        justfile: \"\n          baz:\n            echo subdir\n        \"\n      }\n    })\n    .justfile(\n      \"\n      foo:\n        echo root\n    \",\n    )\n    .args([\"--justfile\", \"justfile\", \"foo\"])\n    .current_dir(\"bar\")\n    .stderr(\"error: Justfile does not contain recipe `foo`\\n\")\n    .failure();\n}\n\n#[test]\nfn doesnt_work_with_justfile_and_working_directory() {\n  Test::new()\n    .tree(tree! {\n      bar: {\n        justfile: \"\n          baz:\n            echo subdir\n        \"\n      }\n    })\n    .justfile(\n      \"\n      foo:\n        echo root\n    \",\n    )\n    .args([\"--justfile\", \"justfile\", \"--working-directory\", \".\", \"foo\"])\n    .current_dir(\"bar\")\n    .stderr(\"error: Justfile does not contain recipe `foo`\\n\")\n    .failure();\n}\n\n#[test]\nfn prints_correct_error_message_when_recipe_not_found() {\n  Test::new()\n    .tree(tree! {\n      bar: {\n        justfile: \"\n          set fallback := true\n\n          bar:\n            echo subdir\n        \"\n      }\n    })\n    .justfile(\n      \"\n      bar:\n        echo root\n    \",\n    )\n    .args([\"foo\"])\n    .current_dir(\"bar\")\n    .stderr(\n      \"\n      error: Justfile does not contain recipe `foo`\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn multiple_levels_of_fallback_work() {\n  Test::new()\n    .tree(tree! {\n      a: {\n        b: {\n          justfile: \"\n            set fallback := true\n\n            foo:\n              echo subdir\n          \"\n        },\n        justfile: \"\n          set fallback := true\n\n          bar:\n            echo subdir\n        \"\n      }\n    })\n    .justfile(\n      \"\n      baz:\n        echo root\n    \",\n    )\n    .args([\"baz\"])\n    .current_dir(\"a/b\")\n    .stdout(\"root\\n\")\n    .stderr(\n      \"\n      echo root\n    \",\n    )\n    .success();\n}\n\n#[test]\nfn stop_fallback_when_fallback_is_false() {\n  Test::new()\n    .tree(tree! {\n      a: {\n        b: {\n          justfile: \"\n            set fallback := true\n\n            foo:\n              echo subdir\n          \"\n        },\n        justfile: \"\n          bar:\n            echo subdir\n        \"\n      }\n    })\n    .justfile(\n      \"\n      baz:\n        echo root\n    \",\n    )\n    .args([\"baz\"])\n    .current_dir(\"a/b\")\n    .stderr(\n      \"\n      error: Justfile does not contain recipe `baz`\n      Did you mean `bar`?\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn works_with_modules() {\n  Test::new()\n    .write(\"bar/justfile\", \"set fallback := true\")\n    .write(\"foo.just\", \"baz:\\n @echo BAZ\")\n    .justfile(\"mod foo\")\n    .args([\"foo::baz\"])\n    .current_dir(\"bar\")\n    .stdout(\"BAZ\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/format.rs",
    "content": "use super::*;\n\n#[test]\nfn unstable_not_passed() {\n  Test::new()\n    .arg(\"--fmt\")\n    .justfile(\"\")\n    .stderr_regex(\"error: The `--fmt` command is currently unstable..*\")\n    .failure();\n}\n\n#[test]\nfn check_without_fmt() {\n  Test::new()\n    .arg(\"--check\")\n    .justfile(\"\")\n    .stderr_regex(\n      \"error: the following required arguments were not provided:\n  --fmt\n(.|\\\\n)+\",\n    )\n    .status(2);\n}\n\n#[test]\nfn check_ok() {\n  Test::new()\n    .arg(\"--unstable\")\n    .arg(\"--fmt\")\n    .arg(\"--check\")\n    .justfile(\n      r#\"\n# comment   with   spaces\n\nexport x := `backtick\nwith\nlines`\n\nrecipe: deps\n    echo \"$x\"\n\ndeps:\n    echo {{ x }}\n    echo '$x'\n\"#,\n    )\n    .success();\n}\n\n#[test]\nfn check_found_diff() {\n  Test::new()\n    .arg(\"--unstable\")\n    .arg(\"--fmt\")\n    .arg(\"--check\")\n    .justfile(\"x:=``\\n\")\n    .stdout(\n      \"\n    -x:=``\n    +x := ``\n  \",\n    )\n    .stderr(\n      \"\n    error: Formatted justfile differs from original.\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn check_found_diff_quiet() {\n  Test::new()\n    .arg(\"--unstable\")\n    .arg(\"--fmt\")\n    .arg(\"--check\")\n    .arg(\"--quiet\")\n    .justfile(\"x:=``\\n\")\n    .failure();\n}\n\n#[test]\nfn check_diff_color() {\n  Test::new()\n        .justfile(\"x:=``\\n\")\n        .arg(\"--unstable\")\n        .arg(\"--fmt\")\n        .arg(\"--check\")\n        .arg(\"--color\")\n        .arg(\"always\")\n        .stdout(\"\\n    \\u{1b}[31m-x:=``\\n    \\u{1b}[0m\\u{1b}[32m+x := ``\\n    \\u{1b}[0m\")\n        .stderr(\"\\n    \\u{1b}[1;31merror\\u{1b}[0m: \\u{1b}[1mFormatted justfile differs from original.\\u{1b}[0m\\n  \")\n        .failure();\n}\n\n#[test]\nfn unstable_passed() {\n  let tmp = tempdir();\n\n  let justfile = tmp.path().join(\"justfile\");\n\n  fs::write(&justfile, \"x    :=    'hello'   \").unwrap();\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--fmt\")\n    .arg(\"--unstable\")\n    .output()\n    .unwrap();\n\n  if !output.status.success() {\n    eprintln!(\"{}\", String::from_utf8_lossy(&output.stderr));\n    eprintln!(\"{}\", String::from_utf8_lossy(&output.stdout));\n    panic!(\"justfile failed with status: {}\", output.status);\n  }\n\n  assert_eq!(fs::read_to_string(&justfile).unwrap(), \"x := 'hello'\\n\");\n}\n\n#[test]\nfn write_error() {\n  // skip this test if running as root, since root can write files even if\n  // permissions would otherwise forbid it\n  #[cfg(not(windows))]\n  if nix::unistd::getuid() == nix::unistd::ROOT {\n    return;\n  }\n\n  let tempdir = temptree! {\n    justfile: \"x    :=    'hello'   \",\n  };\n\n  let test = Test::with_tempdir(tempdir)\n    .no_justfile()\n    .args([\"--fmt\", \"--unstable\"])\n    .stderr_regex(if cfg!(windows) {\n      r\"error: Failed to write justfile to `.*`: Access is denied. \\(os error 5\\)\\n\"\n    } else {\n      r\"error: Failed to write justfile to `.*`: Permission denied \\(os error 13\\)\\n\"\n    });\n\n  let justfile_path = test.justfile_path();\n\n  let output = Command::new(\"chmod\")\n    .arg(\"400\")\n    .arg(&justfile_path)\n    .output()\n    .unwrap();\n\n  assert!(output.status.success());\n\n  let _tempdir = test.failure();\n\n  assert_eq!(\n    fs::read_to_string(&justfile_path).unwrap(),\n    \"x    :=    'hello'   \"\n  );\n}\n\n#[test]\nfn alias_good() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    alias f := foo\n\n    foo:\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    alias f := foo\n\n    foo:\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn alias_fix_indent() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    alias f:=    foo\n\n    foo:\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    alias f := foo\n\n    foo:\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_singlequote() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo := 'foo'\n  \",\n    )\n    .stdout(\n      \"\n    foo := 'foo'\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_doublequote() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      r#\"\n    foo := \"foo\"\n  \"#,\n    )\n    .stdout(\n      r#\"\n    foo := \"foo\"\n  \"#,\n    )\n    .success();\n}\n\n#[test]\nfn assignment_indented_singlequote() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo := '''\n      foo\n    '''\n  \",\n    )\n    .stdout(\n      r\"\n    foo := '''\n      foo\n    '''\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_indented_doublequote() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      r#\"\n    foo := \"\"\"\n      foo\n    \"\"\"\n  \"#,\n    )\n    .stdout(\n      r#\"\n    foo := \"\"\"\n      foo\n    \"\"\"\n  \"#,\n    )\n    .success();\n}\n\n#[test]\nfn assignment_backtick() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo := `foo`\n  \",\n    )\n    .stdout(\n      \"\n    foo := `foo`\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_indented_backtick() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo := ```\n      foo\n    ```\n  \",\n    )\n    .stdout(\n      \"\n    foo := ```\n      foo\n    ```\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_name() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    bar := 'bar'\n    foo := bar\n  \",\n    )\n    .stdout(\n      \"\n    bar := 'bar'\n    foo := bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_parenthesized_expression() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo := ('foo')\n  \",\n    )\n    .stdout(\n      \"\n    foo := ('foo')\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_export() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    export foo := 'foo'\n  \",\n    )\n    .stdout(\n      \"\n    export foo := 'foo'\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_concat_values() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo := 'foo' + 'bar'\n  \",\n    )\n    .stdout(\n      \"\n    foo := 'foo' + 'bar'\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_if_oneline() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo := if 'foo' == 'foo' { 'foo' } else { 'bar' }\n  \",\n    )\n    .stdout(\n      \"\n    foo := if 'foo' == 'foo' { 'foo' } else { 'bar' }\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_if_multiline() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo := if 'foo' != 'foo' {\n      'foo'\n    } else {\n      'bar'\n    }\n  \",\n    )\n    .stdout(\n      \"\n    foo := if 'foo' != 'foo' { 'foo' } else { 'bar' }\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_nullary_function() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo := arch()\n  \",\n    )\n    .stdout(\n      \"\n    foo := arch()\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_unary_function() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo := env_var('foo')\n  \",\n    )\n    .stdout(\n      \"\n    foo := env_var('foo')\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_binary_function() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo := env_var_or_default('foo', 'bar')\n  \",\n    )\n    .stdout(\n      \"\n    foo := env_var_or_default('foo', 'bar')\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn assignment_path_functions() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo  := without_extension('foo/bar.baz')\n    foo2 := file_stem('foo/bar.baz')\n    foo3 := parent_directory('foo/bar.baz')\n    foo4 := file_name('foo/bar.baz')\n    foo5 := extension('foo/bar.baz')\n  \",\n    )\n    .stdout(\n      \"\n  foo := without_extension('foo/bar.baz')\n  foo2 := file_stem('foo/bar.baz')\n  foo3 := parent_directory('foo/bar.baz')\n  foo4 := file_name('foo/bar.baz')\n  foo5 := extension('foo/bar.baz')\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_ordinary() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n        echo bar\n  \",\n    )\n    .arg(\"--dump\")\n    .stdout(\n      \"\n    foo:\n        echo bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_with_docstring() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    # bar\n    foo:\n        echo bar\n  \",\n    )\n    .stdout(\n      \"\n    # bar\n    foo:\n        echo bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_with_comments_in_body() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo:\n        # bar\n        echo bar\n  \",\n    )\n    .stdout(\n      \"\n    foo:\n        # bar\n        echo bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_body_is_comment() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo:\n        # bar\n  \",\n    )\n    .stdout(\n      \"\n    foo:\n        # bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_several_commands() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo:\n        echo bar\n        echo baz\n  \",\n    )\n    .stdout(\n      \"\n    foo:\n        echo bar\n        echo baz\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_quiet() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    @foo:\n        echo bar\n  \",\n    )\n    .stdout(\n      \"\n    @foo:\n        echo bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_quiet_command() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo:\n        @echo bar\n  \",\n    )\n    .stdout(\n      \"\n    foo:\n        @echo bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_quiet_comment() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo:\n        @# bar\n  \",\n    )\n    .stdout(\n      \"\n    foo:\n        @# bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_ignore_errors() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo:\n        -echo foo\n  \",\n    )\n    .stdout(\n      \"\n    foo:\n        -echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_parameter() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo BAR:\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    foo BAR:\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_parameter_default() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo BAR='bar':\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    foo BAR='bar':\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_parameter_envar() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo $BAR:\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    foo $BAR:\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_parameter_default_envar() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo $BAR='foo':\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    foo $BAR='foo':\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_parameter_concat() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo BAR=('bar' + 'baz'):\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    foo BAR=('bar' + 'baz'):\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_parameters() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo BAR BAZ:\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    foo BAR BAZ:\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_parameters_envar() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo $BAR $BAZ:\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    foo $BAR $BAZ:\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_variadic_plus() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo +BAR:\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    foo +BAR:\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_variadic_star() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo *BAR:\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    foo *BAR:\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_positional_variadic() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo BAR *BAZ:\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    foo BAR *BAZ:\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_variadic_default() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo +BAR='bar':\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    foo +BAR='bar':\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_parameter_in_body() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo BAR:\n        echo {{ BAR }}\n  \",\n    )\n    .stdout(\n      \"\n    foo BAR:\n        echo {{ BAR }}\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_parameter_conditional() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo BAR:\n        echo {{ if 'foo' == 'foo' { 'foo' } else { 'bar' } }}\n  \",\n    )\n    .stdout(\n      \"\n    foo BAR:\n        echo {{ if 'foo' == 'foo' { 'foo' } else { 'bar' } }}\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_escaped_braces() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo BAR:\n        echo '{{{{BAR}}}}'\n  \",\n    )\n    .stdout(\n      \"\n    foo BAR:\n        echo '{{{{BAR}}}}'\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_assignment_in_body() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    bar := 'bar'\n\n    foo:\n        echo $bar\n  \",\n    )\n    .stdout(\n      \"\n    bar := 'bar'\n\n    foo:\n        echo $bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_dependency() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    bar:\n        echo bar\n\n    foo: bar\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    bar:\n        echo bar\n\n    foo: bar\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_dependency_param() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    bar BAR:\n        echo bar\n\n    foo: (bar 'bar')\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    bar BAR:\n        echo bar\n\n    foo: (bar 'bar')\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_dependency_params() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    bar BAR BAZ:\n        echo bar\n\n    foo: (bar 'bar' 'baz')\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    bar BAR BAZ:\n        echo bar\n\n    foo: (bar 'bar' 'baz')\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_dependencies() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    bar:\n        echo bar\n\n    baz:\n        echo baz\n\n    foo: baz bar\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    bar:\n        echo bar\n\n    baz:\n        echo baz\n\n    foo: baz bar\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn recipe_dependencies_params() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    bar BAR:\n        echo bar\n\n    baz BAZ:\n        echo baz\n\n    foo: (baz 'baz') (bar 'bar')\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    bar BAR:\n        echo bar\n\n    baz BAZ:\n        echo baz\n\n    foo: (baz 'baz') (bar 'bar')\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn set_true_explicit() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    set export := true\n  \",\n    )\n    .stdout(\n      \"\n    set export := true\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn set_true_implicit() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    set export\n  \",\n    )\n    .stdout(\n      \"\n    set export := true\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn set_false() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    set export := false\n  \",\n    )\n    .stdout(\n      \"\n    set export := false\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn set_shell() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      r#\"\n    set shell := ['sh', \"-c\"]\n  \"#,\n    )\n    .stdout(\n      r#\"\n    set shell := ['sh', \"-c\"]\n  \"#,\n    )\n    .success();\n}\n\n#[test]\nfn comment() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    # foo\n  \",\n    )\n    .stdout(\n      \"\n    # foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn comment_multiline() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    # foo\n    # bar\n  \",\n    )\n    .stdout(\n      \"\n    # foo\n    # bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn comment_leading() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    # foo\n\n    foo := 'bar'\n  \",\n    )\n    .stdout(\n      \"\n    # foo\n\n    foo := 'bar'\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn comment_trailing() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo := 'bar'\n\n    # foo\n  \",\n    )\n    .stdout(\n      \"\n    foo := 'bar'\n\n    # foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn comment_before_recipe() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    # foo\n\n    foo:\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    # foo\n\n    foo:\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn comment_before_docstring_recipe() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    # bar\n\n    # foo\n    foo:\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    # bar\n\n    # foo\n    foo:\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn group_recipes() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo:\n        echo foo\n    bar:\n        echo bar\n  \",\n    )\n    .stdout(\n      \"\n    foo:\n        echo foo\n\n    bar:\n        echo bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn group_aliases() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    alias f := foo\n\n    alias b := bar\n\n    foo:\n        echo foo\n\n    bar:\n        echo bar\n  \",\n    )\n    .stdout(\n      \"\n    alias f := foo\n    alias b := bar\n\n    foo:\n        echo foo\n\n    bar:\n        echo bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn group_assignments() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo := 'foo'\n    bar := 'bar'\n  \",\n    )\n    .stdout(\n      \"\n    foo := 'foo'\n    bar := 'bar'\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn group_sets() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    set export := true\n    set positional-arguments := true\n  \",\n    )\n    .stdout(\n      \"\n    set export := true\n    set positional-arguments := true\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn group_comments() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    # foo\n\n    # bar\n  \",\n    )\n    .stdout(\n      \"\n    # foo\n    # bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn separate_recipes_aliases() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    alias f := foo\n    foo:\n        echo foo\n  \",\n    )\n    .stdout(\n      \"\n    alias f := foo\n\n    foo:\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn no_trailing_newline() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    foo:\n        echo foo\",\n    )\n    .stdout(\n      \"\n    foo:\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn subsequent() {\n  Test::new()\n    .arg(\"--dump\")\n    .justfile(\n      \"\n    bar:\n    foo: && bar\n        echo foo\",\n    )\n    .stdout(\n      \"\n    bar:\n\n    foo: && bar\n        echo foo\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn exported_parameter() {\n  Test::new()\n    .justfile(\"foo +$f:\")\n    .args([\"--dump\"])\n    .stdout(\"foo +$f:\\n\")\n    .success();\n}\n\n#[test]\nfn multi_argument_attribute() {\n  Test::new()\n    .justfile(\n      \"\n        set unstable\n\n        [script('a', 'b', 'c')]\n        foo:\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\n      \"\n        set unstable := true\n\n        [script('a', 'b', 'c')]\n        foo:\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn doc_attribute_suppresses_comment() {\n  Test::new()\n    .justfile(\n      \"\n        set unstable\n\n        # COMMENT\n        [doc('ATTRIBUTE')]\n        foo:\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\n      \"\n        set unstable := true\n\n        [doc('ATTRIBUTE')]\n        foo:\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn unchanged_justfiles_are_not_written_to_disk() {\n  let tmp = tempdir();\n\n  let justfile = tmp.path().join(\"justfile\");\n\n  fs::write(&justfile, \"\").unwrap();\n\n  let mut permissions = fs::metadata(&justfile).unwrap().permissions();\n  permissions.set_readonly(true);\n  fs::set_permissions(&justfile, permissions).unwrap();\n\n  Test::with_tempdir(tmp)\n    .no_justfile()\n    .args([\"--fmt\", \"--unstable\"])\n    .success();\n}\n\n#[test]\nfn if_else() {\n  Test::new()\n    .justfile(\n      \"\n        x := if '' == '' { '' } else if '' == '' { '' } else { '' }\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\n      \"\n        x := if '' == '' { '' } else if '' == '' { '' } else { '' }\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn private_variable() {\n  Test::new()\n    .justfile(\n      \"\n        [private]\n        foo := 'bar'\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\n      \"\n        [private]\n        foo := 'bar'\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn module_groups_are_preserved() {\n  Test::new()\n    .justfile(\n      r#\"\n        [group('bar')]\n        [group(\"baz\")]\n        mod foo\n      \"#,\n    )\n    .write(\"foo.just\", \"\")\n    .arg(\"--dump\")\n    .stdout(\n      r#\"\n        [group: 'bar']\n        [group: \"baz\"]\n        mod foo\n      \"#,\n    )\n    .success();\n}\n\n#[test]\nfn module_docs_are_preserved() {\n  Test::new()\n    .justfile(\n      r\"\n        # bar\n        mod foo\n      \",\n    )\n    .write(\"foo.just\", \"\")\n    .arg(\"--dump\")\n    .stdout(\n      r\"\n        # bar\n        mod foo\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn arg_attribute_long() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar')]\n        @foo bar:\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\n      \"\n        [arg('bar', long='bar')]\n        @foo bar:\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn arg_attribute_pattern() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', pattern='bar')]\n        @foo bar:\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\n      \"\n        [arg('bar', pattern='bar')]\n        @foo bar:\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn arg_attribute_long_and_pattern() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='foo', pattern='baz')]\n        @foo bar:\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\n      \"\n        [arg('bar', long='foo', pattern='baz')]\n        @foo bar:\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn arg_attribute_help() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', help='foo')]\n        @foo bar:\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\n      \"\n        [arg('bar', help='foo')]\n        @foo bar:\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn missing_import_file() {\n  Test::new()\n    .args([\"--unstable\", \"--fmt\", \"--check\"])\n    .justfile(\"import 'foo'\\n\")\n    .test_round_trip(false)\n    .success();\n}\n\n#[test]\nfn missing_module_file() {\n  Test::new()\n    .args([\"--unstable\", \"--fmt\", \"--check\"])\n    .justfile(\"mod foo\\n\")\n    .test_round_trip(false)\n    .success();\n}\n\n#[test]\nfn undefined_variable() {\n  Test::new()\n    .args([\"--unstable\", \"--fmt\", \"--check\"])\n    .justfile(\n      \"\n      foo:\n          echo {{ ABC }}\n      \",\n    )\n    .test_round_trip(false)\n    .success();\n}\n"
  },
  {
    "path": "tests/format_string.rs",
    "content": "use super::*;\n\n#[test]\nfn empty() {\n  Test::new()\n    .justfile(\n      \"\n        foo := f''\n\n        @baz:\n          echo {{foo}}\n      \",\n    )\n    .stdout(\"\\n\")\n    .unindent_stdout(false)\n    .success();\n}\n\n#[test]\nfn simple() {\n  Test::new()\n    .justfile(\n      \"\n        foo := f'bar'\n\n        @baz:\n          echo {{foo}}\n      \",\n    )\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn compound() {\n  Test::new()\n    .justfile(\n      \"\n        bar := 'BAR'\n        foo := f'FOO{{ bar }}BAZ'\n\n        @baz:\n          echo {{foo}}\n      \",\n    )\n    .stdout(\"FOOBARBAZ\\n\")\n    .success();\n}\n\n#[test]\nfn newline() {\n  Test::new()\n    .justfile(\n      \"\n        bar := 'BAR'\n        foo := f'FOO{{\n          bar + 'XYZ'\n        }}BAZ'\n\n        @baz:\n          echo {{foo}}\n      \",\n    )\n    .stdout(\"FOOBARXYZBAZ\\n\")\n    .success();\n}\n\n#[test]\nfn conditional() {\n  Test::new()\n    .justfile(\n      \"\n        foo := f'FOO{{\n          if 'a' == 'b' { 'c' } else { 'd' }\n        }}BAZ'\n\n        @baz:\n          echo {{foo}}\n      \",\n    )\n    .stdout(\"FOOdBAZ\\n\")\n    .success();\n}\n\n#[test]\nfn conditional_no_whitespace() {\n  Test::new()\n    .justfile(\n      \"\n        foo := f'FOO{{if 'a' == 'b' { 'c' } else { 'd' }}}BAZ'\n\n        @baz:\n          echo {{foo}}\n      \",\n    )\n    .stdout(\"FOOdBAZ\\n\")\n    .success();\n}\n\n#[test]\nfn inner_delimiter() {\n  Test::new()\n    .justfile(\n      \"\n        bar := 'BAR'\n        foo := f'FOO{{(bar)}}BAZ'\n\n        @baz:\n          echo {{foo}}\n      \",\n    )\n    .stdout(\"FOOBARBAZ\\n\")\n    .success();\n}\n\n#[test]\nfn nested() {\n  Test::new()\n    .justfile(\n      \"\n        bar := 'BAR'\n        foo := f'FOO{{f'[{{bar}}]'}}BAZ'\n\n        @baz:\n          echo {{foo}}\n      \",\n    )\n    .stdout(\"FOO[BAR]BAZ\\n\")\n    .success();\n}\n\n#[test]\nfn recipe_body() {\n  Test::new()\n    .justfile(\n      \"\n        bar := 'BAR'\n        @baz:\n          echo {{f'FOO{{f'[{{bar}}]'}}BAZ'}}\n      \",\n    )\n    .stdout(\"FOO[BAR]BAZ\\n\")\n    .success();\n}\n\n#[test]\nfn unclosed() {\n  Test::new()\n    .justfile(\"foo := f'FOO{{\")\n    .stderr(\n      \"\n        error: Expected backtick, identifier, '(', '/', or string, but found end of file\n         ——▶ justfile:1:15\n          │\n        1 │ foo := f'FOO{{\n          │               ^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn unmatched_close_is_ignored() {\n  Test::new()\n    .justfile(\n      \"\n        foo := f'}}'\n\n        @baz:\n          echo {{foo}}\n      \",\n    )\n    .stdout(\"}}\\n\")\n    .unindent_stdout(false)\n    .success();\n}\n\n#[test]\nfn delimiter_may_be_escaped_in_double_quoted_strings() {\n  Test::new()\n    .justfile(\n      r#\"\n        foo := f\"{{{{\"\n      \"#,\n    )\n    .args([\"--evaluate\", \"foo\"])\n    .stdout(\"{{\")\n    .success();\n}\n\n#[test]\nfn delimiter_may_be_escaped_in_single_quoted_strings() {\n  Test::new()\n    .justfile(\n      \"\n        foo := f'{{{{'\n      \",\n    )\n    .args([\"--evaluate\", \"foo\"])\n    .stdout(\"{{\")\n    .success();\n}\n\n#[test]\nfn escaped_delimiter_is_ignored_in_normal_strings() {\n  Test::new()\n    .justfile(\n      \"\n        foo := '{{{{'\n      \",\n    )\n    .args([\"--evaluate\", \"foo\"])\n    .stdout(\"{{{{\")\n    .success();\n}\n\n#[test]\nfn escaped_delimiter_in_single_quoted_format_string() {\n  Test::new()\n    .justfile(\n      r\"\n        foo := f'\\{{{{'\n      \",\n    )\n    .args([\"--evaluate\", \"foo\"])\n    .stdout(\"\\\\{{\")\n    .success();\n}\n\n#[test]\nfn escaped_delimiter_in_double_quoted_format_string() {\n  Test::new()\n    .justfile(\n      r#\"\n        foo := f\"\\{{{{\"\n      \"#,\n    )\n    .args([\"--evaluate\", \"foo\"])\n    .stderr(\n      r#\"\n        error: `\\{` is not a valid escape sequence\n         ——▶ justfile:1:9\n          │\n        1 │ foo := f\"\\{{{{\"\n          │         ^^^^^^^\n      \"#,\n    )\n    .failure();\n}\n\n#[test]\nfn double_quotes_process_escapes() {\n  Test::new()\n    .justfile(\n      r#\"\n        foo := f\"\\u{61}{{\"b\"}}\\u{63}{{\"d\"}}\\u{65}\"\n      \"#,\n    )\n    .args([\"--evaluate\", \"foo\"])\n    .stdout(\"abcde\")\n    .success();\n}\n\n#[test]\nfn single_quotes_do_not_process_escapes() {\n  Test::new()\n    .justfile(\n      r#\"\n        foo := f'\\n{{\"a\"}}\\n{{\"b\"}}\\n'\n      \"#,\n    )\n    .args([\"--evaluate\", \"foo\"])\n    .stdout(r\"\\na\\nb\\n\")\n    .success();\n}\n\n#[test]\nfn indented_format_strings() {\n  Test::new()\n    .justfile(\n      r#\"\n        foo := f'''\n          a\n          {{\"b\"}}\n          c\n        '''\n      \"#,\n    )\n    .args([\"--evaluate\", \"foo\"])\n    .stdout(\"a\\nb\\nc\\n\")\n    .success();\n}\n\n#[test]\nfn un_indented_format_strings() {\n  Test::new()\n    .justfile(\n      r#\"\n        foo := f'\n          a\n          {{\"b\"}}\n          c\n        '\n      \"#,\n    )\n    .args([\"--evaluate\", \"foo\"])\n    .stdout(\"\\n  a\\n  b\\n  c\\n\")\n    .unindent_stdout(false)\n    .success();\n}\n\n#[test]\nfn dump() {\n  #[track_caller]\n  fn case(string: &str) {\n    Test::new()\n      .justfile(format!(\n        \"\n          foo := {string}\n        \"\n      ))\n      .arg(\"--dump\")\n      .stdout(format!(\"foo := {string}\\n\"))\n      .success();\n  }\n  case(\"f''\");\n  case(\"f''''''\");\n  case(r#\"f\"\"\"#);\n  case(r#\"f\"\"\"\"\"\"\"#);\n  case(\"f'{{'a'}}b{{'c'}}d'\");\n  case(\"f'''{{'a'}}b{{'c'}}d'''\");\n  case(r#\"f\"\"\"{{'a'}}b{{'c'}}d\"\"\"\"#);\n}\n\n#[test]\nfn undefined_variable_error() {\n  Test::new()\n    .justfile(\n      \"\n        foo := f'{{bar}}'\n      \",\n    )\n    .stderr(\n      \"\n        error: Variable `bar` not defined\n         ——▶ justfile:1:12\n          │\n        1 │ foo := f'{{bar}}'\n          │            ^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn format_string_followed_by_recipe() {\n  Test::new()\n    .justfile(\n      \"\n        foo := f'{{'foo'}}{{'bar'}}'\n        bar:\n      \",\n    )\n    .success();\n}\n"
  },
  {
    "path": "tests/functions.rs",
    "content": "use super::*;\n\n#[test]\nfn test_os_arch_functions_in_interpolation() {\n  Test::new()\n    .justfile(\n      r\"\nfoo:\n  echo {{arch()}} {{os()}} {{os_family()}} {{num_cpus()}}\n\",\n    )\n    .stdout(\n      format!(\n        \"{} {} {} {}\\n\",\n        target::arch(),\n        target::os(),\n        target::family(),\n        num_cpus::get()\n      )\n      .as_str(),\n    )\n    .stderr(\n      format!(\n        \"echo {} {} {} {}\\n\",\n        target::arch(),\n        target::os(),\n        target::family(),\n        num_cpus::get()\n      )\n      .as_str(),\n    )\n    .success();\n}\n\n#[test]\nfn test_os_arch_functions_in_expression() {\n  Test::new()\n    .justfile(\n      r\"\na := arch()\no := os()\nf := os_family()\nn := num_cpus()\n\nfoo:\n  echo {{a}} {{o}} {{f}} {{n}}\n\",\n    )\n    .stdout(\n      format!(\n        \"{} {} {} {}\\n\",\n        target::arch(),\n        target::os(),\n        target::family(),\n        num_cpus::get()\n      )\n      .as_str(),\n    )\n    .stderr(\n      format!(\n        \"echo {} {} {} {}\\n\",\n        target::arch(),\n        target::os(),\n        target::family(),\n        num_cpus::get()\n      )\n      .as_str(),\n    )\n    .success();\n}\n\n#[test]\nfn env_var_functions_unix() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\np := env_var('USER')\nb := env_var_or_default('ZADDY', 'HTAP')\nx := env_var_or_default('XYZ', 'ABC')\n\nfoo:\n  /usr/bin/env echo '{{p}}' '{{b}}' '{{x}}'\n\",\n    )\n    .stdout(format!(\"{} HTAP ABC\\n\", env::var(\"USER\").unwrap()).as_str())\n    .stderr(\n      format!(\n        \"/usr/bin/env echo '{}' 'HTAP' 'ABC'\\n\",\n        env::var(\"USER\").unwrap()\n      )\n      .as_str(),\n    )\n    .success();\n}\n\n#[test]\nfn path_functions() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\nwe  := without_extension('/foo/bar/baz.hello')\nfs  := file_stem('/foo/bar/baz.hello')\nfn  := file_name('/foo/bar/baz.hello')\ndir := parent_directory('/foo/bar/baz.hello')\next := extension('/foo/bar/baz.hello')\njn  := join('a', 'b')\n\nfoo:\n  /usr/bin/env echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}' '{{jn}}'\n\",\n    )\n    .stdout(\"/foo/bar/baz baz baz.hello /foo/bar hello a/b\\n\")\n    .stderr(\"/usr/bin/env echo '/foo/bar/baz' 'baz' 'baz.hello' '/foo/bar' 'hello' 'a/b'\\n\")\n    .success();\n}\n\n#[test]\nfn path_functions2() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\nwe  := without_extension('/foo/bar/baz')\nfs  := file_stem('/foo/bar/baz.hello.ciao')\nfn  := file_name('/bar/baz.hello.ciao')\ndir := parent_directory('/foo/')\next := extension('/foo/bar/baz.hello.ciao')\n\nfoo:\n  /usr/bin/env echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}'\n\",\n    )\n    .stdout(\"/foo/bar/baz baz.hello baz.hello.ciao / ciao\\n\")\n    .stderr(\"/usr/bin/env echo '/foo/bar/baz' 'baz.hello' 'baz.hello.ciao' '/' 'ciao'\\n\")\n    .success();\n}\n\n#[test]\nfn broken_without_extension_function() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\nwe  := without_extension('')\n\nfoo:\n  /usr/bin/env echo '{{we}}'\n\",\n    )\n    .stderr(\n      format!(\n        \"{} {}\\n{}\\n{}\\n{}\\n{}\\n\",\n        \"error: Call to function `without_extension` failed:\",\n        \"Could not extract parent from ``\",\n        \" ——▶ justfile:1:8\",\n        \"  │\",\n        \"1 │ we  := without_extension(\\'\\')\",\n        \"  │        ^^^^^^^^^^^^^^^^^\"\n      )\n      .as_str(),\n    )\n    .failure();\n}\n\n#[test]\nfn broken_extension_function() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\nwe  := extension('')\n\nfoo:\n  /usr/bin/env echo '{{we}}'\n\",\n    )\n    .stderr(\n      format!(\n        \"{}\\n{}\\n{}\\n{}\\n{}\\n\",\n        \"error: Call to function `extension` failed: Could not extract extension from ``\",\n        \" ——▶ justfile:1:8\",\n        \"  │\",\n        \"1 │ we  := extension(\\'\\')\",\n        \"  │        ^^^^^^^^^\"\n      )\n      .as_str(),\n    )\n    .failure();\n}\n\n#[test]\nfn broken_extension_function2() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\nwe  := extension('foo')\n\nfoo:\n  /usr/bin/env echo '{{we}}'\n\",\n    )\n    .stderr(\n      format!(\n        \"{}\\n{}\\n{}\\n{}\\n{}\\n\",\n        \"error: Call to function `extension` failed: Could not extract extension from `foo`\",\n        \" ——▶ justfile:1:8\",\n        \"  │\",\n        \"1 │ we  := extension(\\'foo\\')\",\n        \"  │        ^^^^^^^^^\"\n      )\n      .as_str(),\n    )\n    .failure();\n}\n\n#[test]\nfn broken_file_stem_function() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\nwe  := file_stem('')\n\nfoo:\n  /usr/bin/env echo '{{we}}'\n\",\n    )\n    .stderr(\n      format!(\n        \"{}\\n{}\\n{}\\n{}\\n{}\\n\",\n        \"error: Call to function `file_stem` failed: Could not extract file stem from ``\",\n        \" ——▶ justfile:1:8\",\n        \"  │\",\n        \"1 │ we  := file_stem(\\'\\')\",\n        \"  │        ^^^^^^^^^\"\n      )\n      .as_str(),\n    )\n    .failure();\n}\n\n#[test]\nfn broken_file_name_function() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\nwe  := file_name('')\n\nfoo:\n  /usr/bin/env echo '{{we}}'\n\",\n    )\n    .stderr(\n      format!(\n        \"{}\\n{}\\n{}\\n{}\\n{}\\n\",\n        \"error: Call to function `file_name` failed: Could not extract file name from ``\",\n        \" ——▶ justfile:1:8\",\n        \"  │\",\n        \"1 │ we  := file_name(\\'\\')\",\n        \"  │        ^^^^^^^^^\"\n      )\n      .as_str(),\n    )\n    .failure();\n}\n\n#[test]\nfn broken_directory_function() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\nwe  := parent_directory('')\n\nfoo:\n  /usr/bin/env echo '{{we}}'\n\",\n    )\n    .stderr(\n      format!(\n        \"{} {}\\n{}\\n{}\\n{}\\n{}\\n\",\n        \"error: Call to function `parent_directory` failed:\",\n        \"Could not extract parent directory from ``\",\n        \" ——▶ justfile:1:8\",\n        \"  │\",\n        \"1 │ we  := parent_directory(\\'\\')\",\n        \"  │        ^^^^^^^^^^^^^^^^\"\n      )\n      .as_str(),\n    )\n    .failure();\n}\n\n#[test]\nfn broken_directory_function2() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\nwe  := parent_directory('/')\n\nfoo:\n  /usr/bin/env echo '{{we}}'\n\",\n    )\n    .stderr(\n      format!(\n        \"{} {}\\n{}\\n{}\\n{}\\n{}\\n\",\n        \"error: Call to function `parent_directory` failed:\",\n        \"Could not extract parent directory from `/`\",\n        \" ——▶ justfile:1:8\",\n        \"  │\",\n        \"1 │ we  := parent_directory(\\'/\\')\",\n        \"  │        ^^^^^^^^^^^^^^^^\"\n      )\n      .as_str(),\n    )\n    .failure();\n}\n\n#[test]\nfn env_var_functions_windows() {\n  if cfg!(not(windows)) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\np := env_var('USERNAME')\nb := env_var_or_default('ZADDY', 'HTAP')\nx := env_var_or_default('XYZ', 'ABC')\n\nfoo:\n  /usr/bin/env echo '{{p}}' '{{b}}' '{{x}}'\n\",\n    )\n    .stdout(format!(\"{} HTAP ABC\\n\", env::var(\"USERNAME\").unwrap()).as_str())\n    .stderr(\n      format!(\n        \"/usr/bin/env echo '{}' 'HTAP' 'ABC'\\n\",\n        env::var(\"USERNAME\").unwrap()\n      )\n      .as_str(),\n    )\n    .success();\n}\n\n#[test]\nfn env_var_failure() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\"a:\\n  echo {{env_var('ZADDY')}}\")\n    .stderr(\n      \"error: Call to function `env_var` failed: environment variable `ZADDY` not present\n ——▶ justfile:2:10\n  │\n2 │   echo {{env_var('ZADDY')}}\n  │          ^^^^^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn test_just_executable_function() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      \"\n    a:\n      @printf 'Executable path is: %s\\\\n' '{{ just_executable() }}'\n  \",\n    )\n    .stdout(format!(\"Executable path is: {JUST}\\n\"))\n    .success();\n}\n\n#[test]\nfn test_os_arch_functions_in_default() {\n  Test::new()\n    .justfile(\n      r\"\nfoo a=arch() o=os() f=os_family() n=num_cpus():\n  echo {{a}} {{o}} {{f}} {{n}}\n\",\n    )\n    .stdout(\n      format!(\n        \"{} {} {} {}\\n\",\n        target::arch(),\n        target::os(),\n        target::family(),\n        num_cpus::get()\n      )\n      .as_str(),\n    )\n    .stderr(\n      format!(\n        \"echo {} {} {} {}\\n\",\n        target::arch(),\n        target::os(),\n        target::family(),\n        num_cpus::get()\n      )\n      .as_str(),\n    )\n    .success();\n}\n\n#[test]\nfn clean() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ clean('a/../b') }}\n  \",\n    )\n    .stdout(\"b\\n\")\n    .stderr(\"echo b\\n\")\n    .success();\n}\n\n#[test]\nfn uppercase() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ uppercase('bar') }}\n  \",\n    )\n    .stdout(\"BAR\\n\")\n    .stderr(\"echo BAR\\n\")\n    .success();\n}\n\n#[test]\nfn lowercase() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ lowercase('BAR') }}\n  \",\n    )\n    .stdout(\"bar\\n\")\n    .stderr(\"echo bar\\n\")\n    .success();\n}\n\n#[test]\nfn uppercamelcase() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ uppercamelcase('foo bar') }}\n  \",\n    )\n    .stdout(\"FooBar\\n\")\n    .stderr(\"echo FooBar\\n\")\n    .success();\n}\n\n#[test]\nfn lowercamelcase() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ lowercamelcase('foo bar') }}\n  \",\n    )\n    .stdout(\"fooBar\\n\")\n    .stderr(\"echo fooBar\\n\")\n    .success();\n}\n\n#[test]\nfn snakecase() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ snakecase('foo bar') }}\n  \",\n    )\n    .stdout(\"foo_bar\\n\")\n    .stderr(\"echo foo_bar\\n\")\n    .success();\n}\n\n#[test]\nfn kebabcase() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ kebabcase('foo bar') }}\n  \",\n    )\n    .stdout(\"foo-bar\\n\")\n    .stderr(\"echo foo-bar\\n\")\n    .success();\n}\n\n#[test]\nfn shoutysnakecase() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ shoutysnakecase('foo bar') }}\n  \",\n    )\n    .stdout(\"FOO_BAR\\n\")\n    .stderr(\"echo FOO_BAR\\n\")\n    .success();\n}\n\n#[test]\nfn titlecase() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ titlecase('foo bar') }}\n  \",\n    )\n    .stdout(\"Foo Bar\\n\")\n    .stderr(\"echo Foo Bar\\n\")\n    .success();\n}\n\n#[test]\nfn shoutykebabcase() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ shoutykebabcase('foo bar') }}\n  \",\n    )\n    .stdout(\"FOO-BAR\\n\")\n    .stderr(\"echo FOO-BAR\\n\")\n    .success();\n}\n\n#[test]\nfn trim() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ trim('   bar   ') }}\n  \",\n    )\n    .stdout(\"bar\\n\")\n    .stderr(\"echo bar\\n\")\n    .success();\n}\n\n#[test]\nfn replace() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ replace('barbarbar', 'bar', 'foo') }}\n  \",\n    )\n    .stdout(\"foofoofoo\\n\")\n    .stderr(\"echo foofoofoo\\n\")\n    .success();\n}\n\n#[test]\nfn replace_regex() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ replace_regex('123bar123bar123bar', '\\\\d+bar', 'foo') }}\n  \",\n    )\n    .stdout(\"foofoofoo\\n\")\n    .stderr(\"echo foofoofoo\\n\")\n    .success();\n}\n\n#[test]\nfn invalid_replace_regex() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{ replace_regex('barbarbar', 'foo\\\\', 'foo') }}\n  \",\n    )\n    .stderr(\n      \"error: Call to function `replace_regex` failed: regex parse error:\n    foo\\\\\n       ^\nerror: incomplete escape sequence, reached end of pattern prematurely\n ——▶ justfile:2:11\n  │\n2 │   echo {{ replace_regex('barbarbar', 'foo\\\\', 'foo') }}\n  │           ^^^^^^^^^^^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn capitalize() {\n  Test::new()\n    .justfile(\n      \"\n      foo:\n        echo {{ capitalize('BAR') }}\n    \",\n    )\n    .stdout(\"Bar\\n\")\n    .stderr(\"echo Bar\\n\")\n    .success();\n}\n\n#[test]\nfn semver_matches() {\n  Test::new()\n    .justfile(\n      \"\n      foo:\n        echo {{ semver_matches('0.1.0', '>=0.1.0') }}\n        echo {{ semver_matches('0.1.0', '=0.0.1') }}\n    \",\n    )\n    .stdout(\"true\\nfalse\\n\")\n    .stderr(\"echo true\\necho false\\n\")\n    .success();\n}\n\n#[test]\nfn trim_end_matches() {\n  assert_eval_eq(\"trim_end_matches('foo', 'o')\", \"f\");\n  assert_eval_eq(\"trim_end_matches('fabab', 'ab')\", \"f\");\n  assert_eval_eq(\"trim_end_matches('fbaabab', 'ab')\", \"fba\");\n}\n\n#[test]\nfn trim_end_match() {\n  assert_eval_eq(\"trim_end_match('foo', 'o')\", \"fo\");\n  assert_eval_eq(\"trim_end_match('fabab', 'ab')\", \"fab\");\n}\n\n#[test]\nfn trim_start_matches() {\n  assert_eval_eq(\"trim_start_matches('oof', 'o')\", \"f\");\n  assert_eval_eq(\"trim_start_matches('ababf', 'ab')\", \"f\");\n  assert_eval_eq(\"trim_start_matches('ababbaf', 'ab')\", \"baf\");\n}\n\n#[test]\nfn trim_start_match() {\n  assert_eval_eq(\"trim_start_match('oof', 'o')\", \"of\");\n  assert_eval_eq(\"trim_start_match('ababf', 'ab')\", \"abf\");\n}\n\n#[test]\nfn trim_start() {\n  assert_eval_eq(\"trim_start('  f  ')\", \"f  \");\n}\n\n#[test]\nfn trim_end() {\n  assert_eval_eq(\"trim_end('  f  ')\", \"  f\");\n}\n\n#[test]\nfn append() {\n  assert_eval_eq(\"append('8', 'r s t')\", \"r8 s8 t8\");\n  assert_eval_eq(\"append('.c', 'main sar x11')\", \"main.c sar.c x11.c\");\n  assert_eval_eq(\"append('-', 'c v h y')\", \"c- v- h- y-\");\n  assert_eval_eq(\n    \"append('0000', '11 10 01 00')\",\n    \"110000 100000 010000 000000\",\n  );\n  assert_eval_eq(\n    \"append('tion', '\n    Determina\n    Acquisi\n    Motiva\n    Conjuc\n    ')\",\n    \"Determination Acquisition Motivation Conjuction\",\n  );\n}\n\n#[test]\nfn prepend() {\n  assert_eval_eq(\"prepend('8', 'r s t\\n  \\n  ')\", \"8r 8s 8t\");\n  assert_eval_eq(\n    \"prepend('src/', 'main sar x11')\",\n    \"src/main src/sar src/x11\",\n  );\n  assert_eval_eq(\"prepend('-', 'c\\tv h\\ny')\", \"-c -v -h -y\");\n  assert_eval_eq(\n    \"prepend('0000', '11 10 01 00')\",\n    \"000011 000010 000001 000000\",\n  );\n  assert_eval_eq(\n    \"prepend('April-', '\n      1st,\n        17th,\n    20th,\n    ')\",\n    \"April-1st, April-17th, April-20th,\",\n  );\n}\n\n#[test]\nfn join_unix() {\n  if cfg!(windows) {\n    return;\n  }\n  assert_eval_eq(\"join('a', 'b', 'c', 'd')\", \"a/b/c/d\");\n  assert_eval_eq(\"join('a', '/b', 'c', 'd')\", \"/b/c/d\");\n  assert_eval_eq(\"join('a', '/b', '/c', 'd')\", \"/c/d\");\n  assert_eval_eq(\"join('a', '/b', '/c', '/d')\", \"/d\");\n}\n\n#[test]\nfn join_windows() {\n  if cfg!(not(windows)) {\n    return;\n  }\n  assert_eval_eq(\"join('a', 'b', 'c', 'd')\", \"a\\\\b\\\\c\\\\d\");\n  assert_eval_eq(\"join('a', '\\\\b', 'c', 'd')\", \"\\\\b\\\\c\\\\d\");\n  assert_eval_eq(\"join('a', '\\\\b', '\\\\c', 'd')\", \"\\\\c\\\\d\");\n  assert_eval_eq(\"join('a', '\\\\b', '\\\\c', '\\\\d')\", \"\\\\d\");\n}\n\n#[test]\nfn join_argument_count_error() {\n  Test::new()\n    .justfile(\"x := join('a')\")\n    .args([\"--evaluate\"])\n    .stderr(\n      \"\n      error: Function `join` called with 1 argument but takes 2 or more\n       ——▶ justfile:1:6\n        │\n      1 │ x := join(\\'a\\')\n        │      ^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn test_path_exists_filepath_exist() {\n  Test::new()\n    .tree(tree! {\n      testfile: \"\"\n    })\n    .justfile(\"x := path_exists('testfile')\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"true\")\n    .success();\n}\n\n#[test]\nfn test_path_exists_filepath_doesnt_exist() {\n  Test::new()\n    .justfile(\"x := path_exists('testfile')\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"false\")\n    .success();\n}\n\n#[test]\nfn error_errors_with_message() {\n  Test::new()\n    .justfile(\"x := error ('Thing Not Supported')\")\n    .args([\"--evaluate\"])\n    .stderr(\n      \"\n      error: Call to function `error` failed: Thing Not Supported\n       ——▶ justfile:1:6\n        │\n      1 │ x := error ('Thing Not Supported')\n        │      ^^^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn test_absolute_path_resolves() {\n  let test_object = Test::new()\n    .justfile(\"path := absolute_path('./test_file')\")\n    .args([\"--evaluate\", \"path\"]);\n\n  let mut tempdir = test_object.tempdir.path().to_owned();\n\n  // Just retrieves the current directory via env::current_dir(), which\n  // does the moral equivalent of canonicalize, which will remove symlinks.\n  // So, we have to canonicalize here, so that we can match it.\n  if cfg!(unix) {\n    tempdir = tempdir.canonicalize().unwrap();\n  }\n\n  test_object\n    .stdout(tempdir.join(\"test_file\").to_str().unwrap().to_owned())\n    .success();\n}\n\n#[test]\nfn test_absolute_path_resolves_parent() {\n  let test_object = Test::new()\n    .justfile(\"path := absolute_path('../test_file')\")\n    .args([\"--evaluate\", \"path\"]);\n\n  let mut tempdir = test_object.tempdir.path().to_owned();\n\n  // Just retrieves the current directory via env::current_dir(), which\n  // does the moral equivalent of canonicalize, which will remove symlinks.\n  // So, we have to canonicalize here, so that we can match it.\n  if cfg!(unix) {\n    tempdir = tempdir.canonicalize().unwrap();\n  }\n\n  test_object\n    .stdout(\n      tempdir\n        .parent()\n        .unwrap()\n        .join(\"test_file\")\n        .to_str()\n        .unwrap()\n        .to_owned(),\n    )\n    .success();\n}\n\n#[test]\nfn path_exists_subdir() {\n  Test::new()\n    .tree(tree! {\n      foo: \"\",\n      bar: {\n      }\n    })\n    .justfile(\"x := path_exists('foo')\")\n    .current_dir(\"bar\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"true\")\n    .success();\n}\n\n#[test]\nfn uuid() {\n  Test::new()\n    .justfile(\"x := uuid()\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout_regex(\"........-....-....-....-............\")\n    .success();\n}\n\n#[test]\nfn choose() {\n  Test::new()\n    .justfile(r\"x := choose('10', 'xXyYzZ')\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout_regex(\"^[X-Zx-z]{10}$\")\n    .success();\n}\n\n#[test]\nfn choose_bad_alphabet_empty() {\n  Test::new()\n    .justfile(\"x := choose('10', '')\")\n    .args([\"--evaluate\"])\n    .stderr(\n      \"\n      error: Call to function `choose` failed: empty alphabet\n       ——▶ justfile:1:6\n        │\n      1 │ x := choose('10', '')\n        │      ^^^^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn choose_bad_alphabet_repeated() {\n  Test::new()\n    .justfile(\"x := choose('10', 'aa')\")\n    .args([\"--evaluate\"])\n    .stderr(\n      \"\n      error: Call to function `choose` failed: alphabet contains repeated character `a`\n       ——▶ justfile:1:6\n        │\n      1 │ x := choose('10', 'aa')\n        │      ^^^^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn choose_bad_length() {\n  Test::new()\n    .justfile(\"x := choose('foo', HEX)\")\n    .args([\"--evaluate\"])\n    .stderr(\n      \"\n      error: Call to function `choose` failed: failed to parse `foo` as positive integer: invalid digit found in string\n       ——▶ justfile:1:6\n        │\n      1 │ x := choose('foo', HEX)\n        │      ^^^^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn sha256() {\n  Test::new()\n    .justfile(\"x := sha256('5943ee37-0000-1000-8000-010203040506')\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"2330d7f5eb94a820b54fed59a8eced236f80b633a504289c030b6a65aef58871\")\n    .success();\n}\n\n#[test]\nfn sha256_file() {\n  Test::new()\n    .justfile(\"x := sha256_file('sub/shafile')\")\n    .tree(tree! {\n      sub: {\n        shafile: \"just is great\\n\",\n      }\n    })\n    .current_dir(\"sub\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"177b3d79aaafb53a7a4d7aaba99a82f27c73370e8cb0295571aade1e4fea1cd2\")\n    .success();\n}\n\n#[test]\nfn just_pid() {\n  let Output { stdout, pid, .. } = Test::new()\n    .args([\"--evaluate\", \"x\"])\n    .justfile(\"x := just_pid()\")\n    .stdout_regex(r\"\\d+\")\n    .success();\n\n  assert_eq!(stdout.parse::<u32>().unwrap(), pid);\n}\n\n#[test]\nfn shell_no_argument() {\n  Test::new()\n    .justfile(\"var := shell()\")\n    .args([\"--evaluate\"])\n    .stderr(\n      \"\n      error: Function `shell` called with 0 arguments but takes 1 or more\n       ——▶ justfile:1:8\n        │\n      1 │ var := shell()\n        │        ^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn shell_minimal() {\n  assert_eval_eq(\"shell('echo $1 $2', 'justice', 'legs')\", \"justice legs\");\n}\n\n#[test]\nfn shell_args() {\n  assert_eval_eq(\"shell('echo $@', 'justice', 'legs')\", \"justice legs\");\n}\n\n#[test]\nfn shell_first_arg() {\n  assert_eval_eq(\"shell('echo $0')\", \"echo $0\");\n}\n\n#[test]\nfn shell_error() {\n  Test::new()\n    .justfile(\"var := shell('exit 1')\")\n    .args([\"--evaluate\"])\n    .stderr(\n      \"\n      error: Call to function `shell` failed: Process exited with status code 1\n       ——▶ justfile:1:8\n        │\n      1 │ var := shell('exit 1')\n        │        ^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn blake3() {\n  Test::new()\n    .justfile(\"x := blake3('5943ee37-0000-1000-8000-010203040506')\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"026c9f740a793ff536ddf05f8915ea4179421f47f0fa9545476076e9ba8f3f2b\")\n    .success();\n}\n\n#[test]\nfn blake3_file() {\n  Test::new()\n    .justfile(\"x := blake3_file('sub/blakefile')\")\n    .tree(tree! {\n      sub: {\n        blakefile: \"just is great\\n\",\n      }\n    })\n    .current_dir(\"sub\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"8379241877190ca4b94076a8c8f89fe5747f95c62f3e4bf41f7408a0088ae16d\")\n    .success();\n}\n\n#[cfg(unix)]\n#[test]\nfn canonicalize() {\n  Test::new()\n    .args([\"--evaluate\", \"x\"])\n    .justfile(\"x := canonicalize('foo')\")\n    .symlink(\"justfile\", \"foo\")\n    .stdout_regex(\".*/justfile\")\n    .success();\n}\n\n#[test]\nfn encode_uri_component() {\n  Test::new()\n    .justfile(\"x := encode_uri_component(\\\"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\\\\\\\"#$%&'()*+,-./:;<=>?@[\\\\\\\\]^_`{|}~ \\\\t\\\\r\\\\n🌐\\\")\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!%22%23%24%25%26'()*%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~%20%09%0D%0A%F0%9F%8C%90\")\n    .success();\n}\n\n#[test]\nfn source_file() {\n  Test::new()\n    .args([\"--evaluate\", \"x\"])\n    .justfile(\"x := source_file()\")\n    .stdout_regex(r\".*[/\\\\]justfile\")\n    .success();\n\n  Test::new()\n    .args([\"--evaluate\", \"x\"])\n    .justfile(\n      \"\n        import 'foo.just'\n      \",\n    )\n    .write(\"foo.just\", \"x := source_file()\")\n    .stdout_regex(r\".*[/\\\\]foo.just\")\n    .success();\n\n  Test::new()\n    .args([\"foo\", \"bar\"])\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .write(\"foo.just\", \"x := source_file()\\nbar:\\n @echo '{{x}}'\")\n    .stdout_regex(r\".*[/\\\\]foo.just\\n\")\n    .success();\n}\n\n#[test]\nfn source_directory() {\n  Test::new()\n    .args([\"foo\", \"bar\"])\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .write(\n      \"foo/mod.just\",\n      \"x := source_directory()\\nbar:\\n @echo '{{x}}'\",\n    )\n    .stdout_regex(r\".*[/\\\\]foo\\n\")\n    .success();\n}\n\n#[test]\nfn module_paths() {\n  Test::new()\n    .write(\n      \"foo/bar.just\",\n      \"\nimf := module_file()\nimd := module_directory()\n\nimport-outer: import-inner\n\n@import-inner pmf=module_file() pmd=module_directory():\n  echo import\n  echo '{{ imf }}'\n  echo '{{ imd }}'\n  echo '{{ pmf }}'\n  echo '{{ pmd }}'\n  echo '{{ module_file() }}'\n  echo '{{ module_directory() }}'\n      \",\n    )\n    .write(\n      \"baz/mod.just\",\n      \"\nimport 'foo/bar.just'\n\nmmf := module_file()\nmmd := module_directory()\n\nouter: inner\n\n@inner pmf=module_file() pmd=module_directory():\n  echo module\n  echo '{{ mmf }}'\n  echo '{{ mmd }}'\n  echo '{{ pmf }}'\n  echo '{{ pmd }}'\n  echo '{{ module_file() }}'\n  echo '{{ module_directory() }}'\n      \",\n    )\n    .write(\n      \"baz/foo/bar.just\",\n      \"\nimf := module_file()\nimd := module_directory()\n\nimport-outer: import-inner\n\n@import-inner pmf=module_file() pmd=module_directory():\n  echo import\n  echo '{{ imf }}'\n  echo '{{ imd }}'\n  echo '{{ pmf }}'\n  echo '{{ pmd }}'\n  echo '{{ module_file() }}'\n  echo '{{ module_directory() }}'\n      \",\n    )\n    .justfile(\n      \"\n        import 'foo/bar.just'\n        mod baz\n\n        rmf := module_file()\n        rmd := module_directory()\n\n        outer: inner\n\n        @inner pmf=module_file() pmd=module_directory():\n          echo root\n          echo '{{ rmf }}'\n          echo '{{ rmd }}'\n          echo '{{ pmf }}'\n          echo '{{ pmd }}'\n          echo '{{ module_file() }}'\n          echo '{{ module_directory() }}'\n      \",\n    )\n    .args([\n      \"outer\",\n      \"import-outer\",\n      \"baz\",\n      \"outer\",\n      \"baz\",\n      \"import-outer\",\n    ])\n    .stdout_regex(\n      r\"root\n.*[/\\\\]just-test-tempdir......[/\\\\]justfile\n.*[/\\\\]just-test-tempdir......\n.*[/\\\\]just-test-tempdir......[/\\\\]justfile\n.*[/\\\\]just-test-tempdir......\n.*[/\\\\]just-test-tempdir......[/\\\\]justfile\n.*[/\\\\]just-test-tempdir......\nimport\n.*[/\\\\]just-test-tempdir......[/\\\\]justfile\n.*[/\\\\]just-test-tempdir......\n.*[/\\\\]just-test-tempdir......[/\\\\]justfile\n.*[/\\\\]just-test-tempdir......\n.*[/\\\\]just-test-tempdir......[/\\\\]justfile\n.*[/\\\\]just-test-tempdir......\nmodule\n.*[/\\\\]just-test-tempdir......[/\\\\]baz[/\\\\]mod.just\n.*[/\\\\]just-test-tempdir......[/\\\\]baz\n.*[/\\\\]just-test-tempdir......[/\\\\]baz[/\\\\]mod.just\n.*[/\\\\]just-test-tempdir......[/\\\\]baz\n.*[/\\\\]just-test-tempdir......[/\\\\]baz[/\\\\]mod.just\n.*[/\\\\]just-test-tempdir......[/\\\\]baz\nimport\n.*[/\\\\]just-test-tempdir......[/\\\\]baz[/\\\\]mod.just\n.*[/\\\\]just-test-tempdir......[/\\\\]baz\n.*[/\\\\]just-test-tempdir......[/\\\\]baz[/\\\\]mod.just\n.*[/\\\\]just-test-tempdir......[/\\\\]baz\n.*[/\\\\]just-test-tempdir......[/\\\\]baz[/\\\\]mod.just\n.*[/\\\\]just-test-tempdir......[/\\\\]baz\n\",\n    )\n    .success();\n}\n\n#[test]\nfn is_dependency() {\n  let justfile = \"\n    alpha: beta\n      @echo 'alpha {{is_dependency()}}'\n    beta: && gamma\n      @echo 'beta {{is_dependency()}}'\n    gamma:\n      @echo 'gamma {{is_dependency()}}'\n  \";\n  Test::new()\n    .args([\"alpha\"])\n    .justfile(justfile)\n    .stdout(\"beta true\\ngamma true\\nalpha false\\n\")\n    .success();\n\n  Test::new()\n    .args([\"beta\"])\n    .justfile(justfile)\n    .stdout(\"beta false\\ngamma true\\n\")\n    .success();\n}\n\n#[test]\nfn unary_argument_count_mismamatch_error_message() {\n  Test::new()\n    .justfile(\"x := datetime()\")\n    .args([\"--evaluate\"])\n    .stderr(\n      \"\n      error: Function `datetime` called with 0 arguments but takes 1\n       ——▶ justfile:1:6\n        │\n      1 │ x := datetime()\n        │      ^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn dir_abbreviations_are_accepted() {\n  Test::new()\n    .justfile(\n      \"\n      abbreviated := justfile_dir()\n      unabbreviated := justfile_directory()\n\n      @foo:\n        # {{ assert(abbreviated == unabbreviated, 'fail') }}\n    \",\n    )\n    .success();\n}\n\n#[test]\nfn invocation_dir_native_abbreviation_is_accepted() {\n  Test::new()\n    .justfile(\n      \"\n      abbreviated := invocation_directory_native()\n      unabbreviated := invocation_dir_native()\n\n      @foo:\n        # {{ assert(abbreviated == unabbreviated, 'fail') }}\n    \",\n    )\n    .success();\n}\n\n#[test]\nfn absolute_path_argument_is_relative_to_submodule_working_directory() {\n  Test::new()\n    .justfile(\"mod foo\")\n    .write(\"foo/baz\", \"\")\n    .write(\n      \"foo/mod.just\",\n      r#\"\nbar:\n  @echo \"{{ absolute_path('baz') }}\"\n\n\"#,\n    )\n    .stdout_regex(r\".*[/\\\\]foo[/\\\\]baz\\n\")\n    .args([\"foo\", \"bar\"])\n    .success();\n}\n\n#[test]\nfn blake3_file_argument_is_relative_to_submodule_working_directory() {\n  Test::new()\n    .justfile(\"mod foo\")\n    .write(\"foo/baz\", \"\")\n    .write(\n      \"foo/mod.just\",\n      \"\nbar:\n  @echo {{ blake3_file('baz') }}\n\n\",\n    )\n    .stdout(\"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262\\n\")\n    .args([\"foo\", \"bar\"])\n    .success();\n}\n\n#[test]\nfn canonicalize_argument_is_relative_to_submodule_working_directory() {\n  Test::new()\n    .justfile(\"mod foo\")\n    .write(\"foo/baz\", \"\")\n    .write(\n      \"foo/mod.just\",\n      r#\"\nbar:\n  @echo \"{{ canonicalize('baz') }}\"\n\n\"#,\n    )\n    .stdout_regex(r\".*[/\\\\]foo[/\\\\]baz\\n\")\n    .args([\"foo\", \"bar\"])\n    .success();\n}\n\n#[test]\nfn path_exists_argument_is_relative_to_submodule_working_directory() {\n  Test::new()\n    .justfile(\"mod foo\")\n    .write(\"foo/baz\", \"\")\n    .write(\n      \"foo/mod.just\",\n      \"\nbar:\n  @echo {{ path_exists('baz') }}\n\n\",\n    )\n    .stdout_regex(\"true\\n\")\n    .args([\"foo\", \"bar\"])\n    .success();\n}\n\n#[test]\nfn sha256_file_argument_is_relative_to_submodule_working_directory() {\n  Test::new()\n    .justfile(\"mod foo\")\n    .write(\"foo/baz\", \"\")\n    .write(\n      \"foo/mod.just\",\n      \"\nbar:\n  @echo {{ sha256_file('baz') }}\n\n\",\n    )\n    .stdout_regex(\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\\n\")\n    .args([\"foo\", \"bar\"])\n    .success();\n}\n\n#[test]\nfn style_command_default() {\n  Test::new()\n    .justfile(\n      r#\"\n        foo:\n          @echo '{{ style(\"command\") }}foo{{NORMAL}}'\n      \"#,\n    )\n    .stdout(\"\\x1b[1mfoo\\x1b[0m\\n\")\n    .success();\n}\n\n#[test]\nfn style_command_non_default() {\n  Test::new()\n    .justfile(\n      r#\"\n        foo:\n          @echo '{{ style(\"command\") }}foo{{NORMAL}}'\n      \"#,\n    )\n    .args([\"--command-color\", \"red\"])\n    .stdout(\"\\x1b[1;31mfoo\\x1b[0m\\n\")\n    .success();\n}\n\n#[test]\nfn style_error() {\n  Test::new()\n    .justfile(\n      r#\"\n        foo:\n          @echo '{{ style(\"error\") }}foo{{NORMAL}}'\n      \"#,\n    )\n    .stdout(\"\\x1b[1;31mfoo\\x1b[0m\\n\")\n    .success();\n}\n\n#[test]\nfn style_warning() {\n  Test::new()\n    .justfile(\n      r#\"\n        foo:\n          @echo '{{ style(\"warning\") }}foo{{NORMAL}}'\n      \"#,\n    )\n    .stdout(\"\\x1b[1;33mfoo\\x1b[0m\\n\")\n    .success();\n}\n\n#[test]\nfn style_unknown() {\n  Test::new()\n    .justfile(\n      r#\"\n        foo:\n          @echo '{{ style(\"hippo\") }}foo{{NORMAL}}'\n      \"#,\n    )\n    .stderr(\n      r#\"\n        error: Call to function `style` failed: unknown style: `hippo`\n         ——▶ justfile:2:13\n          │\n        2 │   @echo '{{ style(\"hippo\") }}foo{{NORMAL}}'\n          │             ^^^^^\n      \"#,\n    )\n    .failure();\n}\n\n#[test]\nfn read() {\n  Test::new()\n    .justfile(\"foo := read('bar')\")\n    .write(\"bar\", \"baz\")\n    .args([\"--evaluate\", \"foo\"])\n    .stdout(\"baz\")\n    .success();\n}\n\n#[test]\nfn read_file_not_found() {\n  Test::new()\n    .justfile(\"foo := read('bar')\")\n    .args([\"--evaluate\", \"foo\"])\n    .stderr_regex(r\"error: Call to function `read` failed: I/O error reading `bar`: .*\")\n    .failure();\n}\n"
  },
  {
    "path": "tests/global.rs",
    "content": "use super::*;\n\n#[test]\nfn macos() {\n  if cfg!(not(target_os = \"macos\")) {\n    return;\n  }\n  let tempdir = tempdir();\n\n  let path = tempdir.path().to_owned();\n\n  Test::with_tempdir(tempdir)\n    .no_justfile()\n    .test_round_trip(false)\n    .write(\n      \"Library/Application Support/just/justfile\",\n      \"@default:\\n  echo foo\",\n    )\n    .env(\"HOME\", path.to_str().unwrap())\n    .args([\"--global-justfile\"])\n    .stdout(\"foo\\n\")\n    .success();\n}\n\n#[test]\nfn not_macos() {\n  if cfg!(any(not(unix), target_os = \"macos\")) {\n    return;\n  }\n  let tempdir = tempdir();\n\n  let path = tempdir.path().to_owned();\n\n  Test::with_tempdir(tempdir)\n    .no_justfile()\n    .test_round_trip(false)\n    .write(\"just/justfile\", \"@default:\\n  echo foo\")\n    .env(\"XDG_CONFIG_HOME\", path.to_str().unwrap())\n    .args([\"--global-justfile\"])\n    .stdout(\"foo\\n\")\n    .success();\n}\n\n#[test]\nfn unix() {\n  if cfg!(not(unix)) {\n    return;\n  }\n  let tempdir = tempdir();\n\n  let path = tempdir.path().to_owned();\n\n  let tempdir = Test::with_tempdir(tempdir)\n    .no_justfile()\n    .test_round_trip(false)\n    .write(\"justfile\", \"@default:\\n  echo foo\")\n    .env(\"HOME\", path.to_str().unwrap())\n    .args([\"--global-justfile\"])\n    .stdout(\"foo\\n\")\n    .success()\n    .tempdir;\n\n  Test::with_tempdir(tempdir)\n    .no_justfile()\n    .test_round_trip(false)\n    .write(\".config/just/justfile\", \"@default:\\n  echo bar\")\n    .env(\"HOME\", path.to_str().unwrap())\n    .args([\"--global-justfile\"])\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn case_insensitive() {\n  if cfg!(any(not(unix), target_os = \"macos\")) {\n    return;\n  }\n  let tempdir = tempdir();\n\n  let path = tempdir.path().to_owned();\n\n  Test::with_tempdir(tempdir)\n    .no_justfile()\n    .test_round_trip(false)\n    .write(\"just/JUSTFILE\", \"@default:\\n  echo foo\")\n    .env(\"XDG_CONFIG_HOME\", path.to_str().unwrap())\n    .args([\"--global-justfile\"])\n    .stdout(\"foo\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/groups.rs",
    "content": "use super::*;\n\n#[test]\nfn list_group_unknown() {\n  Test::new()\n    .justfile(\n      \"\n        [group('foo')]\n        a:\n      \",\n    )\n    .args([\"--list\", \"--group\", \"bar\"])\n    .stderr(\"error: Justfile does not contain group `bar`\\n\")\n    .failure();\n}\n\n#[test]\nfn list_group() {\n  Test::new()\n    .justfile(\n      \"\n        [group('alpha')]\n        a:\n        [group('alpha')]\n        [group('beta')]\n        b:\n        c:\n        [group('beta')]\n        d:\n      \",\n    )\n    .args([\"--list\", \"--group\", \"alpha\"])\n    .stdout(\n      \"\n        Available recipes:\n            [alpha]\n            a\n            b\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_multiple_groups() {\n  Test::new()\n    .justfile(\n      \"\n        [group('alpha')]\n        a:\n        [group('alpha')]\n        [group('beta')]\n        b:\n        c:\n        [group('beta')]\n        d:\n        [group('gamma')]\n        e:\n      \",\n    )\n    .args([\n      \"--list\", \"--group\", \"alpha\", \"--group\", \"beta\", \"--group\", \"gamma\",\n    ])\n    .stdout(\n      \"\n        Available recipes:\n            [alpha]\n            a\n            b\n\n            [beta]\n            b\n            d\n\n            [gamma]\n            e\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_with_groups() {\n  Test::new()\n    .justfile(\n      \"\n        [group('alpha')]\n        a:\n        # Doc comment\n        [group('alpha')]\n        [group('beta')]\n        b:\n        c:\n        [group('multi word group')]\n        d:\n        [group('alpha')]\n        e:\n        [group('beta')]\n        [group('alpha')]\n        f:\n      \",\n    )\n    .arg(\"--list\")\n    .stdout(\n      \"\n        Available recipes:\n            c\n\n            [alpha]\n            a\n            b # Doc comment\n            e\n            f\n\n            [beta]\n            b # Doc comment\n            f\n\n            [multi word group]\n            d\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_with_groups_unsorted() {\n  Test::new()\n    .justfile(\n      \"\n        [group('beta')]\n        [group('alpha')]\n        f:\n\n        [group('alpha')]\n        e:\n\n        [group('multi word group')]\n        d:\n\n        c:\n\n        # Doc comment\n        [group('alpha')]\n        [group('beta')]\n        b:\n\n        [group('alpha')]\n        a:\n\n      \",\n    )\n    .args([\"--list\", \"--unsorted\"])\n    .stdout(\n      \"\n        Available recipes:\n            c\n\n            [alpha]\n            f\n            e\n            b # Doc comment\n            a\n\n            [beta]\n            f\n            b # Doc comment\n\n            [multi word group]\n            d\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_with_groups_unsorted_group_order() {\n  Test::new()\n    .justfile(\n      \"\n        [group('y')]\n        [group('x')]\n        f:\n\n        [group('b')]\n        b:\n\n        [group('a')]\n        e:\n\n        c:\n      \",\n    )\n    .args([\"--list\", \"--unsorted\"])\n    .stdout(\n      \"\n        Available recipes:\n            c\n\n            [x]\n            f\n\n            [y]\n            f\n\n            [b]\n            b\n\n            [a]\n            e\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_groups() {\n  Test::new()\n    .justfile(\n      \"\n        [group('B')]\n        bar:\n\n        [group('A')]\n        [group('B')]\n        foo:\n\n      \",\n    )\n    .args([\"--groups\"])\n    .stdout(\n      \"\n      Recipe groups:\n          A\n          B\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_groups_with_custom_prefix() {\n  Test::new()\n    .justfile(\n      \"\n        [group('B')]\n        foo:\n\n        [group('A')]\n        [group('B')]\n        bar:\n      \",\n    )\n    .args([\"--groups\", \"--list-prefix\", \"...\"])\n    .stdout(\n      \"\n      Recipe groups:\n      ...A\n      ...B\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_groups_with_shorthand_syntax() {\n  Test::new()\n    .justfile(\n      \"\n        [group: 'B']\n        foo:\n\n        [group: 'A', group: 'B']\n        bar:\n      \",\n    )\n    .arg(\"--groups\")\n    .stdout(\n      \"\n      Recipe groups:\n          A\n          B\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_groups_unsorted() {\n  Test::new()\n    .justfile(\n      \"\n        [group: 'Z']\n        baz:\n\n        [group: 'B']\n        foo:\n\n        [group: 'A', group: 'B']\n        bar:\n      \",\n    )\n    .args([\"--groups\", \"--unsorted\"])\n    .stdout(\n      \"\n      Recipe groups:\n          Z\n          B\n          A\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_groups_private_unsorted() {\n  Test::new()\n    .justfile(\n      \"\n        [private]\n        [group: 'A']\n        foo:\n\n        [group: 'B']\n        bar:\n\n        [group: 'A']\n        baz:\n      \",\n    )\n    .args([\"--groups\", \"--unsorted\"])\n    .stdout(\n      \"\n      Recipe groups:\n          B\n          A\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_groups_private() {\n  Test::new()\n    .justfile(\n      \"\n        [private]\n        [group: 'A']\n        foo:\n\n        [group: 'B']\n        bar:\n      \",\n    )\n    .args([\"--groups\", \"--unsorted\"])\n    .stdout(\n      \"\n      Recipe groups:\n          B\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_group_with_submodules() {\n  Test::new()\n    .justfile(\n      \"\n        [group('foo')]\n        a:\n\n        b:\n\n        mod bar\n      \",\n    )\n    .write(\"bar.just\", \"c:\\nd:\")\n    .args([\"--list\", \"--group\", \"foo\", \"--list-submodules\"])\n    .stdout(\n      \"\n        Available recipes:\n            [foo]\n            a\n      \",\n    )\n    .success();\n}\n"
  },
  {
    "path": "tests/guards.rs",
    "content": "use super::*;\n\n#[test]\nfn guard_lines_halt_execution() {\n  Test::new()\n    .justfile(\n      \"\n        set guards\n\n        @foo:\n          ?[[ 'foo' == 'bar' ]]\n          echo baz\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn guard_lines_have_no_effect_if_successful() {\n  Test::new()\n    .justfile(\n      \"\n        set guards\n\n        @foo:\n          ?[[ 'foo' == 'foo' ]]\n          echo baz\n      \",\n    )\n    .stdout(\"baz\\n\")\n    .success();\n}\n\n#[test]\nfn exit_codes_above_one_are_reserved() {\n  Test::new()\n    .justfile(\n      \"\n        set guards\n\n        @foo:\n          ?exit 2\n      \",\n    )\n    .stderr(\"error: Guard line in recipe `foo` on line 4 returned reserved exit code 2\\n\")\n    .failure();\n}\n\n#[test]\nfn guard_sigil_may_not_be_used_with_infallible_sigil() {\n  Test::new()\n    .justfile(\n      \"\n        set guards\n\n        @foo:\n          -?exit 2\n      \",\n    )\n    .stderr(\n      \"\n        error: The guard `?` and infallible `-` sigils may not be used together\n         ——▶ justfile:4:3\n          │\n        4 │   -?exit 2\n          │   ^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn guard_lines_are_ignored_without_setting() {\n  Test::new()\n    .justfile(\n      \"\n        @foo:\n          ?() { echo bar; }; ?\n      \",\n    )\n    .stdout(\"bar\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/ignore_comments.rs",
    "content": "use super::*;\n\n#[test]\nfn ignore_comments_in_recipe() {\n  Test::new()\n    .justfile(\n      \"\n      set ignore-comments\n\n      some_recipe:\n        # A recipe-internal comment\n        echo something-useful\n    \",\n    )\n    .stdout(\"something-useful\\n\")\n    .stderr(\"echo something-useful\\n\")\n    .success();\n}\n\n#[test]\nfn dont_ignore_comments_in_recipe_by_default() {\n  Test::new()\n    .justfile(\n      \"\n      some_recipe:\n        # A recipe-internal comment\n        echo something-useful\n    \",\n    )\n    .stdout(\"something-useful\\n\")\n    .stderr(\"# A recipe-internal comment\\necho something-useful\\n\")\n    .success();\n}\n\n#[test]\nfn ignore_recipe_comments_with_shell_setting() {\n  Test::new()\n    .justfile(\n      \"\n      set shell := ['echo', '-n']\n      set ignore-comments\n\n      some_recipe:\n        # Alternate shells still ignore comments\n        echo something-useful\n    \",\n    )\n    .stdout(\"something-useful\\n\")\n    .stderr(\"echo something-useful\\n\")\n    .success();\n}\n\n#[test]\nfn continuations_with_echo_comments_false() {\n  Test::new()\n    .justfile(\n      \"\n      set ignore-comments\n\n      some_recipe:\n        # Comment lines ignore line continuations \\\\\n        echo something-useful\n    \",\n    )\n    .stdout(\"something-useful\\n\")\n    .stderr(\"echo something-useful\\n\")\n    .success();\n}\n\n#[test]\nfn continuations_with_echo_comments_true() {\n  Test::new()\n    .justfile(\n      \"\n      set ignore-comments := false\n\n      some_recipe:\n        # comment lines can be continued \\\\\n        echo something-useful\n    \",\n    )\n    .stderr(\"# comment lines can be continued echo something-useful\\n\")\n    .success();\n}\n\n#[test]\nfn dont_evaluate_comments() {\n  Test::new()\n    .justfile(\n      \"\n      set ignore-comments\n\n      some_recipe:\n        # {{ error('foo') }}\n    \",\n    )\n    .success();\n}\n\n#[test]\nfn dont_analyze_comments() {\n  Test::new()\n    .justfile(\n      \"\n      set ignore-comments\n\n      some_recipe:\n        # {{ bar }}\n    \",\n    )\n    .success();\n}\n\n#[test]\nfn comments_still_must_be_parsable_when_ignored() {\n  Test::new()\n    .justfile(\n      \"\n        set ignore-comments\n\n        some_recipe:\n          # {{ foo bar }}\n      \",\n    )\n    .stderr(\n      \"\n        error: Expected '&&', '||', '}}', '(', '+', or '/', but found identifier\n         ——▶ justfile:4:12\n          │\n        4 │   # {{ foo bar }}\n          │            ^^^\n      \",\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/imports.rs",
    "content": "use super::*;\n\n#[test]\nfn import_succeeds() {\n  Test::new()\n    .tree(tree! {\n      \"import.justfile\": \"\n        b:\n          @echo B\n      \",\n    })\n    .justfile(\n      \"\n        import './import.justfile'\n\n        a: b\n          @echo A\n      \",\n    )\n    .arg(\"a\")\n    .stdout(\"B\\nA\\n\")\n    .success();\n}\n\n#[test]\nfn missing_import_file_error() {\n  Test::new()\n    .justfile(\n      \"\n        import './import.justfile'\n\n        a:\n          @echo A\n      \",\n    )\n    .arg(\"a\")\n    .stderr(\n      \"\n      error: Could not find source file for import.\n       ——▶ justfile:1:8\n        │\n      1 │ import './import.justfile'\n        │        ^^^^^^^^^^^^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn missing_optional_imports_are_ignored() {\n  Test::new()\n    .justfile(\n      \"\n        import? './import.justfile'\n\n        a:\n          @echo A\n      \",\n    )\n    .arg(\"a\")\n    .stdout(\"A\\n\")\n    .success();\n}\n\n#[test]\nfn trailing_spaces_after_import_are_ignored() {\n  Test::new()\n    .tree(tree! {\n      \"import.justfile\": \"\",\n    })\n    .justfile(\n      \"\n      import './import.justfile'\\x20\n      a:\n        @echo A\n    \",\n    )\n    .stdout(\"A\\n\")\n    .success();\n}\n\n#[test]\nfn import_after_recipe() {\n  Test::new()\n    .tree(tree! {\n      \"import.justfile\": \"\n        a:\n          @echo A\n      \",\n    })\n    .justfile(\n      \"\n      b: a\n      import './import.justfile'\n      \",\n    )\n    .stdout(\"A\\n\")\n    .success();\n}\n\n#[test]\nfn circular_import() {\n  Test::new()\n    .justfile(\"import 'a'\")\n    .tree(tree! {\n      a: \"import 'b'\",\n      b: \"import 'a'\",\n    })\n    .stderr_regex(path_for_regex(\n      \"error: Import `.*/a` in `.*/b` is circular\\n\",\n    ))\n    .failure();\n}\n\n#[test]\nfn import_recipes_are_not_default() {\n  Test::new()\n    .tree(tree! {\n      \"import.justfile\": \"bar:\",\n    })\n    .justfile(\"import './import.justfile'\")\n    .stderr(\"error: Justfile contains no default recipe.\\n\")\n    .failure();\n}\n\n#[test]\nfn listed_recipes_in_imports_are_in_load_order() {\n  Test::new()\n    .justfile(\n      \"\n      import './import.justfile'\n      foo:\n    \",\n    )\n    .write(\"import.justfile\", \"bar:\")\n    .args([\"--list\", \"--unsorted\"])\n    .stdout(\n      \"\n      Available recipes:\n          foo\n          bar\n    \",\n    )\n    .success();\n}\n\n#[test]\nfn include_error() {\n  Test::new()\n    .justfile(\"!include foo\")\n    .stderr(\n      \"\n      error: The `!include` directive has been stabilized as `import`\n       ——▶ justfile:1:1\n        │\n      1 │ !include foo\n        │ ^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn recipes_in_import_are_overridden_by_recipes_in_parent() {\n  Test::new()\n    .tree(tree! {\n      \"import.justfile\": \"\n        a:\n          @echo IMPORT\n      \",\n    })\n    .justfile(\n      \"\n        a:\n          @echo ROOT\n\n        import './import.justfile'\n\n        set allow-duplicate-recipes\n      \",\n    )\n    .arg(\"a\")\n    .stdout(\"ROOT\\n\")\n    .success();\n}\n\n#[test]\nfn variables_in_import_are_overridden_by_variables_in_parent() {\n  Test::new()\n    .tree(tree! {\n      \"import.justfile\": \"\n    f := 'foo'\n    \",\n    })\n    .justfile(\n      \"\n      f := 'bar'\n\n      import './import.justfile'\n\n      set allow-duplicate-variables\n\n      a:\n        @echo {{f}}\n    \",\n    )\n    .arg(\"a\")\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn import_paths_beginning_with_tilde_are_expanded_to_homdir() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .write(\"foobar/mod.just\", \"foo:\\n @echo FOOBAR\")\n    .justfile(\n      \"\n        import '~/mod.just'\n      \",\n    )\n    .arg(\"foo\")\n    .stdout(\"FOOBAR\\n\")\n    .env(\"HOME\", \"foobar\")\n    .success();\n}\n\n#[test]\nfn imports_dump_correctly() {\n  Test::new()\n    .write(\"import.justfile\", \"\")\n    .justfile(\n      \"\n        import './import.justfile'\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\"import './import.justfile'\\n\")\n    .success();\n}\n\n#[test]\nfn optional_imports_dump_correctly() {\n  Test::new()\n    .write(\"import.justfile\", \"\")\n    .justfile(\n      \"\n        import? './import.justfile'\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\"import? './import.justfile'\\n\")\n    .success();\n}\n\n#[test]\nfn imports_in_root_run_in_justfile_directory() {\n  Test::new()\n    .write(\"foo/import.justfile\", \"bar:\\n @cat baz\")\n    .write(\"baz\", \"BAZ\")\n    .justfile(\n      \"\n        import 'foo/import.justfile'\n      \",\n    )\n    .arg(\"bar\")\n    .stdout(\"BAZ\")\n    .success();\n}\n\n#[test]\nfn imports_in_submodules_run_in_submodule_directory() {\n  Test::new()\n    .justfile(\"mod foo\")\n    .write(\"foo/mod.just\", \"import 'import.just'\")\n    .write(\"foo/import.just\", \"bar:\\n @cat baz\")\n    .write(\"foo/baz\", \"BAZ\")\n    .arg(\"foo\")\n    .arg(\"bar\")\n    .stdout(\"BAZ\")\n    .success();\n}\n\n#[test]\nfn nested_import_paths_are_relative_to_containing_submodule() {\n  Test::new()\n    .justfile(\"import 'foo/import.just'\")\n    .write(\"foo/import.just\", \"import 'bar.just'\")\n    .write(\"foo/bar.just\", \"bar:\\n @echo BAR\")\n    .arg(\"bar\")\n    .stdout(\"BAR\\n\")\n    .success();\n}\n\n#[test]\nfn recipes_in_nested_imports_run_in_parent_module() {\n  Test::new()\n    .justfile(\"import 'foo/import.just'\")\n    .write(\"foo/import.just\", \"import 'bar/import.just'\")\n    .write(\"foo/bar/import.just\", \"bar:\\n @cat baz\")\n    .write(\"baz\", \"BAZ\")\n    .arg(\"bar\")\n    .stdout(\"BAZ\")\n    .success();\n}\n\n#[test]\nfn shebang_recipes_in_imports_in_root_run_in_justfile_directory() {\n  Test::new()\n    .write(\n      \"foo/import.justfile\",\n      \"bar:\\n #!/usr/bin/env bash\\n cat baz\",\n    )\n    .write(\"baz\", \"BAZ\")\n    .justfile(\n      \"\n        import 'foo/import.justfile'\n      \",\n    )\n    .arg(\"bar\")\n    .stdout(\"BAZ\")\n    .success();\n}\n\n#[test]\nfn recipes_imported_in_root_run_in_command_line_provided_working_directory() {\n  Test::new()\n    .write(\"subdir/b.justfile\", \"@b:\\n  cat baz\")\n    .write(\"subdir/a.justfile\", \"import 'b.justfile'\\n@a: b\\n  cat baz\")\n    .write(\"baz\", \"BAZ\")\n    .args([\n      \"--working-directory\",\n      \".\",\n      \"--justfile\",\n      \"subdir/a.justfile\",\n    ])\n    .stdout(\"BAZBAZ\")\n    .success();\n}\n\n#[test]\nfn reused_import_are_allowed() {\n  Test::new()\n    .justfile(\n      \"\n      import 'a'\n      import 'b'\n\n      bar:\n    \",\n    )\n    .tree(tree! {\n      a: \"import 'c'\",\n      b: \"import 'c'\",\n      c: \"\",\n    })\n    .success();\n}\n\n#[test]\nfn multiply_imported_items_do_not_conflict() {\n  Test::new()\n    .justfile(\n      \"\n      import 'a.just'\n      import 'a.just'\n      foo: bar\n    \",\n    )\n    .write(\n      \"a.just\",\n      \"\nx := 'y'\n\n@bar:\n  echo hello\n\",\n    )\n    .stdout(\"hello\\n\")\n    .success();\n}\n\n#[test]\nfn nested_multiply_imported_items_do_not_conflict() {\n  Test::new()\n    .justfile(\n      \"\n      import 'a.just'\n      import 'b.just'\n      foo: bar\n    \",\n    )\n    .write(\"a.just\", \"import 'c.just'\")\n    .write(\"b.just\", \"import 'c.just'\")\n    .write(\n      \"c.just\",\n      \"\nx := 'y'\n\n@bar:\n  echo hello\n\",\n    )\n    .stdout(\"hello\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/init.rs",
    "content": "use {super::*, just::INIT_JUSTFILE};\n\n#[test]\nfn current_dir() {\n  let tmp = tempdir();\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--init\")\n    .output()\n    .unwrap();\n\n  assert!(output.status.success());\n\n  assert_eq!(\n    fs::read_to_string(tmp.path().join(\"justfile\")).unwrap(),\n    INIT_JUSTFILE\n  );\n}\n\n#[test]\nfn exists() {\n  let output = Test::new()\n    .no_justfile()\n    .arg(\"--init\")\n    .stderr_regex(\"Wrote justfile to `.*`\\n\")\n    .success();\n\n  Test::with_tempdir(output.tempdir)\n    .no_justfile()\n    .arg(\"--init\")\n    .stderr_regex(\"error: Justfile `.*` already exists\\n\")\n    .failure();\n}\n\n#[test]\nfn write_error() {\n  let test = Test::new();\n\n  let justfile_path = test.justfile_path();\n\n  fs::create_dir(justfile_path).unwrap();\n\n  test\n    .no_justfile()\n    .args([\"--init\"])\n    .stderr_regex(if cfg!(windows) {\n      r\"error: Failed to write justfile to `.*`: Access is denied. \\(os error 5\\)\\n\"\n    } else {\n      r\"error: Failed to write justfile to `.*`: Is a directory \\(os error 21\\)\\n\"\n    })\n    .failure();\n}\n\n#[test]\nfn invocation_directory() {\n  let tmp = temptree! {\n    \".git\": {},\n  };\n\n  let test = Test::with_tempdir(tmp);\n\n  let justfile_path = test.justfile_path();\n\n  let _tmp = test\n    .no_justfile()\n    .stderr_regex(\"Wrote justfile to `.*`\\n\")\n    .arg(\"--init\")\n    .success();\n\n  assert_eq!(fs::read_to_string(justfile_path).unwrap(), INIT_JUSTFILE);\n}\n\n#[test]\nfn parent_dir() {\n  let tmp = temptree! {\n    \".git\": {},\n    sub: {},\n  };\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path().join(\"sub\"))\n    .arg(\"--init\")\n    .output()\n    .unwrap();\n\n  assert!(output.status.success());\n\n  assert_eq!(\n    fs::read_to_string(tmp.path().join(\"justfile\")).unwrap(),\n    INIT_JUSTFILE\n  );\n}\n\n#[test]\nfn alternate_marker() {\n  let tmp = temptree! {\n    \"_darcs\": {},\n  };\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--init\")\n    .output()\n    .unwrap();\n\n  assert!(output.status.success());\n\n  assert_eq!(\n    fs::read_to_string(tmp.path().join(\"justfile\")).unwrap(),\n    INIT_JUSTFILE\n  );\n}\n\n#[test]\nfn search_directory() {\n  let tmp = temptree! {\n    sub: {\n      \".git\": {},\n    },\n  };\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--init\")\n    .arg(\"sub/\")\n    .output()\n    .unwrap();\n\n  assert!(output.status.success());\n\n  assert_eq!(\n    fs::read_to_string(tmp.path().join(\"sub/justfile\")).unwrap(),\n    INIT_JUSTFILE\n  );\n}\n\n#[test]\nfn justfile() {\n  let tmp = temptree! {\n    sub: {\n      \".git\": {},\n    },\n  };\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path().join(\"sub\"))\n    .arg(\"--init\")\n    .arg(\"--justfile\")\n    .arg(tmp.path().join(\"justfile\"))\n    .output()\n    .unwrap();\n\n  assert!(output.status.success());\n\n  assert_eq!(\n    fs::read_to_string(tmp.path().join(\"justfile\")).unwrap(),\n    INIT_JUSTFILE\n  );\n}\n\n#[test]\nfn justfile_and_working_directory() {\n  let tmp = temptree! {\n    sub: {\n      \".git\": {},\n    },\n  };\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path().join(\"sub\"))\n    .arg(\"--init\")\n    .arg(\"--justfile\")\n    .arg(tmp.path().join(\"justfile\"))\n    .arg(\"--working-directory\")\n    .arg(\"/\")\n    .output()\n    .unwrap();\n\n  assert!(output.status.success());\n\n  assert_eq!(\n    fs::read_to_string(tmp.path().join(\"justfile\")).unwrap(),\n    INIT_JUSTFILE\n  );\n}\n\n#[test]\nfn fmt_compatibility() {\n  let output = Test::new()\n    .no_justfile()\n    .arg(\"--init\")\n    .stderr_regex(\"Wrote justfile to `.*`\\n\")\n    .success();\n  Test::with_tempdir(output.tempdir)\n    .no_justfile()\n    .arg(\"--unstable\")\n    .arg(\"--check\")\n    .arg(\"--fmt\")\n    .success();\n}\n"
  },
  {
    "path": "tests/interpolation.rs",
    "content": "use super::*;\n\n#[test]\nfn closing_curly_brace_can_abut_interpolation_close() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          echo {{if 'a' == 'b' { 'c' } else { 'd' }}}\n      \",\n    )\n    .stderr(\"echo d\\n\")\n    .stdout(\"d\\n\")\n    .success();\n}\n\n#[test]\nfn eol_with_continuation_in_interpolation() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          echo {{(\n            'a'\n          )}}\n      \",\n    )\n    .stderr(\"echo a\\n\")\n    .stdout(\"a\\n\")\n    .success();\n}\n\n#[test]\nfn eol_without_continuation_in_interpolation() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          echo {{\n            'a'\n          }}\n      \",\n    )\n    .stderr(\"echo a\\n\")\n    .stdout(\"a\\n\")\n    .success();\n}\n\n#[test]\nfn comment_in_interopolation() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          echo {{ # hello\n            'a'\n          }}\n      \",\n    )\n    .stderr(\n      \"\n        error: Expected backtick, identifier, '(', '/', or string, but found comment\n         ——▶ justfile:2:11\n          │\n        2 │   echo {{ # hello\n          │           ^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn indent_and_dedent_are_ignored_in_interpolation() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          echo {{\n            'a'\n        + 'b'\n               + 'c'\n          }}\n          echo foo\n      \",\n    )\n    .stderr(\"echo abc\\necho foo\\n\")\n    .stdout(\"abc\\nfoo\\n\")\n    .success();\n}\n\n#[test]\nfn shebang_line_numbers_are_correct_with_multi_line_interpolations() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          #!/usr/bin/env cat\n          echo {{\n            'a'\n        + 'b'\n               + 'c'\n          }}\n          echo foo\n      \",\n    )\n    .stdout(if cfg!(windows) {\n      \"\n\n\n        echo abc\n\n\n\n\n        echo foo\n      \"\n    } else {\n      \"\n        #!/usr/bin/env cat\n\n        echo abc\n\n\n\n\n        echo foo\n      \"\n    })\n    .success();\n}\n"
  },
  {
    "path": "tests/invocation_directory.rs",
    "content": "use super::*;\n\n#[cfg(unix)]\nfn convert_native_path(path: &Path) -> String {\n  fs::canonicalize(path)\n    .expect(\"canonicalize failed\")\n    .to_str()\n    .map(str::to_string)\n    .expect(\"unicode decode failed\")\n}\n\n#[cfg(windows)]\nfn convert_native_path(path: &Path) -> String {\n  // Translate path from windows style to unix style\n  let mut cygpath = Command::new(env::var_os(\"JUST_CYGPATH\").unwrap_or(\"cygpath\".into()));\n  cygpath.arg(\"--unix\");\n  cygpath.arg(path);\n\n  let output = cygpath.output().expect(\"executing cygpath failed\");\n\n  assert!(output.status.success());\n\n  let stdout = str::from_utf8(&output.stdout).expect(\"cygpath output was not utf8\");\n\n  if stdout.ends_with('\\n') {\n    &stdout[0..stdout.len() - 1]\n  } else if stdout.ends_with(\"\\r\\n\") {\n    &stdout[0..stdout.len() - 2]\n  } else {\n    stdout\n  }\n  .to_owned()\n}\n\n#[test]\nfn test_invocation_directory() {\n  let tmp = tempdir();\n\n  let mut justfile_path = tmp.path().to_path_buf();\n  justfile_path.push(\"justfile\");\n  fs::write(\n    justfile_path,\n    \"default:\\n @cd {{invocation_directory()}}\\n @echo {{invocation_directory()}}\",\n  )\n  .unwrap();\n\n  let mut subdir = tmp.path().to_path_buf();\n  subdir.push(\"subdir\");\n  fs::create_dir(&subdir).unwrap();\n\n  let output = Command::new(JUST)\n    .current_dir(&subdir)\n    .args([\"--shell\", \"sh\"])\n    .output()\n    .expect(\"just invocation failed\");\n\n  let mut failure = false;\n\n  let expected_status = 0;\n  let expected_stdout = convert_native_path(&subdir) + \"\\n\";\n  let expected_stderr = \"\";\n\n  let status = output.status.code().unwrap();\n  if status != expected_status {\n    println!(\"bad status: {status} != {expected_status}\");\n    failure = true;\n  }\n\n  let stdout = str::from_utf8(&output.stdout).unwrap();\n  if stdout != expected_stdout {\n    println!(\"bad stdout:\\ngot:\\n{stdout:?}\\n\\nexpected:\\n{expected_stdout:?}\");\n    failure = true;\n  }\n\n  let stderr = str::from_utf8(&output.stderr).unwrap();\n  if stderr != expected_stderr {\n    println!(\"bad stderr:\\ngot:\\n{stderr:?}\\n\\nexpected:\\n{expected_stderr:?}\");\n    failure = true;\n  }\n\n  assert!(!failure, \"test failed\");\n}\n\n#[test]\nfn invocation_directory_native() {\n  let Output {\n    stdout, tempdir, ..\n  } = Test::new()\n    .justfile(\"x := invocation_directory_native()\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout_regex(\".*\")\n    .success();\n\n  if cfg!(windows) {\n    assert_eq!(Path::new(&stdout), tempdir.path());\n  } else {\n    assert_eq!(Path::new(&stdout), tempdir.path().canonicalize().unwrap());\n  }\n}\n"
  },
  {
    "path": "tests/json.rs",
    "content": "use super::*;\n\n#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]\n#[serde(deny_unknown_fields)]\nstruct Alias<'a> {\n  attributes: Vec<&'a str>,\n  name: &'a str,\n  target: &'a str,\n}\n\n#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]\n#[serde(deny_unknown_fields)]\nstruct Assignment<'a> {\n  eager: bool,\n  export: bool,\n  name: &'a str,\n  private: bool,\n  value: serde_json::Value,\n}\n\n#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]\n#[serde(deny_unknown_fields)]\nstruct Dependency<'a> {\n  arguments: Vec<Value>,\n  recipe: &'a str,\n}\n\n#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]\n#[serde(deny_unknown_fields)]\nstruct Interpreter<'a> {\n  arguments: Vec<&'a str>,\n  command: &'a str,\n}\n\n#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]\n#[serde(deny_unknown_fields)]\nstruct Module<'a> {\n  aliases: BTreeMap<&'a str, Alias<'a>>,\n  assignments: BTreeMap<&'a str, Assignment<'a>>,\n  doc: Option<&'a str>,\n  first: Option<&'a str>,\n  groups: Vec<&'a str>,\n  modules: BTreeMap<&'a str, Module<'a>>,\n  recipes: BTreeMap<&'a str, Recipe<'a>>,\n  settings: Settings<'a>,\n  source: PathBuf,\n  unexports: Vec<&'a str>,\n  warnings: Vec<&'a str>,\n}\n\n#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]\n#[serde(deny_unknown_fields)]\nstruct Parameter<'a> {\n  default: Option<&'a str>,\n  export: bool,\n  help: Option<&'a str>,\n  kind: &'a str,\n  long: Option<&'a str>,\n  name: &'a str,\n  pattern: Option<&'a str>,\n  short: Option<char>,\n  value: Option<&'a str>,\n}\n\n#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]\n#[serde(deny_unknown_fields)]\nstruct Recipe<'a> {\n  attributes: Vec<Value>,\n  body: Vec<Value>,\n  dependencies: Vec<Dependency<'a>>,\n  doc: Option<&'a str>,\n  name: &'a str,\n  namepath: &'a str,\n  parameters: Vec<Parameter<'a>>,\n  priors: u32,\n  private: bool,\n  quiet: bool,\n  shebang: bool,\n}\n\n#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]\n#[serde(deny_unknown_fields)]\nstruct Settings<'a> {\n  allow_duplicate_recipes: bool,\n  allow_duplicate_variables: bool,\n  dotenv_filename: Option<&'a str>,\n  dotenv_load: bool,\n  dotenv_override: bool,\n  dotenv_path: Option<&'a str>,\n  dotenv_required: bool,\n  export: bool,\n  fallback: bool,\n  guards: bool,\n  ignore_comments: bool,\n  lazy: bool,\n  no_exit_message: bool,\n  positional_arguments: bool,\n  quiet: bool,\n  shell: Option<Interpreter<'a>>,\n  tempdir: Option<&'a str>,\n  unstable: bool,\n  windows_powershell: bool,\n  windows_shell: Option<&'a str>,\n  working_directory: Option<&'a str>,\n}\n\n#[track_caller]\nfn case(justfile: &str, expected: Module) {\n  case_with_submodule(justfile, None, expected);\n}\n\nfn fix_source(dir: &Path, module: &mut Module) {\n  let filename = if module.source.as_os_str().is_empty() {\n    Path::new(\"justfile\")\n  } else {\n    &module.source\n  };\n\n  module.source = if cfg!(target_os = \"macos\") {\n    dir.canonicalize().unwrap().join(filename)\n  } else {\n    dir.join(filename)\n  };\n\n  for module in module.modules.values_mut() {\n    fix_source(dir, module);\n  }\n}\n\n#[track_caller]\nfn case_with_submodule(justfile: &str, submodule: Option<(&str, &str)>, mut expected: Module) {\n  let mut test = Test::new()\n    .justfile(justfile)\n    .args([\"--dump\", \"--dump-format\", \"json\"])\n    .stdout_regex(\".*\");\n\n  if let Some((path, source)) = submodule {\n    test = test.write(path, source);\n  }\n\n  fix_source(test.tempdir.path(), &mut expected);\n\n  let actual = test.success().stdout;\n\n  let actual: Module = serde_json::from_str(actual.as_str()).unwrap();\n  pretty_assertions::assert_eq!(actual, expected);\n}\n\n#[test]\nfn alias() {\n  case(\n    \"\n      alias f := foo\n\n      foo:\n    \",\n    Module {\n      aliases: [(\n        \"f\",\n        Alias {\n          name: \"f\",\n          target: \"foo\",\n          ..default()\n        },\n      )]\n      .into(),\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          name: \"foo\",\n          namepath: \"foo\",\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn assignment() {\n  case(\n    \"foo := 'bar'\",\n    Module {\n      assignments: [(\n        \"foo\",\n        Assignment {\n          name: \"foo\",\n          value: \"bar\".into(),\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn private_assignment() {\n  case(\n    \"\n      _foo := 'foo'\n      [private]\n      bar := 'bar'\n    \",\n    Module {\n      assignments: [\n        (\n          \"_foo\",\n          Assignment {\n            name: \"_foo\",\n            value: \"foo\".into(),\n            private: true,\n            ..default()\n          },\n        ),\n        (\n          \"bar\",\n          Assignment {\n            name: \"bar\",\n            value: \"bar\".into(),\n            private: true,\n            ..default()\n          },\n        ),\n      ]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn body() {\n  case(\n    \"\n      foo:\n        bar\n        abc{{ 'xyz' }}def\n    \",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          name: \"foo\",\n          namepath: \"foo\",\n          body: [json!([\"bar\"]), json!([\"abc\", [\"xyz\"], \"def\"])].into(),\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn dependencies() {\n  case(\n    \"\n      foo:\n      bar: foo\n    \",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [\n        (\n          \"foo\",\n          Recipe {\n            name: \"foo\",\n            namepath: \"foo\",\n            ..default()\n          },\n        ),\n        (\n          \"bar\",\n          Recipe {\n            name: \"bar\",\n            namepath: \"bar\",\n            dependencies: [Dependency {\n              recipe: \"foo\",\n              ..default()\n            }]\n            .into(),\n            priors: 1,\n            ..default()\n          },\n        ),\n      ]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn dependency_argument() {\n  case(\n    \"\n      x := 'foo'\n      foo *args:\n      bar: (\n        foo\n        'baz'\n        ('baz')\n        ('a' + 'b')\n        `echo`\n        x\n        if 'a' == 'b' { 'c' } else { 'd' }\n        arch()\n        env_var('foo')\n        join('a', 'b')\n        replace('a', 'b', 'c')\n      )\n    \",\n    Module {\n      assignments: [(\n        \"x\",\n        Assignment {\n          name: \"x\",\n          value: \"foo\".into(),\n          ..default()\n        },\n      )]\n      .into(),\n      first: Some(\"foo\"),\n      recipes: [\n        (\n          \"foo\",\n          Recipe {\n            name: \"foo\",\n            namepath: \"foo\",\n            parameters: [Parameter {\n              kind: \"star\",\n              name: \"args\",\n              ..default()\n            }]\n            .into(),\n            ..default()\n          },\n        ),\n        (\n          \"bar\",\n          Recipe {\n            name: \"bar\",\n            namepath: \"bar\",\n            dependencies: [Dependency {\n              recipe: \"foo\",\n              arguments: [\n                json!(\"baz\"),\n                json!(\"baz\"),\n                json!([\"concatenate\", \"a\", \"b\"]),\n                json!([\"evaluate\", \"echo\"]),\n                json!([\"variable\", \"x\"]),\n                json!([\"if\", [\"==\", \"a\", \"b\"], \"c\", \"d\"]),\n                json!([\"call\", \"arch\"]),\n                json!([\"call\", \"env_var\", \"foo\"]),\n                json!([\"call\", \"join\", \"a\", \"b\"]),\n                json!([\"call\", \"replace\", \"a\", \"b\", \"c\"]),\n              ]\n              .into(),\n            }]\n            .into(),\n            priors: 1,\n            ..default()\n          },\n        ),\n      ]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn duplicate_recipes() {\n  case(\n    \"\n      set allow-duplicate-recipes\n      alias f := foo\n\n      foo:\n      foo bar:\n    \",\n    Module {\n      aliases: [(\n        \"f\",\n        Alias {\n          name: \"f\",\n          target: \"foo\",\n          ..default()\n        },\n      )]\n      .into(),\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          name: \"foo\",\n          namepath: \"foo\",\n          parameters: [Parameter {\n            kind: \"singular\",\n            name: \"bar\",\n            ..default()\n          }]\n          .into(),\n          ..default()\n        },\n      )]\n      .into(),\n      settings: Settings {\n        allow_duplicate_recipes: true,\n        ..default()\n      },\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn duplicate_variables() {\n  case(\n    \"\n      set allow-duplicate-variables\n      x := 'foo'\n      x := 'bar'\n    \",\n    Module {\n      assignments: [(\n        \"x\",\n        Assignment {\n          name: \"x\",\n          value: \"bar\".into(),\n          ..default()\n        },\n      )]\n      .into(),\n      settings: Settings {\n        allow_duplicate_variables: true,\n        ..default()\n      },\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn doc_comment() {\n  case(\n    \"# hello\\nfoo:\",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          doc: Some(\"hello\"),\n          name: \"foo\",\n          namepath: \"foo\",\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn empty_justfile() {\n  case(\"\", Module::default());\n}\n\n#[test]\nfn parameters() {\n  case(\n    \"\n      a:\n      b x:\n      c x='y':\n      d +x:\n      e *x:\n      f $x:\n    \",\n    Module {\n      first: Some(\"a\"),\n      recipes: [\n        (\n          \"a\",\n          Recipe {\n            name: \"a\",\n            namepath: \"a\",\n            ..default()\n          },\n        ),\n        (\n          \"b\",\n          Recipe {\n            name: \"b\",\n            namepath: \"b\",\n            parameters: [Parameter {\n              kind: \"singular\",\n              name: \"x\",\n              ..default()\n            }]\n            .into(),\n            ..default()\n          },\n        ),\n        (\n          \"c\",\n          Recipe {\n            name: \"c\",\n            namepath: \"c\",\n            parameters: [Parameter {\n              default: Some(\"y\"),\n              kind: \"singular\",\n              name: \"x\",\n              ..default()\n            }]\n            .into(),\n            ..default()\n          },\n        ),\n        (\n          \"d\",\n          Recipe {\n            name: \"d\",\n            namepath: \"d\",\n            parameters: [Parameter {\n              kind: \"plus\",\n              name: \"x\",\n              ..default()\n            }]\n            .into(),\n            ..default()\n          },\n        ),\n        (\n          \"e\",\n          Recipe {\n            name: \"e\",\n            namepath: \"e\",\n            parameters: [Parameter {\n              kind: \"star\",\n              name: \"x\",\n              ..default()\n            }]\n            .into(),\n            ..default()\n          },\n        ),\n        (\n          \"f\",\n          Recipe {\n            name: \"f\",\n            namepath: \"f\",\n            parameters: [Parameter {\n              export: true,\n              kind: \"singular\",\n              name: \"x\",\n              ..default()\n            }]\n            .into(),\n            ..default()\n          },\n        ),\n      ]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn priors() {\n  case(\n    \"\n      a:\n      b: a && c\n      c:\n    \",\n    Module {\n      first: Some(\"a\"),\n      recipes: [\n        (\n          \"a\",\n          Recipe {\n            name: \"a\",\n            namepath: \"a\",\n            ..default()\n          },\n        ),\n        (\n          \"b\",\n          Recipe {\n            dependencies: [\n              Dependency {\n                recipe: \"a\",\n                ..default()\n              },\n              Dependency {\n                recipe: \"c\",\n                ..default()\n              },\n            ]\n            .into(),\n            name: \"b\",\n            namepath: \"b\",\n            priors: 1,\n            ..default()\n          },\n        ),\n        (\n          \"c\",\n          Recipe {\n            name: \"c\",\n            namepath: \"c\",\n            ..default()\n          },\n        ),\n      ]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn private() {\n  case(\n    \"_foo:\",\n    Module {\n      first: Some(\"_foo\"),\n      recipes: [(\n        \"_foo\",\n        Recipe {\n          name: \"_foo\",\n          namepath: \"_foo\",\n          private: true,\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn quiet() {\n  case(\n    \"@foo:\",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          name: \"foo\",\n          namepath: \"foo\",\n          quiet: true,\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn settings() {\n  case(\n    \"\n      set allow-duplicate-recipes\n      set dotenv-filename := \\\"filename\\\"\n      set dotenv-load\n      set dotenv-path := \\\"path\\\"\n      set export\n      set fallback\n      set ignore-comments\n      set positional-arguments\n      set quiet\n      set shell := ['a', 'b', 'c']\n      foo:\n        #!bar\n    \",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          name: \"foo\",\n          namepath: \"foo\",\n          shebang: true,\n          body: [json!([\"#!bar\"])].into(),\n          ..default()\n        },\n      )]\n      .into(),\n      settings: Settings {\n        allow_duplicate_recipes: true,\n        dotenv_filename: Some(\"filename\"),\n        dotenv_path: Some(\"path\"),\n        dotenv_load: true,\n        export: true,\n        fallback: true,\n        ignore_comments: true,\n        positional_arguments: true,\n        quiet: true,\n        shell: Some(Interpreter {\n          arguments: [\"b\", \"c\"].into(),\n          command: \"a\",\n        }),\n        ..default()\n      },\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn shebang() {\n  case(\n    \"\n      foo:\n        #!bar\n    \",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          name: \"foo\",\n          namepath: \"foo\",\n          shebang: true,\n          body: [json!([\"#!bar\"])].into(),\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn simple() {\n  case(\n    \"foo:\",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          name: \"foo\",\n          namepath: \"foo\",\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn attribute() {\n  case(\n    \"\n      [no-exit-message]\n      foo:\n    \",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          attributes: [json!(\"no-exit-message\")].into(),\n          name: \"foo\",\n          namepath: \"foo\",\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn single_metadata_attribute() {\n  case(\n    \"\n      [metadata('example')]\n      foo:\n    \",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          attributes: [json!({\"metadata\": [\"example\"]})].into(),\n          name: \"foo\",\n          namepath: \"foo\",\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn multiple_metadata_attributes() {\n  case(\n    \"\n      [metadata('example')]\n      [metadata('sample')]\n      foo:\n    \",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          attributes: [\n            json!({\"metadata\": [\"example\"]}),\n            json!({\"metadata\": [\"sample\"]}),\n          ]\n          .into(),\n          name: \"foo\",\n          namepath: \"foo\",\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn multiple_metadata_attributes_with_multiple_arguments() {\n  case(\n    \"\n      [metadata('example', 'arg1')]\n      [metadata('sample', 'argument')]\n      foo:\n    \",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          attributes: [\n            json!({\"metadata\": [\"example\", \"arg1\"]}),\n            json!({\"metadata\": [\"sample\", \"argument\"]}),\n          ]\n          .into(),\n          name: \"foo\",\n          namepath: \"foo\",\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn module() {\n  case_with_submodule(\n    \"\n      # hello\n      mod foo\n    \",\n    Some((\"foo.just\", \"bar:\")),\n    Module {\n      modules: [(\n        \"foo\",\n        Module {\n          doc: Some(\"hello\"),\n          first: Some(\"bar\"),\n          source: \"foo.just\".into(),\n          recipes: [(\n            \"bar\",\n            Recipe {\n              name: \"bar\",\n              namepath: \"foo::bar\",\n              ..default()\n            },\n          )]\n          .into(),\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn module_group() {\n  case_with_submodule(\n    \"\n      [group('alpha')]\n      mod foo\n    \",\n    Some((\"foo.just\", \"bar:\")),\n    Module {\n      modules: [(\n        \"foo\",\n        Module {\n          first: Some(\"bar\"),\n          groups: [\"alpha\"].into(),\n          source: \"foo.just\".into(),\n          recipes: [(\n            \"bar\",\n            Recipe {\n              name: \"bar\",\n              namepath: \"foo::bar\",\n              ..default()\n            },\n          )]\n          .into(),\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn recipes_with_private_attribute_are_private() {\n  case(\n    \"\n      [private]\n      foo:\n    \",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          attributes: [json!(\"private\")].into(),\n          name: \"foo\",\n          namepath: \"foo\",\n          private: true,\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn doc_attribute_overrides_comment() {\n  case(\n    \"\n      # COMMENT\n      [doc('ATTRIBUTE')]\n      foo:\n    \",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          attributes: [json!({\"doc\": \"ATTRIBUTE\"})].into(),\n          doc: Some(\"ATTRIBUTE\"),\n          name: \"foo\",\n          namepath: \"foo\",\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn format_string() {\n  case(\n    \"\n      foo := f'abc'\n    \",\n    Module {\n      assignments: [(\n        \"foo\",\n        Assignment {\n          name: \"foo\",\n          value: json!([\"format\", \"abc\"]),\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n  case(\n    \"\n      foo := f'abc{{'bar'}}xyz'\n    \",\n    Module {\n      assignments: [(\n        \"foo\",\n        Assignment {\n          name: \"foo\",\n          value: json!([\"format\", \"abc\", \"bar\", \"xyz\"]),\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n  case(\n    \"\n      foo := f'abc{{'bar'}}xyz{{'baz' + 'buzz'}}123'\n    \",\n    Module {\n      assignments: [(\n        \"foo\",\n        Assignment {\n          name: \"foo\",\n          value: json!([\n            \"format\",\n            \"abc\",\n            \"bar\",\n            \"xyz\",\n            [\"concatenate\", \"baz\", \"buzz\"],\n            \"123\"\n          ]),\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn arg_pattern() {\n  case(\n    \"[arg('bar', pattern='BAR')]\\nfoo bar:\",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          name: \"foo\",\n          namepath: \"foo\",\n          attributes: [json!({\n            \"arg\": {\n              \"help\": null,\n              \"long\": null,\n              \"name\": \"bar\",\n              \"pattern\": \"BAR\",\n              \"short\": null,\n              \"value\": null,\n            }\n          })]\n          .into(),\n          parameters: [Parameter {\n            kind: \"singular\",\n            name: \"bar\",\n            pattern: Some(\"BAR\"),\n            ..default()\n          }]\n          .into(),\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn arg_long() {\n  case(\n    \"[arg('bar', long='BAR')]\\nfoo bar:\",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          name: \"foo\",\n          namepath: \"foo\",\n          attributes: [json!({\n            \"arg\": {\n              \"help\": null,\n              \"long\": \"BAR\",\n              \"name\": \"bar\",\n              \"pattern\": null,\n              \"short\": null,\n              \"value\": null,\n            }\n          })]\n          .into(),\n          parameters: [Parameter {\n            kind: \"singular\",\n            name: \"bar\",\n            long: Some(\"BAR\"),\n            ..default()\n          }]\n          .into(),\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn arg_short() {\n  case(\n    \"[arg('bar', short='B')]\\nfoo bar:\",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          name: \"foo\",\n          namepath: \"foo\",\n          attributes: [json!({\n            \"arg\": {\n              \"help\": null,\n              \"long\": null,\n              \"name\": \"bar\",\n              \"pattern\": null,\n              \"short\": \"B\",\n              \"value\": null,\n            }\n          })]\n          .into(),\n          parameters: [Parameter {\n            kind: \"singular\",\n            name: \"bar\",\n            short: Some('B'),\n            ..default()\n          }]\n          .into(),\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn arg_value() {\n  case(\n    \"[arg('bar', short='B', value='hello')]\\nfoo bar:\",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          name: \"foo\",\n          namepath: \"foo\",\n          attributes: [json!({\n            \"arg\": {\n              \"help\": null,\n              \"long\": null,\n              \"name\": \"bar\",\n              \"pattern\": null,\n              \"short\": \"B\",\n              \"value\": \"hello\",\n            }\n          })]\n          .into(),\n          parameters: [Parameter {\n            kind: \"singular\",\n            name: \"bar\",\n            short: Some('B'),\n            value: Some(\"hello\"),\n            ..default()\n          }]\n          .into(),\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n\n#[test]\nfn arg_help() {\n  case(\n    \"[arg('bar', help='hello')]\\nfoo bar:\",\n    Module {\n      first: Some(\"foo\"),\n      recipes: [(\n        \"foo\",\n        Recipe {\n          name: \"foo\",\n          namepath: \"foo\",\n          attributes: [json!({\n            \"arg\": {\n              \"help\": \"hello\",\n              \"long\": null,\n              \"name\": \"bar\",\n              \"pattern\": null,\n              \"short\": null,\n              \"value\": null,\n            }\n          })]\n          .into(),\n          parameters: [Parameter {\n            help: Some(\"hello\"),\n            kind: \"singular\",\n            name: \"bar\",\n            ..default()\n          }]\n          .into(),\n          ..default()\n        },\n      )]\n      .into(),\n      ..default()\n    },\n  );\n}\n"
  },
  {
    "path": "tests/lazy.rs",
    "content": "use super::*;\n\n#[test]\nfn lazy_is_unstable() {\n  Test::new()\n    .justfile(\n      \"\n        set lazy\n\n        foo:\n      \",\n    )\n    .stderr_regex(r\"error: The `lazy` setting is currently unstable\\. .*\")\n    .failure();\n}\n\n#[test]\nfn eager_is_unstable() {\n  Test::new()\n    .justfile(\n      \"\n        eager x := 'hello'\n\n        foo:\n      \",\n    )\n    .stderr_regex(r\"error: `eager` assignments are currently unstable\\. .*\")\n    .failure();\n}\n\n#[test]\nfn unused_assignments_are_evaluated_without_lazy() {\n  Test::new()\n    .justfile(\n      \"\n        x := `exit 1`\n\n        foo:\n      \",\n    )\n    .stderr(\n      \"\n        error: Backtick failed with exit code 1\n         ——▶ justfile:1:6\n          │\n        1 │ x := `exit 1`\n          │      ^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn unused_assignment_not_evaluated() {\n  Test::new()\n    .justfile(\n      \"\n      set lazy\n\n      x := `exit 1`\n\n      foo:\n        @echo foo\n    \",\n    )\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stdout(\"foo\\n\")\n    .success();\n}\n\n#[test]\nfn used_assignment_evaluated() {\n  Test::new()\n    .justfile(\n      \"\n      set lazy\n\n      x := `exit 1`\n\n      foo:\n        @echo {{x}}\n    \",\n    )\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stderr(\n      \"\n        error: Backtick failed with exit code 1\n         ——▶ justfile:3:6\n          │\n        3 │ x := `exit 1`\n          │      ^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn transitively_used_assignment_evaluated() {\n  Test::new()\n    .justfile(\n      \"\n      set lazy\n\n      x := `exit 1`\n      y := x\n\n      foo:\n        @echo {{y}}\n    \",\n    )\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stderr(\n      \"\n        error: Backtick failed with exit code 1\n         ——▶ justfile:3:6\n          │\n        3 │ x := `exit 1`\n          │      ^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn assignment_used_in_parameter_default_evaluated() {\n  Test::new()\n    .justfile(\n      \"\n      set lazy\n\n      x := `exit 1`\n\n      foo val=x:\n    \",\n    )\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stderr(\n      \"\n        error: Backtick failed with exit code 1\n         ——▶ justfile:3:6\n          │\n        3 │ x := `exit 1`\n          │      ^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn assignment_used_in_dependency_argument_evaluated() {\n  Test::new()\n    .justfile(\n      \"\n      set lazy\n\n      x := `exit 1`\n\n      foo: (bar x)\n\n      bar val:\n    \",\n    )\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stderr(\n      \"\n        error: Backtick failed with exit code 1\n         ——▶ justfile:3:6\n          │\n        3 │ x := `exit 1`\n          │      ^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn assignment_in_body_interpolation_evaluated() {\n  Test::new()\n    .justfile(\n      \"\n      set lazy\n\n      x := `exit 1`\n\n      foo:\n        @echo {{x}}\n    \",\n    )\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stderr(\n      \"\n        error: Backtick failed with exit code 1\n         ——▶ justfile:3:6\n          │\n        3 │ x := `exit 1`\n          │      ^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn multiple_invocations_evaluate_union() {\n  Test::new()\n    .justfile(\n      \"\n      set lazy\n\n      x := 'foo'\n      y := 'bar'\n      z := `exit 1`\n\n      a:\n        @echo {{x}}\n\n      b:\n        @echo {{y}}\n    \",\n    )\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .args([\"a\", \"b\"])\n    .stdout(\"foo\\nbar\\n\")\n    .success();\n}\n\n#[test]\nfn assignment_used_in_dependency_evaluated() {\n  Test::new()\n    .justfile(\n      \"\n      set lazy\n\n      x := `exit 1`\n\n      foo: bar\n\n      bar:\n        @echo {{x}}\n    \",\n    )\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stderr(\n      \"\n        error: Backtick failed with exit code 1\n         ——▶ justfile:3:6\n          │\n        3 │ x := `exit 1`\n          │      ^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn assignment_used_in_transitive_dependency_evaluated() {\n  Test::new()\n    .justfile(\n      \"\n      set lazy\n\n      x := `exit 1`\n\n      foo: bar\n\n      bar: baz\n\n      baz:\n        @echo {{x}}\n    \",\n    )\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stderr(\n      \"\n        error: Backtick failed with exit code 1\n         ——▶ justfile:3:6\n          │\n        3 │ x := `exit 1`\n          │      ^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn exported_assignment_is_evaluated() {\n  Test::new()\n    .justfile(\n      \"\n      set lazy\n\n      export x := 'FOO'\n\n      bar:\n        @echo $x\n    \",\n    )\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn assignment_with_set_export_is_evaluated() {\n  Test::new()\n    .justfile(\n      \"\n      set lazy\n      set export\n\n      x := 'FOO'\n\n      bar:\n        @echo $x\n    \",\n    )\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn eager_assignments_are_evaluated() {\n  Test::new()\n    .justfile(\n      \"\n      set lazy\n\n      eager x := `exit 1`\n\n      foo:\n    \",\n    )\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stderr(\n      \"\n        error: Backtick failed with exit code 1\n         ——▶ justfile:3:12\n          │\n        3 │ eager x := `exit 1`\n          │            ^^^^^^^^\n      \",\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/lib.rs",
    "content": "use {\n  crate::{\n    assert_stdout::assert_stdout,\n    assert_success::assert_success,\n    tempdir::tempdir,\n    test::{Output, Test, assert_eval_eq},\n  },\n  just::{Response, unindent},\n  pretty_assertions::Comparison,\n  regex::Regex,\n  serde::{Deserialize, Serialize},\n  serde_json::{Value, json},\n  std::{\n    collections::BTreeMap,\n    env::{self, consts::EXE_SUFFIX},\n    error::Error,\n    fmt::Debug,\n    fs,\n    io::Write,\n    iter,\n    path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR, Path, PathBuf},\n    process::{Command, Stdio},\n    str,\n    time::{Duration, Instant},\n  },\n  tempfile::TempDir,\n  temptree::{Tree, temptree, tree},\n  which::which,\n};\n\nconst JUST: &str = env!(\"CARGO_BIN_EXE_just\");\n\n#[cfg(not(windows))]\nuse std::thread;\n\nfn default<T: Default>() -> T {\n  Default::default()\n}\n\n#[macro_use]\nmod test;\n\nmod alias;\nmod alias_style;\nmod allow_duplicate_recipes;\nmod allow_duplicate_variables;\nmod allow_missing;\nmod arg_attribute;\nmod assert_stdout;\nmod assert_success;\nmod assertions;\nmod assignment;\nmod attributes;\nmod backticks;\nmod byte_order_mark;\nmod ceiling;\nmod changelog;\nmod choose;\nmod command;\nmod completions;\nmod conditional;\nmod confirm;\nmod constants;\nmod datetime;\nmod default;\nmod delimiters;\nmod dependencies;\nmod directories;\nmod dotenv;\nmod dump;\nmod edit;\nmod equals;\nmod error_messages;\nmod evaluate;\nmod examples;\nmod explain;\nmod export;\nmod fallback;\nmod format;\nmod format_string;\nmod functions;\n#[cfg(unix)]\nmod global;\nmod groups;\nmod guards;\nmod ignore_comments;\nmod imports;\nmod init;\nmod interpolation;\nmod invocation_directory;\nmod json;\nmod lazy;\nmod line_prefixes;\nmod list;\nmod logical_operators;\nmod man;\nmod misc;\nmod modules;\nmod multibyte_char;\nmod newline_escape;\nmod no_aliases;\nmod no_cd;\nmod no_dependencies;\nmod no_exit_message;\nmod options;\nmod os_attributes;\nmod overrides;\nmod parallel;\nmod parameters;\nmod parser;\nmod positional_arguments;\nmod private;\nmod quiet;\nmod quote;\nmod readme;\nmod recursion_limit;\nmod regexes;\nmod request;\nmod run;\nmod scope;\nmod script;\nmod search;\nmod search_arguments;\nmod settings;\nmod shadowing_parameters;\nmod shebang;\nmod shell;\nmod shell_expansion;\nmod show;\n#[cfg(unix)]\nmod signals;\nmod slash_operator;\nmod string;\nmod subsequents;\nmod summary;\nmod tempdir;\nmod timestamps;\nmod undefined_variables;\nmod unexport;\nmod unstable;\nmod usage;\nmod which_function;\n#[cfg(windows)]\nmod windows;\n#[cfg(target_family = \"windows\")]\nmod windows_shell;\nmod working_directory;\n\nfn path(s: &str) -> String {\n  if cfg!(windows) {\n    s.replace('/', \"\\\\\")\n  } else {\n    s.into()\n  }\n}\n\nfn path_for_regex(s: &str) -> String {\n  if cfg!(windows) {\n    s.replace('/', \"\\\\\\\\\")\n  } else {\n    s.into()\n  }\n}\n"
  },
  {
    "path": "tests/line_prefixes.rs",
    "content": "use super::*;\n\n#[test]\nfn infallible_after_quiet() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          @-exit 1\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn quiet_after_infallible() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          -@exit 1\n      \",\n    )\n    .success();\n}\n"
  },
  {
    "path": "tests/list.rs",
    "content": "use super::*;\n\n#[test]\nfn modules_unsorted() {\n  Test::new()\n    .write(\"foo.just\", \"foo:\")\n    .write(\"bar.just\", \"bar:\")\n    .justfile(\n      \"\n        mod foo\n\n        mod bar\n      \",\n    )\n    .args([\"--list\", \"--unsorted\"])\n    .stdout(\n      \"\n        Available recipes:\n            foo ...\n            bar ...\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn unsorted_list_order() {\n  Test::new()\n    .write(\"a.just\", \"a:\")\n    .write(\"b.just\", \"b:\")\n    .write(\"c.just\", \"c:\")\n    .write(\"d.just\", \"d:\")\n    .justfile(\n      \"\n        import 'a.just'\n        import 'b.just'\n        import 'c.just'\n        import 'd.just'\n        x:\n        y:\n        z:\n      \",\n    )\n    .args([\"--list\", \"--unsorted\"])\n    .stdout(\n      \"\n        Available recipes:\n            x\n            y\n            z\n            a\n            b\n            c\n            d\n      \",\n    )\n    .success();\n\n  Test::new()\n    .write(\"a.just\", \"a:\")\n    .write(\"b.just\", \"b:\")\n    .write(\"c.just\", \"c:\")\n    .write(\"d.just\", \"d:\")\n    .justfile(\n      \"\n        x:\n        y:\n        z:\n        import 'd.just'\n        import 'c.just'\n        import 'b.just'\n        import 'a.just'\n      \",\n    )\n    .args([\"--list\", \"--unsorted\"])\n    .stdout(\n      \"\n        Available recipes:\n            x\n            y\n            z\n            d\n            c\n            b\n            a\n      \",\n    )\n    .success();\n\n  Test::new()\n    .write(\"a.just\", \"a:\\nimport 'e.just'\")\n    .write(\"b.just\", \"b:\\nimport 'f.just'\")\n    .write(\"c.just\", \"c:\\nimport 'g.just'\")\n    .write(\"d.just\", \"d:\\nimport 'h.just'\")\n    .write(\"e.just\", \"e:\")\n    .write(\"f.just\", \"f:\")\n    .write(\"g.just\", \"g:\")\n    .write(\"h.just\", \"h:\")\n    .justfile(\n      \"\n        x:\n        y:\n        z:\n        import 'd.just'\n        import 'c.just'\n        import 'b.just'\n        import 'a.just'\n      \",\n    )\n    .args([\"--list\", \"--unsorted\"])\n    .stdout(\n      \"\n        Available recipes:\n            x\n            y\n            z\n            d\n            h\n            c\n            g\n            b\n            f\n            a\n            e\n      \",\n    )\n    .success();\n\n  Test::new()\n    .write(\"task1.just\", \"task1:\")\n    .write(\"task2.just\", \"task2:\")\n    .justfile(\n      \"\n        import 'task1.just'\n        import 'task2.just'\n      \",\n    )\n    .args([\"--list\", \"--unsorted\"])\n    .stdout(\n      \"\n        Available recipes:\n            task1\n            task2\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_submodule() {\n  Test::new()\n    .write(\"foo.just\", \"bar:\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .args([\"--list\", \"foo\"])\n    .stdout(\n      \"\n      Available recipes:\n          bar\n    \",\n    )\n    .success();\n}\n\n#[test]\nfn list_nested_submodule() {\n  Test::new()\n    .write(\"foo.just\", \"mod bar\")\n    .write(\"bar.just\", \"baz:\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .args([\"--list\", \"foo\", \"bar\"])\n    .stdout(\n      \"\n        Available recipes:\n            baz\n      \",\n    )\n    .success();\n\n  Test::new()\n    .write(\"foo.just\", \"mod bar\")\n    .write(\"bar.just\", \"baz:\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .args([\"--list\", \"foo::bar\"])\n    .stdout(\n      \"\n        Available recipes:\n            baz\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_invalid_path() {\n  Test::new()\n    .args([\"--list\", \"$hello\"])\n    .stderr(\"error: Invalid module path `$hello`\\n\")\n    .failure();\n}\n\n#[test]\nfn list_unknown_submodule() {\n  Test::new()\n    .args([\"--list\", \"hello\"])\n    .stderr(\"error: Justfile does not contain submodule `hello`\\n\")\n    .failure();\n}\n\n#[test]\nfn list_with_groups_in_modules() {\n  Test::new()\n    .justfile(\n      \"\n        [group('FOO')]\n        foo:\n\n        mod bar\n      \",\n    )\n    .write(\"bar.just\", \"[group('BAZ')]\\nbaz:\")\n    .args([\"--list\", \"--list-submodules\"])\n    .stdout(\n      \"\n        Available recipes:\n            bar:\n                [BAZ]\n                baz\n\n            [FOO]\n            foo\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn list_displays_recipes_in_submodules() {\n  Test::new()\n    .write(\"foo.just\", \"bar:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .args([\"--list\", \"--list-submodules\"])\n    .stdout(\n      \"\n      Available recipes:\n          foo:\n              bar\n    \",\n    )\n    .success();\n}\n\n#[test]\nfn modules_are_space_separated_in_output() {\n  Test::new()\n    .write(\"foo.just\", \"foo:\")\n    .write(\"bar.just\", \"bar:\")\n    .justfile(\n      \"\n        mod foo\n\n        mod bar\n      \",\n    )\n    .args([\"--list\", \"--list-submodules\"])\n    .stdout(\n      \"\n      Available recipes:\n          bar:\n              bar\n\n          foo:\n              foo\n    \",\n    )\n    .success();\n}\n\n#[test]\nfn module_recipe_list_alignment_ignores_private_recipes() {\n  Test::new()\n    .write(\n      \"foo.just\",\n      \"\n# foos\nfoo:\n @echo FOO\n\n[private]\nbarbarbar:\n @echo BAR\n\n@_bazbazbaz:\n @echo BAZ\n      \",\n    )\n    .justfile(\"mod foo\")\n    .args([\"--list\", \"--list-submodules\"])\n    .stdout(\n      \"\n        Available recipes:\n            foo:\n                foo # foos\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn nested_modules_are_properly_indented() {\n  Test::new()\n    .write(\"foo.just\", \"mod bar\")\n    .write(\"bar.just\", \"baz:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .args([\"--list\", \"--list-submodules\"])\n    .stdout(\n      \"\n      Available recipes:\n          foo:\n              bar:\n                  baz\n    \",\n    )\n    .success();\n}\n\n#[test]\nfn module_doc_rendered() {\n  Test::new()\n    .write(\"foo.just\", \"\")\n    .justfile(\n      \"\n        # Module foo\n        mod foo\n      \",\n    )\n    .args([\"--list\"])\n    .stdout(\n      \"\n        Available recipes:\n            foo ... # Module foo\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn module_doc_aligned() {\n  Test::new()\n    .write(\"foo.just\", \"\")\n    .write(\"bar.just\", \"\")\n    .justfile(\n      \"\n        # Module foo\n        mod foo\n\n        # comment\n        mod very_long_name_for_module \\\"bar.just\\\" # comment\n\n        # will change your world\n        recipe:\n            @echo Hi\n      \",\n    )\n    .args([\"--list\"])\n    .stdout(\n      \"\n        Available recipes:\n            recipe                        # will change your world\n            foo ...                       # Module foo\n            very_long_name_for_module ... # comment\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn submodules_without_groups() {\n  Test::new()\n    .write(\"foo.just\", \"\")\n    .justfile(\n      \"\n        mod foo\n\n        [group: 'baz']\n        bar:\n      \",\n    )\n    .args([\"--list\"])\n    .stdout(\n      \"\n        Available recipes:\n            foo ...\n\n            [baz]\n            bar\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn no_space_before_submodules_not_following_groups() {\n  Test::new()\n    .write(\"foo.just\", \"\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .args([\"--list\"])\n    .stdout(\n      \"\n        Available recipes:\n            foo ...\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn backticks_highlighted() {\n  Test::new()\n    .justfile(\n      \"\n        # Comment `` `with backticks` and trailing text\n        recipe:\n      \",\n    )\n    .args([\"--list\", \"--color=always\"])\n    .stdout(\n      \"\n        Available recipes:\n            recipe \\u{1b}[34m#\\u{1b}[0m \\u{1b}[34mComment \\u{1b}[0m\\u{1b}[36m``\\u{1b}[0m\\u{1b}[34m \\u{1b}[0m\\u{1b}[36m`with backticks`\\u{1b}[0m\\u{1b}[34m and trailing text\\u{1b}[0m\n      \")\n    .success();\n}\n\n#[test]\nfn unclosed_backticks() {\n  Test::new()\n    .justfile(\n      \"\n        # Comment `with unclosed backick\n        recipe:\n      \",\n    )\n    .args([\"--list\", \"--color=always\"])\n    .stdout(\n      \"\n        Available recipes:\n            recipe \\u{1b}[34m#\\u{1b}[0m \\u{1b}[34mComment \\u{1b}[0m\\u{1b}[36m`with unclosed backick\\u{1b}[0m\n      \")\n    .success();\n}\n\n#[test]\nfn list_submodules_requires_list() {\n  Test::new()\n    .arg(\"--list-submodules\")\n    .stderr_regex(\"error: the following required arguments were not provided:\\n  --list .*\")\n    .status(2);\n}\n"
  },
  {
    "path": "tests/logical_operators.rs",
    "content": "use super::*;\n\n#[track_caller]\nfn evaluate(expression: &str, expected: &str) {\n  Test::new()\n    .justfile(format!(\"x := {expression}\"))\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(expected)\n    .success();\n}\n\n#[test]\nfn logical_operators_are_unstable() {\n  Test::new()\n    .justfile(\"x := 'foo' && 'bar'\")\n    .args([\"--evaluate\", \"x\"])\n    .stderr_regex(r\"error: The logical operators `&&` and `\\|\\|` are currently unstable. .*\")\n    .failure();\n\n  Test::new()\n    .justfile(\"x := 'foo' || 'bar'\")\n    .args([\"--evaluate\", \"x\"])\n    .stderr_regex(r\"error: The logical operators `&&` and `\\|\\|` are currently unstable. .*\")\n    .failure();\n}\n\n#[test]\nfn and_returns_empty_string_if_lhs_is_empty() {\n  evaluate(\"'' && 'hello'\", \"\");\n}\n\n#[test]\nfn and_returns_rhs_if_lhs_is_non_empty() {\n  evaluate(\"'hello' && 'goodbye'\", \"goodbye\");\n}\n\n#[test]\nfn and_has_lower_precedence_than_plus() {\n  evaluate(\"'' && 'goodbye' + 'foo'\", \"\");\n\n  evaluate(\"'foo' + 'hello' && 'goodbye'\", \"goodbye\");\n\n  evaluate(\"'foo' + '' && 'goodbye'\", \"goodbye\");\n\n  evaluate(\"'foo' + 'hello' && 'goodbye' + 'bar'\", \"goodbyebar\");\n}\n\n#[test]\nfn or_returns_rhs_if_lhs_is_empty() {\n  evaluate(\"'' || 'hello'\", \"hello\");\n}\n\n#[test]\nfn or_returns_lhs_if_lhs_is_non_empty() {\n  evaluate(\"'hello' || 'goodbye'\", \"hello\");\n}\n\n#[test]\nfn or_has_lower_precedence_than_plus() {\n  evaluate(\"'' || 'goodbye' + 'foo'\", \"goodbyefoo\");\n\n  evaluate(\"'foo' + 'hello' || 'goodbye'\", \"foohello\");\n\n  evaluate(\"'foo' + '' || 'goodbye'\", \"foo\");\n\n  evaluate(\"'foo' + 'hello' || 'goodbye' + 'bar'\", \"foohello\");\n}\n\n#[test]\nfn and_has_higher_precedence_than_or() {\n  evaluate(\"('' && 'foo') || 'bar'\", \"bar\");\n  evaluate(\"'' && 'foo' || 'bar'\", \"bar\");\n  evaluate(\"'a' && 'b' || 'c'\", \"b\");\n}\n\n#[test]\nfn nesting() {\n  evaluate(\"'' || '' || '' || '' || 'foo'\", \"foo\");\n  evaluate(\"'foo' && 'foo' && 'foo' && 'foo' && 'bar'\", \"bar\");\n}\n"
  },
  {
    "path": "tests/man.rs",
    "content": "use super::*;\n\n#[test]\nfn output() {\n  Test::new()\n    .arg(\"--man\")\n    .stdout_regex(\"(?s).*.TH just 1.*\")\n    .success();\n}\n"
  },
  {
    "path": "tests/misc.rs",
    "content": "use super::*;\n\n#[test]\nfn alias_listing() {\n  Test::new()\n    .arg(\"--list\")\n    .justfile(\n      \"\n    foo:\n      echo foo\n\n    alias f := foo\n  \",\n    )\n    .stdout(\n      \"\n    Available recipes:\n        foo # [alias: f]\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn alias_listing_with_doc() {\n  Test::new()\n    .justfile(\n      \"\n        # foo command\n        foo:\n          echo foo\n\n        alias f := foo\n      \",\n    )\n    .arg(\"--list\")\n    .stdout(\n      \"\n      Available recipes:\n          foo # foo command [alias: f]\n    \",\n    )\n    .success();\n}\n\n#[test]\nfn alias_listing_multiple_aliases() {\n  Test::new()\n    .arg(\"--list\")\n    .justfile(\"foo:\\n  echo foo\\nalias f := foo\\nalias fo := foo\")\n    .stdout(\n      \"\n    Available recipes:\n        foo # [aliases: f, fo]\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn alias_listing_parameters() {\n  Test::new()\n    .args([\"--list\"])\n    .justfile(\"foo PARAM='foo':\\n  echo {{PARAM}}\\nalias f := foo\")\n    .stdout(\n      \"\n    Available recipes:\n        foo PARAM='foo' # [alias: f]\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn alias_listing_private() {\n  Test::new()\n    .arg(\"--list\")\n    .justfile(\"foo PARAM='foo':\\n  echo {{PARAM}}\\nalias _f := foo\")\n    .stdout(\n      \"\n    Available recipes:\n        foo PARAM='foo'\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn alias() {\n  Test::new()\n    .arg(\"f\")\n    .justfile(\"foo:\\n  echo foo\\nalias f := foo\")\n    .stdout(\"foo\\n\")\n    .stderr(\"echo foo\\n\")\n    .success();\n}\n\n#[test]\nfn alias_with_parameters() {\n  Test::new()\n    .arg(\"f\")\n    .arg(\"bar\")\n    .justfile(\"foo value='foo':\\n  echo {{value}}\\nalias f := foo\")\n    .stdout(\"bar\\n\")\n    .stderr(\"echo bar\\n\")\n    .success();\n}\n\n#[test]\nfn bad_setting() {\n  Test::new()\n    .justfile(\n      \"\n    set foo\n  \",\n    )\n    .stderr(\n      \"\n  error: Unknown setting `foo`\n   ——▶ justfile:1:5\n    │\n  1 │ set foo\n    │     ^^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn bad_setting_with_keyword_name() {\n  Test::new()\n    .justfile(\n      \"\n    set if := 'foo'\n  \",\n    )\n    .stderr(\n      \"\n  error: Unknown setting `if`\n   ——▶ justfile:1:5\n    │\n  1 │ set if := 'foo'\n    │     ^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn alias_with_dependencies() {\n  Test::new()\n    .arg(\"b\")\n    .justfile(\"foo:\\n  echo foo\\nbar: foo\\nalias b := bar\")\n    .stdout(\"foo\\n\")\n    .stderr(\"echo foo\\n\")\n    .success();\n}\n\n#[test]\nfn duplicate_alias() {\n  Test::new()\n    .justfile(\"alias foo := bar\\nalias foo := baz\\n\")\n    .stderr(\n      \"\n    error: Alias `foo` first defined on line 1 is redefined on line 2\n     ——▶ justfile:2:7\n      │\n    2 │ alias foo := baz\n      │       ^^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_alias_target() {\n  Test::new()\n    .justfile(\"alias foo := bar\\n\")\n    .stderr(\n      \"\n    error: Alias `foo` has an unknown target `bar`\n     ——▶ justfile:1:7\n      │\n    1 │ alias foo := bar\n      │       ^^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn alias_shadows_recipe() {\n  Test::new()\n    .justfile(\"bar:\\n  echo bar\\nalias foo := bar\\nfoo:\\n  echo foo\")\n    .stderr(\n      \"\n    error: Alias `foo` defined on line 3 is redefined as a recipe on line 4\n     ——▶ justfile:4:1\n      │\n    4 │ foo:\n      │ ^^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn default() {\n  Test::new()\n    .justfile(\"default:\\n echo hello\\nother: \\n echo bar\")\n    .stdout(\"hello\\n\")\n    .stderr(\"echo hello\\n\")\n    .success();\n}\n\n#[test]\nfn quiet() {\n  Test::new()\n    .justfile(\"default:\\n @echo hello\")\n    .stdout(\"hello\\n\")\n    .success();\n}\n\n#[test]\nfn verbose() {\n  Test::new()\n    .arg(\"--verbose\")\n    .justfile(\"default:\\n @echo hello\")\n    .stdout(\"hello\\n\")\n    .stderr(\"===> Running recipe `default`...\\necho hello\\n\")\n    .success();\n}\n\n#[test]\nfn order() {\n  Test::new()\n    .arg(\"a\")\n    .arg(\"d\")\n    .justfile(\n      \"\nb: a\n  echo b\n  @mv a b\n\na:\n  echo a\n  @touch F\n  @touch a\n\nd: c\n  echo d\n  @rm c\n\nc: b\n  echo c\n  @mv b c\",\n    )\n    .stdout(\"a\\nb\\nc\\nd\\n\")\n    .stderr(\"echo a\\necho b\\necho c\\necho d\\n\")\n    .success();\n}\n\n#[test]\nfn select() {\n  Test::new()\n    .arg(\"d\")\n    .arg(\"c\")\n    .justfile(\"b:\\n  @echo b\\na:\\n  @echo a\\nd:\\n  @echo d\\nc:\\n  @echo c\")\n    .stdout(\"d\\nc\\n\")\n    .success();\n}\n\n#[test]\nfn print() {\n  Test::new()\n    .arg(\"d\")\n    .arg(\"c\")\n    .justfile(\"b:\\n  echo b\\na:\\n  echo a\\nd:\\n  echo d\\nc:\\n  echo c\")\n    .stdout(\"d\\nc\\n\")\n    .stderr(\"echo d\\necho c\\n\")\n    .success();\n}\n\n#[test]\nfn status_passthrough() {\n  Test::new()\n    .arg(\"recipe\")\n    .justfile(\n      \"\n\nhello:\n\nrecipe:\n  @exit 100\",\n    )\n    .stderr(\"error: Recipe `recipe` failed on line 5 with exit code 100\\n\")\n    .status(100);\n}\n\n#[test]\nfn unknown_dependency() {\n  Test::new()\n    .justfile(\"bar:\\nhello:\\nfoo: bar baaaaaaaz hello\")\n    .stderr(\n      \"\n    error: Recipe `foo` has unknown dependency `baaaaaaaz`\n     ——▶ justfile:3:10\n      │\n    3 │ foo: bar baaaaaaaz hello\n      │          ^^^^^^^^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn backtick_success() {\n  Test::new()\n    .justfile(\"a := `printf Hello,`\\nbar:\\n printf '{{a + `printf ' world.'`}}'\")\n    .stdout(\"Hello, world.\")\n    .stderr(\"printf 'Hello, world.'\\n\")\n    .success();\n}\n\n#[test]\nfn backtick_trimming() {\n  Test::new()\n    .justfile(\"a := `echo Hello,`\\nbar:\\n echo '{{a + `echo ' world.'`}}'\")\n    .stdout(\"Hello, world.\\n\")\n    .stderr(\"echo 'Hello, world.'\\n\")\n    .success();\n}\n\n#[test]\nfn backtick_code_assignment() {\n  Test::new()\n    .justfile(\"b := a\\na := `exit 100`\\nbar:\\n echo '{{`exit 200`}}'\")\n    .stderr(\n      \"\n    error: Backtick failed with exit code 100\n     ——▶ justfile:2:6\n      │\n    2 │ a := `exit 100`\n      │      ^^^^^^^^^^\n  \",\n    )\n    .status(100);\n}\n\n#[test]\nfn backtick_code_interpolation() {\n  Test::new()\n    .justfile(\"b := a\\na := `echo hello`\\nbar:\\n echo '{{`exit 200`}}'\")\n    .stderr(\n      \"\n    error: Backtick failed with exit code 200\n     ——▶ justfile:4:10\n      │\n    4 │  echo '{{`exit 200`}}'\n      │          ^^^^^^^^^^\n  \",\n    )\n    .status(200);\n}\n\n#[test]\nfn backtick_code_interpolation_mod() {\n  Test::new()\n    .justfile(\"f:\\n 無{{`exit 200`}}\")\n    .stderr(\n      \"\n    error: Backtick failed with exit code 200\n     ——▶ justfile:2:7\n      │\n    2 │  無{{`exit 200`}}\n      │      ^^^^^^^^^^\n  \",\n    )\n    .status(200);\n}\n\n#[test]\nfn backtick_code_interpolation_tab() {\n  Test::new()\n    .justfile(\n      \"\n    backtick-fail:\n    \\techo {{`exit 200`}}\n  \",\n    )\n    .stderr(\n      \"    error: Backtick failed with exit code 200\n     ——▶ justfile:2:9\n      │\n    2 │     echo {{`exit 200`}}\n      │            ^^^^^^^^^^\n  \",\n    )\n    .status(200);\n}\n\n#[test]\nfn backtick_code_interpolation_tabs() {\n  Test::new()\n    .justfile(\n      \"\n    backtick-fail:\n    \\techo {{\\t`exit 200`}}\n  \",\n    )\n    .stderr(\n      \"error: Backtick failed with exit code 200\n ——▶ justfile:2:10\n  │\n2 │     echo {{    `exit 200`}}\n  │                ^^^^^^^^^^\n\",\n    )\n    .status(200);\n}\n\n#[test]\nfn backtick_code_interpolation_inner_tab() {\n  Test::new()\n    .justfile(\n      \"\n    backtick-fail:\n    \\techo {{\\t`exit\\t\\t200`}}\n  \",\n    )\n    .stderr(\n      \"\n    error: Backtick failed with exit code 200\n     ——▶ justfile:2:10\n      │\n    2 │     echo {{    `exit        200`}}\n      │                ^^^^^^^^^^^^^^^^^\n  \",\n    )\n    .status(200);\n}\n\n#[test]\nfn backtick_code_interpolation_leading_emoji() {\n  Test::new()\n    .justfile(\n      \"\n    backtick-fail:\n    \\techo 😬{{`exit 200`}}\n  \",\n    )\n    .stderr(\n      \"\n    error: Backtick failed with exit code 200\n     ——▶ justfile:2:13\n      │\n    2 │     echo 😬{{`exit 200`}}\n      │              ^^^^^^^^^^\n  \",\n    )\n    .status(200);\n}\n\n#[test]\nfn backtick_code_interpolation_unicode_hell() {\n  Test::new()\n    .justfile(\n      \"\n    backtick-fail:\n    \\techo \\t\\t\\t😬鎌鼬{{\\t\\t`exit 200 # \\t\\t\\tabc`}}\\t\\t\\t😬鎌鼬\n  \",\n    )\n    .stderr(\n      \"\n    error: Backtick failed with exit code 200\n     ——▶ justfile:2:24\n      │\n    2 │     echo             😬鎌鼬{{        `exit 200 #             abc`}}            😬鎌鼬\n      │                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  \",\n    )\n    .status(200);\n}\n\n#[test]\nfn backtick_code_long() {\n  Test::new()\n    .justfile(\n      \"\n\n\n\n\n\n\n    b := a\n    a := `echo hello`\n    bar:\n     echo '{{`exit 200`}}'\n  \",\n    )\n    .stderr(\n      \"\n    error: Backtick failed with exit code 200\n      ——▶ justfile:10:10\n       │\n    10 │  echo '{{`exit 200`}}'\n       │          ^^^^^^^^^^\n  \",\n    )\n    .status(200);\n}\n\n#[test]\nfn shebang_backtick_failure() {\n  Test::new()\n    .justfile(\n      \"foo:\n #!/bin/sh\n echo hello\n echo {{`exit 123`}}\",\n    )\n    .stderr(\n      \"\n    error: Backtick failed with exit code 123\n     ——▶ justfile:4:9\n      │\n    4 │  echo {{`exit 123`}}\n      │         ^^^^^^^^^^\n  \",\n    )\n    .status(123);\n}\n\n#[test]\nfn command_backtick_failure() {\n  Test::new()\n    .justfile(\n      \"foo:\n echo hello\n echo {{`exit 123`}}\",\n    )\n    .stdout(\"hello\\n\")\n    .stderr(\n      \"\n    echo hello\n    error: Backtick failed with exit code 123\n     ——▶ justfile:3:9\n      │\n    3 │  echo {{`exit 123`}}\n      │         ^^^^^^^^^^\n  \",\n    )\n    .status(123);\n}\n\n#[test]\nfn assignment_backtick_failure() {\n  Test::new()\n    .justfile(\n      \"foo:\n echo hello\n echo {{`exit 111`}}\na := `exit 222`\",\n    )\n    .stderr(\n      \"\n    error: Backtick failed with exit code 222\n     ——▶ justfile:4:6\n      │\n    4 │ a := `exit 222`\n      │      ^^^^^^^^^^\n  \",\n    )\n    .status(222);\n}\n\n#[test]\nfn dry_run() {\n  Test::new()\n    .arg(\"--dry-run\")\n    .arg(\"shebang\")\n    .arg(\"command\")\n    .justfile(\n      r\"\nvar := `echo stderr 1>&2; echo backtick`\n\ncommand:\n  @touch /this/is/not/a/file\n  {{var}}\n  echo {{`echo command interpolation`}}\n\nshebang:\n  #!/bin/sh\n  touch /this/is/not/a/file\n  {{var}}\n  echo {{`echo shebang interpolation`}}\",\n    )\n    .stderr(\n      \"#!/bin/sh\ntouch /this/is/not/a/file\n`echo stderr 1>&2; echo backtick`\necho `echo shebang interpolation`\ntouch /this/is/not/a/file\n`echo stderr 1>&2; echo backtick`\necho `echo command interpolation`\n\",\n    )\n    .success();\n}\n\n#[test]\nfn line_error_spacing() {\n  Test::new()\n    .justfile(\n      r\"\n\n\n\n\n\n\n\n\n\n^^^\n\",\n    )\n    .stderr(\n      \"error: Unknown start of token '^'\n  ——▶ justfile:10:1\n   │\n10 │ ^^^\n   │ ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn argument_single() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"ARGUMENT\")\n    .justfile(\n      \"\nfoo A:\n  echo {{A}}\n    \",\n    )\n    .stdout(\"ARGUMENT\\n\")\n    .stderr(\"echo ARGUMENT\\n\")\n    .success();\n}\n\n#[test]\nfn argument_multiple() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"ONE\")\n    .arg(\"TWO\")\n    .justfile(\n      \"\nfoo A B:\n  echo A:{{A}} B:{{B}}\n    \",\n    )\n    .stdout(\"A:ONE B:TWO\\n\")\n    .stderr(\"echo A:ONE B:TWO\\n\")\n    .success();\n}\n\n#[test]\nfn argument_mismatch_more() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"ONE\")\n    .arg(\"TWO\")\n    .arg(\"THREE\")\n    .stderr(\"error: Justfile does not contain recipe `THREE`\\n\")\n    .justfile(\n      \"\nfoo A B:\n  echo A:{{A}} B:{{B}}\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn argument_mismatch_fewer() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"ONE\")\n    .justfile(\n      \"\nfoo A B:\n  echo A:{{A}} B:{{B}}\n    \",\n    )\n    .stderr(\"error: Recipe `foo` got 1 positional argument but takes 2\\nusage:\\n    just foo A B\\n\")\n    .failure();\n}\n\n#[test]\nfn argument_mismatch_more_with_default() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"ONE\")\n    .arg(\"TWO\")\n    .arg(\"THREE\")\n    .justfile(\n      \"\nfoo A B='B':\n  echo A:{{A}} B:{{B}}\n    \",\n    )\n    .stderr(\"error: Justfile does not contain recipe `THREE`\\n\")\n    .failure();\n}\n\n#[test]\nfn argument_mismatch_fewer_with_default() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"bar\")\n    .justfile(\n      \"\nfoo A B C='C':\n  echo A:{{A}} B:{{B}} C:{{C}}\n    \",\n    )\n    .stderr(\n      \"\n    error: Recipe `foo` got 1 positional argument but takes at least 2\n    usage:\n        just foo A B [C]\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_recipe() {\n  Test::new()\n    .arg(\"foo\")\n    .justfile(\"hello:\")\n    .stderr(\"error: Justfile does not contain recipe `foo`\\n\")\n    .failure();\n}\n\n#[test]\nfn unknown_recipes() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"bar\")\n    .justfile(\"hello:\")\n    .stderr(\"error: Justfile does not contain recipe `foo`\\n\")\n    .failure();\n}\n\n#[test]\nfn color_always() {\n  Test::new()\n        .arg(\"--color\")\n        .arg(\"always\")\n        .justfile(\"b := a\\na := `exit 100`\\nbar:\\n echo '{{`exit 200`}}'\")\n        .stderr(\"\\u{1b}[1;31merror\\u{1b}[0m: \\u{1b}[1mBacktick failed with exit code 100\\u{1b}[0m\\n \\u{1b}[1;34m——▶\\u{1b}[0m justfile:2:6\\n  \\u{1b}[1;34m│\\u{1b}[0m\\n\\u{1b}[1;34m2 │\\u{1b}[0m a := `exit 100`\\n  \\u{1b}[1;34m│\\u{1b}[0m      \\u{1b}[1;31m^^^^^^^^^^\\u{1b}[0m\\n\")\n        .status(100);\n}\n\n#[test]\nfn color_never() {\n  Test::new()\n    .arg(\"--color\")\n    .arg(\"never\")\n    .justfile(\"b := a\\na := `exit 100`\\nbar:\\n echo '{{`exit 200`}}'\")\n    .stderr(\n      \"error: Backtick failed with exit code 100\n ——▶ justfile:2:6\n  │\n2 │ a := `exit 100`\n  │      ^^^^^^^^^^\n\",\n    )\n    .status(100);\n}\n\n#[test]\nfn color_auto() {\n  Test::new()\n    .arg(\"--color\")\n    .arg(\"auto\")\n    .justfile(\"b := a\\na := `exit 100`\\nbar:\\n echo '{{`exit 200`}}'\")\n    .stderr(\n      \"error: Backtick failed with exit code 100\n ——▶ justfile:2:6\n  │\n2 │ a := `exit 100`\n  │      ^^^^^^^^^^\n\",\n    )\n    .status(100);\n}\n\n#[test]\nfn colors_no_context() {\n  Test::new()\n    .arg(\"--color=always\")\n    .stderr(\n      \"\\u{1b}[1;31merror\\u{1b}[0m: \\u{1b}[1m\\\nRecipe `recipe` failed on line 2 with exit code 100\\u{1b}[0m\\n\",\n    )\n    .justfile(\n      \"\nrecipe:\n  @exit 100\",\n    )\n    .status(100);\n}\n\n#[test]\nfn mixed_whitespace() {\n  Test::new()\n    .justfile(\"bar:\\n\\t echo hello\")\n    .stderr(\n      \"error: Found a mix of tabs and spaces in leading whitespace: `␉␠`\nLeading whitespace may consist of tabs or spaces, but not both\n ——▶ justfile:2:1\n  │\n2 │      echo hello\n  │ ^^^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn extra_leading_whitespace() {\n  Test::new()\n    .justfile(\"bar:\\n\\t\\techo hello\\n\\t\\t\\techo goodbye\")\n    .stderr(\n      \"error: Recipe line has extra leading whitespace\n ——▶ justfile:3:3\n  │\n3 │             echo goodbye\n  │         ^^^^^^^^^^^^^^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn inconsistent_leading_whitespace() {\n  Test::new()\n    .justfile(\"bar:\\n\\t\\techo hello\\n\\t echo goodbye\")\n    .stderr(\n      \"error: Recipe line has inconsistent leading whitespace. \\\n            Recipe started with `␉␉` but found line with `␉␠`\n ——▶ justfile:3:1\n  │\n3 │      echo goodbye\n  │ ^^^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn required_after_default() {\n  Test::new()\n    .justfile(\"bar:\\nhello baz arg='foo' bar:\")\n    .stderr(\n      \"error: Non-default parameter `bar` follows default parameter\n ——▶ justfile:2:21\n  │\n2 │ hello baz arg='foo' bar:\n  │                     ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn required_after_plus_variadic() {\n  Test::new()\n    .justfile(\"bar:\\nhello baz +arg bar:\")\n    .stderr(\n      \"error: Parameter `bar` follows variadic parameter\n ——▶ justfile:2:16\n  │\n2 │ hello baz +arg bar:\n  │                ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn required_after_star_variadic() {\n  Test::new()\n    .justfile(\"bar:\\nhello baz *arg bar:\")\n    .stderr(\n      \"error: Parameter `bar` follows variadic parameter\n ——▶ justfile:2:16\n  │\n2 │ hello baz *arg bar:\n  │                ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn use_string_default() {\n  Test::new()\n    .arg(\"hello\")\n    .arg(\"ABC\")\n    .justfile(\n      r#\"\nbar:\nhello baz arg=\"XYZ\\t\\\"\t\":\n  echo '{{baz}}...{{arg}}'\n\"#,\n    )\n    .stdout(\"ABC...XYZ\\t\\\"\\t\\n\")\n    .stderr(\"echo 'ABC...XYZ\\t\\\"\\t'\\n\")\n    .success();\n}\n\n#[test]\nfn use_raw_string_default() {\n  Test::new()\n    .arg(\"hello\")\n    .arg(\"ABC\")\n    .justfile(\n      r#\"\nbar:\nhello baz arg='XYZ\"\t':\n  printf '{{baz}}...{{arg}}'\n\"#,\n    )\n    .stdout(\"ABC...XYZ\\\"\\t\")\n    .stderr(\"printf 'ABC...XYZ\\\"\\t'\\n\")\n    .success();\n}\n\n#[test]\nfn supply_use_default() {\n  Test::new()\n    .arg(\"hello\")\n    .arg(\"0\")\n    .arg(\"1\")\n    .justfile(\n      r\"\nhello a b='B' c='C':\n  echo {{a}} {{b}} {{c}}\n\",\n    )\n    .stdout(\"0 1 C\\n\")\n    .stderr(\"echo 0 1 C\\n\")\n    .success();\n}\n\n#[test]\nfn supply_defaults() {\n  Test::new()\n    .arg(\"hello\")\n    .arg(\"0\")\n    .arg(\"1\")\n    .arg(\"2\")\n    .justfile(\n      r\"\nhello a b='B' c='C':\n  echo {{a}} {{b}} {{c}}\n\",\n    )\n    .stdout(\"0 1 2\\n\")\n    .stderr(\"echo 0 1 2\\n\")\n    .success();\n}\n\n#[test]\nfn list() {\n  Test::new()\n    .arg(\"--list\")\n    .justfile(\n      r#\"\n\n# this does a thing\nhello a b='B\t' c='C':\n  echo {{a}} {{b}} {{c}}\n\n# this comment will be ignored\n\na Z=\"\\t z\":\n\n# this recipe will not appear\n_private-recipe:\n\"#,\n    )\n    .stdout(\n      r#\"\n    Available recipes:\n        a Z=\"\\t z\"\n        hello a b='B\t' c='C' # this does a thing\n  \"#,\n    )\n    .success();\n}\n\n#[test]\nfn list_alignment() {\n  Test::new()\n    .arg(\"--list\")\n    .justfile(\n      r#\"\n\n# this does a thing\nhello a b='B\t' c='C':\n  echo {{a}} {{b}} {{c}}\n\n# something else\na Z=\"\\t z\":\n\n# this recipe will not appear\n_private-recipe:\n\"#,\n    )\n    .stdout(\n      r#\"\n    Available recipes:\n        a Z=\"\\t z\"           # something else\n        hello a b='B\t' c='C' # this does a thing\n  \"#,\n    )\n    .success();\n}\n\n#[test]\nfn list_alignment_long() {\n  Test::new()\n    .arg(\"--list\")\n    .justfile(\n      r#\"\n\n# this does a thing\nhello a b='B\t' c='C':\n  echo {{a}} {{b}} {{c}}\n\n# this does another thing\nx a b='B\t' c='C':\n  echo {{a}} {{b}} {{c}}\n\n# something else\nthis-recipe-is-very-very-very-very-very-very-very-very-important Z=\"\\t z\":\n\n# this recipe will not appear\n_private-recipe:\n\"#,\n    )\n    .stdout(\n      r#\"\n    Available recipes:\n        hello a b='B\t' c='C' # this does a thing\n        this-recipe-is-very-very-very-very-very-very-very-very-important Z=\"\\t z\" # something else\n        x a b='B\t' c='C'     # this does another thing\n  \"#,\n    )\n    .success();\n}\n\n#[test]\nfn list_sorted() {\n  Test::new()\n    .arg(\"--list\")\n    .justfile(\n      r\"\nalias c := b\nb:\na:\n\",\n    )\n    .stdout(\n      r\"\n    Available recipes:\n        a\n        b # [alias: c]\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn list_unsorted() {\n  Test::new()\n    .arg(\"--list\")\n    .arg(\"--unsorted\")\n    .justfile(\n      r\"\nalias c := b\nb:\na:\n\",\n    )\n    .stdout(\n      r\"\n    Available recipes:\n        b # [alias: c]\n        a\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn list_heading() {\n  Test::new()\n    .arg(\"--list\")\n    .arg(\"--list-heading\")\n    .arg(\"Cool stuff…\\n\")\n    .justfile(\n      r\"\na:\nb:\n\",\n    )\n    .stdout(\n      r\"\n    Cool stuff…\n        a\n        b\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn list_prefix() {\n  Test::new()\n    .arg(\"--list\")\n    .arg(\"--list-prefix\")\n    .arg(\"····\")\n    .justfile(\n      r\"\na:\nb:\n\",\n    )\n    .stdout(\n      r\"\n    Available recipes:\n    ····a\n    ····b\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn list_empty_prefix_and_heading() {\n  Test::new()\n    .arg(\"--list\")\n    .arg(\"--list-heading\")\n    .arg(\"\")\n    .arg(\"--list-prefix\")\n    .arg(\"\")\n    .justfile(\n      r\"\na:\nb:\n\",\n    )\n    .stdout(\n      r\"\n    a\n    b\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn run_suggestion() {\n  Test::new()\n    .arg(\"hell\")\n    .justfile(\"hello:\")\n    .stderr(\"error: Justfile does not contain recipe `hell`\\nDid you mean `hello`?\\n\")\n    .failure();\n}\n\n#[test]\nfn private_recipes_are_not_suggested() {\n  Test::new()\n    .arg(\"hell\")\n    .justfile(\n      \"\n        [private]\n        hello:\n      \",\n    )\n    .stderr(\"error: Justfile does not contain recipe `hell`\\n\")\n    .failure();\n}\n\n#[test]\nfn alias_suggestion() {\n  Test::new()\n    .arg(\"hell\")\n    .justfile(\n      \"\n        alias hello := bar\n\n        bar:\n      \",\n    )\n    .stderr(\n      \"error: Justfile does not contain recipe `hell`\\nDid you mean `hello`, an alias for `bar`?\\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn private_aliases_are_not_suggested() {\n  Test::new()\n    .arg(\"hell\")\n    .justfile(\n      \"\n        [private]\n        alias hello := bar\n\n        bar:\n      \",\n    )\n    .stderr(\"error: Justfile does not contain recipe `hell`\\n\")\n    .failure();\n}\n\n#[test]\nfn line_continuation_with_space() {\n  Test::new()\n    .justfile(\n      r\"\nfoo:\n  echo a\\\n         b  \\\n             c\n\",\n    )\n    .stdout(\"ab c\\n\")\n    .stderr(\"echo ab  c\\n\")\n    .success();\n}\n\n#[test]\nfn line_continuation_with_quoted_space() {\n  Test::new()\n    .justfile(\n      r\"\nfoo:\n  echo 'a\\\n         b  \\\n             c'\n\",\n    )\n    .stdout(\"ab  c\\n\")\n    .stderr(\"echo 'ab  c'\\n\")\n    .success();\n}\n\n#[test]\nfn line_continuation_no_space() {\n  Test::new()\n    .justfile(\n      r\"\nfoo:\n  echo a\\\n  b\\\n  c\n\",\n    )\n    .stdout(\"abc\\n\")\n    .stderr(\"echo abc\\n\")\n    .success();\n}\n\n#[test]\nfn infallible_command() {\n  Test::new()\n    .justfile(\n      r\"\ninfallible:\n  -exit 101\n\",\n    )\n    .stderr(\"exit 101\\n\")\n    .success();\n}\n\n#[test]\nfn infallible_with_failing() {\n  Test::new()\n    .justfile(\n      r\"\ninfallible:\n  -exit 101\n  exit 202\n\",\n    )\n    .stderr(\n      r\"exit 101\nexit 202\nerror: Recipe `infallible` failed on line 3 with exit code 202\n\",\n    )\n    .status(202);\n}\n\n#[test]\nfn quiet_recipe() {\n  Test::new()\n    .justfile(\n      r\"\n@quiet:\n  # a\n  # b\n  @echo c\n\",\n    )\n    .stdout(\"c\\n\")\n    .stderr(\"echo c\\n\")\n    .success();\n}\n\n#[test]\nfn quiet_shebang_recipe() {\n  Test::new()\n    .justfile(\n      r\"\n@quiet:\n  #!/bin/sh\n  echo hello\n\",\n    )\n    .stdout(\"hello\\n\")\n    .stderr(\"#!/bin/sh\\necho hello\\n\")\n    .success();\n}\n\n#[test]\nfn complex_dependencies() {\n  Test::new()\n    .arg(\"b\")\n    .justfile(\n      r\"\na: b\nb:\nc: b a\n\",\n    )\n    .success();\n}\n\n#[test]\nfn unknown_function_in_assignment() {\n  Test::new()\n    .arg(\"bar\")\n    .justfile(\n      r#\"foo := foo() + \"hello\"\nbar:\"#,\n    )\n    .stderr(\n      r#\"error: Call to unknown function `foo`\n ——▶ justfile:1:8\n  │\n1 │ foo := foo() + \"hello\"\n  │        ^^^\n\"#,\n    )\n    .failure();\n}\n\n#[test]\nfn dependency_takes_arguments_exact() {\n  Test::new()\n    .arg(\"b\")\n    .justfile(\n      \"\n    a FOO:\n    b: a\n  \",\n    )\n    .stderr(\n      \"error: Dependency `a` got 0 arguments but takes 1 argument\n ——▶ justfile:2:4\n  │\n2 │ b: a\n  │    ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn dependency_takes_arguments_at_least() {\n  Test::new()\n    .arg(\"b\")\n    .justfile(\n      \"\n    a FOO LUZ='hello':\n    b: a\n  \",\n    )\n    .stderr(\n      \"error: Dependency `a` got 0 arguments but takes at least 1 argument\n ——▶ justfile:2:4\n  │\n2 │ b: a\n  │    ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn dependency_takes_arguments_at_most() {\n  Test::new()\n    .arg(\"b\")\n    .justfile(\n      \"\n    a FOO LUZ='hello':\n    b: (a '0' '1' '2')\n  \",\n    )\n    .stderr(\n      \"error: Dependency `a` got 3 arguments but takes at most 2 arguments\n ——▶ justfile:2:5\n  │\n2 │ b: (a '0' '1' '2')\n  │     ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn duplicate_parameter() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\"a foo foo:\")\n    .stderr(\n      \"error: Recipe `a` has duplicate parameter `foo`\n ——▶ justfile:1:7\n  │\n1 │ a foo foo:\n  │       ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn duplicate_recipe() {\n  Test::new()\n    .arg(\"b\")\n    .justfile(\"b:\\nb:\")\n    .stderr(\n      \"error: Recipe `b` first defined on line 1 is redefined on line 2\n ——▶ justfile:2:1\n  │\n2 │ b:\n  │ ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn duplicate_variable() {\n  Test::new()\n    .arg(\"foo\")\n    .justfile(\"a := 'hello'\\na := 'hello'\\nfoo:\")\n    .stderr(\n      \"error: Variable `a` has multiple definitions\n ——▶ justfile:2:1\n  │\n2 │ a := 'hello'\n  │ ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn unexpected_token_in_dependency_position() {\n  Test::new()\n    .arg(\"foo\")\n    .justfile(\"foo: 'bar'\")\n    .stderr(\n      \"error: Expected '&&', comment, end of file, end of line, \\\n    identifier, or '(', but found string\n ——▶ justfile:1:6\n  │\n1 │ foo: 'bar'\n  │      ^^^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn unexpected_token_after_name() {\n  Test::new()\n    .arg(\"foo\")\n    .justfile(\"foo 'bar'\")\n    .stderr(\n      \"error: Expected '*', ':', '$', identifier, or '+', but found string\n ——▶ justfile:1:5\n  │\n1 │ foo 'bar'\n  │     ^^^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn self_dependency() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\"a: a\")\n    .stderr(\n      \"error: Recipe `a` depends on itself\n ——▶ justfile:1:4\n  │\n1 │ a: a\n  │    ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn long_circular_recipe_dependency() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\"a: b\\nb: c\\nc: d\\nd: a\")\n    .stderr(\n      \"error: Recipe `d` has circular dependency `a -> b -> c -> d -> a`\n ——▶ justfile:4:4\n  │\n4 │ d: a\n  │    ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn variable_self_dependency() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\"z := z\\na:\")\n    .stderr(\n      \"error: Variable `z` is defined in terms of itself\n ——▶ justfile:1:1\n  │\n1 │ z := z\n  │ ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn variable_circular_dependency() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\"x := y\\ny := z\\nz := x\\na:\")\n    .stderr(\n      \"error: Variable `x` depends on its own value: `x -> y -> z -> x`\n ——▶ justfile:1:1\n  │\n1 │ x := y\n  │ ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn variable_circular_dependency_with_additional_variable() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      \"\n    a := ''\n    x := y\n    y := x\n\n    a:\n  \",\n    )\n    .stderr(\n      \"error: Variable `x` depends on its own value: `x -> y -> x`\n ——▶ justfile:2:1\n  │\n2 │ x := y\n  │ ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn plus_variadic_recipe() {\n  Test::new()\n    .arg(\"a\")\n    .arg(\"0\")\n    .arg(\"1\")\n    .arg(\"2\")\n    .arg(\"3\")\n    .arg(\" 4 \")\n    .justfile(\n      \"\na x y +z:\n  echo {{x}} {{y}} {{z}}\n\",\n    )\n    .stdout(\"0 1 2 3 4\\n\")\n    .stderr(\"echo 0 1 2 3  4 \\n\")\n    .success();\n}\n\n#[test]\nfn plus_variadic_ignore_default() {\n  Test::new()\n    .arg(\"a\")\n    .arg(\"0\")\n    .arg(\"1\")\n    .arg(\"2\")\n    .arg(\"3\")\n    .arg(\" 4 \")\n    .justfile(\n      \"\na x y +z='HELLO':\n  echo {{x}} {{y}} {{z}}\n\",\n    )\n    .stdout(\"0 1 2 3 4\\n\")\n    .stderr(\"echo 0 1 2 3  4 \\n\")\n    .success();\n}\n\n#[test]\nfn plus_variadic_use_default() {\n  Test::new()\n    .arg(\"a\")\n    .arg(\"0\")\n    .arg(\"1\")\n    .justfile(\n      \"\na x y +z='HELLO':\n  echo {{x}} {{y}} {{z}}\n\",\n    )\n    .stdout(\"0 1 HELLO\\n\")\n    .stderr(\"echo 0 1 HELLO\\n\")\n    .success();\n}\n\n#[test]\nfn plus_variadic_too_few() {\n  Test::new()\n    .arg(\"a\")\n    .arg(\"0\")\n    .arg(\"1\")\n    .justfile(\n      \"\na x y +z:\n  echo {{x}} {{y}} {{z}}\n\",\n    )\n    .stderr(\n      \"\n        error: Recipe `a` got 2 positional arguments but takes at least 3\n        usage:\n            just a x y z...\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn star_variadic_recipe() {\n  Test::new()\n    .arg(\"a\")\n    .arg(\"0\")\n    .arg(\"1\")\n    .arg(\"2\")\n    .arg(\"3\")\n    .arg(\" 4 \")\n    .justfile(\n      \"\na x y *z:\n  echo {{x}} {{y}} {{z}}\n\",\n    )\n    .stdout(\"0 1 2 3 4\\n\")\n    .stderr(\"echo 0 1 2 3  4 \\n\")\n    .success();\n}\n\n#[test]\nfn star_variadic_none() {\n  Test::new()\n    .arg(\"a\")\n    .arg(\"0\")\n    .arg(\"1\")\n    .justfile(\n      \"\na x y *z:\n  echo {{x}} {{y}} {{z}}\n\",\n    )\n    .stdout(\"0 1\\n\")\n    .stderr(\"echo 0 1 \\n\")\n    .success();\n}\n\n#[test]\nfn star_variadic_ignore_default() {\n  Test::new()\n    .arg(\"a\")\n    .arg(\"0\")\n    .arg(\"1\")\n    .arg(\"2\")\n    .arg(\"3\")\n    .arg(\" 4 \")\n    .justfile(\n      \"\na x y *z='HELLO':\n  echo {{x}} {{y}} {{z}}\n\",\n    )\n    .stdout(\"0 1 2 3 4\\n\")\n    .stderr(\"echo 0 1 2 3  4 \\n\")\n    .success();\n}\n\n#[test]\nfn star_variadic_use_default() {\n  Test::new()\n    .arg(\"a\")\n    .arg(\"0\")\n    .arg(\"1\")\n    .justfile(\n      \"\na x y *z='HELLO':\n  echo {{x}} {{y}} {{z}}\n\",\n    )\n    .stdout(\"0 1 HELLO\\n\")\n    .stderr(\"echo 0 1 HELLO\\n\")\n    .success();\n}\n\n#[test]\nfn star_then_plus_variadic() {\n  Test::new()\n    .justfile(\n      \"\nfoo *a +b:\n  echo {{a}} {{b}}\n\",\n    )\n    .stderr(\n      \"error: Expected \\':\\' or \\'=\\', but found \\'+\\'\n ——▶ justfile:1:8\n  │\n1 │ foo *a +b:\n  │        ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn plus_then_star_variadic() {\n  Test::new()\n    .justfile(\n      \"\nfoo +a *b:\n  echo {{a}} {{b}}\n\",\n    )\n    .stderr(\n      \"error: Expected \\':\\' or \\'=\\', but found \\'*\\'\n ——▶ justfile:1:8\n  │\n1 │ foo +a *b:\n  │        ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn argument_grouping() {\n  Test::new()\n    .arg(\"BAR\")\n    .arg(\"0\")\n    .arg(\"FOO\")\n    .arg(\"1\")\n    .arg(\"2\")\n    .arg(\"BAZ\")\n    .arg(\"3\")\n    .arg(\"4\")\n    .arg(\"5\")\n    .justfile(\n      \"\nFOO A B='blarg':\n  echo foo: {{A}} {{B}}\n\nBAR X:\n  echo bar: {{X}}\n\nBAZ +Z:\n  echo baz: {{Z}}\n\",\n    )\n    .stdout(\"bar: 0\\nfoo: 1 2\\nbaz: 3 4 5\\n\")\n    .stderr(\"echo bar: 0\\necho foo: 1 2\\necho baz: 3 4 5\\n\")\n    .success();\n}\n\n#[test]\nfn missing_second_dependency() {\n  Test::new()\n    .justfile(\n      \"\nx:\n\na: x y\n\",\n    )\n    .stderr(\n      \"error: Recipe `a` has unknown dependency `y`\n ——▶ justfile:3:6\n  │\n3 │ a: x y\n  │      ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn list_colors() {\n  Test::new()\n    .arg(\"--color\")\n    .arg(\"always\")\n    .arg(\"--list\")\n    .justfile(\n      \"\n# comment\na B C +D='hello':\n  echo {{B}} {{C}} {{D}}\n\",\n    )\n    .stdout(\n      \"\n    Available recipes:\n        a \\\n    \\u{1b}[36mB\\u{1b}[0m \\u{1b}[36mC\\u{1b}[0m \\u{1b}[35m+\\\n    \\u{1b}[0m\\u{1b}[36mD\\u{1b}[0m=\\u{1b}[32m'hello'\\u{1b}[0m \\\n     \\u{1b}[34m#\\u{1b}[0m \\u{1b}[34mcomment\\u{1b}[0m\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn run_colors() {\n  Test::new()\n    .arg(\"--color\")\n    .arg(\"always\")\n    .arg(\"--highlight\")\n    .arg(\"--verbose\")\n    .justfile(\n      \"\n# comment\na:\n  echo hi\n\",\n    )\n    .stdout(\"hi\\n\")\n    .stderr(\"\\u{1b}[1;36m===> Running recipe `a`...\\u{1b}[0m\\n\\u{1b}[1mecho hi\\u{1b}[0m\\n\")\n    .success();\n}\n\n#[test]\nfn no_highlight() {\n  Test::new()\n    .arg(\"--color\")\n    .arg(\"always\")\n    .arg(\"--highlight\")\n    .arg(\"--no-highlight\")\n    .arg(\"--verbose\")\n    .justfile(\n      \"\n# comment\na:\n  echo hi\n\",\n    )\n    .stdout(\"hi\\n\")\n    .stderr(\"\\u{1b}[1;36m===> Running recipe `a`...\\u{1b}[0m\\necho hi\\n\")\n    .success();\n}\n\n#[test]\nfn trailing_flags() {\n  Test::new()\n    .arg(\"echo\")\n    .arg(\"--some\")\n    .arg(\"--awesome\")\n    .arg(\"--flags\")\n    .justfile(\n      \"\necho A B C:\n  echo {{A}} {{B}} {{C}}\n\",\n    )\n    .stdout(\"--some --awesome --flags\\n\")\n    .stderr(\"echo --some --awesome --flags\\n\")\n    .success();\n}\n\n#[test]\nfn comment_before_variable() {\n  Test::new()\n    .arg(\"echo\")\n    .justfile(\n      \"\n#\nA:='1'\necho:\n  echo {{A}}\n \",\n    )\n    .stdout(\"1\\n\")\n    .stderr(\"echo 1\\n\")\n    .success();\n}\n\n#[test]\nfn invalid_escape_sequence_message() {\n  Test::new()\n    .justfile(\n      r#\"\nX := \"\\'\"\n\"#,\n    )\n    .stderr(\n      r#\"error: `\\'` is not a valid escape sequence\n ——▶ justfile:1:6\n  │\n1 │ X := \"\\'\"\n  │      ^^^^\n\"#,\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_variable_in_default() {\n  Test::new()\n    .justfile(\n      \"\n     foo x=bar:\n   \",\n    )\n    .stderr(\n      r\"error: Variable `bar` not defined\n ——▶ justfile:1:7\n  │\n1 │ foo x=bar:\n  │       ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_function_in_default() {\n  Test::new()\n    .justfile(\n      \"\nfoo x=bar():\n\",\n    )\n    .stderr(\n      r\"error: Call to unknown function `bar`\n ——▶ justfile:1:7\n  │\n1 │ foo x=bar():\n  │       ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn default_string() {\n  Test::new()\n    .justfile(\n      \"\nfoo x='bar':\n  echo {{x}}\n\",\n    )\n    .stdout(\"bar\\n\")\n    .stderr(\"echo bar\\n\")\n    .success();\n}\n\n#[test]\nfn default_concatenation() {\n  Test::new()\n    .justfile(\n      \"\nfoo x=(`echo foo` + 'bar'):\n  echo {{x}}\n\",\n    )\n    .stdout(\"foobar\\n\")\n    .stderr(\"echo foobar\\n\")\n    .success();\n}\n\n#[test]\nfn default_backtick() {\n  Test::new()\n    .justfile(\n      \"\nfoo x=`echo foo`:\n  echo {{x}}\n\",\n    )\n    .stdout(\"foo\\n\")\n    .stderr(\"echo foo\\n\")\n    .success();\n}\n\n#[test]\nfn default_variable() {\n  Test::new()\n    .justfile(\n      \"\ny := 'foo'\nfoo x=y:\n  echo {{x}}\n\",\n    )\n    .stdout(\"foo\\n\")\n    .stderr(\"echo foo\\n\")\n    .success();\n}\n\n#[test]\nfn unterminated_interpolation_eol() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{\n  \",\n    )\n    .stderr(\n      r\"\n    error: Unterminated interpolation\n     ——▶ justfile:2:8\n      │\n    2 │   echo {{\n      │        ^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn unterminated_interpolation_eof() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{\n  \",\n    )\n    .stderr(\n      r\"\n    error: Unterminated interpolation\n     ——▶ justfile:2:8\n      │\n    2 │   echo {{\n      │        ^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_start_of_token() {\n  Test::new()\n    .justfile(\n      \"\nassembly_source_files = %(wildcard src/arch/$(arch)/*.s)\n      \",\n    )\n    .stderr(\n      r\"\n    error: Unknown start of token '%'\n     ——▶ justfile:1:25\n      │\n    1 │ assembly_source_files = %(wildcard src/arch/$(arch)/*.s)\n      │                         ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_start_of_token_invisible_unicode() {\n  Test::new()\n    .justfile(\n      \"\n\\u{200b}foo := 'bar'\n      \",\n    )\n    .stderr(\n      \"\nerror: Unknown start of token '\\u{200b}' (U+200B)\n ——▶ justfile:1:1\n  │\n1 │ \\u{200b}foo := 'bar'\n  │ ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_start_of_token_ascii_control_char() {\n  Test::new()\n    .justfile(\n      \"\n\\0foo := 'bar'\n\",\n    )\n    .stderr(\n      \"\nerror: Unknown start of token '\\0' (U+0000)\n ——▶ justfile:1:1\n  │\n1 │ \\0foo := 'bar'\n  │ ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn backtick_variable_cat() {\n  Test::new()\n    .justfile(\n      \"\nstdin := `cat`\n\ndefault:\n  echo {{stdin}}\n\",\n    )\n    .stdin(\"STDIN\")\n    .stdout(\"STDIN\\n\")\n    .stderr(\"echo STDIN\\n\")\n    .success();\n}\n\n#[test]\nfn backtick_default_cat_stdin() {\n  Test::new()\n    .justfile(\n      \"\ndefault stdin = `cat`:\n  echo {{stdin}}\n\",\n    )\n    .stdin(\"STDIN\")\n    .stdout(\"STDIN\\n\")\n    .stderr(\"echo STDIN\\n\")\n    .success();\n}\n\n#[test]\nfn backtick_default_cat_justfile() {\n  Test::new()\n    .justfile(\n      \"\n    default stdin = `cat justfile`:\n      echo '{{stdin}}'\n  \",\n    )\n    .stdout(\n      \"\n    default stdin = `cat justfile`:\n      echo {{stdin}}\n  \",\n    )\n    .stderr(\n      \"\n    echo 'default stdin = `cat justfile`:\n      echo '{{stdin}}''\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn backtick_variable_read_single() {\n  Test::new()\n    .justfile(\n      \"\npassword := `read PW && echo $PW`\n\ndefault:\n  echo {{password}}\n\",\n    )\n    .stdin(\"foobar\\n\")\n    .stdout(\"foobar\\n\")\n    .stderr(\"echo foobar\\n\")\n    .success();\n}\n\n#[test]\nfn backtick_variable_read_multiple() {\n  Test::new()\n    .justfile(\n      \"\na := `read A && echo $A`\nb := `read B && echo $B`\n\ndefault:\n  echo {{a}}\n  echo {{b}}\n\",\n    )\n    .stdin(\"foo\\nbar\\n\")\n    .stdout(\"foo\\nbar\\n\")\n    .stderr(\"echo foo\\necho bar\\n\")\n    .success();\n}\n\n#[test]\nfn backtick_default_read_multiple() {\n  Test::new()\n    .justfile(\n      \"\n\ndefault a=`read A && echo $A` b=`read B && echo $B`:\n  echo {{a}}\n  echo {{b}}\n\",\n    )\n    .stdin(\"foo\\nbar\\n\")\n    .stdout(\"foo\\nbar\\n\")\n    .stderr(\"echo foo\\necho bar\\n\")\n    .success();\n}\n\n#[test]\nfn old_equals_assignment_syntax_produces_error() {\n  Test::new()\n    .justfile(\n      \"\n    foo = 'bar'\n\n    default:\n      echo {{foo}}\n  \",\n    )\n    .stderr(\n      \"\n    error: Expected '*', ':', '$', identifier, or '+', but found '='\n     ——▶ justfile:1:5\n      │\n    1 │ foo = 'bar'\n      │     ^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn dependency_argument_string() {\n  Test::new()\n    .justfile(\n      \"\n    release: (build 'foo') (build 'bar')\n\n    build target:\n      echo 'Building {{target}}...'\n  \",\n    )\n    .stdout(\"Building foo...\\nBuilding bar...\\n\")\n    .stderr(\"echo 'Building foo...'\\necho 'Building bar...'\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn dependency_argument_parameter() {\n  Test::new()\n    .justfile(\n      \"\n    default: (release '1.0')\n\n    release version: (build 'foo' version) (build 'bar' version)\n\n    build target version:\n      echo 'Building {{target}}@{{version}}...'\n  \",\n    )\n    .stdout(\"Building foo@1.0...\\nBuilding bar@1.0...\\n\")\n    .stderr(\"echo 'Building foo@1.0...'\\necho 'Building bar@1.0...'\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn dependency_argument_function() {\n  Test::new()\n    .justfile(\n      \"\n    foo: (bar env_var_or_default('x', 'y'))\n\n    bar arg:\n      echo {{arg}}\n  \",\n    )\n    .stdout(\"y\\n\")\n    .stderr(\"echo y\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn env_function_as_env_var() {\n  Test::new()\n    .env(\"x\", \"z\")\n    .justfile(\n      \"\n    foo: (bar env('x'))\n\n    bar arg:\n      echo {{arg}}\n  \",\n    )\n    .stdout(\"z\\n\")\n    .stderr(\"echo z\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn env_function_as_env_var_or_default() {\n  Test::new()\n    .env(\"x\", \"z\")\n    .justfile(\n      \"\n    foo: (bar env('x', 'y'))\n\n    bar arg:\n      echo {{arg}}\n  \",\n    )\n    .stdout(\"z\\n\")\n    .stderr(\"echo z\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn env_function_as_env_var_with_existing_env_var() {\n  Test::new()\n    .env(\"x\", \"z\")\n    .justfile(\n      \"\n    foo: (bar env('x'))\n\n    bar arg:\n      echo {{arg}}\n  \",\n    )\n    .stdout(\"z\\n\")\n    .stderr(\"echo z\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn env_function_as_env_var_or_default_with_existing_env_var() {\n  Test::new()\n    .env(\"x\", \"z\")\n    .justfile(\n      \"\n    foo: (bar env('x', 'y'))\n\n    bar arg:\n      echo {{arg}}\n  \",\n    )\n    .stdout(\"z\\n\")\n    .stderr(\"echo z\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn dependency_argument_backtick() {\n  Test::new()\n    .justfile(\n      \"\n    export X := 'X'\n\n    foo: (bar `echo $X`)\n\n    bar arg:\n      echo {{arg}}\n      echo $X\n  \",\n    )\n    .stdout(\"X\\nX\\n\")\n    .stderr(\"echo X\\necho $X\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn dependency_argument_assignment() {\n  Test::new()\n    .justfile(\n      \"\n    v := '1.0'\n\n    default: (release v)\n\n    release version:\n      echo Release {{version}}...\n  \",\n    )\n    .stdout(\"Release 1.0...\\n\")\n    .stderr(\"echo Release 1.0...\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn dependency_argument_plus_variadic() {\n  Test::new()\n    .justfile(\n      \"\n    foo: (bar 'A' 'B' 'C')\n\n    bar +args:\n      echo {{args}}\n  \",\n    )\n    .stdout(\"A B C\\n\")\n    .stderr(\"echo A B C\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn duplicate_dependency_no_args() {\n  Test::new()\n    .justfile(\n      \"\n    foo: bar bar bar bar\n\n    bar:\n      echo BAR\n  \",\n    )\n    .stdout(\"BAR\\n\")\n    .stderr(\"echo BAR\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn duplicate_dependency_argument() {\n  Test::new()\n    .justfile(\n      \"\n    foo: (bar 'BAR') (bar `echo BAR`)\n\n    bar bar:\n      echo {{bar}}\n  \",\n    )\n    .stdout(\"BAR\\n\")\n    .stderr(\"echo BAR\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn pwsh_invocation_directory() {\n  if cfg!(not(windows)) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r#\"\n    set shell := [\"pwsh\", \"-NoProfile\", \"-c\"]\n\n    pwd:\n      @Test-Path {{invocation_directory()}} > result.txt\n  \"#,\n    )\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn variables() {\n  Test::new()\n    .arg(\"--variables\")\n    .justfile(\n      \"\n    z := 'a'\n    a := 'z'\n  \",\n    )\n    .stdout(\"a z\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn interpolation_evaluation_ignore_quiet() {\n  Test::new()\n    .justfile(\n      r#\"\n    foo:\n      {{\"@echo foo 2>/dev/null\"}}\n  \"#,\n    )\n    .stderr(\n      \"\n    @echo foo 2>/dev/null\n    error: Recipe `foo` failed on line 2 with exit code 127\n  \",\n    )\n    .shell(false)\n    .status(127);\n}\n\n#[test]\nfn interpolation_evaluation_ignore_quiet_continuation() {\n  Test::new()\n    .justfile(\n      r#\"\n    foo:\n      {{\"\"}}\\\n      @echo foo 2>/dev/null\n  \"#,\n    )\n    .stderr(\n      \"\n    @echo foo 2>/dev/null\n    error: Recipe `foo` failed on line 3 with exit code 127\n  \",\n    )\n    .shell(false)\n    .status(127);\n}\n\n#[test]\nfn brace_escape() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo '{{{{'\n  \",\n    )\n    .stdout(\"{{\\n\")\n    .stderr(\n      \"\n    echo '{{'\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn brace_escape_extra() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo '{{{{{'\n  \",\n    )\n    .stdout(\"{{{\\n\")\n    .stderr(\n      \"\n    echo '{{{'\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn multi_line_string_in_interpolation() {\n  Test::new()\n    .justfile(\n      \"\n    foo:\n      echo {{'a\n      echo b\n      echo c'}}z\n      echo baz\n  \",\n    )\n    .stdout(\"a\\nb\\ncz\\nbaz\\n\")\n    .stderr(\"echo a\\n  echo b\\n  echo cz\\necho baz\\n\")\n    .success();\n}\n\n#[test]\nfn windows_interpreter_path_no_base() {\n  if cfg!(not(windows)) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\n    foo:\n      #!powershell\n\n      exit 0\n  \",\n    )\n    .success();\n}\n"
  },
  {
    "path": "tests/modules.rs",
    "content": "use super::*;\n\n#[test]\nfn modules_are_stable() {\n  Test::new()\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .write(\"foo.just\", \"@bar:\\n echo ok\")\n    .args([\"foo\", \"bar\"])\n    .stdout(\"ok\\n\")\n    .success();\n}\n\n#[test]\nfn default_recipe_in_submodule_must_have_no_arguments() {\n  Test::new()\n    .write(\"foo.just\", \"foo bar:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo\")\n    .stderr(\"error: Recipe `foo` cannot be used as default recipe since it requires at least 1 argument.\\n\")\n    .failure();\n}\n\n#[test]\nfn module_recipes_can_be_run_as_subcommands() {\n  Test::new()\n    .write(\"foo.just\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"foo\")\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn module_recipes_can_be_run_with_path_syntax() {\n  Test::new()\n    .write(\"foo.just\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo::foo\")\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn nested_module_recipes_can_be_run_with_path_syntax() {\n  Test::new()\n    .write(\"foo.just\", \"mod bar\")\n    .write(\"bar.just\", \"baz:\\n @echo BAZ\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo::bar::baz\")\n    .stdout(\"BAZ\\n\")\n    .success();\n}\n\n#[test]\nfn invalid_path_syntax() {\n  Test::new()\n    .arg(\":foo::foo\")\n    .stderr(\"error: Justfile does not contain recipe `:foo::foo`\\n\")\n    .failure();\n\n  Test::new()\n    .arg(\"foo::foo:\")\n    .stderr(\"error: Justfile does not contain recipe `foo::foo:`\\n\")\n    .failure();\n\n  Test::new()\n    .arg(\"foo:::foo\")\n    .stderr(\"error: Justfile does not contain recipe `foo:::foo`\\n\")\n    .failure();\n}\n\n#[test]\nfn missing_recipe_after_invalid_path() {\n  Test::new()\n    .arg(\":foo::foo\")\n    .arg(\"bar\")\n    .stderr(\"error: Justfile does not contain recipe `:foo::foo`\\n\")\n    .failure();\n}\n\n#[test]\nfn assignments_are_evaluated_in_modules() {\n  Test::new()\n    .write(\"foo.just\", \"bar := 'CHILD'\\nfoo:\\n @echo {{bar}}\")\n    .justfile(\n      \"\n        mod foo\n        bar := 'PARENT'\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"foo\")\n    .stdout(\"CHILD\\n\")\n    .success();\n}\n\n#[test]\nfn module_subcommand_runs_default_recipe() {\n  Test::new()\n    .write(\"foo.just\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo\")\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn modules_can_contain_other_modules() {\n  Test::new()\n    .write(\"bar.just\", \"baz:\\n @echo BAZ\")\n    .write(\"foo.just\", \"mod bar\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"bar\")\n    .arg(\"baz\")\n    .stdout(\"BAZ\\n\")\n    .success();\n}\n\n#[test]\nfn circular_module_imports_are_detected() {\n  Test::new()\n    .write(\"bar.just\", \"mod foo\")\n    .write(\"foo.just\", \"mod bar\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"bar\")\n    .arg(\"baz\")\n    .stderr_regex(path_for_regex(\n      \"error: Import `.*/foo.just` in `.*/bar.just` is circular\\n\",\n    ))\n    .failure();\n}\n\n#[test]\nfn modules_use_module_settings() {\n  Test::new()\n    .write(\n      \"foo.just\",\n      \"set allow-duplicate-recipes\nfoo:\nfoo:\n  @echo FOO\n\",\n    )\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"foo\")\n    .stdout(\"FOO\\n\")\n    .success();\n\n  Test::new()\n    .write(\n      \"foo.just\",\n      \"foo:\nfoo:\n  @echo FOO\n\",\n    )\n    .justfile(\n      \"\n        mod foo\n\n        set allow-duplicate-recipes\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"foo\")\n    .stderr(\n      \"\n      error: Recipe `foo` first defined on line 1 is redefined on line 2\n       ——▶ foo.just:2:1\n        │\n      2 │ foo:\n        │ ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn modules_conflict_with_recipes() {\n  Test::new()\n    .write(\"foo.just\", \"\")\n    .justfile(\n      \"\n        mod foo\n        foo:\n      \",\n    )\n    .stderr(\n      \"\n      error: Module `foo` defined on line 1 is redefined as a recipe on line 2\n       ——▶ justfile:2:1\n        │\n      2 │ foo:\n        │ ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn modules_conflict_with_aliases() {\n  Test::new()\n    .write(\"foo.just\", \"\")\n    .justfile(\n      \"\n        mod foo\n        bar:\n        alias foo := bar\n      \",\n    )\n    .stderr(\n      \"\n      error: Module `foo` defined on line 1 is redefined as an alias on line 3\n       ——▶ justfile:3:7\n        │\n      3 │ alias foo := bar\n        │       ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn modules_conflict_with_other_modules() {\n  Test::new()\n    .write(\"foo.just\", \"\")\n    .justfile(\n      \"\n        mod foo\n        mod foo\n\n        bar:\n      \",\n    )\n    .stderr(\n      \"\n      error: Module `foo` first defined on line 1 is redefined on line 2\n       ——▶ justfile:2:5\n        │\n      2 │ mod foo\n        │     ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn modules_are_dumped_correctly() {\n  Test::new()\n    .write(\"foo.just\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\"mod foo\\n\")\n    .success();\n}\n\n#[test]\nfn optional_modules_are_dumped_correctly() {\n  Test::new()\n    .write(\"foo.just\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod? foo\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\"mod? foo\\n\")\n    .success();\n}\n\n#[test]\nfn modules_can_be_in_subdirectory() {\n  Test::new()\n    .write(\"foo/mod.just\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"foo\")\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn modules_in_subdirectory_can_be_named_justfile() {\n  Test::new()\n    .write(\"foo/justfile\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"foo\")\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn modules_in_subdirectory_can_be_named_justfile_with_any_case() {\n  Test::new()\n    .write(\"foo/JUSTFILE\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"foo\")\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn modules_in_subdirectory_can_have_leading_dot() {\n  Test::new()\n    .write(\"foo/.justfile\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"foo\")\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn modules_require_unambiguous_file() {\n  Test::new()\n    .write(\"foo/justfile\", \"foo:\\n @echo FOO\")\n    .write(\"foo.just\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .stderr(\n      \"\n      error: Found multiple source files for module `foo`: `foo/justfile` and `foo.just`\n       ——▶ justfile:1:5\n        │\n      1 │ mod foo\n        │     ^^^\n      \"\n      .replace('/', MAIN_SEPARATOR_STR),\n    )\n    .failure();\n}\n\n#[test]\nfn missing_module_file_error() {\n  Test::new()\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .stderr(\n      \"\n      error: Could not find source file for module `foo`.\n       ——▶ justfile:1:5\n        │\n      1 │ mod foo\n        │     ^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn missing_optional_modules_do_not_trigger_error() {\n  Test::new()\n    .justfile(\n      \"\n        mod? foo\n\n        bar:\n          @echo BAR\n      \",\n    )\n    .stdout(\"BAR\\n\")\n    .success();\n}\n\n#[test]\nfn missing_optional_modules_do_not_conflict() {\n  Test::new()\n    .justfile(\n      \"\n        mod? foo\n        mod? foo\n        mod foo 'baz.just'\n      \",\n    )\n    .write(\"baz.just\", \"baz:\\n @echo BAZ\")\n    .arg(\"foo\")\n    .arg(\"baz\")\n    .stdout(\"BAZ\\n\")\n    .success();\n}\n\n#[test]\nfn root_dotenv_is_available_to_submodules() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-load\n\n        mod foo\n      \",\n    )\n    .write(\"foo.just\", \"foo:\\n @echo $DOTENV_KEY\")\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .args([\"foo\", \"foo\"])\n    .stdout(\"dotenv-value\\n\")\n    .success();\n}\n\n#[test]\nfn dotenv_settings_in_submodule_are_ignored() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-load\n\n        mod foo\n      \",\n    )\n    .write(\n      \"foo.just\",\n      \"set dotenv-load := false\\nfoo:\\n @echo $DOTENV_KEY\",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .args([\"foo\", \"foo\"])\n    .stdout(\"dotenv-value\\n\")\n    .success();\n}\n\n#[test]\nfn modules_may_specify_path() {\n  Test::new()\n    .write(\"commands/foo.just\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo 'commands/foo.just'\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"foo\")\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn modules_may_specify_path_to_directory() {\n  Test::new()\n    .write(\"commands/bar/mod.just\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo 'commands/bar'\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"foo\")\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn modules_with_paths_are_dumped_correctly() {\n  Test::new()\n    .write(\"commands/foo.just\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo 'commands/foo.just'\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\"mod foo 'commands/foo.just'\\n\")\n    .success();\n}\n\n#[test]\nfn optional_modules_with_paths_are_dumped_correctly() {\n  Test::new()\n    .write(\"commands/foo.just\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod? foo 'commands/foo.just'\n      \",\n    )\n    .arg(\"--dump\")\n    .stdout(\"mod? foo 'commands/foo.just'\\n\")\n    .success();\n}\n\n#[test]\nfn recipes_may_be_named_mod() {\n  Test::new()\n    .justfile(\n      \"\n        mod foo:\n          @echo FOO\n      \",\n    )\n    .arg(\"mod\")\n    .arg(\"bar\")\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn submodule_linewise_recipes_run_in_submodule_directory() {\n  Test::new()\n    .write(\"foo/bar\", \"BAR\")\n    .write(\"foo/mod.just\", \"foo:\\n @cat bar\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"foo\")\n    .stdout(\"BAR\")\n    .success();\n}\n\n#[test]\nfn submodule_shebang_recipes_run_in_submodule_directory() {\n  Test::new()\n    .write(\"foo/bar\", \"BAR\")\n    .write(\"foo/mod.just\", \"foo:\\n #!/bin/sh\\n cat bar\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"foo\")\n    .stdout(\"BAR\")\n    .success();\n}\n\n#[test]\nfn cross_module_dependency_runs_in_submodule_directory() {\n  Test::new()\n    .write(\"foo/bar\", \"BAR\")\n    .write(\"foo/mod.just\", \"foo:\\n @cat bar\")\n    .justfile(\n      \"\n        mod foo\n\n        main: foo::foo\n      \",\n    )\n    .arg(\"main\")\n    .stdout(\"BAR\")\n    .success();\n}\n\n#[test]\nfn cross_module_dependency_with_no_cd_runs_in_invocation_directory() {\n  Test::new()\n    .write(\"root_file\", \"ROOT\")\n    .write(\n      \"foo/mod.just\",\n      \"\n[no-cd]\nfoo:\n  @cat root_file\n      \",\n    )\n    .justfile(\n      \"\n        mod foo\n\n        main: foo::foo\n      \",\n    )\n    .arg(\"main\")\n    .stdout(\"ROOT\")\n    .success();\n}\n\n#[test]\nfn nested_cross_module_dependency_runs_in_correct_directory() {\n  Test::new()\n    .write(\"outer/inner/file\", \"NESTED\")\n    .write(\"outer/inner/mod.just\", \"task:\\n @cat file\")\n    .write(\"outer/mod.just\", \"mod inner\")\n    .justfile(\n      \"\n        mod outer\n\n        main: outer::inner::task\n      \",\n    )\n    .arg(\"main\")\n    .stdout(\"NESTED\")\n    .success();\n}\n\n#[test]\nfn modulepaths_beginning_with_tilde_are_expanded_to_homdir() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .write(\"foobar/mod.just\", \"foo:\\n @echo FOOBAR\")\n    .justfile(\n      \"\n        mod foo '~/mod.just'\n      \",\n    )\n    .arg(\"foo\")\n    .arg(\"foo\")\n    .stdout(\"FOOBAR\\n\")\n    .env(\"HOME\", \"foobar\")\n    .success();\n}\n\n#[test]\nfn recipes_with_same_name_are_both_run() {\n  Test::new()\n    .write(\"foo.just\", \"bar:\\n @echo MODULE\")\n    .justfile(\n      \"\n        mod foo\n\n        bar:\n          @echo ROOT\n      \",\n    )\n    .arg(\"foo::bar\")\n    .arg(\"bar\")\n    .stdout(\"MODULE\\nROOT\\n\")\n    .success();\n}\n\n#[test]\nfn submodule_recipe_not_found_error_message() {\n  Test::new()\n    .args([\"foo::bar\"])\n    .stderr(\"error: Justfile does not contain submodule `foo`\\n\")\n    .failure();\n}\n\n#[test]\nfn submodule_recipe_not_found_spaced_error_message() {\n  Test::new()\n    .write(\"foo.just\", \"bar:\\n @echo MODULE\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .args([\"foo\", \"baz\"])\n    .stderr(\"error: Justfile does not contain recipe `foo baz`\\nDid you mean `bar`?\\n\")\n    .failure();\n}\n\n#[test]\nfn submodule_recipe_not_found_colon_separated_error_message() {\n  Test::new()\n    .write(\"foo.just\", \"bar:\\n @echo MODULE\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .args([\"foo::baz\"])\n    .stderr(\"error: Justfile does not contain recipe `foo::baz`\\nDid you mean `bar`?\\n\")\n    .failure();\n}\n\n#[test]\nfn colon_separated_path_does_not_run_recipes() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          @echo FOO\n\n        bar:\n          @echo BAR\n      \",\n    )\n    .args([\"foo::bar\"])\n    .stderr(\"error: Expected submodule at `foo` but found recipe.\\n\")\n    .failure();\n}\n\n#[test]\nfn expected_submodule_but_found_recipe_in_root_error() {\n  Test::new()\n    .justfile(\"foo:\")\n    .arg(\"foo::baz\")\n    .stderr(\"error: Expected submodule at `foo` but found recipe.\\n\")\n    .failure();\n}\n\n#[test]\nfn expected_submodule_but_found_recipe_in_submodule_error() {\n  Test::new()\n    .justfile(\"mod foo\")\n    .write(\"foo.just\", \"bar:\")\n    .args([\"foo::bar::baz\"])\n    .stderr(\"error: Expected submodule at `foo::bar` but found recipe.\\n\")\n    .failure();\n}\n\n#[test]\nfn colon_separated_path_components_are_not_used_as_arguments() {\n  Test::new()\n    .justfile(\"foo bar:\")\n    .args([\"foo::bar\"])\n    .stderr(\"error: Expected submodule at `foo` but found recipe.\\n\")\n    .failure();\n}\n\n#[test]\nfn comments_can_follow_modules() {\n  Test::new()\n    .write(\"foo.just\", \"foo:\\n @echo FOO\")\n    .justfile(\n      \"\n        mod foo # this is foo\n      \",\n    )\n    .args([\"foo\", \"foo\"])\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn doc_comment_on_module() {\n  Test::new()\n    .write(\"foo.just\", \"\")\n    .justfile(\n      \"\n        # Comment\n        mod foo\n      \",\n    )\n    .test_round_trip(false)\n    .arg(\"--list\")\n    .stdout(\"Available recipes:\\n    foo ... # Comment\\n\")\n    .success();\n}\n\n#[test]\nfn doc_attribute_on_module() {\n  Test::new()\n    .write(\"foo.just\", \"\")\n    .justfile(\n      r#\"\n        # Suppressed comment\n        [doc: \"Comment\"]\n        mod foo\n      \"#,\n    )\n    .test_round_trip(false)\n    .arg(\"--list\")\n    .stdout(\"Available recipes:\\n    foo ... # Comment\\n\")\n    .success();\n}\n\n#[test]\nfn group_attribute_on_module() {\n  Test::new()\n    .write(\"foo.just\", \"\")\n    .write(\"bar.just\", \"\")\n    .write(\"zee.just\", \"\")\n    .justfile(\n      r\"\n        [group('alpha')]\n        mod zee\n\n        [group('alpha')]\n        mod foo\n\n        [group('alpha')]\n        a:\n\n        [group('beta')]\n        b:\n\n        [group('beta')]\n        mod bar\n\n        c:\n      \",\n    )\n    .test_round_trip(false)\n    .arg(\"--list\")\n    .stdout(\n      \"\n        Available recipes:\n            c\n\n            [alpha]\n            a\n            foo ...\n            zee ...\n\n            [beta]\n            b\n            bar ...\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn group_attribute_on_module_unsorted() {\n  Test::new()\n    .write(\"foo.just\", \"\")\n    .write(\"bar.just\", \"\")\n    .write(\"zee.just\", \"\")\n    .justfile(\n      r\"\n        [group('alpha')]\n        mod zee\n\n        [group('alpha')]\n        mod foo\n\n        [group('alpha')]\n        a:\n\n        [group('beta')]\n        b:\n\n        [group('beta')]\n        mod bar\n\n        c:\n      \",\n    )\n    .test_round_trip(false)\n    .arg(\"--list\")\n    .arg(\"--unsorted\")\n    .stdout(\n      \"\n        Available recipes:\n            c\n\n            [alpha]\n            a\n            zee ...\n            foo ...\n\n            [beta]\n            b\n            bar ...\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn group_attribute_on_module_list_submodule() {\n  Test::new()\n    .write(\"foo.just\", \"d:\")\n    .write(\"bar.just\", \"e:\")\n    .write(\"zee.just\", \"f:\")\n    .justfile(\n      r\"\n        [group('alpha')]\n        mod zee\n\n        [group('alpha')]\n        mod foo\n\n        [group('alpha')]\n        a:\n\n        [group('beta')]\n        b:\n\n        [group('beta')]\n        mod bar\n\n        c:\n      \",\n    )\n    .test_round_trip(false)\n    .arg(\"--list\")\n    .arg(\"--list-submodules\")\n    .stdout(\n      \"\n        Available recipes:\n            c\n\n            [alpha]\n            a\n            foo:\n                d\n            zee:\n                f\n\n            [beta]\n            b\n            bar:\n                e\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn group_attribute_on_module_list_submodule_unsorted() {\n  Test::new()\n    .write(\"foo.just\", \"d:\")\n    .write(\"bar.just\", \"e:\")\n    .write(\"zee.just\", \"f:\")\n    .justfile(\n      r\"\n        [group('alpha')]\n        mod zee\n\n        [group('alpha')]\n        mod foo\n\n        [group('alpha')]\n        a:\n\n        [group('beta')]\n        b:\n\n        [group('beta')]\n        mod bar\n\n        c:\n      \",\n    )\n    .test_round_trip(false)\n    .arg(\"--list\")\n    .arg(\"--list-submodules\")\n    .arg(\"--unsorted\")\n    .stdout(\n      \"\n        Available recipes:\n            c\n\n            [alpha]\n            a\n            zee:\n                f\n            foo:\n                d\n\n            [beta]\n            b\n            bar:\n                e\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn bad_module_attribute_fails() {\n  Test::new()\n    .write(\"foo.just\", \"\")\n    .justfile(\n      r\"\n        [no-cd]\n        mod foo\n      \",\n    )\n    .test_round_trip(false)\n    .arg(\"--list\")\n    .stderr(\"error: Module `foo` has invalid attribute `no-cd`\\n ——▶ justfile:2:5\\n  │\\n2 │ mod foo\\n  │     ^^^\\n\")\n    .failure();\n}\n\n#[test]\nfn empty_doc_attribute_on_module() {\n  Test::new()\n    .write(\"foo.just\", \"\")\n    .justfile(\n      \"\n        # Suppressed comment\n        [doc]\n        mod foo\n      \",\n    )\n    .test_round_trip(false)\n    .arg(\"--list\")\n    .stdout(\"Available recipes:\\n    foo ...\\n\")\n    .success();\n}\n\n#[test]\nfn overrides_work_when_submodule_is_present() {\n  Test::new()\n    .write(\"bar.just\", \"\")\n    .justfile(\n      \"\n        mod bar\n\n        x := 'a'\n\n        foo:\n          @echo {{ x }}\n      \",\n    )\n    .test_round_trip(false)\n    .arg(\"x=b\")\n    .stdout(\"b\\n\")\n    .success();\n}\n\n#[test]\nfn exported_variables_are_available_in_submodules() {\n  Test::new()\n    .write(\"foo.just\", \"bar:\\n @echo $x\")\n    .justfile(\n      \"\n        mod foo\n\n        export x := 'a'\n      \",\n    )\n    .test_round_trip(false)\n    .arg(\"foo::bar\")\n    .stdout(\"a\\n\")\n    .success();\n}\n\n#[test]\nfn exported_variables_can_be_unexported_in_submodules() {\n  Test::new()\n    .write(\"foo.just\", \"unexport x\\nbar:\\n @echo ${x:-default}\")\n    .justfile(\n      \"\n        mod foo\n\n        export x := 'a'\n      \",\n    )\n    .test_round_trip(false)\n    .arg(\"foo::bar\")\n    .stdout(\"default\\n\")\n    .success();\n}\n\n#[test]\nfn exported_variables_can_be_overridden_in_submodules() {\n  Test::new()\n    .write(\"foo.just\", \"export x := 'b'\\nbar:\\n @echo $x\")\n    .justfile(\n      \"\n        mod foo\n\n        export x := 'a'\n      \",\n    )\n    .test_round_trip(false)\n    .arg(\"foo::bar\")\n    .stdout(\"b\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/multibyte_char.rs",
    "content": "use super::*;\n\n#[test]\nfn bugfix() {\n  Test::new().justfile(\"foo:\\nx := '''ǩ'''\").success();\n}\n"
  },
  {
    "path": "tests/newline_escape.rs",
    "content": "use super::*;\n\n#[test]\nfn newline_escape_deps() {\n  Test::new()\n    .justfile(\n      \"\n      default: a \\\\\n               b \\\\\n               c\n      a:\n        echo a\n      b:\n        echo b\n      c:\n        echo c\n    \",\n    )\n    .stdout(\"a\\nb\\nc\\n\")\n    .stderr(\"echo a\\necho b\\necho c\\n\")\n    .success();\n}\n\n#[test]\nfn newline_escape_deps_no_indent() {\n  Test::new()\n    .justfile(\n      \"\n      default: a\\\\\n      b\\\\\n      c\n      a:\n        echo a\n      b:\n        echo b\n      c:\n        echo c\n    \",\n    )\n    .stdout(\"a\\nb\\nc\\n\")\n    .stderr(\"echo a\\necho b\\necho c\\n\")\n    .success();\n}\n\n#[test]\nfn newline_escape_deps_linefeed() {\n  Test::new()\n    .justfile(\n      \"\n        default: a\\\\\\r\n                b\n        a:\n          echo a\n        b:\n          echo b\n      \",\n    )\n    .stdout(\"a\\nb\\n\")\n    .stderr(\"echo a\\necho b\\n\")\n    .success();\n}\n\n#[test]\nfn newline_escape_deps_invalid_esc() {\n  Test::new()\n    .justfile(\n      \"\n      default: a\\\\ b\n    \",\n    )\n    .stderr(\n      \"\n        error: `\\\\ ` is not a valid escape sequence\n         ——▶ justfile:1:11\n          │\n        1 │ default: a\\\\ b\n          │           ^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn newline_escape_unpaired_linefeed() {\n  Test::new()\n    .justfile(\n      \"\n      default:\\\\\\ra\",\n    )\n    .stderr(\n      \"\n        error: Unpaired carriage return\n         ——▶ justfile:1:9\n          │\n        1 │ default:\\\\\\ra\n          │         ^\n      \",\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/no_aliases.rs",
    "content": "use super::*;\n\n#[test]\nfn skip_alias() {\n  Test::new()\n    .justfile(\n      \"\n      alias t := test1\n\n      test1:\n        @echo 'test1'\n\n      test2:\n        @echo 'test2'\n      \",\n    )\n    .args([\"--no-aliases\", \"--list\"])\n    .stdout(\"Available recipes:\\n    test1\\n    test2\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/no_cd.rs",
    "content": "use super::*;\n\n#[test]\nfn linewise() {\n  Test::new()\n    .justfile(\n      \"\n      [no-cd]\n      foo:\n        cat bar\n    \",\n    )\n    .current_dir(\"foo\")\n    .tree(tree! {\n      foo: {\n        bar: \"hello\",\n      }\n    })\n    .stderr(\"cat bar\\n\")\n    .stdout(\"hello\")\n    .success();\n}\n\n#[test]\nfn shebang() {\n  Test::new()\n    .justfile(\n      \"\n      [no-cd]\n      foo:\n        #!/bin/sh\n        cat bar\n    \",\n    )\n    .current_dir(\"foo\")\n    .tree(tree! {\n      foo: {\n        bar: \"hello\",\n      }\n    })\n    .stdout(\"hello\")\n    .success();\n}\n"
  },
  {
    "path": "tests/no_dependencies.rs",
    "content": "use super::*;\n\n#[test]\nfn skip_normal_dependency() {\n  Test::new()\n    .justfile(\n      \"\n        a:\n          @echo 'a'\n        b: a\n          @echo 'b'\n        \",\n    )\n    .args([\"--no-deps\", \"b\"])\n    .stdout(\"b\\n\")\n    .success();\n}\n\n#[test]\nfn skip_prior_dependency() {\n  Test::new()\n    .justfile(\n      \"\n        a:\n            @echo 'a'\n        b: && a\n            @echo 'b'\n        \",\n    )\n    .args([\"--no-deps\", \"b\"])\n    .stdout(\"b\\n\")\n    .success();\n}\n\n#[test]\nfn skip_dependency_multi() {\n  Test::new()\n    .justfile(\n      \"\n          a:\n              @echo 'a'\n          b: && a\n              @echo 'b'\n          \",\n    )\n    .args([\"--no-deps\", \"b\", \"a\"])\n    .stdout(\"b\\na\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/no_exit_message.rs",
    "content": "use super::*;\n\n#[test]\nfn recipe_exit_message_suppressed() {\n  Test::new()\n    .justfile(\n      \"\n      # This is a doc comment\n      [no-exit-message]\n      hello:\n        @echo 'Hello, World!'\n        @exit 100\n      \",\n    )\n    .stdout(\"Hello, World!\\n\")\n    .status(100);\n}\n\n#[test]\nfn silent_recipe_exit_message_suppressed() {\n  Test::new()\n    .justfile(\n      \"\n      # This is a doc comment\n      [no-exit-message]\n      @hello:\n        echo 'Hello, World!'\n        exit 100\n      \",\n    )\n    .stdout(\"Hello, World!\\n\")\n    .status(100);\n}\n\n#[test]\nfn recipe_has_doc_comment() {\n  Test::new()\n    .justfile(\n      \"\n    # This is a doc comment\n    [no-exit-message]\n    hello:\n      @exit 100\n        \",\n    )\n    .arg(\"--list\")\n    .stdout(\n      \"\n      Available recipes:\n          hello # This is a doc comment\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn unknown_attribute() {\n  Test::new()\n    .justfile(\n      \"\n      # This is a doc comment\n      [unknown-attribute]\n      hello:\n        @exit 100\n    \",\n    )\n    .stderr(\n      \"\n      error: Unknown attribute `unknown-attribute`\n       ——▶ justfile:2:2\n        │\n      2 │ [unknown-attribute]\n        │  ^^^^^^^^^^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn empty_attribute() {\n  Test::new()\n    .justfile(\n      \"\n      # This is a doc comment\n      []\n      hello:\n        @exit 100\n    \",\n    )\n    .stderr(\n      \"\n      error: Expected identifier, but found ']'\n       ——▶ justfile:2:2\n        │\n      2 │ []\n        │  ^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn extraneous_attribute_before_comment() {\n  Test::new()\n    .justfile(\n      \"\n      [no-exit-message]\n      # This is a doc comment\n      hello:\n        @exit 100\n    \",\n    )\n    .stderr(\n      \"\n      error: Extraneous attribute\n       ——▶ justfile:1:1\n        │\n      1 │ [no-exit-message]\n        │ ^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn extraneous_attribute_before_empty_line() {\n  Test::new()\n    .justfile(\n      \"\n      [no-exit-message]\n\n      hello:\n        @exit 100\n    \",\n    )\n    .stderr(\n      \"\n      error: Extraneous attribute\n       ——▶ justfile:1:1\n        │\n      1 │ [no-exit-message]\n        │ ^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn shebang_exit_message_suppressed() {\n  Test::new()\n    .justfile(\n      \"\n      [no-exit-message]\n      hello:\n        #!/usr/bin/env bash\n        echo 'Hello, World!'\n        exit 100\n    \",\n    )\n    .stdout(\"Hello, World!\\n\")\n    .status(100);\n}\n\n#[test]\nfn no_exit_message() {\n  Test::new()\n    .justfile(\n      \"\n      [no-exit-message]\n      @hello:\n        echo 'Hello, World!'\n        exit 100\n    \",\n    )\n    .stdout(\"Hello, World!\\n\")\n    .status(100);\n}\n\n#[test]\nfn exit_message() {\n  Test::new()\n    .justfile(\n      \"\n      [exit-message]\n      @hello:\n        echo 'Hello, World!'\n        exit 100\n    \",\n    )\n    .stdout(\"Hello, World!\\n\")\n    .stderr(\"error: Recipe `hello` failed on line 4 with exit code 100\\n\")\n    .status(100);\n}\n\n#[test]\nfn recipe_exit_message_setting_suppressed() {\n  Test::new()\n    .justfile(\n      \"\n      set no-exit-message\n\n      # This is a doc comment\n      hello:\n        @echo 'Hello, World!'\n        @exit 100\n    \",\n    )\n    .stdout(\"Hello, World!\\n\")\n    .status(100);\n}\n\n#[test]\nfn shebang_exit_message_setting_suppressed() {\n  Test::new()\n    .justfile(\n      \"\n      set no-exit-message\n\n      hello:\n        #!/usr/bin/env bash\n        echo 'Hello, World!'\n        exit 100\n    \",\n    )\n    .stdout(\"Hello, World!\\n\")\n    .status(100);\n}\n\n#[test]\nfn exit_message_override_no_exit_setting() {\n  Test::new()\n    .justfile(\n      \"\n      set no-exit-message\n\n      [exit-message]\n      fail:\n        @exit 100\n    \",\n    )\n    .stderr(\"error: Recipe `fail` failed on line 5 with exit code 100\\n\")\n    .status(100);\n}\n\n#[test]\nfn exit_message_and_no_exit_message_compile_forbidden() {\n  Test::new()\n    .justfile(\n      \"\n      [exit-message, no-exit-message]\n      bar:\n    \",\n    )\n    .stderr(\n      \"\n        error: Recipe `bar` has both `[exit-message]` and `[no-exit-message]` attributes\n         ——▶ justfile:2:1\n          │\n        2 │ bar:\n          │ ^^^\n      \",\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/options.rs",
    "content": "use super::*;\n\n#[test]\nfn long_options_may_not_be_empty() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='')]\n        @foo bar:\n          echo bar={{bar}}\n      \",\n    )\n    .stderr(\n      \"\n        error: Option name for parameter `bar` is empty\n         ——▶ justfile:1:18\n          │\n        1 │ [arg('bar', long='')]\n          │                  ^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn short_options_may_not_be_empty() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', short='')]\n        @foo bar:\n          echo bar={{bar}}\n      \",\n    )\n    .stderr(\n      \"\n        error: Option name for parameter `bar` is empty\n         ——▶ justfile:1:19\n          │\n        1 │ [arg('bar', short='')]\n          │                   ^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn short_options_may_not_have_multiple_characters() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', short='abc')]\n        @foo bar:\n          echo bar={{bar}}\n      \",\n    )\n    .stderr(\n      \"\n        error: Short option name for parameter `bar` contains multiple characters\n         ——▶ justfile:1:19\n          │\n        1 │ [arg('bar', short='abc')]\n          │                   ^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn parameters_may_be_passed_with_long_options() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar')]\n        @foo bar:\n          echo bar={{bar}}\n      \",\n    )\n    .args([\"foo\", \"--bar\", \"baz\"])\n    .stdout(\"bar=baz\\n\")\n    .success();\n}\n\n#[test]\nfn long_option_defaults_to_parameter_name() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long)]\n        @foo bar:\n          echo bar={{bar}}\n      \",\n    )\n    .args([\"foo\", \"--bar\", \"baz\"])\n    .stdout(\"bar=baz\\n\")\n    .success();\n}\n\n#[test]\nfn parameters_may_be_passed_with_short_options() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', short='b')]\n        @foo bar:\n          echo bar={{bar}}\n      \",\n    )\n    .args([\"foo\", \"-b\", \"baz\"])\n    .stdout(\"bar=baz\\n\")\n    .success();\n}\n\nconst LONG_SHORT: &str = \"\n  [arg('bar', long='bar', short='b')]\n  @foo bar:\n    echo bar={{bar}}\n\";\n\n#[test]\nfn parameters_with_both_long_and_short_option_may_be_passed_as_long() {\n  Test::new()\n    .justfile(LONG_SHORT)\n    .args([\"foo\", \"--bar\", \"baz\"])\n    .stdout(\"bar=baz\\n\")\n    .success();\n}\n\n#[test]\nfn parameters_with_both_long_and_short_option_may_be_passed_as_short() {\n  Test::new()\n    .justfile(LONG_SHORT)\n    .args([\"foo\", \"-b\", \"baz\"])\n    .stdout(\"bar=baz\\n\")\n    .success();\n}\n\n#[test]\nfn parameters_with_both_long_and_short_may_not_use_both() {\n  Test::new()\n    .justfile(LONG_SHORT)\n    .args([\"foo\", \"--bar\", \"baz\", \"-b\", \"baz\"])\n    .stderr(\"error: Recipe `foo` option `-b` cannot be passed more than once\\n\")\n    .failure();\n}\n\n#[test]\nfn multiple_short_options_in_one_argument_is_an_error() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', short='a')]\n        [arg('baz', short='b')]\n        @foo bar baz:\n      \",\n    )\n    .args([\"foo\", \"-ab\"])\n    .stderr(\"error: Passing multiple short options (`-ab`) in one argument is not supported\\n\")\n    .failure();\n}\n\n#[test]\nfn duplicate_long_option_attributes_are_forbidden() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar')]\n        [arg('baz', long='bar')]\n        foo bar baz:\n      \",\n    )\n    .stderr(\n      \"\n        error: Recipe `foo` defines option `--bar` multiple times\n         ——▶ justfile:2:18\n          │\n        2 │ [arg('baz', long='bar')]\n          │                  ^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn defaulted_duplicate_long_option() {\n  Test::new()\n    .justfile(\n      \"\n        [arg(\n          'aaa',\n          long='bar'\n        )]\n        [arg(      'bar', long)]\n        foo aaa bar:\n      \",\n    )\n    .stderr(\n      \"\n        error: Recipe `foo` defines option `--bar` multiple times\n         ——▶ justfile:5:19\n          │\n        5 │ [arg(      'bar', long)]\n          │                   ^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn duplicate_short_option_attributes_are_forbidden() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', short='b')]\n        [arg('baz', short='b')]\n        foo bar baz:\n      \",\n    )\n    .stderr(\n      \"\n        error: Recipe `foo` defines option `-b` multiple times\n         ——▶ justfile:2:19\n          │\n        2 │ [arg('baz', short='b')]\n          │                   ^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn variadics_with_long_options_are_forbidden() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar')]\n        foo +bar:\n      \",\n    )\n    .stderr(\n      \"\n        error: Variadic parameters may not be options\n         ——▶ justfile:2:6\n          │\n        2 │ foo +bar:\n          │      ^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn variadics_with_short_options_are_forbidden() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', short='b')]\n        foo +bar:\n      \",\n    )\n    .stderr(\n      \"\n        error: Variadic parameters may not be options\n         ——▶ justfile:2:6\n          │\n        2 │ foo +bar:\n          │      ^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn long_option_names_may_not_contain_equal_sign() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar=baz')]\n        foo bar:\n      \",\n    )\n    .stderr(\n      \"\n        error: Option name for parameter `bar` contains equal sign\n         ——▶ justfile:1:18\n          │\n        1 │ [arg('bar', long='bar=baz')]\n          │                  ^^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn short_option_names_may_not_contain_equal_sign() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', short='=')]\n        foo bar:\n      \",\n    )\n    .stderr(\n      \"\n        error: Option name for parameter `bar` contains equal sign\n         ——▶ justfile:1:19\n          │\n        1 │ [arg('bar', short='=')]\n          │                   ^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn long_options_may_follow_an_omitted_positional_argument() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('baz', long='baz')]\n        @foo bar='BAR' baz:\n          echo bar={{bar}}\n          echo baz={{baz}}\n      \",\n    )\n    .args([\"foo\", \"--baz\", \"BAZ\"])\n    .stdout(\n      \"\n        bar=BAR\n        baz=BAZ\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn short_options_may_follow_an_omitted_positional_argument() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('baz', short='b')]\n        @foo bar='BAR' baz:\n          echo bar={{bar}}\n          echo baz={{baz}}\n      \",\n    )\n    .args([\"foo\", \"-b\", \"BAZ\"])\n    .stdout(\n      \"\n        bar=BAR\n        baz=BAZ\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn options_with_a_default_may_be_omitted() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar')]\n        @foo bar='BAR':\n          echo bar={{bar}}\n      \",\n    )\n    .args([\"foo\"])\n    .stdout(\n      \"\n        bar=BAR\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn variadics_can_follow_options() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar')]\n        @foo bar *baz:\n          echo bar={{bar}}\n          echo baz={{baz}}\n      \",\n    )\n    .args([\"foo\", \"--bar=BAR\", \"A\", \"B\", \"C\"])\n    .stdout(\n      \"\n        bar=BAR\n        baz=A B C\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn argument_values_starting_with_dashes_are_accepted_if_recipe_does_not_take_options() {\n  Test::new()\n    .justfile(\n      \"\n        @foo *baz:\n          echo baz={{baz}}\n      \",\n    )\n    .args([\"foo\", \"--bar=BAR\", \"--A\", \"--B\", \"--C\"])\n    .stdout(\n      \"\n        baz=--bar=BAR --A --B --C\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn argument_values_starting_with_dashes_are_an_error_if_recipe_takes_options() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar')]\n        @foo bar *baz:\n          echo bar={{bar}}\n          echo baz={{baz}}\n      \",\n    )\n    .args([\"foo\", \"--bar=BAR\", \"--A\", \"--B\", \"--C\"])\n    .stderr(\"error: Recipe `foo` does not have option `--A`\\n\")\n    .failure();\n}\n\n#[test]\nfn argument_values_starting_with_dashes_are_accepted_after_double_dash() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar')]\n        @foo bar *baz:\n          echo bar={{bar}}\n          echo baz={{baz}}\n      \",\n    )\n    .args([\"foo\", \"--bar=BAR\", \"--\", \"--A\", \"--B\", \"--C\"])\n    .stdout(\n      \"\n        bar=BAR\n        baz=--A --B --C\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn positional_and_long_option_arguments_can_be_intermixed() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('b', long='b')]\n        [arg('d', long='d')]\n        @foo a b c d e:\n          echo a={{a}}\n          echo b={{b}}\n          echo c={{c}}\n          echo d={{d}}\n          echo e={{e}}\n      \",\n    )\n    .args([\"foo\", \"A\", \"--d\", \"D\", \"C\", \"--b\", \"B\", \"E\"])\n    .stdout(\n      \"\n        a=A\n        b=B\n        c=C\n        d=D\n        e=E\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn positional_and_short_option_arguments_can_be_intermixed() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('b', short='b')]\n        [arg('d', short='d')]\n        @foo a b c d e:\n          echo a={{a}}\n          echo b={{b}}\n          echo c={{c}}\n          echo d={{d}}\n          echo e={{e}}\n      \",\n    )\n    .args([\"foo\", \"A\", \"-d\", \"D\", \"C\", \"-b\", \"B\", \"E\"])\n    .stdout(\n      \"\n        a=A\n        b=B\n        c=C\n        d=D\n        e=E\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn unknown_options_are_an_error() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar')]\n        @foo bar:\n      \",\n    )\n    .args([\"foo\", \"--baz\", \"BAZ\"])\n    .stderr(\"error: Recipe `foo` does not have option `--baz`\\n\")\n    .failure();\n}\n\n#[test]\nfn missing_required_options_are_an_error() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar')]\n        @foo bar:\n      \",\n    )\n    .arg(\"foo\")\n    .stderr(\"error: Recipe `foo` requires option `--bar`\\n\")\n    .failure();\n}\n\n#[test]\nfn duplicate_long_options_are_an_error() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar')]\n        @foo bar:\n      \",\n    )\n    .args([\"foo\", \"--bar=a\", \"--bar=b\"])\n    .stderr(\"error: Recipe `foo` option `--bar` cannot be passed more than once\\n\")\n    .failure();\n}\n\n#[test]\nfn duplicate_short_options_are_an_error() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', short='b')]\n        @foo bar:\n      \",\n    )\n    .args([\"foo\", \"-b=a\", \"-b=b\"])\n    .stderr(\"error: Recipe `foo` option `-b` cannot be passed more than once\\n\")\n    .failure();\n}\n\n#[test]\nfn options_require_value() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar')]\n        @foo bar:\n      \",\n    )\n    .args([\"foo\", \"--bar\"])\n    .stderr(\"error: Recipe `foo` option `--bar` missing value\\n\")\n    .failure();\n}\n\n#[test]\nfn recipes_with_long_options_have_correct_positional_argument_mismatch_message() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar')]\n        @foo bar baz:\n      \",\n    )\n    .args([\"foo\", \"--bar=value\"])\n    .stderr(\n      \"\n        error: Recipe `foo` got 0 positional arguments but takes 1\n        usage:\n            just foo [OPTIONS] baz\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn recipes_with_short_options_have_correct_positional_argument_mismatch_message() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', short='b')]\n        @foo bar baz:\n      \",\n    )\n    .args([\"foo\", \"-b=value\"])\n    .stderr(\n      \"\n        error: Recipe `foo` got 0 positional arguments but takes 1\n        usage:\n            just foo [OPTIONS] baz\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn long_options_with_values_are_flags() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', long='bar', value='baz')]\n        @foo bar:\n          echo bar={{bar}}\n      \",\n    )\n    .args([\"foo\", \"--bar\"])\n    .stdout(\"bar=baz\\n\")\n    .success();\n}\n\n#[test]\nfn short_options_with_values_are_flags() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', short='b', value='baz')]\n        @foo bar:\n          echo bar={{bar}}\n      \",\n    )\n    .args([\"foo\", \"-b\"])\n    .stdout(\"bar=baz\\n\")\n    .success();\n}\n\n#[test]\nfn flags_cannot_take_values() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', short='b', value='baz')]\n        @foo bar:\n      \",\n    )\n    .args([\"foo\", \"-b=hello\"])\n    .stderr(\"error: Recipe `foo` flag `-b` does not take value\\n\")\n    .failure();\n}\n\n#[test]\nfn value_requires_long_or_short() {\n  Test::new()\n    .justfile(\n      \"\n        [arg('bar', value='baz')]\n        @foo bar:\n      \",\n    )\n    .args([\"foo\", \"-b=hello\"])\n    .stderr(\n      \"\n        error: Argument attribute `value` only valid with `long` or `short`\n         ——▶ justfile:1:13\n          │\n        1 │ [arg('bar', value='baz')]\n          │             ^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn options_arg_passed_as_positional_arguments() {\n  Test::new()\n    .justfile(\n      r#\"\nset positional-arguments\n\n[arg('bar', short='b')]\n@foo bar:\n  echo args=\"$@\"\n      \"#,\n    )\n    .args([\"foo\", \"-b\", \"baz\"])\n    .stdout(\"args=baz\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/os_attributes.rs",
    "content": "use super::*;\n\n#[test]\nfn os_family() {\n  Test::new()\n    .justfile(\n      \"\n      [unix]\n      foo:\n        echo bar\n\n      [windows]\n      foo:\n        echo baz\n    \",\n    )\n    .stdout(if cfg!(unix) {\n      \"bar\\n\"\n    } else if cfg!(windows) {\n      \"baz\\n\"\n    } else {\n      panic!(\"unexpected os family\")\n    })\n    .stderr(if cfg!(unix) {\n      \"echo bar\\n\"\n    } else if cfg!(windows) {\n      \"echo baz\\n\"\n    } else {\n      panic!(\"unexpected os family\")\n    })\n    .success();\n}\n\n#[test]\nfn os() {\n  Test::new()\n    .justfile(\n      \"\n      [macos]\n      foo:\n        echo bar\n\n      [windows]\n      foo:\n        echo baz\n\n      [linux]\n      foo:\n        echo quxx\n\n      [openbsd]\n      foo:\n        echo bob\n\n      [freebsd]\n      foo:\n        echo corge\n\n      [dragonfly]\n      foo:\n        echo grault\n\n      [netbsd]\n      foo:\n        echo garply\n    \",\n    )\n    .stdout(if cfg!(target_os = \"macos\") {\n      \"bar\\n\"\n    } else if cfg!(windows) {\n      \"baz\\n\"\n    } else if cfg!(target_os = \"linux\") {\n      \"quxx\\n\"\n    } else if cfg!(target_os = \"openbsd\") {\n      \"bob\\n\"\n    } else if cfg!(target_os = \"freebsd\") {\n      \"corge\\n\"\n    } else if cfg!(target_os = \"dragonfly\") {\n      \"grault\\n\"\n    } else if cfg!(target_os = \"netbsd\") {\n      \"garply\\n\"\n    } else {\n      panic!(\"unexpected os family\")\n    })\n    .stderr(if cfg!(target_os = \"macos\") {\n      \"echo bar\\n\"\n    } else if cfg!(windows) {\n      \"echo baz\\n\"\n    } else if cfg!(target_os = \"linux\") {\n      \"echo quxx\\n\"\n    } else if cfg!(target_os = \"openbsd\") {\n      \"echo bob\\n\"\n    } else if cfg!(target_os = \"freebsd\") {\n      \"echo corge\\n\"\n    } else if cfg!(target_os = \"dragonfly\") {\n      \"echo grault\\n\"\n    } else if cfg!(target_os = \"netbsd\") {\n      \"echo garply\\n\"\n    } else {\n      panic!(\"unexpected os family\")\n    })\n    .success();\n}\n\n#[test]\nfn all() {\n  Test::new()\n    .justfile(\n      \"\n      [linux]\n      [macos]\n      [openbsd]\n      [freebsd]\n      [dragonfly]\n      [netbsd]\n      [unix]\n      [windows]\n      foo:\n        echo bar\n    \",\n    )\n    .stdout(\"bar\\n\")\n    .stderr(\"echo bar\\n\")\n    .success();\n}\n\n#[test]\nfn none() {\n  Test::new()\n    .justfile(\n      \"\n      foo:\n        echo bar\n    \",\n    )\n    .stdout(\"bar\\n\")\n    .stderr(\"echo bar\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/overrides.rs",
    "content": "use super::*;\n\n#[test]\nfn unknown_override() {\n  Test::new()\n    .justfile(\n      \"\n        a:\n          echo {{`f() { return 100; }; f`}}\n      \",\n    )\n    .args([\"foo=bar\", \"baz=bob\", \"a\"])\n    .stderr(\n      \"\n        error: Variables `baz` and `foo` overridden on the command line but not present in justfile\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_override_options() {\n  Test::new()\n    .arg(\"--set\")\n    .arg(\"foo\")\n    .arg(\"bar\")\n    .arg(\"--set\")\n    .arg(\"baz\")\n    .arg(\"bob\")\n    .arg(\"--set\")\n    .arg(\"a\")\n    .arg(\"b\")\n    .arg(\"a\")\n    .arg(\"b\")\n    .justfile(\n      \"foo:\n echo hello\n echo {{`exit 111`}}\na := `exit 222`\",\n    )\n    .stderr(\n      \"error: Variables `baz` and `foo` overridden on the command line but not present \\\n    in justfile\\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_override_args() {\n  Test::new()\n    .arg(\"foo=bar\")\n    .arg(\"baz=bob\")\n    .arg(\"a=b\")\n    .arg(\"a\")\n    .arg(\"b\")\n    .justfile(\n      \"foo:\n echo hello\n echo {{`exit 111`}}\na := `exit 222`\",\n    )\n    .stderr(\n      \"error: Variables `baz` and `foo` overridden on the command line but not present \\\n    in justfile\\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_override_arg() {\n  Test::new()\n    .arg(\"foo=bar\")\n    .arg(\"a=b\")\n    .arg(\"a\")\n    .arg(\"b\")\n    .justfile(\n      \"foo:\n echo hello\n echo {{`exit 111`}}\na := `exit 222`\",\n    )\n    .stderr(\"error: Variable `foo` overridden on the command line but not present in justfile\\n\")\n    .failure();\n}\n\n#[test]\nfn overrides_first() {\n  Test::new()\n    .arg(\"foo=bar\")\n    .arg(\"a=b\")\n    .arg(\"recipe\")\n    .arg(\"baz=bar\")\n    .justfile(\n      r#\"\nfoo := \"foo\"\na := \"a\"\nbaz := \"baz\"\n\nrecipe arg:\n echo arg={{arg}}\n echo {{foo + a + baz}}\"#,\n    )\n    .stdout(\"arg=baz=bar\\nbarbbaz\\n\")\n    .stderr(\"echo arg=baz=bar\\necho barbbaz\\n\")\n    .success();\n}\n\n#[test]\nfn overrides_not_evaluated() {\n  Test::new()\n    .arg(\"foo=bar\")\n    .arg(\"a=b\")\n    .arg(\"recipe\")\n    .arg(\"baz=bar\")\n    .justfile(\n      r#\"\nfoo := `exit 1`\na := \"a\"\nbaz := \"baz\"\n\nrecipe arg:\n echo arg={{arg}}\n echo {{foo + a + baz}}\"#,\n    )\n    .stdout(\"arg=baz=bar\\nbarbbaz\\n\")\n    .stderr(\"echo arg=baz=bar\\necho barbbaz\\n\")\n    .success();\n}\n\n#[test]\nfn invalid_override_path_set() {\n  Test::new()\n    .arg(\"--set\")\n    .arg(\"0::foo\")\n    .arg(\"bar\")\n    .stderr(\"error: Invalid override path `0::foo`\\n\")\n    .failure();\n}\n\n#[test]\nfn invalid_override_path_positional() {\n  Test::new()\n    .arg(\"0::foo=bar\")\n    .stderr(\"error: Invalid override path `0::foo`\\n\")\n    .failure();\n}\n\n#[test]\nfn unknown_variable_in_submodule_override() {\n  Test::new()\n    .justfile(\"mod foo\")\n    .write(\"foo.just\", \"bar:\\n @echo bar\")\n    .arg(\"foo::x=b\")\n    .arg(\"foo::bar\")\n    .stderr(\"error: Variable `foo::x` overridden on the command line but not present in justfile\\n\")\n    .failure();\n}\n\n#[test]\nfn override_variable_in_submodule() {\n  Test::new()\n    .justfile(\"mod foo\")\n    .write(\"foo.just\", \"x := 'a'\\nbar:\\n @echo {{x}}\")\n    .arg(\"foo::x=b\")\n    .arg(\"foo::bar\")\n    .stdout(\"b\\n\")\n    .success();\n}\n\n#[test]\nfn override_variable_in_nested_submodule() {\n  Test::new()\n    .justfile(\"mod foo\")\n    .write(\"foo/mod.just\", \"mod bar\")\n    .write(\"foo/bar.just\", \"x := 'a'\\nbaz:\\n @echo {{x}}\")\n    .arg(\"foo::bar::x=b\")\n    .arg(\"foo::bar::baz\")\n    .stdout(\"b\\n\")\n    .success();\n}\n\n#[test]\nfn override_variable_used_in_setting() {\n  Test::new()\n    .justfile(\n      \"\n        dir := 'foo'\n        set working-directory := dir\n        bar:\n          @cat file.txt\n      \",\n    )\n    .write(\"baz/file.txt\", \"BAZ\")\n    .arg(\"dir=baz\")\n    .arg(\"bar\")\n    .stdout(\"BAZ\")\n    .success();\n}\n\n#[test]\nfn submodule_override_does_not_affect_parent() {\n  Test::new()\n    .justfile(\n      \"\n        mod foo\n        x := 'root'\n        bar:\n          @echo {{x}}\n      \",\n    )\n    .write(\"foo.just\", \"x := 'a'\\nbaz:\\n @echo {{x}}\")\n    .arg(\"foo::x=b\")\n    .arg(\"bar\")\n    .stdout(\"root\\n\")\n    .success();\n}\n\n#[test]\nfn unknown_submodule_in_override_path() {\n  Test::new()\n    .arg(\"foo::x=b\")\n    .stderr(\"error: Variable `foo::x` overridden on the command line but not present in justfile\\n\")\n    .failure();\n}\n\n#[test]\nfn submodule_override_not_evaluated() {\n  Test::new()\n    .justfile(\"mod foo\")\n    .write(\"foo.just\", \"x := `exit 1`\\nbar:\\n @echo {{x}}\")\n    .arg(\"foo::x=b\")\n    .arg(\"foo::bar\")\n    .stdout(\"b\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/parallel.rs",
    "content": "use super::*;\n\n#[test]\n#[ignore]\nfn prior_dependencies_run_in_parallel() {\n  let start = Instant::now();\n\n  Test::new()\n    .justfile(\n      \"\n        [parallel]\n        foo: a b c d e\n\n        a:\n          sleep 1\n\n        b:\n          sleep 1\n\n        c:\n          sleep 1\n\n        d:\n          sleep 1\n\n        e:\n          sleep 1\n      \",\n    )\n    .stderr(\n      \"\n        sleep 1\n        sleep 1\n        sleep 1\n        sleep 1\n        sleep 1\n      \",\n    )\n    .success();\n\n  assert!(start.elapsed() < Duration::from_secs(2));\n}\n\n#[test]\n#[ignore]\nfn subsequent_dependencies_run_in_parallel() {\n  let start = Instant::now();\n\n  Test::new()\n    .justfile(\n      \"\n        [parallel]\n        foo: && a b c d e\n\n        a:\n          sleep 1\n\n        b:\n          sleep 1\n\n        c:\n          sleep 1\n\n        d:\n          sleep 1\n\n        e:\n          sleep 1\n      \",\n    )\n    .stderr(\n      \"\n        sleep 1\n        sleep 1\n        sleep 1\n        sleep 1\n        sleep 1\n      \",\n    )\n    .success();\n\n  assert!(start.elapsed() < Duration::from_secs(2));\n}\n\n#[test]\nfn parallel_dependencies_report_errors() {\n  Test::new()\n    .justfile(\n      \"\n        [parallel]\n        foo: bar\n\n        bar:\n          exit 1\n      \",\n    )\n    .stderr(\n      \"\n        exit 1\n        error: Recipe `bar` failed on line 5 with exit code 1\n      \",\n    )\n    .failure();\n}\n\n#[test]\n#[ignore]\nfn dependents_block_on_running_dependencies() {\n  Test::new()\n    .justfile(\n      \"\n        set quiet\n\n        [parallel]\n        a: b c\n          echo a\n\n        b: x\n          echo b\n\n        c: x\n          echo c\n\n        x:\n          sleep 1\n          echo x\n      \",\n    )\n    .stdout_regex(\n      r\"(?x)\n      x\\n\n      (\n        b\\nc\\n\n        |\n        c\\nb\\n\n      )\n      a\\n\",\n    )\n    .success();\n}\n"
  },
  {
    "path": "tests/parameters.rs",
    "content": "use super::*;\n\n#[test]\nfn parameter_default_values_may_use_earlier_parameters() {\n  Test::new()\n    .justfile(\n      \"\n        @foo a b=a:\n          echo {{ b }}\n      \",\n    )\n    .args([\"foo\", \"bar\"])\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn parameter_default_values_may_not_use_later_parameters() {\n  Test::new()\n    .justfile(\n      \"\n        @foo a b=c c='':\n          echo {{ b }}\n      \",\n    )\n    .args([\"foo\", \"bar\"])\n    .stderr(\n      \"\n        error: Variable `c` not defined\n         ——▶ justfile:1:10\n          │\n        1 │ @foo a b=c c='':\n          │          ^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn star_may_follow_default() {\n  Test::new()\n    .justfile(\n      \"\n        foo bar='baz' *bob:\n          @echo {{bar}} {{bob}}\n      \",\n    )\n    .args([\"foo\", \"hello\", \"goodbye\"])\n    .stdout(\"hello goodbye\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/parser.rs",
    "content": "use super::*;\n\n#[test]\nfn dont_run_duplicate_recipes() {\n  Test::new()\n    .justfile(\n      \"\n      set dotenv-load # foo\n      bar:\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn invalid_bang_operator() {\n  Test::new()\n    .justfile(\n      \"\n      x := if '' !! '' { '' } else { '' }\n      \",\n    )\n    .stderr(\n      r\"\nerror: Expected character `=` or `~`\n ——▶ justfile:1:13\n  │\n1 │ x := if '' !! '' { '' } else { '' }\n  │             ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn truncated_bang_operator() {\n  Test::new()\n    .justfile(\"x := if '' !\")\n    .stderr(\n      r\"\nerror: Expected character `=` or `~` but found end-of-file\n ——▶ justfile:1:13\n  │\n1 │ x := if '' !\n  │             ^\n\",\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/positional_arguments.rs",
    "content": "use super::*;\n\n#[test]\nfn linewise() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"hello\")\n    .arg(\"goodbye\")\n    .justfile(\n      r#\"\n    set positional-arguments\n\n    foo bar baz:\n      echo $0\n      echo $1\n      echo $2\n      echo \"$@\"\n  \"#,\n    )\n    .stdout(\n      \"\n    foo\n    hello\n    goodbye\n    hello goodbye\n  \",\n    )\n    .stderr(\n      r#\"\n    echo $0\n    echo $1\n    echo $2\n    echo \"$@\"\n  \"#,\n    )\n    .success();\n}\n\n#[test]\nfn linewise_with_attribute() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"hello\")\n    .arg(\"goodbye\")\n    .justfile(\n      r#\"\n    [positional-arguments]\n    foo bar baz:\n      echo $0\n      echo $1\n      echo $2\n      echo \"$@\"\n  \"#,\n    )\n    .stdout(\n      \"\n    foo\n    hello\n    goodbye\n    hello goodbye\n  \",\n    )\n    .stderr(\n      r#\"\n    echo $0\n    echo $1\n    echo $2\n    echo \"$@\"\n  \"#,\n    )\n    .success();\n}\n\n#[test]\nfn variadic_linewise() {\n  Test::new()\n    .args([\"foo\", \"a\", \"b\", \"c\"])\n    .justfile(\n      r#\"\n    set positional-arguments\n\n    foo *bar:\n      echo $1\n      echo \"$@\"\n  \"#,\n    )\n    .stdout(\"a\\na b c\\n\")\n    .stderr(\"echo $1\\necho \\\"$@\\\"\\n\")\n    .success();\n}\n\n#[test]\nfn shebang() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"hello\")\n    .justfile(\n      \"\n    set positional-arguments\n\n    foo bar:\n      #!/bin/sh\n      echo $1\n  \",\n    )\n    .stdout(\"hello\\n\")\n    .success();\n}\n\n#[test]\nfn shebang_with_attribute() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"hello\")\n    .justfile(\n      \"\n    [positional-arguments]\n    foo bar:\n      #!/bin/sh\n      echo $1\n  \",\n    )\n    .stdout(\"hello\\n\")\n    .success();\n}\n\n#[test]\nfn variadic_shebang() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"a\")\n    .arg(\"b\")\n    .arg(\"c\")\n    .justfile(\n      r#\"\n    set positional-arguments\n\n    foo *bar:\n      #!/bin/sh\n      echo $1\n      echo \"$@\"\n  \"#,\n    )\n    .stdout(\"a\\na b c\\n\")\n    .success();\n}\n\n#[test]\nfn default_arguments() {\n  Test::new()\n    .justfile(\n      r\"\n    set positional-arguments\n\n    foo bar='baz':\n      echo $1\n  \",\n    )\n    .stdout(\"baz\\n\")\n    .stderr(\"echo $1\\n\")\n    .success();\n}\n\n#[test]\nfn empty_variadic_is_undefined() {\n  Test::new()\n    .justfile(\n      r#\"\n    set positional-arguments\n\n    foo *bar:\n      if [ -n \"${1+1}\" ]; then echo defined; else echo undefined; fi\n  \"#,\n    )\n    .stdout(\"undefined\\n\")\n    .stderr(\"if [ -n \\\"${1+1}\\\" ]; then echo defined; else echo undefined; fi\\n\")\n    .success();\n}\n\n#[test]\nfn variadic_arguments_are_separate() {\n  Test::new()\n    .arg(\"foo\")\n    .arg(\"a\")\n    .arg(\"b\")\n    .justfile(\n      r\"\n    set positional-arguments\n\n    foo *bar:\n      echo $1\n      echo $2\n  \",\n    )\n    .stdout(\"a\\nb\\n\")\n    .stderr(\"echo $1\\necho $2\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/private.rs",
    "content": "use super::*;\n\n#[test]\nfn private_attribute_for_recipe() {\n  Test::new()\n    .justfile(\n      \"\n      [private]\n      foo:\n      \",\n    )\n    .args([\"--list\"])\n    .stdout(\n      \"\n      Available recipes:\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn private_attribute_for_alias() {\n  Test::new()\n    .justfile(\n      \"\n      [private]\n      alias f := foo\n\n      foo:\n      \",\n    )\n    .args([\"--list\"])\n    .stdout(\n      \"\n      Available recipes:\n          foo\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn private_attribute_for_module() {\n  Test::new()\n    .write(\"foo.just\", \"bar:\")\n    .justfile(\n      r\"\n        [private]\n        mod foo\n\n        baz:\n      \",\n    )\n    .test_round_trip(false)\n    .arg(\"--list\")\n    .stdout(\n      \"\n        Available recipes:\n            baz\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn private_variables_are_not_listed() {\n  Test::new()\n    .justfile(\n      \"\n      [private]\n      foo := 'one'\n      bar := 'two'\n      _baz := 'three'\n      \",\n    )\n    .args([\"--variables\"])\n    .stdout(\"bar\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/quiet.rs",
    "content": "use super::*;\n\n#[test]\nfn no_stdout() {\n  Test::new()\n    .arg(\"--quiet\")\n    .justfile(\n      r\"\ndefault:\n  @echo hello\n\",\n    )\n    .success();\n}\n\n#[test]\nfn stderr() {\n  Test::new()\n    .arg(\"--quiet\")\n    .justfile(\n      r\"\ndefault:\n  @echo hello 1>&2\n\",\n    )\n    .success();\n}\n\n#[test]\nfn command_echoing() {\n  Test::new()\n    .arg(\"--quiet\")\n    .justfile(\n      r\"\ndefault:\n  exit\n\",\n    )\n    .success();\n}\n\n#[test]\nfn error_messages() {\n  Test::new()\n    .arg(\"--quiet\")\n    .justfile(\n      r\"\ndefault:\n  exit 100\n\",\n    )\n    .status(100);\n}\n\n#[test]\nfn assignment_backtick_stderr() {\n  Test::new()\n    .arg(\"--quiet\")\n    .justfile(\n      r\"\na := `echo hello 1>&2`\ndefault:\n  exit 100\n\",\n    )\n    .status(100);\n}\n\n#[test]\nfn interpolation_backtick_stderr() {\n  Test::new()\n    .arg(\"--quiet\")\n    .justfile(\n      r\"\ndefault:\n  echo `echo hello 1>&2`\n  exit 100\n\",\n    )\n    .status(100);\n}\n\n#[test]\nfn choose_none() {\n  Test::new()\n    .arg(\"--choose\")\n    .arg(\"--quiet\")\n    .justfile(\"\")\n    .failure();\n}\n\n#[test]\nfn choose_invocation() {\n  Test::new()\n    .arg(\"--choose\")\n    .arg(\"--quiet\")\n    .arg(\"--shell\")\n    .arg(\"asdfasdfasfdasdfasdfadsf\")\n    .justfile(\"foo:\")\n    .shell(false)\n    .failure();\n}\n\n#[test]\nfn choose_status() {\n  Test::new()\n    .arg(\"--choose\")\n    .arg(\"--quiet\")\n    .arg(\"--chooser\")\n    .arg(\"/usr/bin/env false\")\n    .justfile(\"foo:\")\n    .failure();\n}\n\n#[test]\nfn edit_invocation() {\n  Test::new()\n    .arg(\"--edit\")\n    .arg(\"--quiet\")\n    .env(\"VISUAL\", \"adsfasdfasdfadsfadfsaf\")\n    .justfile(\"foo:\")\n    .failure();\n}\n\n#[test]\nfn edit_status() {\n  Test::new()\n    .arg(\"--edit\")\n    .arg(\"--quiet\")\n    .env(\"VISUAL\", \"false\")\n    .justfile(\"foo:\")\n    .failure();\n}\n\n#[test]\nfn init_exists() {\n  Test::new()\n    .arg(\"--init\")\n    .arg(\"--quiet\")\n    .justfile(\"foo:\")\n    .failure();\n}\n\n#[test]\nfn show_missing() {\n  Test::new()\n    .arg(\"--show\")\n    .arg(\"bar\")\n    .arg(\"--quiet\")\n    .justfile(\"foo:\")\n    .failure();\n}\n\n#[test]\nfn quiet_shebang() {\n  Test::new()\n    .arg(\"--quiet\")\n    .justfile(\n      \"\n    @foo:\n      #!/bin/sh\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn no_quiet_setting() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          echo FOO\n      \",\n    )\n    .stdout(\"FOO\\n\")\n    .stderr(\"echo FOO\\n\")\n    .success();\n}\n\n#[test]\nfn quiet_setting() {\n  Test::new()\n    .justfile(\n      \"\n      set quiet\n\n      foo:\n        echo FOO\n      \",\n    )\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn quiet_setting_with_no_quiet_attribute() {\n  Test::new()\n    .justfile(\n      \"\n      set quiet\n\n      [no-quiet]\n      foo:\n        echo FOO\n      \",\n    )\n    .stdout(\"FOO\\n\")\n    .stderr(\"echo FOO\\n\")\n    .success();\n}\n\n#[test]\nfn quiet_setting_with_quiet_recipe() {\n  Test::new()\n    .justfile(\n      \"\n      set quiet\n\n      @foo:\n        echo FOO\n      \",\n    )\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn quiet_setting_with_quiet_line() {\n  Test::new()\n    .justfile(\n      \"\n      set quiet\n\n      foo:\n        @echo FOO\n      \",\n    )\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn quiet_setting_with_no_quiet_attribute_and_quiet_recipe() {\n  Test::new()\n    .justfile(\n      \"\n      set quiet\n\n      [no-quiet]\n      @foo:\n        echo FOO\n      \",\n    )\n    .stdout(\"FOO\\n\")\n    .success();\n}\n\n#[test]\nfn quiet_setting_with_no_quiet_attribute_and_quiet_line() {\n  Test::new()\n    .justfile(\n      \"\n      set quiet\n\n      [no-quiet]\n      foo:\n        @echo FOO\n      \",\n    )\n    .stdout(\"FOO\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/quote.rs",
    "content": "use super::*;\n\n#[test]\nfn single_quotes_are_prepended_and_appended() {\n  Test::new()\n    .justfile(\n      \"\n      x := quote('abc')\n    \",\n    )\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"'abc'\")\n    .success();\n}\n\n#[test]\nfn quotes_are_escaped() {\n  Test::new()\n    .justfile(\n      r#\"\n      x := quote(\"'\")\n    \"#,\n    )\n    .args([\"--evaluate\", \"x\"])\n    .stdout(r\"''\\'''\")\n    .success();\n}\n\n#[test]\nfn quoted_strings_can_be_used_as_arguments() {\n  Test::new()\n    .justfile(\n      r#\"\n      file := quote(\"foo ' bar\")\n\n      @foo:\n        touch {{ file }}\n        ls -1\n    \"#,\n    )\n    .stdout(\"foo ' bar\\njustfile\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/readme.rs",
    "content": "use super::*;\n\n#[test]\nfn readme() {\n  let mut justfiles = Vec::new();\n  let mut current = None;\n\n  for line in fs::read_to_string(\"README.md\").unwrap().lines() {\n    if let Some(mut justfile) = current {\n      if line == \"```\" {\n        justfiles.push(justfile);\n        current = None;\n      } else {\n        justfile += line;\n        justfile += \"\\n\";\n        current = Some(justfile);\n      }\n    } else if line == \"```just\" {\n      current = Some(String::new());\n    }\n  }\n\n  for justfile in justfiles {\n    let tmp = tempdir();\n\n    let path = tmp.path().join(\"justfile\");\n\n    fs::write(path, justfile).unwrap();\n\n    let output = Command::new(JUST)\n      .current_dir(tmp.path())\n      .arg(\"--dump\")\n      .output()\n      .unwrap();\n\n    assert_success(&output);\n  }\n}\n"
  },
  {
    "path": "tests/recursion_limit.rs",
    "content": "use super::*;\n\n#[test]\nfn bugfix() {\n  let mut justfile = String::from(\"foo: (x \");\n  for _ in 0..500 {\n    justfile.push('(');\n  }\n  Test::new()\n    .justfile(&justfile)\n    .stderr(RECURSION_LIMIT_REACHED)\n    .failure();\n}\n\nconst RECURSION_LIMIT_REACHED: &str = if cfg!(windows) {\n  \"\nerror: Parsing recursion depth exceeded\n ——▶ justfile:1:57\n  │\n1 │ foo: (x ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((\n  │                                                         ^\n\"\n} else {\n  \"\nerror: Parsing recursion depth exceeded\n ——▶ justfile:1:265\n  │\n1 │ foo: (x ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((\n  │                                                                                                                                                                                                                                                                         ^\n\"\n};\n"
  },
  {
    "path": "tests/regexes.rs",
    "content": "use super::*;\n\n#[test]\nfn match_succeeds_evaluates_to_first_branch() {\n  Test::new()\n    .justfile(\n      \"\n      foo := if 'abbbc' =~ 'ab+c' {\n        'yes'\n      } else {\n        'no'\n      }\n\n      default:\n        echo {{ foo }}\n    \",\n    )\n    .stderr(\"echo yes\\n\")\n    .stdout(\"yes\\n\")\n    .success();\n}\n\n#[test]\nfn match_fails_evaluates_to_second_branch() {\n  Test::new()\n    .justfile(\n      \"\n      foo := if 'abbbc' =~ 'ab{4}c' {\n        'yes'\n      } else {\n        'no'\n      }\n\n      default:\n        echo {{ foo }}\n    \",\n    )\n    .stderr(\"echo no\\n\")\n    .stdout(\"no\\n\")\n    .success();\n}\n\n#[test]\nfn bad_regex_fails_at_runtime() {\n  Test::new()\n    .justfile(\n      \"\n        default:\n          echo before\n          echo {{ if '' =~ '(' { 'a' } else { 'b' } }}\n          echo after\n      \",\n    )\n    .stderr(\n      \"\n        echo before\n        error: regex parse error:\n            (\n            ^\n        error: unclosed group\n      \",\n    )\n    .stdout(\"before\\n\")\n    .failure();\n}\n\n#[test]\nfn mismatch() {\n  Test::new()\n    .justfile(\n      \"\n      foo := if 'Foo' !~ '^ab+c' {\n        'mismatch'\n      } else {\n        'match'\n      }\n\n      bar := if 'Foo' !~ 'Foo' {\n        'mismatch'\n      } else {\n        'match'\n      }\n\n      @default:\n        echo {{ foo }} {{ bar }}\n    \",\n    )\n    .stdout(\"mismatch match\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/request.rs",
    "content": "use super::*;\n\n#[test]\nfn environment_variable_set() {\n  Test::new()\n    .justfile(\n      r#\"\n      export BAR := 'baz'\n\n      @foo:\n        '{{just_executable()}}' --request '{\"environment-variable\": \"BAR\"}'\n    \"#,\n    )\n    .response(Response::EnvironmentVariable(Some(\"baz\".into())))\n    .success();\n}\n\n#[test]\nfn environment_variable_missing() {\n  Test::new()\n    .justfile(\n      r#\"\n      @foo:\n        '{{just_executable()}}' --request '{\"environment-variable\": \"FOO_BAR_BAZ\"}'\n    \"#,\n    )\n    .response(Response::EnvironmentVariable(None))\n    .success();\n}\n"
  },
  {
    "path": "tests/run.rs",
    "content": "use super::*;\n\n#[test]\nfn dont_run_duplicate_recipes() {\n  Test::new()\n    .justfile(\n      \"\n        @foo:\n          echo foo\n      \",\n    )\n    .args([\"foo\", \"foo\"])\n    .stdout(\"foo\\n\")\n    .success();\n}\n\n#[test]\nfn one_flag_only_allows_one_invocation() {\n  Test::new()\n    .justfile(\n      \"\n        @foo:\n          echo foo\n      \",\n    )\n    .args([\"--one\", \"foo\"])\n    .stdout(\"foo\\n\")\n    .success();\n\n  Test::new()\n    .justfile(\n      \"\n        @foo:\n          echo foo\n\n        @bar:\n          echo bar\n      \",\n    )\n    .args([\"--one\", \"foo\", \"bar\"])\n    .stderr(\"error: Expected 1 command-line recipe invocation but found 2.\\n\")\n    .failure();\n}\n"
  },
  {
    "path": "tests/scope.rs",
    "content": "use super::*;\n\n#[test]\nfn dependencies_in_submodules_run_with_submodule_scope() {\n  Test::new()\n    .write(\"bar.just\", \"x := 'X'\\nbar a=x:\\n echo {{ a }} {{ x }}\")\n    .justfile(\n      \"\n        mod bar\n\n        foo: bar::bar\n      \",\n    )\n    .stdout(\"X X\\n\")\n    .stderr(\"echo X X\\n\")\n    .success();\n}\n\n#[test]\nfn aliases_in_submodules_run_with_submodule_scope() {\n  Test::new()\n    .write(\"bar.just\", \"x := 'X'\\nbar a=x:\\n echo {{ a }} {{ x }}\")\n    .justfile(\n      \"\n        mod bar\n\n        alias foo := bar::bar\n      \",\n    )\n    .arg(\"foo\")\n    .stdout(\"X X\\n\")\n    .stderr(\"echo X X\\n\")\n    .success();\n}\n\n#[test]\nfn dependencies_in_nested_submodules_run_with_submodule_scope() {\n  Test::new()\n    .write(\n      \"b.just\",\n      \"\nx := 'y'\n\nfoo:\n    @echo {{ x }}\n\",\n    )\n    .write(\"a.just\", \"mod b\")\n    .stdout(\"y\\n\")\n    .justfile(\"mod a\")\n    .args([\"a\", \"b\", \"foo\"])\n    .success();\n}\n\n#[test]\nfn imported_recipes_run_in_correct_scope() {\n  Test::new()\n    .justfile(\n      \"\n        mod a\n        mod b\n      \",\n    )\n    .write(\"a.just\", \"X := 'A'\\nimport 'shared.just'\")\n    .write(\"b.just\", \"X := 'B'\\nimport 'shared.just'\")\n    .write(\"shared.just\", \"foo:\\n @echo {{ X }}\")\n    .args([\"a::foo\", \"b::foo\"])\n    .stdout(\"A\\nB\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/script.rs",
    "content": "use super::*;\n\n#[test]\nfn runs_with_command() {\n  Test::new()\n    .justfile(\n      \"\n        [script('cat')]\n        foo:\n          FOO\n      \",\n    )\n    .stdout(\n      \"\n\n\n        FOO\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn no_arguments() {\n  Test::new()\n    .justfile(\n      \"\n        [script('sh')]\n        foo:\n          echo foo\n      \",\n    )\n    .stdout(\"foo\\n\")\n    .success();\n}\n\n#[test]\nfn with_arguments() {\n  Test::new()\n    .justfile(\n      \"\n        [script('sh', '-x')]\n        foo:\n          echo foo\n      \",\n    )\n    .stdout(\"foo\\n\")\n    .stderr(\"+ echo foo\\n\")\n    .success();\n}\n\n#[test]\nfn allowed_with_shebang() {\n  Test::new()\n    .justfile(\n      \"\n        [script('cat')]\n        foo:\n          #!/bin/sh\n      \",\n    )\n    .stdout(\n      \"\n\n\n        #!/bin/sh\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn script_line_numbers() {\n  Test::new()\n    .justfile(\n      \"\n        [script('cat')]\n        foo:\n          FOO\n\n          BAR\n      \",\n    )\n    .stdout(\n      \"\n\n\n        FOO\n\n        BAR\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn script_line_numbers_with_multi_line_recipe_signature() {\n  Test::new()\n    .justfile(\n      r\"\n        [script('cat')]\n        foo bar='baz' \\\n          :\n          FOO\n\n          BAR\n\n          {{ \\\n             bar \\\n          }}\n\n          BAZ\n      \",\n    )\n    .stdout(\n      \"\n\n\n\n        FOO\n\n        BAR\n\n        baz\n\n\n\n        BAZ\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn shebang_line_numbers() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      \"foo:\n  #!/usr/bin/env cat\n\n  a\n\n  b\n\n\n  c\n\n\n\",\n    )\n    .stdout(\n      \"#!/usr/bin/env cat\n\n\na\n\nb\n\n\nc\n\",\n    )\n    .success();\n}\n\n#[test]\nfn shebang_line_numbers_with_multiline_constructs() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"foo b='b'\\\n        :\n  #!/usr/bin/env cat\n\n  a\n\n  {{ \\\n     b \\\n  }}\n\n\n  c\n\n\n\",\n    )\n    .stdout(\n      \"#!/usr/bin/env cat\n\n\n\na\n\nb\n\n\n\n\nc\n\",\n    )\n    .success();\n}\n\n#[test]\nfn multiline_shebang_line_numbers() {\n  if cfg!(windows) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      \"foo:\n  #!/usr/bin/env cat\n  #!shebang\n  #!shebang\n\n  a\n\n  b\n\n\n  c\n\n\n\",\n    )\n    .stdout(\n      \"#!/usr/bin/env cat\n#!shebang\n#!shebang\n\n\na\n\nb\n\n\nc\n\",\n    )\n    .success();\n}\n\n#[test]\nfn shebang_line_numbers_windows() {\n  if cfg!(not(windows)) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      \"foo:\n  #!/usr/bin/env cat\n\n  a\n\n  b\n\n\n  c\n\n\n\",\n    )\n    .stdout(\n      \"\n\n\n\na\n\nb\n\n\nc\n\",\n    )\n    .success();\n}\n\n#[test]\nfn no_arguments_with_default_script_interpreter() {\n  Test::new()\n    .justfile(\n      \"\n        [script]\n        foo:\n          case $- in\n            *e*) echo '-e is set';;\n          esac\n\n          case $- in\n            *u*) echo '-u is set';;\n          esac\n      \",\n    )\n    .stdout(\n      \"\n        -e is set\n        -u is set\n      \",\n    )\n    .success();\n}\n\n#[test]\nfn no_arguments_with_non_default_script_interpreter() {\n  Test::new()\n    .justfile(\n      \"\n        set script-interpreter := ['sh']\n\n        [script]\n        foo:\n          case $- in\n            *e*) echo '-e is set';;\n          esac\n\n          case $- in\n            *u*) echo '-u is set';;\n          esac\n      \",\n    )\n    .success();\n}\n"
  },
  {
    "path": "tests/search.rs",
    "content": "use super::*;\n\nfn search_test<P: AsRef<Path>>(path: P, args: &[&str]) {\n  let output = Command::new(JUST)\n    .current_dir(path)\n    .args(args)\n    .output()\n    .expect(\"just invocation failed\");\n\n  assert_eq!(output.status.code().unwrap(), 0);\n\n  let stdout = str::from_utf8(&output.stdout).unwrap();\n  assert_eq!(stdout, \"ok\\n\");\n\n  let stderr = str::from_utf8(&output.stderr).unwrap();\n  assert_eq!(stderr, \"echo ok\\n\");\n}\n\n#[test]\nfn test_justfile_search() {\n  let tmp = temptree! {\n    justfile: \"default:\\n\\techo ok\",\n    a: {\n      b: {\n        c: {\n          d: {},\n        },\n      },\n    },\n  };\n\n  search_test(tmp.path().join(\"a/b/c/d\"), &[]);\n}\n\n#[test]\nfn test_capitalized_justfile_search() {\n  let tmp = temptree! {\n    Justfile: \"default:\\n\\techo ok\",\n    a: {\n      b: {\n        c: {\n          d: {},\n        },\n      },\n    },\n  };\n\n  search_test(tmp.path().join(\"a/b/c/d\"), &[]);\n}\n\n#[test]\nfn test_upwards_path_argument() {\n  let tmp = temptree! {\n    justfile: \"default:\\n\\techo ok\",\n    a: {\n      justfile: \"default:\\n\\techo bad\",\n    },\n  };\n\n  search_test(tmp.path().join(\"a\"), &[\"../\"]);\n  search_test(tmp.path().join(\"a\"), &[\"../default\"]);\n}\n\n#[test]\nfn test_downwards_path_argument() {\n  let tmp = temptree! {\n    justfile: \"default:\\n\\techo bad\",\n    a: {\n      justfile: \"default:\\n\\techo ok\",\n    },\n  };\n\n  let path = tmp.path();\n\n  search_test(path, &[\"a/\"]);\n  search_test(path, &[\"a/default\"]);\n  search_test(path, &[\"./a/\"]);\n  search_test(path, &[\"./a/default\"]);\n  search_test(path, &[\"./a/\"]);\n  search_test(path, &[\"./a/default\"]);\n}\n\n#[test]\nfn test_upwards_multiple_path_argument() {\n  let tmp = temptree! {\n    justfile: \"default:\\n\\techo ok\",\n    a: {\n      b: {\n        justfile: \"default:\\n\\techo bad\",\n      },\n    },\n  };\n\n  let path = tmp.path().join(\"a\").join(\"b\");\n  search_test(&path, &[\"../../\"]);\n  search_test(&path, &[\"../../default\"]);\n}\n\n#[test]\nfn test_downwards_multiple_path_argument() {\n  let tmp = temptree! {\n    justfile: \"default:\\n\\techo bad\",\n    a: {\n      b: {\n        justfile: \"default:\\n\\techo ok\",\n      },\n    },\n  };\n\n  let path = tmp.path();\n\n  search_test(path, &[\"a/b/\"]);\n  search_test(path, &[\"a/b/default\"]);\n  search_test(path, &[\"./a/b/\"]);\n  search_test(path, &[\"./a/b/default\"]);\n  search_test(path, &[\"./a/b/\"]);\n  search_test(path, &[\"./a/b/default\"]);\n}\n\n#[test]\nfn single_downwards() {\n  let tmp = temptree! {\n    justfile: \"default:\\n\\techo ok\",\n    child: {},\n  };\n\n  let path = tmp.path();\n\n  search_test(path, &[\"child/\"]);\n}\n\n#[test]\nfn single_upwards() {\n  let tmp = temptree! {\n    justfile: \"default:\\n\\techo ok\",\n    child: {},\n  };\n\n  let path = tmp.path().join(\"child\");\n\n  search_test(path, &[\"../\"]);\n}\n\n#[test]\nfn double_upwards() {\n  let tmp = temptree! {\n    justfile: \"default:\\n\\techo ok\",\n    foo: {\n      bar: {\n        justfile: \"default:\\n\\techo foo\",\n      },\n    },\n  };\n\n  let path = tmp.path().join(\"foo/bar\");\n\n  search_test(path, &[\"../default\"]);\n}\n\n#[test]\nfn find_dot_justfile() {\n  Test::new()\n    .justfile(\n      \"\n      foo:\n        echo bad\n    \",\n    )\n    .tree(tree! {\n      dir: {\n        \".justfile\": \"\n          foo:\n            echo ok\n        \"\n      }\n    })\n    .current_dir(\"dir\")\n    .stderr(\"echo ok\\n\")\n    .stdout(\"ok\\n\")\n    .success();\n}\n\n#[test]\nfn dot_justfile_conflicts_with_justfile() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n    \",\n    )\n    .tree(tree! {\n      \".justfile\": \"\n        foo:\n      \",\n    })\n    .stderr_regex(\"error: Multiple candidate justfiles found in `.*`: `.justfile` and `justfile`\\n\")\n    .failure();\n}\n"
  },
  {
    "path": "tests/search_arguments.rs",
    "content": "use super::*;\n\n#[test]\nfn argument_with_different_path_prefix_is_allowed() {\n  Test::new()\n    .justfile(\"foo bar:\")\n    .args([\"./foo\", \"../bar\"])\n    .success();\n}\n\n#[test]\nfn passing_dot_as_argument_is_allowed() {\n  Test::new()\n    .justfile(\n      \"\n        say ARG:\n          echo {{ARG}}\n      \",\n    )\n    .write(\n      \"child/justfile\",\n      \"say ARG:\\n '{{just_executable()}}' ../say {{ARG}}\",\n    )\n    .current_dir(\"child\")\n    .args([\"say\", \".\"])\n    .stdout(\".\\n\")\n    .stderr_regex(\"'.*' ../say .\\necho .\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/settings.rs",
    "content": "use super::*;\n\n#[test]\nfn all_settings_allow_expressions() {\n  Test::new()\n    .justfile(\n      \"\n        foo := 'hello'\n\n        set dotenv-filename := foo\n        set dotenv-path := foo\n        set script-interpreter := [foo, foo, foo]\n        set shell := [foo, foo, foo]\n        set tempdir := foo\n        set windows-shell := [foo, foo, foo]\n        set working-directory := foo\n      \",\n    )\n    .arg(\"--summary\")\n    .stdout(\n      \"\n\n      \",\n    )\n    .stderr(\"Justfile contains no recipes.\\n\")\n    .success();\n}\n\n#[test]\nfn undefined_variable_in_working_directory() {\n  Test::new()\n    .justfile(\n      \"\n        set working-directory := foo\n      \",\n    )\n    .stderr(\n      \"\n      error: Variable `foo` not defined\n       ——▶ justfile:1:26\n        │\n      1 │ set working-directory := foo\n        │                          ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn undefined_variable_in_dotenv_filename() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-filename := foo\n      \",\n    )\n    .stderr(\n      \"\n      error: Variable `foo` not defined\n       ——▶ justfile:1:24\n        │\n      1 │ set dotenv-filename := foo\n        │                        ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn undefined_variable_in_dotenv_path() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-path := foo\n      \",\n    )\n    .stderr(\n      \"\n      error: Variable `foo` not defined\n       ——▶ justfile:1:20\n        │\n      1 │ set dotenv-path := foo\n        │                    ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn undefined_variable_in_tempdir() {\n  Test::new()\n    .justfile(\n      \"\n        set tempdir := foo\n      \",\n    )\n    .stderr(\n      \"\n      error: Variable `foo` not defined\n       ——▶ justfile:1:16\n        │\n      1 │ set tempdir := foo\n        │                ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn undefined_variable_in_script_interpreter_command() {\n  Test::new()\n    .justfile(\n      \"\n        set script-interpreter := [foo]\n      \",\n    )\n    .stderr(\n      \"\n      error: Variable `foo` not defined\n       ——▶ justfile:1:28\n        │\n      1 │ set script-interpreter := [foo]\n        │                            ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn undefined_variable_in_script_interpreter_argument() {\n  Test::new()\n    .justfile(\n      \"\n        set script-interpreter := ['foo', bar]\n      \",\n    )\n    .stderr(\n      \"\n      error: Variable `bar` not defined\n       ——▶ justfile:1:35\n        │\n      1 │ set script-interpreter := ['foo', bar]\n        │                                   ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn undefined_variable_in_shell_command() {\n  Test::new()\n    .justfile(\n      \"\n        set shell := [foo]\n      \",\n    )\n    .stderr(\n      \"\n      error: Variable `foo` not defined\n       ——▶ justfile:1:15\n        │\n      1 │ set shell := [foo]\n        │               ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn undefined_variable_in_shell_argument() {\n  Test::new()\n    .justfile(\n      \"\n        set shell := ['foo', bar]\n      \",\n    )\n    .stderr(\n      \"\n      error: Variable `bar` not defined\n       ——▶ justfile:1:22\n        │\n      1 │ set shell := ['foo', bar]\n        │                      ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn undefined_variable_in_windows_shell_command() {\n  Test::new()\n    .justfile(\n      \"\n        set windows-shell := [foo]\n      \",\n    )\n    .stderr(\n      \"\n      error: Variable `foo` not defined\n       ——▶ justfile:1:23\n        │\n      1 │ set windows-shell := [foo]\n        │                       ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn undefined_variable_in_windows_shell_argument() {\n  Test::new()\n    .justfile(\n      \"\n        set windows-shell := ['foo', bar]\n      \",\n    )\n    .stderr(\n      \"\n      error: Variable `bar` not defined\n       ——▶ justfile:1:30\n        │\n      1 │ set windows-shell := ['foo', bar]\n        │                              ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn built_in_constant() {\n  Test::new()\n    .justfile(\n      \"\n        set working-directory := HEX\n\n        @foo:\n          cat file.txt\n      \",\n    )\n    .write(\"0123456789abcdef/file.txt\", \"bar\")\n    .stdout(\"bar\")\n    .success();\n}\n\n#[test]\nfn variable() {\n  Test::new()\n    .justfile(\n      \"\n        dir := 'bar'\n\n        set working-directory := dir\n\n        @foo:\n          cat file.txt\n      \",\n    )\n    .write(\"bar/file.txt\", \"baz\")\n    .arg(\"foo\")\n    .stdout(\"baz\")\n    .success();\n}\n\n#[test]\nfn unused_non_const_assignments() {\n  Test::new()\n    .justfile(\n      \"\n        baz := `pwd`\n\n        dir := 'bar'\n\n        set working-directory := dir\n\n        @foo:\n          cat file.txt\n      \",\n    )\n    .write(\"bar/file.txt\", \"baz\")\n    .arg(\"foo\")\n    .stdout(\"baz\")\n    .success();\n}\n\n#[test]\nfn variable_with_override() {\n  Test::new()\n    .justfile(\n      \"\n        dir := 'bar'\n\n        set working-directory := dir\n\n        @foo:\n          cat file.txt\n      \",\n    )\n    .arg(\"dir=bob\")\n    .write(\"bob/file.txt\", \"baz\")\n    .arg(\"foo\")\n    .stdout(\"baz\")\n    .success();\n}\n\n#[test]\nfn expression() {\n  Test::new()\n    .justfile(\n      \"\n        dir := 'bar'\n\n        set working-directory := dir + '-bob'\n\n        @foo:\n          cat file.txt\n      \",\n    )\n    .write(\"bar-bob/file.txt\", \"baz\")\n    .arg(\"foo\")\n    .stdout(\"baz\")\n    .success();\n}\n\n#[test]\nfn expression_with_override() {\n  Test::new()\n    .justfile(\n      \"\n        dir := 'bar'\n\n        set working-directory := dir + '-bob'\n\n        @foo:\n          cat file.txt\n      \",\n    )\n    .write(\"bob-bob/file.txt\", \"baz\")\n    .args([\"dir=bob\", \"foo\"])\n    .stdout(\"baz\")\n    .success();\n}\n\n#[test]\nfn backtick() {\n  Test::new()\n    .justfile(\n      \"\n        set working-directory := `pwd`\n      \",\n    )\n    .stderr(\n      \"\n      error: Cannot call backticks in const context\n       ——▶ justfile:1:26\n        │\n      1 │ set working-directory := `pwd`\n        │                          ^^^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn function_call() {\n  Test::new()\n    .justfile(\n      \"\n        set working-directory := arch()\n      \",\n    )\n    .stderr(\n      \"\n      error: Cannot call functions in const context\n       ——▶ justfile:1:26\n        │\n      1 │ set working-directory := arch()\n        │                          ^^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn non_const_variable() {\n  Test::new()\n    .justfile(\n      \"\n        foo := `pwd`\n\n        set working-directory := foo\n      \",\n    )\n    .stderr(\n      \"\n      error: Cannot access non-const variable `foo` in const context\n       ——▶ justfile:3:26\n        │\n      3 │ set working-directory := foo\n        │                          ^^^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn assert() {\n  Test::new()\n    .justfile(\n      \"\n        set working-directory := assert('foo' == 'bar', 'fail')\n      \",\n    )\n    .stderr(\n      \"\n        error: Assert failed: fail\n         ——▶ justfile:1:26\n          │\n        1 │ set working-directory := assert('foo' == 'bar', 'fail')\n          │                          ^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn bad_regex() {\n  Test::new()\n    .justfile(\n      \"\n        set working-directory := if '' =~ '(' {\n          'a'\n        } else {\n          'b'\n        }\n      \",\n    )\n    .stderr(\n      \"\n        error: regex parse error:\n            (\n            ^\n        error: unclosed group\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn backtick_override() {\n  Test::new()\n    .justfile(\n      \"\n        bar := `pwd`\n\n        set working-directory := bar\n\n        @foo:\n          cat file.txt\n      \",\n    )\n    .test_round_trip(false)\n    .arg(\"bar=foo\")\n    .write(\"foo/file.txt\", \"baz\")\n    .arg(\"foo\")\n    .stdout(\"baz\")\n    .success();\n}\n\n#[test]\nfn submodule_expression() {\n  Test::new()\n    .write(\n      \"foo/mod.just\",\n      \"\ndir := 'bar'\n\nset working-directory := dir + '-baz'\n\nfoo:\n  @cat file.txt\n\",\n    )\n    .justfile(\n      \"\n        dir := 'hello'\n\n        mod foo\n      \",\n    )\n    .write(\"foo/bar-baz/file.txt\", \"ok\")\n    .args([\"foo\", \"foo\"])\n    .stdout(\"ok\")\n    .success();\n}\n\n#[test]\nfn overrides_are_ignored_in_submodules() {\n  Test::new()\n    .write(\n      \"foo.just\",\n      \"\ndir := 'bar'\n\nset working-directory := dir\n\nfoo:\n  @cat file.txt\n\",\n    )\n    .justfile(\n      \"\n        mod foo\n\n        dir := 'root'\n\n        bob := 'baz'\n      \",\n    )\n    .args([\"dir=bob\", \"bob=foo\", \"foo::foo\"])\n    .write(\"bar/file.txt\", \"ok\")\n    .stdout(\"ok\")\n    .success();\n}\n"
  },
  {
    "path": "tests/shadowing_parameters.rs",
    "content": "use super::*;\n\n#[test]\nfn parameter_may_shadow_variable() {\n  Test::new()\n    .arg(\"a\")\n    .arg(\"bar\")\n    .justfile(\"FOO := 'hello'\\na FOO:\\n echo {{FOO}}\\n\")\n    .stdout(\"bar\\n\")\n    .stderr(\"echo bar\\n\")\n    .success();\n}\n\n#[test]\nfn shadowing_parameters_do_not_change_environment() {\n  Test::new()\n    .arg(\"a\")\n    .arg(\"bar\")\n    .justfile(\"export FOO := 'hello'\\na FOO:\\n echo $FOO\\n\")\n    .stdout(\"hello\\n\")\n    .stderr(\"echo $FOO\\n\")\n    .success();\n}\n\n#[test]\nfn exporting_shadowing_parameters_does_change_environment() {\n  Test::new()\n    .arg(\"a\")\n    .arg(\"bar\")\n    .justfile(\"export FOO := 'hello'\\na $FOO:\\n echo $FOO\\n\")\n    .stdout(\"bar\\n\")\n    .stderr(\"echo $FOO\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/shebang.rs",
    "content": "use super::*;\n\n#[test]\nfn powershell() {\n  if cfg!(not(windows)) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\ndefault:\n  #!powershell\n  Write-Host Hello-World\n\",\n    )\n    .stdout(\"Hello-World\\n\")\n    .success();\n}\n\n#[test]\nfn powershell_exe() {\n  if cfg!(not(windows)) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\ndefault:\n  #!powershell.exe\n   Write-Host Hello-World\n\",\n    )\n    .stdout(\"Hello-World\\n\")\n    .success();\n}\n\n#[test]\nfn cmd() {\n  if cfg!(not(windows)) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\ndefault:\n  #!cmd /c\n  @echo Hello-World\n\",\n    )\n    .stdout(\"Hello-World\\r\\n\")\n    .success();\n}\n\n#[test]\nfn cmd_exe() {\n  if cfg!(not(windows)) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\ndefault:\n  #!cmd.exe /c\n  @echo Hello-World\n\",\n    )\n    .stdout(\"Hello-World\\r\\n\")\n    .success();\n}\n\n#[test]\nfn multi_line_cmd_shebangs_are_removed() {\n  if cfg!(not(windows)) {\n    return;\n  }\n  Test::new()\n    .justfile(\n      r\"\ndefault:\n  #!cmd.exe /c\n  #!foo\n  @echo Hello-World\n\",\n    )\n    .stdout(\"Hello-World\\r\\n\")\n    .success();\n}\n\n#[test]\nfn simple() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          #!/bin/sh\n          echo bar\n      \",\n    )\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn echo() {\n  Test::new()\n    .justfile(\n      \"\n        @baz:\n          #!/bin/sh\n          echo fizz\n      \",\n    )\n    .stdout(\"fizz\\n\")\n    .stderr(\"#!/bin/sh\\necho fizz\\n\")\n    .success();\n}\n\n#[test]\nfn echo_with_command_color() {\n  Test::new()\n    .justfile(\n      \"\n        @baz:\n          #!/bin/sh\n          echo fizz\n      \",\n    )\n    .args([\"--color\", \"always\", \"--command-color\", \"purple\"])\n    .stdout(\"fizz\\n\")\n    .stderr(\"\\u{1b}[1;35m#!/bin/sh\\u{1b}[0m\\n\\u{1b}[1;35mecho fizz\\u{1b}[0m\\n\")\n    .success();\n}\n\n// This test exists to make sure that shebang recipes run correctly.  Although\n// this script is still executed by a shell its behavior depends on the value of\n// a variable and continuing even though a command fails, whereas in plain\n// recipes variables are not available in subsequent lines and execution stops\n// when a line fails.\n#[test]\nfn run_shebang() {\n  Test::new()\n    .justfile(\n      \"\n        a:\n          #!/usr/bin/env sh\n          code=200\n          x() { return $code; }\n          x\n          x\n      \",\n    )\n    .stderr(\"error: Recipe `a` failed with exit code 200\\n\")\n    .status(200);\n}\n"
  },
  {
    "path": "tests/shell.rs",
    "content": "use super::*;\n\nconst JUSTFILE: &str = \"\nexpression := `EXPRESSION`\n\nrecipe default=`DEFAULT`:\n  {{expression}}\n  {{default}}\n  RECIPE\n\";\n\n/// Test that --shell correctly sets the shell\n#[test]\n#[cfg_attr(windows, ignore)]\nfn flag() {\n  let tmp = temptree! {\n    justfile: JUSTFILE,\n    shell: \"#!/usr/bin/env bash\\necho \\\"$@\\\"\",\n  };\n\n  let shell = tmp.path().join(\"shell\");\n\n  #[cfg(not(windows))]\n  {\n    let permissions = std::os::unix::fs::PermissionsExt::from_mode(0o700);\n    fs::set_permissions(&shell, permissions).unwrap();\n  }\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--shell\")\n    .arg(shell)\n    .output()\n    .unwrap();\n\n  let stdout = \"-cu -cu EXPRESSION\\n-cu -cu DEFAULT\\n-cu RECIPE\\n\";\n  assert_stdout(&output, stdout);\n}\n\n/// Test that we can use `set shell` to use cmd.exe on windows\n#[test]\nfn cmd() {\n  if cfg!(not(windows)) {\n    return;\n  }\n  let tmp = temptree! {\n    justfile: r#\"\n\nset shell := [\"cmd.exe\", \"/C\"]\n\nx := `Echo`\n\nrecipe:\n  REM foo\n  Echo \"{{x}}\"\n\"#,\n  };\n\n  let output = Command::new(JUST).current_dir(tmp.path()).output().unwrap();\n\n  let stdout = \"\\\\\\\"ECHO is on.\\\\\\\"\\r\\n\";\n\n  assert_stdout(&output, stdout);\n}\n\n/// Test that we can use `set shell` to use cmd.exe on windows\n#[test]\nfn powershell() {\n  if cfg!(not(windows)) {\n    return;\n  }\n  let tmp = temptree! {\n      justfile: r#\"\n\nset shell := [\"powershell.exe\", \"-c\"]\n\nx := `Write-Host \"Hello, world!\"`\n\nrecipe:\n  For ($i=0; $i -le 10; $i++) { Write-Host $i }\n  Write-Host \"{{x}}\"\n\"#\n  ,\n    };\n\n  let output = Command::new(JUST).current_dir(tmp.path()).output().unwrap();\n\n  let stdout = \"0\\n1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\n9\\n10\\nHello, world!\\n\";\n\n  assert_stdout(&output, stdout);\n}\n\n#[test]\nfn shell_args() {\n  Test::new()\n    .arg(\"--shell-arg\")\n    .arg(\"-c\")\n    .justfile(\n      \"\n    default:\n      echo A${foo}A\n  \",\n    )\n    .shell(false)\n    .stdout(\"AA\\n\")\n    .stderr(\"echo A${foo}A\\n\")\n    .success();\n}\n\n#[test]\nfn shell_override() {\n  Test::new()\n    .arg(\"--shell\")\n    .arg(\"bash\")\n    .justfile(\n      \"\n    set shell := ['foo-bar-baz']\n\n    default:\n      echo hello\n  \",\n    )\n    .shell(false)\n    .stdout(\"hello\\n\")\n    .stderr(\"echo hello\\n\")\n    .success();\n}\n\n#[test]\nfn shell_arg_override() {\n  Test::new()\n    .arg(\"--shell-arg\")\n    .arg(\"-cu\")\n    .justfile(\n      \"\n    set shell := ['foo-bar-baz']\n\n    default:\n      echo hello\n  \",\n    )\n    .stdout(\"hello\\n\")\n    .stderr(\"echo hello\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn set_shell() {\n  Test::new()\n    .justfile(\n      \"\n    set shell := ['echo', '-n']\n\n    x := `bar`\n\n    foo:\n      echo {{x}}\n      echo foo\n  \",\n    )\n    .stdout(\"echo barecho foo\")\n    .stderr(\"echo bar\\necho foo\\n\")\n    .shell(false)\n    .success();\n}\n\n#[test]\nfn recipe_shell_not_found_error_message() {\n  Test::new()\n    .justfile(\n      \"\n        foo:\n          @echo bar\n      \",\n    )\n    .shell(false)\n    .args([\"--shell\", \"NOT_A_REAL_SHELL\"])\n    .stderr_regex(\n      \"error: Recipe `foo` could not be run because just could not find the shell: .*\\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn backtick_recipe_shell_not_found_error_message() {\n  Test::new()\n    .justfile(\n      \"\n        bar := `echo bar`\n\n        foo:\n          echo {{bar}}\n      \",\n    )\n    .shell(false)\n    .args([\"--shell\", \"NOT_A_REAL_SHELL\"])\n    .stderr_regex(\"(?s)error: Backtick could not be run because just could not find the shell:.*\")\n    .failure();\n}\n"
  },
  {
    "path": "tests/shell_expansion.rs",
    "content": "use super::*;\n\n#[test]\nfn strings_are_shell_expanded() {\n  Test::new()\n    .justfile(\n      \"\n        x := x'$JUST_TEST_VARIABLE'\n      \",\n    )\n    .env(\"JUST_TEST_VARIABLE\", \"FOO\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"FOO\")\n    .success();\n}\n\n#[test]\nfn shell_expanded_strings_must_not_have_whitespace() {\n  Test::new()\n    .justfile(\n      \"\n        x := x '$JUST_TEST_VARIABLE'\n      \",\n    )\n    .stderr(\n      \"\n        error: Expected '&&', '||', comment, end of file, end of line, '(', '+', or '/', but found string\n         ——▶ justfile:1:8\n          │\n        1 │ x := x '$JUST_TEST_VARIABLE'\n          │        ^^^^^^^^^^^^^^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn shell_expanded_error_messages_highlight_string_token() {\n  Test::new()\n    .justfile(\n      \"\n        x := x'$FOOOOOOOOOOOOOOOOOOOOOOOOOOOOO'\n      \",\n    )\n    .env(\"JUST_TEST_VARIABLE\", \"FOO\")\n    .args([\"--evaluate\", \"x\"])\n    .stderr(\n    \"\n      error: Shell expansion failed: error looking key 'FOOOOOOOOOOOOOOOOOOOOOOOOOOOOO' up: environment variable not found\n       ——▶ justfile:1:7\n        │\n      1 │ x := x'$FOOOOOOOOOOOOOOOOOOOOOOOOOOOOO'\n        │       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n      \")\n    .failure();\n}\n\n#[test]\nfn shell_expanded_strings_are_dumped_correctly() {\n  Test::new()\n    .justfile(\n      \"\n        x := x'$JUST_TEST_VARIABLE'\n      \",\n    )\n    .env(\"JUST_TEST_VARIABLE\", \"FOO\")\n    .args([\"--dump\"])\n    .stdout(\"x := x'$JUST_TEST_VARIABLE'\\n\")\n    .success();\n}\n\n#[test]\nfn shell_expanded_strings_can_be_used_in_settings() {\n  Test::new()\n    .justfile(\n      \"\n        set dotenv-filename := x'$JUST_TEST_VARIABLE'\n\n        @foo:\n          echo $DOTENV_KEY\n      \",\n    )\n    .write(\".env\", \"DOTENV_KEY=dotenv-value\")\n    .env(\"JUST_TEST_VARIABLE\", \".env\")\n    .stdout(\"dotenv-value\\n\")\n    .success();\n}\n\n#[test]\nfn shell_expanded_strings_can_be_used_in_import_paths() {\n  Test::new()\n    .justfile(\n      \"\n        import x'$JUST_TEST_VARIABLE'\n\n        foo: bar\n      \",\n    )\n    .write(\"import.just\", \"@bar:\\n echo BAR\")\n    .env(\"JUST_TEST_VARIABLE\", \"import.just\")\n    .stdout(\"BAR\\n\")\n    .success();\n}\n\n#[test]\nfn shell_expanded_strings_can_be_used_in_mod_paths() {\n  Test::new()\n    .justfile(\n      \"\n        mod foo x'$JUST_TEST_VARIABLE'\n      \",\n    )\n    .write(\"mod.just\", \"@bar:\\n echo BAR\")\n    .env(\"JUST_TEST_VARIABLE\", \"mod.just\")\n    .args([\"foo\", \"bar\"])\n    .stdout(\"BAR\\n\")\n    .success();\n}\n\n#[test]\nfn shell_expanded_strings_do_not_conflict_with_dependencies() {\n  Test::new()\n    .justfile(\n      \"\n        foo a b:\n          @echo {{a}}{{b}}\n        bar a b: (foo a 'c')\n      \",\n    )\n    .args([\"bar\", \"A\", \"B\"])\n    .stdout(\"Ac\\n\")\n    .success();\n\n  Test::new()\n    .justfile(\n      \"\n        foo a b:\n          @echo {{a}}{{b}}\n        bar a b: (foo a'c')\n      \",\n    )\n    .args([\"bar\", \"A\", \"B\"])\n    .stdout(\"Ac\\n\")\n    .success();\n\n  Test::new()\n    .justfile(\n      \"\n        foo a b:\n          @echo {{a}}{{b}}\n        bar x b: (foo x 'c')\n      \",\n    )\n    .args([\"bar\", \"A\", \"B\"])\n    .stdout(\"Ac\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/show.rs",
    "content": "use super::*;\n\n#[test]\nfn show() {\n  Test::new()\n    .arg(\"--show\")\n    .arg(\"recipe\")\n    .justfile(\n      r#\"hello := \"foo\"\nbar := hello + hello\nrecipe:\n echo {{hello + \"bar\" + bar}}\"#,\n    )\n    .stdout(\n      r#\"\n    recipe:\n        echo {{ hello + \"bar\" + bar }}\n  \"#,\n    )\n    .success();\n}\n\n#[test]\nfn alias_show() {\n  Test::new()\n    .arg(\"--show\")\n    .arg(\"f\")\n    .justfile(\"foo:\\n    bar\\nalias f := foo\")\n    .stdout(\n      \"\n    alias f := foo\n    foo:\n        bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn alias_show_missing_target() {\n  Test::new()\n    .arg(\"--show\")\n    .arg(\"f\")\n    .justfile(\"alias f := foo\")\n    .stderr(\n      \"\n    error: Alias `f` has an unknown target `foo`\n     ——▶ justfile:1:7\n      │\n    1 │ alias f := foo\n      │       ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn show_suggestion() {\n  Test::new()\n    .arg(\"--show\")\n    .arg(\"hell\")\n    .justfile(\n      r#\"\nhello a b='B\t' c='C':\n  echo {{a}} {{b}} {{c}}\n\na Z=\"\\t z\":\n\"#,\n    )\n    .stderr(\"error: Justfile does not contain recipe `hell`\\nDid you mean `hello`?\\n\")\n    .failure();\n}\n\n#[test]\nfn show_alias_suggestion() {\n  Test::new()\n    .arg(\"--show\")\n    .arg(\"fo\")\n    .justfile(\n      r#\"\nhello a b='B\t' c='C':\n  echo {{a}} {{b}} {{c}}\n\nalias foo := hello\n\na Z=\"\\t z\":\n\"#,\n    )\n    .stderr(\n      \"\n    error: Justfile does not contain recipe `fo`\n    Did you mean `foo`, an alias for `hello`?\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn show_no_suggestion() {\n  Test::new()\n    .arg(\"--show\")\n    .arg(\"hell\")\n    .justfile(\n      r#\"\nhelloooooo a b='B\t' c='C':\n  echo {{a}} {{b}} {{c}}\n\na Z=\"\\t z\":\n\"#,\n    )\n    .stderr(\"error: Justfile does not contain recipe `hell`\\n\")\n    .failure();\n}\n\n#[test]\nfn show_no_alias_suggestion() {\n  Test::new()\n    .arg(\"--show\")\n    .arg(\"fooooooo\")\n    .justfile(\n      r#\"\nhello a b='B\t' c='C':\n  echo {{a}} {{b}} {{c}}\n\nalias foo := hello\n\na Z=\"\\t z\":\n\"#,\n    )\n    .stderr(\"error: Justfile does not contain recipe `fooooooo`\\n\")\n    .failure();\n}\n\n#[test]\nfn show_recipe_at_path() {\n  Test::new()\n    .write(\"foo.just\", \"bar:\\n @echo MODULE\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .args([\"--show\", \"foo::bar\"])\n    .stdout(\"bar:\\n    @echo MODULE\\n\")\n    .success();\n}\n\n#[test]\nfn show_invalid_path() {\n  Test::new()\n    .args([\"--show\", \"$hello\"])\n    .stderr(\"error: Invalid module path `$hello`\\n\")\n    .failure();\n}\n\n#[test]\nfn show_space_separated_path() {\n  Test::new()\n    .write(\"foo.just\", \"bar:\\n @echo MODULE\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .args([\"--show\", \"foo bar\"])\n    .stdout(\"bar:\\n    @echo MODULE\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/signals.rs",
    "content": "use {super::*, nix::sys::signal::Signal, nix::unistd::Pid, std::process::Child};\n\nfn kill(child: &Child, signal: Signal) {\n  nix::sys::signal::kill(Pid::from_raw(child.id().try_into().unwrap()), signal).unwrap();\n}\n\nfn interrupt_test(arguments: &[&str], justfile: &str) {\n  let tmp = tempdir();\n  let mut justfile_path = tmp.path().to_path_buf();\n  justfile_path.push(\"justfile\");\n  fs::write(justfile_path, unindent(justfile)).unwrap();\n\n  let start = Instant::now();\n\n  let mut child = Command::new(JUST)\n    .current_dir(&tmp)\n    .args(arguments)\n    .stdout(Stdio::piped())\n    .stderr(Stdio::piped())\n    .spawn()\n    .expect(\"just invocation failed\");\n\n  while start.elapsed() < Duration::from_millis(500) {}\n\n  kill(&child, Signal::SIGINT);\n\n  let status = child.wait().unwrap();\n\n  let elapsed = start.elapsed();\n\n  assert!(\n    elapsed <= Duration::from_secs(2),\n    \"process returned too late: {elapsed:?}\"\n  );\n\n  assert!(\n    elapsed >= Duration::from_millis(100),\n    \"process returned too early : {elapsed:?}\"\n  );\n\n  assert_eq!(status.code(), Some(130));\n}\n\n#[test]\n#[ignore]\nfn interrupt_shebang() {\n  interrupt_test(\n    &[],\n    \"\n        default:\n          #!/usr/bin/env sh\n          sleep 1\n      \",\n  );\n}\n\n#[test]\n#[ignore]\nfn interrupt_line() {\n  interrupt_test(\n    &[],\n    \"\n        default:\n          @sleep 1\n      \",\n  );\n}\n\n#[test]\n#[ignore]\nfn interrupt_backtick() {\n  interrupt_test(\n    &[],\n    \"\n        foo := `sleep 1`\n\n        default:\n          @echo {{foo}}\n      \",\n  );\n}\n\n#[test]\n#[ignore]\nfn interrupt_command() {\n  interrupt_test(&[\"--command\", \"sleep\", \"1\"], \"\");\n}\n\n// This test is ignored because it is sensitive to the process signal mask.\n// Programs like `watchexec` and `cargo-watch` change the signal mask to ignore\n// `SIGHUP`, which causes this test to fail.\n#[test]\n#[ignore]\nfn forwarding() {\n  let tempdir = tempdir();\n\n  fs::write(\n    tempdir.path().join(\"justfile\"),\n    \"foo:\\n @{{just_executable()}} --request '\\\"signal\\\"'\",\n  )\n  .unwrap();\n\n  for signal in [Signal::SIGINT, Signal::SIGQUIT, Signal::SIGHUP] {\n    let mut child = Command::new(JUST)\n      .current_dir(&tempdir)\n      .stdout(Stdio::piped())\n      .stderr(Stdio::piped())\n      .spawn()\n      .unwrap();\n\n    // wait for child to start\n    thread::sleep(Duration::from_millis(500));\n\n    // send non-forwarded signal\n    kill(&child, signal);\n\n    // wait for child to receive signal\n    thread::sleep(Duration::from_millis(500));\n\n    // assert that child does not exit, because signal is not forwarded\n    assert!(child.try_wait().unwrap().is_none());\n\n    // send forwarded signal\n    kill(&child, Signal::SIGTERM);\n\n    // child exits\n    let output = child.wait_with_output().unwrap();\n\n    let status = output.status;\n    let stderr = str::from_utf8(&output.stderr).unwrap();\n    let stdout = str::from_utf8(&output.stdout).unwrap();\n\n    let mut failures = 0;\n\n    if status.code() != Some(128 + signal as i32) {\n      failures += 1;\n      eprintln!(\"unexpected status: {status}\");\n    }\n\n    // just reports that it was interrupted by first, non-forwarded signal\n    if stderr != format!(\"error: Interrupted by {signal}\\n\") {\n      failures += 1;\n      eprintln!(\"unexpected stderr: {stderr}\");\n    }\n\n    // child reports that it was terminated by forwarded signal\n    if stdout != r#\"{\"signal\":\"SIGTERM\"}\"# {\n      failures += 1;\n      eprintln!(\"unexpected stdout: {stdout}\");\n    }\n\n    assert!(failures == 0, \"{failures} failures\");\n  }\n}\n\n#[test]\n#[ignore]\n#[cfg(any(\n  target_os = \"dragonfly\",\n  target_os = \"freebsd\",\n  target_os = \"ios\",\n  target_os = \"macos\",\n  target_os = \"netbsd\",\n  target_os = \"openbsd\",\n))]\nfn siginfo_prints_current_process() {\n  let tempdir = tempdir();\n\n  fs::write(tempdir.path().join(\"justfile\"), \"foo:\\n @sleep 1\").unwrap();\n\n  let child = Command::new(JUST)\n    .current_dir(&tempdir)\n    .stdout(Stdio::piped())\n    .stderr(Stdio::piped())\n    .spawn()\n    .unwrap();\n\n  thread::sleep(Duration::from_millis(500));\n\n  kill(&child, Signal::SIGINFO);\n\n  let output = child.wait_with_output().unwrap();\n\n  let status = output.status;\n  let stderr = str::from_utf8(&output.stderr).unwrap();\n  let stdout = str::from_utf8(&output.stdout).unwrap();\n\n  let mut failures = 0;\n\n  if !status.success() {\n    failures += 1;\n    eprintln!(\"unexpected status: {status}\");\n  }\n\n  let re =\n    Regex::new(r#\"just \\d+: 1 child process:\\n\\d+: cd \".*\" && \"sh\" \"-cu\" \"sleep 1\"\\n\"#).unwrap();\n\n  if !re.is_match(stderr) {\n    failures += 1;\n    eprintln!(\"unexpected stderr: {stderr}\");\n  }\n\n  if !stdout.is_empty() {\n    failures += 1;\n    eprintln!(\"unexpected stdout: {stdout}\");\n  }\n\n  assert!(failures == 0, \"{failures} failures\");\n}\n"
  },
  {
    "path": "tests/slash_operator.rs",
    "content": "use super::*;\n\n#[test]\nfn once() {\n  Test::new()\n    .justfile(\"x := 'a' / 'b'\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"a/b\")\n    .success();\n}\n\n#[test]\nfn twice() {\n  Test::new()\n    .justfile(\"x := 'a' / 'b' / 'c'\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"a/b/c\")\n    .success();\n}\n\n#[test]\nfn no_lhs_once() {\n  Test::new()\n    .justfile(\"x := / 'a'\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"/a\")\n    .success();\n}\n\n#[test]\nfn no_lhs_twice() {\n  Test::new()\n    .justfile(\"x := / 'a' / 'b'\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"/a/b\")\n    .success();\n  Test::new()\n    .justfile(\"x := // 'a'\")\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"//a\")\n    .success();\n}\n\n#[test]\nfn no_rhs_once() {\n  Test::new()\n    .justfile(\"x := 'a' /\")\n    .stderr(\n      \"\n      error: Expected backtick, identifier, '(', '/', or string, but found end of file\n       ——▶ justfile:1:11\n        │\n      1 │ x := 'a' /\n        │           ^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn default_un_parenthesized() {\n  Test::new()\n    .justfile(\n      \"\n      foo x='a' / 'b':\n        echo {{x}}\n    \",\n    )\n    .stderr(\n      \"\n      error: Expected '*', ':', '$', identifier, or '+', but found '/'\n       ——▶ justfile:1:11\n        │\n      1 │ foo x='a' / 'b':\n        │           ^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn no_lhs_un_parenthesized() {\n  Test::new()\n    .justfile(\n      \"\n      foo x=/ 'a' / 'b':\n        echo {{x}}\n    \",\n    )\n    .stderr(\n      \"\n      error: Expected backtick, identifier, '(', or string, but found '/'\n       ——▶ justfile:1:7\n        │\n      1 │ foo x=/ 'a' / 'b':\n        │       ^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn default_parenthesized() {\n  Test::new()\n    .justfile(\n      \"\n      foo x=('a' / 'b'):\n        echo {{x}}\n    \",\n    )\n    .stderr(\"echo a/b\\n\")\n    .stdout(\"a/b\\n\")\n    .success();\n}\n\n#[test]\nfn no_lhs_parenthesized() {\n  Test::new()\n    .justfile(\n      \"\n      foo x=(/ 'a' / 'b'):\n        echo {{x}}\n    \",\n    )\n    .stderr(\"echo /a/b\\n\")\n    .stdout(\"/a/b\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/string.rs",
    "content": "use super::*;\n\n#[test]\nfn raw_string() {\n  Test::new()\n    .justfile(\n      r#\"\nexport EXPORTED_VARIABLE := '\\z'\n\nrecipe:\n  printf \"$EXPORTED_VARIABLE\"\n\"#,\n    )\n    .stdout(\"\\\\z\")\n    .stderr(\"printf \\\"$EXPORTED_VARIABLE\\\"\\n\")\n    .success();\n}\n\n#[test]\nfn multiline_raw_string() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      \"\nstring := 'hello\nwhatever'\n\na:\n  echo '{{string}}'\n\",\n    )\n    .stdout(\n      \"hello\nwhatever\n\",\n    )\n    .stderr(\n      \"echo 'hello\nwhatever'\n\",\n    )\n    .success();\n}\n\n#[test]\nfn multiline_backtick() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      \"\nstring := `echo hello\necho goodbye\n`\n\na:\n  echo '{{string}}'\n\",\n    )\n    .stdout(\"hello\\ngoodbye\\n\")\n    .stderr(\n      \"echo 'hello\ngoodbye'\n\",\n    )\n    .success();\n}\n\n#[test]\nfn multiline_cooked_string() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      r#\"\nstring := \"hello\nwhatever\"\n\na:\n  echo '{{string}}'\n\"#,\n    )\n    .stdout(\n      \"hello\nwhatever\n\",\n    )\n    .stderr(\n      \"echo 'hello\nwhatever'\n\",\n    )\n    .success();\n}\n\n#[test]\nfn cooked_string_suppress_newline() {\n  Test::new()\n    .justfile(\n      r#\"\n    a := \"\"\"\n      foo\\\n      bar\n    \"\"\"\n\n    @default:\n      printf %s '{{a}}'\n  \"#,\n    )\n    .stdout(\n      \"\n    foobar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn invalid_escape_sequence() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      r#\"x := \"\\q\"\na:\"#,\n    )\n    .stderr(\n      \"error: `\\\\q` is not a valid escape sequence\n ——▶ justfile:1:6\n  │\n1 │ x := \\\"\\\\q\\\"\n  │      ^^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn error_line_after_multiline_raw_string() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      \"\nstring := 'hello\n\nwhatever' + 'yo'\n\na:\n  echo '{{foo}}'\n\",\n    )\n    .stderr(\n      \"error: Variable `foo` not defined\n ——▶ justfile:6:11\n  │\n6 │   echo '{{foo}}'\n  │           ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn error_column_after_multiline_raw_string() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      \"\nstring := 'hello\n\nwhatever' + bar\n\na:\n  echo '{{string}}'\n\",\n    )\n    .stderr(\n      \"error: Variable `bar` not defined\n ——▶ justfile:3:13\n  │\n3 │ whatever' + bar\n  │             ^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn multiline_raw_string_in_interpolation() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      r#\"\na:\n  echo '{{\"a\" + '\n  ' + \"b\"}}'\n\"#,\n    )\n    .stdout(\n      \"\n    a\n      b\n  \",\n    )\n    .stderr(\n      \"\n    echo 'a\n      b'\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn error_line_after_multiline_raw_string_in_interpolation() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      r#\"\na:\n  echo '{{\"a\" + '\n  ' + \"b\"}}'\n\n  echo {{b}}\n\"#,\n    )\n    .stderr(\n      \"error: Variable `b` not defined\n ——▶ justfile:5:10\n  │\n5 │   echo {{b}}\n  │          ^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn unterminated_raw_string() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      \"\n    a b= ':\n  \",\n    )\n    .stderr(\n      \"\n    error: Unterminated string\n     ——▶ justfile:1:6\n      │\n    1 │ a b= ':\n      │      ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn unterminated_string() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      r#\"\n    a b= \":\n  \"#,\n    )\n    .stderr(\n      r#\"\n    error: Unterminated string\n     ——▶ justfile:1:6\n      │\n    1 │ a b= \":\n      │      ^\n  \"#,\n    )\n    .failure();\n}\n\n#[test]\nfn unterminated_backtick() {\n  Test::new()\n    .justfile(\n      \"\n    foo a=\\t`echo blaaaaaah:\n      echo {{a}}\n  \",\n    )\n    .stderr(\n      r\"\n    error: Unterminated backtick\n     ——▶ justfile:1:8\n      │\n    1 │ foo a=    `echo blaaaaaah:\n      │           ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn unterminated_indented_raw_string() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      \"\n    a b= ''':\n  \",\n    )\n    .stderr(\n      \"\n    error: Unterminated string\n     ——▶ justfile:1:6\n      │\n    1 │ a b= ''':\n      │      ^^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn unterminated_indented_string() {\n  Test::new()\n    .arg(\"a\")\n    .justfile(\n      r#\"\n    a b= \"\"\":\n  \"#,\n    )\n    .stderr(\n      r#\"\n    error: Unterminated string\n     ——▶ justfile:1:6\n      │\n    1 │ a b= \"\"\":\n      │      ^^^\n  \"#,\n    )\n    .failure();\n}\n\n#[test]\nfn unterminated_indented_backtick() {\n  Test::new()\n    .justfile(\n      \"\n    foo a=\\t```echo blaaaaaah:\n      echo {{a}}\n  \",\n    )\n    .stderr(\n      r\"\n    error: Unterminated backtick\n     ——▶ justfile:1:8\n      │\n    1 │ foo a=    ```echo blaaaaaah:\n      │           ^^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn indented_raw_string_contents_indentation_removed() {\n  Test::new()\n    .justfile(\n      \"\n    a := '''\n      foo\n      bar\n    '''\n\n    @default:\n      printf '{{a}}'\n  \",\n    )\n    .stdout(\n      \"\n    foo\n    bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn indented_cooked_string_contents_indentation_removed() {\n  Test::new()\n    .justfile(\n      r#\"\n    a := \"\"\"\n      foo\n      bar\n    \"\"\"\n\n    @default:\n      printf '{{a}}'\n  \"#,\n    )\n    .stdout(\n      \"\n    foo\n    bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn indented_backtick_string_contents_indentation_removed() {\n  Test::new()\n    .justfile(\n      r\"\n    a := ```\n      printf '\n      foo\n      bar\n      '\n    ```\n\n    @default:\n      printf '{{a}}'\n  \",\n    )\n    .stdout(\"\\n\\nfoo\\nbar\")\n    .success();\n}\n\n#[test]\nfn indented_raw_string_escapes() {\n  Test::new()\n    .justfile(\n      r\"\n    a := '''\n      foo\\n\n      bar\n    '''\n\n    @default:\n      printf %s '{{a}}'\n  \",\n    )\n    .stdout(\n      r\"\n    foo\\n\n    bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn indented_cooked_string_escapes() {\n  Test::new()\n    .justfile(\n      r#\"\n    a := \"\"\"\n      foo\\n\n      bar\n    \"\"\"\n\n    @default:\n      printf %s '{{a}}'\n  \"#,\n    )\n    .stdout(\n      \"\n    foo\n\n    bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn indented_backtick_string_escapes() {\n  Test::new()\n    .justfile(\n      r\"\n    a := ```\n      printf %s '\n      foo\\n\n      bar\n      '\n    ```\n\n    @default:\n      printf %s '{{a}}'\n  \",\n    )\n    .stdout(\"\\n\\nfoo\\\\n\\nbar\")\n    .success();\n}\n\n#[test]\nfn shebang_backtick() {\n  Test::new()\n    .justfile(\n      \"\n    x := `#!/usr/bin/env sh`\n  \",\n    )\n    .stderr(\n      \"\n    error: Backticks may not start with `#!`\n     ——▶ justfile:1:6\n      │\n    1 │ x := `#!/usr/bin/env sh`\n      │      ^^^^^^^^^^^^^^^^^^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn valid_unicode_escape() {\n  Test::new()\n    .justfile(r#\"x := \"\\u{1f916}\\u{1F916}\"\"#)\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"🤖🤖\")\n    .success();\n}\n\n#[test]\nfn unicode_escapes_with_all_hex_digits() {\n  Test::new()\n    .justfile(r#\"x := \"\\u{012345}\\u{6789a}\\u{bcdef}\\u{ABCDE}\\u{F}\"\"#)\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"\\u{012345}\\u{6789a}\\u{bcdef}\\u{ABCDE}\\u{F}\")\n    .success();\n}\n\n#[test]\nfn maximum_valid_unicode_escape() {\n  Test::new()\n    .justfile(r#\"x := \"\\u{10FFFF}\"\"#)\n    .args([\"--evaluate\", \"x\"])\n    .stdout(\"\\u{10FFFF}\")\n    .success();\n}\n\n#[test]\nfn unicode_escape_no_braces() {\n  Test::new()\n    .justfile(\"x := \\\"\\\\u1234\\\"\")\n    .args([\"--evaluate\", \"x\"])\n    .stderr(\n      r#\"\nerror: expected unicode escape sequence delimiter `{` but found `1`\n ——▶ justfile:1:6\n  │\n1 │ x := \"\\u1234\"\n  │      ^^^^^^^^\n\"#,\n    )\n    .failure();\n}\n\n#[test]\nfn unicode_escape_empty() {\n  Test::new()\n    .justfile(\"x := \\\"\\\\u{}\\\"\")\n    .args([\"--evaluate\", \"x\"])\n    .stderr(\n      r#\"\nerror: unicode escape sequences must not be empty\n ——▶ justfile:1:6\n  │\n1 │ x := \"\\u{}\"\n  │      ^^^^^^\n\"#,\n    )\n    .failure();\n}\n\n#[test]\nfn unicode_escape_requires_immediate_opening_brace() {\n  Test::new()\n    .justfile(\"x := \\\"\\\\u {1f916}\\\"\")\n    .args([\"--evaluate\", \"x\"])\n    .stderr(\n      r#\"\nerror: expected unicode escape sequence delimiter `{` but found ` `\n ——▶ justfile:1:6\n  │\n1 │ x := \"\\u {1f916}\"\n  │      ^^^^^^^^^^^^\n\"#,\n    )\n    .failure();\n}\n\n#[test]\nfn unicode_escape_non_hex() {\n  Test::new()\n    .justfile(\"x := \\\"\\\\u{foo}\\\"\")\n    .args([\"--evaluate\", \"x\"])\n    .stderr(\n      r#\"\nerror: expected hex digit [0-9A-Fa-f] but found `o`\n ——▶ justfile:1:6\n  │\n1 │ x := \"\\u{foo}\"\n  │      ^^^^^^^^^\n\"#,\n    )\n    .failure();\n}\n\n#[test]\nfn unicode_escape_invalid_character() {\n  Test::new()\n    .justfile(\"x := \\\"\\\\u{BadBad}\\\"\")\n    .args([\"--evaluate\", \"x\"])\n    .stderr(\n      r#\"\nerror: unicode escape sequence value `BadBad` greater than maximum valid code point `10FFFF`\n ——▶ justfile:1:6\n  │\n1 │ x := \"\\u{BadBad}\"\n  │      ^^^^^^^^^^^^\n\"#,\n    )\n    .failure();\n}\n\n#[test]\nfn unicode_escape_too_long() {\n  Test::new()\n    .justfile(\"x := \\\"\\\\u{FFFFFFFFFF}\\\"\")\n    .args([\"--evaluate\", \"x\"])\n    .stderr(\n      r#\"\nerror: unicode escape sequence starting with `\\u{FFFFFFF` longer than six hex digits\n ——▶ justfile:1:6\n  │\n1 │ x := \"\\u{FFFFFFFFFF}\"\n  │      ^^^^^^^^^^^^^^^^\n\"#,\n    )\n    .failure();\n}\n\n#[test]\nfn unicode_escape_unterminated() {\n  Test::new()\n    .justfile(\"x := \\\"\\\\u{1f917\\\"\")\n    .args([\"--evaluate\", \"x\"])\n    .stderr(\n      r#\"\nerror: unterminated unicode escape sequence\n ——▶ justfile:1:6\n  │\n1 │ x := \"\\u{1f917\"\n  │      ^^^^^^^^^^\n\"#,\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/subsequents.rs",
    "content": "use super::*;\n\n#[test]\nfn success() {\n  Test::new()\n    .justfile(\n      \"\n    foo: && bar\n      echo foo\n\n    bar:\n      echo bar\n  \",\n    )\n    .stdout(\n      \"\n    foo\n    bar\n  \",\n    )\n    .stderr(\n      \"\n    echo foo\n    echo bar\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn failure() {\n  Test::new()\n    .justfile(\n      \"\n    foo: && bar\n      echo foo\n      false\n\n    bar:\n      echo bar\n  \",\n    )\n    .stdout(\n      \"\n    foo\n  \",\n    )\n    .stderr(\n      \"\n    echo foo\n    false\n    error: Recipe `foo` failed on line 3 with exit code 1\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn circular_dependency() {\n  Test::new()\n    .justfile(\n      \"\n    foo: && foo\n  \",\n    )\n    .stderr(\n      \"\n    error: Recipe `foo` depends on itself\n     ——▶ justfile:1:9\n      │\n    1 │ foo: && foo\n      │         ^^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown() {\n  Test::new()\n    .justfile(\n      \"\n    foo: && bar\n  \",\n    )\n    .stderr(\n      \"\n    error: Recipe `foo` has unknown dependency `bar`\n     ——▶ justfile:1:9\n      │\n    1 │ foo: && bar\n      │         ^^^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_argument() {\n  Test::new()\n    .justfile(\n      \"\n    bar x:\n\n    foo: && (bar y)\n  \",\n    )\n    .stderr(\n      \"\n    error: Variable `y` not defined\n     ——▶ justfile:3:14\n      │\n    3 │ foo: && (bar y)\n      │              ^\n  \",\n    )\n    .failure();\n}\n\n#[test]\nfn argument() {\n  Test::new()\n    .justfile(\n      \"\n    foo: && (bar 'hello')\n\n    bar x:\n      echo {{ x }}\n  \",\n    )\n    .stdout(\n      \"\n    hello\n  \",\n    )\n    .stderr(\n      \"\n    echo hello\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn duplicate_subsequents_dont_run() {\n  Test::new()\n    .justfile(\n      \"\n    a: && b c\n      echo a\n\n    b: d\n      echo b\n\n    c: d\n      echo c\n\n    d:\n      echo d\n  \",\n    )\n    .stdout(\n      \"\n    a\n    d\n    b\n    c\n  \",\n    )\n    .stderr(\n      \"\n    echo a\n    echo d\n    echo b\n    echo c\n  \",\n    )\n    .success();\n}\n\n#[test]\nfn subsequents_run_even_if_already_ran_as_prior() {\n  Test::new()\n    .justfile(\n      \"\n    a: b && b\n      echo a\n\n    b:\n      echo b\n  \",\n    )\n    .stdout(\n      \"\n    b\n    a\n    b\n  \",\n    )\n    .stderr(\n      \"\n    echo b\n    echo a\n    echo b\n  \",\n    )\n    .success();\n}\n"
  },
  {
    "path": "tests/summary.rs",
    "content": "use super::*;\n\n#[test]\nfn summary() {\n  Test::new()\n    .arg(\"--summary\")\n    .justfile(\n      \"b: a\na:\nd: c\nc: b\n_z: _y\n_y:\n\",\n    )\n    .stdout(\"a b c d\\n\")\n    .success();\n}\n\n#[test]\nfn summary_sorted() {\n  Test::new()\n    .arg(\"--summary\")\n    .justfile(\n      \"\nb:\nc:\na:\n\",\n    )\n    .stdout(\"a b c\\n\")\n    .success();\n}\n\n#[test]\nfn summary_unsorted() {\n  Test::new()\n    .arg(\"--summary\")\n    .arg(\"--unsorted\")\n    .justfile(\n      \"\nb:\nc:\na:\n\",\n    )\n    .stdout(\"b c a\\n\")\n    .success();\n}\n\n#[test]\nfn summary_none() {\n  Test::new()\n    .arg(\"--summary\")\n    .arg(\"--quiet\")\n    .justfile(\"\")\n    .stdout(\"\\n\\n\\n\")\n    .success();\n}\n\n#[test]\nfn no_recipes() {\n  Test::new()\n    .arg(\"--summary\")\n    .stderr(\"Justfile contains no recipes.\\n\")\n    .stdout(\"\\n\\n\\n\")\n    .success();\n}\n\n#[test]\nfn submodule_recipes() {\n  Test::new()\n    .write(\"foo.just\", \"mod bar\\nfoo:\")\n    .write(\"bar.just\", \"mod baz\\nbar:\")\n    .write(\"baz.just\", \"mod biz\\nbaz:\")\n    .write(\"biz.just\", \"biz:\")\n    .justfile(\n      \"\n        mod foo\n\n        bar:\n      \",\n    )\n    .arg(\"--summary\")\n    .stdout(\"bar foo::foo foo::bar::bar foo::bar::baz::baz foo::bar::baz::biz::biz\\n\")\n    .success();\n}\n\n#[test]\nfn summary_implies_unstable() {\n  Test::new()\n    .write(\"foo.just\", \"foo:\")\n    .justfile(\n      \"\n        mod foo\n      \",\n    )\n    .arg(\"--summary\")\n    .stdout(\"foo::foo\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/tempdir.rs",
    "content": "use super::*;\n\npub(crate) fn tempdir() -> TempDir {\n  let mut builder = tempfile::Builder::new();\n\n  builder.prefix(\"just-test-tempdir\");\n\n  if let Some(runtime_dir) = dirs::runtime_dir() {\n    let path = runtime_dir.join(\"just\");\n    fs::create_dir_all(&path).unwrap();\n    builder.tempdir_in(path)\n  } else {\n    builder.tempdir()\n  }\n  .expect(\"failed to create temporary directory\")\n}\n\n#[test]\nfn setting() {\n  Test::new()\n    .justfile(\n      \"\n      set tempdir := '.'\n      foo:\n          #!/usr/bin/env bash\n          cat just*/foo\n      \",\n    )\n    .shell(false)\n    .tree(tree! {\n      bar: {\n      }\n    })\n    .current_dir(\"bar\")\n    .stdout(if cfg!(windows) {\n      \"\n\n\n\n      cat just*/foo\n      \"\n    } else {\n      \"\n      #!/usr/bin/env bash\n\n\n      cat just*/foo\n      \"\n    })\n    .success();\n}\n\n#[test]\nfn argument_overrides_setting() {\n  Test::new()\n    .args([\"--tempdir\", \".\"])\n    .justfile(\n      \"\n      set tempdir := 'hello'\n      foo:\n          #!/usr/bin/env bash\n          cat just*/foo\n      \",\n    )\n    .shell(false)\n    .tree(tree! {\n      bar: {\n      }\n    })\n    .current_dir(\"bar\")\n    .stdout(if cfg!(windows) {\n      \"\n\n\n\n      cat just*/foo\n      \"\n    } else {\n      \"\n      #!/usr/bin/env bash\n\n\n      cat just*/foo\n      \"\n    })\n    .success();\n}\n"
  },
  {
    "path": "tests/test.rs",
    "content": "use {\n  super::*,\n  pretty_assertions::{StrComparison, assert_eq},\n};\n\npub(crate) struct Output {\n  pub(crate) pid: u32,\n  pub(crate) stdout: String,\n  pub(crate) tempdir: TempDir,\n}\n\n#[must_use]\npub(crate) struct Test {\n  pub(crate) args: Vec<String>,\n  pub(crate) current_dir: PathBuf,\n  pub(crate) env: BTreeMap<String, String>,\n  pub(crate) expected_files: BTreeMap<PathBuf, Vec<u8>>,\n  pub(crate) justfile: Option<String>,\n  pub(crate) response: Option<Response>,\n  pub(crate) shell: bool,\n  pub(crate) stderr: String,\n  pub(crate) stderr_regex: Option<Regex>,\n  pub(crate) stdin: String,\n  pub(crate) stdout: String,\n  pub(crate) stdout_regex: Option<Regex>,\n  pub(crate) tempdir: TempDir,\n  pub(crate) test_round_trip: bool,\n  pub(crate) unindent_stdout: bool,\n}\n\nimpl Test {\n  pub(crate) fn new() -> Self {\n    Self::with_tempdir(tempdir())\n  }\n\n  pub(crate) fn with_tempdir(tempdir: TempDir) -> Self {\n    Self {\n      args: Vec::new(),\n      current_dir: PathBuf::new(),\n      env: BTreeMap::new(),\n      expected_files: BTreeMap::new(),\n      justfile: Some(String::new()),\n      response: None,\n      shell: true,\n      stderr: String::new(),\n      stderr_regex: None,\n      stdin: String::new(),\n      stdout: String::new(),\n      stdout_regex: None,\n      tempdir,\n      test_round_trip: true,\n      unindent_stdout: true,\n    }\n  }\n\n  pub(crate) fn arg(mut self, val: &str) -> Self {\n    self.args.push(val.to_owned());\n    self\n  }\n\n  pub(crate) fn args<'a>(mut self, args: impl AsRef<[&'a str]>) -> Self {\n    for arg in args.as_ref() {\n      self = self.arg(arg);\n    }\n    self\n  }\n\n  pub(crate) fn create_dir(self, path: impl AsRef<Path>) -> Self {\n    fs::create_dir_all(self.tempdir.path().join(path)).unwrap();\n    self\n  }\n\n  pub(crate) fn current_dir(mut self, path: impl AsRef<Path>) -> Self {\n    path.as_ref().clone_into(&mut self.current_dir);\n    self\n  }\n\n  pub(crate) fn env(mut self, key: &str, val: &str) -> Self {\n    self.env.insert(key.to_string(), val.to_string());\n    self\n  }\n\n  pub(crate) fn justfile(mut self, justfile: impl Into<String>) -> Self {\n    self.justfile = Some(justfile.into());\n    self\n  }\n\n  pub(crate) fn justfile_path(&self) -> PathBuf {\n    self.tempdir.path().join(\"justfile\")\n  }\n\n  #[cfg(unix)]\n  #[track_caller]\n  pub(crate) fn symlink(self, original: &str, link: &str) -> Self {\n    std::os::unix::fs::symlink(\n      self.tempdir.path().join(original),\n      self.tempdir.path().join(link),\n    )\n    .unwrap();\n    self\n  }\n\n  pub(crate) fn no_justfile(mut self) -> Self {\n    self.justfile = None;\n    self\n  }\n\n  pub(crate) fn response(mut self, response: Response) -> Self {\n    self.response = Some(response);\n    self.stdout_regex(\".*\")\n  }\n\n  pub(crate) fn shell(mut self, shell: bool) -> Self {\n    self.shell = shell;\n    self\n  }\n\n  pub(crate) fn stderr(mut self, stderr: impl Into<String>) -> Self {\n    self.stderr = stderr.into();\n    self\n  }\n\n  pub(crate) fn stderr_regex(mut self, stderr_regex: impl AsRef<str>) -> Self {\n    self.stderr_regex = Some(Regex::new(&format!(\"^(?s){}$\", stderr_regex.as_ref())).unwrap());\n    self\n  }\n\n  pub(crate) fn stdin(mut self, stdin: impl Into<String>) -> Self {\n    self.stdin = stdin.into();\n    self\n  }\n\n  pub(crate) fn stdout(mut self, stdout: impl Into<String>) -> Self {\n    self.stdout = stdout.into();\n    self\n  }\n\n  pub(crate) fn stdout_regex(mut self, stdout_regex: impl AsRef<str>) -> Self {\n    self.stdout_regex = Some(Regex::new(&format!(\"(?s)^{}$\", stdout_regex.as_ref())).unwrap());\n    self\n  }\n\n  pub(crate) fn test_round_trip(mut self, test_round_trip: bool) -> Self {\n    self.test_round_trip = test_round_trip;\n    self\n  }\n\n  pub(crate) fn tree(self, mut tree: Tree) -> Self {\n    tree.map(|_name, content| unindent(content));\n    tree.instantiate(self.tempdir.path()).unwrap();\n    self\n  }\n\n  pub(crate) fn unindent_stdout(mut self, unindent_stdout: bool) -> Self {\n    self.unindent_stdout = unindent_stdout;\n    self\n  }\n\n  pub(crate) fn write(self, path: impl AsRef<Path>, content: impl AsRef<[u8]>) -> Self {\n    let path = self.tempdir.path().join(path);\n    fs::create_dir_all(path.parent().unwrap()).unwrap();\n    fs::write(path, content).unwrap();\n    self\n  }\n\n  pub(crate) fn make_executable(self, path: impl AsRef<Path>) -> Self {\n    let file = self.tempdir.path().join(path);\n\n    // Make sure it exists first, as a sanity check.\n    assert!(file.exists(), \"file does not exist: {}\", file.display());\n\n    // Windows uses file extensions to determine whether a file is executable.\n    // Other systems don't care. To keep these tests cross-platform, just make\n    // sure all executables end with \".exe\" suffix.\n    assert!(\n      file.extension() == Some(\"exe\".as_ref()),\n      \"executable file does not end with .exe: {}\",\n      file.display()\n    );\n\n    #[cfg(unix)]\n    {\n      let perms = std::os::unix::fs::PermissionsExt::from_mode(0o755);\n      fs::set_permissions(file, perms).unwrap();\n    }\n\n    self\n  }\n\n  pub(crate) fn expect_file(mut self, path: impl AsRef<Path>, content: impl AsRef<[u8]>) -> Self {\n    let path = path.as_ref();\n    self\n      .expected_files\n      .insert(path.into(), content.as_ref().into());\n    self\n  }\n\n  #[track_caller]\n  pub(crate) fn success(self) -> Output {\n    self.status(0)\n  }\n\n  #[track_caller]\n  pub(crate) fn failure(self) -> Output {\n    self.status(1)\n  }\n\n  #[track_caller]\n  pub(crate) fn status(self, status: i32) -> Output {\n    fn compare<T: PartialEq + Debug>(name: &str, have: T, want: T) -> bool {\n      let equal = have == want;\n      if !equal {\n        eprintln!(\"Bad {name}: {}\", Comparison::new(&have, &want));\n      }\n      equal\n    }\n\n    fn compare_string(name: &str, have: &str, want: &str) -> bool {\n      let equal = have == want;\n      if !equal {\n        eprintln!(\"Bad {name}: {}\", StrComparison::new(&have, &want));\n      }\n      equal\n    }\n\n    if let Some(justfile) = &self.justfile {\n      let justfile = unindent(justfile);\n      fs::write(self.justfile_path(), justfile).unwrap();\n    }\n\n    let stdout = if self.unindent_stdout {\n      unindent(&self.stdout)\n    } else {\n      self.stdout.clone()\n    };\n\n    let stderr = unindent(&self.stderr);\n\n    let mut command = Command::new(JUST);\n\n    if self.shell {\n      command.args([\"--shell\", \"bash\"]);\n    }\n\n    let mut child = command\n      .args(&self.args)\n      .envs(&self.env)\n      .current_dir(self.tempdir.path().join(&self.current_dir))\n      .stdin(Stdio::piped())\n      .stdout(Stdio::piped())\n      .stderr(Stdio::piped())\n      .spawn()\n      .expect(\"just invocation failed\");\n\n    let pid = child.id();\n\n    {\n      let mut stdin_handle = child.stdin.take().expect(\"failed to unwrap stdin handle\");\n\n      stdin_handle\n        .write_all(self.stdin.as_bytes())\n        .expect(\"failed to write stdin to just process\");\n    }\n\n    let output = child\n      .wait_with_output()\n      .expect(\"failed to wait for just process\");\n\n    let output_stdout = str::from_utf8(&output.stdout).unwrap();\n    let output_stderr = str::from_utf8(&output.stderr).unwrap();\n\n    if let Some(ref stdout_regex) = self.stdout_regex {\n      assert!(\n        stdout_regex.is_match(output_stdout),\n        \"Stdout regex mismatch:\\n{output_stdout:?}\\n!~=\\n/{stdout_regex:?}/\",\n      );\n    }\n\n    if let Some(ref stderr_regex) = self.stderr_regex {\n      assert!(\n        stderr_regex.is_match(output_stderr),\n        \"Stderr regex mismatch:\\n{output_stderr:?}\\n!~=\\n/{stderr_regex:?}/\",\n      );\n    }\n\n    if !compare(\"status\", output.status.code(), Some(status))\n      | (self.stdout_regex.is_none() && !compare_string(\"stdout\", output_stdout, &stdout))\n      | (self.stderr_regex.is_none() && !compare_string(\"stderr\", output_stderr, &stderr))\n    {\n      panic!(\"Output mismatch.\");\n    }\n\n    if let Some(ref response) = self.response {\n      assert_eq!(\n        &serde_json::from_str::<Response>(output_stdout)\n          .expect(\"failed to deserialize stdout as response\"),\n        response,\n        \"response mismatch\"\n      );\n    }\n\n    for (path, expected) in &self.expected_files {\n      let actual = fs::read(self.tempdir.path().join(path)).unwrap();\n      assert_eq!(\n        actual,\n        expected.as_slice(),\n        \"mismatch for expected file at path {}\",\n        path.display(),\n      );\n    }\n\n    if self.test_round_trip && status == 0 {\n      self.round_trip();\n    }\n\n    Output {\n      pid,\n      stdout: output_stdout.into(),\n      tempdir: self.tempdir,\n    }\n  }\n\n  fn round_trip(&self) {\n    let output = Command::new(JUST)\n      .current_dir(self.tempdir.path())\n      .arg(\"--dump\")\n      .envs(&self.env)\n      .output()\n      .expect(\"just invocation failed\");\n\n    assert!(\n      output.status.success(),\n      \"dump failed: {} {:?}\",\n      output.status,\n      output,\n    );\n\n    let dumped = String::from_utf8(output.stdout).unwrap();\n\n    let reparsed_path = self.tempdir.path().join(\"reparsed.just\");\n\n    fs::write(&reparsed_path, &dumped).unwrap();\n\n    let output = Command::new(JUST)\n      .current_dir(self.tempdir.path())\n      .arg(\"--justfile\")\n      .arg(&reparsed_path)\n      .arg(\"--dump\")\n      .envs(&self.env)\n      .output()\n      .expect(\"just invocation failed\");\n\n    assert!(output.status.success(), \"reparse failed: {}\", output.status);\n\n    let reparsed = String::from_utf8(output.stdout).unwrap();\n\n    assert_eq!(reparsed, dumped, \"reparse mismatch\");\n  }\n}\n\npub(crate) fn assert_eval_eq(expression: &str, result: &str) {\n  Test::new()\n    .justfile(format!(\"x := {expression}\"))\n    .args([\"--evaluate\", \"x\"])\n    .stdout(result)\n    .unindent_stdout(false)\n    .success();\n}\n"
  },
  {
    "path": "tests/timestamps.rs",
    "content": "use super::*;\n\n#[test]\nfn quiet() {\n  Test::new()\n    .justfile(\n      \"\n      set quiet\n      recipe:\n        echo foo\n    \",\n    )\n    .arg(\"--timestamp\")\n    .stderr_regex(concat!(r\"\\[\\d\\d:\\d\\d:\\d\\d\\] echo foo\", \"\\n\"))\n    .stdout(\"foo\\n\")\n    .success();\n}\n\n#[test]\nfn linewise() {\n  Test::new()\n    .justfile(\n      \"\n     recipe:\n        echo 'one'\n    \",\n    )\n    .arg(\"--timestamp\")\n    .stderr_regex(concat!(r\"\\[\\d\\d:\\d\\d:\\d\\d\\] echo 'one'\", \"\\n\"))\n    .stdout(\"one\\n\")\n    .success();\n}\n\n#[test]\nfn script() {\n  Test::new()\n    .justfile(\n      \"\n     recipe:\n        #!/bin/sh\n        echo 'one'\n    \",\n    )\n    .arg(\"--timestamp\")\n    .stderr_regex(concat!(r\"\\[\\d\\d:\\d\\d:\\d\\d\\] recipe\", \"\\n\"))\n    .stdout(\"one\\n\")\n    .success();\n}\n\n#[test]\nfn format_string() {\n  Test::new()\n    .justfile(\n      \"\n     recipe:\n        echo 'one'\n    \",\n    )\n    .args([\"--timestamp\", \"--timestamp-format\", \"%H:%M:%S.%3f\"])\n    .stderr_regex(concat!(r\"\\[\\d\\d:\\d\\d:\\d\\d\\.\\d\\d\\d] echo 'one'\", \"\\n\"))\n    .stdout(\"one\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/undefined_variables.rs",
    "content": "use super::*;\n\n#[test]\nfn parameter_default_unknown_variable_in_expression() {\n  Test::new()\n    .justfile(\"foo a=(b+''):\")\n    .stderr(\n      \"\n      error: Variable `b` not defined\n       ——▶ justfile:1:8\n        │\n      1 │ foo a=(b+''):\n        │        ^\n    \",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_variable_in_unary_call() {\n  Test::new()\n    .justfile(\n      \"\n    foo x=env_var(a):\n  \",\n    )\n    .stderr(\n      \"\n      error: Variable `a` not defined\n       ——▶ justfile:1:15\n        │\n      1 │ foo x=env_var(a):\n        │               ^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_first_variable_in_binary_call() {\n  Test::new()\n    .justfile(\n      \"\n    foo x=env_var_or_default(a, b):\n  \",\n    )\n    .stderr(\n      \"\n      error: Variable `a` not defined\n       ——▶ justfile:1:26\n        │\n      1 │ foo x=env_var_or_default(a, b):\n        │                          ^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_second_variable_in_binary_call() {\n  Test::new()\n    .justfile(\n      \"\n    foo x=env_var_or_default('', b):\n  \",\n    )\n    .stderr(\n      \"\n      error: Variable `b` not defined\n       ——▶ justfile:1:30\n        │\n      1 │ foo x=env_var_or_default('', b):\n        │                              ^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn unknown_variable_in_ternary_call() {\n  Test::new()\n    .justfile(\n      \"\n    foo x=replace(a, b, c):\n  \",\n    )\n    .stderr(\n      \"\n      error: Variable `a` not defined\n       ——▶ justfile:1:15\n        │\n      1 │ foo x=replace(a, b, c):\n        │               ^\n      \",\n    )\n    .failure();\n}\n"
  },
  {
    "path": "tests/unexport.rs",
    "content": "use super::*;\n\n#[test]\nfn unexport_environment_variable_linewise() {\n  Test::new()\n    .justfile(\n      \"\n     unexport JUST_TEST_VARIABLE\n\n     @recipe:\n         echo ${JUST_TEST_VARIABLE:-unset}\n      \",\n    )\n    .env(\"JUST_TEST_VARIABLE\", \"foo\")\n    .stdout(\"unset\\n\")\n    .success();\n}\n\n#[test]\nfn unexport_environment_variable_shebang() {\n  Test::new()\n    .justfile(\n      \"\n     unexport JUST_TEST_VARIABLE\n\n     recipe:\n         #!/usr/bin/env bash\n         echo ${JUST_TEST_VARIABLE:-unset}\n      \",\n    )\n    .env(\"JUST_TEST_VARIABLE\", \"foo\")\n    .stdout(\"unset\\n\")\n    .success();\n}\n\n#[test]\nfn duplicate_unexport_fails() {\n  Test::new()\n    .justfile(\n      \"\n     unexport JUST_TEST_VARIABLE\n\n     recipe:\n         echo \\\"variable: $JUST_TEST_VARIABLE\\\"\n\n     unexport JUST_TEST_VARIABLE\n      \",\n    )\n    .env(\"JUST_TEST_VARIABLE\", \"foo\")\n    .stderr(\n      \"\n        error: Variable `JUST_TEST_VARIABLE` is unexported multiple times\n         ——▶ justfile:6:10\n          │\n        6 │ unexport JUST_TEST_VARIABLE\n          │          ^^^^^^^^^^^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn export_unexport_conflict() {\n  Test::new()\n    .justfile(\n      \"\n     unexport JUST_TEST_VARIABLE\n\n     recipe:\n         echo variable: $JUST_TEST_VARIABLE\n\n     export JUST_TEST_VARIABLE := 'foo'\n      \",\n    )\n    .stderr(\n      \"\n        error: Variable JUST_TEST_VARIABLE is both exported and unexported\n         ——▶ justfile:6:8\n          │\n        6 │ export JUST_TEST_VARIABLE := 'foo'\n          │        ^^^^^^^^^^^^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn unexport_doesnt_override_local_recipe_export() {\n  Test::new()\n    .justfile(\n      \"\n     unexport JUST_TEST_VARIABLE\n\n     recipe $JUST_TEST_VARIABLE:\n         @echo \\\"variable: $JUST_TEST_VARIABLE\\\"\n      \",\n    )\n    .args([\"recipe\", \"value\"])\n    .stdout(\"variable: value\\n\")\n    .success();\n}\n\n#[test]\nfn unexport_does_not_conflict_with_recipe_syntax() {\n  Test::new()\n    .justfile(\n      \"\n        unexport foo:\n          @echo {{foo}}\n      \",\n    )\n    .args([\"unexport\", \"bar\"])\n    .stdout(\"bar\\n\")\n    .success();\n}\n\n#[test]\nfn unexport_does_not_conflict_with_assignment_syntax() {\n  Test::new()\n    .justfile(\"unexport := 'foo'\")\n    .args([\"--evaluate\", \"unexport\"])\n    .stdout(\"foo\")\n    .success();\n}\n"
  },
  {
    "path": "tests/unstable.rs",
    "content": "use super::*;\n\n#[test]\nfn set_unstable_true_with_env_var() {\n  for val in [\"true\", \"some-arbitrary-string\"] {\n    Test::new()\n      .justfile(\"# hello\")\n      .args([\"--fmt\"])\n      .env(\"JUST_UNSTABLE\", val)\n      .stderr_regex(\"Wrote justfile to `.*`\\n\")\n      .success();\n  }\n}\n\n#[test]\nfn set_unstable_false_with_env_var() {\n  for val in [\"0\", \"\", \"false\"] {\n    Test::new()\n      .justfile(\"\")\n      .args([\"--fmt\"])\n      .env(\"JUST_UNSTABLE\", val)\n      .stderr_regex(\"error: The `--fmt` command is currently unstable.*\")\n      .failure();\n  }\n}\n\n#[test]\nfn set_unstable_false_with_env_var_unset() {\n  Test::new()\n    .justfile(\"\")\n    .args([\"--fmt\"])\n    .stderr_regex(\"error: The `--fmt` command is currently unstable.*\")\n    .failure();\n}\n\n#[test]\nfn set_unstable_with_setting() {\n  Test::new()\n    .justfile(\"set unstable\")\n    .arg(\"--fmt\")\n    .stderr_regex(\"Wrote justfile to .*\")\n    .success();\n}\n\n// This test should be re-enabled if we get a new unstable feature which is\n// encountered in source files. (As opposed to, for example, the unstable\n// `--fmt` subcommand, which is encountered on the command line.)\n#[cfg(any())]\n#[test]\nfn unstable_setting_does_not_affect_submodules() {\n  Test::new()\n    .justfile(\n      \"\n        set unstable\n\n        mod foo\n      \",\n    )\n    .write(\"foo.just\", \"mod bar\")\n    .write(\"bar.just\", \"baz:\\n echo hello\")\n    .args([\"foo\", \"bar\"])\n    .stderr_regex(\"error: Modules are currently unstable.*\")\n    .failure();\n}\n"
  },
  {
    "path": "tests/usage.rs",
    "content": "use super::*;\n\n#[test]\nfn usage() {\n  Test::new()\n    .justfile(\"mod bar\")\n    .write(\n      \"bar.just\",\n      \"\n[arg('a', short='a')]\n[arg('b', pattern='123|789', help='hello')]\n[arg('d', short='d', long='delightful')]\n[arg('e', short='e', pattern='abc|xyz')]\n[arg('f', long='f', pattern='lucky')]\n[arg('g', short='g', value='foo')]\nfoo a b c='abc' d e f='xyz' g='bar' *h:\n\",\n    )\n    .args([\"--usage\", \"bar\", \"foo\"])\n    .stdout(\n      \"\n        Usage: just bar foo [OPTIONS] b [c] [h...]\n\n        Arguments:\n          b hello [pattern: '123|789']\n          [c] [default: 'abc']\n          [h...]\n\n        Options:\n          -a a\n          -d, --delightful d\n          -e e [pattern: 'abc|xyz']\n              --f f [default: 'xyz'] [pattern: 'lucky']\n          -g\n      \",\n    )\n    .success();\n}\n"
  },
  {
    "path": "tests/which_function.rs",
    "content": "use super::*;\n\nconst HELLO_SCRIPT: &str = \"#!/usr/bin/env bash\necho hello\n\";\n\n#[test]\nfn finds_executable() {\n  let tmp = tempdir();\n  let path = PathBuf::from(tmp.path());\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := which('hello.exe')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"hello.exe\", HELLO_SCRIPT)\n    .make_executable(\"hello.exe\")\n    .env(\"PATH\", path.to_str().unwrap())\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stdout(path.join(\"hello.exe\").display().to_string())\n    .success();\n}\n\n#[test]\nfn prints_empty_string_for_missing_executable() {\n  let tmp = tempdir();\n  let path = PathBuf::from(tmp.path());\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := which('goodbye.exe')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"hello.exe\", HELLO_SCRIPT)\n    .make_executable(\"hello.exe\")\n    .env(\"PATH\", path.to_str().unwrap())\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .success();\n}\n\n#[test]\nfn skips_non_executable_files() {\n  let tmp = tempdir();\n  let path = PathBuf::from(tmp.path());\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := which('hi')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"hello.exe\", HELLO_SCRIPT)\n    .make_executable(\"hello.exe\")\n    .write(\"hi\", \"just some regular file\")\n    .env(\"PATH\", path.to_str().unwrap())\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .success();\n}\n\n#[test]\nfn supports_multiple_paths() {\n  let tmp = tempdir();\n  let path = PathBuf::from(tmp.path());\n  let path_var = env::join_paths([\n    path.join(\"subdir1\").to_str().unwrap(),\n    path.join(\"subdir2\").to_str().unwrap(),\n  ])\n  .unwrap();\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := which('hello1.exe') + '+' + which('hello2.exe')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"subdir1/hello1.exe\", HELLO_SCRIPT)\n    .make_executable(\"subdir1/hello1.exe\")\n    .write(\"subdir2/hello2.exe\", HELLO_SCRIPT)\n    .make_executable(\"subdir2/hello2.exe\")\n    .env(\"PATH\", path_var.to_str().unwrap())\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stdout(format!(\n      \"{}+{}\",\n      path.join(\"subdir1\").join(\"hello1.exe\").display(),\n      path.join(\"subdir2\").join(\"hello2.exe\").display(),\n    ))\n    .success();\n}\n\n#[test]\nfn supports_shadowed_executables() {\n  enum Variation {\n    Dir1Dir2, // PATH=/tmp/.../dir1:/tmp/.../dir2\n    Dir2Dir1, // PATH=/tmp/.../dir2:/tmp/.../dir1\n  }\n\n  for variation in [Variation::Dir1Dir2, Variation::Dir2Dir1] {\n    let tmp = tempdir();\n    let path = PathBuf::from(tmp.path());\n\n    let path_var = match variation {\n      Variation::Dir1Dir2 => env::join_paths([\n        path.join(\"dir1\").to_str().unwrap(),\n        path.join(\"dir2\").to_str().unwrap(),\n      ]),\n      Variation::Dir2Dir1 => env::join_paths([\n        path.join(\"dir2\").to_str().unwrap(),\n        path.join(\"dir1\").to_str().unwrap(),\n      ]),\n    }\n    .unwrap();\n\n    let stdout = match variation {\n      Variation::Dir1Dir2 => format!(\"{}\", path.join(\"dir1\").join(\"shadowed.exe\").display()),\n      Variation::Dir2Dir1 => format!(\"{}\", path.join(\"dir2\").join(\"shadowed.exe\").display()),\n    };\n\n    Test::with_tempdir(tmp)\n      .justfile(\"p := which('shadowed.exe')\")\n      .args([\"--evaluate\", \"p\"])\n      .write(\"dir1/shadowed.exe\", HELLO_SCRIPT)\n      .make_executable(\"dir1/shadowed.exe\")\n      .write(\"dir2/shadowed.exe\", HELLO_SCRIPT)\n      .make_executable(\"dir2/shadowed.exe\")\n      .env(\"PATH\", path_var.to_str().unwrap())\n      .env(\"JUST_UNSTABLE\", \"1\")\n      .stdout(stdout)\n      .success();\n  }\n}\n\n#[test]\nfn ignores_nonexecutable_candidates() {\n  let tmp = tempdir();\n  let path = PathBuf::from(tmp.path());\n\n  let path_var = env::join_paths([\n    path.join(\"dummy\").to_str().unwrap(),\n    path.join(\"subdir\").to_str().unwrap(),\n    path.join(\"dummy\").to_str().unwrap(),\n  ])\n  .unwrap();\n\n  let dummy_exe = if cfg!(windows) {\n    \"dummy/foo\"\n  } else {\n    \"dummy/foo.exe\"\n  };\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := which('foo.exe')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"subdir/foo.exe\", HELLO_SCRIPT)\n    .make_executable(\"subdir/foo.exe\")\n    .write(dummy_exe, HELLO_SCRIPT)\n    .env(\"PATH\", path_var.to_str().unwrap())\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stdout(path.join(\"subdir\").join(\"foo.exe\").display().to_string())\n    .success();\n}\n\n#[test]\nfn handles_absolute_path() {\n  let tmp = tempdir();\n  let path = PathBuf::from(tmp.path());\n  let abspath = path.join(\"subdir\").join(\"foo.exe\");\n\n  Test::with_tempdir(tmp)\n    .justfile(format!(\"p := which('{}')\", abspath.display()))\n    .write(\"subdir/foo.exe\", HELLO_SCRIPT)\n    .make_executable(\"subdir/foo.exe\")\n    .write(\"pathdir/foo.exe\", HELLO_SCRIPT)\n    .make_executable(\"pathdir/foo.exe\")\n    .env(\"PATH\", path.join(\"pathdir\").to_str().unwrap())\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .args([\"--evaluate\", \"p\"])\n    .stdout(abspath.display().to_string())\n    .success();\n}\n\n#[test]\nfn handles_dotslash() {\n  let tmp = tempdir();\n\n  let path = if cfg!(windows) {\n    tmp.path().into()\n  } else {\n    // canonicalize() is necessary here to account for the justfile prepending\n    // the canonicalized working directory to 'subdir/foo.exe'.\n    tmp.path().canonicalize().unwrap()\n  };\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := which('./foo.exe')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"foo.exe\", HELLO_SCRIPT)\n    .make_executable(\"foo.exe\")\n    .write(\"pathdir/foo.exe\", HELLO_SCRIPT)\n    .make_executable(\"pathdir/foo.exe\")\n    .env(\"PATH\", path.join(\"pathdir\").to_str().unwrap())\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stdout(path.join(\"foo.exe\").display().to_string())\n    .success();\n}\n\n#[test]\nfn handles_dir_slash() {\n  let tmp = tempdir();\n\n  let path = if cfg!(windows) {\n    tmp.path().into()\n  } else {\n    // canonicalize() is necessary here to account for the justfile prepending\n    // the canonicalized working directory to 'subdir/foo.exe'.\n    tmp.path().canonicalize().unwrap()\n  };\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := which('subdir/foo.exe')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"subdir/foo.exe\", HELLO_SCRIPT)\n    .make_executable(\"subdir/foo.exe\")\n    .write(\"pathdir/foo.exe\", HELLO_SCRIPT)\n    .make_executable(\"pathdir/foo.exe\")\n    .env(\"PATH\", path.join(\"pathdir\").to_str().unwrap())\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stdout(path.join(\"subdir\").join(\"foo.exe\").display().to_string())\n    .success();\n}\n\n#[test]\nfn is_unstable() {\n  let tmp = tempdir();\n  let path = PathBuf::from(tmp.path());\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := which('hello.exe')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"hello.exe\", HELLO_SCRIPT)\n    .make_executable(\"hello.exe\")\n    .env(\"PATH\", path.to_str().unwrap())\n    .stderr_regex(r\".*The `which\\(\\)` function is currently unstable\\..*\")\n    .failure();\n}\n\n#[test]\nfn require_error() {\n  Test::new()\n    .justfile(\"p := require('asdfasdf')\")\n    .args([\"--evaluate\", \"p\"])\n    .stderr(\n      \"\n        error: Call to function `require` failed: could not find executable `asdfasdf`\n         ——▶ justfile:1:6\n          │\n        1 │ p := require('asdfasdf')\n          │      ^^^^^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn finds_executable_via_pathext() {\n  if !cfg!(windows) {\n    return;\n  }\n\n  let tmp = tempdir();\n  let path = PathBuf::from(tmp.path());\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := which('foo')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"foo.exe\", HELLO_SCRIPT)\n    .make_executable(\"foo.exe\")\n    .env(\"PATH\", path.to_str().unwrap())\n    .env(\"PATHEXT\", \".exe\")\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stdout(path.join(\"foo.exe\").display().to_string())\n    .success();\n}\n\n#[test]\nfn pathext_not_applied_when_candidate_has_extension() {\n  if !cfg!(windows) {\n    return;\n  }\n\n  let tmp = tempdir();\n  let path = PathBuf::from(tmp.path());\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := which('foo.bat')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"foo.bat.exe\", HELLO_SCRIPT)\n    .make_executable(\"foo.bat.exe\")\n    .env(\"PATH\", path.to_str().unwrap())\n    .env(\"PATHEXT\", \".EXE\")\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .success();\n}\n\n#[test]\nfn pathext_custom_extension() {\n  if !cfg!(windows) {\n    return;\n  }\n\n  let tmp = tempdir();\n  let path = PathBuf::from(tmp.path());\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := which('foo')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"foo.bar\", HELLO_SCRIPT)\n    .env(\"PATH\", path.to_str().unwrap())\n    .env(\"PATHEXT\", \".BAR\")\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stdout(path.join(\"foo.BAR\").display().to_string())\n    .success();\n}\n\n#[test]\nfn pathext_entry_missing_dot_is_error() {\n  if !cfg!(windows) {\n    return;\n  }\n\n  let tmp = tempdir();\n  let path = PathBuf::from(tmp.path());\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := which('foo')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"foo.exe\", HELLO_SCRIPT)\n    .make_executable(\"foo.exe\")\n    .env(\"PATH\", path.to_str().unwrap())\n    .env(\"PATHEXT\", \"EXE\")\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .stderr_regex(\".*`PATHEXT` entry `EXE` does not start with `.`.*\")\n    .failure();\n}\n\n#[test]\nfn pathext_ignored_on_non_windows() {\n  if cfg!(windows) {\n    return;\n  }\n\n  let tmp = tempdir();\n  let path = PathBuf::from(tmp.path());\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := which('foo')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"foo.exe\", HELLO_SCRIPT)\n    .make_executable(\"foo.exe\")\n    .env(\"PATH\", path.to_str().unwrap())\n    .env(\"PATHEXT\", \".EXE\")\n    .env(\"JUST_UNSTABLE\", \"1\")\n    .success();\n}\n\n#[test]\nfn require_success() {\n  let tmp = tempdir();\n  let path = PathBuf::from(tmp.path());\n\n  Test::with_tempdir(tmp)\n    .justfile(\"p := require('hello.exe')\")\n    .args([\"--evaluate\", \"p\"])\n    .write(\"hello.exe\", HELLO_SCRIPT)\n    .make_executable(\"hello.exe\")\n    .env(\"PATH\", path.to_str().unwrap())\n    .stdout(path.join(\"hello.exe\").display().to_string())\n    .success();\n}\n"
  },
  {
    "path": "tests/windows.rs",
    "content": "use super::*;\n\n#[test]\nfn bare_bash_in_shebang() {\n  Test::new()\n    .justfile(\n      \"\n        default:\n            #!bash\n            echo FOO\n      \",\n    )\n    .stdout(\"FOO\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/windows_shell.rs",
    "content": "use super::*;\n\n#[test]\nfn windows_shell_setting() {\n  Test::new()\n    .justfile(\n      r#\"\n      set windows-shell := [\"pwsh.exe\", \"-NoLogo\", \"-Command\"]\n      set shell := [\"asdfasdfasdfasdf\"]\n\n      foo:\n        Write-Output bar\n    \"#,\n    )\n    .shell(false)\n    .stdout(\"bar\\r\\n\")\n    .stderr(\"Write-Output bar\\n\")\n    .success();\n}\n\n#[test]\nfn windows_powershell_setting_uses_powershell_set_shell() {\n  Test::new()\n    .justfile(\n      r#\"\n      set windows-powershell\n      set shell := [\"asdfasdfasdfasdf\"]\n\n      foo:\n        Write-Output bar\n    \"#,\n    )\n    .shell(false)\n    .stdout(\"bar\\r\\n\")\n    .stderr(\"Write-Output bar\\n\")\n    .success();\n}\n\n#[test]\nfn windows_powershell_setting_uses_powershell() {\n  Test::new()\n    .justfile(\n      r#\"\n      set windows-powershell\n\n      foo:\n        Write-Output bar\n    \"#,\n    )\n    .shell(false)\n    .stdout(\"bar\\r\\n\")\n    .stderr(\"Write-Output bar\\n\")\n    .success();\n}\n"
  },
  {
    "path": "tests/working_directory.rs",
    "content": "use super::*;\n\nconst JUSTFILE: &str = r#\"\nfoo := `cat data`\n\nlinewise bar=`cat data`: shebang\n  echo expression: {{foo}}\n  echo default: {{bar}}\n  echo linewise: `cat data`\n\nshebang:\n  #!/usr/bin/env sh\n  echo \"shebang:\" `cat data`\n\"#;\n\nconst DATA: &str = \"OK\";\n\nconst WANT: &str = \"shebang: OK\\nexpression: OK\\ndefault: OK\\nlinewise: OK\\n\";\n\n/// Test that just runs with the correct working directory when invoked with\n/// `--justfile` but not `--working-directory`\n#[test]\nfn justfile_without_working_directory() -> Result<(), Box<dyn Error>> {\n  let tmp = temptree! {\n    justfile: JUSTFILE,\n    data: DATA,\n  };\n\n  let output = Command::new(JUST)\n    .arg(\"--justfile\")\n    .arg(tmp.path().join(\"justfile\"))\n    .output()?;\n\n  if !output.status.success() {\n    eprintln!(\"{:?}\", String::from_utf8_lossy(&output.stderr));\n    panic!();\n  }\n\n  let stdout = String::from_utf8(output.stdout).unwrap();\n\n  assert_eq!(stdout, WANT);\n\n  Ok(())\n}\n\n/// Test that just runs with the correct working directory when invoked with\n/// `--justfile` but not `--working-directory`, and justfile path has no parent\n#[test]\nfn justfile_without_working_directory_relative() -> Result<(), Box<dyn Error>> {\n  let tmp = temptree! {\n    justfile: JUSTFILE,\n    data: DATA,\n  };\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"--justfile\")\n    .arg(\"justfile\")\n    .output()?;\n\n  if !output.status.success() {\n    eprintln!(\"{:?}\", String::from_utf8_lossy(&output.stderr));\n    panic!();\n  }\n\n  let stdout = String::from_utf8(output.stdout).unwrap();\n\n  assert_eq!(stdout, WANT);\n\n  Ok(())\n}\n\n/// Test that just invokes commands from the directory in which the justfile is\n/// found\n#[test]\nfn change_working_directory_to_search_justfile_parent() -> Result<(), Box<dyn Error>> {\n  let tmp = temptree! {\n    justfile: JUSTFILE,\n    data: DATA,\n    subdir: {},\n  };\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path().join(\"subdir\"))\n    .output()?;\n\n  if !output.status.success() {\n    eprintln!(\"{:?}\", String::from_utf8_lossy(&output.stderr));\n    panic!();\n  }\n\n  let stdout = String::from_utf8_lossy(&output.stdout);\n\n  assert_eq!(stdout, WANT);\n\n  Ok(())\n}\n\n/// Test that just runs with the correct working directory when invoked with\n/// `--justfile` but not `--working-directory`\n#[test]\nfn justfile_and_working_directory() -> Result<(), Box<dyn Error>> {\n  let tmp = temptree! {\n    justfile: JUSTFILE,\n    sub: {\n      data: DATA,\n    },\n  };\n\n  let output = Command::new(JUST)\n    .arg(\"--justfile\")\n    .arg(tmp.path().join(\"justfile\"))\n    .arg(\"--working-directory\")\n    .arg(tmp.path().join(\"sub\"))\n    .output()?;\n\n  if !output.status.success() {\n    eprintln!(\"{:?}\", String::from_utf8_lossy(&output.stderr));\n    panic!();\n  }\n\n  let stdout = String::from_utf8(output.stdout).unwrap();\n\n  assert_eq!(stdout, WANT);\n\n  Ok(())\n}\n\n/// Test that just runs with the correct working directory when invoked with\n/// `--justfile` but not `--working-directory`\n#[test]\nfn search_dir_child() -> Result<(), Box<dyn Error>> {\n  let tmp = temptree! {\n    child: {\n      justfile: JUSTFILE,\n      data: DATA,\n    },\n  };\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path())\n    .arg(\"child/\")\n    .output()?;\n\n  if !output.status.success() {\n    eprintln!(\"{:?}\", String::from_utf8_lossy(&output.stderr));\n    panic!();\n  }\n\n  let stdout = String::from_utf8(output.stdout).unwrap();\n\n  assert_eq!(stdout, WANT);\n\n  Ok(())\n}\n\n/// Test that just runs with the correct working directory when invoked with\n/// `--justfile` but not `--working-directory`\n#[test]\nfn search_dir_parent() -> Result<(), Box<dyn Error>> {\n  let tmp = temptree! {\n    child: {\n    },\n    justfile: JUSTFILE,\n    data: DATA,\n  };\n\n  let output = Command::new(JUST)\n    .current_dir(tmp.path().join(\"child\"))\n    .arg(\"../\")\n    .output()?;\n\n  if !output.status.success() {\n    eprintln!(\"{:?}\", String::from_utf8_lossy(&output.stderr));\n    panic!();\n  }\n\n  let stdout = String::from_utf8(output.stdout).unwrap();\n\n  assert_eq!(stdout, WANT);\n\n  Ok(())\n}\n\n#[test]\nfn setting() {\n  Test::new()\n    .justfile(\n      r#\"\n      set working-directory := 'bar'\n\n      print1:\n        echo \"$(basename \"$PWD\")\"\n\n      [no-cd]\n      print2:\n        echo \"$(basename \"$PWD\")\"\n    \"#,\n    )\n    .current_dir(\"foo\")\n    .tree(tree! {\n      foo: {},\n      bar: {}\n    })\n    .args([\"print1\", \"print2\"])\n    .stderr(\n      r#\"echo \"$(basename \"$PWD\")\"\necho \"$(basename \"$PWD\")\"\n\"#,\n    )\n    .stdout(\"bar\\nfoo\\n\")\n    .success();\n}\n\n#[test]\nfn no_cd_overrides_setting() {\n  Test::new()\n    .justfile(\n      \"\n      set working-directory := 'bar'\n\n      [no-cd]\n      foo:\n        cat bar\n    \",\n    )\n    .current_dir(\"foo\")\n    .tree(tree! {\n      foo: {\n        bar: \"hello\",\n      }\n    })\n    .stderr(\"cat bar\\n\")\n    .stdout(\"hello\")\n    .success();\n}\n\n#[test]\nfn working_dir_in_submodule_is_relative_to_module_path() {\n  Test::new()\n    .write(\n      \"foo/mod.just\",\n      \"\nset working-directory := 'bar'\n\n@foo:\n  cat file.txt\n\",\n    )\n    .justfile(\"mod foo\")\n    .write(\"foo/bar/file.txt\", \"FILE\")\n    .arg(\"foo\")\n    .stdout(\"FILE\")\n    .success();\n}\n\n#[test]\nfn working_dir_applies_to_backticks() {\n  Test::new()\n    .justfile(\n      \"\n        set working-directory := 'foo'\n\n        file := `cat file.txt`\n\n        @foo:\n          echo {{ file }}\n      \",\n    )\n    .write(\"foo/file.txt\", \"FILE\")\n    .stdout(\"FILE\\n\")\n    .success();\n}\n\n#[test]\nfn working_dir_applies_to_shell_function() {\n  Test::new()\n    .justfile(\n      \"\n        set working-directory := 'foo'\n\n        file := shell('cat file.txt')\n\n        @foo:\n          echo {{ file }}\n      \",\n    )\n    .write(\"foo/file.txt\", \"FILE\")\n    .stdout(\"FILE\\n\")\n    .success();\n}\n\n#[test]\nfn working_dir_applies_to_backticks_in_submodules() {\n  Test::new()\n    .justfile(\"mod foo\")\n    .write(\n      \"foo/mod.just\",\n      \"\nset working-directory := 'bar'\n\nfile := `cat file.txt`\n\n@foo:\n  echo {{ file }}\n\",\n    )\n    .arg(\"foo\")\n    .write(\"foo/bar/file.txt\", \"FILE\")\n    .stdout(\"FILE\\n\")\n    .success();\n}\n\n#[test]\nfn working_dir_applies_to_shell_function_in_submodules() {\n  Test::new()\n    .justfile(\"mod foo\")\n    .write(\n      \"foo/mod.just\",\n      \"\nset working-directory := 'bar'\n\nfile := shell('cat file.txt')\n\n@foo:\n  echo {{ file }}\n\",\n    )\n    .arg(\"foo\")\n    .write(\"foo/bar/file.txt\", \"FILE\")\n    .stdout(\"FILE\\n\")\n    .success();\n}\n\n#[test]\nfn attribute_duplicate() {\n  Test::new()\n    .justfile(\n      \"\n        [working-directory('bar')]\n        [working-directory('baz')]\n        foo:\n      \",\n    )\n    .stderr(\n      \"error: Recipe attribute `working-directory` first used on line 1 is duplicated on line 2\n ——▶ justfile:2:2\n  │\n2 │ [working-directory('baz')]\n  │  ^^^^^^^^^^^^^^^^^\n\",\n    )\n    .failure();\n}\n\n#[test]\nfn attribute() {\n  Test::new()\n    .justfile(\n      \"\n        [working-directory('foo')]\n        @qux:\n          echo baz > bar\n      \",\n    )\n    .create_dir(\"foo\")\n    .expect_file(\"foo/bar\", \"baz\\n\")\n    .success();\n}\n\n#[test]\nfn attribute_with_nocd_is_forbidden() {\n  Test::new()\n    .justfile(\n      \"\n        [working-directory('foo')]\n        [no-cd]\n        bar:\n      \",\n    )\n    .stderr(\n      \"\n        error: Recipe `bar` has both `[no-cd]` and `[working-directory]` attributes\n         ——▶ justfile:3:1\n          │\n        3 │ bar:\n          │ ^^^\n      \",\n    )\n    .failure();\n}\n\n#[test]\nfn setting_and_attribute() {\n  Test::new()\n    .justfile(\n      \"\n        set working-directory := 'foo'\n\n        [working-directory('bar')]\n        @baz:\n          echo bob > fred\n      \",\n    )\n    .create_dir(\"foo/bar\")\n    .expect_file(\"foo/bar/fred\", \"bob\\n\")\n    .success();\n}\n"
  },
  {
    "path": "www/.nojekyll",
    "content": ""
  },
  {
    "path": "www/CNAME",
    "content": "just.systems\n"
  },
  {
    "path": "www/index.css",
    "content": ":root {\n --width-target: calc(100vw / 6);\n --height-target: calc(100vh / 3);\n --size: min(var(--width-target), var(--height-target));\n --margin-vertical: calc((100vh - var(--size) * 2) / 2);\n --margin-horizontal: calc((100vw - var(--size) * 5) / 2);\n}\n\n* {\n  margin: 0;\n  padding: 0;\n}\n\nhtml {\n  background-color: black;\n  color: white;\n  overflow: hidden;\n  text-align: center;\n  font-family: monospace;\n  font-size: var(--size);\n  line-height: var(--size);\n}\n\na {\n  color: white;\n  text-decoration: none;\n}\n\na:hover {\n  text-shadow: 0 0 5px #fff;\n}\n\nbody {\n  display: grid;\n  grid-template-columns: repeat(4, 1fr);\n  margin: var(--margin-vertical) var(--margin-horizontal);\n}\n\nbody > * {\n  width: var(--size);\n}\n\nbody > div {\n  height: var(--size);\n  text-shadow: 0 0 5px #fff;\n}\n\nbody > a:nth-child(n+5) {\n  align-items: center;\n  display: flex;\n  font-size: calc(var(--size) / 9);\n  height: calc(var(--size) / 2);\n  justify-content: center;\n  line-height: calc(var(--size) / 9);\n}\n\n/* just is an isogram */\n#j:after       { content: 'j'; }\n#j:hover:after { content: ':'; }\n#u:after       { content: 'u'; }\n#u:hover:after { content: '~'; }\n#s:after       { content: 's'; }\n#s:hover:after { content: '$'; }\n#t:after       { content: 't'; }\n#t:hover:after { content: '='; }\n"
  },
  {
    "path": "www/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=utf-8>\n    <meta name=viewport content='width=device-width,initial-scale=1'>\n    <title>Just: A Command Runner</title>\n    <link href=index.css rel=stylesheet type=text/css>\n  </head>\n  <body>\n    <a href=https://github.com/casey/just><div id=j></div></a>\n    <a href=man/en/><div id=u></div></a>\n    <a href=https://discord.gg/ezYScXR><div id=s></div></a>\n    <a href=https://crates.io/crates/just><div id=t></div></a>\n\n    <a href=https://github.com/casey/just>github</a>\n    <a href=man/en/>manual</a>\n    <a href=https://discord.gg/ezYScXR>discord</a>\n    <a href=https://crates.io/crates/just>crates.io</a>\n\n    <div></div>\n    <a href=man/zh/>用户指南</a>\n    <div></div>\n    <div></div>\n  </body>\n</html>\n<!-- Love, Casey -->\n"
  },
  {
    "path": "www/install.sh",
    "content": "#!/usr/bin/env bash\n\nset -eu\n\n# Check pipefail support in a subshell, ignore if unsupported\n# shellcheck disable=SC3040\n(set -o pipefail 2> /dev/null) && set -o pipefail\n\nhelp() {\n  cat <<'EOF'\nInstall a binary release of a just hosted on GitHub\n\nUSAGE:\n    install.sh [options]\n\nFLAGS:\n    -h, --help      Display this message\n    -f, --force     Force overwriting an existing binary\n\nOPTIONS:\n    --tag TAG       Tag (version) of the crate to install, defaults to latest release\n    --to LOCATION   Where to install the binary [default: ~/bin]\n    --target TARGET\nEOF\n}\n\ncrate=just\nurl=https://github.com/casey/just\nreleases=$url/releases\n\nsay() {\n  echo \"install: $*\" >&2\n}\n\nerr() {\n  if [ -n \"${td-}\" ]; then\n    rm -rf \"$td\"\n  fi\n\n  say \"error: $*\"\n  exit 1\n}\n\nneed() {\n  if ! command -v \"$1\" > /dev/null 2>&1; then\n    err \"need $1 (command not found)\"\n  fi\n}\n\ndownload() {\n  url=\"$1\"\n  output=\"$2\"\n\n  args=()\n  if [ -n \"${GITHUB_TOKEN+x}\" ]; then\n    args+=(--header \"Authorization: Bearer $GITHUB_TOKEN\")\n  fi\n\n  if command -v curl > /dev/null; then\n    curl --proto =https --tlsv1.2 -sSfL ${args[@]+\"${args[@]}\"} \"$url\" -o\"$output\"\n  else\n    wget --https-only --secure-protocol=TLSv1_2 --quiet ${args[@]+\"${args[@]}\"} \"$url\" -O\"$output\"\n  fi\n}\n\nforce=false\nwhile test $# -gt 0; do\n  case $1 in\n    --force | -f)\n      force=true\n      ;;\n    --help | -h)\n      help\n      exit 0\n      ;;\n    --tag)\n      tag=$2\n      shift\n      ;;\n    --target)\n      target=$2\n      shift\n      ;;\n    --to)\n      dest=$2\n      shift\n      ;;\n    *)\n      say \"error: unrecognized argument '$1'. Usage:\"\n      help\n      exit 1\n      ;;\n  esac\n  shift\ndone\n\ncommand -v curl > /dev/null 2>&1 ||\n  command -v wget > /dev/null 2>&1 ||\n  err \"need wget or curl (command not found)\"\n\nneed mkdir\nneed mktemp\n\nif [ -z \"${tag-}\" ]; then\n  need grep\n  need cut\nfi\n\nif [ -z \"${target-}\" ]; then\n  need cut\nfi\n\nif [ -z \"${dest-}\" ]; then\n  dest=\"$HOME/bin\"\nfi\n\nif [ -z \"${tag-}\" ]; then\n  tag=$(\n    download https://api.github.com/repos/casey/just/releases/latest - |\n    grep tag_name |\n    cut -d'\"' -f4\n  )\nfi\n\nif [ -z \"${target-}\" ]; then\n  # bash compiled with MINGW (e.g. git-bash, used in github windows runners),\n  # unhelpfully includes a version suffix in `uname -s` output, so handle that.\n  # e.g. MINGW64_NT-10-0.19044\n  kernel=$(uname -s | cut -d- -f1)\n  uname_target=\"$(uname -m)-$kernel\"\n\n  case $uname_target in\n    aarch64-Linux) target=aarch64-unknown-linux-musl;;\n    arm64-Darwin) target=aarch64-apple-darwin;;\n    armv6l-Linux) target=arm-unknown-linux-musleabihf;;\n    armv7l-Linux) target=armv7-unknown-linux-musleabihf;;\n    loongarch64-Linux) target=loongarch64-unknown-linux-musl;;\n    x86_64-Darwin) target=x86_64-apple-darwin;;\n    x86_64-Linux) target=x86_64-unknown-linux-musl;;\n    x86_64-MINGW64_NT) target=x86_64-pc-windows-msvc;;\n    x86_64-Windows_NT) target=x86_64-pc-windows-msvc;;\n    *)\n      # shellcheck disable=SC2016\n      err 'Could not determine target from output of `uname -m`-`uname -s`, please use `--target`:' \"$uname_target\"\n    ;;\n  esac\nfi\n\ncase $target in\n  x86_64-pc-windows-msvc) extension=zip; need unzip;;\n  *) extension=tar.gz; need tar;;\nesac\n\narchive=\"$releases/download/$tag/$crate-$tag-$target.$extension\"\n\nsay \"Repository:  $url\"\nsay \"Crate:       $crate\"\nsay \"Tag:         $tag\"\nsay \"Target:      $target\"\nsay \"Destination: $dest\"\nsay \"Archive:     $archive\"\n\ntd=$(mktemp -d || mktemp -d -t tmp)\n\nif [ \"$extension\" = \"zip\" ]; then\n  download \"$archive\" \"$td/just.zip\"\n  unzip -d \"$td\" \"$td/just.zip\"\nelse\n  download \"$archive\" - | tar -C \"$td\" -xz\nfi\n\nif [ -e \"$dest/just\" ] && [ \"$force\" = false ]; then\n  err \"\\`$dest/just\\` already exists\"\nelse\n  mkdir -p \"$dest\"\n  cp \"$td/just\" \"$dest/just\"\n  chmod 755 \"$dest/just\"\nfi\n\nrm -rf \"$td\"\n"
  }
]