just-1.21.0/.cargo_vcs_info.json0000644000000001360000000000100121020ustar { "git": { "sha1": "f3b103a2e0af6e1c12ffc5f9e00a09de000e8bb0" }, "path_in_vcs": "" }just-1.21.0/.editorconfig000064400000000000000000000005461046102023000133540ustar 00000000000000# EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true [*] # Text is UTF-8 charset = utf-8 # Unix-style newlines end_of_line = lf # Newline ending every file insert_final_newline = true # Soft tabs indent_style = space # Two-space indentation indent_size = 2 # Trim trailing whitespace trim_trailing_whitespace = true just-1.21.0/.gitattributes000064400000000000000000000000101046102023000135540ustar 00000000000000* -text just-1.21.0/.github/workflows/ci.yaml000064400000000000000000000056331046102023000155550ustar 00000000000000name: CI on: pull_request: branches: - '*' push: branches: - master defaults: run: shell: bash jobs: lint: runs-on: ubuntu-latest env: RUSTFLAGS: --deny warnings steps: - uses: actions/checkout@v2 - name: Install Rust Toolchain Components uses: actions-rs/toolchain@v1 with: components: clippy, rustfmt toolchain: stable - uses: Swatinem/rust-cache@v2 - name: Clippy run: cargo clippy --all --all-targets - name: Format run: cargo fmt --all -- --check - name: Completion Scripts run: | ./bin/generate-completions git diff --no-ext-diff --quiet --exit-code ./tests/completions/just.bash - name: Check for Forbidden Words run: | sudo apt-get update sudo apt-get install ripgrep ./bin/forbid pages: runs-on: ubuntu-latest permissions: contents: write env: RUSTFLAGS: --deny warnings steps: - uses: actions/checkout@v2 - name: Install Rust Toolchain Components uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable - uses: Swatinem/rust-cache@v2 - name: Install `mdbook` uses: peaceiris/actions-mdbook@v1 with: mdbook-version: latest - name: Install `mdbook-linkcheck` run: | mkdir -p mdbook-linkcheck cd mdbook-linkcheck wget https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/latest/download/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip unzip mdbook-linkcheck.x86_64-unknown-linux-gnu.zip chmod +x mdbook-linkcheck pwd >> $GITHUB_PATH - name: Build book run: | cargo run --package generate-book mdbook build book/en mdbook build book/zh - name: Deploy Pages uses: peaceiris/actions-gh-pages@v3 if: github.ref == 'refs/heads/master' with: github_token: ${{secrets.GITHUB_TOKEN}} publish_branch: gh-pages publish_dir: www test: strategy: matrix: os: - ubuntu-latest - macos-latest - windows-latest runs-on: ${{matrix.os}} env: RUSTFLAGS: --deny warnings steps: - uses: actions/checkout@v2 - name: Remove Broken WSL bash executable if: ${{ matrix.os == 'windows-latest' }} shell: cmd run: | takeown /F C:\Windows\System32\bash.exe icacls C:\Windows\System32\bash.exe /grant administrators:F del C:\Windows\System32\bash.exe - name: Install Rust Toolchain Components uses: actions-rs/toolchain@v1 with: components: clippy, rustfmt override: true toolchain: stable - uses: Swatinem/rust-cache@v2 - name: Test run: cargo test --all - name: Test install.sh run: | bash www/install.sh --to /tmp --tag 1.0.0 /tmp/just --version just-1.21.0/.github/workflows/release.yaml000064400000000000000000000057551046102023000166070ustar 00000000000000name: Release on: push: tags: - '*' defaults: run: shell: bash jobs: release: strategy: matrix: target: - aarch64-apple-darwin - aarch64-unknown-linux-musl - arm-unknown-linux-musleabihf - armv7-unknown-linux-musleabihf - x86_64-apple-darwin - x86_64-pc-windows-msvc - x86_64-unknown-linux-musl include: - target: aarch64-apple-darwin os: macos-latest target_rustflags: '' - target: aarch64-unknown-linux-musl os: ubuntu-latest target_rustflags: '--codegen linker=aarch64-linux-gnu-gcc' - target: arm-unknown-linux-musleabihf os: ubuntu-latest target_rustflags: '--codegen linker=arm-linux-gnueabihf-gcc' - target: armv7-unknown-linux-musleabihf os: ubuntu-latest target_rustflags: '--codegen linker=arm-linux-gnueabihf-gcc' - target: x86_64-apple-darwin os: macos-latest target_rustflags: '' - target: x86_64-pc-windows-msvc os: windows-latest target_rustflags: '' - target: x86_64-unknown-linux-musl os: ubuntu-latest target_rustflags: '' runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v2 - name: Install Rust Toolchain Components uses: actions-rs/toolchain@v1 with: profile: minimal target: ${{ matrix.target }} toolchain: stable - name: Install AArch64 Toolchain if: ${{ matrix.target == 'aarch64-unknown-linux-musl' }} run: | sudo apt-get update sudo apt-get install gcc-aarch64-linux-gnu - name: Install ARM Toolchain if: ${{ matrix.target == 'arm-unknown-linux-musleabihf' || matrix.target == 'armv7-unknown-linux-musleabihf' }} run: | sudo apt-get update sudo apt-get install gcc-arm-linux-gnueabihf - name: Ref Type id: ref-type run: cargo run --package ref-type -- --reference ${{ github.ref }} - name: Package id: package env: TARGET: ${{ matrix.target }} REF: ${{ github.ref }} OS: ${{ matrix.os }} TARGET_RUSTFLAGS: ${{ matrix.target_rustflags }} run: ./bin/package shell: bash - name: Publish Archive uses: softprops/action-gh-release@v0.1.5 if: ${{ startsWith(github.ref, 'refs/tags/') }} with: draft: false files: ${{ steps.package.outputs.archive }} prerelease: ${{ steps.ref-type.outputs.value != 'release' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish Changelog uses: softprops/action-gh-release@v0.1.5 if: >- ${{ startsWith(github.ref, 'refs/tags/') && matrix.target == 'x86_64-unknown-linux-musl' }} with: draft: false files: CHANGELOG.md prerelease: ${{ steps.ref-type.outputs.value != 'release' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} just-1.21.0/.gitignore000064400000000000000000000003211046102023000126560ustar 00000000000000.DS_Store .idea /.vagrant /.vscode /README.html /book/en/build /book/en/src /book/zh/build /book/zh/src /fuzz/artifacts /fuzz/corpus /fuzz/target /target /test-utilities/Cargo.lock /test-utilities/target /tmp just-1.21.0/CHANGELOG.md000064400000000000000000002217541046102023000125160ustar 00000000000000Changelog ========= [1.21.0](https://github.com/casey/just/releases/tag/1.21.0) - 2023-12-29 ------------------------------------------------------------------------ ### Added - Optional modules and imports ([#1797](https://github.com/casey/just/pull/1797)) - Print submodule recipes in --summary ([#1794](https://github.com/casey/just/pull/1794)) ### Misc - Use box-drawing characters in error messages ([#1798](https://github.com/casey/just/pull/1798)) - Use Self ([#1795](https://github.com/casey/just/pull/1795)) [1.20.0](https://github.com/casey/just/releases/tag/1.20.0) - 2023-12-28 ------------------------------------------------------------------------ ### Added - Allow mod statements with path to source file ([#1786](https://github.com/casey/just/pull/1786)) ### Changed - Expand tilde in import and module paths ([#1792](https://github.com/casey/just/pull/1792)) - Override imported recipes ([#1790](https://github.com/casey/just/pull/1790)) - Run recipes with working directory set to submodule directory ([#1788](https://github.com/casey/just/pull/1788)) ### Misc - Document import override behavior ([#1791](https://github.com/casey/just/pull/1791)) - Document submodule working directory ([#1789](https://github.com/casey/just/pull/1789)) [1.19.0](https://github.com/casey/just/releases/tag/1.19.0) - 2023-12-27 ------------------------------------------------------------------------ ### Added - Add modules ([#1782](https://github.com/casey/just/pull/1782)) [1.18.1](https://github.com/casey/just/releases/tag/1.18.1) - 2023-12-24 ------------------------------------------------------------------------ ### Added - Display a descriptive error for `!include` directives ([#1779](https://github.com/casey/just/pull/1779)) [1.18.0](https://github.com/casey/just/releases/tag/1.18.0) - 2023-12-24 ------------------------------------------------------------------------ ### Added - Stabilize `!include path` as `import 'path'` ([#1771](https://github.com/casey/just/pull/1771)) ### Misc - Tweak readme ([#1775](https://github.com/casey/just/pull/1775)) [1.17.0](https://github.com/casey/just/releases/tag/1.17.0) - 2023-12-20 ------------------------------------------------------------------------ ### Added - Add `[confirm]` attribute ([#1723](https://github.com/casey/just/pull/1723) by [Hwatwasthat](https://github.com/Hwatwasthat)) ### Changed - Don't default to included recipes ([#1740](https://github.com/casey/just/pull/1740)) ### Fixed - Pass justfile path to default chooser ([#1759](https://github.com/casey/just/pull/1759) by [Qeole](https://github.com/Qeole)) - Pass `--unstable` and `--color always` to default chooser ([#1758](https://github.com/casey/just/pull/1758) by [Qeole](https://github.com/Qeole)) ### Misc - Update Gentoo package repository ([#1757](https://github.com/casey/just/pull/1757) by [paul-jewell](https://github.com/paul-jewell)) - Fix readme header level ([#1752](https://github.com/casey/just/pull/1752) by [laniakea64](https://github.com/laniakea64)) - Document line continuations ([#1751](https://github.com/casey/just/pull/1751) by [laniakea64](https://github.com/laniakea64)) - List included recipes in load order ([#1745](https://github.com/casey/just/pull/1745)) - Fix build badge in zh readme ([#1743](https://github.com/casey/just/pull/1743) by [chenrui333](https://github.com/chenrui333)) - Rename Justfile::first → Justfile::default ([#1741](https://github.com/casey/just/pull/1741)) - Add file paths to error messages ([#1737](https://github.com/casey/just/pull/1737)) - Move !include processing into compiler ([#1618](https://github.com/casey/just/pull/1618) by [neunenak](https://github.com/neunenak)) - Update Arch Linux package URL in readme ([#1733](https://github.com/casey/just/pull/1733) by [felixonmars](https://github.com/felixonmars)) - 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)) - Remove VALID_ALIAS_ATTRIBUTES array ([#1731](https://github.com/casey/just/pull/1731)) - Fix justfile search link in Chinese docs ([#1730](https://github.com/casey/just/pull/1730) by [oluceps](https://github.com/oluceps)) - Add example of Windows shebang handling ([#1709](https://github.com/casey/just/pull/1709) by [pfmoore](https://github.com/pfmoore)) - Fix CI ([#1728](https://github.com/casey/just/pull/1728)) [1.16.0](https://github.com/casey/just/releases/tag/1.16.0) - 2023-11-08 ------------------------------------------------------------------------ ### Added - Add ARMv6 release target ([#1715](https://github.com/casey/just/pull/1715) by [ragazenta](https://github.com/ragazenta)) - Add `semver_matches` function ([#1713](https://github.com/casey/just/pull/1713) by [t3hmrman](https://github.com/t3hmrman)) - Add `dotenv-filename` and `dotenv-path` settings ([#1692](https://github.com/casey/just/pull/1692) by [ltfourrier](https://github.com/ltfourrier)) - Allow setting echoed recipe line color ([#1670](https://github.com/casey/just/pull/1670) by [avi-cenna](https://github.com/avi-cenna)) ### Fixed - Fix Fish completion script ([#1710](https://github.com/casey/just/pull/1710) by [l4zygreed](https://github.com/l4zygreed)) ### Misc - Fix readme typo ([#1717](https://github.com/casey/just/pull/1717) by [barraponto](https://github.com/barraponto)) - Clean up error display ([#1699](https://github.com/casey/just/pull/1699) by [nyurik](https://github.com/nyurik)) - Misc fixes ([#1700](https://github.com/casey/just/pull/1700) by [nyurik](https://github.com/nyurik)) - Fix readme build badge ([#1697](https://github.com/casey/just/pull/1697)) - Fix set tempdir grammar ([#1695](https://github.com/casey/just/pull/1695)) - Add version to attributes ([#1694](https://github.com/casey/just/pull/1694) by [JoeyTeng](https://github.com/JoeyTeng)) - Update README.md ([#1691](https://github.com/casey/just/pull/1691) by [laniakea64](https://github.com/laniakea64)) [1.15.0](https://github.com/casey/just/releases/tag/1.15.0) - 2023-10-09 ------------------------------------------------------------------------ ### Added - Add Nushell completion script ([#1571](https://github.com/casey/just/pull/1571) by [presidento](https://github.com/presidento)) - Allow unstable features to be enabled with environment variable ([#1588](https://github.com/casey/just/pull/1588) by [neunenak](https://github.com/neunenak)) - Add num_cpus() function ([#1568](https://github.com/casey/just/pull/1568) by [schultetwin1](https://github.com/schultetwin1)) - Allow escaping newlines ([#1551](https://github.com/casey/just/pull/1551) by [ids1024](https://github.com/ids1024)) - Stabilize JSON dump format ([#1633](https://github.com/casey/just/pull/1633)) - Add env() function ([#1613](https://github.com/casey/just/pull/1613) by [kykyi](https://github.com/kykyi)) ### Changed - Allow selecting multiple recipes with default chooser ([#1547](https://github.com/casey/just/pull/1547) by [fzdwx](https://github.com/fzdwx)) ### Misc - Don't recommend `vim-polyglot` in readme ([#1644](https://github.com/casey/just/pull/1644) by [laniakea64](https://github.com/laniakea64)) - Note Micro support in readme ([#1316](https://github.com/casey/just/pull/1316) by [tomodachi94](https://github.com/tomodachi94)) - Update Indentation Documentation ([#1600](https://github.com/casey/just/pull/1600) by [GinoMan](https://github.com/GinoMan)) - Fix triple-quoted string example in readme ([#1620](https://github.com/casey/just/pull/1620) by [avi-cenna](https://github.com/avi-cenna)) - 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)) - Placate clippy ([#1689](https://github.com/casey/just/pull/1689)) - Fix README typos ([#1660](https://github.com/casey/just/pull/1660) by [akuhnregnier](https://github.com/akuhnregnier)) - Document Windows Package Manager install instructions ([#1656](https://github.com/casey/just/pull/1656)) - Test unpaired escaped carriage return error ([#1650](https://github.com/casey/just/pull/1650)) - Avoid grep aliases in bash completions ([#1622](https://github.com/casey/just/pull/1622) by [BojanStipic](https://github.com/BojanStipic)) - Clarify [unix] attribute in readme ([#1619](https://github.com/casey/just/pull/1619) by [neunenak](https://github.com/neunenak)) - Add descriptions to fish recipe completions ([#1578](https://github.com/casey/just/pull/1578) by [patricksjackson](https://github.com/patricksjackson)) - Add better documentation for --dump and --fmt ([#1603](https://github.com/casey/just/pull/1603) by [neunenak](https://github.com/neunenak)) - Cleanup ([#1566](https://github.com/casey/just/pull/1566) by [nyurik](https://github.com/nyurik)) - Document Helix editor support in readme ([#1604](https://github.com/casey/just/pull/1604) by [kenden](https://github.com/kenden)) [1.14.0](https://github.com/casey/just/releases/tag/1.14.0) - 2023-06-02 ------------------------------------------------------------------------ ### Changed - Use `just --show` in default chooser ([#1539](https://github.com/casey/just/pull/1539) by [fzdwx](https://github.com/fzdwx)) ### Misc - Fix justfile search link ([#1607](https://github.com/casey/just/pull/1607) by [jbaber](https://github.com/jbaber)) - Ignore clippy::let_underscore_untyped ([#1609](https://github.com/casey/just/pull/1609)) - Link to private recipes section in readme ([#1542](https://github.com/casey/just/pull/1542) by [quad](https://github.com/quad)) - Update README to reflect new attribute syntax ([#1538](https://github.com/casey/just/pull/1538) by [neunenak](https://github.com/neunenak)) - Allow multiple attributes on one line ([#1537](https://github.com/casey/just/pull/1537) by [neunenak](https://github.com/neunenak)) - Analyze and Compiler tweaks ([#1534](https://github.com/casey/just/pull/1534) by [neunenak](https://github.com/neunenak)) - Downgrade to TLS 1.2 in install script ([#1536](https://github.com/casey/just/pull/1536)) [1.13.0](https://github.com/casey/just/releases/tag/1.13.0) - 2023-01-24 ------------------------------------------------------------------------ ### Added - Add -n as a short flag for --for dry-run ([#1524](https://github.com/casey/just/pull/1524) by [maiha](https://github.com/maiha)) - Add invocation_directory_native() ([#1507](https://github.com/casey/just/pull/1507)) ### Changed - Ignore additional search path arguments ([#1528](https://github.com/casey/just/pull/1528) by [neunenak](https://github.com/neunenak)) - Only print fallback message when verbose ([#1510](https://github.com/casey/just/pull/1510)) - Print format diff to stdout ([#1506](https://github.com/casey/just/pull/1506)) ### Fixed - Test passing dot as argument between justfiles ([#1530](https://github.com/casey/just/pull/1530)) - Fix install script default directory ([#1525](https://github.com/casey/just/pull/1525)) ### Misc - Note that justfiles are order-insensitive ([#1529](https://github.com/casey/just/pull/1529)) - Borrow Ast in Analyser ([#1527](https://github.com/casey/just/pull/1527) by [neunenak](https://github.com/neunenak)) - Ignore chooser tests ([#1513](https://github.com/casey/just/pull/1513)) - Put default setting values in backticks ([#1512](https://github.com/casey/just/pull/1512) by [s1ck](https://github.com/s1ck)) - Use lowercase boolean literals in readme ([#1511](https://github.com/casey/just/pull/1511) by [s1ck](https://github.com/s1ck)) - Document invocation_directory_native() ([#1508](https://github.com/casey/just/pull/1508)) - Fix interrupt tests ([#1505](https://github.com/casey/just/pull/1505)) [1.12.0](https://github.com/casey/just/releases/tag/1.12.0) - 2023-01-12 ------------------------------------------------------------------------ ### Added - Add `!include` directives ([#1470](https://github.com/casey/just/pull/1470) by [neunenak](https://github.com/neunenak)) ### Changed - Allow matching search path arguments ([#1475](https://github.com/casey/just/pull/1475) by [neunenak](https://github.com/neunenak)) - Allow recipe parameters to shadow variables ([#1480](https://github.com/casey/just/pull/1480)) ### Misc - Remove --unstable from fallback example in readme ([#1502](https://github.com/casey/just/pull/1502)) - Specify minimum rust version ([#1496](https://github.com/casey/just/pull/1496) by [benmoss](https://github.com/benmoss)) - Note that install.sh may fail on GitHub actions ([#1499](https://github.com/casey/just/pull/1499)) - Fix readme typo ([#1489](https://github.com/casey/just/pull/1489) by [auberisky](https://github.com/auberisky)) - Update install script and readmes to use tls v1.3 ([#1481](https://github.com/casey/just/pull/1481)) - Renable install.sh test on CI([#1478](https://github.com/casey/just/pull/1478)) - Don't test install.sh on CI ([#1477](https://github.com/casey/just/pull/1477)) - Update Chinese translation of readme ([#1476](https://github.com/casey/just/pull/1476) by [hustcer](https://github.com/hustcer)) - Fix install.sh for Windows ([#1474](https://github.com/casey/just/pull/1474) by [bloodearnest](https://github.com/bloodearnest)) [1.11.0](https://github.com/casey/just/releases/tag/1.11.0) - 2023-01-03 ------------------------------------------------------------------------ ### Added - Stabilize fallback ([#1471](https://github.com/casey/just/pull/1471)) ### Misc - Update Sublime syntax instructions ([#1455](https://github.com/casey/just/pull/1455) by [nk9](https://github.com/nk9)) [1.10.0](https://github.com/casey/just/releases/tag/1.10.0) - 2023-01-01 ------------------------------------------------------------------------ ### Added - Allow private attribute on aliases ([#1434](https://github.com/casey/just/pull/1434) by [neunenak](https://github.com/neunenak)) ### Changed - Suppress --fmt --check diff if --quiet is passed ([#1457](https://github.com/casey/just/pull/1457)) ### Fixed - Format exported variadic parameters correctly ([#1451](https://github.com/casey/just/pull/1451)) ### Misc - Fix section title grammar ([#1466](https://github.com/casey/just/pull/1466) by [brettcannon](https://github.com/brettcannon)) - Give pages job write permissions([#1464](https://github.com/casey/just/pull/1464) by [jsoref](https://github.com/jsoref)) - Fix spelling ([#1463](https://github.com/casey/just/pull/1463) by [jsoref](https://github.com/jsoref)) - Merge imports ([#1462](https://github.com/casey/just/pull/1462)) - Add instructions for taiki-e/install-action ([#1459](https://github.com/casey/just/pull/1459) by [azzamsa](https://github.com/azzamsa)) - Differentiate between shell and nushell example ([#1427](https://github.com/casey/just/pull/1427) by [Dialga](https://github.com/Dialga)) - Link regex docs in readme ([#1454](https://github.com/casey/just/pull/1454)) - Linkify changelog PRs and usernames ([#1440](https://github.com/casey/just/pull/1440) by [nk9](https://github.com/nk9)) - Eliminate lazy_static ([#1442](https://github.com/casey/just/pull/1442) by [camsteffen](https://github.com/camsteffen)) - Add attributes to sublime syntax file ([#1452](https://github.com/casey/just/pull/1452) by [crdx](https://github.com/crdx)) - Fix homepage style ([#1453](https://github.com/casey/just/pull/1453)) - Linkify homepage letters ([#1448](https://github.com/casey/just/pull/1448) by [nk9](https://github.com/nk9)) - Use `just` in readme codeblocks ([#1447](https://github.com/casey/just/pull/1447) by [nicochatzi](https://github.com/nicochatzi)) - Update MSRV in readme ([#1446](https://github.com/casey/just/pull/1446)) - Merge CI workflows ([#1444](https://github.com/casey/just/pull/1444)) - Use dotenvy instead of dotenv ([#1443](https://github.com/casey/just/pull/1443) by [mike-burns](https://github.com/mike-burns)) - Update Chinese translation of readme ([#1428](https://github.com/casey/just/pull/1428) by [hustcer](https://github.com/hustcer)) [1.9.0](https://github.com/casey/just/releases/tag/1.9.0) - 2022-11-25 ---------------------------------------------------------------------- ### Breaking Changes to Unstable Features - Change `fallback` setting default to false ([#1425](https://github.com/casey/just/pull/1425)) ### Added - Hide recipes with `[private]` attribute ([#1422](https://github.com/casey/just/pull/1422)) - Add replace_regex function ([#1393](https://github.com/casey/just/pull/1393) by [miles170](https://github.com/miles170)) - Add [no-cd] attribute ([#1400](https://github.com/casey/just/pull/1400)) ### Changed - Omit shebang lines on Windows ([#1417](https://github.com/casey/just/pull/1417)) ### Misc - Placate clippy ([#1423](https://github.com/casey/just/pull/1423)) - Make include_shebang_line clearer ([#1418](https://github.com/casey/just/pull/1418)) - Use more secure cURL options in install.sh ([#1416](https://github.com/casey/just/pull/1416)) - Document how shebang recipes are executed ([#1412](https://github.com/casey/just/pull/1412)) - Fix typo: regec → regex ([#1409](https://github.com/casey/just/pull/1409)) - Use powershell.exe instead of pwsh.exe in readme ([#1394](https://github.com/casey/just/pull/1394) by [asdf8dfafjk](https://github.com/asdf8dfafjk)) - Expand alternatives and prior art in readme ([#1401](https://github.com/casey/just/pull/1401)) - Split up CI workflow ([#1399](https://github.com/casey/just/pull/1399)) [1.8.0](https://github.com/casey/just/releases/tag/1.8.0) - 2022-11-02 ---------------------------------------------------------------------- ### Added - Add OS Configuration Attributes ([#1387](https://github.com/casey/just/pull/1387)) ### Misc - Link to sclu1034/vscode-just in readme ([#1396](https://github.com/casey/just/pull/1396)) [1.7.0](https://github.com/casey/just/releases/tag/1.7.0) - 2022-10-26 ---------------------------------------------------------------------- ### Breaking Changes to Unstable Features - Make `fallback` setting default to true ([#1384](https://github.com/casey/just/pull/1384)) ### Added - Add more case-conversion functions ([#1383](https://github.com/casey/just/pull/1383) by [gVirtu](https://github.com/gVirtu)) - Add `tempdir` setting ([#1369](https://github.com/casey/just/pull/1369) by [dmatos2012](https://github.com/dmatos2012)) - Add [no-exit-message] recipe annotation ([#1354](https://github.com/casey/just/pull/1354) by [gokhanettin](https://github.com/gokhanettin)) - Add `capitalize(s)` function ([#1375](https://github.com/casey/just/pull/1375) by [femnad](https://github.com/femnad)) ### Misc - Credit contributors in changelog ([#1385](https://github.com/casey/just/pull/1385)) - Update asdf just plugin repository ([#1380](https://github.com/casey/just/pull/1380) by [kachick](https://github.com/kachick)) - Prepend commit messages with `- ` in changelog ([#1379](https://github.com/casey/just/pull/1379)) - Fail publish if `master` is found in README.md ([#1378](https://github.com/casey/just/pull/1378)) - Use for loop in capitalize implementation ([#1377](https://github.com/casey/just/pull/1377)) [1.6.0](https://github.com/casey/just/releases/tag/1.6.0) - 2022-10-19 ---------------------------------------------------------------------- ### Breaking Changes to Unstable Features - Require `set fallback := true` to enable recipe fallback ([#1368](https://github.com/casey/just/pull/1368)) ### Changed - Allow fallback with search directory ([#1348](https://github.com/casey/just/pull/1348)) ### Added - Don't evaluate comments ([#1358](https://github.com/casey/just/pull/1358)) - Add skip-comments setting ([#1333](https://github.com/casey/just/pull/1333) by [neunenak](https://github.com/neunenak)) - Allow bash completion to complete tasks in other directories ([#1303](https://github.com/casey/just/pull/1303) by [jpbochi](https://github.com/jpbochi)) ### Misc - Restore www/CNAME ([#1364](https://github.com/casey/just/pull/1364)) - Improve book config ([#1363](https://github.com/casey/just/pull/1363)) - Add kitchen sink justfile to test syntax highlighting ([#1362](https://github.com/casey/just/pull/1362) by [nk9](https://github.com/nk9)) - Note version in which absolute path construction was added ([#1361](https://github.com/casey/just/pull/1361)) - Inline setup and cleanup functions in completion script test ([#1352](https://github.com/casey/just/pull/1352)) [1.5.0](https://github.com/casey/just/releases/tag/1.5.0) - 2022-9-11 --------------------------------------------------------------------- ### Changed - Allow constructing absolute paths with `/` operator ([#1320](https://github.com/casey/just/pull/1320) by [erikkrieg](https://github.com/erikkrieg)) ### Misc - Allow fewer lints ([#1340](https://github.com/casey/just/pull/1340)) - Fix issues reported by nightly clippy ([#1336](https://github.com/casey/just/pull/1336) by [neunenak](https://github.com/neunenak)) - Refactor run.rs ([#1335](https://github.com/casey/just/pull/1335) by [neunenak](https://github.com/neunenak)) - Allow comments on same line as settings ([#1339](https://github.com/casey/just/pull/1339)) - Fix justfile env shebang on Linux ([#1330](https://github.com/casey/just/pull/1330)) - Update Chinese translation of README.md ([#1325](https://github.com/casey/just/pull/1325) by [hustcer](https://github.com/hustcer)) - Add additional settings to grammar - 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)) [1.4.0](https://github.com/casey/just/releases/tag/1.4.0) - 2022-8-08 --------------------------------------------------------------------- ### Fixed - Fix shell setting precedence ([#1306](https://github.com/casey/just/pull/1306)) ### Misc - Don't hardcode homebrew prefix ([#1295](https://github.com/casey/just/pull/1295)) - Exclude files from cargo package ([#1283](https://github.com/casey/just/pull/1283)) - Add usage note to default list recipe ([#1296](https://github.com/casey/just/pull/1296) by [jpbochi](https://github.com/jpbochi)) - Add MPR/Prebuilt-MPR installation instructions to README.md ([#1280](https://github.com/casey/just/pull/1280) by [hwittenborn](https://github.com/hwittenborn)) - Add make and makesure to readme ([#1299](https://github.com/casey/just/pull/1299)) - Document how to configure zsh completions on MacOS ([#1285](https://github.com/casey/just/pull/1285) by [nk9](https://github.com/nk9)) - Convert package table to HTML ([#1291](https://github.com/casey/just/pull/1291)) [1.3.0](https://github.com/casey/just/releases/tag/1.3.0) - 2022-7-25 --------------------------------------------------------------------- ### Added - Add `/` operator ([#1237](https://github.com/casey/just/pull/1237)) ### Fixed - Fix multibyte codepoint crash ([#1243](https://github.com/casey/just/pull/1243)) ### Misc - Update just-install reference on README.md ([#1275](https://github.com/casey/just/pull/1275) by [0xradical](https://github.com/0xradical)) - Split Recipe::run into Recipe::{run_shebang,run_linewise} ([#1270](https://github.com/casey/just/pull/1270)) - Add asdf package to readme([#1264](https://github.com/casey/just/pull/1264) by [jaacko-torus](https://github.com/jaacko-torus)) - Add mdbook deps for build-book recipe ([#1259](https://github.com/casey/just/pull/1259) by [TopherIsSwell](https://github.com/TopherIsSwell)) - Fix typo: argumant -> argument ([#1257](https://github.com/casey/just/pull/1257) by [kianmeng](https://github.com/kianmeng)) - Improve error message if `if` is missing the `else` ([#1252](https://github.com/casey/just/pull/1252) by [nk9](https://github.com/nk9)) - 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)) - Update Chinese translation of README.md ([#1253](https://github.com/casey/just/pull/1253) by [hustcer](https://github.com/hustcer)) - Improvements to Sublime syntax file ([#1250](https://github.com/casey/just/pull/1250) by [nk9](https://github.com/nk9)) - Prevent unbounded recursion when parsing expressions ([#1248](https://github.com/casey/just/pull/1248) by [evanrichter](https://github.com/evanrichter)) - Publish to snap store ([#1245](https://github.com/casey/just/pull/1245)) - Restore fuzz test harness ([#1246](https://github.com/casey/just/pull/1246) by [evanrichter](https://github.com/evanrichter)) - Add just-install to README file ([#1241](https://github.com/casey/just/pull/1241) by [brombal](https://github.com/brombal)) - Fix dead readme link ([#1240](https://github.com/casey/just/pull/1240) by [wdroz](https://github.com/wdroz)) - Do `use super::*;` instead of `use crate::common::*;` ([#1239](https://github.com/casey/just/pull/1239)) - Fix readme punctuation ([#1235](https://github.com/casey/just/pull/1235)) - Add argument splitting section to readme ([#1230](https://github.com/casey/just/pull/1230)) - Add notes about environment variables to readme ([#1229](https://github.com/casey/just/pull/1229)) - Fix book links ([#1227](https://github.com/casey/just/pull/1227)) - Add nushell README.md ([#1224](https://github.com/casey/just/pull/1224) by [hustcer](https://github.com/hustcer)) - Use absolute links in readme ([#1223](https://github.com/casey/just/pull/1223)) - Copy changelog into manual ([#1222](https://github.com/casey/just/pull/1222)) - Translate Chinese manual introduction and title ([#1220](https://github.com/casey/just/pull/1220) by [hustcer](https://github.com/hustcer)) - Build Chinese language user manual ([#1219](https://github.com/casey/just/pull/1219)) - Update Chinese translation of README.md ([#1218](https://github.com/casey/just/pull/1218) by [hustcer](https://github.com/hustcer)) - Translate all of README.md into Chinese ([#1217](https://github.com/casey/just/pull/1217) by [hustcer](https://github.com/hustcer)) - Translate all of features in README into Chinese ([#1215](https://github.com/casey/just/pull/1215) by [hustcer](https://github.com/hustcer)) - Make link to examples directory absolute ([#1213](https://github.com/casey/just/pull/1213)) - Translate part of features in README into Chinese ([#1211](https://github.com/casey/just/pull/1211) by [hustcer](https://github.com/hustcer)) - Add JetBrains IDE plugin to readme ([#1209](https://github.com/casey/just/pull/1209) by [linux-china](https://github.com/linux-china)) - Translate features chapter of readme to Chinese ([#1208](https://github.com/casey/just/pull/1208) by [hustcer](https://github.com/hustcer)) [1.2.0](https://github.com/casey/just/releases/tag/1.2.0) - 2022-5-31 --------------------------------------------------------------------- ### Added - Add `windows-shell` setting ([#1198](https://github.com/casey/just/pull/1198)) - SHA-256 and UUID functions ([#1170](https://github.com/casey/just/pull/1170) by [mbodmer](https://github.com/mbodmer)) ### Misc - Translate editor support and quick start to Chinese ([#1206](https://github.com/casey/just/pull/1206) by [hustcer](https://github.com/hustcer)) - Translate first section of readme into Chinese ([#1205](https://github.com/casey/just/pull/1205) by [hustcer](https://github.com/hustcer)) - Fix a bunch of typos ([#1204](https://github.com/casey/just/pull/1204)) - Remove cargo-limit usage from justfile ([#1199](https://github.com/casey/just/pull/1199)) - Add nix package manager install instructions ([#1194](https://github.com/casey/just/pull/1194) by [risingBirdSong](https://github.com/risingBirdSong)) - Fix broken link in readme ([#1183](https://github.com/casey/just/pull/1183) by [Vlad-Shcherbina](https://github.com/Vlad-Shcherbina)) - Add screenshot to manual ([#1181](https://github.com/casey/just/pull/1181)) - Style homepage ([#1180](https://github.com/casey/just/pull/1180)) - Center readme ([#1178](https://github.com/casey/just/pull/1178)) - Style and add links to homepage ([#1177](https://github.com/casey/just/pull/1177)) - Fix readme badge links ([#1176](https://github.com/casey/just/pull/1176)) - Generate book from readme ([#1155](https://github.com/casey/just/pull/1155)) [1.1.3](https://github.com/casey/just/releases/tag/1.1.3) - 2022-5-3 -------------------------------------------------------------------- ### Fixed - Skip duplicate recipe arguments ([#1174](https://github.com/casey/just/pull/1174)) ### Misc - Fix install script ([#1172](https://github.com/casey/just/pull/1172)) - Document that `invocation_directory()` returns an absolute path ([#1162](https://github.com/casey/just/pull/1162)) - Fix absolute_path documentation ([#1160](https://github.com/casey/just/pull/1160)) - Add cross-platform justfile example ([#1152](https://github.com/casey/just/pull/1152) by [presidento](https://github.com/presidento)) [1.1.2](https://github.com/casey/just/releases/tag/1.1.2) - 2022-3-30 --------------------------------------------------------------------- ### Misc - Document indentation rules ([#1142](https://github.com/casey/just/pull/1142)) - Remove stale link from readme ([#1141](https://github.com/casey/just/pull/1141)) ### Unstable - Search for missing recipes in parent directory justfiles ([#1149](https://github.com/casey/just/pull/1149)) [1.1.1](https://github.com/casey/just/releases/tag/1.1.1) - 2022-3-22 --------------------------------------------------------------------- ### Misc - Build MacOS ARM release binaries ([#1138](https://github.com/casey/just/pull/1138)) - Upgrade Windows Actions runners to windows-latest ([#1137](https://github.com/casey/just/pull/1137)) [1.1.0](https://github.com/casey/just/releases/tag/1.1.0) - 2022-3-10 --------------------------------------------------------------------- ### Added - Add `error()` function ([#1118](https://github.com/casey/just/pull/1118) by [chamons](https://github.com/chamons)) - Add `absolute_path` function ([#1121](https://github.com/casey/just/pull/1121) by [Laura7089](https://github.com/Laura7089)) [1.0.1](https://github.com/casey/just/releases/tag/1.0.1) - 2022-2-28 --------------------------------------------------------------------- ### Fixed - Make path_exists() relative to current directory ([#1122](https://github.com/casey/just/pull/1122)) ### Misc - Detail environment variable usage in readme ([#1086](https://github.com/casey/just/pull/1086) by [kenden](https://github.com/kenden)) - Format --init justfile ([#1116](https://github.com/casey/just/pull/1116) by [TheLocehiliosan](https://github.com/TheLocehiliosan)) - Add hint for Node.js script compatibility ([#1113](https://github.com/casey/just/pull/1113)) [1.0.0](https://github.com/casey/just/releases/tag/1.0.0) - 2022-2-22 --------------------------------------------------------------------- ### Added - Add path_exists() function ([#1106](https://github.com/casey/just/pull/1106) by [heavelock](https://github.com/heavelock)) ### Misc - Note that `pipefail` isn't normally set ([#1108](https://github.com/casey/just/pull/1108)) [0.11.2](https://github.com/casey/just/releases/tag/0.11.2) - 2022-2-15 ----------------------------------------------------------------------- ### Misc - Fix dotenv-load documentation ([#1104](https://github.com/casey/just/pull/1104)) - Fixup broken release package script ([#1100](https://github.com/casey/just/pull/1100) by [lutostag](https://github.com/lutostag)) [0.11.1](https://github.com/casey/just/releases/tag/0.11.1) - 2022-2-14 ----------------------------------------------------------------------- ### Added - Allow duplicate recipes ([#1095](https://github.com/casey/just/pull/1095) by [lutostag](https://github.com/lutostag)) ### Misc - Add arrow pointing to table of contents button ([#1096](https://github.com/casey/just/pull/1096)) - Improve readme ([#1093](https://github.com/casey/just/pull/1093) by [halostatue](https://github.com/halostatue)) - Remove asciidoc readme ([#1092](https://github.com/casey/just/pull/1092)) - Convert README.adoc to markdown ([#1091](https://github.com/casey/just/pull/1091)) - Add choco package to README ([#1090](https://github.com/casey/just/pull/1090) by [michidk](https://github.com/michidk)) [0.11.0](https://github.com/casey/just/releases/tag/0.11.0) - 2022-2-3 ---------------------------------------------------------------------- ### Breaking - Change dotenv-load default to false ([#1082](https://github.com/casey/just/pull/1082)) [0.10.7](https://github.com/casey/just/releases/tag/0.10.7) - 2022-1-30 ----------------------------------------------------------------------- ### Misc - Don't run tests in release workflow ([#1080](https://github.com/casey/just/pull/1080)) - Fix windows chooser invocation error message test ([#1079](https://github.com/casey/just/pull/1079)) - Remove call to sed in justfile ([#1078](https://github.com/casey/just/pull/1078)) [0.10.6](https://github.com/casey/just/releases/tag/0.10.6) - 2022-1-29 ----------------------------------------------------------------------- ### Added - Add windows-powershell setting ([#1057](https://github.com/casey/just/pull/1057) by [michidk](https://github.com/michidk)) ### Changed - Allow using `-` and `@` in any order ([#1063](https://github.com/casey/just/pull/1063)) ### Misc - Use `Context` suffix for snafu error contexts ([#1068](https://github.com/casey/just/pull/1068)) - Upgrade snafu to 0.7 ([#1067](https://github.com/casey/just/pull/1067) by [shepmaster](https://github.com/shepmaster)) - Mention "$@" in the README ([#1064](https://github.com/casey/just/pull/1064) by [mpdude](https://github.com/mpdude)) - Note how to use PowerShell with CLI in readme ([#1056](https://github.com/casey/just/pull/1056) by [michidk](https://github.com/michidk)) - Link to cheatsheet from readme ([#1053](https://github.com/casey/just/pull/1053)) - Link to Homebrew installation docs in readme ([#1049](https://github.com/casey/just/pull/1049) by [michidk](https://github.com/michidk)) - Workflow tweaks ([#1045](https://github.com/casey/just/pull/1045)) - Push to correct origin in publish recipe ([#1044](https://github.com/casey/just/pull/1044)) [0.10.5](https://github.com/casey/just/releases/tag/0.10.5) - 2021-12-4 ----------------------------------------------------------------------- ### Changed - Use musl libc for ARM binaries ([#1037](https://github.com/casey/just/pull/1037)) ### Misc - Make completions work with Bash alias ([#1035](https://github.com/casey/just/pull/1035) by [kurtbuilds](https://github.com/kurtbuilds)) - Run tests on PRs ([#1040](https://github.com/casey/just/pull/1040)) - Improve GitHub Actions workflow triggers ([#1033](https://github.com/casey/just/pull/1033)) - Publish from GitHub master branch instead of local master ([#1032](https://github.com/casey/just/pull/1032)) [0.10.4](https://github.com/casey/just/releases/tag/0.10.4) - 2021-11-21 ------------------------------------------------------------------------ ### Added - Add `--dump-format json` ([#992](https://github.com/casey/just/pull/992)) - Add `quote(s)` function for escaping strings ([#1022](https://github.com/casey/just/pull/1022)) - fmt: check formatting with `--check` ([#1001](https://github.com/casey/just/pull/1001) by [hdhoang](https://github.com/hdhoang)) ### Misc - Refactor github actions ([#1028](https://github.com/casey/just/pull/1028)) - Fix readme formatting ([#1030](https://github.com/casey/just/pull/1030) by [soenkehahn](https://github.com/soenkehahn)) - Use ps1 extension for pwsh shebangs ([#1027](https://github.com/casey/just/pull/1027) by [dmringo](https://github.com/dmringo)) - Ignore leading byte order mark in source files ([#1021](https://github.com/casey/just/pull/1021)) - Add color to `just --fmt --check` diff ([#1015](https://github.com/casey/just/pull/1015)) [0.10.3](https://github.com/casey/just/releases/tag/0.10.3) - 2021-10-30 ------------------------------------------------------------------------ ### Added - Add `trim_end(s)` and `trim_start(s)` functions ([#999](https://github.com/casey/just/pull/999)) - Add more string manipulation functions ([#998](https://github.com/casey/just/pull/998)) ### Changed - Make `join` accept two or more arguments ([#1000](https://github.com/casey/just/pull/1000)) ### Misc - Add alternatives and prior art section to readme ([#1008](https://github.com/casey/just/pull/1008)) - Fix readme `make`'s not correctly displayed ([#1007](https://github.com/casey/just/pull/1007) by [peter50216](https://github.com/peter50216)) - Document the default recipe ([#1006](https://github.com/casey/just/pull/1006)) - Document creating user justfile recipe aliases ([#1005](https://github.com/casey/just/pull/1005)) - Fix readme typo ([#1004](https://github.com/casey/just/pull/1004) by [0xflotus](https://github.com/0xflotus)) - Add packaging status table to readme ([#1003](https://github.com/casey/just/pull/1003)) - Reword `sh` not found error messages ([#1002](https://github.com/casey/just/pull/1002) by [hdhoang](https://github.com/hdhoang)) - Only pass +crt-static to cargo build ([#997](https://github.com/casey/just/pull/997)) - Stop using tabs in justfile in editorconfig ([#996](https://github.com/casey/just/pull/996)) - Use consistent rustflags formatting ([#994](https://github.com/casey/just/pull/994)) - Use `cargo build` instead of `cargo rustc` ([#993](https://github.com/casey/just/pull/993)) - Don't skip variables in variable iterator ([#991](https://github.com/casey/just/pull/991)) - Remove deprecated equals error ([#985](https://github.com/casey/just/pull/985)) [0.10.2](https://github.com/casey/just/releases/tag/0.10.2) - 2021-9-26 ----------------------------------------------------------------------- ### Added - Implement regular expression match conditionals ([#970](https://github.com/casey/just/pull/970)) ### Misc - Add detailed instructions for installing prebuilt binaries ([#978](https://github.com/casey/just/pull/978)) - Improve readme package table formatting ([#977](https://github.com/casey/just/pull/977)) - Add conda package to README ([#976](https://github.com/casey/just/pull/976) by [kellpossible](https://github.com/kellpossible)) - Change MSRV to 1.46.0 ([#968](https://github.com/casey/just/pull/968)) - Use stable rustfmt instead of nightly ([#967](https://github.com/casey/just/pull/967)) - Fix readme typo: FOO → WORLD ([#964](https://github.com/casey/just/pull/964)) - Reword Emacs section in readme ([#962](https://github.com/casey/just/pull/962)) - Mention justl mode for Emacs ([#961](https://github.com/casey/just/pull/961) by [psibi](https://github.com/psibi)) [0.10.1](https://github.com/casey/just/releases/tag/0.10.1) - 2021-8-27 ----------------------------------------------------------------------- ### Added - Add flags for specifying name and path to environment file ([#941](https://github.com/casey/just/pull/941) by [Celeo](https://github.com/Celeo)) ### Misc - Fix error message tests for Alpine Linux ([#956](https://github.com/casey/just/pull/956)) - Bump `target` version to 2.0 ([#957](https://github.com/casey/just/pull/957)) - Mention `tree-sitter-just` in readme ([#951](https://github.com/casey/just/pull/951)) - Document release RSS feed in readme ([#950](https://github.com/casey/just/pull/950)) - Add installation instructions for Gentoo Linux ([#946](https://github.com/casey/just/pull/946) by [dm9pZCAq](https://github.com/dm9pZCAq)) - Make GitHub Actions instructions more prominent ([#944](https://github.com/casey/just/pull/944)) - Wrap `--help` text to terminal width ([#940](https://github.com/casey/just/pull/940)) - Add `.justfile` to sublime syntax file_extensions ([#938](https://github.com/casey/just/pull/938)) - Suggest using `~/.global.justfile` instead of `~/.justfile` ([#937](https://github.com/casey/just/pull/937)) - Update man page ([#935](https://github.com/casey/just/pull/935)) [0.10.0](https://github.com/casey/just/releases/tag/0.10.0) - 2021-8-2 ---------------------------------------------------------------------- ### Changed - Warn if `.env` file is loaded in `dotenv-load` isn't explicitly set ([#925](https://github.com/casey/just/pull/925)) ### Added - Add `--changelog` subcommand ([#932](https://github.com/casey/just/pull/932)) - Support `.justfile` as an alternative to `justfile` ([#931](https://github.com/casey/just/pull/931)) ### Misc - Use cargo-limit for all recipes ([#928](https://github.com/casey/just/pull/928)) - Fix colors ([#927](https://github.com/casey/just/pull/927)) - Use ColorDisplay trait to print objects to the terminal ([#926](https://github.com/casey/just/pull/926)) - Deduplicate recipe parsing ([#923](https://github.com/casey/just/pull/923)) - Move subcommand functions into Subcommand ([#918](https://github.com/casey/just/pull/918)) - Check GitHub Actions workflow with actionlint ([#921](https://github.com/casey/just/pull/921)) - Add loader and refactor errors ([#917](https://github.com/casey/just/pull/917)) - Rename: Module → Ast ([#915](https://github.com/casey/just/pull/915)) [0.9.9](https://github.com/casey/just/releases/tag/0.9.9) - 2021-7-22 --------------------------------------------------------------------- ### Added - Add subsequent dependencies ([#820](https://github.com/casey/just/pull/820)) - Implement `else if` chaining ([#910](https://github.com/casey/just/pull/910)) ### Fixed - Fix circular variable dependency error message ([#909](https://github.com/casey/just/pull/909)) ### Misc - Improve readme ([#904](https://github.com/casey/just/pull/904) by [mtsknn](https://github.com/mtsknn)) - Add screenshot to readme ([#911](https://github.com/casey/just/pull/911)) - Add install instructions for Fedora Linux ([#898](https://github.com/casey/just/pull/898) by [olivierlemasle](https://github.com/olivierlemasle)) - Fix readme typos ([#903](https://github.com/casey/just/pull/903) by [rokf](https://github.com/rokf)) - Actually fix release tagging and publish changelog with releases ([#901](https://github.com/casey/just/pull/901)) - Fix broken prerelease tagging ([#900](https://github.com/casey/just/pull/900)) - Use string value for ref-type check ([#897](https://github.com/casey/just/pull/897)) [0.9.8](https://github.com/casey/just/releases/tag/0.9.8) - 2021-7-3 -------------------------------------------------------------------- ### Misc - Fix changelog formatting ([#894](https://github.com/casey/just/pull/894)) - Only run install script on CI for non-releases ([#895](https://github.com/casey/just/pull/895)) [0.9.7](https://github.com/casey/just/releases/tag/0.9.7) - 2021-7-3 -------------------------------------------------------------------- ### Added - Add string manipulation functions ([#888](https://github.com/casey/just/pull/888) by [terror](https://github.com/terror)) ### Misc - Remove test-utilities crate ([#892](https://github.com/casey/just/pull/892)) - Remove outdated note in `Cargo.toml` ([#891](https://github.com/casey/just/pull/891)) - Link to GitHub release pages in changelog ([#886](https://github.com/casey/just/pull/886)) [0.9.6](https://github.com/casey/just/releases/tag/0.9.6) - 2021-6-24 --------------------------------------------------------------------- ### Added - Add `clean` function for simplifying paths ([#883](https://github.com/casey/just/pull/883)) - Add `join` function for joining paths ([#882](https://github.com/casey/just/pull/882)) - Add path manipulation functions ([#872](https://github.com/casey/just/pull/872) by [TonioGela](https://github.com/TonioGela)) ### Misc - Add `file_extensions` to Sublime syntax file ([#878](https://github.com/casey/just/pull/878) by [Frederick888](https://github.com/Frederick888)) - Document path manipulation functions in readme ([#877](https://github.com/casey/just/pull/877)) [0.9.5](https://github.com/casey/just/releases/tag/0.9.5) - 2021-6-12 --------------------------------------------------------------------- ### Added - Add `--unstable` flag ([#869](https://github.com/casey/just/pull/869)) - Add Sublime Text syntax file ([#864](https://github.com/casey/just/pull/864)) - Add `--fmt` subcommand ([#837](https://github.com/casey/just/pull/837) by [vglfr](https://github.com/vglfr)) ### Misc - Mention doniogela.dev/just/ in readme ([#866](https://github.com/casey/just/pull/866)) - Mention that vim-just is now available from vim-polyglot ([#865](https://github.com/casey/just/pull/865)) - Mention `--list-heading` newline behavior ([#860](https://github.com/casey/just/pull/860)) - Check for `rg` in `bin/forbid` ([#859](https://github.com/casey/just/pull/859)) - Document that variables are not exported to backticks in the same scope ([#856](https://github.com/casey/just/pull/856)) - Remove `dotenv_load` from tests ([#853](https://github.com/casey/just/pull/853)) - Remove `v` prefix from version ([#850](https://github.com/casey/just/pull/850)) - Improve install script ([#847](https://github.com/casey/just/pull/847)) - Move pages assets back to `docs` ([#846](https://github.com/casey/just/pull/846)) - Move pages assets to `www` ([#845](https://github.com/casey/just/pull/845)) [0.9.4](https://github.com/casey/just/releases/tag/v0.9.4) - 2021-5-27 ---------------------------------------------------------------------- ### Misc - Release `aarch64-unknown-linux-gnu` binaries ([#843](https://github.com/casey/just/pull/843)) - Add `$` to non-default parameter grammar ([#839](https://github.com/casey/just/pull/839)) - Add `$` to parameter grammar ([#838](https://github.com/casey/just/pull/838) by [NoahTheDuke](https://github.com/NoahTheDuke)) - Fix readme links ([#836](https://github.com/casey/just/pull/836)) - Add `vim-just` installation instructions to readme ([#835](https://github.com/casey/just/pull/835)) - Refactor shebang handling ([#833](https://github.com/casey/just/pull/833)) [0.9.3](https://github.com/casey/just/releases/tag/v0.9.3) - 2021-5-16 ---------------------------------------------------------------------- ### Added - Add shebang support for 'cmd.exe' ([#828](https://github.com/casey/just/pull/828) by [pansila](https://github.com/pansila)) - Add `.exe` to powershell scripts ([#826](https://github.com/casey/just/pull/826) by [sigoden](https://github.com/sigoden)) - Add the `--command` subcommand ([#824](https://github.com/casey/just/pull/824)) ### Fixed - Fix bang lexing and placate clippy ([#821](https://github.com/casey/just/pull/821)) ### Misc - Fixed missing close apostrophe in GRAMMAR.md ([#830](https://github.com/casey/just/pull/830) by [SOF3](https://github.com/SOF3)) - Make 'else' keyword in grammar ([#829](https://github.com/casey/just/pull/829) by [SOF3](https://github.com/SOF3)) - Add forbid script ([#827](https://github.com/casey/just/pull/827)) - Remove `summary` feature ([#823](https://github.com/casey/just/pull/823)) - Document that just is now in Arch official repo ([#814](https://github.com/casey/just/pull/814) by [svenstaro](https://github.com/svenstaro)) - Fix changelog years ([#813](https://github.com/casey/just/pull/813)) [0.9.2](https://github.com/casey/just/releases/tag/v0.9.2) - 2021-5-02 ---------------------------------------------------------------------- ### Fixed - Pass evaluated arguments as positional arguments ([#810](https://github.com/casey/just/pull/810)) [0.9.1](https://github.com/casey/just/releases/tag/v0.9.1) - 2021-4-24 ---------------------------------------------------------------------- ### Added - Change `--eval` to print variable value only ([#806](https://github.com/casey/just/pull/806)) - Add `positional-arguments` setting ([#804](https://github.com/casey/just/pull/804)) - Allow filtering variables to evaluate ([#795](https://github.com/casey/just/pull/795)) ### Changed - Reform and improve string literals ([#793](https://github.com/casey/just/pull/793)) - Allow evaluating justfiles with no recipes ([#794](https://github.com/casey/just/pull/794)) - Unify string lexing ([#790](https://github.com/casey/just/pull/790)) ### Misc - Test multi-line strings in interpolation ([#789](https://github.com/casey/just/pull/789)) - Add shell setting examples to README ([#787](https://github.com/casey/just/pull/787)) - Disable .env warning for now - Warn if `.env` file loaded and `dotenv-load` unset ([#784](https://github.com/casey/just/pull/784)) [0.9.0](https://github.com/casey/just/releases/tag/v0.9.0) - 2021-3-28 ---------------------------------------------------------------------- ### Changed - Turn `=` deprecation warning into a hard error ([#780](https://github.com/casey/just/pull/780)) [0.8.7](https://github.com/casey/just/releases/tag/v0.8.7) - 2021-3-28 ---------------------------------------------------------------------- ### Added - Add `dotenv-load` setting ([#778](https://github.com/casey/just/pull/778)) ### Misc - Change publish recipe to use stable rust ([#777](https://github.com/casey/just/pull/777)) [0.8.6](https://github.com/casey/just/releases/tag/v0.8.6) - 2021-3-28 ---------------------------------------------------------------------- ### Added - Add just_executable() function ([#775](https://github.com/casey/just/pull/775) by [bew](https://github.com/bew)) - Prefix parameters with `$` to export to environment ([#773](https://github.com/casey/just/pull/773)) - Add `set export` to export all variables as environment variables ([#767](https://github.com/casey/just/pull/767)) ### Changed - Suppress all output to stderr when `--quiet` ([#771](https://github.com/casey/just/pull/771)) ### Misc - Improve chooser invocation error message ([#772](https://github.com/casey/just/pull/772)) - De-emphasize cmd.exe in readme ([#768](https://github.com/casey/just/pull/768)) - Fix warnings ([#770](https://github.com/casey/just/pull/770)) [0.8.5](https://github.com/casey/just/releases/tag/v0.8.5) - 2021-3-24 ---------------------------------------------------------------------- ### Added - Allow escaping double braces with `{{{{` ([#765](https://github.com/casey/just/pull/765)) ### Misc - Reorganize readme to highlight editor support ([#764](https://github.com/casey/just/pull/764)) - Add categories and keywords to Cargo manifest ([#763](https://github.com/casey/just/pull/763)) - Fix command output in readme ([#760](https://github.com/casey/just/pull/760) by [vvv](https://github.com/vvv)) - Note Emacs package `just-mode` in readme ([#759](https://github.com/casey/just/pull/759) by [leon-barrett](https://github.com/leon-barrett)) - Note shebang line splitting inconsistency in readme ([#757](https://github.com/casey/just/pull/757)) [0.8.4](https://github.com/casey/just/releases/tag/v0.8.4) - 2021-2-9 --------------------------------------------------------------------- ### Added - Add options to control list formatting ([#753](https://github.com/casey/just/pull/753)) ### Misc - Document how to change the working directory in a recipe ([#752](https://github.com/casey/just/pull/752)) - Implement `Default` for `Table` ([#748](https://github.com/casey/just/pull/748)) - Add Alpine Linux package to readme ([#736](https://github.com/casey/just/pull/736) by [jirutka](https://github.com/jirutka)) - Update to actions/cache@v2 ([#742](https://github.com/casey/just/pull/742) by [zyctree](https://github.com/zyctree)) - Add link in readme to GitHub Action ([#729](https://github.com/casey/just/pull/729) by [rossmacarthur](https://github.com/rossmacarthur)) - Add docs for justfile() and justfile_directory() ([#726](https://github.com/casey/just/pull/726) by [rminderhoud](https://github.com/rminderhoud)) - Fix CI ([#727](https://github.com/casey/just/pull/727)) - Improve readme ([#725](https://github.com/casey/just/pull/725)) - Replace saythanks.io link with malto: link ([#723](https://github.com/casey/just/pull/723)) - Update man page to v0.8.3 ([#720](https://github.com/casey/just/pull/720)) [0.8.3](https://github.com/casey/just/releases/tag/v0.8.3) - 2020-10-27 ----------------------------------------------------------------------- ### Added - Allow ignoring line endings inside delimiters ([#717](https://github.com/casey/just/pull/717)) [0.8.2](https://github.com/casey/just/releases/tag/v0.8.2) - 2020-10-26 ----------------------------------------------------------------------- ### Added - Add conditional expressions ([#714](https://github.com/casey/just/pull/714)) ### Fixed - 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)) ### Misc - Add Parser::forbid ([#712](https://github.com/casey/just/pull/712)) - Automatically track expected tokens while parsing ([#711](https://github.com/casey/just/pull/711)) - Document feature flags in Cargo.toml ([#709](https://github.com/casey/just/pull/709)) [0.8.1](https://github.com/casey/just/releases/tag/v0.8.1) - 2020-10-15 ----------------------------------------------------------------------- ### Changed - Allow choosing multiple recipes to run ([#700](https://github.com/casey/just/pull/700)) - Complete recipes in bash completion script ([#685](https://github.com/casey/just/pull/685) by [vikesh-raj](https://github.com/vikesh-raj)) - Complete recipes names in PowerShell completion script ([#651](https://github.com/casey/just/pull/651) by [Insomniak47](https://github.com/Insomniak47)) ### Misc - Add FreeBSD port to readme ([#705](https://github.com/casey/just/pull/705)) - Placate clippy ([#698](https://github.com/casey/just/pull/698)) - Fix build fix ([#693](https://github.com/casey/just/pull/693)) - Fix readme documentation for ignoring errors ([#692](https://github.com/casey/just/pull/692) by [kenden](https://github.com/kenden)) [0.8.0](https://github.com/casey/just/releases/tag/v0.8.0) - 2020-10-3 ---------------------------------------------------------------------- ### Breaking - Allow suppressing failures with `-` prefix ([#687](https://github.com/casey/just/pull/687) by [iwillspeak](https://github.com/iwillspeak)) ### Misc - Document how to ignore errors with `-` in readme ([#690](https://github.com/casey/just/pull/690)) - Install BSD Tar on GitHub Actions to fix CI errors ([#689](https://github.com/casey/just/pull/689)) - Move separate quiet config value to verbosity ([#686](https://github.com/casey/just/pull/686) by [Celeo](https://github.com/Celeo)) [0.7.3](https://github.com/casey/just/releases/tag/v0.7.3) - 2020-9-17 ---------------------------------------------------------------------- ### Added - Add the `--choose` subcommand ([#680](https://github.com/casey/just/pull/680)) ### Misc - Combine integration tests into single binary ([#679](https://github.com/casey/just/pull/679)) - Document `--unsorted` flag in readme ([#672](https://github.com/casey/just/pull/672)) [0.7.2](https://github.com/casey/just/releases/tag/v0.7.2) - 2020-8-23 ---------------------------------------------------------------------- ### Added - Add option to print recipes in source order ([#669](https://github.com/casey/just/pull/669)) ### Misc - Mention Linux, MacOS and Windows support in readme ([#666](https://github.com/casey/just/pull/666)) - Add list highlighting nice features to readme ([#664](https://github.com/casey/just/pull/664)) [0.7.1](https://github.com/casey/just/releases/tag/v0.7.1) - 2020-7-19 ---------------------------------------------------------------------- ### Fixed - Search for `.env` file from working directory ([#661](https://github.com/casey/just/pull/661)) ### Misc - Move link-time optimization config into `Cargo.toml` ([#658](https://github.com/casey/just/pull/658)) [0.7.0](https://github.com/casey/just/releases/tag/v0.7.0) - 2020-7-16 ---------------------------------------------------------------------- ### Breaking - Skip `.env` items which are set in environment ([#656](https://github.com/casey/just/pull/656)) ### Misc - Mark tags that start with `v` as releases ([#654](https://github.com/casey/just/pull/654)) [0.6.1](https://github.com/casey/just/releases/tag/v0.6.1) - 2020-6-28 ---------------------------------------------------------------------- ### Changed - Only use `cygpath` on shebang if it contains `/` ([#652](https://github.com/casey/just/pull/652)) [0.6.0](https://github.com/casey/just/releases/tag/v0.6.0) - 2020-6-18 ---------------------------------------------------------------------- ### Changed - Ignore '@' returned from interpolation evaluation ([#636](https://github.com/casey/just/pull/636) by [rjsberry](https://github.com/rjsberry)) - Strip leading spaces after line continuation ([#635](https://github.com/casey/just/pull/635)) ### Added - Add variadic parameters that accept zero or more arguments ([#645](https://github.com/casey/just/pull/645) by [rjsberry](https://github.com/rjsberry)) ### Misc - Clarify variadic parameter default values ([#646](https://github.com/casey/just/pull/646) by [rjsberry](https://github.com/rjsberry)) - Add keybase example justfile ([#640](https://github.com/casey/just/pull/640) by [blaggacao](https://github.com/blaggacao)) - Strip trailing whitespace in `examples/pre-commit.just` ([#644](https://github.com/casey/just/pull/644)) - Test that example justfiles successfully parse ([#643](https://github.com/casey/just/pull/643)) - Link example justfiles in readme ([#641](https://github.com/casey/just/pull/641)) - Add example justfile ([#639](https://github.com/casey/just/pull/639) by [blaggacao](https://github.com/blaggacao)) - Document how to run recipes after another recipe ([#630](https://github.com/casey/just/pull/630)) [0.5.11](https://github.com/casey/just/releases/tag/v0.5.11) - 2020-5-23 ------------------------------------------------------------------------ ### Added - Don't load `.env` file when `--no-dotenv` is passed ([#627](https://github.com/casey/just/pull/627)) ### Changed - Complete recipe names in fish completion script ([#625](https://github.com/casey/just/pull/625) by [tyehle](https://github.com/tyehle)) - Suggest aliases for unknown recipes ([#624](https://github.com/casey/just/pull/624) by [Celeo](https://github.com/Celeo)) [0.5.10](https://github.com/casey/just/releases/tag/v0.5.10) - 2020-3-18 ------------------------------------------------------------------------ [0.5.9](https://github.com/casey/just/releases/tag/v0.5.9) - 2020-3-18 ---------------------------------------------------------------------- ### Added - Update zsh completion file ([#606](https://github.com/casey/just/pull/606) by [heyrict](https://github.com/heyrict)) - Add `--variables` subcommand that prints variable names ([#608](https://github.com/casey/just/pull/608)) - Add github pages site with improved install script ([#597](https://github.com/casey/just/pull/597)) ### Fixed - Don't require justfile to print completions ([#596](https://github.com/casey/just/pull/596)) ### Misc - Only build for linux on docs.rs ([#611](https://github.com/casey/just/pull/611)) - Trim completions and ensure final newline ([#609](https://github.com/casey/just/pull/609)) - Trigger build on pushes and pull requests ([#607](https://github.com/casey/just/pull/607)) - Document behavior of `@` on shebang recipes ([#602](https://github.com/casey/just/pull/602)) - Add `.nojekyll` file to github pages site ([#599](https://github.com/casey/just/pull/599)) - Add `:` favicon ([#598](https://github.com/casey/just/pull/598)) - Delete old CI configuration and update build badge ([#595](https://github.com/casey/just/pull/595)) - Add download count badge to readme ([#594](https://github.com/casey/just/pull/594)) - Wrap comments at 80 characters ([#593](https://github.com/casey/just/pull/593)) - Use unstable rustfmt configuration options ([#592](https://github.com/casey/just/pull/592)) [0.5.8](https://github.com/casey/just/releases/tag/v0.5.8) - 2020-1-28 ---------------------------------------------------------------------- ### Changed - Only use `cygpath` on windows if present ([#586](https://github.com/casey/just/pull/586)) ### Misc - Improve comments in justfile ([#588](https://github.com/casey/just/pull/588)) - Remove unused dependencies ([#587](https://github.com/casey/just/pull/587)) [0.5.7](https://github.com/casey/just/releases/tag/v0.5.7) - 2020-1-28 ---------------------------------------------------------------------- ### Misc - Don't include directories in release archive ([#583](https://github.com/casey/just/pull/583)) [0.5.6](https://github.com/casey/just/releases/tag/v0.5.6) - 2020-1-28 ---------------------------------------------------------------------- ### Misc - Build and upload release artifacts from GitHub Actions ([#581](https://github.com/casey/just/pull/581)) - List solus package in readme ([#579](https://github.com/casey/just/pull/579)) - Expand use of GitHub Actions ([#580](https://github.com/casey/just/pull/580)) - Fix readme typo: interpetation -> interpretation ([#578](https://github.com/casey/just/pull/578) by [Plommonsorbet](https://github.com/Plommonsorbet)) [0.5.5](https://github.com/casey/just/releases/tag/v0.5.5) - 2020-1-15 ---------------------------------------------------------------------- ### Added - Generate shell completion scripts with `--completions` ([#572](https://github.com/casey/just/pull/572)) ### Misc - Check long lines and FIXME/TODO on CI ([#575](https://github.com/casey/just/pull/575)) - Add additional continuous integration checks ([#574](https://github.com/casey/just/pull/574)) [0.5.4](https://github.com/casey/just/releases/tag/v0.5.4) - 2019-12-25 ----------------------------------------------------------------------- ### Added - Add `justfile_directory()` and `justfile()` ([#569](https://github.com/casey/just/pull/569)) ### Misc - Add table of package managers that include just to readme ([#568](https://github.com/casey/just/pull/568)) - Remove yaourt AUR helper from readme ([#567](https://github.com/casey/just/pull/567) by [ky0n](https://github.com/ky0n)) - Fix regression in error message color printing ([#566](https://github.com/casey/just/pull/566)) - Reform indentation handling ([#565](https://github.com/casey/just/pull/565)) - Update Cargo.lock with new version ([#564](https://github.com/casey/just/pull/564)) [0.5.3](https://github.com/casey/just/releases/tag/v0.5.3) - 2019-12-11 ----------------------------------------------------------------------- ### Misc - Assert that lexer advances over entire input ([#560](https://github.com/casey/just/pull/560)) - Fix typo: `chracter` -> `character` ([#561](https://github.com/casey/just/pull/561)) - Improve pre-publish check ([#562](https://github.com/casey/just/pull/562)) [0.5.2](https://github.com/casey/just/releases/tag/v0.5.2) - 2019-12-7 ---------------------------------------------------------------------- ### Added - Add flags to set and clear shell arguments ([#551](https://github.com/casey/just/pull/551)) - Allow passing arguments to dependencies ([#555](https://github.com/casey/just/pull/555)) ### Misc - Un-implement Deref for Table ([#546](https://github.com/casey/just/pull/546)) - Resolve recipe dependencies ([#547](https://github.com/casey/just/pull/547)) - Resolve alias targets ([#548](https://github.com/casey/just/pull/548)) - Remove unnecessary type argument to Alias ([#549](https://github.com/casey/just/pull/549)) - Resolve functions ([#550](https://github.com/casey/just/pull/550)) - Reform scope and binding ([#556](https://github.com/casey/just/pull/556)) [0.5.1](https://github.com/casey/just/releases/tag/v0.5.1) - 2019-11-20 ----------------------------------------------------------------------- ### Added - Add `--init` subcommand ([#541](https://github.com/casey/just/pull/541)) ### Changed - Avoid fs::canonicalize ([#539](https://github.com/casey/just/pull/539)) ### Misc - Mention `set shell` as alternative to installing `sh` ([#533](https://github.com/casey/just/pull/533)) - Refactor Compilation error to contain a Token ([#535](https://github.com/casey/just/pull/535)) - Move lexer comment ([#536](https://github.com/casey/just/pull/536)) - Add missing `--init` test ([#543](https://github.com/casey/just/pull/543)) [0.5.0](https://github.com/casey/just/releases/tag/v0.5.0) - 2019-11-12 ----------------------------------------------------------------------- ### Added - Add `set shell := [...]` to grammar ([#526](https://github.com/casey/just/pull/526)) - Add `shell` setting ([#525](https://github.com/casey/just/pull/525)) - Document settings in readme ([#527](https://github.com/casey/just/pull/527)) ### Changed - Reform positional argument parsing ([#523](https://github.com/casey/just/pull/523)) - Highlight echoed recipe lines in bold by default ([#512](https://github.com/casey/just/pull/512)) ### Misc - Gargantuan refactor ([#522](https://github.com/casey/just/pull/522)) - Move subcommand execution into Subcommand ([#514](https://github.com/casey/just/pull/514)) - Move `cd` out of Config::from_matches ([#513](https://github.com/casey/just/pull/513)) - Remove now-unnecessary borrow checker appeasement ([#511](https://github.com/casey/just/pull/511)) - Reform Parser ([#509](https://github.com/casey/just/pull/509)) - Note need to publish with nightly cargo ([#506](https://github.com/casey/just/pull/506)) [0.4.5](https://github.com/casey/just/releases/tag/v0.4.5) - 2019-10-31 ----------------------------------------------------------------------- ### User-visible ### Changed - Display alias with `--show NAME` if one exists ### Documented - Document multi-line constructs (for/if/while) ([#453](https://github.com/casey/just/pull/453)) - Generate man page with help2man ([#463](https://github.com/casey/just/pull/463)) - Add context to deprecation warnings ([#473](https://github.com/casey/just/pull/473)) - Improve messages for alias error messages ([#500](https://github.com/casey/just/pull/500)) ### Misc ### Cleanup - Update deprecated rust range patterns and clippy config ([#450](https://github.com/casey/just/pull/450) by [light4](https://github.com/light4)) - Make comments in common.rs lowercase ([#470](https://github.com/casey/just/pull/470)) - Use `pub(crate)` instead of `pub` ([#471](https://github.com/casey/just/pull/471)) - Hide summary functionality behind feature flag ([#472](https://github.com/casey/just/pull/472)) - Fix `summary` feature conditional compilation ([#475](https://github.com/casey/just/pull/475)) - Allow integration test cases to omit common values ([#480](https://github.com/casey/just/pull/480)) - Add `unindent()` for nicer integration test strings ([#481](https://github.com/casey/just/pull/481)) - Start pulling argument parsing out of run::run() ([#483](https://github.com/casey/just/pull/483)) - Add explicit `Subcommand` enum ([#484](https://github.com/casey/just/pull/484)) - Avoid using error code `1` in integration tests ([#486](https://github.com/casey/just/pull/486)) - Use more indented strings in integration tests ([#489](https://github.com/casey/just/pull/489)) - Refactor `run::run` and Config ([#490](https://github.com/casey/just/pull/490)) - Remove `misc.rs` ([#491](https://github.com/casey/just/pull/491)) - Remove unused `use` statements ([#497](https://github.com/casey/just/pull/497)) - Refactor lexer tests ([#498](https://github.com/casey/just/pull/498)) - Use constants instead of literals in arg parser ([#504](https://github.com/casey/just/pull/504)) ### Infrastructure - Add repository attribute to Cargo.toml ([#493](https://github.com/casey/just/pull/493) by [SOF3](https://github.com/SOF3)) - Check minimal version compatibility before publishing ([#487](https://github.com/casey/just/pull/487)) ### Continuous Integration - Disable FreeBSD builds ([#474](https://github.com/casey/just/pull/474)) - Use `bash` as shell for all integration tests ([#479](https://github.com/casey/just/pull/479)) - Don't install `dash` on Travis ([#482](https://github.com/casey/just/pull/482)) ### Dependencies - Use `tempfile` crate instead of `tempdir` ([#455](https://github.com/casey/just/pull/455) by [NickeZ](https://github.com/NickeZ)) - Bump clap dependency to 2.33.0 ([#458](https://github.com/casey/just/pull/458) by [NickeZ](https://github.com/NickeZ)) - Minimize dependency version requirements ([#461](https://github.com/casey/just/pull/461)) - Remove dependency on brev ([#462](https://github.com/casey/just/pull/462)) - Update dependencies ([#501](https://github.com/casey/just/pull/501)) [0.4.4](https://github.com/casey/just/releases/tag/v0.4.4) - 2019-06-02 ----------------------------------------------------------------------- ### Changed - Ignore file name case while searching for justfile ([#436](https://github.com/casey/just/pull/436) by [shevtsiv](https://github.com/shevtsiv)) ### Added - Display alias target with `--show` ([#443](https://github.com/casey/just/pull/443)) [0.4.3](https://github.com/casey/just/releases/tag/v0.4.3) - 2019-05-07 ----------------------------------------------------------------------- ### Changed - Deprecate `=` in assignments, aliases, and exports in favor of `:=` ([#413](https://github.com/casey/just/pull/413)) ### Added - Pass stdin handle to backtick process ([#409](https://github.com/casey/just/pull/409)) ### Documented - Fix readme command line ([#411](https://github.com/casey/just/pull/411)) - Typo: "command equivelant" -> "command equivalent" ([#418](https://github.com/casey/just/pull/418)) - Mention Make’s “phony target” workaround in the comparison ([#421](https://github.com/casey/just/pull/421) by [roryokane](https://github.com/roryokane)) - Add Void Linux install instructions to readme ([#423](https://github.com/casey/just/pull/423)) ### Cleaned up or Refactored - Remove stray source files ([#408](https://github.com/casey/just/pull/408)) - Replace some calls to brev crate ([#410](https://github.com/casey/just/pull/410)) - Lexer code deduplication and refactoring ([#414](https://github.com/casey/just/pull/414)) - Refactor and rename test macros ([#415](https://github.com/casey/just/pull/415)) - Move CompilationErrorKind into separate module ([#416](https://github.com/casey/just/pull/416)) - Remove `write_token_error_context` ([#417](https://github.com/casey/just/pull/417)) [0.4.2](https://github.com/casey/just/releases/tag/v0.4.2) - 2019-04-12 ----------------------------------------------------------------------- ### Changed - Regex-based lexer replaced with much nicer character-at-a-time lexer ([#406](https://github.com/casey/just/pull/406)) [0.4.1](https://github.com/casey/just/releases/tag/v0.4.1) - 2019-04-12 ----------------------------------------------------------------------- ### Changed - Make summary function non-generic ([#404](https://github.com/casey/just/pull/404)) [0.4.0](https://github.com/casey/just/releases/tag/v0.4.0) - 2019-04-12 ----------------------------------------------------------------------- ### Added - Add recipe aliases ([#390](https://github.com/casey/just/pull/390) by [ryloric](https://github.com/ryloric)) - Allow arbitrary expressions as default arguments ([#400](https://github.com/casey/just/pull/400)) - Add justfile summaries ([#399](https://github.com/casey/just/pull/399)) - Allow outer shebang lines so justfiles can be used as scripts ([#393](https://github.com/casey/just/pull/393)) - Allow `--justfile` without `--working-directory` ([#392](https://github.com/casey/just/pull/392) by [smonami](https://github.com/smonami)) - Add link to Chinese translation of readme by chinanf-boy ([#377](https://github.com/casey/just/pull/377)) ### Changed - Upgrade to Rust 2018 ([#394](https://github.com/casey/just/pull/394)) - Format the codebase with rustfmt ([#346](https://github.com/casey/just/pull/346)) [0.3.13](https://github.com/casey/just/releases/tag/v0.3.13) - 2018-11-06 ------------------------------------------------------------------------- ### Added - Print recipe signature if missing arguments ([#369](https://github.com/casey/just/pull/369) by [ladysamantha](https://github.com/ladysamantha)) - Add grandiloquent verbosity level that echos shebang recipes ([#348](https://github.com/casey/just/pull/348)) - Wait for child processes to finish ([#345](https://github.com/casey/just/pull/345)) - Improve invalid escape sequence error messages ([#328](https://github.com/casey/just/pull/328)) ### Fixed - Use PutBackN instead of PutBack in parser ([#364](https://github.com/casey/just/pull/364)) [0.3.12](https://github.com/casey/just/releases/tag/v0.3.12) - 2018-06-19 ------------------------------------------------------------------------- ### Added - Implemented invocation_directory function [0.3.11](https://github.com/casey/just/releases/tag/v0.3.11) - 2018-05-6 ------------------------------------------------------------------------ ### Fixed - Fixed colors on windows ([#317](https://github.com/casey/just/pull/317)) [0.3.10](https://github.com/casey/just/releases/tag/v0.3.10) - 2018-3-19 ------------------------------------------------------------------------ ### Added - Make .env vars available in env_var functions ([#310](https://github.com/casey/just/pull/310)) [0.3.8](https://github.com/casey/just/releases/tag/v0.3.8) - 2018-3-5 --------------------------------------------------------------------- ### Added - Add dotenv integration ([#306](https://github.com/casey/just/pull/306)) [0.3.7](https://github.com/casey/just/releases/tag/v0.3.7) - 2017-12-11 ----------------------------------------------------------------------- ### Fixed - Fix error if ! appears in comment ([#296](https://github.com/casey/just/pull/296)) [0.3.6](https://github.com/casey/just/releases/tag/v0.3.6) - 2017-12-11 ----------------------------------------------------------------------- ### Fixed - Lex CRLF line endings properly ([#292](https://github.com/casey/just/pull/292)) [0.3.5](https://github.com/casey/just/releases/tag/v0.3.5) - 2017-12-11 ----------------------------------------------------------------------- ### Added - Align doc-comments in `--list` output ([#273](https://github.com/casey/just/pull/273)) - Add `arch()`, `os()`, and `os_family()` functions ([#277](https://github.com/casey/just/pull/277)) - Add `env_var(key)` and `env_var_or_default(key, default)` functions ([#280](https://github.com/casey/just/pull/280)) [0.3.4](https://github.com/casey/just/releases/tag/v0.3.4) - 2017-10-06 ----------------------------------------------------------------------- ### Added - Do not evaluate backticks in assignments during dry runs ([#253](https://github.com/casey/just/pull/253) by [aoeu](https://github.com/aoeu)) ### Changed - Change license to CC0 going forward ([#270](https://github.com/casey/just/pull/270)) [0.3.1](https://github.com/casey/just/releases/tag/v0.3.1) - 2017-10-06 ----------------------------------------------------------------------- ### Added - Started keeping a changelog in CHANGELOG.md ([#220](https://github.com/casey/just/pull/220)) - Recipes whose names begin with an underscore will not appear in `--list` or `--summary` ([#229](https://github.com/casey/just/pull/229)) just-1.21.0/CONTRIBUTING.md000064400000000000000000000003441046102023000131240ustar 00000000000000Contributing ============ Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you shall be licensed as in [LICENSE](LICENSE), without any additional terms or conditions. just-1.21.0/Cargo.lock0000644000000637230000000000100100700ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ "winapi", ] [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", "winapi", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bstr" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" dependencies = [ "lazy_static", "memchr", "regex-automata 0.1.10", ] [[package]] name = "camino" version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", "bitflags 1.3.2", "strsim", "term_size", "textwrap", "unicode-width", "vec_map", ] [[package]] name = "cpufeatures" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] [[package]] name = "cradle" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7096122c1023d53de7298f322590170540ad3eba46bbc2750b495f098c27c09a" dependencies = [ "rustversion", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "ctrlc" version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b" dependencies = [ "nix", "windows-sys 0.52.0", ] [[package]] name = "derivative" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys 0.48.0", ] [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "edit-distance" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbaaaf38131deb9ca518a274a45bfdb8771f139517b073b16c2d3d32ae5037b" [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "env_logger" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" dependencies = [ "humantime", "is-terminal", "log", "regex", "termcolor", ] [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "executable-path" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ebc5a6d89e3c90b84e8f33c8737933dda8f1c106b5415900b38b9d433841478" [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "hermit-abi" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "home" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "is-terminal" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" dependencies = [ "hermit-abi 0.3.3", "rustix", "windows-sys 0.52.0", ] [[package]] name = "itoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "just" version = "1.21.0" dependencies = [ "ansi_term", "atty", "camino", "clap", "cradle", "ctrlc", "derivative", "dirs", "dotenvy", "edit-distance", "env_logger", "executable-path", "heck", "lexiclean", "libc", "log", "num_cpus", "pretty_assertions", "regex", "semver", "serde", "serde_json", "sha2", "similar", "snafu", "strum", "target", "tempfile", "temptree", "typed-arena", "unicode-width", "uuid", "which", "yaml-rust", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lexiclean" version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "441225017b106b9f902e97947a6d31e44ebcf274b91bdbfb51e5c477fcd468e5" [[package]] name = "libc" version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libredox" version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ "bitflags 2.4.1", "libc", "redox_syscall", ] [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "nix" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ "bitflags 2.4.1", "cfg-if", "libc", ] [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi 0.3.3", "libc", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "pretty_assertions" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ "diff", "yansi", ] [[package]] name = "proc-macro2" version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_users" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ "getrandom", "libredox", "thiserror", ] [[package]] name = "regex" version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.3", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-automata" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rustix" version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustversion" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "semver" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", "syn 2.0.43", ] [[package]] name = "serde_json" version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "similar" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" dependencies = [ "bstr", "unicode-segmentation", ] [[package]] name = "snafu" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" dependencies = [ "doc-comment", "snafu-derive", ] [[package]] name = "snafu-derive" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" dependencies = [ "heck", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "strum" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", "syn 2.0.43", ] [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "target" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba852e71502340e2eaf2fa51f9b3ec6aa25750da1aa65771491c69d67789b05c" [[package]] name = "tempfile" version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", "windows-sys 0.52.0", ] [[package]] name = "temptree" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fda94d8251b40088cb769576f436da19ac1d1ae792c97d0afe1cadc890c8630" dependencies = [ "tempfile", ] [[package]] name = "term_size" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" dependencies = [ "libc", "winapi", ] [[package]] name = "termcolor" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] [[package]] name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ "term_size", "unicode-width", ] [[package]] name = "thiserror" version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" dependencies = [ "proc-macro2", "quote", "syn 2.0.43", ] [[package]] name = "typed-arena" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-segmentation" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "uuid" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "getrandom", ] [[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "which" version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14" dependencies = [ "either", "home", "once_cell", "rustix", "windows-sys 0.48.0", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.0", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ "windows_aarch64_gnullvm 0.52.0", "windows_aarch64_msvc 0.52.0", "windows_i686_gnu 0.52.0", "windows_i686_msvc 0.52.0", "windows_x86_64_gnu 0.52.0", "windows_x86_64_gnullvm 0.52.0", "windows_x86_64_msvc 0.52.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "yaml-rust" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] [[package]] name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" just-1.21.0/Cargo.toml0000644000000055770000000000100101160ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.63" name = "just" version = "1.21.0" authors = ["Casey Rodarmor "] exclude = [ "/book", "/icon.png", "/screenshot.png", "/www", ] autotests = false description = "🤖 Just a command runner" homepage = "https://github.com/casey/just" readme = "crates-io-readme.md" keywords = [ "command-line", "task", "runner", "development", "utility", ] categories = [ "command-line-utilities", "development-tools", ] license = "CC0-1.0" repository = "https://github.com/casey/just" [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] [profile.release] lto = true [lib] doctest = false [[bin]] name = "just" path = "src/main.rs" test = false [[test]] name = "integration" path = "tests/lib.rs" [dependencies.ansi_term] version = "0.12.0" [dependencies.atty] version = "0.2.0" [dependencies.camino] version = "1.0.4" [dependencies.clap] version = "2.33.0" features = ["wrap_help"] [dependencies.ctrlc] version = "3.1.1" features = ["termination"] [dependencies.derivative] version = "2.0.0" [dependencies.dirs] version = "5.0.1" [dependencies.dotenvy] version = "0.15" [dependencies.edit-distance] version = "2.0.0" [dependencies.env_logger] version = "0.10.0" [dependencies.heck] version = "0.4.0" [dependencies.lexiclean] version = "0.0.1" [dependencies.libc] version = "0.2.0" [dependencies.log] version = "0.4.4" [dependencies.num_cpus] version = "1.15.0" [dependencies.regex] version = "1.5.4" [dependencies.semver] version = "1.0.20" [dependencies.serde] version = "1.0.130" features = [ "derive", "rc", ] [dependencies.serde_json] version = "1.0.68" [dependencies.sha2] version = "0.10" [dependencies.similar] version = "2.1.0" features = ["unicode"] [dependencies.snafu] version = "0.7.0" [dependencies.strum] version = "0.25.0" features = ["derive"] [dependencies.target] version = "2.0.0" [dependencies.tempfile] version = "3.0.0" [dependencies.typed-arena] version = "2.0.1" [dependencies.unicode-width] version = "0.1.0" [dependencies.uuid] version = "1.0.0" features = ["v4"] [dev-dependencies.cradle] version = "0.2.0" [dev-dependencies.executable-path] version = "1.0.0" [dev-dependencies.pretty_assertions] version = "1.0.0" [dev-dependencies.temptree] version = "0.2.0" [dev-dependencies.which] version = "5.0.0" [dev-dependencies.yaml-rust] version = "0.4.5" [features] default = [] help4help2man = [] just-1.21.0/Cargo.toml.orig000064400000000000000000000041061046102023000135620ustar 00000000000000[package] name = "just" version = "1.21.0" authors = ["Casey Rodarmor "] autotests = false categories = ["command-line-utilities", "development-tools"] description = "🤖 Just a command runner" edition = "2021" exclude = ["/book", "/icon.png", "/screenshot.png", "/www"] homepage = "https://github.com/casey/just" keywords = ["command-line", "task", "runner", "development", "utility"] license = "CC0-1.0" readme = "crates-io-readme.md" repository = "https://github.com/casey/just" rust-version = "1.63" [workspace] members = [".", "bin/ref-type", "bin/generate-book", "bin/update-contributors"] [dependencies] ansi_term = "0.12.0" atty = "0.2.0" camino = "1.0.4" clap = { version = "2.33.0", features = ["wrap_help"] } ctrlc = { version = "3.1.1", features = ["termination"] } derivative = "2.0.0" dirs = "5.0.1" dotenvy = "0.15" edit-distance = "2.0.0" env_logger = "0.10.0" heck = "0.4.0" lexiclean = "0.0.1" libc = "0.2.0" log = "0.4.4" num_cpus = "1.15.0" regex = "1.5.4" semver = "1.0.20" serde = { version = "1.0.130", features = ["derive", "rc"] } serde_json = "1.0.68" sha2 = "0.10" similar = { version = "2.1.0", features = ["unicode"] } snafu = "0.7.0" strum = { version = "0.25.0", features = ["derive"] } target = "2.0.0" tempfile = "3.0.0" typed-arena = "2.0.1" unicode-width = "0.1.0" uuid = { version = "1.0.0", features = ["v4"] } [dev-dependencies] cradle = "0.2.0" executable-path = "1.0.0" pretty_assertions = "1.0.0" temptree = "0.2.0" which = "5.0.0" yaml-rust = "0.4.5" [lib] doctest = false [[bin]] path = "src/main.rs" name = "just" test = false [features] # No features are active by default. default = [] # The `help4help2man` feature modifies the message produced by `--help` # so that `help2man` produces a reasonable man page. help4help2man = [] # The public documentation is minimal and doesn't change between # platforms, so we only build them for linux on docs.rs to save # their build machines some cycles. [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] [profile.release] lto = true [[test]] name = "integration" path = "tests/lib.rs" just-1.21.0/GRAMMAR.md000064400000000000000000000064401046102023000123060ustar 00000000000000justfile grammar ================ Justfiles are processed by a mildly context-sensitive tokenizer and a recursive descent parser. The grammar is LL(k), for an unknown but hopefully reasonable value of k. tokens ------ ``` BACKTICK = `[^`]*` INDENTED_BACKTICK = ```[^(```)]*``` COMMENT = #([^!].*)?$ DEDENT = emitted when indentation decreases EOF = emitted at the end of the file INDENT = emitted when indentation increases LINE = emitted before a recipe line NAME = [a-zA-Z_][a-zA-Z0-9_-]* NEWLINE = \n|\r\n RAW_STRING = '[^']*' INDENTED_RAW_STRING = '''[^(''')]*''' STRING = "[^"]*" # also processes \n \r \t \" \\ escapes INDENTED_STRING = """[^("""]*""" # also processes \n \r \t \" \\ escapes LINE_PREFIX = @-|-@|@|- TEXT = recipe text, only matches in a recipe body ``` grammar syntax -------------- ``` | alternation () grouping _? option (0 or 1 times) _* repetition (0 or more times) _+ repetition (1 or more times) ``` grammar ------- ``` justfile : item* EOF item : alias | assignment | eol | export | import | module | recipe | setting eol : NEWLINE | COMMENT NEWLINE alias : 'alias' NAME ':=' NAME assignment : NAME ':=' expression eol export : 'export' assignment setting : 'set' 'allow-duplicate-recipes' boolean? | 'set' 'dotenv-filename' ':=' string | 'set' 'dotenv-load' boolean? | 'set' 'dotenv-path' ':=' string | 'set' 'export' boolean? | 'set' 'fallback' boolean? | 'set' 'ignore-comments' boolean? | 'set' 'positional-arguments' boolean? | 'set' 'shell' ':=' '[' string (',' string)* ','? ']' | 'set' 'tempdir ':=' string | 'set' 'windows-powershell' boolean? | 'set' 'windows-shell' ':=' '[' string (',' string)* ','? ']' import : 'import' '?'? string? module : 'mod' '?'? NAME string? boolean : ':=' ('true' | 'false') expression : 'if' condition '{' expression '}' 'else' '{' expression '}' | value '/' expression | value '+' expression | value condition : expression '==' expression | expression '!=' expression value : NAME '(' sequence? ')' | BACKTICK | INDENTED_BACKTICK | NAME | string | '(' expression ')' string : STRING | INDENTED_STRING | RAW_STRING | INDENTED_RAW_STRING sequence : expression ',' sequence | expression ','? recipe : attribute? '@'? NAME parameter* variadic? ':' dependency* body? attribute : '[' NAME ']' eol parameter : '$'? NAME | '$'? NAME '=' value variadic : '*' parameter | '+' parameter dependency : NAME | '(' NAME expression* ')' body : INDENT line+ DEDENT line : LINE LINE_PREFIX? (TEXT | interpolation)+ NEWLINE | NEWLINE interpolation : '{{' expression '}}' ``` just-1.21.0/LICENSE000064400000000000000000000156101046102023000117020ustar 00000000000000Creative Commons Legal Code CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. just-1.21.0/README.md000064400000000000000000002454011046102023000121570ustar 00000000000000↖️ Table of Contents

just

crates.io version build status downloads chat on discord say thanks

`just` is a handy way to save and run project-specific commands. This readme is also available as a [book](https://just.systems/man/en/). (中文文档在 [这里](https://github.com/casey/just/blob/master/README.中文.md), 快看过来!) Commands, called recipes, are stored in a file called `justfile` with syntax inspired by `make`: ![screenshot](https://raw.githubusercontent.com/casey/just/master/screenshot.png) You can then run them with `just RECIPE`: ```sh $ just test-all cc *.c -o main ./test --all Yay, all your tests passed! ``` `just` has a ton of useful features, and many improvements over `make`: - `just` is a command runner, not a build system, so it avoids much of [`make`'s complexity and idiosyncrasies](#what-are-the-idiosyncrasies-of-make-that-just-avoids). No need for `.PHONY` recipes! - Linux, MacOS, and Windows are supported with no additional dependencies. (Although if your system doesn't have an `sh`, you'll need to [choose a different shell](#shell).) - Errors are specific and informative, and syntax errors are reported along with their source context. - Recipes can accept [command line arguments](#recipe-parameters). - Wherever possible, errors are resolved statically. Unknown recipes and circular dependencies are reported before anything runs. - `just` [loads `.env` files](#dotenv-settings), making it easy to populate environment variables. - Recipes can be [listed from the command line](#listing-available-recipes). - Command line completion scripts are [available for most popular shells](#shell-completion-scripts). - Recipes can be written in [arbitrary languages](#writing-recipes-in-other-languages), like Python or NodeJS. - `just` can be invoked from any subdirectory, not just the directory that contains the `justfile`. - And [much more](https://just.systems/man/en/)! If you need help with `just` please feel free to open an issue or ping me on [Discord](https://discord.gg/ezYScXR). Feature requests and bug reports are always welcome! Installation ------------ ### Prerequisites `just` should run on any system with a reasonable `sh`, including Linux, MacOS, and the BSDs. On Windows, `just` works with the `sh` provided by [Git for Windows](https://git-scm.com), [GitHub Desktop](https://desktop.github.com), or [Cygwin](http://www.cygwin.com). If you'd rather not install `sh`, you can use the `shell` setting to use the shell of your choice. Like PowerShell: ```just # use PowerShell instead of sh: set shell := ["powershell.exe", "-c"] hello: Write-Host "Hello, world!" ``` …or `cmd.exe`: ```just # use cmd.exe instead of sh: set shell := ["cmd.exe", "/c"] list: dir ``` You can also set the shell using command-line arguments. For example, to use PowerShell, launch `just` with `--shell powershell.exe --shell-arg -c`. (PowerShell is installed by default on Windows 7 SP1 and Windows Server 2008 R2 S1 and later, and `cmd.exe` is quite fiddly, so PowerShell is recommended for most Windows users.) ### Packages
Operating System Package Manager Package Command
Various Cargo just cargo install just
Microsoft Windows Scoop just scoop install just
Various Homebrew just brew install just
macOS MacPorts just port install just
Arch Linux pacman just pacman -S just
Various Nix just nix-env -iA nixpkgs.just
NixOS Nix just nix-env -iA nixos.just
Solus eopkg just eopkg install just
Void Linux XBPS just xbps-install -S just
FreeBSD pkg just pkg install just
Alpine Linux apk-tools just apk add just
Fedora Linux DNF just dnf install just
Gentoo Linux Portage guru/sys-devel/just eselect repository enable guru
emerge --sync guru
emerge sys-devel/just
Various Conda just conda install -c conda-forge just
Microsoft Windows Chocolatey just choco install just
Various Snap just snap install --edge --classic just
Various asdf just asdf plugin add just
asdf install just <version>
Debian and Ubuntu derivatives MPR just git clone 'https://mpr.makedeb.org/just'
cd just
makedeb -si
Debian and Ubuntu derivatives Prebuilt-MPR just You must have the Prebuilt-MPR set up on your system in order to run this command.
sudo apt install just
Microsoft Windows Windows Package Manager Casey/Just winget install --id Casey.Just --exact
![package version table](https://repology.org/badge/vertical-allrepos/just.svg) ### Pre-Built Binaries Pre-built binaries for Linux, MacOS, and Windows can be found on [the releases page](https://github.com/casey/just/releases). You can use the following command on Linux, MacOS, or Windows to download the latest release, just replace `DEST` with the directory where you'd like to put `just`: ```sh curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to DEST ``` For example, to install `just` to `~/bin`: ```sh # create ~/bin mkdir -p ~/bin # download and extract just to ~/bin/just curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/bin # add `~/bin` to the paths that your shell searches for executables # this line should be added to your shells initialization file, # e.g. `~/.bashrc` or `~/.zshrc` export PATH="$PATH:$HOME/bin" # just should now be executable just --help ``` Note that `install.sh` may fail on GitHub actions, or in other environments where many machines share IP addresses. `install.sh` calls GitHub APIs in order to determine the latest version of `just` to install, and those API calls are rate-limited on a per-IP basis. To make `install.sh` more reliable in such circumstances, pass a specific tag to install with `--tag`. ### GitHub Actions With [extractions/setup-just](https://github.com/extractions/setup-just): ```yaml - uses: extractions/setup-just@v1 with: just-version: 0.8 # optional semver specification, otherwise latest ``` With [taiki-e/install-action](https://github.com/taiki-e/install-action): ```yaml - uses: taiki-e/install-action@just ``` ### Release RSS Feed An [RSS feed](https://en.wikipedia.org/wiki/RSS) of `just` releases is available [here](https://github.com/casey/just/releases.atom). ### Node.js Installation [just-install](https://npmjs.com/package/just-install) can be used to automate installation of `just` in Node.js applications. `just` is a great, more robust alternative to npm scripts. If you want to include `just` in the dependencies of a Node.js application, `just-install` will install a local, platform-specific binary as part of the `npm install` command. This removes the need for every developer to install `just` independently using one of the processes mentioned above. After installation, the `just` command will work in npm scripts or with npx. It's great for teams who want to make the set up process for their project as easy as possible. For more information, see the [just-install README file](https://github.com/brombal/just-install#readme). Backwards Compatibility ----------------------- With the release of version 1.0, `just` features a strong commitment to backwards compatibility and stability. Future releases will not introduce backwards incompatible changes that make existing `justfile`s stop working, or break working invocations of the command-line interface. This does not, however, preclude fixing outright bugs, even if doing so might break `justfiles` that rely on their behavior. There will never be a `just` 2.0. Any desirable backwards-incompatible changes will be opt-in on a per-`justfile` basis, so users may migrate at their leisure. Features that aren't yet ready for stabilization are gated behind the `--unstable` flag. Features enabled by `--unstable` may change in backwards incompatible ways at any time. Unstable features can also be enabled by setting the environment variable `JUST_UNSTABLE` to any value other than `false`, `0`, or the empty string. Editor Support -------------- `justfile` syntax is close enough to `make` that you may want to tell your editor to use `make` syntax highlighting for `just`. ### Vim and Neovim #### `vim-just` The [vim-just](https://github.com/NoahTheDuke/vim-just) plugin provides syntax highlighting for `justfile`s. Install it with your favorite package manager, like [Plug](https://github.com/junegunn/vim-plug): ```vim call plug#begin() Plug 'NoahTheDuke/vim-just' call plug#end() ``` Or with Vim's built-in package support: ```sh mkdir -p ~/.vim/pack/vendor/start cd ~/.vim/pack/vendor/start git clone https://github.com/NoahTheDuke/vim-just.git ``` #### `tree-sitter-just` [tree-sitter-just](https://github.com/IndianBoy42/tree-sitter-just) is an [Nvim Treesitter](https://github.com/nvim-treesitter/nvim-treesitter) plugin for Neovim. #### Makefile Syntax Highlighting Vim's built-in makefile syntax highlighting isn't perfect for `justfile`s, but it's better than nothing. You can put the following in `~/.vim/filetype.vim`: ```vimscript if exists("did_load_filetypes") finish endif augroup filetypedetect au BufNewFile,BufRead justfile setf make augroup END ``` Or add the following to an individual `justfile` to enable `make` mode on a per-file basis: ```text # vim: set ft=make : ``` ### Emacs [just-mode](https://github.com/leon-barrett/just-mode.el) provides syntax highlighting and automatic indentation of `justfile`s. It is available on [MELPA](https://melpa.org/) as [just-mode](https://melpa.org/#/just-mode). [justl](https://github.com/psibi/justl.el) provides commands for executing and listing recipes. You can add the following to an individual `justfile` to enable `make` mode on a per-file basis: ```text # Local Variables: # mode: makefile # End: ``` ### Visual Studio Code An extension for VS Code by [skellock](https://github.com/skellock) is [available here](https://marketplace.visualstudio.com/items?itemName=skellock.just) ([repository](https://github.com/skellock/vscode-just)), but is no longer actively developed. You can install it from the command line by running: ```sh code --install-extension skellock.just ``` An more recently active fork by [sclu1034](https://github.com/sclu1034) is available [here](https://github.com/sclu1034/vscode-just). ### JetBrains IDEs A plugin for JetBrains IDEs by [linux_china](https://github.com/linux-china) is [available here](https://plugins.jetbrains.com/plugin/18658-just). ### Kakoune Kakoune supports `justfile` syntax highlighting out of the box, thanks to TeddyDD. ### Helix [Helix](https://helix-editor.com/) supports `justfile` syntax highlighting out-of-the-box since version 23.05. ### Sublime Text The [Just package](https://github.com/nk9/just_sublime) by [nk9](https://github.com/nk9) with `just` syntax and some other tools is available on [PackageControl](https://packagecontrol.io/packages/Just). ### Micro [Micro](https://micro-editor.github.io/) supports Justfile syntax highlighting out of the box, thanks to [tomodachi94](https://github.com/tomodachi94). ### Other Editors Feel free to send me the commands necessary to get syntax highlighting working in your editor of choice so that I may include them here. Quick Start ----------- See [the installation section](#installation) for how to install `just` on your computer. Try running `just --version` to make sure that it's installed correctly. For an overview of the syntax, check out [this cheatsheet](https://cheatography.com/linux-china/cheat-sheets/justfile/). Once `just` is installed and working, create a file named `justfile` in the root of your project with the following contents: ```just recipe-name: echo 'This is a recipe!' # this is a comment another-recipe: @echo 'This is another recipe.' ``` When you invoke `just` it looks for file `justfile` in the current directory and upwards, so you can invoke it from any subdirectory of your project. The search for a `justfile` is case insensitive, so any case, like `Justfile`, `JUSTFILE`, or `JuStFiLe`, will work. `just` will also look for files with the name `.justfile`, in case you'd like to hide a `justfile`. Running `just` with no arguments runs the first recipe in the `justfile`: ```sh $ just echo 'This is a recipe!' This is a recipe! ``` One or more arguments specify the recipe(s) to run: ```sh $ just another-recipe This is another recipe. ``` `just` prints each command to standard error before running it, which is why `echo 'This is a recipe!'` was printed. This is suppressed for lines starting with `@`, which is why `echo 'This is another recipe.'` was not printed. Recipes stop running if a command fails. Here `cargo publish` will only run if `cargo test` succeeds: ```just publish: cargo test # tests passed, time to publish! cargo publish ``` Recipes can depend on other recipes. Here the `test` recipe depends on the `build` recipe, so `build` will run before `test`: ```just build: cc main.c foo.c bar.c -o main test: build ./test sloc: @echo "`wc -l *.c` lines of code" ``` ```sh $ just test cc main.c foo.c bar.c -o main ./test testing… all tests passed! ``` Recipes without dependencies will run in the order they're given on the command line: ```sh $ just build sloc cc main.c foo.c bar.c -o main 1337 lines of code ``` Dependencies will always run first, even if they are passed after a recipe that depends on them: ```sh $ just test build cc main.c foo.c bar.c -o main ./test testing… all tests passed! ``` Examples -------- A variety of example `justfile`s can be found in the [examples directory](https://github.com/casey/just/tree/master/examples). Features -------- ### The Default Recipe When `just` is invoked without a recipe, it runs the first recipe in the `justfile`. This recipe might be the most frequently run command in the project, like running the tests: ```just test: cargo test ``` You can also use dependencies to run multiple recipes by default: ```just default: lint build test build: echo Building… test: echo Testing… lint: echo Linting… ``` If no recipe makes sense as the default recipe, you can add a recipe to the beginning of your `justfile` that lists the available recipes: ```just default: just --list ``` ### Listing Available Recipes Recipes can be listed in alphabetical order with `just --list`: ```sh $ just --list Available recipes: build test deploy lint ``` `just --summary` is more concise: ```sh $ just --summary build test deploy lint ``` Pass `--unsorted` to print recipes in the order they appear in the `justfile`: ```just test: echo 'Testing!' build: echo 'Building!' ``` ```sh $ just --list --unsorted Available recipes: test build ``` ```sh $ just --summary --unsorted test build ``` If you'd like `just` to default to listing the recipes in the `justfile`, you can use this as your default recipe: ```just default: @just --list ``` Note that you may need to add `--justfile {{justfile()}}` to the line above above. Without it, if you executed `just -f /some/distant/justfile -d .` or `just -f ./non-standard-justfile`, the plain `just --list` inside the recipe would not necessarily use the file you provided. It would try to find a justfile in your current path, maybe even resulting in a `No justfile found` error. The heading text can be customized with `--list-heading`: ```sh $ just --list --list-heading $'Cool stuff…\n' Cool stuff… test build ``` And the indentation can be customized with `--list-prefix`: ```sh $ just --list --list-prefix ···· Available recipes: ····test ····build ``` The argument to `--list-heading` replaces both the heading and the newline following it, so it should contain a newline if non-empty. It works this way so you can suppress the heading line entirely by passing the empty string: ```sh $ just --list --list-heading '' test build ``` ### Aliases Aliases allow recipes to be invoked on the command line with alternative names: ```just alias b := build build: echo 'Building!' ``` ```sh $ just b echo 'Building!' Building! ``` ### Settings Settings control interpretation and execution. Each setting may be specified at most once, anywhere in the `justfile`. For example: ```just set shell := ["zsh", "-cu"] foo: # this line will be run as `zsh -cu 'ls **/*.txt'` ls **/*.txt ``` #### Table of Settings | Name | Value | Default | Description | | -----| ------| ------- |-------------| | `allow-duplicate-recipes` | boolean | `false` | Allow recipes appearing later in a `justfile` to override earlier recipes with the same name. | | `dotenv-filename` | string | - | Load a `.env` file with a custom name, if present. | | `dotenv-load` | boolean | `false` | Load a `.env` file, if present. | | `dotenv-path` | string | - | Load a `.env` file from a custom path, if present. Overrides `dotenv-filename`. | | `export` | boolean | `false` | Export all variables as environment variables. | | `fallback` | boolean | `false` | Search `justfile` in parent directory if the first recipe on the command line is not found. | | `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. | | `positional-arguments` | boolean | `false` | Pass positional arguments. | | `shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. | | `tempdir` | string | - | Create temporary directories in `tempdir` instead of the system default temporary directory. | | `windows-powershell` | boolean | `false` | Use PowerShell on Windows as default shell. (Deprecated. Use `windows-shell` instead. | | `windows-shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. | Boolean settings can be written as: ```justfile set NAME ``` Which is equivalent to: ```justfile set NAME := true ``` #### Allow Duplicate Recipes If `allow-duplicate-recipes` is set to `true`, defining multiple recipes with the same name is not an error and the last definition is used. Defaults to `false`. ```just set allow-duplicate-recipes @foo: echo foo @foo: echo bar ``` ```sh $ just foo bar ``` #### Dotenv Settings If `dotenv-load`, `dotenv-filename` or `dotenv-path` is set, `just` will load environment variables from a file. If `dotenv-path` is set, `just` will look for a file at the given path. Otherwise, `just` looks for a file named `.env` by default, unless `dotenv-filename` set, in which case the value of `dotenv-filename` is used. This file can be located in the same directory as your `justfile` or in a parent directory. The loaded variables are environment variables, not `just` variables, and so must be accessed using `$VARIABLE_NAME` in recipes and backticks. For example, if your `.env` file contains: ```sh # a comment, will be ignored DATABASE_ADDRESS=localhost:6379 SERVER_PORT=1337 ``` And your `justfile` contains: ```just set dotenv-load serve: @echo "Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…" ./server --database $DATABASE_ADDRESS --port $SERVER_PORT ``` `just serve` will output: ```sh $ just serve Starting server with database localhost:6379 on port 1337… ./server --database $DATABASE_ADDRESS --port $SERVER_PORT ``` #### Export The `export` setting causes all `just` variables to be exported as environment variables. Defaults to `false`. ```just set export a := "hello" @foo b: echo $a echo $b ``` ```sh $ just foo goodbye hello goodbye ``` #### Positional Arguments If `positional-arguments` is `true`, recipe arguments will be passed as positional arguments to commands. For linewise recipes, argument `$0` will be the name of the recipe. For example, running this recipe: ```just set positional-arguments @foo bar: echo $0 echo $1 ``` Will produce the following output: ```sh $ just foo hello foo hello ``` When using an `sh`-compatible shell, such as `bash` or `zsh`, `$@` expands to the positional arguments given to the recipe, starting from one. When used within double quotes as `"$@"`, arguments including whitespace will be passed on as if they were double-quoted. That is, `"$@"` is equivalent to `"$1" "$2"`… When there are no positional parameters, `"$@"` and `$@` expand to nothing (i.e., they are removed). This example recipe will print arguments one by one on separate lines: ```just set positional-arguments @test *args='': bash -c 'while (( "$#" )); do echo - $1; shift; done' -- "$@" ``` Running it with _two_ arguments: ```sh $ just test foo "bar baz" - foo - bar baz ``` #### Shell The `shell` setting controls the command used to invoke recipe lines and backticks. Shebang recipes are unaffected. ```just # use python3 to execute recipe lines and backticks set shell := ["python3", "-c"] # use print to capture result of evaluation foos := `print("foo" * 4)` foo: print("Snake snake snake snake.") print("{{foos}}") ``` `just` passes the command to be executed as an argument. Many shells will need an additional flag, often `-c`, to make them evaluate the first argument. ##### Windows Shell `just` uses `sh` on Windows by default. To use a different shell on Windows, use `windows-shell`: ```just set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] hello: Write-Host "Hello, world!" ``` See [powershell.just](https://github.com/casey/just/blob/master/examples/powershell.just) for a justfile that uses PowerShell on all platforms. ##### Windows PowerShell *`set windows-powershell` uses the legacy `powershell.exe` binary, and is no longer recommended. See the `windows-shell` setting above for a more flexible way to control which shell is used on Windows.* `just` uses `sh` on Windows by default. To use `powershell.exe` instead, set `windows-powershell` to true. ```just set windows-powershell := true hello: Write-Host "Hello, world!" ``` ##### Python 3 ```just set shell := ["python3", "-c"] ``` ##### Bash ```just set shell := ["bash", "-uc"] ``` ##### Z Shell ```just set shell := ["zsh", "-uc"] ``` ##### Fish ```just set shell := ["fish", "-c"] ``` ##### Nushell ```just set shell := ["nu", "-c"] ``` If you want to change the default table mode to `light`: ```just set shell := ['nu', '-m', 'light', '-c'] ``` *[Nushell](https://github.com/nushell/nushell) was written in Rust, and **has cross-platform support for Windows / macOS and Linux**.* ### Documentation Comments Comments immediately preceding a recipe will appear in `just --list`: ```just # build stuff build: ./bin/build # test stuff test: ./bin/test ``` ```sh $ just --list Available recipes: build # build stuff test # test stuff ``` ### Variables and Substitution Variables, strings, concatenation, path joining, and substitution using `{{…}}` are supported: ```just tmpdir := `mktemp -d` version := "0.2.7" tardir := tmpdir / "awesomesauce-" + version tarball := tardir + ".tar.gz" publish: rm -f {{tarball}} mkdir {{tardir}} cp README.md *.c {{tardir}} tar zcvf {{tarball}} {{tardir}} scp {{tarball}} me@server.com:release/ rm -rf {{tarball}} {{tardir}} ``` #### Joining Paths The `/` operator can be used to join two strings with a slash: ```just foo := "a" / "b" ``` ``` $ just --evaluate foo a/b ``` Note that a `/` is added even if one is already present: ```just foo := "a/" bar := foo / "b" ``` ``` $ just --evaluate bar a//b ``` Absolute paths can also be constructed1.5.0: ```just foo := / "b" ``` ``` $ just --evaluate foo /b ``` The `/` operator uses the `/` character, even on Windows. Thus, using the `/` operator should be avoided with paths that use universal naming convention (UNC), i.e., those that start with `\?`, since forward slashes are not supported with UNC paths. #### Escaping `{{` To write a recipe containing `{{`, use `{{{{`: ```just braces: echo 'I {{{{LOVE}} curly braces!' ``` (An unmatched `}}` is ignored, so it doesn't need to be escaped.) Another option is to put all the text you'd like to escape inside of an interpolation: ```just braces: echo '{{'I {{LOVE}} curly braces!'}}' ``` Yet another option is to use `{{ "{{" }}`: ```just braces: echo 'I {{ "{{" }}LOVE}} curly braces!' ``` ### Strings Double-quoted strings support escape sequences: ```just string-with-tab := "\t" string-with-newline := "\n" string-with-carriage-return := "\r" string-with-double-quote := "\"" string-with-slash := "\\" string-with-no-newline := "\ " ``` ```sh $ just --evaluate "tring-with-carriage-return := " string-with-double-quote := """ string-with-newline := " " string-with-no-newline := "" string-with-slash := "\" string-with-tab := " " ``` Strings may contain line breaks: ```just single := ' hello ' double := " goodbye " ``` Single-quoted strings do not recognize escape sequences: ```just escapes := '\t\n\r\"\\' ``` ```sh $ just --evaluate escapes := "\t\n\r\"\\" ``` Indented versions of both single- and double-quoted strings, delimited by triple single- or triple double-quotes, are supported. Indented string lines are stripped of a leading line break, and leading whitespace common to all non-blank lines: ```just # this string will evaluate to `foo\nbar\n` x := ''' foo bar ''' # this string will evaluate to `abc\n wuv\nxyz\n` y := """ abc wuv xyz """ ``` Similar to unindented strings, indented double-quoted strings process escape sequences, and indented single-quoted strings ignore escape sequences. Escape sequence processing takes place after unindentation. The unindentation algorithm does not take escape-sequence produced whitespace or newlines into account. ### Ignoring Errors Normally, if a command returns a non-zero exit status, execution will stop. To continue execution after a command, even if it fails, prefix the command with `-`: ```just foo: -cat foo echo 'Done!' ``` ```sh $ just foo cat foo cat: foo: No such file or directory echo 'Done!' Done! ``` ### Functions `just` provides a few built-in functions that might be useful when writing recipes. #### System Information - `arch()` — Instruction set architecture. Possible values are: `"aarch64"`, `"arm"`, `"asmjs"`, `"hexagon"`, `"mips"`, `"msp430"`, `"powerpc"`, `"powerpc64"`, `"s390x"`, `"sparc"`, `"wasm32"`, `"x86"`, `"x86_64"`, and `"xcore"`. - `num_cpus()`1.15.0 - Number of logical CPUs. - `os()` — Operating system. Possible values are: `"android"`, `"bitrig"`, `"dragonfly"`, `"emscripten"`, `"freebsd"`, `"haiku"`, `"ios"`, `"linux"`, `"macos"`, `"netbsd"`, `"openbsd"`, `"solaris"`, and `"windows"`. - `os_family()` — Operating system family; possible values are: `"unix"` and `"windows"`. For example: ```just system-info: @echo "This is an {{arch()}} machine". ``` ```sh $ just system-info This is an x86_64 machine ``` The `os_family()` function can be used to create cross-platform `justfile`s that work on various operating systems. For an example, see [cross-platform.just](https://github.com/casey/just/blob/master/examples/cross-platform.just) file. #### Environment Variables - `env_var(key)` — Retrieves the environment variable with name `key`, aborting if it is not present. ```just home_dir := env_var('HOME') test: echo "{{home_dir}}" ``` ```sh $ just /home/user1 ``` - `env_var_or_default(key, default)` — Retrieves the environment variable with name `key`, returning `default` if it is not present. - `env(key)`1.15.0 — Alias for `env_var(key)`. - `env(key, default)`1.15.0 — Alias for `env_var_or_default(key, default)`. #### Invocation Directory - `invocation_directory()` - Retrieves the absolute path to the current directory when `just` was invoked, before `just` changed it (chdir'd) prior to executing commands. On Windows, `invocation_directory()` uses `cygpath` to convert the invocation directory to a Cygwin-compatible `/`-separated path. Use `invocation_directory_native()` to return the verbatim invocation directory on all platforms. For example, to call `rustfmt` on files just under the "current directory" (from the user/invoker's perspective), use the following rule: ```just rustfmt: find {{invocation_directory()}} -name \*.rs -exec rustfmt {} \; ``` Alternatively, if your command needs to be run from the current directory, you could use (e.g.): ```just build: cd {{invocation_directory()}}; ./some_script_that_needs_to_be_run_from_here ``` - `invocation_directory_native()` - Retrieves the absolute path to the current directory when `just` was invoked, before `just` changed it (chdir'd) prior to executing commands. #### Justfile and Justfile Directory - `justfile()` - Retrieves the path of the current `justfile`. - `justfile_directory()` - Retrieves the path of the parent directory of the current `justfile`. For example, to run a command relative to the location of the current `justfile`: ```just script: ./{{justfile_directory()}}/scripts/some_script ``` #### Just Executable - `just_executable()` - Absolute path to the `just` executable. For example: ```just executable: @echo The executable is at: {{just_executable()}} ``` ```sh $ just The executable is at: /bin/just ``` #### String Manipulation - `quote(s)` - Replace all single quotes with `'\''` and prepend and append single quotes to `s`. This is sufficient to escape special characters for many shells, including most Bourne shell descendants. - `replace(s, from, to)` - Replace all occurrences of `from` in `s` to `to`. - `replace_regex(s, regex, replacement)` - Replace all occurrences of `regex` in `s` to `replacement`. Regular expressions are provided by the [Rust `regex` crate](https://docs.rs/regex/latest/regex/). See the [syntax documentation](https://docs.rs/regex/latest/regex/#syntax) for usage examples. Capture groups are supported. The `replacement` string uses [Replacement string syntax](https://docs.rs/regex/latest/regex/struct.Regex.html#replacement-string-syntax). - `trim(s)` - Remove leading and trailing whitespace from `s`. - `trim_end(s)` - Remove trailing whitespace from `s`. - `trim_end_match(s, pat)` - Remove suffix of `s` matching `pat`. - `trim_end_matches(s, pat)` - Repeatedly remove suffixes of `s` matching `pat`. - `trim_start(s)` - Remove leading whitespace from `s`. - `trim_start_match(s, pat)` - Remove prefix of `s` matching `pat`. - `trim_start_matches(s, pat)` - Repeatedly remove prefixes of `s` matching `pat`. #### Case Conversion - `capitalize(s)`1.7.0 - Convert first character of `s` to uppercase and the rest to lowercase. - `kebabcase(s)`1.7.0 - Convert `s` to `kebab-case`. - `lowercamelcase(s)`1.7.0 - Convert `s` to `lowerCamelCase`. - `lowercase(s)` - Convert `s` to lowercase. - `shoutykebabcase(s)`1.7.0 - Convert `s` to `SHOUTY-KEBAB-CASE`. - `shoutysnakecase(s)`1.7.0 - Convert `s` to `SHOUTY_SNAKE_CASE`. - `snakecase(s)`1.7.0 - Convert `s` to `snake_case`. - `titlecase(s)`1.7.0 - Convert `s` to `Title Case`. - `uppercamelcase(s)`1.7.0 - Convert `s` to `UpperCamelCase`. - `uppercase(s)` - Convert `s` to uppercase. #### Path Manipulation ##### Fallible - `absolute_path(path)` - Absolute path to relative `path` in the working directory. `absolute_path("./bar.txt")` in directory `/foo` is `/foo/bar.txt`. - `extension(path)` - Extension of `path`. `extension("/foo/bar.txt")` is `txt`. - `file_name(path)` - File name of `path` with any leading directory components removed. `file_name("/foo/bar.txt")` is `bar.txt`. - `file_stem(path)` - File name of `path` without extension. `file_stem("/foo/bar.txt")` is `bar`. - `parent_directory(path)` - Parent directory of `path`. `parent_directory("/foo/bar.txt")` is `/foo`. - `without_extension(path)` - `path` without extension. `without_extension("/foo/bar.txt")` is `/foo/bar`. These functions can fail, for example if a path does not have an extension, which will halt execution. ##### Infallible - `clean(path)` - Simplify `path` by removing extra path separators, intermediate `.` components, and `..` where possible. `clean("foo//bar")` is `foo/bar`, `clean("foo/..")` is `.`, `clean("foo/./bar")` is `foo/bar`. - `join(a, b…)` - *This function uses `/` on Unix and `\` on Windows, which can be lead to unwanted behavior. The `/` operator, e.g., `a / b`, which always uses `/`, should be considered as a replacement unless `\`s are specifically desired on Windows.* Join path `a` with path `b`. `join("foo/bar", "baz")` is `foo/bar/baz`. Accepts two or more arguments. #### Filesystem Access - `path_exists(path)` - Returns `true` if the path points at an existing entity and `false` otherwise. Traverses symbolic links, and returns `false` if the path is inaccessible or points to a broken symlink. ##### Error Reporting - `error(message)` - Abort execution and report error `message` to user. #### UUID and Hash Generation - `sha256(string)` - Return the SHA-256 hash of `string` as a hexadecimal string. - `sha256_file(path)` - Return the SHA-256 hash of the file at `path` as a hexadecimal string. - `uuid()` - Return a randomly generated UUID. #### Semantic Versions - `semver_matches(version, requirement)`1.16.0 - Check whether a [semantic `version`](https://semver.org), e.g., `"0.1.0"` matches a `requirement`, e.g., `">=0.1.0"`, returning `"true"` if so and `"false"` otherwise. ### Recipe Attributes Recipes may be annotated with attributes that change their behavior. | Name | Description | | -----| ------------| | `[confirm]`1.17.0 | Require confirmation prior to executing recipe. | | `[linux]`1.8.0 | Enable recipe on Linux. | | `[macos]`1.8.0 | Enable recipe on MacOS. | | `[no-cd]`1.9.0 | Don't change directory before executing recipe. | | `[no-exit-message]`1.7.0 | Don't print an error message if recipe fails. | | `[private]`1.10.0 | See [Private Recipes](#private-recipes). | | `[unix]`1.8.0 | Enable recipe on Unixes. (Includes MacOS). | | `[windows]`1.8.0 | Enable recipe on Windows. | A recipe can have multiple attributes, either on multiple lines: ```just [no-cd] [private] foo: echo "foo" ``` Or separated by commas on a single line1.14.0: ```just [no-cd, private] foo: echo "foo" ``` #### Enabling and Disabling Recipes1.8.0 The `[linux]`, `[macos]`, `[unix]`, and `[windows]` attributes are configuration attributes. By default, recipes are always enabled. A recipe with one or more configuration attributes will only be enabled when one or more of those configurations is active. This can be used to write `justfile`s that behave differently depending on which operating system they run on. The `run` recipe in this `justfile` will compile and run `main.c`, using a different C compiler and using the correct output binary name for that compiler depending on the operating system: ```just [unix] run: cc main.c ./a.out [windows] run: cl main.c main.exe ``` #### Disabling Changing Directory1.9.0 `just` normally executes recipes with the current directory set to the directory that contains the `justfile`. This can be disabled using the `[no-cd]` attribute. This can be used to create recipes which use paths relative to the invocation directory, or which operate on the current directory. For example, this `commit` recipe: ```just [no-cd] commit file: git add {{file}} git commit ``` Can be used with paths that are relative to the current directory, because `[no-cd]` prevents `just` from changing the current directory when executing `commit`. #### Requiring Confirmation for Recipes1.17.0 `just` normally executes all recipes unless there is an error. The `[confirm]` attribute allows recipes require confirmation in the terminal prior to running. This can be overridden by passing `--yes` to `just`, which will automatically confirm any recipes marked by this attribute. Recipes dependent on a recipe that requires confirmation will not be run if the relied upon recipe is not confirmed, as well as recipes passed after any recipe that requires confirmation. ```just [confirm] delete all: rm -rf * ``` ### Command Evaluation Using Backticks Backticks can be used to store the result of commands: ```just localhost := `dumpinterfaces | cut -d: -f2 | sed 's/\/.*//' | sed 's/ //g'` serve: ./serve {{localhost}} 8080 ``` Indented backticks, delimited by three backticks, are de-indented in the same manner as indented strings: ````just # This backtick evaluates the command `echo foo\necho bar\n`, which produces the value `foo\nbar\n`. stuff := ``` echo foo echo bar ``` ```` See the [Strings](#strings) section for details on unindenting. Backticks may not start with `#!`. This syntax is reserved for a future upgrade. ### Conditional Expressions `if`/`else` expressions evaluate different branches depending on if two expressions evaluate to the same value: ```just foo := if "2" == "2" { "Good!" } else { "1984" } bar: @echo "{{foo}}" ``` ```sh $ just bar Good! ``` It is also possible to test for inequality: ```just foo := if "hello" != "goodbye" { "xyz" } else { "abc" } bar: @echo {{foo}} ``` ```sh $ just bar xyz ``` And match against regular expressions: ```just foo := if "hello" =~ 'hel+o' { "match" } else { "mismatch" } bar: @echo {{foo}} ``` ```sh $ just bar match ``` Regular expressions are provided by the [regex crate](https://github.com/rust-lang/regex), whose syntax is documented on [docs.rs](https://docs.rs/regex/1.5.4/regex/#syntax). Since regular expressions commonly use backslash escape sequences, consider using single-quoted string literals, which will pass slashes to the regex parser unmolested. Conditional expressions short-circuit, which means they only evaluate one of their branches. This can be used to make sure that backtick expressions don't run when they shouldn't. ```just foo := if env_var("RELEASE") == "true" { `get-something-from-release-database` } else { "dummy-value" } ``` Conditionals can be used inside of recipes: ```just bar foo: echo {{ if foo == "bar" { "hello" } else { "goodbye" } }} ``` Note the space after the final `}`! Without the space, the interpolation will be prematurely closed. Multiple conditionals can be chained: ```just foo := if "hello" == "goodbye" { "xyz" } else if "a" == "a" { "abc" } else { "123" } bar: @echo {{foo}} ``` ```sh $ just bar abc ``` ### Stopping execution with error Execution can be halted with the `error` function. For example: ```just foo := if "hello" == "goodbye" { "xyz" } else if "a" == "b" { "abc" } else { error("123") } ``` Which produce the following error when run: ``` error: Call to function `error` failed: 123 | 16 | error("123") ``` ### Setting Variables from the Command Line Variables can be overridden from the command line. ```just os := "linux" test: build ./test --test {{os}} build: ./build {{os}} ``` ```sh $ just ./build linux ./test --test linux ``` Any number of arguments of the form `NAME=VALUE` can be passed before recipes: ```sh $ just os=plan9 ./build plan9 ./test --test plan9 ``` Or you can use the `--set` flag: ```sh $ just --set os bsd ./build bsd ./test --test bsd ``` ### Getting and Setting Environment Variables #### Exporting `just` Variables Assignments prefixed with the `export` keyword will be exported to recipes as environment variables: ```just export RUST_BACKTRACE := "1" test: # will print a stack trace if it crashes cargo test ``` Parameters prefixed with a `$` will be exported as environment variables: ```just test $RUST_BACKTRACE="1": # will print a stack trace if it crashes cargo test ``` Exported variables and parameters are not exported to backticks in the same scope. ```just export WORLD := "world" # This backtick will fail with "WORLD: unbound variable" BAR := `echo hello $WORLD` ``` ```just # Running `just a foo` will fail with "A: unbound variable" a $A $B=`echo $A`: echo $A $B ``` When [export](#export) is set, all `just` variables are exported as environment variables. #### Getting Environment Variables from the environment Environment variables from the environment are passed automatically to the recipes. ```just print_home_folder: echo "HOME is: '${HOME}'" ``` ```sh $ just HOME is '/home/myuser' ``` #### Setting `just` Variables from Environment Variables Environment variables can be propagated to `just` variables using the functions `env_var()` and `env_var_or_default()`. See [environment-variables](#environment-variables). ### Recipe Parameters Recipes may have parameters. Here recipe `build` has a parameter called `target`: ```just build target: @echo 'Building {{target}}…' cd {{target}} && make ``` To pass arguments on the command line, put them after the recipe name: ```sh $ just build my-awesome-project Building my-awesome-project… cd my-awesome-project && make ``` To pass arguments to a dependency, put the dependency in parentheses along with the arguments: ```just default: (build "main") build target: @echo 'Building {{target}}…' cd {{target}} && make ``` Variables can also be passed as arguments to dependencies: ```just target := "main" _build version: @echo 'Building {{version}}…' cd {{version}} && make build: (_build target) ``` A command's arguments can be passed to dependency by putting the dependency in parentheses along with the arguments: ```just build target: @echo "Building {{target}}…" push target: (build target) @echo 'Pushing {{target}}…' ``` Parameters may have default values: ```just default := 'all' test target tests=default: @echo 'Testing {{target}}:{{tests}}…' ./test --tests {{tests}} {{target}} ``` Parameters with default values may be omitted: ```sh $ just test server Testing server:all… ./test --tests all server ``` Or supplied: ```sh $ just test server unit Testing server:unit… ./test --tests unit server ``` Default values may be arbitrary expressions, but concatenations or path joins must be parenthesized: ```just arch := "wasm" test triple=(arch + "-unknown-unknown") input=(arch / "input.dat"): ./test {{triple}} ``` The last parameter of a recipe may be variadic, indicated with either a `+` or a `*` before the argument name: ```just backup +FILES: scp {{FILES}} me@server.com: ``` Variadic parameters prefixed with `+` accept _one or more_ arguments and expand to a string containing those arguments separated by spaces: ```sh $ just backup FAQ.md GRAMMAR.md scp FAQ.md GRAMMAR.md me@server.com: FAQ.md 100% 1831 1.8KB/s 00:00 GRAMMAR.md 100% 1666 1.6KB/s 00:00 ``` Variadic parameters prefixed with `*` accept _zero or more_ arguments and expand to a string containing those arguments separated by spaces, or an empty string if no arguments are present: ```just commit MESSAGE *FLAGS: git commit {{FLAGS}} -m "{{MESSAGE}}" ``` Variadic parameters can be assigned default values. These are overridden by arguments passed on the command line: ```just test +FLAGS='-q': cargo test {{FLAGS}} ``` `{{…}}` substitutions may need to be quoted if they contain spaces. For example, if you have the following recipe: ```just search QUERY: lynx https://www.google.com/?q={{QUERY}} ``` And you type: ```sh $ just search "cat toupee" ``` `just` will run the command `lynx https://www.google.com/?q=cat toupee`, which will get parsed by `sh` as `lynx`, `https://www.google.com/?q=cat`, and `toupee`, and not the intended `lynx` and `https://www.google.com/?q=cat toupee`. You can fix this by adding quotes: ```just search QUERY: lynx 'https://www.google.com/?q={{QUERY}}' ``` Parameters prefixed with a `$` will be exported as environment variables: ```just foo $bar: echo $bar ``` ### Running Recipes at the End of a Recipe Normal dependencies of a recipes always run before a recipe starts. That is to say, the dependee always runs before the depender. These dependencies are called "prior dependencies". A recipe can also have subsequent dependencies, which run after the recipe and are introduced with an `&&`: ```just a: echo 'A!' b: a && c d echo 'B!' c: echo 'C!' d: echo 'D!' ``` …running _b_ prints: ```sh $ just b echo 'A!' A! echo 'B!' B! echo 'C!' C! echo 'D!' D! ``` ### Running Recipes in the Middle of a Recipe `just` doesn't support running recipes in the middle of another recipe, but you can call `just` recursively in the middle of a recipe. Given the following `justfile`: ```just a: echo 'A!' b: a echo 'B start!' just c echo 'B end!' c: echo 'C!' ``` …running _b_ prints: ```sh $ just b echo 'A!' A! echo 'B start!' B start! echo 'C!' C! echo 'B end!' B end! ``` This has limitations, since recipe `c` is run with an entirely new invocation of `just`: Assignments will be recalculated, dependencies might run twice, and command line arguments will not be propagated to the child `just` process. ### Writing Recipes in Other Languages Recipes that start with `#!` are called shebang recipes, and are executed by saving the recipe body to a file and running it. This lets you write recipes in different languages: ```just polyglot: python js perl sh ruby nu python: #!/usr/bin/env python3 print('Hello from python!') js: #!/usr/bin/env node console.log('Greetings from JavaScript!') perl: #!/usr/bin/env perl print "Larry Wall says Hi!\n"; sh: #!/usr/bin/env sh hello='Yo' echo "$hello from a shell script!" nu: #!/usr/bin/env nu let hello = 'Hola' echo $"($hello) from a nushell script!" ruby: #!/usr/bin/env ruby puts "Hello from ruby!" ``` ```sh $ just polyglot Hello from python! Greetings from JavaScript! Larry Wall says Hi! Yo from a shell script! Hola from a nushell script! Hello from ruby! ``` On Unix-like operating systems, including Linux and MacOS, shebang recipes are executed by saving the recipe body to a file in a temporary directory, marking the file as executable, and executing it. The OS then parses the shebang line into a command line and invokes it, including the path to the file. For example, if a recipe starts with `#!/usr/bin/env bash`, the final command that the OS runs will be something like `/usr/bin/env bash /tmp/PATH_TO_SAVED_RECIPE_BODY`. Keep in mind that different operating systems split shebang lines differently. Windows does not support shebang lines. On Windows, `just` splits the shebang line into a command and arguments, saves the recipe body to a file, and invokes the split command and arguments, adding the path to the saved recipe body as the final argument. For example, on Windows, if a recipe starts with `#! py`, the final command the OS runs will be something like `py C:\Temp\PATH_TO_SAVED_RECIPE_BODY`. ### Safer Bash Shebang Recipes If you're writing a `bash` shebang recipe, consider adding `set -euxo pipefail`: ```just foo: #!/usr/bin/env bash set -euxo pipefail hello='Yo' echo "$hello from Bash!" ``` It isn't strictly necessary, but `set -euxo pipefail` turns on a few useful features that make `bash` shebang recipes behave more like normal, linewise `just` recipe: - `set -e` makes `bash` exit if a command fails. - `set -u` makes `bash` exit if a variable is undefined. - `set -x` makes `bash` print each script line before it's run. - `set -o pipefail` makes `bash` exit if a command in a pipeline fails. This is `bash`-specific, so isn't turned on in normal linewise `just` recipes. Together, these avoid a lot of shell scripting gotchas. #### Shebang Recipe Execution on Windows On Windows, shebang interpreter paths containing a `/` are translated from Unix-style paths to Windows-style paths using `cygpath`, a utility that ships with [Cygwin](http://www.cygwin.com). For example, to execute this recipe on Windows: ```just echo: #!/bin/sh echo "Hello!" ``` The interpreter path `/bin/sh` will be translated to a Windows-style path using `cygpath` before being executed. If the interpreter path does not contain a `/` it will be executed without being translated. This is useful if `cygpath` is not available, or you wish to pass a Windows-style path to the interpreter. ### Setting Variables in a Recipe Recipe lines are interpreted by the shell, not `just`, so it's not possible to set `just` variables in the middle of a recipe: ```mf foo: x := "hello" # This doesn't work! echo {{x}} ``` It is possible to use shell variables, but there's another problem. Every recipe line is run by a new shell instance, so variables set in one line won't be set in the next: ```just foo: x=hello && echo $x # This works! y=bye echo $y # This doesn't, `y` is undefined here! ``` The best way to work around this is to use a shebang recipe. Shebang recipe bodies are extracted and run as scripts, so a single shell instance will run the whole thing: ```just foo: #!/usr/bin/env bash set -euxo pipefail x=hello echo $x ``` ### Sharing Environment Variables Between Recipes Each line of each recipe is executed by a fresh shell, so it is not possible to share environment variables between recipes. #### Using Python Virtual Environments Some tools, like [Python's venv](https://docs.python.org/3/library/venv.html), require loading environment variables in order to work, making them challenging to use with `just`. As a workaround, you can execute the virtual environment binaries directly: ```just venv: [ -d foo ] || python3 -m venv foo run: venv ./foo/bin/python3 main.py ``` ### Changing the Working Directory in a Recipe Each recipe line is executed by a new shell, so if you change the working directory on one line, it won't have an effect on later lines: ```just foo: pwd # This `pwd` will print the same directory… cd bar pwd # …as this `pwd`! ``` There are a couple ways around this. One is to call `cd` on the same line as the command you want to run: ```just foo: cd bar && pwd ``` The other is to use a shebang recipe. Shebang recipe bodies are extracted and run as scripts, so a single shell instance will run the whole thing, and thus a `pwd` on one line will affect later lines, just like a shell script: ```just foo: #!/usr/bin/env bash set -euxo pipefail cd bar pwd ``` ### Indentation Recipe lines can be indented with spaces or tabs, but not a mix of both. All of a recipe's lines must have the same type of indentation, but different recipes in the same `justfile` may use different indentation. Each recipe must be indented at least one level from the `recipe-name` but after that may be further indented. Here's a justfile with a recipe indented with spaces, represented as `·`, and tabs, represented as `→`. ```justfile set windows-shell := ["pwsh", "-NoLogo", "-NoProfileLoadTime", "-Command"] set ignore-comments list-space directory: ··#!pwsh ··foreach ($item in $(Get-ChildItem {{directory}} )) { ····echo $item.Name ··} ··echo "" # indentation nesting works even when newlines are escaped list-tab directory: → @foreach ($item in $(Get-ChildItem {{directory}} )) { \ → → echo $item.Name \ → } → @echo "" ``` ```pwsh PS > just list-space ~ Desktop Documents Downloads PS > just list-tab ~ Desktop Documents Downloads ``` ### Multi-Line Constructs Recipes without an initial shebang are evaluated and run line-by-line, which means that multi-line constructs probably won't do what you want. For example, with the following `justfile`: ```mf conditional: if true; then echo 'True!' fi ``` The extra leading whitespace before the second line of the `conditional` recipe will produce a parse error: ```sh $ just conditional error: Recipe line has extra leading whitespace | 3 | echo 'True!' | ^^^^^^^^^^^^^^^^ ``` To work around this, you can write conditionals on one line, escape newlines with slashes, or add a shebang to your recipe. Some examples of multi-line constructs are provided for reference. #### `if` statements ```just conditional: if true; then echo 'True!'; fi ``` ```just conditional: if true; then \ echo 'True!'; \ fi ``` ```just conditional: #!/usr/bin/env sh if true; then echo 'True!' fi ``` #### `for` loops ```just for: for file in `ls .`; do echo $file; done ``` ```just for: for file in `ls .`; do \ echo $file; \ done ``` ```just for: #!/usr/bin/env sh for file in `ls .`; do echo $file done ``` #### `while` loops ```just while: while `server-is-dead`; do ping -c 1 server; done ``` ```just while: while `server-is-dead`; do \ ping -c 1 server; \ done ``` ```just while: #!/usr/bin/env sh while `server-is-dead`; do ping -c 1 server done ``` #### Outside Recipe Bodies Parenthesized expressions can span multiple lines: ```just abc := ('a' + 'b' + 'c') abc2 := ( 'a' + 'b' + 'c' ) foo param=('foo' + 'bar' ): echo {{param}} bar: (foo 'Foo' ) echo 'Bar!' ``` Lines ending with a backslash continue on to the next line as if the lines were joined by whitespace1.15.0: ```just a := 'foo' + \ 'bar' foo param1 \ param2='foo' \ *varparam='': dep1 \ (dep2 'foo') echo {{param1}} {{param2}} {{varparam}} dep1: \ # this comment is not part of the recipe body echo 'dep1' dep2 \ param: echo 'Dependency with parameter {{param}}' ``` Backslash line continuations can also be used in interpolations. The line following the backslash must start with the same indentation as the recipe body, although additional indentation is accepted. ```just recipe: echo '{{ \ "This interpolation " + \ "has a lot of text." \ }}' echo 'back to recipe body' ``` ### Command Line Options `just` supports a number of useful command line options for listing, dumping, and debugging recipes and variables: ```sh $ just --list Available recipes: js perl polyglot python ruby $ just --show perl perl: #!/usr/bin/env perl print "Larry Wall says Hi!\n"; $ just --show polyglot polyglot: python js perl sh ruby ``` Run `just --help` to see all the options. ### Private Recipes Recipes and aliases whose name starts with a `_` are omitted from `just --list`: ```just test: _test-helper ./bin/test _test-helper: ./bin/super-secret-test-helper-stuff ``` ```sh $ just --list Available recipes: test ``` And from `just --summary`: ```sh $ just --summary test ``` The `[private]` attribute1.10.0 may also be used to hide recipes or aliases without needing to change the name: ```just [private] foo: [private] alias b := bar bar: ``` ```sh $ just --list Available recipes: bar ``` This is useful for helper recipes which are only meant to be used as dependencies of other recipes. ### Quiet Recipes A recipe name may be prefixed with `@` to invert the meaning of `@` before each line: ```just @quiet: echo hello echo goodbye @# all done! ``` Now only the lines starting with `@` will be echoed: ```sh $ j quiet hello goodbye # all done! ``` Shebang recipes are quiet by default: ```just foo: #!/usr/bin/env bash echo 'Foo!' ``` ```sh $ just foo Foo! ``` Adding `@` to a shebang recipe name makes `just` print the recipe before executing it: ```just @bar: #!/usr/bin/env bash echo 'Bar!' ``` ```sh $ just bar #!/usr/bin/env bash echo 'Bar!' Bar! ``` `just` normally prints error messages when a recipe line fails. These error messages can be suppressed using the `[no-exit-message]`1.7.0 attribute. You may find this especially useful with a recipe that wraps a tool: ```just git *args: @git {{args}} ``` ```sh $ just git status fatal: not a git repository (or any of the parent directories): .git error: Recipe `git` failed on line 2 with exit code 128 ``` Add the attribute to suppress the exit error message when the tool exits with a non-zero code: ```just [no-exit-message] git *args: @git {{args}} ``` ```sh $ just git status fatal: not a git repository (or any of the parent directories): .git ``` ### Selecting Recipes to Run With an Interactive Chooser The `--choose` subcommand makes `just` invoke a chooser to select which recipes to run. Choosers should read lines containing recipe names from standard input and print one or more of those names separated by spaces to standard output. Because there is currently no way to run a recipe that requires arguments with `--choose`, such recipes will not be given to the chooser. Private recipes and aliases are also skipped. The chooser can be overridden with the `--chooser` flag. If `--chooser` is not given, then `just` first checks if `$JUST_CHOOSER` is set. If it isn't, then the chooser defaults to `fzf`, a popular fuzzy finder. Arguments can be included in the chooser, i.e. `fzf --exact`. The chooser is invoked in the same way as recipe lines. For example, if the chooser is `fzf`, it will be invoked with `sh -cu 'fzf'`, and if the shell, or the shell arguments are overridden, the chooser invocation will respect those overrides. If you'd like `just` to default to selecting recipes with a chooser, you can use this as your default recipe: ```just default: @just --choose ``` ### Invoking `justfile`s in Other Directories If the first argument passed to `just` contains a `/`, then the following occurs: 1. The argument is split at the last `/`. 2. The part before the last `/` is treated as a directory. `just` will start its search for the `justfile` there, instead of in the current directory. 3. The part after the last slash is treated as a normal argument, or ignored if it is empty. This may seem a little strange, but it's useful if you wish to run a command in a `justfile` that is in a subdirectory. For example, if you are in a directory which contains a subdirectory named `foo`, which contains a `justfile` with the recipe `build`, which is also the default recipe, the following are all equivalent: ```sh $ (cd foo && just build) $ just foo/build $ just foo/ ``` Additional recipes after the first are sought in the same `justfile`. For example, the following are both equivalent: ```sh $ just foo/a b $ (cd foo && just a b) ``` And will both invoke recipes `a` and `b` in `foo/justfile`. ### Imports One `justfile` can include the contents of another using `import` statements. If you have the following `justfile`: ```mf import 'foo/bar.just' a: b @echo A ``` And the following text in `foo/bar.just`: ```just b: @echo B ``` `foo/bar.just` will be included in `justfile` and recipe `b` will be defined: ```sh $ just b B $ just a B A ``` The `import` path can be absolute or relative to the location of the justfile containing it. A leading `~/` in the import path is replaced with the current users home directory. Justfiles are insensitive to order, so included files can reference variables and recipes defined after the `import` statement. Imported files can themselves contain `import`s, which are processed recursively. When `allow-duplicate-recipes` is set, recipes in parent modules override recipes in imports. Imports may be made optional by putting a `?` after the `import` keyword: ```mf import? 'foo/bar.just' ``` Missing source files for optional imports do not produce an error. ### Modules1.19.0 A `justfile` can declare modules using `mod` statements. `mod` statements are currently unstable, so you'll need to use the `--unstable` flag, or set the `JUST_UNSTABLE` environment variable to use them. If you have the following `justfile`: ```mf mod bar a: @echo A ``` And the following text in `bar.just`: ```just b: @echo B ``` `bar.just` will be included in `justfile` as a submodule. Recipes, aliases, and variables defined in one submodule cannot be used in another, and each module uses its own settings. Recipes in submodules can be invoked as subcommands: ```sh $ just --unstable bar b B ``` If a module is named `foo`, just will search for the module file in `foo.just`, `foo/mod.just`, `foo/justfile`, and `foo/.justfile`. In the latter two cases, the module file may have any capitalization. Module statements may be of the form: ```mf mod foo 'PATH' ``` Which loads the module's source file from `PATH`, instead of from the usual locations. A leading `~/` in `PATH` is replaced with the current user's home directory. Environment files are only loaded for the root justfile, and loaded environment variables are available in submodules. Settings in submodules that affect enviroment file loading are ignored. Recipes in submodules without the `[no-cd]` attribute run with the working directory set to the directory containing the submodule source file. `justfile()` and `justfile_directory()` always return the path to the root justfile and the directory that contains it, even when called from submodule recipes. Modules may be made optional by putting a `?` after the `mod` keyword: ```mf mod? foo ``` Missing source files for optional modules do not produce an error. Optional modules with no source file do not conflict, so you can have multiple mod statements with the same name, but with different source file paths, as long as at most one source file exists: ```mf mod? foo 'bar.just' mod? foo 'baz.just' ``` See the [module stabilization tracking issue](https://github.com/casey/just/issues/929) for more information. ### Hiding `justfile`s `just` looks for `justfile`s named `justfile` and `.justfile`, which can be used to keep a `justfile` hidden. ### Just Scripts By adding a shebang line to the top of a `justfile` and making it executable, `just` can be used as an interpreter for scripts: ```sh $ cat > script < formatted-justfile ``` The `--dump` command can be used with `--dump-format json` to print a JSON representation of a `justfile`. ### Fallback to parent `justfile`s If a recipe is not found in a `justfile` and the `fallback` setting is set, `just` will look for `justfile`s in the parent directory and up, until it reaches the root directory. `just` will stop after it reaches a `justfile` in which the `fallback` setting is `false` or unset. As an example, suppose the current directory contains this `justfile`: ```just set fallback foo: echo foo ``` And the parent directory contains this `justfile`: ```just bar: echo bar ``` ```sh $ just bar Trying ../justfile echo bar bar ``` ### Avoiding Argument Splitting Given this `justfile`: ```just foo argument: touch {{argument}} ``` The following command will create two files, `some` and `argument.txt`: ```sh $ just foo "some argument.txt" ``` The users shell will parse `"some argument.txt"` as a single argument, but when `just` replaces `touch {{argument}}` with `touch some argument.txt`, the quotes are not preserved, and `touch` will receive two arguments. There are a few ways to avoid this: quoting, positional arguments, and exported arguments. #### Quoting Quotes can be added around the `{{argument}}` interpolation: ```just foo argument: touch '{{argument}}' ``` This preserves `just`'s ability to catch variable name typos before running, for example if you were to write `{{arument}}`, but will not do what you want if the value of `argument` contains single quotes. #### Positional Arguments The `positional-arguments` setting causes all arguments to be passed as positional arguments, allowing them to be accessed with `$1`, `$2`, …, and `$@`, which can be then double-quoted to avoid further splitting by the shell: ```just set positional-arguments foo argument: touch "$1" ``` This defeats `just`'s ability to catch typos, for example if you type `$2`, but works for all possible values of `argument`, including those with double quotes. #### Exported Arguments All arguments are exported when the `export` setting is set: ```just set export foo argument: touch "$argument" ``` Or individual arguments may be exported by prefixing them with `$`: ```just foo $argument: touch "$argument" ``` This defeats `just`'s ability to catch typos, for example if you type `$argumant`, but works for all possible values of `argument`, including those with double quotes. ### Configuring the Shell There are a number of ways to configure the shell for linewise recipes, which are the default when a recipe does not start with a `#!` shebang. Their precedence, from highest to lowest, is: 1. The `--shell` and `--shell-arg` command line options. Passing either of these will cause `just` to ignore any settings in the current justfile. 2. `set windows-shell := [...]` 3. `set windows-powershell` (deprecated) 4. `set shell := [...]` Since `set windows-shell` has higher precedence than `set shell`, you can use `set windows-shell` to pick a shell on Windows, and `set shell` to pick a shell for all other platforms. Changelog --------- A changelog for the latest release is available in [CHANGELOG.md](https://raw.githubusercontent.com/casey/just/master/CHANGELOG.md). Changelogs for previous releases are available on [the releases page](https://github.com/casey/just/releases). `just --changelog` can also be used to make a `just` binary print its changelog. Miscellanea ----------- ### Companion Tools Tools that pair nicely with `just` include: - [`watchexec`](https://github.com/mattgreen/watchexec) — a simple tool that watches a path and runs a command whenever it detects modifications. ### Shell Alias For lightning-fast command running, put `alias j=just` in your shell's configuration file. In `bash`, the aliased command may not keep the shell completion functionality described in the next section. Add the following line to your `.bashrc` to use the same completion function as `just` for your aliased command: ```sh complete -F _just -o bashdefault -o default j ``` ### Shell Completion Scripts Shell completion scripts for Bash, Zsh, Fish, PowerShell, and Elvish are available in the [completions](https://github.com/casey/just/tree/master/completions) directory. Please refer to your shell's documentation for how to install them. The `just` binary can also generate the same completion scripts at runtime, using the `--completions` command: ```sh $ just --completions zsh > just.zsh ``` *macOS Note:* Recent versions of macOS use zsh as the default shell. If you use Homebrew to install `just`, it will automatically install the most recent copy of the zsh completion script in the Homebrew zsh directory, which the built-in version of zsh doesn't know about by default. It's best to use this copy of the script if possible, since it will be updated whenever you update `just` via Homebrew. Also, many other Homebrew packages use the same location for completion scripts, and the built-in zsh doesn't know about those either. To take advantage of `just` completion in zsh in this scenario, you can set `fpath` to the Homebrew location before calling `compinit`. Note also that Oh My Zsh runs `compinit` by default. So your `.zshrc` file could look like this: ```zsh # Init Homebrew, which adds environment variables eval "$(brew shellenv)" fpath=($HOMEBREW_PREFIX/share/zsh/site-functions $fpath) # Then choose one of these options: # 1. If you're using Oh My Zsh, you can initialize it here # source $ZSH/oh-my-zsh.sh # 2. Otherwise, run compinit yourself # autoload -U compinit # compinit ``` ### Grammar A non-normative grammar of `justfile`s can be found in [GRAMMAR.md](https://github.com/casey/just/blob/master/GRAMMAR.md). ### just.sh Before `just` was a fancy Rust program it was a tiny shell script that called `make`. You can find the old version in [extras/just.sh](https://github.com/casey/just/blob/master/extras/just.sh). ### User `justfile`s If you want some recipes to be available everywhere, you have a few options. First, create a `justfile` in `~/.user.justfile` with some recipes. #### Recipe Aliases If you want to call the recipes in `~/.user.justfile` by name, and don't mind creating an alias for every recipe, add the following to your shell's initialization script: ```sh for recipe in `just --justfile ~/.user.justfile --summary`; do alias $recipe="just --justfile ~/.user.justfile --working-directory . $recipe" done ``` Now, if you have a recipe called `foo` in `~/.user.justfile`, you can just type `foo` at the command line to run it. It took me way too long to realize that you could create recipe aliases like this. Notwithstanding my tardiness, I am very pleased to bring you this major advance in `justfile` technology. #### Forwarding Alias If you'd rather not create aliases for every recipe, you can create a single alias: ```sh alias .j='just --justfile ~/.user.justfile --working-directory .' ``` Now, if you have a recipe called `foo` in `~/.user.justfile`, you can just type `.j foo` at the command line to run it. I'm pretty sure that nobody actually uses this feature, but it's there. ¯\\\_(ツ)\_/¯ #### Customization You can customize the above aliases with additional options. For example, if you'd prefer to have the recipes in your `justfile` run in your home directory, instead of the current directory: ```sh alias .j='just --justfile ~/.user.justfile --working-directory ~' ``` ### Node.js `package.json` Script Compatibility The following export statement gives `just` recipes access to local Node module binaries, and makes `just` recipe commands behave more like `script` entries in Node.js `package.json` files: ```just export PATH := "./node_modules/.bin:" + env_var('PATH') ``` ### Alternatives and Prior Art There is no shortage of command runners! Some more or less similar alternatives to `just` include: - [make](https://en.wikipedia.org/wiki/Make_(software)): The Unix build tool that inspired `just`. There are a few different modern day descendents of the original `make`, including [FreeBSD Make](https://www.freebsd.org/cgi/man.cgi?make(1)) and [GNU Make](https://www.gnu.org/software/make/). - [task](https://github.com/go-task/task): A YAML-based command runner written in Go. - [maid](https://github.com/egoist/maid): A Markdown-based command runner written in JavaScript. - [microsoft/just](https://github.com/microsoft/just): A JavaScript-based command runner written in JavaScript. - [cargo-make](https://github.com/sagiegurari/cargo-make): A command runner for Rust projects. - [mmake](https://github.com/tj/mmake): A wrapper around `make` with a number of improvements, including remote includes. - [robo](https://github.com/tj/robo): A YAML-based command runner written in Go. - [mask](https://github.com/jakedeichert/mask): A Markdown-based command runner written in Rust. - [makesure](https://github.com/xonixx/makesure): A simple and portable command runner written in AWK and shell. - [haku](https://github.com/VladimirMarkelov/haku): A make-like command runner written in Rust. Contributing ------------ `just` welcomes your contributions! `just` is released under the maximally permissive [CC0](https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt) public domain dedication and fallback license, so your changes must also be released under this license. ### Janus [Janus](https://github.com/casey/janus) is a tool that collects and analyzes `justfile`s, and can determine if a new version of `just` breaks or changes the interpretation of existing `justfile`s. Before merging a particularly large or gruesome change, Janus should be run to make sure that nothing breaks. Don't worry about running Janus yourself, Casey will happily run it for you on changes that need it. ### Minimum Supported Rust Version The minimum supported Rust version, or MSRV, is current stable Rust. It may build on older versions of Rust, but this is not guaranteed. ### New Releases New releases of `just` are made frequently so that users quickly get access to new features. Release commit messages use the following template: ``` Release x.y.z - Bump version: x.y.z → x.y.z - Update changelog - Update changelog contributor credits - Update dependencies - Update man page - Update version references in readme ``` Frequently Asked Questions -------------------------- ### What are the idiosyncrasies of Make that Just avoids? `make` has some behaviors which are confusing, complicated, or make it unsuitable for use as a general command runner. One example is that under some circumstances, `make` won't actually run the commands in a recipe. For example, if you have a file called `test` and the following makefile: ```just test: ./test ``` `make` will refuse to run your tests: ```sh $ make test make: `test' is up to date. ``` `make` assumes that the `test` recipe produces a file called `test`. Since this file exists and the recipe has no other dependencies, `make` thinks that it doesn't have anything to do and exits. To be fair, this behavior is desirable when using `make` as a build system, but not when using it as a command runner. You can disable this behavior for specific targets using `make`'s built-in [`.PHONY` target name](https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html), but the syntax is verbose and can be hard to remember. The explicit list of phony targets, written separately from the recipe definitions, also introduces the risk of accidentally defining a new non-phony target. In `just`, all recipes are treated as if they were phony. Other examples of `make`'s idiosyncrasies include the difference between `=` and `:=` in assignments, the confusing error messages that are produced if you mess up your makefile, needing `$$` to use environment variables in recipes, and incompatibilities between different flavors of `make`. ### What's the relationship between Just and Cargo build scripts? [`cargo` build scripts](http://doc.crates.io/build-script.html) have a pretty specific use, which is to control how `cargo` builds your Rust project. This might include adding flags to `rustc` invocations, building an external dependency, or running some kind of codegen step. `just`, on the other hand, is for all the other miscellaneous commands you might run as part of development. Things like running tests in different configurations, linting your code, pushing build artifacts to a server, removing temporary files, and the like. Also, although `just` is written in Rust, it can be used regardless of the language or build system your project uses. Further Ramblings ----------------- I personally find it very useful to write a `justfile` for almost every project, big or small. On a big project with multiple contributors, it's very useful to have a file with all the commands needed to work on the project close at hand. There are probably different commands to test, build, lint, deploy, and the like, and having them all in one place is useful and cuts down on the time you have to spend telling people which commands to run and how to type them. And, with an easy place to put commands, it's likely that you'll come up with other useful things which are part of the project's collective wisdom, but which aren't written down anywhere, like the arcane commands needed for some part of your revision control workflow, to install all your project's dependencies, or all the random flags you might need to pass to the build system. Some ideas for recipes: - Deploying/publishing the project - Building in release mode vs debug mode - Running in debug mode or with logging enabled - Complex git workflows - Updating dependencies - Running different sets of tests, for example fast tests vs slow tests, or running them with verbose output - Any complex set of commands that you really should write down somewhere, if only to be able to remember them Even for small, personal projects it's nice to be able to remember commands by name instead of ^Reverse searching your shell history, and it's a huge boon to be able to go into an old project written in a random language with a mysterious build system and know that all the commands you need to do whatever you need to do are in the `justfile`, and that if you type `just` something useful (or at least interesting!) will probably happen. For ideas for recipes, check out [this project's `justfile`](https://github.com/casey/just/blob/master/justfile), or some of the `justfile`s [out in the wild](https://github.com/search?q=path%3A**%2Fjustfile&type=code). Anyways, I think that's about it for this incredibly long-winded README. I hope you enjoy using `just` and find great success and satisfaction in all your computational endeavors! 😸 just-1.21.0/README.中文.md000064400000000000000000002207331046102023000143520ustar 00000000000000↖️ 目录

just

crates.io version build status downloads chat on discord say thanks

`just` 为您提供一种保存和运行项目特有命令的便捷方式。 本指南同时也可以以 [书](https://just.systems/man/zh/) 的形式提供在线阅读。 命令,在此也称为配方,存储在一个名为 `justfile` 的文件中,其语法受 `make` 启发: ![screenshot](https://raw.githubusercontent.com/casey/just/master/screenshot.png) 然后你可以用 `just RECIPE` 运行它们: ```sh $ just test-all cc *.c -o main ./test --all Yay, all your tests passed! ``` `just` 有很多很棒的特性,而且相比 `make` 有很多改进: - `just` 是一个命令运行器,而不是一个构建系统,所以它避免了许多 [`make` 的复杂性和特异性](#just-避免了-make-的哪些特异性)。不需要 `.PHONY` 配方! - 支持 Linux、MacOS 和 Windows,而且无需额外的依赖。(尽管如果你的系统没有 `sh`,你需要 [选择一个不同的 Shell](#shell))。 - 错误具体且富有参考价值,语法错误将会与产生它们的上下文一起被报告。 - 配方可以接受 [命令行参数](#配方参数)。 - 错误会尽可能被静态地解决。未知的配方和循环依赖关系会在运行之前被报告。 - `just` 可以 [加载`.env`文件](#env-集成),简化环境变量注入。 - 配方可以在 [命令行中列出](#列出可用的配方)。 - 命令行自动补全脚本 [支持大多数流行的 Shell](#shell-自动补全脚本)。 - 配方可以用 [任意语言](#用其他语言书写配方) 编写,如 Python 或 NodeJS。 - `just` 可以从任何子目录中调用,而不仅仅是包含 `justfile` 的目录。 - 不仅如此,还有 [更多](https://just.systems/man/zh/)! 如果你在使用 `just` 方面需要帮助,请随时创建一个 Issue 或在 [Discord](https://discord.gg/ezYScXR) 上与我联系。我们随时欢迎功能请求和错误报告! 安装 ------------ ### 预备知识 `just` 应该可以在任何有合适的 `sh` 的系统上运行,包括 Linux、MacOS 和 BSD。 在 Windows 上,`just` 可以使用 [Git for Windows](https://git-scm.com)、[GitHub Desktop](https://desktop.github.com) 或 [Cygwin](http://www.cygwin.com) 所提供的 `sh`。 如果你不愿意安装 `sh`,也可以使用 `shell` 设置来指定你要使用的 Shell。 比如 PowerShell: ```just # 使用 PowerShell 替代 sh: set shell := ["powershell.exe", "-c"] hello: Write-Host "Hello, world!" ``` …或者 `cmd.exe`: ```just # 使用 cmd.exe 替代 sh: set shell := ["cmd.exe", "/c"] list: dir ``` 你也可以使用命令行参数来设置 Shell。例如,若要使用 PowerShell 也可以用 `--shell powershell.exe --shell-arg -c` 启动`just`。 (PowerShell 默认安装在 Windows 7 SP1 和 Windows Server 2008 R2 S1 及更高版本上,而 `cmd.exe` 相当麻烦,所以 PowerShell 被推荐给大多数 Windows 用户) ### 安装包
操作系统 包管理器 安装包 命令
Various Cargo just cargo install just
Microsoft Windows Scoop just scoop install just
Various Homebrew just brew install just
macOS MacPorts just port install just
Arch Linux pacman just pacman -S just
Various Nix just nix-env -iA nixpkgs.just
NixOS Nix just nix-env -iA nixos.just
Solus eopkg just eopkg install just
Void Linux XBPS just xbps-install -S just
FreeBSD pkg just pkg install just
Alpine Linux apk-tools just apk add just
Fedora Linux DNF just dnf install just
Gentoo Linux Portage guru/sys-devel/just eselect repository enable guru
emerge --sync guru
emerge sys-devel/just
Various Conda just conda install -c conda-forge just
Microsoft Windows Chocolatey just choco install just
Various Snap just snap install --edge --classic just
Various asdf just asdf plugin add just
asdf install just <version>
Debian and Ubuntu derivatives MPR just git clone 'https://mpr.makedeb.org/just'
cd just
makedeb -si
Debian and Ubuntu derivatives Prebuilt-MPR just You must have the Prebuilt-MPR set up on your system in order to run this command.
sudo apt install just
![package version table](https://repology.org/badge/vertical-allrepos/just.svg) ### 预制二进制文件 Linux、MacOS 和 Windows 的预制二进制文件可以在 [发布页](https://github.com/casey/just/releases) 上找到。 你也可以在 Linux、MacOS 或 Windows 上使用下面的命令来下载最新的版本,只需将 `DEST` 替换为你想安装 `just` 的目录即可: ```sh curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to DEST ``` 例如,安装 `just` 到 `~/bin` 目录: ```sh # 创建 ~/bin mkdir -p ~/bin # 下载并解压 just 到 ~/bin/just curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/bin # 在 Shell 搜索可执行文件的路径中添加`~/bin` # 这一行应该被添加到你的 Shell 初始化文件中,e.g. `~/.bashrc` 或者 `~/.zshrc`: export PATH="$PATH:$HOME/bin" # 现在 just 应该就可以执行了 just --help ``` ### GitHub Actions 使用 [extractions/setup-just](https://github.com/extractions/setup-just): ```yaml - uses: extractions/setup-just@v1 with: just-version: 0.8 # optional semver specification, otherwise latest ``` 使用 [taiki-e/install-action](https://github.com/taiki-e/install-action): ```yaml - uses: taiki-e/install-action@just ``` ### 发布 RSS 订阅 `just` 的发布 [RSS 订阅](https://en.wikipedia.org/wiki/RSS) 可以在 [这里](https://github.com/casey/just/releases.atom) 找到。 ### Node.js 安装 [just-install](https://npmjs.com/package/just-install) 可用于在 Node.js 应用程序中自动安装 `just`。 `just` 是一个很赞的比 npm 脚本更强大的替代品。如果你想在 Node.js 应用程序的依赖中包含 `just`,可以通过 `just-install`,它将在本机安装一个针对特定平台的二进制文件作为 `npm install` 安装结果的一部分。这样就不需要每个开发者使用上述提到的步骤独立安装 `just`。安装后,`just` 命令将在 npm 脚本或 npx 中工作。这对那些想让项目的设置过程尽可能简单的团队来说是很有用的。 想了解更多信息, 请查看 [just-install 说明文件](https://github.com/brombal/just-install#readme)。 向后兼容性 ----------------------- 随着 1.0 版本的发布,`just` 突出对向后兼容性和稳定性的强烈承诺。 未来的版本将不会引入向后不兼容的变化,不会使现有的 `justfile` 停止工作,或破坏命令行界面的正常调用。 然而,这并不排除修复全面的错误,即使这样做可能会破坏依赖其行为的 `justfiles`。 永远不会有一个 `just` 2.0。任何理想的向后兼容的变化都是在每个 `justfile` 的基础上选择性加入的,所以用户可以在他们的闲暇时间进行迁移。 还没有准备好稳定化的功能将在 `--unstable` 标志后被选择性启用。由 `--unstable` 启用的功能可能会在任何时候以不兼容的方式发生变化。 编辑器支持 -------------- `justfile` 的语法与 `make` 非常接近,你可以让你的编辑器对 `just` 使用 `make` 语法高亮。 ### Vim 和 Neovim #### `vim-just` [vim-just](https://github.com/NoahTheDuke/vim-just) 插件可以为 vim 提供 `justfile` 语法高亮显示。 你可以用你喜欢的软件包管理器安装它,如 [Plug](https://github.com/junegunn/vim-plug): ```vim call plug#begin() Plug 'NoahTheDuke/vim-just' call plug#end() ``` 或者使用 Vim 的内置包支持: ```sh mkdir -p ~/.vim/pack/vendor/start cd ~/.vim/pack/vendor/start git clone https://github.com/NoahTheDuke/vim-just.git ``` #### `tree-sitter-just` [tree-sitter-just](https://github.com/IndianBoy42/tree-sitter-just) 是一个针对 Neovim 的 [Nvim Treesitter](https://github.com/nvim-treesitter/nvim-treesitter) 插件。 #### Makefile 语法高亮 Vim 内置的 makefile 语法高亮对 `justfile` 来说并不完美,但总比没有好。你可以把以下内容放在 `~/.vim/filetype.vim` 中: ```vimscript if exists("did_load_filetypes") finish endif augroup filetypedetect au BufNewFile,BufRead justfile setf make augroup END ``` 或者在单个 `justfile` 中添加以下内容,以在每个文件的基础上启用 `make` 模式: ```text # vim: set ft=make : ``` ### Emacs [just-mode](https://github.com/leon-barrett/just-mode.el) 可以为 `justfile` 提供语法高亮和自动缩进。它可以在 [MELPA](https://melpa.org/) 上通过 [just-mode](https://melpa.org/#/just-mode) 获得。 [justl](https://github.com/psibi/justl.el) 提供了执行和列出配方的命令。 你可以在一个单独的 `justfile` 中添加以下内容,以便对每个文件启用 `make` 模式: ```text # Local Variables: # mode: makefile # End: ``` ### Visual Studio Code 由 [skellock](https://github.com/skellock) 为 VS Code 提供的扩展 [可在此获得](https://marketplace.visualstudio.com/items?itemName=skellock.just)([仓库](https://github.com/skellock/vscode-just)),但是开发已经不活跃了。 你可以通过运行以下命令来安装它: ```sh code --install-extension skellock.just ``` 最近由 [sclu1034](https://github.com/sclu1034) 提供的一个更活跃的分叉可以在 [这里](https://github.com/sclu1034/vscode-just) 找到。 ### JetBrains IDEs 由 [linux_china](https://github.com/linux-china) 为 JetBrains IDEs 提供的插件可 [由此获得](https://plugins.jetbrains.com/plugin/18658-just)。 ### Kakoune Kakoune 已经内置支持 `justfile` 语法高亮,这要感谢 TeddyDD。 ### Sublime Text 由 [nk9](https://github.com/nk9) 提供的 [Just 包](https://github.com/nk9/just_sublime) 支持 `just` 语法高亮,同时还有其它工具,这些可以在 [PackageControl](https://packagecontrol.io/packages/Just) 上找到。 ### 其它编辑器 欢迎给我发送必要的命令,以便在你选择的编辑器中实现语法高亮,这样我就可以把它们放在这里。 快速开始 ----------- 参见 [安装部分](#安装) 了解如何在你的电脑上安装 `just`。试着运行 `just --version` 以确保它被正确安装。 关于语法的概述,请查看这个 [速查表](https://cheatography.com/linux-china/cheat-sheets/justfile/)。 一旦 `just` 安装完毕并开始工作,在你的项目根目录创建一个名为 `justfile` 的文件,内容如下: ```just recipe-name: echo 'This is a recipe!' # 这是一行注释 another-recipe: @echo 'This is another recipe.' ``` 当你调用 `just` 时,它会在当前目录和父目录寻找文件 `justfile`,所以你可以从你项目的任何子目录中调用它。 搜索 `justfile` 是不分大小写的,所以任何大小写,如 `Justfile`、`JUSTFILE` 或 `JuStFiLe` 都可以工作。`just` 也会寻找名字为 `.justfile` 的文件,以便你打算隐藏一个 `justfile`。 运行 `just` 时未传参数,则运行 `justfile` 中的第一个配方: ```sh $ just echo 'This is a recipe!' This is a recipe! ``` 通过一个或多个参数指定要运行的配方: ```sh $ just another-recipe This is another recipe. ``` `just` 在运行每条命令前都会将其打印到标准错误中,这就是为什么 `echo 'This is a recipe!'` 被打印出来。对于以 `@` 开头的行,这将被抑制,这就是为什么 `echo 'This is another recipe.'` 没有被打印。 如果一个命令失败,配方就会停止运行。这里 `cargo publish` 只有在 `cargo test` 成功后才会运行: ```just publish: cargo test # 前面的测试通过才会执行 publish! cargo publish ``` 配方可以依赖其他配方。在这里,`test` 配方依赖于 `build` 配方,所以 `build` 将在 `test` 之前运行: ```just build: cc main.c foo.c bar.c -o main test: build ./test sloc: @echo "`wc -l *.c` lines of code" ``` ```sh $ just test cc main.c foo.c bar.c -o main ./test testing… all tests passed! ``` 没有依赖关系的配方将按照命令行上给出的顺序运行: ```sh $ just build sloc cc main.c foo.c bar.c -o main 1337 lines of code ``` 依赖项总是先运行,即使它们被放在依赖它们的配方之后: ```sh $ just test build cc main.c foo.c bar.c -o main ./test testing… all tests passed! ``` 示例 -------- 在 [Examples 目录](https://github.com/casey/just/tree/master/examples) 中可以找到各种 `justfile` 的例子。 特性介绍 -------- ### 默认配方 当 `just` 被调用而没有传入任何配方时,它会运行 `justfile` 中的第一个配方。这个配方可能是项目中最常运行的命令,比如运行测试: ```just test: cargo test ``` 你也可以使用依赖关系来默认运行多个配方: ```just default: lint build test build: echo Building… test: echo Testing… lint: echo Linting… ``` 在没有合适配方作为默认配方的情况下,你也可以在 `justfile` 的开头添加一个配方,用于列出可用的配方: ```just default: just --list ``` ### 列出可用的配方 可以用 `just --list` 按字母顺序列出配方: ```sh $ just --list Available recipes: build test deploy lint ``` `just --summary` 以更简洁的形式列出配方: ```sh $ just --summary build test deploy lint ``` 传入 `--unsorted` 选项可以按照它们在 `justfile` 中出现的顺序打印配方: ```just test: echo 'Testing!' build: echo 'Building!' ``` ```sh $ just --list --unsorted Available recipes: test build ``` ```sh $ just --summary --unsorted test build ``` 如果你想让 `just` 默认列出 `justfile` 中的配方,你可以使用这个作为默认配方: ```just default: @just --list ``` 请注意,你可能需要在上面这一行中添加 `--justfile {{justfile()}}`。没有它,如果你执行 `just -f /some/distant/justfile -d .` 或 `just -f ./non-standard-justfile` 配方中的普通 `just --list` 就不一定会使用你提供的文件,它将试图在你的当前路径中找到一个 `justfile`,甚至可能导致 `No justfile found` 的错误。 标题文本可以用 `--list-heading` 来定制: ```sh $ just --list --list-heading $'Cool stuff…\n' Cool stuff… test build ``` 而缩进可以用 `--list-prefix` 来定制: ```sh $ just --list --list-prefix ···· Available recipes: ····test ····build ``` `--list-heading` 参数同时替换了标题和后面的换行,所以如果不是空的,应该包含一个换行。这样做是为了允许你通过传递空字符串来完全抑制标题行: ```sh $ just --list --list-heading '' test build ``` ### 别名 别名允许你用其他名称来调用配方: ```just alias b := build build: echo 'Building!' ``` ```sh $ just b build echo 'Building!' Building! ``` ### 设置 设置控制解释和执行。每个设置最多可以指定一次,可以出现在 `justfile` 的任何地方。 例如: ```just set shell := ["zsh", "-cu"] foo: # this line will be run as `zsh -cu 'ls **/*.txt'` ls **/*.txt ``` #### 设置一览表 | 名称 | 值 | 默认 | 描述 | | ------------------------- | ------------------ | --------|------------------------------------------------------------------------------- | | `allow-duplicate-recipes` | boolean | False | 允许在 `justfile` 后面出现的配方覆盖之前的同名配方 | | `dotenv-load` | boolean | False | 如果有`.env` 环境变量文件的话,则将其加载 | | `export` | boolean | False | 将所有变量导出为环境变量 | | `fallback` | boolean | False | 如果命令行中的第一个配方没有找到,则在父目录中搜索 `justfile` | | `ignore-comments` | boolean | False | 忽略以`#`开头的配方行 | | `positional-arguments` | boolean | False | 传递位置参数 | | `shell` | `[COMMAND, ARGS…]` | - | 设置用于调用配方和评估反引号内包裹内容的命令 | | `tempdir` | string | - | 在 `tempdir` 位置创建临时目录,而不是系统默认的临时目录 | | `windows-powershell` | boolean | False | 在 Windows 上使用 PowerShell 作为默认 Shell(废弃,建议使用 `windows-shell`) | | `windows-shell` | `[COMMAND, ARGS…]` | - | 设置用于调用配方和评估反引号内包裹内容的命令 | Bool 类型设置可以写成: ```justfile set NAME ``` 这就相当于: ```justfile set NAME := true ``` #### 允许重复的配方 如果 `allow-duplicate-recipes` 被设置为 `true`,那么定义多个同名的配方就不会出错,而会使用最后的定义。默认为 `false`。 ```just set allow-duplicate-recipes @foo: echo foo @foo: echo bar ``` ```sh $ just foo bar ``` #### 环境变量加载 如果将 `dotenv-load` 设置为 `true`,并且存在 `.env` 文件,则该环境配置文件将被加载。默认为 `false`。 #### 导出 `export` 设置使所有 `just` 变量作为环境变量被导出。默认值为 `false`。 ```just set export a := "hello" @foo b: echo $a echo $b ``` ```sh $ just foo goodbye hello goodbye ``` #### 位置参数 如果 `positional-arguments` 为 `true`,配方参数将作为位置参数传递给命令。对于行式配方,参数 `$0` 将是配方的名称。 例如,运行这个配方: ```just set positional-arguments @foo bar: echo $0 echo $1 ``` 将产生以下输出: ```sh $ just foo hello foo hello ``` 当使用 `sh` 兼容的 Shell,如 `bash` 或 `zsh` 时,`$@` 会展开为传给配方的位置参数,从1开始。当在双引号内使用 `"$@"` 时,包括空白的参数将被传递,就像它们是双引号一样。也就是说,`"$@"` 相当于 `"$1" "$2"`......当没有位置参数时,`"$@"` 和 `$@` 将展开为空(即,它们被删除)。 这个例子的配方将逐行打印参数: ```just set positional-arguments @test *args='': bash -c 'while (( "$#" )); do echo - $1; shift; done' -- "$@" ``` 用 _两个_ 参数运行: ```sh $ just test foo "bar baz" - foo - bar baz ``` #### Shell `shell` 设置控制用于调用执行配方代码行和反引号内指令的命令。Shebang 配方不受影响。 ```just # use python3 to execute recipe lines and backticks set shell := ["python3", "-c"] # use print to capture result of evaluation foos := `print("foo" * 4)` foo: print("Snake snake snake snake.") print("{{foos}}") ``` `just` 把要执行的命令作为一个参数进行传递。许多 Shell 需要一个额外的标志,通常是 `-c`,以使它们评估执行第一个参数。 ##### Windows Shell `just` 在 Windows 上默认使用 `sh`。要在 Windows 上使用不同的 Shell,请使用`windows-shell`: ```just set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] hello: Write-Host "Hello, world!" ``` 参考 [powershell.just](https://github.com/casey/just/blob/master/examples/powershell.just) ,了解在所有平台上使用 PowerShell 的 justfile。 ##### Windows PowerShell *`set windows-powershell` 使用遗留的 `powershell.exe` 二进制文件,不再推荐。请参阅上面的 `windows-shell` 设置,以通过更灵活的方式来控制在 Windows 上使用哪个 Shell。* `just` 在 Windows 上默认使用 `sh`。要使用 `powershell.exe` 作为替代,请将 `windows-powershell` 设置为 `true`。 ```just set windows-powershell := true hello: Write-Host "Hello, world!" ``` ##### Python 3 ```just set shell := ["python3", "-c"] ``` ##### Bash ```just set shell := ["bash", "-uc"] ``` ##### Z Shell ```just set shell := ["zsh", "-uc"] ``` ##### Fish ```just set shell := ["fish", "-c"] ``` ##### Nushell ```just set shell := ["nu", "-c"] ``` 如果你想设置默认的表格显示模式为 `light`: ```just set shell := ['nu', '-m', 'light', '-c'] ``` *[Nushell](https://github.com/nushell/nushell) 使用 Rust 开发并且具备良好的跨平台能力,**支持 Windows / macOS 和各种 Linux 发行版*** ### 文档注释 紧接着配方前面的注释将出现在 `just --list` 中: ```just # build stuff build: ./bin/build # test stuff test: ./bin/test ``` ```sh $ just --list Available recipes: build # build stuff test # test stuff ``` ### `.env` 集成 如果 [`dotenv-load`](#环境变量加载) 被设置,`just` 将从一个名为 `.env` 的文件中加载环境变量。这个文件可以和你的 `justfile` 位于同一目录下,或者位于其父目录下。这些变量是环境变量,而不是 `just` 的变量,因此必须使用 `$VARIABLE_NAME` 在配方和反引号中访问。 例如,假如你的 `.env` 文件包含: ```sh # 注释,将被忽略 DATABASE_ADDRESS=localhost:6379 SERVER_PORT=1337 ``` 而你的 `justfile` 包含: ```just set dotenv-load serve: @echo "Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…" ./server --database $DATABASE_ADDRESS --port $SERVER_PORT ``` `just serve` 将会输出: ```sh $ just serve Starting server with database localhost:6379 on port 1337… ./server --database $DATABASE_ADDRESS --port $SERVER_PORT ``` ### 变量和替换 支持在变量、字符串、拼接、路径连接和替换中使用 `{{…}}` : ```just tmpdir := `mktemp -d` version := "0.2.7" tardir := tmpdir / "awesomesauce-" + version tarball := tardir + ".tar.gz" publish: rm -f {{tarball}} mkdir {{tardir}} cp README.md *.c {{tardir}} tar zcvf {{tarball}} {{tardir}} scp {{tarball}} me@server.com:release/ rm -rf {{tarball}} {{tardir}} ``` #### 路径拼接 `/` 操作符可用于通过斜线连接两个字符串: ```just foo := "a" / "b" ``` ``` $ just --evaluate foo a/b ``` 请注意,即使已经有一个 `/`,也会添加一个 `/`: ```just foo := "a/" bar := foo / "b" ``` ``` $ just --evaluate bar a//b ``` 也可以构建绝对路径1.5.0: ```just foo := / "b" ``` ``` $ just --evaluate foo /b ``` `/` 操作符使用 `/` 字符,即使在 Windows 上也是如此。因此,在使用通用命名规则(UNC)的路径中应避免使用 `/` 操作符,即那些以 `\?` 开头的路径,因为 UNC 路径不支持正斜线。 #### 转义 `{{` 想要写一个包含 `{{` 的配方,可以使用 `{{{{`: ```just braces: echo 'I {{{{LOVE}} curly braces!' ``` (未匹配的 `}}` 会被忽略,所以不需要转义) 另一个选择是把所有你想转义的文本都放在插值里面: ```just braces: echo '{{'I {{LOVE}} curly braces!'}}' ``` 然而,另一个选择是使用 `{{ "{{" }}`: ```just braces: echo 'I {{ "{{" }}LOVE}} curly braces!' ``` ### 字符串 双引号字符串支持转义序列: ```just string-with-tab := "\t" string-with-newline := "\n" string-with-carriage-return := "\r" string-with-double-quote := "\"" string-with-slash := "\\" string-with-no-newline := "\ " ``` ```sh $ just --evaluate "tring-with-carriage-return := " string-with-double-quote := """ string-with-newline := " " string-with-no-newline := "" string-with-slash := "\" string-with-tab := " " ``` 字符串可以包含换行符: ```just single := ' hello ' double := " goodbye " ``` 单引号字符串不支持转义序列: ```just escapes := '\t\n\r\"\\' ``` ```sh $ just --evaluate escapes := "\t\n\r\"\\" ``` 支持单引号和双引号字符串的缩进版本,以三个单引号或三个双引号为界。缩进的字符串行被删除了所有非空行所共有的前导空白: ```just # 这个字符串执行结果为 `foo\nbar\n` x := ''' foo bar ''' # 这个字符串执行结果为 `abc\n wuv\nbar\n` y := """ abc wuv xyz """ ``` 与未缩进的字符串类似,缩进的双引号字符串处理转义序列,而缩进的单引号字符串则忽略转义序列。转义序列的处理是在取消缩进后进行的。取消缩进的算法不考虑转义序列产生的空白或换行。 ### 错误忽略 通常情况下,如果一个命令返回一个非零的退出状态,将停止执行。要想在一个命令之后继续执行,即使它失败了,需要在命令前加上 `-`: ```just foo: -cat foo echo 'Done!' ``` ```sh $ just foo cat foo cat: foo: No such file or directory echo 'Done!' Done! ``` ### 函数 `just` 提供了一些内置函数,在编写配方时可能很有用。 #### 系统信息 - `arch()` — 指令集结构。可能的值是:`"aarch64"`, `"arm"`, `"asmjs"`, `"hexagon"`, `"mips"`, `"msp430"`, `"powerpc"`, `"powerpc64"`, `"s390x"`, `"sparc"`, `"wasm32"`, `"x86"`, `"x86_64"`, 和 `"xcore"`。 - `os()` — 操作系统,可能的值是: `"android"`, `"bitrig"`, `"dragonfly"`, `"emscripten"`, `"freebsd"`, `"haiku"`, `"ios"`, `"linux"`, `"macos"`, `"netbsd"`, `"openbsd"`, `"solaris"`, 和 `"windows"`。 - `os_family()` — 操作系统系列;可能的值是:`"unix"` 和 `"windows"`。 例如: ```just system-info: @echo "This is an {{arch()}} machine". ``` ```sh $ just system-info This is an x86_64 machine ``` `os_family()` 函数可以用来创建跨平台的 `justfile`,使其可以在不同的操作系统上工作。一个例子,见 [cross-platform.just](https://github.com/casey/just/blob/master/examples/cross-platform.just) 文件。 #### 环境变量 - `env_var(key)` — 获取名称为 `key` 的环境变量,如果不存在则终止。 ```just home_dir := env_var('HOME') test: echo "{{home_dir}}" ``` ```sh $ just /home/user1 ``` - `env_var_or_default(key, default)` — 获取名称为 `key` 的环境变量,如果不存在则返回 `default`。 #### 调用目录 - `invocation_directory()` - 获取 `just` 被调用时当前目录所对应的绝对路径,在 `just` 改变路径并执行相应命令前。 例如,要对 "当前目录" 下的文件调用 `rustfmt`(从用户/调用者的角度看),使用以下规则: ```just rustfmt: find {{invocation_directory()}} -name \*.rs -exec rustfmt {} \; ``` 另外,如果你的命令需要从当前目录运行,你可以使用如下方式: ```just build: cd {{invocation_directory()}}; ./some_script_that_needs_to_be_run_from_here ``` #### Justfile 和 Justfile 目录 - `justfile()` - 取得当前 `justfile` 的路径。 - `justfile_directory()` - 取得当前 `justfile` 文件父目录的路径。 例如,运行一个相对于当前 `justfile` 位置的命令: ```just script: ./{{justfile_directory()}}/scripts/some_script ``` #### Just 可执行程序 - `just_executable()` - `just` 可执行文件的绝对路径。 例如: ```just executable: @echo The executable is at: {{just_executable()}} ``` ```sh $ just The executable is at: /bin/just ``` #### 字符串处理 - `quote(s)` - 用 `'\''` 替换所有的单引号,并在 `s` 的首尾添加单引号。这足以为许多 Shell 转义特殊字符,包括大多数 Bourne Shell 的后代。 - `replace(s, from, to)` - 将 `s` 中的所有 `from` 替换为 `to`。 - `replace_regex(s, regex, replacement)` - 将 `s` 中所有的 `regex` 替换为 `replacement`。正则表达式由 [Rust `regex` 包](https://docs.rs/regex/latest/regex/) 提供。参见 [语法文档](https://docs.rs/regex/latest/regex/#syntax) 以了解使用示例。 - `trim(s)` - 去掉 `s` 的首尾空格。 - `trim_end(s)` - 去掉 `s` 的尾部空格。 - `trim_end_match(s, pat)` - 删除与 `pat` 匹配的 `s` 的后缀。 - `trim_end_matches(s, pat)` - 反复删除与 `pat` 匹配的 `s` 的后缀。 - `trim_start(s)` - 去掉 `s` 的首部空格。 - `trim_start_match(s, pat)` - 删除与 `pat` 匹配的 `s` 的前缀。 - `trim_start_matches(s, pat)` - 反复删除与 `pat` 匹配的 `s` 的前缀。 #### 大小写转换 - `capitalize(s)`1.7.0 - 将 `s` 的第一个字符转换成大写字母,其余的转换成小写字母。 - `kebabcase(s)`1.7.0 - 将 `s` 转换为 `kebab-case`。 - `lowercamelcase(s)`1.7.0 - 将 `s` 转换为小驼峰形式:`lowerCamelCase`。 - `lowercase(s)` - 将 `s` 转换为全小写形式。 - `shoutykebabcase(s)`1.7.0 - 将 `s` 转换为 `SHOUTY-KEBAB-CASE`。 - `shoutysnakecase(s)`1.7.0 - 将 `s` 转换为 `SHOUTY_SNAKE_CASE`。 - `snakecase(s)`1.7.0 - 将 `s` 转换为 `snake_case`。 - `titlecase(s)`1.7.0 - 将 `s` 转换为 `Title Case`。 - `uppercamelcase(s)`1.7.0 - 将 `s` 转换为 `UpperCamelCase`。 - `uppercase(s)` - 将 `s` 转换为大写形式。 #### 路径操作 ##### 非可靠的 - `absolute_path(path)` - 将当前工作目录中到相对路径 `path` 的路径转换为绝对路径。在 `/foo` 目录通过 `absolute_path("./bar.txt")` 可以得到 `/foo/bar.txt`。 - `extension(path)` - 获取 `path` 的扩展名。`extension("/foo/bar.txt")` 结果为 `txt`。 - `file_name(path)` - 获取 `path` 的文件名,去掉任何前面的目录部分。`file_name("/foo/bar.txt")` 的结果为 `bar.txt`。 - `file_stem(path)` - 获取 `path` 的文件名,不含扩展名。`file_stem("/foo/bar.txt")` 的结果为 `bar`。 - `parent_directory(path)` - 获取 `path` 的父目录。`parent_directory("/foo/bar.txt")` 的结果为 `/foo`。 - `without_extension(path)` - 获取 `path` 不含扩展名部分。`without_extension("/foo/bar.txt")` 的结果为 `/foo/bar`。 这些函数可能会失败,例如,如果一个路径没有扩展名,则将停止执行。 ##### 可靠的 - `clean(path)` - 通过删除多余的路径分隔符、中间的 `.` 和 `..` 来简化 `path`。`clean("foo//bar")` 结果为 `foo/bar`,`clean("foo/..")` 为 `.`,`clean("foo/./bar")` 结果为 `foo/bar`。 - `join(a, b…)` - *这个函数在 Unix 上使用 `/`,在 Windows 上使用 `\`,这可能会导致非预期的行为。`/` 操作符,例如,`a / b`,总是使用 `/`,应该被考虑作为替代,除非在 Windows 上特别指定需要 `\`。* 将路径 `a` 和 路径 `b` 拼接在一起。`join("foo/bar", "baz")` 结果为 `foo/bar/baz`。它接受两个或多个参数。 #### 文件系统访问 - `path_exists(path)` - 如果路径指向一个存在的文件或目录,则返回 `true`,否则返回 `false`。也会遍历符号链接,如果路径无法访问或指向一个无效的符号链接,则返回 `false`。 ##### 错误报告 - `error(message)` - 终止执行并向用户报告错误 `message`。 #### UUID 和哈希值生成 - `sha256(string)` - 以十六进制字符串形式返回 `string` 的 SHA-256 哈希值。 - `sha256_file(path)` - 以十六进制字符串形式返回 `path` 处的文件的 SHA-256 哈希值。 - `uuid()` - 返回一个随机生成的 UUID。 ### 配方属性 配方可以通过添加属性注释来改变其行为。 | 名称 | 描述 | | ----------------------------------- | -------------------------------------- | | `[no-cd]`1.9.0 | 在执行配方之前不要改变目录。 | | `[no-exit-message]`1.7.0 | 如果配方执行失败,不要打印错误信息。 | | `[linux]`1.8.0 | 在Linux上启用配方。 | | `[macos]`1.8.0 | 在MacOS上启用配方。 | | `[unix]`1.8.0 | 在Unixes上启用配方。 | | `[windows]`1.8.0 | 在Windows上启用配方。 | | `[private]`1.10.0 | 参见 [私有配方](#私有配方). | #### 启用和禁用配方1.8.0 `[linux]`, `[macos]`, `[unix]` 和 `[windows]` 属性是配置属性。默认情况下,配方总是被启用。一个带有一个或多个配置属性的配方只有在其中一个或多个配置处于激活状态时才会被启用。 这可以用来编写因运行的操作系统不同,其行为也不同的 `justfile`。以下 `justfile` 中的 `run` 配方将编译和运行 `main.c`,并且根据操作系统的不同而使用不同的C编译器,同时使用正确的二进制产物名称: ```just [unix] run: cc main.c ./a.out [windows] run: cl main.c main.exe ``` #### 禁用变更目录1.9.0 `just` 通常在执行配方时将当前目录设置为包含 `justfile` 的目录,你可以通过 `[no-cd]` 属性来禁用此行为。这可以用来创建使用调用目录相对路径或者对当前目录进行操作的配方。 例如这个 `commit` 配方: ```just [no-cd] commit file: git add {{file}} git commit ``` 可以使用相对于当前目录的路径,因为 `[no-cd]` 可以防止 `just` 在执行 `commit` 配方时改变当前目录。 ### 使用反引号的命令求值 反引号可以用来存储命令的求值结果: ```just localhost := `dumpinterfaces | cut -d: -f2 | sed 's/\/.*//' | sed 's/ //g'` serve: ./serve {{localhost}} 8080 ``` 缩进的反引号,以三个反引号为界,与字符串缩进的方式一样,会被去掉缩进: ````just # This backtick evaluates the command `echo foo\necho bar\n`, which produces the value `foo\nbar\n`. stuff := ``` echo foo echo bar ``` ```` 参见 [字符串](#字符串) 部分,了解去除缩进的细节。 反引号内不能以 `#!` 开头。这种语法是为将来的升级而保留的。 ### 条件表达式 `if` / `else` 表达式评估不同的分支,取决于两个表达式是否评估为相同的值: ```just foo := if "2" == "2" { "Good!" } else { "1984" } bar: @echo "{{foo}}" ``` ```sh $ just bar Good! ``` 也可以用于测试不相等: ```just foo := if "hello" != "goodbye" { "xyz" } else { "abc" } bar: @echo {{foo}} ``` ```sh $ just bar xyz ``` 还支持与正则表达式进行匹配: ```just foo := if "hello" =~ 'hel+o' { "match" } else { "mismatch" } bar: @echo {{foo}} ``` ```sh $ just bar match ``` 正则表达式由 [Regex 包](https://github.com/rust-lang/regex) 提供,其语法在 [docs.rs](https://docs.rs/regex/1.5.4/regex/#syntax) 上有对应文档。由于正则表达式通常使用反斜线转义序列,请考虑使用单引号的字符串字面值,这将使斜线不受干扰地传递给正则分析器。 条件表达式是短路的,这意味着它们只评估其中的一个分支。这可以用来确保反引号内的表达式在不应该运行的时候不会运行。 ```just foo := if env_var("RELEASE") == "true" { `get-something-from-release-database` } else { "dummy-value" } ``` 条件语句也可以在配方中使用: ```just bar foo: echo {{ if foo == "bar" { "hello" } else { "goodbye" } }} ``` 注意最后的 `}` 后面的空格! 没有这个空格,插值将被提前结束。 多个条件语句可以被连起来: ```just foo := if "hello" == "goodbye" { "xyz" } else if "a" == "a" { "abc" } else { "123" } bar: @echo {{foo}} ``` ```sh $ just bar abc ``` ### 出现错误停止执行 可以用 `error` 函数停止执行。比如: ```just foo := if "hello" == "goodbye" { "xyz" } else if "a" == "b" { "abc" } else { error("123") } ``` 在运行时产生以下错误: ``` error: Call to function `error` failed: 123 | 16 | error("123") ``` ### 从命令行设置变量 变量可以从命令行进行覆盖。 ```just os := "linux" test: build ./test --test {{os}} build: ./build {{os}} ``` ```sh $ just ./build linux ./test --test linux ``` 任何数量的 `NAME=VALUE` 形式的参数都可以在配方前传递: ```sh $ just os=plan9 ./build plan9 ./test --test plan9 ``` 或者你可以使用 `--set` 标志: ```sh $ just --set os bsd ./build bsd ./test --test bsd ``` ### 获取和设置环境变量 #### 导出 `just` 变量 以 `export` 关键字为前缀的赋值将作为环境变量导出到配方中: ```just export RUST_BACKTRACE := "1" test: # 如果它崩溃了,将打印一个堆栈追踪 cargo test ``` 以 `$` 为前缀的参数将被作为环境变量导出: ```just test $RUST_BACKTRACE="1": # 如果它崩溃了,将打印一个堆栈追踪 cargo test ``` 导出的变量和参数不会被导出到同一作用域内反引号包裹的表达式里。 ```just export WORLD := "world" # This backtick will fail with "WORLD: unbound variable" BAR := `echo hello $WORLD` ``` ```just # Running `just a foo` will fail with "A: unbound variable" a $A $B=`echo $A`: echo $A $B ``` 当 [export](#导出) 被设置时,所有的 `just` 变量都将作为环境变量被导出。 #### 从环境中获取环境变量 来自环境的环境变量会自动传递给配方: ```just print_home_folder: echo "HOME is: '${HOME}'" ``` ```sh $ just HOME is '/home/myuser' ``` #### 从 `.env` 文件加载环境变量 如果 [dotenv-load](#环境变量加载) 被设置,`just` 将从 `.env` 文件中加载环境变量。该文件中的变量将作为环境变量提供给配方。参见 [环境变量集成](#env-集成) 以获得更多信息。 #### 从环境变量中设置 `just` 变量 环境变量可以通过函数 `env_var()` 和 `env_var_or_default()` 传入到 `just` 变量。 参见 [environment-variables](#环境变量)。 ### 配方参数 配方可以有参数。这里的配方 `build` 有一个参数叫 `target`: ```just build target: @echo 'Building {{target}}…' cd {{target}} && make ``` 要在命令行上传递参数,请把它们放在配方名称后面: ```sh $ just build my-awesome-project Building my-awesome-project… cd my-awesome-project && make ``` 要向依赖配方传递参数,请将依赖配方和参数一起放在括号里: ```just default: (build "main") build target: @echo 'Building {{target}}…' cd {{target}} && make ``` 变量也可以作为参数传递给依赖: ```just target := "main" _build version: @echo 'Building {{version}}…' cd {{version}} && make build: (_build target) ``` 命令的参数可以通过将依赖与参数一起放在括号中的方式传递给依赖: ```just build target: @echo "Building {{target}}…" push target: (build target) @echo 'Pushing {{target}}…' ``` 参数可以有默认值: ```just default := 'all' test target tests=default: @echo 'Testing {{target}}:{{tests}}…' ./test --tests {{tests}} {{target}} ``` 有默认值的参数可以省略: ```sh $ just test server Testing server:all… ./test --tests all server ``` 或者提供: ```sh $ just test server unit Testing server:unit… ./test --tests unit server ``` 默认值可以是任意的表达式,但字符串或路径拼接必须放在括号内: ```just arch := "wasm" test triple=(arch + "-unknown-unknown") input=(arch / "input.dat"): ./test {{triple}} ``` 配方的最后一个参数可以是变长的,在参数名称前用 `+` 或 `*` 表示: ```just backup +FILES: scp {{FILES}} me@server.com: ``` 以 `+` 为前缀的变长参数接受 _一个或多个_ 参数,并展开为一个包含这些参数的字符串,以空格分隔: ```sh $ just backup FAQ.md GRAMMAR.md scp FAQ.md GRAMMAR.md me@server.com: FAQ.md 100% 1831 1.8KB/s 00:00 GRAMMAR.md 100% 1666 1.6KB/s 00:00 ``` 以 `*` 为前缀的变长参数接受 _0个或更多_ 参数,并展开为一个包含这些参数的字符串,以空格分隔,如果没有参数,则为空字符串: ```just commit MESSAGE *FLAGS: git commit {{FLAGS}} -m "{{MESSAGE}}" ``` 变长参数可以被分配默认值。这些参数被命令行上传递的参数所覆盖: ```just test +FLAGS='-q': cargo test {{FLAGS}} ``` `{{…}}` 的替换可能需要加引号,如果它们包含空格。例如,如果你有以下配方: ```just search QUERY: lynx https://www.google.com/?q={{QUERY}} ``` 然后你输入: ```sh $ just search "cat toupee" ``` `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`。 你可以通过添加引号来解决这个问题: ```just search QUERY: lynx 'https://www.google.com/?q={{QUERY}}' ``` 以 `$` 为前缀的参数将被作为环境变量导出: ```just foo $bar: echo $bar ``` ### 在配方的末尾运行配方 一个配方的正常依赖总是在配方开始之前运行。也就是说,被依赖方总是在依赖方之前运行。这些依赖被称为 "前期依赖"。 一个配方也可以有后续的依赖,它们在配方之后运行,用 `&&` 表示: ```just a: echo 'A!' b: a && c d echo 'B!' c: echo 'C!' d: echo 'D!' ``` …运行 _b_ 输出: ```sh $ just b echo 'A!' A! echo 'B!' B! echo 'C!' C! echo 'D!' D! ``` ### 在配方中间运行配方 `just` 不支持在配方的中间运行另一个配方,但你可以在一个配方的中间递归调用 `just`。例如以下 `justfile`: ```just a: echo 'A!' b: a echo 'B start!' just c echo 'B end!' c: echo 'C!' ``` …运行 _b_ 输出: ```sh $ just b echo 'A!' A! echo 'B start!' B start! echo 'C!' C! echo 'B end!' B end! ``` 这有局限性,因为配方 `c` 是以一个全新的 `just` 调用来运行的,赋值将被重新计算,依赖可能会运行两次,命令行参数不会被传入到子 `just` 进程。 ### 用其他语言书写配方 以 `#!` 开头的配方被称为 Shebang 配方,它通过将配方主体保存到文件中并运行它来执行。这让你可以用不同的语言来编写配方: ```just polyglot: python js perl sh ruby nu python: #!/usr/bin/env python3 print('Hello from python!') js: #!/usr/bin/env node console.log('Greetings from JavaScript!') perl: #!/usr/bin/env perl print "Larry Wall says Hi!\n"; sh: #!/usr/bin/env sh hello='Yo' echo "$hello from a shell script!" nu: #!/usr/bin/env nu let hello = 'Hola' echo $"($hello) from a nushell script!" ruby: #!/usr/bin/env ruby puts "Hello from ruby!" ``` ```sh $ just polyglot Hello from python! Greetings from JavaScript! Larry Wall says Hi! Yo from a shell script! Hola from a nushell script! Hello from ruby! ``` 在类似 Unix 的操作系统中,包括 Linux 和 MacOS,Shebang 配方的执行方式是将配方主体保存到临时目录下的一个文件中,将该文件标记为可执行文件,然后执行它。操作系统将 Shebang 行解析为一个命令行并调用它,包括文件的路径。例如,如果一个配方以 `#!/usr/bin/env bash` 开头,操作系统运行的最终命令将是 `/usr/bin/env bash /tmp/PATH_TO_SAVED_RECIPE_BODY` 之类。请记住,不同的操作系统对 Shebang 行的分割方式不同。 Windows 不支持 Shebang 行。在 Windows 上,`just` 将 Shebang 行分割成命令和参数,将配方主体保存到一个文件中,并调用分割后的命令和参数,同时将保存的配方主体的路径作为最后一个参数。 ### 更加安全的 Bash Shebang 配方 如果你正在写一个 `bash` Shebang 配方,考虑加入 `set -euxo pipefail`: ```just foo: #!/usr/bin/env bash set -euxo pipefail hello='Yo' echo "$hello from Bash!" ``` 严格意义上说这不是必须的,但是 `set -euxo pipefail` 开启了一些有用的功能,使 `bash` Shebang 配方的行为更像正常的、行式的 `just` 配方: - `set -e` 使 `bash` 在命令失败时退出。 - `set -u` 使 `bash` 在变量未定义时退出。 - `set -x` 使 `bash` 在运行前打印每一行脚本。 - `set -o pipefail` 使 `bash` 在管道中的一个命令失败时退出。这是 `bash` 特有的,所以在普通的行式 `just` 配方中没有开启。 这些措施共同避免了很多 Shell 脚本的问题。 #### 在 Windows 上执行 Shebang 配方 在 Windows 上,包含 `/` 的 Shebang 解释器路径通过 `cygpath` 从 Unix 风格的路径转换为 Windows 风格的路径,该工具随 [Cygwin](http://www.cygwin.com) 一起提供。 例如,要在 Windows 上执行这个配方: ```just echo: #!/bin/sh echo "Hello!" ``` 解释器路径 `/bin/sh` 在执行前将被 `cygpath` 翻译成 Windows 风格的路径。 如果解释器路径不包含 `/`,它将被执行而不被翻译。这主要用于 `cygpath` 不可用或者你希望向解释器传递一个 Windows 风格的路径的情况下。 ### 在配方中设置变量 配方代码行是由 Shell 解释的,而不是 `just`,所以不可能在配方中设置 `just` 变量: ```mf foo: x := "hello" # This doesn't work! echo {{x}} ``` 使用 Shell 变量是可能的,但还有一个问题:每一行配方都由一个新的 Shell 实例运行,所以在一行中设置的变量不会在下一行中生效: ```just foo: x=hello && echo $x # 这个没问题! y=bye echo $y # 这个是有问题的, `y` 在此处未定义! ``` 解决这个问题的最好方法是使用 Shebang 配方。Shebang 配方体被提取出来并作为脚本运行,所以一个 Shell 实例就可以运行整个配方体: ```just foo: #!/usr/bin/env bash set -euxo pipefail x=hello echo $x ``` ### 在配方之间共享环境变量 每个配方的每一行都由一个新的shell执行,所以不可能在配方之间共享环境变量。 #### 使用 Python 虚拟环境 一些工具,像 [Python 的 venv](https://docs.python.org/3/library/venv.html),需要加载环境变量才能工作,这使得它们在使用 `just` 时具有挑战性。作为一种变通方法,你可以直接执行虚拟环境二进制文件: ```just venv: [ -d foo ] || python3 -m venv foo run: venv ./foo/bin/python3 main.py ``` ### 改变配方中的工作目录 每一行配方都由一个新的 Shell 执行,所以如果你在某一行改变了工作目录,对后面的行不会有影响: ```just foo: pwd # This `pwd` will print the same directory… cd bar pwd # …as this `pwd`! ``` 有几个方法可以解决这个问题。一个是在你想运行的命令的同一行调用 `cd`: ```just foo: cd bar && pwd ``` 另一种方法是使用 Shebang 配方。Shebang 配方体被提取并作为脚本运行,因此一个 Shell 实例将运行整个配方体,所以一行的 `pwd` 改变将影响后面的行,就像一个 Shell 脚本: ```just foo: #!/usr/bin/env bash set -euxo pipefail cd bar pwd ``` ### 缩进 配方代码行可以用空格或制表符缩进,但不能两者混合使用。一个配方的所有行必须有相同的缩进,但同一 `justfile` 中的不同配方可以使用不同的缩进。 ### 多行结构 没有初始 Shebang 的配方会被逐行评估和运行,这意味着多行结构可能不会像你预期的那样工作。 例如对于下面的 `justfile`: ```mf conditional: if true; then echo 'True!' fi ``` 在 `conditional` 配方的第二行前有额外的前导空格,会产生一个解析错误: ```sh $ just conditional error: Recipe line has extra leading whitespace | 3 | echo 'True!' | ^^^^^^^^^^^^^^^^ ``` 为了解决这个问题,你可以在一行上写条件,用斜线转义换行,或者在你的配方中添加一个 Shebang。我们提供了一些多行结构的例子可供参考。 #### `if` 语句 ```just conditional: if true; then echo 'True!'; fi ``` ```just conditional: if true; then \ echo 'True!'; \ fi ``` ```just conditional: #!/usr/bin/env sh if true; then echo 'True!' fi ``` #### `for` 循环 ```just for: for file in `ls .`; do echo $file; done ``` ```just for: for file in `ls .`; do \ echo $file; \ done ``` ```just for: #!/usr/bin/env sh for file in `ls .`; do echo $file done ``` #### `while` 循环 ```just while: while `server-is-dead`; do ping -c 1 server; done ``` ```just while: while `server-is-dead`; do \ ping -c 1 server; \ done ``` ```just while: #!/usr/bin/env sh while `server-is-dead`; do ping -c 1 server done ``` ### 命令行选项 `just` 提供了一些有用的命令行选项,用于列出、Dump 和调试配方以及变量: ```sh $ just --list Available recipes: js perl polyglot python ruby $ just --show perl perl: #!/usr/bin/env perl print "Larry Wall says Hi!\n"; $ just --show polyglot polyglot: python js perl sh ruby ``` 可以通过 `just --help` 命令查看所有选项。 ### 私有配方 名字以 `_` 开头的配方和别名将在 `just --list` 中被忽略: ```just test: _test-helper ./bin/test _test-helper: ./bin/super-secret-test-helper-stuff ``` ```sh $ just --list Available recipes: test ``` `just --summary` 亦然: ```sh $ just --summary test ``` `[private]` 属性1.10.0也可用于隐藏配方,而不需要改变名称: ```just [private] foo: [private] alias b := bar bar: ``` ```sh $ just --list Available recipes: bar ``` 这对那些只作为其他配方的依赖使用的辅助配方很有用。 ### 安静配方 配方名称可在前面加上 `@`,可以在每行反转行首 `@` 的含义: ```just @quiet: echo hello echo goodbye @# all done! ``` 现在只有以 `@` 开头的行才会被回显: ```sh $ j quiet hello goodbye # all done! ``` Shebang 配方默认是安静的: ```just foo: #!/usr/bin/env bash echo 'Foo!' ``` ```sh $ just foo Foo! ``` 在 Shebang 配方名称前面添加 `@`,使 `just` 在执行配方前打印该配方: ```just @bar: #!/usr/bin/env bash echo 'Bar!' ``` ```sh $ just bar #!/usr/bin/env bash echo 'Bar!' Bar! ``` `just` 在配方行失败时通常会打印错误信息,这些错误信息可以通过 `[no-exit-message]`1.7.0 属性来抑制。你可能会发现这在包装工具的配方中特别有用: ```just git *args: @git {{args}} ``` ```sh $ just git status fatal: not a git repository (or any of the parent directories): .git error: Recipe `git` failed on line 2 with exit code 128 ``` 添加属性,当工具以非零代码退出时抑制退出错误信息: ```just [no-exit-message] git *args: @git {{args}} ``` ```sh $ just git status fatal: not a git repository (or any of the parent directories): .git ``` ### 通过交互式选择器选择要运行的配方 `--choose` 子命令可以使 `just` 唤起一个选择器来让您选择要运行的配方。选择器应该从标准输入中读取包含配方名称的行,并将其中一个或多个用空格分隔的名称打印到标准输出。 因为目前没有办法通过 `--choose` 运行一个需要传入参数的配方,所以这样的配方将不会在选择器中列出。另外,私有配方和别名也会被忽略。 选择器可以用 `--chooser` 标志来覆写。如果 `--chooser` 没有给出,那么 `just` 首先检查 `$JUST_CHOOSER` 是否被设置。如果没有,那么将使用默认选择器 `fzf`,这是一个流行的模糊查找器。 参数可以包含在选择器中,例如:`fzf --exact`。 选择器的调用方式与配方行的调用方式相同。例如,如果选择器是 `fzf`,它将被通过 `sh -cu 'fzf'` 调用,如果 Shell 或 Shell 参数被覆写,选择器的调用将尊重这些覆写。 如果你希望 `just` 默认用选择器来选择配方,你可以用这个作为你的默认配方: ```just default: @just --choose ``` ### 在其他目录下调用 `justfile` 如果传递给 `just` 的第一个参数包含 `/`,那么就会发生以下情况: 1. 参数在最后的 `/` 处被分割; 2. 最后一个 `/` 之前的部分将被视为一个目录。`just` 将从这里开始搜索 `justfile`,而不是在当前目录下; 3. 最后一个斜线之后的部分被视为正常参数,如果是空的,则被忽略; 这可能看起来有点奇怪,但如果你想在一个子目录下的 `justfile` 中运行一个命令,这很有用。 例如,如果你在一个目录中,该目录包含一个名为 `foo` 的子目录,该目录包含一个 `justfile`,其配方为 `build`,也是默认的配方,以下都是等同的: ```sh $ (cd foo && just build) $ just foo/build $ just foo/ ``` ### 隐藏 `justfile` `just` 会寻找名为 `justfile` 和 `.justfile` 的 `justfile`,因此你也可以使用隐藏的 `justfile`(即 `.justfile`)。 ### Just 脚本 通过在 `justfile` 的顶部添加 Shebang 行并使其可执行,`just` 可以作为脚本的解释器使用: ```sh $ cat > script < just.zsh ``` *macOS 注意:* 最近版本的 macOS 使用 zsh 作为默认的 Shell。如果你使用 Homebrew 安装 `just`,它会自动安装 zsh 补全脚本的最新副本到 Homebrew zsh 目录下,而内置默认版本的 zsh 是不知道的。如果可能的话,最好使用这个脚本副本,因为当你通过 Homebrew 更新 `just` 时,它也会被更新。另外,许多其他的 Homebrew 软件包也使用相同位置的补全脚本,而内置的 zsh 也不知道这些。为了在这种情况下在 zsh 中使用 `just` 的补全,你可以在调用 `compinit` 之前将 `fpath` 设置为 Homebrew 的位置。还要注意,Oh My Zsh 默认会运行 `compinit`,所以你的 `.zshrc` 文件看起来像这样: ```zsh # 启动Homebrew,添加环境变量 eval "$(brew shellenv)" fpath=($HOMEBREW_PREFIX/share/zsh/site-functions $fpath) # 然后从这些选项中选择一个: # 1. 如果你使用的是 Oh My Zsh,你可以在这里初始化它 # source $ZSH/oh-my-zsh.sh # 2. 否则就自己运行 compinit # autoload -U compinit # compinit ``` ### 语法 在 [GRAMMAR.md](https://github.com/casey/just/blob/master/GRAMMAR.md) 中可以找到一个非正式的 `justfile` 语法说明。 ### just.sh 在 `just` 成为一个精致的 Rust 程序之前,它是一个很小的 Shell 脚本,叫 `make`。你可以在 [extras/just.sh](https://github.com/casey/just/blob/master/extras/just.sh) 中找到旧版本。 ### 用户 `justfile` 如果你想让一些配方在任何地方都能使用,你有几个选择。 首先,在 `~/.user.justfile` 中创建一个带有一些配方的 `justfile`。 #### 配方别名 如果你想通过名称来调用 `~/.user.justfile` 中的配方,并且不介意为每个配方创建一个别名,可以在你的 Shell 初始化脚本中加入以下内容: ```sh for recipe in `just --justfile ~/.user.justfile --summary`; do alias $recipe="just --justfile ~/.user.justfile --working-directory . $recipe" done ``` 现在,如果你在 `~/.user.justfile` 里有一个叫 `foo` 的配方,你可以在命令行输入 `foo` 来运行它。 我花了很长时间才意识到你可以像这样创建配方别名。尽管有点迟,但我很高兴给你带来这个 `justfile` 技术的重大进步。 #### 别名转发 如果你不想为每个配方创建别名,你可以创建一个别名: ```sh alias .j='just --justfile ~/.user.justfile --working-directory .' ``` 现在,如果你在 `~/.user.justfile` 里有一个叫 `foo` 的配方,你可以在命令行输入 `.j foo` 来运行它。 我很确定没有人真正使用这个功能,但它确实存在。 ¯\\\_(ツ)\_/¯ #### 定制化 你可以用额外的选项来定制上述别名。例如,如果你想让你的 `justfile` 中的配方在你的主目录中运行,而不是在当前目录中运行: ```sh alias .j='just --justfile ~/.user.justfile --working-directory ~' ``` ### Node.js `package.json` 脚本兼容性 下面的导出语句使 `just` 配方能够访问本地 Node 模块二进制文件,并使 `just` 配方命令的行为更像 Node.js `package.json` 文件中的 `script` 条目: ```just export PATH := "./node_modules/.bin:" + env_var('PATH') ``` ### 替代方案 现在并不缺少命令运行器!在这里,有一些或多或少比较类似于 `just` 的替代方案,包括: - [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/)。 - [task](https://github.com/go-task/task): 一个用 Go 编写的基于 YAML 的命令运行器。 - [maid](https://github.com/egoist/maid): 一个用 JavaScript 编写的基于 Markdown 的命令运行器。 - [microsoft/just](https://github.com/microsoft/just): 一个用 JavaScript 编写的基于 JavasScript 的命令运行器。 - [cargo-make](https://github.com/sagiegurari/cargo-make): 一个用于 Rust 项目的命令运行器。 - [mmake](https://github.com/tj/mmake): 一个针对 `make` 的包装器,有很多改进,包括远程包含。 - [robo](https://github.com/tj/robo): 一个用 Go 编写的基于 YAML 的命令运行器。 - [mask](https://github.com/jakedeichert/mask): 一个用 Rust 编写的基于 Markdown 的命令运行器。 - [makesure](https://github.com/xonixx/makesure): 一个用 AWK 和 Shell 编写的简单而便携的命令运行器。 - [haku](https://github.com/VladimirMarkelov/haku): 一个用 Rust 编写的类似 make 的命令运行器。 贡献 ------------ `just` 欢迎你的贡献! `just` 是在最大许可的 [CC0](https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt) 公共领域奉献和后备许可下发布的,所以你的修改也必须在这个许可下发布。 ### Janus [Janus](https://github.com/casey/janus) 是一个收集和分析 `justfile` 的工具,可以确定新版本的 `just` 是否会破坏或改变现有 `justfile` 的解析。 在合并一个特别大的或可怕的变化之前,应该运行 `Janus` 以确保没有任何破坏。不要担心自己运行 `Janus`,Casey 会很乐意在需要时为你运行它。 ### 最小支持的 Rust 版本 最低支持的 Rust 版本,或 MSRV,是当前稳定的(current stable) Rust。它可能可以在旧版本的 Rust 上构建,但这并不保证。 ### 新版本 `just` 会经常发布新版本,以便用户快速获得新功能。 发布的提交信息使用如下模板: ``` Release x.y.z - Bump version: x.y.z → x.y.z - Update changelog - Update changelog contributor credits - Update dependencies - Update man page - Update version references in readme ``` 常见问题 -------------------------- ### Just 避免了 Make 的哪些特异性? `make` 有一些行为令人感到困惑、复杂,或者使它不适合作为通用的命令运行器。 一个例子是,在某些情况下,`make` 不会实际运行配方中的命令。例如,如果你有一个名为 `test` 的文件和以下 makefile: ```just test: ./test ``` `make` 将会拒绝运行你的测试: ```sh $ make test make: `test' is up to date. ``` `make` 假定 `test` 配方产生一个名为 `test` 的文件。由于这个文件已经存在,而且由于配方没有其他依赖,`make` 认为它没有任何事情可做并退出。 公平地说,当把 `make` 作为一个构建系统时,这种行为是可取的,但当把它作为一个命令运行器时就不可取了。你可以使用 `make` 内置的 [`.PHONY` 目标名称](https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html) 为特定的目标禁用这种行为,但其语法很冗长,而且很难记住。明确的虚假目标列表与配方定义分开写,也带来了意外定义新的非虚假目标的风险。在 `just` 中,所有的配方都被当作是虚假的。 其他 `make` 特异行为的例子包括赋值中 `=` 和 `:=` 的区别;如果你弄乱了你的 makefile,将会产生混乱的错误信息;需要 `$$` 在配方中使用环境变量;以及不同口味的 `make` 之间的不相容性。 ### Just 和 Cargo 构建脚本之间有什么关系? [`cargo` 构建脚本](http://doc.crates.io/build-script.html) 有一个相当特定的用途,就是控制 `cargo` 如何构建你的 Rust 项目。这可能包括给 `rustc` 调用添加标志,构建外部依赖,或运行某种 codegen 步骤。 另一方面,`just` 是用于你可能在开发中会运行的所有其他的杂项命令。比如在不同的配置下运行测试,对代码进行检查,将构建的产出推送到服务器,删除临时文件,等等。 另外,尽管 `just` 是用 Rust 编写的,但它可以被用于任何语言或项目使用的构建系统。 进一步漫谈 ----------------- 我个人认为为几乎每个项目写一个 `justfile` 非常有用,无论大小。 在一个有多个贡献者的大项目中,有一个包含项目工作所需的所有命令的文件是非常有用的,这样所有命令唾手可得。 可能有不同的命令来测试、构建、检查、部署等等,把它们都放在一个地方是很方便的,可以减少你花在告诉人们要运行哪些命令和如何输入这些命令的时间。 而且,有了一个容易放置命令的地方,你很可能会想出其他有用的东西,这些东西是项目集体智慧的一部分,但没有写在任何地方,比如修订控制工作流程的某些部分需要的神秘命令,安装你项目的所有依赖,或者所有你可能需要传递给构建系统的任意标志等。 一些关于配方的想法: - 部署/发布项目 - 在发布模式与调试模式下进行构建 - 在调试模式下运行或启用日志记录功能 - 复杂的 git 工作流程 - 更新依赖 - 运行不同的测试集,例如快速测试与慢速测试,或以更多输出模式运行它们 - 任何复杂的命令集,你真的应该写下来,如果只是为了能够记住它们的话 即使是小型的个人项目,能够通过名字记住命令,而不是通过 ^Reverse 搜索你的 Shell 历史,这也是一个巨大的福音,能够进入一个用任意语言编写的旧项目,并知道你需要用到的所有命令都在 `justfile` 中,如果你输入 `just`,就可能会输出一些有用的(或至少是有趣的!)信息。 关于配方的想法,请查看 [这个项目的 `justfile`](https://github.com/casey/just/blob/master/justfile),或一些 [在其他项目里](https://github.com/search?q=path%3A**%2Fjustfile&type=code) 的 `justfile`。 总之,我想这个令人难以置信地啰嗦的 README 就到此为止了。 我希望你喜欢使用 `just`,并在你所有的计算工作中找到巨大的成功和满足! 😸 just-1.21.0/Vagrantfile000064400000000000000000000010001046102023000130460ustar 00000000000000Vagrant.configure(2) do |config| config.vm.box = 'debian/jessie64' config.vm.provision "shell", inline: <<-EOS apt-get -y update apt-get install -y clang git vim curl EOS config.vm.provision "shell", privileged: false, inline: <<-EOS curl https://sh.rustup.rs -sSf > install-rustup chmod +x install-rustup ./install-rustup -y source ~/.cargo/env rustup target add x86_64-unknown-linux-musl cargo install -f just git clone https://github.com/casey/just.git EOS end just-1.21.0/bin/forbid000075500000000000000000000002241046102023000126330ustar 00000000000000#!/usr/bin/env bash set -euxo pipefail which rg ! rg \ --glob !bin/forbid \ --glob !CHANGELOG.md \ --ignore-case \ 'dbg!|fixme|todo|xxx' just-1.21.0/bin/generate-completions000075500000000000000000000003001046102023000155050ustar 00000000000000#!/usr/bin/env bash set -euxo pipefail for script in completions/*; do shell=${script##*.} if [ $shell == nu ]; then continue fi cargo run -- --completions $shell > $script done just-1.21.0/bin/package000075500000000000000000000017351046102023000127710ustar 00000000000000#!/usr/bin/env bash set -euxo pipefail VERSION=${REF#"refs/tags/"} DIST=`pwd`/dist echo "Packaging just $VERSION for $TARGET..." test -f Cargo.lock || cargo generate-lockfile echo "Building just..." RUSTFLAGS="--deny warnings --codegen target-feature=+crt-static $TARGET_RUSTFLAGS" \ cargo build --bin just --target $TARGET --release EXECUTABLE=target/$TARGET/release/just if [[ $OS == windows-latest ]]; then EXECUTABLE=$EXECUTABLE.exe fi echo "Copying release files..." mkdir dist cp \ $EXECUTABLE \ Cargo.lock \ Cargo.toml \ GRAMMAR.md \ LICENSE \ README.md \ man/just.1 \ $DIST cd $DIST echo "Creating release archive..." case $OS in ubuntu-latest | macos-latest) ARCHIVE=$DIST/just-$VERSION-$TARGET.tar.gz tar czf $ARCHIVE * echo "::set-output name=archive::$ARCHIVE" ;; windows-latest) ARCHIVE=$DIST/just-$VERSION-$TARGET.zip 7z a $ARCHIVE * echo "::set-output name=archive::`pwd -W`/just-$VERSION-$TARGET.zip" ;; esac just-1.21.0/clippy.toml000064400000000000000000000000461046102023000130670ustar 00000000000000cognitive-complexity-threshold = 1337 just-1.21.0/completions/just.bash000064400000000000000000000113301046102023000150500ustar 00000000000000_just() { local i cur prev opts cmds COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" cmd="" opts="" for i in ${COMP_WORDS[@]} do case "${i}" in "$1") cmd="just" ;; *) ;; esac done case "${cmd}" in just) opts=" -n -q -u -v -e -l -h -V -f -d -c -s --check --yes --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --unstable --verbose --changelog --choose --dump --edit --evaluate --fmt --init --list --summary --variables --help --version --chooser --color --command-color --dump-format --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show --dotenv-filename --dotenv-path ... " if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 elif [[ ${COMP_CWORD} -eq 1 ]]; then local recipes=$(just --summary 2> /dev/null) if echo "${cur}" | \grep -qF '/'; then local path_prefix=$(echo "${cur}" | sed 's/[/][^/]*$/\//') local recipes=$(just --summary 2> /dev/null -- "${path_prefix}") local recipes=$(printf "${path_prefix}%s\t" $recipes) fi if [[ $? -eq 0 ]]; then COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") ) return 0 fi fi case "${prev}" in --chooser) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --color) COMPREPLY=($(compgen -W "auto always never" -- "${cur}")) return 0 ;; --command-color) COMPREPLY=($(compgen -W "black blue cyan green purple red yellow" -- "${cur}")) return 0 ;; --dump-format) COMPREPLY=($(compgen -W "just json" -- "${cur}")) return 0 ;; --list-heading) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --list-prefix) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --justfile) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -f) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --set) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --shell) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --shell-arg) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --working-directory) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -d) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --command) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -c) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --completions) COMPREPLY=($(compgen -W "zsh bash fish powershell elvish" -- "${cur}")) return 0 ;; --show) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -s) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --dotenv-filename) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --dotenv-path) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; *) COMPREPLY=() ;; esac COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; esac } complete -F _just -o bashdefault -o default just just-1.21.0/completions/just.elvish000064400000000000000000000102571046102023000154340ustar 00000000000000edit:completion:arg-completer[just] = [@words]{ fn spaces [n]{ repeat $n ' ' | joins '' } fn cand [text desc]{ edit:complex-candidate $text &display-suffix=' '(spaces (- 14 (wcswidth $text)))$desc } command = 'just' for word $words[1:-1] { if (has-prefix $word '-') { break } command = $command';'$word } completions = [ &'just'= { cand --chooser 'Override binary invoked by `--choose`' cand --color 'Print colorful output' cand --command-color 'Echo recipe lines in ' cand --dump-format 'Dump justfile as ' cand --list-heading 'Print before list' cand --list-prefix 'Print before each list item' cand -f 'Use as justfile' cand --justfile 'Use as justfile' cand --set 'Override with ' cand --shell 'Invoke to run recipes' cand --shell-arg 'Invoke shell with as an argument' cand -d 'Use as working directory. --justfile must also be set' cand --working-directory 'Use as working directory. --justfile must also be set' cand -c 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set' cand --command 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set' cand --completions 'Print shell completion script for ' cand -s 'Show information about ' cand --show 'Show information about ' cand --dotenv-filename 'Search for environment file named instead of `.env`' cand --dotenv-path 'Load environment file at instead of searching for one' 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.' cand --yes 'Automatically confirm all recipes.' cand -n 'Print what just would do without doing it' cand --dry-run 'Print what just would do without doing it' cand --highlight 'Highlight echoed recipe lines in bold' cand --no-dotenv 'Don''t load `.env` file' cand --no-highlight 'Don''t highlight echoed recipe lines in bold' cand -q 'Suppress all output' cand --quiet 'Suppress all output' cand --shell-command 'Invoke with the shell used to run recipe lines and backticks' cand --clear-shell-args 'Clear shell arguments' cand -u 'Return list and summary entries in source order' cand --unsorted 'Return list and summary entries in source order' cand --unstable 'Enable unstable features' cand -v 'Use verbose output' cand --verbose 'Use verbose output' cand --changelog 'Print changelog' 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`' cand --dump 'Print justfile' cand -e 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`' cand --edit 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`' cand --evaluate 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable''s value.' cand --fmt 'Format and overwrite justfile' cand --init 'Initialize new justfile in project root' cand -l 'List available recipes and their arguments' cand --list 'List available recipes and their arguments' cand --summary 'List names of available recipes' cand --variables 'List names of variables' cand -h 'Print help information' cand --help 'Print help information' cand -V 'Print version information' cand --version 'Print version information' } ] $completions[$command] } just-1.21.0/completions/just.fish000064400000000000000000000124371046102023000150750ustar 00000000000000function __fish_just_complete_recipes just --list 2> /dev/null | tail -n +2 | awk '{ command = $1; args = $0; desc = ""; delim = ""; sub(/^[[:space:]]*[^[:space:]]*/, "", args); gsub(/^[[:space:]]+|[[:space:]]+$/, "", args); if (match(args, /#.*/)) { desc = substr(args, RSTART+2, RLENGTH); args = substr(args, 0, RSTART-1); gsub(/^[[:space:]]+|[[:space:]]+$/, "", args); } gsub(/\+|=[`\'"][^`\'"]*[`\'"]/, "", args); gsub(/ /, ",", args); if (args != ""){ args = "Args: " args; } if (args != "" && desc != "") { delim = "; "; } print command "\t" args delim desc }' end # don't suggest files right off complete -c just -n "__fish_is_first_arg" --no-files # complete recipes complete -c just -a '(__fish_just_complete_recipes)' # autogenerated completions complete -c just -n "__fish_use_subcommand" -l chooser -d 'Override binary invoked by `--choose`' complete -c just -n "__fish_use_subcommand" -l color -d 'Print colorful output' -r -f -a "auto always never" complete -c just -n "__fish_use_subcommand" -l command-color -d 'Echo recipe lines in ' -r -f -a "black blue cyan green purple red yellow" complete -c just -n "__fish_use_subcommand" -l dump-format -d 'Dump justfile as ' -r -f -a "just json" complete -c just -n "__fish_use_subcommand" -l list-heading -d 'Print before list' complete -c just -n "__fish_use_subcommand" -l list-prefix -d 'Print before each list item' complete -c just -n "__fish_use_subcommand" -s f -l justfile -d 'Use as justfile' complete -c just -n "__fish_use_subcommand" -l set -d 'Override with ' complete -c just -n "__fish_use_subcommand" -l shell -d 'Invoke to run recipes' complete -c just -n "__fish_use_subcommand" -l shell-arg -d 'Invoke shell with as an argument' complete -c just -n "__fish_use_subcommand" -s d -l working-directory -d 'Use as working directory. --justfile must also be set' complete -c just -n "__fish_use_subcommand" -s c -l command -d 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set' complete -c just -n "__fish_use_subcommand" -l completions -d 'Print shell completion script for ' -r -f -a "zsh bash fish powershell elvish" complete -c just -n "__fish_use_subcommand" -s s -l show -d 'Show information about ' complete -c just -n "__fish_use_subcommand" -l dotenv-filename -d 'Search for environment file named instead of `.env`' complete -c just -n "__fish_use_subcommand" -l dotenv-path -d 'Load environment file at instead of searching for one' complete -c just -n "__fish_use_subcommand" -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.' complete -c just -n "__fish_use_subcommand" -l yes -d 'Automatically confirm all recipes.' complete -c just -n "__fish_use_subcommand" -s n -l dry-run -d 'Print what just would do without doing it' complete -c just -n "__fish_use_subcommand" -l highlight -d 'Highlight echoed recipe lines in bold' complete -c just -n "__fish_use_subcommand" -l no-dotenv -d 'Don\'t load `.env` file' complete -c just -n "__fish_use_subcommand" -l no-highlight -d 'Don\'t highlight echoed recipe lines in bold' complete -c just -n "__fish_use_subcommand" -s q -l quiet -d 'Suppress all output' complete -c just -n "__fish_use_subcommand" -l shell-command -d 'Invoke with the shell used to run recipe lines and backticks' complete -c just -n "__fish_use_subcommand" -l clear-shell-args -d 'Clear shell arguments' complete -c just -n "__fish_use_subcommand" -s u -l unsorted -d 'Return list and summary entries in source order' complete -c just -n "__fish_use_subcommand" -l unstable -d 'Enable unstable features' complete -c just -n "__fish_use_subcommand" -s v -l verbose -d 'Use verbose output' complete -c just -n "__fish_use_subcommand" -l changelog -d 'Print changelog' complete -c just -n "__fish_use_subcommand" -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`' complete -c just -n "__fish_use_subcommand" -l dump -d 'Print justfile' complete -c just -n "__fish_use_subcommand" -s e -l edit -d 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`' complete -c just -n "__fish_use_subcommand" -l evaluate -d 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable\'s value.' complete -c just -n "__fish_use_subcommand" -l fmt -d 'Format and overwrite justfile' complete -c just -n "__fish_use_subcommand" -l init -d 'Initialize new justfile in project root' complete -c just -n "__fish_use_subcommand" -s l -l list -d 'List available recipes and their arguments' complete -c just -n "__fish_use_subcommand" -l summary -d 'List names of available recipes' complete -c just -n "__fish_use_subcommand" -l variables -d 'List names of variables' complete -c just -n "__fish_use_subcommand" -s h -l help -d 'Print help information' complete -c just -n "__fish_use_subcommand" -s V -l version -d 'Print version information' just-1.21.0/completions/just.nu000064400000000000000000000005671046102023000145670ustar 00000000000000def "nu-complete just" [] { (^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 } # Just: A Command Runner export extern "just" [ ...recipe: string@"nu-complete just", # Recipe(s) to run, may be with argument(s) ] just-1.21.0/completions/just.powershell000064400000000000000000000217721046102023000163320ustar 00000000000000using namespace System.Management.Automation using namespace System.Management.Automation.Language Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) $commandElements = $commandAst.CommandElements $command = @( 'just' for ($i = 1; $i -lt $commandElements.Count; $i++) { $element = $commandElements[$i] if ($element -isnot [StringConstantExpressionAst] -or $element.StringConstantType -ne [StringConstantType]::BareWord -or $element.Value.StartsWith('-')) { break } $element.Value }) -join ';' $completions = @(switch ($command) { 'just' { [CompletionResult]::new('--chooser', 'chooser', [CompletionResultType]::ParameterName, 'Override binary invoked by `--choose`') [CompletionResult]::new('--color', 'color', [CompletionResultType]::ParameterName, 'Print colorful output') [CompletionResult]::new('--command-color', 'command-color', [CompletionResultType]::ParameterName, 'Echo recipe lines in ') [CompletionResult]::new('--dump-format', 'dump-format', [CompletionResultType]::ParameterName, 'Dump justfile as ') [CompletionResult]::new('--list-heading', 'list-heading', [CompletionResultType]::ParameterName, 'Print before list') [CompletionResult]::new('--list-prefix', 'list-prefix', [CompletionResultType]::ParameterName, 'Print before each list item') [CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Use as justfile') [CompletionResult]::new('--justfile', 'justfile', [CompletionResultType]::ParameterName, 'Use as justfile') [CompletionResult]::new('--set', 'set', [CompletionResultType]::ParameterName, 'Override with ') [CompletionResult]::new('--shell', 'shell', [CompletionResultType]::ParameterName, 'Invoke to run recipes') [CompletionResult]::new('--shell-arg', 'shell-arg', [CompletionResultType]::ParameterName, 'Invoke shell with as an argument') [CompletionResult]::new('-d', 'd', [CompletionResultType]::ParameterName, 'Use as working directory. --justfile must also be set') [CompletionResult]::new('--working-directory', 'working-directory', [CompletionResultType]::ParameterName, 'Use as working directory. --justfile must also be set') [CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set') [CompletionResult]::new('--command', 'command', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set') [CompletionResult]::new('--completions', 'completions', [CompletionResultType]::ParameterName, 'Print shell completion script for ') [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Show information about ') [CompletionResult]::new('--show', 'show', [CompletionResultType]::ParameterName, 'Show information about ') [CompletionResult]::new('--dotenv-filename', 'dotenv-filename', [CompletionResultType]::ParameterName, 'Search for environment file named instead of `.env`') [CompletionResult]::new('--dotenv-path', 'dotenv-path', [CompletionResultType]::ParameterName, 'Load environment file at instead of searching for one') [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.') [CompletionResult]::new('--yes', 'yes', [CompletionResultType]::ParameterName, 'Automatically confirm all recipes.') [CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Print what just would do without doing it') [CompletionResult]::new('--dry-run', 'dry-run', [CompletionResultType]::ParameterName, 'Print what just would do without doing it') [CompletionResult]::new('--highlight', 'highlight', [CompletionResultType]::ParameterName, 'Highlight echoed recipe lines in bold') [CompletionResult]::new('--no-dotenv', 'no-dotenv', [CompletionResultType]::ParameterName, 'Don''t load `.env` file') [CompletionResult]::new('--no-highlight', 'no-highlight', [CompletionResultType]::ParameterName, 'Don''t highlight echoed recipe lines in bold') [CompletionResult]::new('-q', 'q', [CompletionResultType]::ParameterName, 'Suppress all output') [CompletionResult]::new('--quiet', 'quiet', [CompletionResultType]::ParameterName, 'Suppress all output') [CompletionResult]::new('--shell-command', 'shell-command', [CompletionResultType]::ParameterName, 'Invoke with the shell used to run recipe lines and backticks') [CompletionResult]::new('--clear-shell-args', 'clear-shell-args', [CompletionResultType]::ParameterName, 'Clear shell arguments') [CompletionResult]::new('-u', 'u', [CompletionResultType]::ParameterName, 'Return list and summary entries in source order') [CompletionResult]::new('--unsorted', 'unsorted', [CompletionResultType]::ParameterName, 'Return list and summary entries in source order') [CompletionResult]::new('--unstable', 'unstable', [CompletionResultType]::ParameterName, 'Enable unstable features') [CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Use verbose output') [CompletionResult]::new('--verbose', 'verbose', [CompletionResultType]::ParameterName, 'Use verbose output') [CompletionResult]::new('--changelog', 'changelog', [CompletionResultType]::ParameterName, 'Print changelog') [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`') [CompletionResult]::new('--dump', 'dump', [CompletionResultType]::ParameterName, 'Print justfile') [CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`') [CompletionResult]::new('--edit', 'edit', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`') [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.') [CompletionResult]::new('--fmt', 'fmt', [CompletionResultType]::ParameterName, 'Format and overwrite justfile') [CompletionResult]::new('--init', 'init', [CompletionResultType]::ParameterName, 'Initialize new justfile in project root') [CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List available recipes and their arguments') [CompletionResult]::new('--list', 'list', [CompletionResultType]::ParameterName, 'List available recipes and their arguments') [CompletionResult]::new('--summary', 'summary', [CompletionResultType]::ParameterName, 'List names of available recipes') [CompletionResult]::new('--variables', 'variables', [CompletionResultType]::ParameterName, 'List names of variables') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') break } }) function Get-JustFileRecipes([string[]]$CommandElements) { $justFileIndex = $commandElements.IndexOf("--justfile"); if ($justFileIndex -ne -1 && $justFileIndex + 1 -le $commandElements.Length) { $justFileLocation = $commandElements[$justFileIndex + 1] } $justArgs = @("--summary") if (Test-Path $justFileLocation) { $justArgs += @("--justfile", $justFileLocation) } $recipes = $(just @justArgs) -split ' ' return $recipes | ForEach-Object { [CompletionResult]::new($_) } } $elementValues = $commandElements | Select-Object -ExpandProperty Value $recipes = Get-JustFileRecipes -CommandElements $elementValues $completions += $recipes $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | Sort-Object -Property ListItemText } just-1.21.0/completions/just.zsh000064400000000000000000000136511046102023000147470ustar 00000000000000#compdef just autoload -U is-at-least _just() { typeset -A opt_args typeset -a _arguments_options local ret=1 if is-at-least 5.2; then _arguments_options=(-s -S -C) else _arguments_options=(-s -C) fi local context curcontext="$curcontext" state line local common=( '--chooser=[Override binary invoked by `--choose`]' \ '--color=[Print colorful output]: :(auto always never)' \ '--command-color=[Echo recipe lines in ]: :(black blue cyan green purple red yellow)' \ '--dump-format=[Dump justfile as ]: :(just json)' \ '--list-heading=[Print before list]' \ '--list-prefix=[Print before each list item]' \ '-f+[Use as justfile]' \ '--justfile=[Use as justfile]' \ '*--set[Override with ]: :_just_variables' \ '--shell=[Invoke to run recipes]' \ '*--shell-arg=[Invoke shell with as an argument]' \ '-d+[Use as working directory. --justfile must also be set]' \ '--working-directory=[Use as working directory. --justfile must also be set]' \ '-c+[Run an arbitrary command with the working directory, `.env`, overrides, and exports set]' \ '--command=[Run an arbitrary command with the working directory, `.env`, overrides, and exports set]' \ '--completions=[Print shell completion script for ]: :(zsh bash fish powershell elvish)' \ '-s+[Show information about ]: :_just_commands' \ '--show=[Show information about ]: :_just_commands' \ '(--dotenv-path)--dotenv-filename=[Search for environment file named instead of `.env`]' \ '--dotenv-path=[Load environment file at instead of searching for one]' \ '--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.]' \ '--yes[Automatically confirm all recipes.]' \ '(-q --quiet)-n[Print what just would do without doing it]' \ '(-q --quiet)--dry-run[Print what just would do without doing it]' \ '--highlight[Highlight echoed recipe lines in bold]' \ '--no-dotenv[Don'\''t load `.env` file]' \ '--no-highlight[Don'\''t highlight echoed recipe lines in bold]' \ '(-n --dry-run)-q[Suppress all output]' \ '(-n --dry-run)--quiet[Suppress all output]' \ '--shell-command[Invoke with the shell used to run recipe lines and backticks]' \ '--clear-shell-args[Clear shell arguments]' \ '-u[Return list and summary entries in source order]' \ '--unsorted[Return list and summary entries in source order]' \ '--unstable[Enable unstable features]' \ '*-v[Use verbose output]' \ '*--verbose[Use verbose output]' \ '--changelog[Print changelog]' \ '--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`]' \ '--dump[Print justfile]' \ '-e[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \ '--edit[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \ '--evaluate[Evaluate and print all variables. If a variable name is given as an argument, only print that variable'\''s value.]' \ '--fmt[Format and overwrite justfile]' \ '--init[Initialize new justfile in project root]' \ '-l[List available recipes and their arguments]' \ '--list[List available recipes and their arguments]' \ '--summary[List names of available recipes]' \ '--variables[List names of variables]' \ '-h[Print help information]' \ '--help[Print help information]' \ '-V[Print version information]' \ '--version[Print version information]' \ ) _arguments "${_arguments_options[@]}" $common \ '1: :_just_commands' \ '*: :->args' \ && ret=0 case $state in args) curcontext="${curcontext%:*}-${words[2]}:" local lastarg=${words[${#words}]} local recipe local cmds; cmds=( ${(s: :)$(_call_program commands just --summary)} ) # Find first recipe name for ((i = 2; i < $#words; i++ )) do if [[ ${cmds[(I)${words[i]}]} -gt 0 ]]; then recipe=${words[i]} break fi done if [[ $lastarg = */* ]]; then # Arguments contain slash would be recognised as a file _arguments -s -S $common '*:: :_files' elif [[ $lastarg = *=* ]]; then # Arguments contain equal would be recognised as a variable _message "value" elif [[ $recipe ]]; then # Show usage message _message "`just --show $recipe`" # Or complete with other commands #_arguments -s -S $common '*:: :_just_commands' else _arguments -s -S $common '*:: :_just_commands' fi ;; esac return ret } (( $+functions[_just_commands] )) || _just_commands() { [[ $PREFIX = -* ]] && return 1 integer ret=1 local variables; variables=( ${(s: :)$(_call_program commands just --variables)} ) local commands; commands=( ${${${(M)"${(f)$(_call_program commands just --list)}":# *}/ ##/}/ ##/:Args: } ) if compset -P '*='; then case "${${words[-1]%=*}#*=}" in *) _message 'value' && ret=0 ;; esac else _describe -t variables 'variables' variables -qS "=" && ret=0 _describe -t commands 'just commands' commands "$@" fi } (( $+functions[_just_variables] )) || _just_variables() { [[ $PREFIX = -* ]] && return 1 integer ret=1 local variables; variables=( ${(s: :)$(_call_program commands just --variables)} ) if compset -P '*='; then case "${${words[-1]%=*}#*=}" in *) _message 'value' && ret=0 ;; esac else _describe -t variables 'variables' variables && ret=0 fi return ret } _just "$@" just-1.21.0/crates-io-readme.md000064400000000000000000000010731046102023000143360ustar 00000000000000`just` is a handy way to save and run project-specific commands. Commands are stored in a file called `justfile` or `Justfile` with syntax inspired by `make`: ```make build: cc *.c -o main # test everything test-all: build ./test --all # run a specific test test TEST: build ./test --test {{TEST}} ``` `just` produces detailed error messages and avoids `make`'s idiosyncrasies, so debugging a justfile is easier and less surprising than debugging a makefile. It works on Linux, MacOS, and Windows. Read more on [GitHub](https://github.com/casey/just). just-1.21.0/examples/cross-platform.just000064400000000000000000000014341046102023000163740ustar 00000000000000# use with https://github.com/casey/just # # Example cross-platform Python project # python_dir := if os_family() == "windows" { "./.venv/Scripts" } else { "./.venv/bin" } python := python_dir + if os_family() == "windows" { "/python.exe" } else { "/python3" } system_python := if os_family() == "windows" { "py.exe -3.9" } else { "python3.9" } # Set up development environment bootstrap: if test ! -e .venv; then {{ system_python }} -m venv .venv; fi {{ python }} -m pip install --upgrade pip wheel pip-tools {{ python_dir }}/pip-sync # Upgrade Python dependencies upgrade-deps: && bootstrap {{ python_dir }}/pip-compile --upgrade # Sample project script 1 script1: {{ python }} script1.py # Sample project script 2 script2 *ARGS: {{ python }} script2.py {{ ARGS }} just-1.21.0/examples/keybase.just000064400000000000000000000005441046102023000150450ustar 00000000000000# use with https://github.com/casey/just # Be inspired to use just to notify a chat # channel, this examples shows use with keybase # since it - practically - authenticates at the # device level and needs no additional secrets # notify update in keybase notify m="": keybase chat send --topic-type "chat" --channel "upd(): {{m}}" just-1.21.0/examples/kitchen-sink.just000064400000000000000000000105621046102023000160120ustar 00000000000000set shell := ["sh", "-c"] set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] set allow-duplicate-recipes set positional-arguments set dotenv-load set export alias s := serve bt := '0' export RUST_BACKTRACE_1 := bt log := "warn" export JUST_LOG := (log + "ing" + `grep loop /etc/networks | cut -f2`) tmpdir := `mktemp` version := "0.2.7" tardir := tmpdir / "awesomesauce-" + version foo1 := / "tmp" foo2_3 := "a/" tarball := tardir + ".tar.gz" export RUST_BACKTRACE_2 := "1" string-with-tab := "\t" string-with-newline := "\n" string-with-carriage-return := "\r" string-with-double-quote := "\"" string-with-slash := "\\" string-with-no-newline := "\ " # Newlines in variables single := ' hello ' double := " goodbye " escapes := '\t\n\r\"\\' # this string will evaluate to `foo\nbar\n` x := ''' foo bar ''' # this string will evaluate to `abc\n wuv\nbar\n` y := """ abc wuv xyz """ for: for file in `ls .`; do \ echo $file; \ done serve: touch {{tmpdir}}/file # This backtick evaluates the command `echo foo\necho bar\n`, which produces the value `foo\nbar\n`. stuff := ``` echo foo echo bar ``` an_arch := trim(lowercase(justfile())) + arch() trim_end := trim_end("99.99954% ") home_dir := replace(env_var('HOME') / "yep", 'yep', '') quoted := quote("some things beyond\"$()^%#@!|-+=_*&'`") smartphone := trim_end_match('blah.txt', 'txt') museum := trim_start_match(trim_start(trim_end_matches(' yep_blah.txt.txt', '.txt')), 'yep_') water := trim_start_matches('ssssssoup.txt', 's') congress := uppercase(os()) fam := os_family() path_1 := absolute_path('test') path_2 := '/tmp/subcommittee.txt' ext_z := extension(path_2) exe_name := file_name(just_executable()) a_stem := file_stem(path_2) a_parent := parent_directory(path_2) sans_ext := without_extension(path_2) camera := join('tmp', 'dir1', 'dir2', path_2) cleaned := clean('/tmp/blah/..///thing.txt') id__path := '/tmp' / sha256('blah') / sha256_file(justfile()) _another_var := env_var_or_default("HOME", justfile_directory()) python := `which python` exists := if path_exists(just_executable()) =~ '^/User' { uuid() } else { 'yeah' } foo := if env_var("_") == "/usr/bin/env" { `touch /tmp/a_file` } else { "dummy-value" } foo_b := if "hello" == "goodbye" { "xyz" } else { if "no" == "no" { "yep"} else { error("123") } } foo_c := if "hello" == "goodbye" { "xyz" } else if "a" == "a" { "abc" } else { "123" } bar: @echo {{foo}} bar2 foo_stuff: echo {{ if foo_stuff == "bar" { "hello" } else { "goodbye" } }} executable: @echo The executable is at: {{just_executable()}} rustfmt: find {{invocation_directory()}} -name \*.rs -exec rustfmt {} \; test: echo "{{home_dir}}" linewise: Write-Host "Hello, world!" serve2: @echo "Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…" shebang := if os() == 'windows' { 'powershell.exe' } else { '/usr/bin/env pwsh' } shebang: #!{{shebang}} $PSV = $PSVersionTable.PSVersion | % {"$_" -split "\." } $psver = $PSV[0] + "." + $PSV[1] if ($PSV[2].Length -lt 4) { $psver += "." + $PSV[2] + " Core" } else { $psver += " Desktop" } echo "PowerShell $psver" @foo: echo bar @test5 *args='': bash -c 'while (( "$#" )); do echo - $1; shift; done' -- "$@" test2 $RUST_BACKTRACE="1": # will print a stack trace if it crashes cargo test notify m="": keybase chat send --topic-type "chat" --channel "upd(): {{m}}" # Sample project script 2 script2 *ARGS: {{ python }} script2.py {{ ARGS }} braces: echo 'I {{{{LOVE}} curly braces!' _braces2: echo '{{'I {{LOVE}} curly braces!'}}' _braces3: echo 'I {{ "{{" }}LOVE}} curly braces!' foo2: -@cat foo echo 'Done!' test3 target tests=path_1: @echo 'Testing {{target}}:{{tests}}…' ./test --tests {{tests}} {{target}} test4 triple=(an_arch + "-unknown-unknown") input=(an_arch / "input.dat"): ./test {{triple}} variadic $VAR1_1 VAR2 VAR3 VAR4=("a") +$FLAGS='-q': foo2 braces cargo test {{FLAGS}} time: @-date +"%H:%S" -cat /tmp/nonexistent_file.txt @echo "finished" justwords: grep just \ --text /usr/share/dict/words \ > /tmp/justwords # Subsequent dependencies # https://just.systems/man/en/chapter_37.html # To test, run `$ just -f test-suite.just b` a: echo 'A!' b: a && d echo 'B start!' just -f {{justfile()}} c echo 'B end!' c: echo 'C!' d: echo 'D!' just-1.21.0/examples/powershell.just000064400000000000000000000012271046102023000156050ustar 00000000000000# Cross platform shebang: shebang := if os() == 'windows' { 'powershell.exe' } else { '/usr/bin/env pwsh' } # Set shell for non-Windows OSs: set shell := ["powershell", "-c"] # Set shell for Windows OSs: set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] # If you have PowerShell Core installed and want to use it, # use `pwsh.exe` instead of `powershell.exe` linewise: Write-Host "Hello, world!" shebang: #!{{shebang}} $PSV = $PSVersionTable.PSVersion | % {"$_" -split "\." } $psver = $PSV[0] + "." + $PSV[1] if ($PSV[2].Length -lt 4) { $psver += "." + $PSV[2] + " Core" } else { $psver += " Desktop" } echo "PowerShell $psver" just-1.21.0/examples/pre-commit.just000064400000000000000000000042211046102023000154720ustar 00000000000000# use with https://github.com/casey/just # Example combining just + pre-commit # pre-commit: https://pre-commit.com/ # > A framework for managing and maintaining # > multi-language pre-commit hooks. # pre-commit brings about encapsulation of your # most common repo scripting tasks. It is perfectly # usable without actually setting up precommit hooks. # If you chose to, this justfiles includes shorthands # for git commit and amend to keep pre-commit out of # the way when in flow on a feature branch. # uses: https://github.com/tekwizely/pre-commit-golang # uses: https://github.com/prettier/prettier (pre-commit hook) # configures: https://www.git-town.com/ (setup receipt) # fix auto-fixable lint issues in staged files fix: pre-commit run go-returns # fixes all Go lint issues pre-commit run prettier # fixes all Markdown (& other) lint issues # lint most common issues in - or due - to staged files lint: pre-commit run go-vet-mod || true # runs go vet pre-commit run go-lint || true # runs golint pre-commit run go-critic || true # runs gocritic # lint all issues in - or due - to staged files: lint-all: pre-commit run golangci-lint-mod || true # runs golangci-lint # run tests in - or due - to staged files test: pre-commit run go-test-mod || true # runs go test # commit skipping pre-commit hooks commit m: git commit --no-verify -m "{{m}}" # amend skipping pre-commit hooks amend: git commit --amend --no-verify # install/update code automation (prettier, pre-commit, goreturns, lintpack, gocritic, golangci-lint) install: npm i -g prettier curl https://pre-commit.com/install-local.py | python3 - go get github.com/sqs/goreturns go get github.com/go-lintpack/lintpack/... go get github.com/go-critic/go-critic/... curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.27.0 # setup/update pre-commit hooks (optional) setup: pre-commit install --install-hooks # uninstall: `pre-commit uninstall` git config git-town.code-hosting-driver gitea # setup git-town with gitea git config git-town.code-hosting-origin-hostname gitea.example.org # setup git-town origin hostname just-1.21.0/examples/screenshot.just000064400000000000000000000003041046102023000155710ustar 00000000000000alias b := build host := `uname -a` # build main build: cc *.c -o main # test everything test-all: build ./test --all # run a specific test test TEST: build ./test --test {{TEST}} just-1.21.0/extras/just.sh000075500000000000000000000012051046102023000135220ustar 00000000000000#!/usr/bin/env bash # cd upwards to the justfile while [[ ! -e justfile ]]; do if [[ $PWD = / ]] || [[ $PWD = $JUSTSTOP ]] || [[ -e juststop ]]; then echo 'No justfile found.' exit 1 fi cd .. done # prefer gmake if it exists if command -v gmake > /dev/null; then MAKE=gmake else MAKE=make fi declare -a RECIPES for ARG in "$@"; do test $ARG = '--' && shift && break RECIPES+=($ARG) && shift done # export arguments after '--' so they can be used in recipes I=0 for ARG in "$@"; do export ARG$I=$ARG I=$((I + 1)) done # go! exec $MAKE MAKEFLAGS='' --always-make --no-print-directory -f justfile ${RECIPES[*]} just-1.21.0/justfile000075500000000000000000000112321046102023000124440ustar 00000000000000#!/usr/bin/env -S just --justfile # ^ A shebang isn't required, but allows a justfile to be executed # like a script, with `./justfile test`, for example. alias t := test alias c := check log := "warn" export JUST_LOG := log test: cargo test ci: build-book cargo test --all cargo clippy --all --all-targets -- --deny warnings cargo fmt --all -- --check ./bin/forbid cargo update --locked --package just fuzz: cargo +nightly fuzz run fuzz-compiler run: cargo run # only run tests matching PATTERN filter PATTERN: cargo test {{PATTERN}} build: cargo build fmt: cargo fmt --all watch +COMMAND='test': cargo watch --clear --exec "{{COMMAND}}" man: cargo build --features help4help2man help2man \ --name 'save and run commands' \ --manual 'Just Manual' \ --no-info \ target/debug/just \ > man/just.1 view-man: man man man/just.1 # add git log messages to changelog update-changelog: echo >> CHANGELOG.md git log --pretty='format:- %s' >> CHANGELOG.md update-contributors: cargo run --release --package update-contributors check: fmt clippy test forbid #!/usr/bin/env bash set -euxo pipefail git diff --no-ext-diff --quiet --exit-code VERSION=`sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.toml | head -1` grep "^\[$VERSION\]" CHANGELOG.md # publish current GitHub master branch publish: #!/usr/bin/env bash set -euxo pipefail rm -rf tmp/release git clone git@github.com:casey/just.git tmp/release cd tmp/release ! grep 'master' README.md VERSION=`sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.toml | head -1` git tag -a $VERSION -m "Release $VERSION" git push origin $VERSION cargo publish cd ../.. rm -rf tmp/release readme-version-notes: grep 'master' README.md push: check ! git branch | grep '* master' git push github pr: push gh pr create --web # clean up feature branch BRANCH done BRANCH=`git rev-parse --abbrev-ref HEAD`: git checkout master git diff --no-ext-diff --quiet --exit-code git pull --rebase github master git diff --no-ext-diff --quiet --exit-code {{BRANCH}} git branch -D {{BRANCH}} # install just from crates.io install: cargo install -f just # install development dependencies install-dev-deps: rustup install nightly rustup update nightly cargo +nightly install cargo-fuzz cargo install cargo-check cargo install cargo-watch cargo install mdbook mdbook-linkcheck # install system development dependencies with homebrew install-dev-deps-homebrew: brew install help2man # everyone's favorite animate paper clip clippy: cargo clippy --all --all-targets --all-features forbid: ./bin/forbid # count non-empty lines of code sloc: @cat src/*.rs | sed '/^\s*$/d' | wc -l replace FROM TO: sd '{{FROM}}' '{{TO}}' src/*.rs test-quine: cargo run -- quine # make a quine, compile it, and verify it quine: mkdir -p tmp @echo '{{quine-text}}' > tmp/gen0.c cc tmp/gen0.c -o tmp/gen0 ./tmp/gen0 > tmp/gen1.c cc tmp/gen1.c -o tmp/gen1 ./tmp/gen1 > tmp/gen2.c diff tmp/gen1.c tmp/gen2.c rm -r tmp @echo 'It was a quine!' quine-text := ' int printf(const char*, ...); int main() { char *s = "int printf(const char*, ...);" "int main() {" " char *s = %c%s%c;" " printf(s, 34, s, 34);" " return 0;" "}"; printf(s, 34, s, 34); return 0; } ' render-readme: #!/usr/bin/env ruby require 'github/markup' $rendered = GitHub::Markup.render("README.adoc", File.read("README.adoc")) File.write('tmp/README.html', $rendered) watch-readme: just render-readme fswatch -ro README.adoc | xargs -n1 -I{} just render-readme generate-completions: ./bin/generate-completions test-completions: ./tests/completions/just.bash build-book: cargo run --package generate-book mdbook build book/en mdbook build book/zh convert-integration-test test: cargo expand --test integration {{test}} | \ sed \ -E \ -e 's/#\[cfg\(test\)\]/#\[test\]/' \ -e 's/^ *let test = //' \ -e 's/^ *test[.]/./' \ -e 's/;$//' \ -e 's/crate::test::Test/Test/' \ -e 's/\.run\(\)/.run();/' # run all polyglot recipes polyglot: _python _js _perl _sh _ruby _python: #!/usr/bin/env python3 print('Hello from python!') _js: #!/usr/bin/env node console.log('Greetings from JavaScript!') _perl: #!/usr/bin/env perl print "Larry Wall says Hi!\n"; _sh: #!/usr/bin/env sh hello='Yo' echo "$hello from a shell script!" _ruby: #!/usr/bin/env ruby puts "Hello from ruby!" # Print working directory, for demonstration purposes! pwd: echo {{invocation_directory()}} # Local Variables: # mode: makefile # End: # vim: set ft=make : just-1.21.0/man/just.1000064400000000000000000000073231046102023000125210ustar 00000000000000.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. .TH JUST "1" "December 2023" "just 1.21.0" "Just Manual" .SH NAME just \- save and run commands .SH DESCRIPTION just 1.21.0 \- Please see https://github.com/casey/just for more information. .SS "USAGE:" .IP just [FLAGS] [OPTIONS] [\-\-] [ARGUMENTS]... .SS "FLAGS:" .TP \fB\-\-changelog\fR Print changelog .TP \fB\-\-check\fR Run `\-\-fmt` in 'check' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required. .TP \fB\-\-choose\fR 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` .TP \fB\-\-clear\-shell\-args\fR Clear shell arguments .TP \fB\-n\fR, \fB\-\-dry\-run\fR Print what just would do without doing it .TP \fB\-\-dump\fR Print justfile .TP \fB\-e\fR, \fB\-\-edit\fR Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim` .TP \fB\-\-evaluate\fR Evaluate and print all variables. If a variable name is given as an argument, only print that variable's value. .TP \fB\-\-fmt\fR Format and overwrite justfile .TP \fB\-\-highlight\fR Highlight echoed recipe lines in bold .TP \fB\-\-init\fR Initialize new justfile in project root .TP \fB\-l\fR, \fB\-\-list\fR List available recipes and their arguments .TP \fB\-\-no\-dotenv\fR Don't load `.env` file .TP \fB\-\-no\-highlight\fR Don't highlight echoed recipe lines in bold .TP \fB\-q\fR, \fB\-\-quiet\fR Suppress all output .TP \fB\-\-shell\-command\fR Invoke with the shell used to run recipe lines and backticks .TP \fB\-\-summary\fR List names of available recipes .TP \fB\-u\fR, \fB\-\-unsorted\fR Return list and summary entries in source order .TP \fB\-\-unstable\fR Enable unstable features .TP \fB\-\-variables\fR List names of variables .TP \fB\-v\fR, \fB\-\-verbose\fR Use verbose output .TP \fB\-\-yes\fR Automatically confirm all recipes. .TP \fB\-h\fR, \fB\-\-help\fR Print help information .TP \fB\-V\fR, \fB\-\-version\fR Print version information .SS "OPTIONS:" .TP \fB\-\-chooser\fR Override binary invoked by `\-\-choose` .HP \fB\-\-color\fR .TP Print colorful output [default: auto] [possible values: auto, always, never] .HP \fB\-c\fR, \fB\-\-command\fR .IP Run an arbitrary command with the working directory, `.env`, overrides, and exports set .HP \fB\-\-command\-color\fR .IP Echo recipe lines in [possible values: black, blue, cyan, green, purple, red, yellow] .HP \fB\-\-completions\fR .IP Print shell completion script for [possible values: zsh, bash, fish, powershell, elvish] .TP \fB\-\-dotenv\-filename\fR Search for environment file named instead of `.env` .TP \fB\-\-dotenv\-path\fR Load environment file at instead of searching for one .HP \fB\-\-dump\-format\fR .TP Dump justfile as [default: just] [possible values: just, json] .TP \fB\-f\fR, \fB\-\-justfile\fR Use as justfile .TP \fB\-\-list\-heading\fR Print before list .TP \fB\-\-list\-prefix\fR Print before each list item .TP \fB\-\-set\fR Override with .TP \fB\-\-shell\fR Invoke to run recipes .TP \fB\-\-shell\-arg\fR ... Invoke shell with as an argument .TP \fB\-s\fR, \fB\-\-show\fR Show information about .HP \fB\-d\fR, \fB\-\-working\-directory\fR .IP Use as working directory. \fB\-\-justfile\fR must also be set .SS "ARGS:" .TP ... Overrides and recipe(s) to run, defaulting to the first recipe in the justfile just-1.21.0/rustfmt.toml000064400000000000000000000002001046102023000132630ustar 00000000000000edition = "2018" max_width = 100 newline_style = "Unix" tab_spaces = 2 use_field_init_shorthand = true use_try_shorthand = true just-1.21.0/snapcraft.yaml000064400000000000000000000010321046102023000135330ustar 00000000000000base: core20 confinement: classic contact: casey@rodarmor.com description: Just is a handy way to save and run project-specific commands. grade: stable icon: icon.png issues: https://github.com/casey/just/issues license: CC0-1.0 name: just source-code: https://github.com/casey/just summary: Just a command runner version: '1.2.0' website: https://just.systems apps: just: command: bin/just parts: just: plugin: rust source-depth: 1 source-tag: '1.2.0' source-type: git source: https://github.com/casey/just just-1.21.0/src/alias.rs000064400000000000000000000024251046102023000131230ustar 00000000000000use super::*; /// An alias, e.g. `name := target` #[derive(Debug, PartialEq, Clone, Serialize)] pub(crate) struct Alias<'src, T = Rc>> { pub(crate) attributes: BTreeSet, pub(crate) name: Name<'src>, #[serde( bound(serialize = "T: Keyed<'src>"), serialize_with = "keyed::serialize" )] pub(crate) target: T, } impl<'src> Alias<'src, Name<'src>> { pub(crate) fn resolve(self, target: Rc>) -> Alias<'src> { assert_eq!(self.target.lexeme(), target.name.lexeme()); Alias { attributes: self.attributes, name: self.name, target, } } } impl Alias<'_> { pub(crate) fn is_private(&self) -> bool { self.name.lexeme().starts_with('_') || self.attributes.contains(&Attribute::Private) } } impl<'src, T> Keyed<'src> for Alias<'src, T> { fn key(&self) -> &'src str { self.name.lexeme() } } impl<'src> Display for Alias<'src, Name<'src>> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!( f, "alias {} := {}", self.name.lexeme(), self.target.lexeme() ) } } impl<'src> Display for Alias<'src> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!( f, "alias {} := {}", self.name.lexeme(), self.target.name.lexeme() ) } } just-1.21.0/src/analyzer.rs000064400000000000000000000231731046102023000136620ustar 00000000000000use {super::*, CompileErrorKind::*}; #[derive(Default)] pub(crate) struct Analyzer<'src> { assignments: Table<'src, Assignment<'src>>, aliases: Table<'src, Alias<'src, Name<'src>>>, sets: Table<'src, Set<'src>>, } impl<'src> Analyzer<'src> { pub(crate) fn analyze( loaded: &[PathBuf], paths: &HashMap, asts: &HashMap>, root: &Path, ) -> CompileResult<'src, Justfile<'src>> { Analyzer::default().justfile(loaded, paths, asts, root) } fn justfile( mut self, loaded: &[PathBuf], paths: &HashMap, asts: &HashMap>, root: &Path, ) -> CompileResult<'src, Justfile<'src>> { let mut recipes = Vec::new(); let mut stack = Vec::new(); stack.push(asts.get(root).unwrap()); let mut warnings = Vec::new(); let mut modules: BTreeMap = BTreeMap::new(); let mut definitions: HashMap<&str, (&'static str, Name)> = HashMap::new(); let mut define = |name: Name<'src>, second_type: &'static str, duplicates_allowed: bool| -> CompileResult<'src> { if let Some((first_type, original)) = definitions.get(name.lexeme()) { if !(*first_type == second_type && duplicates_allowed) { let (original, redefinition) = if name.line < original.line { (name, *original) } else { (*original, name) }; return Err(redefinition.token().error(Redefinition { first_type, second_type, name: name.lexeme(), first: original.line, })); } } definitions.insert(name.lexeme(), (second_type, name)); Ok(()) }; while let Some(ast) = stack.pop() { for item in &ast.items { match item { Item::Alias(alias) => { define(alias.name, "alias", false)?; Self::analyze_alias(alias)?; self.aliases.insert(alias.clone()); } Item::Assignment(assignment) => { self.analyze_assignment(assignment)?; self.assignments.insert(assignment.clone()); } Item::Comment(_) => (), Item::Import { absolute, .. } => { if let Some(absolute) = absolute { stack.push(asts.get(absolute).unwrap()); } } Item::Module { absolute, name, .. } => { if let Some(absolute) = absolute { define(*name, "module", false)?; modules.insert( name.to_string(), (*name, Self::analyze(loaded, paths, asts, absolute)?), ); } } Item::Recipe(recipe) => { if recipe.enabled() { Self::analyze_recipe(recipe)?; recipes.push(recipe); } } Item::Set(set) => { self.analyze_set(set)?; self.sets.insert(set.clone()); } } } warnings.extend(ast.warnings.iter().cloned()); } let settings = Settings::from_setting_iter(self.sets.into_iter().map(|(_, set)| set.value)); let mut recipe_table: Table<'src, UnresolvedRecipe<'src>> = Table::default(); AssignmentResolver::resolve_assignments(&self.assignments)?; for recipe in recipes { define(recipe.name, "recipe", settings.allow_duplicate_recipes)?; if recipe_table .get(recipe.name.lexeme()) .map_or(true, |original| recipe.depth <= original.depth) { recipe_table.insert(recipe.clone()); } } let recipes = RecipeResolver::resolve_recipes(recipe_table, &self.assignments)?; let mut aliases = Table::new(); while let Some(alias) = self.aliases.pop() { aliases.insert(Self::resolve_alias(&recipes, alias)?); } let root = paths.get(root).unwrap(); Ok(Justfile { default: recipes .values() .filter(|recipe| recipe.name.path == root) .fold(None, |accumulator, next| match accumulator { None => Some(Rc::clone(next)), Some(previous) => Some(if previous.line_number() < next.line_number() { previous } else { Rc::clone(next) }), }), aliases, assignments: self.assignments, loaded: loaded.into(), recipes, settings, warnings, modules: modules .into_iter() .map(|(name, (_name, justfile))| (name, justfile)) .collect(), }) } fn analyze_recipe(recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src> { let mut parameters = BTreeSet::new(); let mut passed_default = false; for parameter in &recipe.parameters { if parameters.contains(parameter.name.lexeme()) { return Err(parameter.name.token().error(DuplicateParameter { recipe: recipe.name.lexeme(), parameter: parameter.name.lexeme(), })); } parameters.insert(parameter.name.lexeme()); if parameter.default.is_some() { passed_default = true; } else if passed_default { return Err( parameter .name .token() .error(RequiredParameterFollowsDefaultParameter { parameter: parameter.name.lexeme(), }), ); } } let mut continued = false; for line in &recipe.body { if !recipe.shebang && !continued { if let Some(Fragment::Text { token }) = line.fragments.first() { let text = token.lexeme(); if text.starts_with(' ') || text.starts_with('\t') { return Err(token.error(ExtraLeadingWhitespace)); } } } continued = line.is_continuation(); } Ok(()) } fn analyze_assignment(&self, assignment: &Assignment<'src>) -> CompileResult<'src> { if self.assignments.contains_key(assignment.name.lexeme()) { return Err(assignment.name.token().error(DuplicateVariable { variable: assignment.name.lexeme(), })); } Ok(()) } fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src> { let name = alias.name.lexeme(); for attr in &alias.attributes { if *attr != Attribute::Private { return Err(alias.name.token().error(AliasInvalidAttribute { alias: name, attr: *attr, })); } } Ok(()) } fn analyze_set(&self, set: &Set<'src>) -> CompileResult<'src> { if let Some(original) = self.sets.get(set.name.lexeme()) { return Err(set.name.error(DuplicateSet { setting: original.name.lexeme(), first: original.name.line, })); } Ok(()) } fn resolve_alias( recipes: &Table<'src, Rc>>, alias: Alias<'src, Name<'src>>, ) -> CompileResult<'src, Alias<'src>> { let token = alias.name.token(); // Make sure the alias doesn't conflict with any recipe if let Some(recipe) = recipes.get(alias.name.lexeme()) { return Err(token.error(AliasShadowsRecipe { alias: alias.name.lexeme(), recipe_line: recipe.line_number(), })); } // Make sure the target recipe exists match recipes.get(alias.target.lexeme()) { Some(target) => Ok(alias.resolve(Rc::clone(target))), None => Err(token.error(UnknownAliasTarget { alias: alias.name.lexeme(), target: alias.target.lexeme(), })), } } } #[cfg(test)] mod tests { use super::*; analysis_error! { name: duplicate_alias, input: "alias foo := bar\nalias foo := baz", offset: 23, line: 1, column: 6, width: 3, kind: Redefinition { first_type: "alias", second_type: "alias", name: "foo", first: 0 }, } analysis_error! { name: unknown_alias_target, input: "alias foo := bar\n", offset: 6, line: 0, column: 6, width: 3, kind: UnknownAliasTarget {alias: "foo", target: "bar"}, } analysis_error! { name: alias_shadows_recipe_before, input: "bar: \n echo bar\nalias foo := bar\nfoo:\n echo foo", offset: 34, line: 3, column: 0, width: 3, kind: Redefinition { first_type: "alias", second_type: "recipe", name: "foo", first: 2 }, } analysis_error! { name: alias_shadows_recipe_after, input: "foo:\n echo foo\nalias foo := bar\nbar:\n echo bar", offset: 22, line: 2, column: 6, width: 3, kind: Redefinition { first_type: "alias", second_type: "recipe", name: "foo", first: 0 }, } analysis_error! { name: required_after_default, input: "hello arg='foo' bar:", offset: 16, line: 0, column: 16, width: 3, kind: RequiredParameterFollowsDefaultParameter{parameter: "bar"}, } analysis_error! { name: duplicate_parameter, input: "a b b:", offset: 4, line: 0, column: 4, width: 1, kind: DuplicateParameter{recipe: "a", parameter: "b"}, } analysis_error! { name: duplicate_variadic_parameter, input: "a b +b:", offset: 5, line: 0, column: 5, width: 1, kind: DuplicateParameter{recipe: "a", parameter: "b"}, } analysis_error! { name: duplicate_recipe, input: "a:\nb:\na:", offset: 6, line: 2, column: 0, width: 1, kind: Redefinition { first_type: "recipe", second_type: "recipe", name: "a", first: 0 }, } analysis_error! { name: duplicate_variable, input: "a := \"0\"\na := \"0\"", offset: 9, line: 1, column: 0, width: 1, kind: DuplicateVariable{variable: "a"}, } analysis_error! { name: extra_whitespace, input: "a:\n blah\n blarg", offset: 10, line: 2, column: 1, width: 6, kind: ExtraLeadingWhitespace, } } just-1.21.0/src/assignment.rs000064400000000000000000000005201046102023000141740ustar 00000000000000use super::*; /// An assignment, e.g `foo := bar` pub(crate) type Assignment<'src> = Binding<'src, Expression<'src>>; impl<'src> Display for Assignment<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { if self.export { write!(f, "export ")?; } write!(f, "{} := {}", self.name, self.value) } } just-1.21.0/src/assignment_resolver.rs000064400000000000000000000121031046102023000161150ustar 00000000000000use {super::*, CompileErrorKind::*}; pub(crate) struct AssignmentResolver<'src: 'run, 'run> { assignments: &'run Table<'src, Assignment<'src>>, stack: Vec<&'src str>, evaluated: BTreeSet<&'src str>, } impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { pub(crate) fn resolve_assignments( assignments: &'run Table<'src, Assignment<'src>>, ) -> CompileResult<'src> { let mut resolver = Self { stack: Vec::new(), evaluated: BTreeSet::new(), assignments, }; for name in assignments.keys() { resolver.resolve_assignment(name)?; } Ok(()) } fn resolve_assignment(&mut self, name: &'src str) -> CompileResult<'src> { if self.evaluated.contains(name) { return Ok(()); } self.stack.push(name); if let Some(assignment) = self.assignments.get(name) { self.resolve_expression(&assignment.value)?; self.evaluated.insert(name); } else { let message = format!("attempted to resolve unknown assignment `{name}`"); let token = Token { src: "", offset: 0, line: 0, column: 0, length: 0, kind: TokenKind::Unspecified, path: "".as_ref(), }; return Err(CompileError::new(token, Internal { message })); } self.stack.pop(); Ok(()) } fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src> { match expression { Expression::Variable { name } => { let variable = name.lexeme(); if self.evaluated.contains(variable) { Ok(()) } else if self.stack.contains(&variable) { let token = self.assignments[variable].name.token(); self.stack.push(variable); Err(token.error(CircularVariableDependency { variable, circle: self.stack.clone(), })) } else if self.assignments.contains_key(variable) { self.resolve_assignment(variable) } else { Err(name.token().error(UndefinedVariable { variable })) } } Expression::Call { thunk } => match thunk { Thunk::Nullary { .. } => Ok(()), Thunk::Unary { arg, .. } => self.resolve_expression(arg), Thunk::UnaryOpt { args: (a, b), .. } => { self.resolve_expression(a)?; if let Some(b) = b.as_ref() { self.resolve_expression(b)?; } Ok(()) } Thunk::Binary { args: [a, b], .. } => { self.resolve_expression(a)?; self.resolve_expression(b) } Thunk::BinaryPlus { args: ([a, b], rest), .. } => { self.resolve_expression(a)?; self.resolve_expression(b)?; for arg in rest { self.resolve_expression(arg)?; } Ok(()) } Thunk::Ternary { args: [a, b, c], .. } => { self.resolve_expression(a)?; self.resolve_expression(b)?; self.resolve_expression(c) } }, Expression::Concatenation { lhs, rhs } => { self.resolve_expression(lhs)?; self.resolve_expression(rhs) } Expression::Join { lhs, rhs } => { if let Some(lhs) = lhs { self.resolve_expression(lhs)?; } self.resolve_expression(rhs) } Expression::Conditional { lhs, rhs, then, otherwise, .. } => { self.resolve_expression(lhs)?; self.resolve_expression(rhs)?; self.resolve_expression(then)?; self.resolve_expression(otherwise) } Expression::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()), Expression::Group { contents } => self.resolve_expression(contents), } } } #[cfg(test)] mod tests { use super::*; analysis_error! { name: circular_variable_dependency, input: "a := b\nb := a", offset: 0, line: 0, column: 0, width: 1, kind: CircularVariableDependency{variable: "a", circle: vec!["a", "b", "a"]}, } analysis_error! { name: self_variable_dependency, input: "a := a", offset: 0, line: 0, column: 0, width: 1, kind: CircularVariableDependency{variable: "a", circle: vec!["a", "a"]}, } analysis_error! { name: unknown_expression_variable, input: "x := yy", offset: 5, line: 0, column: 5, width: 2, kind: UndefinedVariable{variable: "yy"}, } analysis_error! { name: unknown_function_parameter, input: "x := env_var(yy)", offset: 13, line: 0, column: 13, width: 2, kind: UndefinedVariable{variable: "yy"}, } analysis_error! { name: unknown_function_parameter_binary_first, input: "x := env_var_or_default(yy, 'foo')", offset: 24, line: 0, column: 24, width: 2, kind: UndefinedVariable{variable: "yy"}, } analysis_error! { name: unknown_function_parameter_binary_second, input: "x := env_var_or_default('foo', yy)", offset: 31, line: 0, column: 31, width: 2, kind: UndefinedVariable{variable: "yy"}, } } just-1.21.0/src/ast.rs000064400000000000000000000016241046102023000126210ustar 00000000000000use super::*; /// The top-level type produced by the parser. Not all successful parses result /// in valid justfiles, so additional consistency checks and name resolution /// are performed by the `Analyzer`, which produces a `Justfile` from an `Ast`. #[derive(Debug, Clone)] pub(crate) struct Ast<'src> { /// Items in the justfile pub(crate) items: Vec>, /// Non-fatal warnings encountered during parsing pub(crate) warnings: Vec, } impl<'src> Display for Ast<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { let mut iter = self.items.iter().peekable(); while let Some(item) = iter.next() { writeln!(f, "{item}")?; if let Some(next_item) = iter.peek() { if matches!(item, Item::Recipe(_)) || mem::discriminant(item) != mem::discriminant(next_item) { writeln!(f)?; } } } Ok(()) } } just-1.21.0/src/attribute.rs000064400000000000000000000011761046102023000140370ustar 00000000000000use super::*; #[derive( EnumString, PartialEq, Debug, Copy, Clone, Serialize, Ord, PartialOrd, Eq, IntoStaticStr, )] #[strum(serialize_all = "kebab-case")] #[serde(rename_all = "kebab-case")] pub(crate) enum Attribute { Confirm, Linux, Macos, NoCd, NoExitMessage, Private, Unix, Windows, } impl Attribute { pub(crate) fn from_name(name: Name) -> Option { name.lexeme().parse().ok() } pub(crate) fn to_str(self) -> &'static str { self.into() } } #[cfg(test)] mod tests { use super::*; #[test] fn to_str() { assert_eq!(Attribute::NoExitMessage.to_str(), "no-exit-message"); } } just-1.21.0/src/binding.rs000064400000000000000000000006711046102023000134450ustar 00000000000000use super::*; /// A binding of `name` to `value` #[derive(Debug, Clone, PartialEq, Serialize)] pub(crate) struct Binding<'src, V = String> { /// Export binding as an environment variable to child processes pub(crate) export: bool, /// Binding name pub(crate) name: Name<'src>, /// Binding value pub(crate) value: V, } impl<'src, V> Keyed<'src> for Binding<'src, V> { fn key(&self) -> &'src str { self.name.lexeme() } } just-1.21.0/src/color.rs000064400000000000000000000054041046102023000131500ustar 00000000000000use { super::*, ansi_term::{ANSIGenericString, Color::*, Prefix, Style, Suffix}, atty::Stream, }; #[derive(Copy, Clone, Debug, PartialEq)] pub(crate) struct Color { use_color: UseColor, atty: bool, style: Style, } impl Color { fn restyle(self, style: Style) -> Self { Self { style, ..self } } fn redirect(self, stream: Stream) -> Self { Self { atty: atty::is(stream), ..self } } fn effective_style(&self) -> Style { if self.active() { self.style } else { Style::new() } } pub(crate) fn auto() -> Self { Self { use_color: UseColor::Auto, ..Color::default() } } pub(crate) fn always() -> Self { Self { use_color: UseColor::Always, ..Color::default() } } pub(crate) fn never() -> Self { Self { use_color: UseColor::Never, ..Color::default() } } pub(crate) fn stderr(self) -> Self { self.redirect(Stream::Stderr) } pub(crate) fn stdout(self) -> Self { self.redirect(Stream::Stdout) } pub(crate) fn context(self) -> Self { self.restyle(Style::new().fg(Blue).bold()) } pub(crate) fn doc(self) -> Self { self.restyle(Style::new().fg(Blue)) } pub(crate) fn error(self) -> Self { self.restyle(Style::new().fg(Red).bold()) } pub(crate) fn warning(self) -> Self { self.restyle(Style::new().fg(Yellow).bold()) } pub(crate) fn banner(self) -> Self { self.restyle(Style::new().fg(Cyan).bold()) } pub(crate) fn command(self, foreground: Option) -> Self { self.restyle(Style { foreground, is_bold: true, ..Style::default() }) } pub(crate) fn parameter(self) -> Self { self.restyle(Style::new().fg(Cyan)) } pub(crate) fn message(self) -> Self { self.restyle(Style::new().bold()) } pub(crate) fn annotation(self) -> Self { self.restyle(Style::new().fg(Purple)) } pub(crate) fn string(self) -> Self { self.restyle(Style::new().fg(Green)) } pub(crate) fn diff_added(self) -> Self { self.restyle(Style::new().fg(Green)) } pub(crate) fn diff_deleted(self) -> Self { self.restyle(Style::new().fg(Red)) } pub(crate) fn active(&self) -> bool { match self.use_color { UseColor::Always => true, UseColor::Never => false, UseColor::Auto => self.atty, } } pub(crate) fn paint<'a>(&self, text: &'a str) -> ANSIGenericString<'a, str> { self.effective_style().paint(text) } pub(crate) fn prefix(&self) -> Prefix { self.effective_style().prefix() } pub(crate) fn suffix(&self) -> Suffix { self.effective_style().suffix() } } impl Default for Color { fn default() -> Self { Self { use_color: UseColor::Auto, atty: false, style: Style::new(), } } } just-1.21.0/src/color_display.rs000064400000000000000000000006251046102023000146750ustar 00000000000000use super::*; pub(crate) trait ColorDisplay { fn color_display(&self, color: Color) -> Wrapper where Self: Sized, { Wrapper(self, color) } fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result; } pub(crate) struct Wrapper<'a>(&'a dyn ColorDisplay, Color); impl<'a> Display for Wrapper<'a> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { self.0.fmt(f, self.1) } } just-1.21.0/src/command_ext.rs000064400000000000000000000014671046102023000143350ustar 00000000000000use super::*; pub(crate) trait CommandExt { fn export(&mut self, settings: &Settings, dotenv: &BTreeMap, scope: &Scope); fn export_scope(&mut self, settings: &Settings, scope: &Scope); } impl CommandExt for Command { fn export(&mut self, settings: &Settings, dotenv: &BTreeMap, scope: &Scope) { for (name, value) in dotenv { self.env(name, value); } if let Some(parent) = scope.parent() { self.export_scope(settings, parent); } } fn export_scope(&mut self, settings: &Settings, scope: &Scope) { if let Some(parent) = scope.parent() { self.export_scope(settings, parent); } for binding in scope.bindings() { if settings.export || binding.export { self.env(binding.name.lexeme(), &binding.value); } } } } just-1.21.0/src/compilation.rs000064400000000000000000000007031046102023000143450ustar 00000000000000use super::*; #[derive(Debug)] pub(crate) struct Compilation<'src> { pub(crate) asts: HashMap>, pub(crate) justfile: Justfile<'src>, pub(crate) root: PathBuf, pub(crate) srcs: HashMap, } impl<'src> Compilation<'src> { pub(crate) fn root_ast(&self) -> &Ast<'src> { self.asts.get(&self.root).unwrap() } pub(crate) fn root_src(&self) -> &'src str { self.srcs.get(&self.root).unwrap() } } just-1.21.0/src/compile_error.rs000064400000000000000000000174321046102023000146770ustar 00000000000000use super::*; #[derive(Debug, PartialEq)] pub(crate) struct CompileError<'src> { pub(crate) token: Token<'src>, pub(crate) kind: Box>, } impl<'src> CompileError<'src> { pub(crate) fn context(&self) -> Token<'src> { self.token } pub(crate) fn new(token: Token<'src>, kind: CompileErrorKind<'src>) -> CompileError<'src> { Self { token, kind: Box::new(kind), } } } fn capitalize(s: &str) -> String { let mut chars = s.chars(); match chars.next() { None => String::new(), Some(first) => first.to_uppercase().collect::() + chars.as_str(), } } impl Display for CompileError<'_> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { use CompileErrorKind::*; match &*self.kind { AliasInvalidAttribute { alias, attr } => write!( f, "Alias {alias} has an invalid attribute `{}`", attr.to_str(), ), AliasShadowsRecipe { alias, recipe_line } => write!( f, "Alias `{alias}` defined on line {} shadows recipe `{alias}` defined on line {}", self.token.line.ordinal(), recipe_line.ordinal(), ), BacktickShebang => write!(f, "Backticks may not start with `#!`"), CircularRecipeDependency { recipe, ref circle } => { if circle.len() == 2 { write!(f, "Recipe `{recipe}` depends on itself") } else { write!( f, "Recipe `{recipe}` has circular dependency `{}`", circle.join(" -> ") ) } } CircularVariableDependency { variable, ref circle, } => { if circle.len() == 2 { write!(f, "Variable `{variable}` is defined in terms of itself") } else { write!( f, "Variable `{variable}` depends on its own value: `{}`", circle.join(" -> "), ) } } DependencyArgumentCountMismatch { dependency, found, min, max, } => { write!( f, "Dependency `{dependency}` got {found} {} but takes ", Count("argument", *found), )?; if min == max { let expected = min; write!(f, "{expected} {}", Count("argument", *expected)) } else if found < min { write!(f, "at least {min} {}", Count("argument", *min)) } else { write!(f, "at most {max} {}", Count("argument", *max)) } } DuplicateAttribute { attribute, first } => write!( f, "Recipe attribute `{attribute}` first used on line {} is duplicated on line {}", first.ordinal(), self.token.line.ordinal(), ), DuplicateParameter { recipe, parameter } => { write!(f, "Recipe `{recipe}` has duplicate parameter `{parameter}`") } DuplicateSet { setting, first } => write!( f, "Setting `{setting}` first set on line {} is redefined on line {}", first.ordinal(), self.token.line.ordinal(), ), DuplicateVariable { variable } => { write!(f, "Variable `{variable}` has multiple definitions") } ExpectedKeyword { expected, found } => { let expected = List::or_ticked(expected); if found.kind == TokenKind::Identifier { write!( f, "Expected keyword {expected} but found identifier `{}`", found.lexeme() ) } else { write!(f, "Expected keyword {expected} but found `{}`", found.kind) } } ExtraLeadingWhitespace => write!(f, "Recipe line has extra leading whitespace"), FunctionArgumentCountMismatch { function, found, expected, } => write!( f, "Function `{function}` called with {found} {} but takes {}", Count("argument", *found), expected.display(), ), Include => write!( f, "The `!include` directive has been stabilized as `import`" ), InconsistentLeadingWhitespace { expected, found } => write!( f, "Recipe line has inconsistent leading whitespace. Recipe started with `{}` but found \ line with `{}`", ShowWhitespace(expected), ShowWhitespace(found) ), Internal { ref message } => write!( f, "Internal error, this may indicate a bug in just: {message}\n\ consider filing an issue: https://github.com/casey/just/issues/new" ), InvalidEscapeSequence { character } => write!( f, "`\\{}` is not a valid escape sequence", match character { '`' => r"\`".to_owned(), '\\' => r"\".to_owned(), '\'' => r"'".to_owned(), '"' => r#"""#.to_owned(), _ => character.escape_default().collect(), } ), MismatchedClosingDelimiter { open, open_line, close, } => write!( f, "Mismatched closing delimiter `{}`. (Did you mean to close the `{}` on line {}?)", close.close(), open.open(), open_line.ordinal(), ), MixedLeadingWhitespace { whitespace } => write!( f, "Found a mix of tabs and spaces in leading whitespace: `{}`\nLeading whitespace may \ consist of tabs or spaces, but not both", ShowWhitespace(whitespace) ), ParameterFollowsVariadicParameter { parameter } => { write!(f, "Parameter `{parameter}` follows variadic parameter") } ParsingRecursionDepthExceeded => write!(f, "Parsing recursion depth exceeded"), Redefinition { first, first_type, name, second_type, } => { if first_type == second_type { write!( f, "{} `{name}` first defined on line {} is redefined on line {}", capitalize(first_type), first.ordinal(), self.token.line.ordinal(), ) } else { write!( f, "{} `{name}` defined on line {} is redefined as {} {second_type} on line {}", capitalize(first_type), first.ordinal(), if *second_type == "alias" { "an" } else { "a" }, self.token.line.ordinal(), ) } } RequiredParameterFollowsDefaultParameter { parameter } => write!( f, "Non-default parameter `{parameter}` follows default parameter" ), UndefinedVariable { variable } => write!(f, "Variable `{variable}` not defined"), UnexpectedCharacter { expected } => write!(f, "Expected character `{expected}`"), UnexpectedClosingDelimiter { close } => { write!(f, "Unexpected closing delimiter `{}`", close.close()) } UnexpectedEndOfToken { expected } => { write!(f, "Expected character `{expected}` but found end-of-file") } UnexpectedToken { ref expected, found, } => write!(f, "Expected {}, but found {found}", List::or(expected)), UnknownAliasTarget { alias, target } => { write!(f, "Alias `{alias}` has an unknown target `{target}`") } UnknownAttribute { attribute } => write!(f, "Unknown attribute `{attribute}`"), UnknownDependency { recipe, unknown } => { write!(f, "Recipe `{recipe}` has unknown dependency `{unknown}`") } UnknownFunction { function } => write!(f, "Call to unknown function `{function}`"), UnknownSetting { setting } => write!(f, "Unknown setting `{setting}`"), UnknownStartOfToken => write!(f, "Unknown start of token:"), UnpairedCarriageReturn => write!(f, "Unpaired carriage return"), UnterminatedBacktick => write!(f, "Unterminated backtick"), UnterminatedInterpolation => write!(f, "Unterminated interpolation"), UnterminatedString => write!(f, "Unterminated string"), } } } just-1.21.0/src/compile_error_kind.rs000064400000000000000000000045121046102023000156770ustar 00000000000000use super::*; #[derive(Debug, PartialEq)] pub(crate) enum CompileErrorKind<'src> { AliasInvalidAttribute { alias: &'src str, attr: Attribute, }, AliasShadowsRecipe { alias: &'src str, recipe_line: usize, }, BacktickShebang, CircularRecipeDependency { recipe: &'src str, circle: Vec<&'src str>, }, CircularVariableDependency { variable: &'src str, circle: Vec<&'src str>, }, DependencyArgumentCountMismatch { dependency: &'src str, found: usize, min: usize, max: usize, }, Redefinition { first: usize, first_type: &'static str, name: &'src str, second_type: &'static str, }, DuplicateAttribute { attribute: &'src str, first: usize, }, DuplicateParameter { recipe: &'src str, parameter: &'src str, }, DuplicateSet { setting: &'src str, first: usize, }, DuplicateVariable { variable: &'src str, }, ExpectedKeyword { expected: Vec, found: Token<'src>, }, ExtraLeadingWhitespace, FunctionArgumentCountMismatch { function: &'src str, found: usize, expected: Range, }, Include, InconsistentLeadingWhitespace { expected: &'src str, found: &'src str, }, Internal { message: String, }, InvalidEscapeSequence { character: char, }, MismatchedClosingDelimiter { close: Delimiter, open: Delimiter, open_line: usize, }, MixedLeadingWhitespace { whitespace: &'src str, }, ParameterFollowsVariadicParameter { parameter: &'src str, }, ParsingRecursionDepthExceeded, RequiredParameterFollowsDefaultParameter { parameter: &'src str, }, UndefinedVariable { variable: &'src str, }, UnexpectedCharacter { expected: char, }, UnexpectedClosingDelimiter { close: Delimiter, }, UnexpectedEndOfToken { expected: char, }, UnexpectedToken { expected: Vec, found: TokenKind, }, UnknownAliasTarget { alias: &'src str, target: &'src str, }, UnknownAttribute { attribute: &'src str, }, UnknownDependency { recipe: &'src str, unknown: &'src str, }, UnknownFunction { function: &'src str, }, UnknownSetting { setting: &'src str, }, UnknownStartOfToken, UnpairedCarriageReturn, UnterminatedBacktick, UnterminatedInterpolation, UnterminatedString, } just-1.21.0/src/compiler.rs000064400000000000000000000145351046102023000136510ustar 00000000000000use super::*; pub(crate) struct Compiler; impl Compiler { pub(crate) fn compile<'src>( unstable: bool, loader: &'src Loader, root: &Path, ) -> RunResult<'src, Compilation<'src>> { let mut asts: HashMap = HashMap::new(); let mut paths: HashMap = HashMap::new(); let mut srcs: HashMap = HashMap::new(); let mut loaded = Vec::new(); let mut stack: Vec<(PathBuf, u32)> = Vec::new(); stack.push((root.into(), 0)); while let Some((current, depth)) = stack.pop() { let (relative, src) = loader.load(root, ¤t)?; loaded.push(relative.into()); let tokens = Lexer::lex(relative, src)?; let mut ast = Parser::parse(depth, ¤t, &tokens)?; paths.insert(current.clone(), relative.into()); srcs.insert(current.clone(), src); for item in &mut ast.items { match item { Item::Module { absolute, name, optional, relative, } => { if !unstable { return Err(Error::Unstable { message: "Modules are currently unstable.".into(), }); } let parent = current.parent().unwrap(); let import = if let Some(relative) = relative { let path = parent.join(Self::expand_tilde(&relative.cooked)?); if path.is_file() { Some(path) } else { None } } else { Self::find_module_file(parent, *name)? }; if let Some(import) = import { if srcs.contains_key(&import) { return Err(Error::CircularImport { current, import }); } *absolute = Some(import.clone()); stack.push((import, depth + 1)); } else if !*optional { return Err(Error::MissingModuleFile { module: *name }); } } Item::Import { relative, absolute, optional, path, } => { let import = current .parent() .unwrap() .join(Self::expand_tilde(&relative.cooked)?) .lexiclean(); if import.is_file() { if srcs.contains_key(&import) { return Err(Error::CircularImport { current, import }); } *absolute = Some(import.clone()); stack.push((import, depth + 1)); } else if !*optional { return Err(Error::MissingImportFile { path: *path }); } } _ => {} } } asts.insert(current.clone(), ast.clone()); } let justfile = Analyzer::analyze(&loaded, &paths, &asts, root)?; Ok(Compilation { asts, srcs, justfile, root: root.into(), }) } fn find_module_file<'src>(parent: &Path, module: Name<'src>) -> RunResult<'src, Option> { let mut candidates = vec![format!("{module}.just"), format!("{module}/mod.just")] .into_iter() .filter(|path| parent.join(path).is_file()) .collect::>(); let directory = parent.join(module.lexeme()); if directory.exists() { let entries = fs::read_dir(&directory).map_err(|io_error| SearchError::Io { io_error, directory: directory.clone(), })?; for entry in entries { let entry = entry.map_err(|io_error| SearchError::Io { io_error, directory: directory.clone(), })?; if let Some(name) = entry.file_name().to_str() { for justfile_name in search::JUSTFILE_NAMES { if name.eq_ignore_ascii_case(justfile_name) { candidates.push(format!("{module}/{name}")); } } } } } match candidates.as_slice() { [] => Ok(None), [file] => Ok(Some(parent.join(file).lexiclean())), found => Err(Error::AmbiguousModuleFile { found: found.into(), module, }), } } fn expand_tilde(path: &str) -> RunResult<'static, PathBuf> { Ok(if let Some(path) = path.strip_prefix("~/") { dirs::home_dir() .ok_or(Error::Homedir)? .join(path.trim_start_matches('/')) } else { PathBuf::from(path) }) } #[cfg(test)] pub(crate) fn test_compile(src: &str) -> CompileResult { let tokens = Lexer::test_lex(src)?; let ast = Parser::parse(0, &PathBuf::new(), &tokens)?; let root = PathBuf::from("justfile"); let mut asts: HashMap = HashMap::new(); asts.insert(root.clone(), ast); let mut paths: HashMap = HashMap::new(); paths.insert(root.clone(), root.clone()); Analyzer::analyze(&[], &paths, &asts, &root) } } #[cfg(test)] mod tests { use {super::*, temptree::temptree}; #[test] fn include_justfile() { let justfile_a = r#" # A comment at the top of the file import "./justfile_b" #some_recipe: recipe_b some_recipe: echo "some recipe" "#; let justfile_b = r#"import "./subdir/justfile_c" recipe_b: recipe_c echo "recipe b" "#; let justfile_c = r#"recipe_c: echo "recipe c" "#; let tmp = temptree! { justfile: justfile_a, justfile_b: justfile_b, subdir: { justfile_c: justfile_c } }; let loader = Loader::new(); let justfile_a_path = tmp.path().join("justfile"); let compilation = Compiler::compile(false, &loader, &justfile_a_path).unwrap(); assert_eq!(compilation.root_src(), justfile_a); } #[test] fn recursive_includes_fail() { let justfile_a = r#" # A comment at the top of the file import "./subdir/justfile_b" some_recipe: recipe_b echo "some recipe" "#; let justfile_b = r#" import "../justfile" recipe_b: echo "recipe b" "#; let tmp = temptree! { justfile: justfile_a, subdir: { justfile_b: justfile_b } }; let loader = Loader::new(); let justfile_a_path = tmp.path().join("justfile"); let loader_output = Compiler::compile(false, &loader, &justfile_a_path).unwrap_err(); assert_matches!(loader_output, Error::CircularImport { current, import } if current == tmp.path().join("subdir").join("justfile_b").lexiclean() && import == tmp.path().join("justfile").lexiclean() ); } } just-1.21.0/src/completions.rs000064400000000000000000000144311046102023000143660ustar 00000000000000pub(crate) const FISH_RECIPE_COMPLETIONS: &str = r#"function __fish_just_complete_recipes just --list 2> /dev/null | tail -n +2 | awk '{ command = $1; args = $0; desc = ""; delim = ""; sub(/^[[:space:]]*[^[:space:]]*/, "", args); gsub(/^[[:space:]]+|[[:space:]]+$/, "", args); if (match(args, /#.*/)) { desc = substr(args, RSTART+2, RLENGTH); args = substr(args, 0, RSTART-1); gsub(/^[[:space:]]+|[[:space:]]+$/, "", args); } gsub(/\+|=[`\'"][^`\'"]*[`\'"]/, "", args); gsub(/ /, ",", args); if (args != ""){ args = "Args: " args; } if (args != "" && desc != "") { delim = "; "; } print command "\t" args delim desc }' end # don't suggest files right off complete -c just -n "__fish_is_first_arg" --no-files # complete recipes complete -c just -a '(__fish_just_complete_recipes)' # autogenerated completions "#; pub(crate) const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[ ( r#" _arguments "${_arguments_options[@]}" \"#, r" local common=(", ), ( r"'*--set=[Override with ]' \", r"'*--set[Override with ]: :_just_variables' \", ), ( r"'-s+[Show information about ]' \ '--show=[Show information about ]' \", r"'-s+[Show information about ]: :_just_commands' \ '--show=[Show information about ]: :_just_commands' \", ), ( "'::ARGUMENTS -- Overrides and recipe(s) to run, defaulting to the first recipe in the \ justfile:_files' \\ && ret=0 \x20\x20\x20\x20 ", r#") _arguments "${_arguments_options[@]}" $common \ '1: :_just_commands' \ '*: :->args' \ && ret=0 case $state in args) curcontext="${curcontext%:*}-${words[2]}:" local lastarg=${words[${#words}]} local recipe local cmds; cmds=( ${(s: :)$(_call_program commands just --summary)} ) # Find first recipe name for ((i = 2; i < $#words; i++ )) do if [[ ${cmds[(I)${words[i]}]} -gt 0 ]]; then recipe=${words[i]} break fi done if [[ $lastarg = */* ]]; then # Arguments contain slash would be recognised as a file _arguments -s -S $common '*:: :_files' elif [[ $lastarg = *=* ]]; then # Arguments contain equal would be recognised as a variable _message "value" elif [[ $recipe ]]; then # Show usage message _message "`just --show $recipe`" # Or complete with other commands #_arguments -s -S $common '*:: :_just_commands' else _arguments -s -S $common '*:: :_just_commands' fi ;; esac return ret "#, ), ( " local commands; commands=( \x20\x20\x20\x20\x20\x20\x20\x20 )", r#" [[ $PREFIX = -* ]] && return 1 integer ret=1 local variables; variables=( ${(s: :)$(_call_program commands just --variables)} ) local commands; commands=( ${${${(M)"${(f)$(_call_program commands just --list)}":# *}/ ##/}/ ##/:Args: } ) "#, ), ( r#" _describe -t commands 'just commands' commands "$@""#, r#" if compset -P '*='; then case "${${words[-1]%=*}#*=}" in *) _message 'value' && ret=0 ;; esac else _describe -t variables 'variables' variables -qS "=" && ret=0 _describe -t commands 'just commands' commands "$@" fi "#, ), ( r#"_just "$@""#, r#"(( $+functions[_just_variables] )) || _just_variables() { [[ $PREFIX = -* ]] && return 1 integer ret=1 local variables; variables=( ${(s: :)$(_call_program commands just --variables)} ) if compset -P '*='; then case "${${words[-1]%=*}#*=}" in *) _message 'value' && ret=0 ;; esac else _describe -t variables 'variables' variables && ret=0 fi return ret } _just "$@""#, ), ]; pub(crate) const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[( r#"$completions.Where{ $_.CompletionText -like "$wordToComplete*" } | Sort-Object -Property ListItemText"#, r#"function Get-JustFileRecipes([string[]]$CommandElements) { $justFileIndex = $commandElements.IndexOf("--justfile"); if ($justFileIndex -ne -1 && $justFileIndex + 1 -le $commandElements.Length) { $justFileLocation = $commandElements[$justFileIndex + 1] } $justArgs = @("--summary") if (Test-Path $justFileLocation) { $justArgs += @("--justfile", $justFileLocation) } $recipes = $(just @justArgs) -split ' ' return $recipes | ForEach-Object { [CompletionResult]::new($_) } } $elementValues = $commandElements | Select-Object -ExpandProperty Value $recipes = Get-JustFileRecipes -CommandElements $elementValues $completions += $recipes $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | Sort-Object -Property ListItemText"#, )]; pub(crate) const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[ ( r#" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi"#, r#" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 elif [[ ${COMP_CWORD} -eq 1 ]]; then local recipes=$(just --summary 2> /dev/null) if echo "${cur}" | \grep -qF '/'; then local path_prefix=$(echo "${cur}" | sed 's/[/][^/]*$/\//') local recipes=$(just --summary 2> /dev/null -- "${path_prefix}") local recipes=$(printf "${path_prefix}%s\t" $recipes) fi if [[ $? -eq 0 ]]; then COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") ) return 0 fi fi"#, ), (r" just)", r#" "$1")"#), ]; just-1.21.0/src/conditional_operator.rs000064400000000000000000000007101046102023000162430ustar 00000000000000use super::*; /// A conditional expression operator. #[derive(PartialEq, Debug, Copy, Clone)] pub(crate) enum ConditionalOperator { /// `==` Equality, /// `!=` Inequality, /// `=~` RegexMatch, } impl Display for ConditionalOperator { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::Equality => write!(f, "=="), Self::Inequality => write!(f, "!="), Self::RegexMatch => write!(f, "=~"), } } } just-1.21.0/src/config.rs000064400000000000000000001134201046102023000132750ustar 00000000000000use { super::*, clap::{App, AppSettings, Arg, ArgGroup, ArgMatches, ArgSettings}, }; pub(crate) const CHOOSER_ENVIRONMENT_KEY: &str = "JUST_CHOOSER"; pub(crate) const CHOOSE_HELP: &str = "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`"; pub(crate) fn chooser_default(justfile: &Path) -> OsString { let mut chooser = OsString::new(); chooser.push("fzf --multi --preview 'just --unstable --color always --justfile \""); chooser.push(justfile); chooser.push("\" --show {}'"); chooser } #[derive(Debug, PartialEq)] pub(crate) struct Config { pub(crate) check: bool, pub(crate) color: Color, pub(crate) command_color: Option, pub(crate) dotenv_filename: Option, pub(crate) dotenv_path: Option, pub(crate) dry_run: bool, pub(crate) dump_format: DumpFormat, pub(crate) highlight: bool, pub(crate) invocation_directory: PathBuf, pub(crate) list_heading: String, pub(crate) list_prefix: String, pub(crate) load_dotenv: bool, pub(crate) search_config: SearchConfig, pub(crate) shell: Option, pub(crate) shell_args: Option>, pub(crate) shell_command: bool, pub(crate) subcommand: Subcommand, pub(crate) unsorted: bool, pub(crate) unstable: bool, pub(crate) verbosity: Verbosity, pub(crate) yes: bool, } mod cmd { pub(crate) const CHANGELOG: &str = "CHANGELOG"; pub(crate) const CHOOSE: &str = "CHOOSE"; pub(crate) const COMMAND: &str = "COMMAND"; pub(crate) const COMPLETIONS: &str = "COMPLETIONS"; pub(crate) const DUMP: &str = "DUMP"; pub(crate) const EDIT: &str = "EDIT"; pub(crate) const EVALUATE: &str = "EVALUATE"; pub(crate) const FORMAT: &str = "FORMAT"; pub(crate) const INIT: &str = "INIT"; pub(crate) const LIST: &str = "LIST"; pub(crate) const SHOW: &str = "SHOW"; pub(crate) const SUMMARY: &str = "SUMMARY"; pub(crate) const VARIABLES: &str = "VARIABLES"; pub(crate) const ALL: &[&str] = &[ CHANGELOG, CHOOSE, COMMAND, COMPLETIONS, DUMP, EDIT, EVALUATE, FORMAT, INIT, LIST, SHOW, SUMMARY, VARIABLES, ]; pub(crate) const ARGLESS: &[&str] = &[ CHANGELOG, COMPLETIONS, DUMP, EDIT, FORMAT, INIT, LIST, SHOW, SUMMARY, VARIABLES, ]; } mod arg { pub(crate) const ARGUMENTS: &str = "ARGUMENTS"; pub(crate) const CHECK: &str = "CHECK"; pub(crate) const CHOOSER: &str = "CHOOSER"; pub(crate) const CLEAR_SHELL_ARGS: &str = "CLEAR-SHELL-ARGS"; pub(crate) const COLOR: &str = "COLOR"; pub(crate) const COMMAND_COLOR: &str = "COMMAND-COLOR"; pub(crate) const DOTENV_FILENAME: &str = "DOTENV-FILENAME"; pub(crate) const DOTENV_PATH: &str = "DOTENV-PATH"; pub(crate) const DRY_RUN: &str = "DRY-RUN"; pub(crate) const DUMP_FORMAT: &str = "DUMP-FORMAT"; pub(crate) const HIGHLIGHT: &str = "HIGHLIGHT"; pub(crate) const JUSTFILE: &str = "JUSTFILE"; pub(crate) const LIST_HEADING: &str = "LIST-HEADING"; pub(crate) const LIST_PREFIX: &str = "LIST-PREFIX"; pub(crate) const NO_DOTENV: &str = "NO-DOTENV"; pub(crate) const NO_HIGHLIGHT: &str = "NO-HIGHLIGHT"; pub(crate) const QUIET: &str = "QUIET"; pub(crate) const SET: &str = "SET"; pub(crate) const SHELL: &str = "SHELL"; pub(crate) const SHELL_ARG: &str = "SHELL-ARG"; pub(crate) const SHELL_COMMAND: &str = "SHELL-COMMAND"; pub(crate) const UNSORTED: &str = "UNSORTED"; pub(crate) const UNSTABLE: &str = "UNSTABLE"; pub(crate) const VERBOSE: &str = "VERBOSE"; pub(crate) const WORKING_DIRECTORY: &str = "WORKING-DIRECTORY"; pub(crate) const YES: &str = "YES"; pub(crate) const COLOR_ALWAYS: &str = "always"; pub(crate) const COLOR_AUTO: &str = "auto"; pub(crate) const COLOR_NEVER: &str = "never"; pub(crate) const COLOR_VALUES: &[&str] = &[COLOR_AUTO, COLOR_ALWAYS, COLOR_NEVER]; pub(crate) const COMMAND_COLOR_BLACK: &str = "black"; pub(crate) const COMMAND_COLOR_BLUE: &str = "blue"; pub(crate) const COMMAND_COLOR_CYAN: &str = "cyan"; pub(crate) const COMMAND_COLOR_GREEN: &str = "green"; pub(crate) const COMMAND_COLOR_PURPLE: &str = "purple"; pub(crate) const COMMAND_COLOR_RED: &str = "red"; pub(crate) const COMMAND_COLOR_YELLOW: &str = "yellow"; pub(crate) const COMMAND_COLOR_VALUES: &[&str] = &[ COMMAND_COLOR_BLACK, COMMAND_COLOR_BLUE, COMMAND_COLOR_CYAN, COMMAND_COLOR_GREEN, COMMAND_COLOR_PURPLE, COMMAND_COLOR_RED, COMMAND_COLOR_YELLOW, ]; pub(crate) const DUMP_FORMAT_JSON: &str = "json"; pub(crate) const DUMP_FORMAT_JUST: &str = "just"; pub(crate) const DUMP_FORMAT_VALUES: &[&str] = &[DUMP_FORMAT_JUST, DUMP_FORMAT_JSON]; } impl Config { pub(crate) fn app() -> App<'static, 'static> { let app = App::new(env!("CARGO_PKG_NAME")) .help_message("Print help information") .version_message("Print version information") .setting(AppSettings::ColoredHelp) .setting(AppSettings::TrailingVarArg) .arg( Arg::with_name(arg::CHECK) .long("check") .requires(cmd::FORMAT) .help("Run `--fmt` in 'check' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required."), ) .arg( Arg::with_name(arg::CHOOSER) .long("chooser") .takes_value(true) .help("Override binary invoked by `--choose`"), ) .arg( Arg::with_name(arg::COLOR) .long("color") .takes_value(true) .possible_values(arg::COLOR_VALUES) .default_value(arg::COLOR_AUTO) .help("Print colorful output"), ) .arg( Arg::with_name(arg::COMMAND_COLOR) .long("command-color") .takes_value(true) .possible_values(arg::COMMAND_COLOR_VALUES) .help("Echo recipe lines in "), ) .arg(Arg::with_name(arg::YES).long("yes").help("Automatically confirm all recipes.")) .arg( Arg::with_name(arg::DRY_RUN) .short("n") .long("dry-run") .help("Print what just would do without doing it") .conflicts_with(arg::QUIET), ) .arg( Arg::with_name(arg::DUMP_FORMAT) .long("dump-format") .takes_value(true) .possible_values(arg::DUMP_FORMAT_VALUES) .default_value(arg::DUMP_FORMAT_JUST) .value_name("FORMAT") .help("Dump justfile as "), ) .arg( Arg::with_name(arg::HIGHLIGHT) .long("highlight") .help("Highlight echoed recipe lines in bold") .overrides_with(arg::NO_HIGHLIGHT), ) .arg( Arg::with_name(arg::LIST_HEADING) .long("list-heading") .help("Print before list") .value_name("TEXT") .takes_value(true), ) .arg( Arg::with_name(arg::LIST_PREFIX) .long("list-prefix") .help("Print before each list item") .value_name("TEXT") .takes_value(true), ) .arg( Arg::with_name(arg::NO_DOTENV) .long("no-dotenv") .help("Don't load `.env` file"), ) .arg( Arg::with_name(arg::NO_HIGHLIGHT) .long("no-highlight") .help("Don't highlight echoed recipe lines in bold") .overrides_with(arg::HIGHLIGHT), ) .arg( Arg::with_name(arg::JUSTFILE) .short("f") .long("justfile") .takes_value(true) .help("Use as justfile"), ) .arg( Arg::with_name(arg::QUIET) .short("q") .long("quiet") .help("Suppress all output") .conflicts_with(arg::DRY_RUN), ) .arg( Arg::with_name(arg::SET) .long("set") .takes_value(true) .number_of_values(2) .value_names(&["VARIABLE", "VALUE"]) .multiple(true) .help("Override with "), ) .arg( Arg::with_name(arg::SHELL) .long("shell") .takes_value(true) .help("Invoke to run recipes"), ) .arg( Arg::with_name(arg::SHELL_ARG) .long("shell-arg") .takes_value(true) .multiple(true) .number_of_values(1) .allow_hyphen_values(true) .overrides_with(arg::CLEAR_SHELL_ARGS) .help("Invoke shell with as an argument"), ) .arg( Arg::with_name(arg::SHELL_COMMAND) .long("shell-command") .requires(cmd::COMMAND) .help("Invoke with the shell used to run recipe lines and backticks"), ) .arg( Arg::with_name(arg::CLEAR_SHELL_ARGS) .long("clear-shell-args") .overrides_with(arg::SHELL_ARG) .help("Clear shell arguments"), ) .arg( Arg::with_name(arg::UNSORTED) .long("unsorted") .short("u") .help("Return list and summary entries in source order"), ) .arg( Arg::with_name(arg::UNSTABLE) .long("unstable") .help("Enable unstable features"), ) .arg( Arg::with_name(arg::VERBOSE) .short("v") .long("verbose") .multiple(true) .help("Use verbose output"), ) .arg( Arg::with_name(arg::WORKING_DIRECTORY) .short("d") .long("working-directory") .takes_value(true) .help("Use as working directory. --justfile must also be set") .requires(arg::JUSTFILE), ) .arg( Arg::with_name(cmd::CHANGELOG) .long("changelog") .help("Print changelog"), ) .arg(Arg::with_name(cmd::CHOOSE).long("choose").help(CHOOSE_HELP)) .arg( Arg::with_name(cmd::COMMAND) .long("command") .short("c") .min_values(1) .allow_hyphen_values(true) .help( "Run an arbitrary command with the working directory, `.env`, overrides, and exports \ set", ), ) .arg( Arg::with_name(cmd::COMPLETIONS) .long("completions") .takes_value(true) .value_name("SHELL") .possible_values(&clap::Shell::variants()) .set(ArgSettings::CaseInsensitive) .help("Print shell completion script for "), ) .arg( Arg::with_name(cmd::DUMP) .long("dump") .help("Print justfile"), ) .arg( Arg::with_name(cmd::EDIT) .short("e") .long("edit") .help("Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`"), ) .arg(Arg::with_name(cmd::EVALUATE).long("evaluate").help( "Evaluate and print all variables. If a variable name is given as an argument, only print \ that variable's value.", )) .arg( Arg::with_name(cmd::FORMAT) .long("fmt") .help("Format and overwrite justfile"), ) .arg( Arg::with_name(cmd::INIT) .long("init") .help("Initialize new justfile in project root"), ) .arg( Arg::with_name(cmd::LIST) .short("l") .long("list") .help("List available recipes and their arguments"), ) .arg( Arg::with_name(cmd::SHOW) .short("s") .long("show") .takes_value(true) .value_name("RECIPE") .help("Show information about "), ) .arg( Arg::with_name(cmd::SUMMARY) .long("summary") .help("List names of available recipes"), ) .arg( Arg::with_name(cmd::VARIABLES) .long("variables") .help("List names of variables"), ) .arg( Arg::with_name(arg::DOTENV_FILENAME) .long("dotenv-filename") .takes_value(true) .help("Search for environment file named instead of `.env`") .conflicts_with(arg::DOTENV_PATH), ) .arg( Arg::with_name(arg::DOTENV_PATH) .long("dotenv-path") .help("Load environment file at instead of searching for one") .takes_value(true), ) .group(ArgGroup::with_name("SUBCOMMAND").args(cmd::ALL)) .arg( Arg::with_name(arg::ARGUMENTS) .multiple(true) .help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"), ); if cfg!(feature = "help4help2man") { app.version(env!("CARGO_PKG_VERSION")).about(concat!( "- Please see ", env!("CARGO_PKG_HOMEPAGE"), " for more information." )) } else { app .version(env!("CARGO_PKG_VERSION")) .author(env!("CARGO_PKG_AUTHORS")) .about(concat!( env!("CARGO_PKG_DESCRIPTION"), " - ", env!("CARGO_PKG_HOMEPAGE") )) } } fn color_from_matches(matches: &ArgMatches) -> ConfigResult { let value = matches .value_of(arg::COLOR) .ok_or_else(|| ConfigError::Internal { message: "`--color` had no value".to_string(), })?; match value { arg::COLOR_AUTO => Ok(Color::auto()), arg::COLOR_ALWAYS => Ok(Color::always()), arg::COLOR_NEVER => Ok(Color::never()), _ => Err(ConfigError::Internal { message: format!("Invalid argument `{value}` to --color."), }), } } fn command_color_from_matches(matches: &ArgMatches) -> ConfigResult> { if let Some(value) = matches.value_of(arg::COMMAND_COLOR) { match value { arg::COMMAND_COLOR_BLACK => Ok(Some(ansi_term::Color::Black)), arg::COMMAND_COLOR_BLUE => Ok(Some(ansi_term::Color::Blue)), arg::COMMAND_COLOR_CYAN => Ok(Some(ansi_term::Color::Cyan)), arg::COMMAND_COLOR_GREEN => Ok(Some(ansi_term::Color::Green)), arg::COMMAND_COLOR_PURPLE => Ok(Some(ansi_term::Color::Purple)), arg::COMMAND_COLOR_RED => Ok(Some(ansi_term::Color::Red)), arg::COMMAND_COLOR_YELLOW => Ok(Some(ansi_term::Color::Yellow)), value => Err(ConfigError::Internal { message: format!("Invalid argument `{value}` to --command-color."), }), } } else { Ok(None) } } fn dump_format_from_matches(matches: &ArgMatches) -> ConfigResult { let value = matches .value_of(arg::DUMP_FORMAT) .ok_or_else(|| ConfigError::Internal { message: "`--dump-format` had no value".to_string(), })?; match value { arg::DUMP_FORMAT_JSON => Ok(DumpFormat::Json), arg::DUMP_FORMAT_JUST => Ok(DumpFormat::Just), _ => Err(ConfigError::Internal { message: format!("Invalid argument `{value}` to --dump-format."), }), } } pub(crate) fn from_matches(matches: &ArgMatches) -> ConfigResult { let invocation_directory = env::current_dir().context(config_error::CurrentDirContext)?; let verbosity = if matches.is_present(arg::QUIET) { Verbosity::Quiet } else { Verbosity::from_flag_occurrences(matches.occurrences_of(arg::VERBOSE)) }; let color = Self::color_from_matches(matches)?; let command_color = Self::command_color_from_matches(matches)?; let set_count = matches.occurrences_of(arg::SET); let mut overrides = BTreeMap::new(); if set_count > 0 { let mut values = matches.values_of(arg::SET).unwrap(); for _ in 0..set_count { overrides.insert( values.next().unwrap().to_owned(), values.next().unwrap().to_owned(), ); } } let positional = Positional::from_values(matches.values_of(arg::ARGUMENTS)); for (name, value) in positional.overrides { overrides.insert(name.clone(), value.clone()); } let search_config = { let justfile = matches.value_of(arg::JUSTFILE).map(PathBuf::from); let working_directory = matches.value_of(arg::WORKING_DIRECTORY).map(PathBuf::from); if let Some(search_directory) = positional.search_directory.map(PathBuf::from) { if justfile.is_some() || working_directory.is_some() { return Err(ConfigError::SearchDirConflict); } SearchConfig::FromSearchDirectory { search_directory } } else { match (justfile, working_directory) { (None, None) => SearchConfig::FromInvocationDirectory, (Some(justfile), None) => SearchConfig::WithJustfile { justfile }, (Some(justfile), Some(working_directory)) => { SearchConfig::WithJustfileAndWorkingDirectory { justfile, working_directory, } } (None, Some(_)) => { return Err(ConfigError::internal( "--working-directory set without --justfile", )) } } } }; for subcommand in cmd::ARGLESS { if matches.is_present(subcommand) { match (!overrides.is_empty(), !positional.arguments.is_empty()) { (false, false) => {} (true, false) => { return Err(ConfigError::SubcommandOverrides { subcommand, overrides, }); } (false, true) => { return Err(ConfigError::SubcommandArguments { arguments: positional.arguments, subcommand, }); } (true, true) => { return Err(ConfigError::SubcommandOverridesAndArguments { arguments: positional.arguments, subcommand, overrides, }); } } } } let subcommand = if matches.is_present(cmd::CHANGELOG) { Subcommand::Changelog } else if matches.is_present(cmd::CHOOSE) { Subcommand::Choose { chooser: matches.value_of(arg::CHOOSER).map(str::to_owned), overrides, } } else if let Some(values) = matches.values_of_os(cmd::COMMAND) { let mut arguments = values.map(OsStr::to_owned).collect::>(); Subcommand::Command { binary: arguments.remove(0), arguments, overrides, } } else if let Some(shell) = matches.value_of(cmd::COMPLETIONS) { Subcommand::Completions { shell: shell.to_owned(), } } else if matches.is_present(cmd::EDIT) { Subcommand::Edit } else if matches.is_present(cmd::SUMMARY) { Subcommand::Summary } else if matches.is_present(cmd::DUMP) { Subcommand::Dump } else if matches.is_present(cmd::FORMAT) { Subcommand::Format } else if matches.is_present(cmd::INIT) { Subcommand::Init } else if matches.is_present(cmd::LIST) { Subcommand::List } else if let Some(name) = matches.value_of(cmd::SHOW) { Subcommand::Show { name: name.to_owned(), } } else if matches.is_present(cmd::EVALUATE) { if positional.arguments.len() > 1 { return Err(ConfigError::SubcommandArguments { subcommand: cmd::EVALUATE, arguments: positional .arguments .into_iter() .skip(1) .collect::>(), }); } Subcommand::Evaluate { variable: positional.arguments.into_iter().next(), overrides, } } else if matches.is_present(cmd::VARIABLES) { Subcommand::Variables } else { Subcommand::Run { arguments: positional.arguments, overrides, } }; let shell_args = if matches.occurrences_of(arg::SHELL_ARG) > 0 || matches.occurrences_of(arg::CLEAR_SHELL_ARGS) > 0 { Some( matches .values_of(arg::SHELL_ARG) .map_or(Vec::new(), |shell_args| { shell_args.map(str::to_owned).collect() }), ) } else { None }; let unstable = matches.is_present(arg::UNSTABLE) || env::var_os("JUST_UNSTABLE") .map(|val| !(val == "false" || val == "0" || val.is_empty())) .unwrap_or_default(); Ok(Self { check: matches.is_present(arg::CHECK), color, command_color, dotenv_filename: matches.value_of(arg::DOTENV_FILENAME).map(str::to_owned), dotenv_path: matches.value_of(arg::DOTENV_PATH).map(PathBuf::from), dry_run: matches.is_present(arg::DRY_RUN), dump_format: Self::dump_format_from_matches(matches)?, highlight: !matches.is_present(arg::NO_HIGHLIGHT), invocation_directory, list_heading: matches .value_of(arg::LIST_HEADING) .unwrap_or("Available recipes:\n") .to_owned(), list_prefix: matches .value_of(arg::LIST_PREFIX) .unwrap_or(" ") .to_owned(), load_dotenv: !matches.is_present(arg::NO_DOTENV), search_config, shell: matches.value_of(arg::SHELL).map(str::to_owned), shell_args, shell_command: matches.is_present(arg::SHELL_COMMAND), subcommand, unsorted: matches.is_present(arg::UNSORTED), unstable, verbosity, yes: matches.is_present(arg::YES), }) } pub(crate) fn require_unstable(&self, message: &str) -> Result<(), Error<'static>> { if self.unstable { Ok(()) } else { Err(Error::Unstable { message: message.to_owned(), }) } } pub(crate) fn run(self, loader: &Loader) -> Result<(), Error> { if let Err(error) = InterruptHandler::install(self.verbosity) { warn!("Failed to set CTRL-C handler: {error}"); } self.subcommand.execute(&self, loader) } } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; macro_rules! test { { name: $name:ident, args: [$($arg:expr),*], $(color: $color:expr,)? $(dry_run: $dry_run:expr,)? $(dump_format: $dump_format:expr,)? $(highlight: $highlight:expr,)? $(search_config: $search_config:expr,)? $(shell: $shell:expr,)? $(shell_args: $shell_args:expr,)? $(subcommand: $subcommand:expr,)? $(unsorted: $unsorted:expr,)? $(verbosity: $verbosity:expr,)? } => { #[test] fn $name() { let arguments = &[ "just", $($arg,)* ]; let want = Config { $(color: $color,)? $(dry_run: $dry_run,)? $(dump_format: $dump_format,)? $(highlight: $highlight,)? $(search_config: $search_config,)? $(shell: $shell,)? $(shell_args: $shell_args,)? $(subcommand: $subcommand,)? $(unsorted: $unsorted,)? $(verbosity: $verbosity,)? ..testing::config(&[]) }; test(arguments, want); } } } fn test(arguments: &[&str], want: Config) { let app = Config::app(); let matches = app .get_matches_from_safe(arguments) .expect("argument parsing failed"); let have = Config::from_matches(&matches).expect("config parsing failed"); assert_eq!(have, want); } macro_rules! error { { name: $name:ident, args: [$($arg:expr),*], } => { #[test] fn $name() { let arguments = &[ "just", $($arg,)* ]; let app = Config::app(); app.get_matches_from_safe(arguments).expect_err("Expected clap error"); } }; { name: $name:ident, args: [$($arg:expr),*], error: $error:pat, $(check: $check:block,)? } => { #[test] fn $name() { let arguments = &[ "just", $($arg,)* ]; let app = Config::app(); let matches = app.get_matches_from_safe(arguments).expect("Matching fails"); match Config::from_matches(&matches).expect_err("config parsing succeeded") { $error => { $($check)? } other => panic!("Unexpected config error: {other}"), } } } } macro_rules! map { {} => { BTreeMap::new() }; { $($key:literal : $value:literal),* $(,)? } => { { let mut map: BTreeMap = BTreeMap::new(); $( map.insert($key.to_owned(), $value.to_owned()); )* map } } } test! { name: default_config, args: [], } test! { name: color_default, args: [], color: Color::auto(), } test! { name: color_never, args: ["--color", "never"], color: Color::never(), } test! { name: color_always, args: ["--color", "always"], color: Color::always(), } test! { name: color_auto, args: ["--color", "auto"], color: Color::auto(), } error! { name: color_bad_value, args: ["--color", "foo"], } test! { name: dry_run_default, args: [], dry_run: false, } test! { name: dry_run_long, args: ["--dry-run"], dry_run: true, } test! { name: dry_run_short, args: ["-n"], dry_run: true, } error! { name: dry_run_quiet, args: ["--dry-run", "--quiet"], } test! { name: highlight_default, args: [], highlight: true, } test! { name: highlight_yes, args: ["--highlight"], highlight: true, } test! { name: highlight_no, args: ["--no-highlight"], highlight: false, } test! { name: highlight_no_yes, args: ["--no-highlight", "--highlight"], highlight: true, } test! { name: highlight_no_yes_no, args: ["--no-highlight", "--highlight", "--no-highlight"], highlight: false, } test! { name: highlight_yes_no, args: ["--highlight", "--no-highlight"], highlight: false, } test! { name: unsorted_default, args: [], unsorted: false, } test! { name: unsorted_long, args: ["--unsorted"], unsorted: true, } test! { name: unsorted_short, args: ["-u"], unsorted: true, } test! { name: quiet_default, args: [], verbosity: Verbosity::Taciturn, } test! { name: quiet_long, args: ["--quiet"], verbosity: Verbosity::Quiet, } test! { name: quiet_short, args: ["-q"], verbosity: Verbosity::Quiet, } error! { name: dotenv_both_filename_and_path, args: ["--dotenv-filename", "foo", "--dotenv-path", "bar"], } test! { name: set_default, args: [], subcommand: Subcommand::Run { arguments: Vec::new(), overrides: map!(), }, } test! { name: set_one, args: ["--set", "foo", "bar"], subcommand: Subcommand::Run { arguments: Vec::new(), overrides: map!{"foo": "bar"}, }, } test! { name: set_empty, args: ["--set", "foo", ""], subcommand: Subcommand::Run { arguments: Vec::new(), overrides: map!{"foo": ""}, }, } test! { name: set_two, args: ["--set", "foo", "bar", "--set", "bar", "baz"], subcommand: Subcommand::Run { arguments: Vec::new(), overrides: map!{"foo": "bar", "bar": "baz"}, }, } test! { name: set_override, args: ["--set", "foo", "bar", "--set", "foo", "baz"], subcommand: Subcommand::Run { arguments: Vec::new(), overrides: map!{"foo": "baz"}, }, } error! { name: set_bad, args: ["--set", "foo"], } test! { name: shell_default, args: [], shell: None, shell_args: None, } test! { name: shell_set, args: ["--shell", "tclsh"], shell: Some("tclsh".to_owned()), } test! { name: shell_args_set, args: ["--shell-arg", "hello"], shell: None, shell_args: Some(vec!["hello".into()]), } test! { name: verbosity_default, args: [], verbosity: Verbosity::Taciturn, } test! { name: verbosity_long, args: ["--verbose"], verbosity: Verbosity::Loquacious, } test! { name: verbosity_loquacious, args: ["-v"], verbosity: Verbosity::Loquacious, } test! { name: verbosity_grandiloquent, args: ["-v", "-v"], verbosity: Verbosity::Grandiloquent, } test! { name: verbosity_great_grandiloquent, args: ["-v", "-v", "-v"], verbosity: Verbosity::Grandiloquent, } test! { name: subcommand_default, args: [], subcommand: Subcommand::Run { arguments: Vec::new(), overrides: map!{}, }, } error! { name: subcommand_conflict_changelog, args: ["--list", "--changelog"], } error! { name: subcommand_conflict_summary, args: ["--list", "--summary"], } error! { name: subcommand_conflict_dump, args: ["--list", "--dump"], } error! { name: subcommand_conflict_fmt, args: ["--list", "--fmt"], } error! { name: subcommand_conflict_init, args: ["--list", "--init"], } error! { name: subcommand_conflict_evaluate, args: ["--list", "--evaluate"], } error! { name: subcommand_conflict_show, args: ["--list", "--show"], } error! { name: subcommand_conflict_completions, args: ["--list", "--completions"], } error! { name: subcommand_conflict_variables, args: ["--list", "--variables"], } error! { name: subcommand_conflict_choose, args: ["--list", "--choose"], } test! { name: subcommand_completions, args: ["--completions", "bash"], subcommand: Subcommand::Completions{shell: "bash".to_owned()}, } test! { name: subcommand_completions_uppercase, args: ["--completions", "BASH"], subcommand: Subcommand::Completions{shell: "BASH".to_owned()}, } error! { name: subcommand_completions_invalid, args: ["--completions", "monstersh"], } test! { name: subcommand_dump, args: ["--dump"], subcommand: Subcommand::Dump, } test! { name: dump_format, args: ["--dump-format", "json"], dump_format: DumpFormat::Json, } test! { name: subcommand_edit, args: ["--edit"], subcommand: Subcommand::Edit, } test! { name: subcommand_evaluate, args: ["--evaluate"], subcommand: Subcommand::Evaluate { overrides: map!{}, variable: None, }, } test! { name: subcommand_evaluate_overrides, args: ["--evaluate", "x=y"], subcommand: Subcommand::Evaluate { overrides: map!{"x": "y"}, variable: None, }, } test! { name: subcommand_evaluate_overrides_with_argument, args: ["--evaluate", "x=y", "foo"], subcommand: Subcommand::Evaluate { overrides: map!{"x": "y"}, variable: Some("foo".to_owned()), }, } test! { name: subcommand_list_long, args: ["--list"], subcommand: Subcommand::List, } test! { name: subcommand_list_short, args: ["-l"], subcommand: Subcommand::List, } test! { name: subcommand_show_long, args: ["--show", "build"], subcommand: Subcommand::Show { name: String::from("build") }, } test! { name: subcommand_show_short, args: ["-s", "build"], subcommand: Subcommand::Show { name: String::from("build") }, } error! { name: subcommand_show_no_arg, args: ["--show"], } test! { name: subcommand_summary, args: ["--summary"], subcommand: Subcommand::Summary, } test! { name: arguments, args: ["foo", "bar"], subcommand: Subcommand::Run { arguments: vec![String::from("foo"), String::from("bar")], overrides: map!{}, }, } test! { name: arguments_leading_equals, args: ["=foo"], subcommand: Subcommand::Run { arguments: vec!["=foo".to_owned()], overrides: map!{}, }, } test! { name: overrides, args: ["foo=bar", "bar=baz"], subcommand: Subcommand::Run { arguments: Vec::new(), overrides: map!{"foo": "bar", "bar": "baz"}, }, } test! { name: overrides_empty, args: ["foo=", "bar="], subcommand: Subcommand::Run { arguments: Vec::new(), overrides: map!{"foo": "", "bar": ""}, }, } test! { name: overrides_override_sets, args: ["--set", "foo", "0", "--set", "bar", "1", "foo=bar", "bar=baz"], subcommand: Subcommand::Run { arguments: Vec::new(), overrides: map!{"foo": "bar", "bar": "baz"}, }, } test! { name: shell_args_default, args: [], } test! { name: shell_args_set_hyphen, args: ["--shell-arg", "--foo"], shell_args: Some(vec!["--foo".to_owned()]), } test! { name: shell_args_set_word, args: ["--shell-arg", "foo"], shell_args: Some(vec!["foo".to_owned()]), } test! { name: shell_args_set_multiple, args: ["--shell-arg", "foo", "--shell-arg", "bar"], shell_args: Some(vec!["foo".to_owned(), "bar".to_owned()]), } test! { name: shell_args_clear, args: ["--clear-shell-args"], shell_args: Some(vec![]), } test! { name: shell_args_clear_and_set, args: ["--clear-shell-args", "--shell-arg", "bar"], shell_args: Some(vec!["bar".to_owned()]), } test! { name: shell_args_set_and_clear, args: ["--shell-arg", "bar", "--clear-shell-args"], shell_args: Some(vec![]), } test! { name: shell_args_set_multiple_and_clear, args: ["--shell-arg", "bar", "--shell-arg", "baz", "--clear-shell-args"], shell_args: Some(vec![]), } test! { name: search_config_default, args: [], search_config: SearchConfig::FromInvocationDirectory, } test! { name: search_config_from_working_directory_and_justfile, args: ["--working-directory", "foo", "--justfile", "bar"], search_config: SearchConfig::WithJustfileAndWorkingDirectory { justfile: PathBuf::from("bar"), working_directory: PathBuf::from("foo"), }, } test! { name: search_config_justfile_long, args: ["--justfile", "foo"], search_config: SearchConfig::WithJustfile { justfile: PathBuf::from("foo"), }, } test! { name: search_config_justfile_short, args: ["-f", "foo"], search_config: SearchConfig::WithJustfile { justfile: PathBuf::from("foo"), }, } test! { name: search_directory_parent, args: ["../"], search_config: SearchConfig::FromSearchDirectory { search_directory: PathBuf::from(".."), }, } test! { name: search_directory_parent_with_recipe, args: ["../build"], search_config: SearchConfig::FromSearchDirectory { search_directory: PathBuf::from(".."), }, subcommand: Subcommand::Run { arguments: vec!["build".to_owned()], overrides: BTreeMap::new() }, } test! { name: search_directory_child, args: ["foo/"], search_config: SearchConfig::FromSearchDirectory { search_directory: PathBuf::from("foo"), }, } test! { name: search_directory_deep, args: ["foo/bar/"], search_config: SearchConfig::FromSearchDirectory { search_directory: PathBuf::from("foo/bar"), }, } test! { name: search_directory_child_with_recipe, args: ["foo/build"], search_config: SearchConfig::FromSearchDirectory { search_directory: PathBuf::from("foo"), }, subcommand: Subcommand::Run { arguments: vec!["build".to_owned()], overrides: BTreeMap::new() }, } error! { name: search_directory_conflict_justfile, args: ["--justfile", "bar", "foo/build"], error: ConfigError::SearchDirConflict, } error! { name: search_directory_conflict_working_directory, args: ["--justfile", "bar", "--working-directory", "baz", "foo/build"], error: ConfigError::SearchDirConflict, } error! { name: completions_arguments, args: ["--completions", "zsh", "foo"], error: ConfigError::SubcommandArguments { subcommand, arguments }, check: { assert_eq!(subcommand, cmd::COMPLETIONS); assert_eq!(arguments, &["foo"]); }, } error! { name: changelog_arguments, args: ["--changelog", "bar"], error: ConfigError::SubcommandArguments { subcommand, arguments }, check: { assert_eq!(subcommand, cmd::CHANGELOG); assert_eq!(arguments, &["bar"]); }, } error! { name: list_arguments, args: ["--list", "bar"], error: ConfigError::SubcommandArguments { subcommand, arguments }, check: { assert_eq!(subcommand, cmd::LIST); assert_eq!(arguments, &["bar"]); }, } error! { name: dump_arguments, args: ["--dump", "bar"], error: ConfigError::SubcommandArguments { subcommand, arguments }, check: { assert_eq!(subcommand, cmd::DUMP); assert_eq!(arguments, &["bar"]); }, } error! { name: edit_arguments, args: ["--edit", "bar"], error: ConfigError::SubcommandArguments { subcommand, arguments }, check: { assert_eq!(subcommand, cmd::EDIT); assert_eq!(arguments, &["bar"]); }, } error! { name: fmt_arguments, args: ["--fmt", "bar"], error: ConfigError::SubcommandArguments { subcommand, arguments }, check: { assert_eq!(subcommand, cmd::FORMAT); assert_eq!(arguments, &["bar"]); }, } error! { name: init_arguments, args: ["--init", "bar"], error: ConfigError::SubcommandArguments { subcommand, arguments }, check: { assert_eq!(subcommand, cmd::INIT); assert_eq!(arguments, &["bar"]); }, } error! { name: show_arguments, args: ["--show", "foo", "bar"], error: ConfigError::SubcommandArguments { subcommand, arguments }, check: { assert_eq!(subcommand, cmd::SHOW); assert_eq!(arguments, &["bar"]); }, } error! { name: summary_arguments, args: ["--summary", "bar"], error: ConfigError::SubcommandArguments { subcommand, arguments }, check: { assert_eq!(subcommand, cmd::SUMMARY); assert_eq!(arguments, &["bar"]); }, } error! { name: subcommand_overrides_and_arguments, args: ["--summary", "bar=baz", "bar"], error: ConfigError::SubcommandOverridesAndArguments { subcommand, arguments, overrides }, check: { assert_eq!(subcommand, cmd::SUMMARY); assert_eq!(overrides, map!{"bar": "baz"}); assert_eq!(arguments, &["bar"]); }, } error! { name: summary_overrides, args: ["--summary", "bar=baz"], error: ConfigError::SubcommandOverrides { subcommand, overrides }, check: { assert_eq!(subcommand, cmd::SUMMARY); assert_eq!(overrides, map!{"bar": "baz"}); }, } } just-1.21.0/src/config_error.rs000064400000000000000000000032011046102023000145010ustar 00000000000000use super::*; #[derive(Debug, Snafu)] #[snafu(visibility(pub(crate)), context(suffix(Context)))] pub(crate) enum ConfigError { #[snafu(display("Failed to get current directory: {}", source))] CurrentDir { source: io::Error }, #[snafu(display( "Internal config error, this may indicate a bug in just: {} \ consider filing an issue: https://github.com/casey/just/issues/new", message ))] Internal { message: String }, #[snafu(display( "Path-prefixed recipes may not be used with `--working-directory` or `--justfile`." ))] SearchDirConflict, #[snafu(display( "`--{}` used with unexpected {}: {}", subcommand.to_lowercase(), Count("argument", arguments.len()), List::and_ticked(arguments) ))] SubcommandArguments { subcommand: &'static str, arguments: Vec, }, #[snafu(display( "`--{}` used with unexpected overrides: {}", subcommand.to_lowercase(), List::and_ticked(overrides.iter().map(|(key, value)| format!("{key}={value}"))), ))] SubcommandOverrides { subcommand: &'static str, overrides: BTreeMap, }, #[snafu(display( "`--{}` used with unexpected overrides: {}; and arguments: {}", subcommand.to_lowercase(), List::and_ticked(overrides.iter().map(|(key, value)| format!("{key}={value}"))), List::and_ticked(arguments))) ] SubcommandOverridesAndArguments { subcommand: &'static str, overrides: BTreeMap, arguments: Vec, }, } impl ConfigError { pub(crate) fn internal(message: impl Into) -> Self { Self::Internal { message: message.into(), } } } just-1.21.0/src/count.rs000064400000000000000000000007571046102023000131700ustar 00000000000000use super::*; pub struct Count(pub T, pub usize); impl Display for Count { fn fmt(&self, f: &mut Formatter) -> fmt::Result { if self.1 == 1 { write!(f, "{}", self.0) } else { write!(f, "{}s", self.0) } } } #[cfg(test)] mod tests { use super::*; #[test] fn count() { assert_eq!(Count("dog", 0).to_string(), "dogs"); assert_eq!(Count("dog", 1).to_string(), "dog"); assert_eq!(Count("dog", 2).to_string(), "dogs"); } } just-1.21.0/src/delimiter.rs000064400000000000000000000006361046102023000140120ustar 00000000000000#[derive(PartialEq, Eq, Debug, Copy, Clone)] pub(crate) enum Delimiter { Brace, Bracket, Paren, } impl Delimiter { pub(crate) fn open(self) -> char { match self { Self::Brace => '{', Self::Bracket => '[', Self::Paren => '(', } } pub(crate) fn close(self) -> char { match self { Self::Brace => '}', Self::Bracket => ']', Self::Paren => ')', } } } just-1.21.0/src/dependency.rs000064400000000000000000000011121046102023000141400ustar 00000000000000use super::*; #[derive(PartialEq, Debug, Serialize)] pub(crate) struct Dependency<'src> { pub(crate) arguments: Vec>, #[serde(serialize_with = "keyed::serialize")] pub(crate) recipe: Rc>, } impl<'src> Display for Dependency<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { if self.arguments.is_empty() { write!(f, "{}", self.recipe.name()) } else { write!(f, "({}", self.recipe.name())?; for argument in &self.arguments { write!(f, " {argument}")?; } write!(f, ")") } } } just-1.21.0/src/dump_format.rs000064400000000000000000000001131046102023000143370ustar 00000000000000#[derive(Debug, PartialEq)] pub(crate) enum DumpFormat { Json, Just, } just-1.21.0/src/enclosure.rs000064400000000000000000000010211046102023000140200ustar 00000000000000use super::*; pub struct Enclosure { enclosure: &'static str, value: T, } impl Enclosure { pub fn tick(value: T) -> Enclosure { Self { enclosure: "`", value, } } } impl Display for Enclosure { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "{}{}{}", self.enclosure, self.value, self.enclosure) } } #[cfg(test)] mod tests { use super::*; #[test] fn tick() { assert_eq!(Enclosure::tick("foo").to_string(), "`foo`"); } } just-1.21.0/src/error.rs000064400000000000000000000357331046102023000131730ustar 00000000000000use super::*; #[derive(Debug)] pub(crate) enum Error<'src> { AmbiguousModuleFile { module: Name<'src>, found: Vec, }, ArgumentCountMismatch { recipe: &'src str, parameters: Vec>, found: usize, min: usize, max: usize, }, Backtick { token: Token<'src>, output_error: OutputError, }, ChooserInvoke { shell_binary: String, shell_arguments: String, chooser: OsString, io_error: io::Error, }, ChooserRead { chooser: OsString, io_error: io::Error, }, ChooserStatus { chooser: OsString, status: ExitStatus, }, ChooserWrite { chooser: OsString, io_error: io::Error, }, CircularImport { current: PathBuf, import: PathBuf, }, Code { recipe: &'src str, line_number: Option, code: i32, print_message: bool, }, CommandInvoke { binary: OsString, arguments: Vec, io_error: io::Error, }, CommandStatus { binary: OsString, arguments: Vec, status: ExitStatus, }, Compile { compile_error: CompileError<'src>, }, Config { config_error: ConfigError, }, Cygpath { recipe: &'src str, output_error: OutputError, }, DefaultRecipeRequiresArguments { recipe: &'src str, min_arguments: usize, }, Dotenv { dotenv_error: dotenvy::Error, }, DumpJson { serde_json_error: serde_json::Error, }, EditorInvoke { editor: OsString, io_error: io::Error, }, EditorStatus { editor: OsString, status: ExitStatus, }, EvalUnknownVariable { variable: String, suggestion: Option>, }, FormatCheckFoundDiff, FunctionCall { function: Name<'src>, message: String, }, GetConfirmation { io_error: io::Error, }, Homedir, InitExists { justfile: PathBuf, }, Internal { message: String, }, Io { recipe: &'src str, io_error: io::Error, }, Load { path: PathBuf, io_error: io::Error, }, MissingImportFile { path: Token<'src>, }, MissingModuleFile { module: Name<'src>, }, NoChoosableRecipes, NoDefaultRecipe, NoRecipes, NotConfirmed { recipe: &'src str, }, RegexCompile { source: regex::Error, }, Search { search_error: SearchError, }, Shebang { recipe: &'src str, command: String, argument: Option, io_error: io::Error, }, Signal { recipe: &'src str, line_number: Option, signal: i32, }, TmpdirIo { recipe: &'src str, io_error: io::Error, }, Unknown { recipe: &'src str, line_number: Option, }, UnknownOverrides { overrides: Vec, }, UnknownRecipes { recipes: Vec, suggestion: Option>, }, Unstable { message: String, }, WriteJustfile { justfile: PathBuf, io_error: io::Error, }, } impl<'src> Error<'src> { pub(crate) fn code(&self) -> Option { match self { Self::Code { code, .. } | Self::Backtick { output_error: OutputError::Code(code), .. } => Some(*code), Self::ChooserStatus { status, .. } | Self::EditorStatus { status, .. } => status.code(), _ => None, } } fn context(&self) -> Option> { match self { Self::AmbiguousModuleFile { module, .. } | Self::MissingModuleFile { module, .. } => { Some(module.token()) } Self::Backtick { token, .. } => Some(*token), Self::Compile { compile_error } => Some(compile_error.context()), Self::FunctionCall { function, .. } => Some(function.token()), Self::MissingImportFile { path } => Some(*path), _ => None, } } pub(crate) fn internal(message: impl Into) -> Self { Self::Internal { message: message.into(), } } pub(crate) fn print_message(&self) -> bool { !matches!( self, Error::Code { print_message: false, .. } ) } } impl<'src> From> for Error<'src> { fn from(compile_error: CompileError<'src>) -> Self { Self::Compile { compile_error } } } impl<'src> From for Error<'src> { fn from(config_error: ConfigError) -> Self { Self::Config { config_error } } } impl<'src> From for Error<'src> { fn from(dotenv_error: dotenvy::Error) -> Error<'src> { Self::Dotenv { dotenv_error } } } impl<'src> From for Error<'src> { fn from(search_error: SearchError) -> Self { Self::Search { search_error } } } impl<'src> ColorDisplay for Error<'src> { fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result { use Error::*; let error = color.error().paint("error"); let message = color.message().prefix(); write!(f, "{error}: {message}")?; match self { AmbiguousModuleFile { module, found } => write!(f, "Found multiple source files for module `{module}`: {}", List::and_ticked(found), )?, ArgumentCountMismatch { recipe, found, min, max, .. } => { let count = Count("argument", *found); if min == max { let expected = min; let only = if expected < found { "only " } else { "" }; write!(f, "Recipe `{recipe}` got {found} {count} but {only}takes {expected}")?; } else if found < min { write!(f, "Recipe `{recipe}` got {found} {count} but takes at least {min}")?; } else if found > max { write!(f, "Recipe `{recipe}` got {found} {count} but takes at most {max}")?; } } Backtick { output_error, .. } => match output_error { OutputError::Code(code) => write!(f, "Backtick failed with exit code {code}")?, OutputError::Signal(signal) => write!(f, "Backtick was terminated by signal {signal}")?, OutputError::Unknown => write!(f, "Backtick failed for an unknown reason")?, OutputError::Io(io_error) => match io_error.kind() { io::ErrorKind::NotFound => write!(f, "Backtick could not be run because just could not find the shell:\n{io_error}"), io::ErrorKind::PermissionDenied => write!(f, "Backtick could not be run because just could not run the shell:\n{io_error}"), _ => write!(f, "Backtick could not be run because of an IO error while launching the shell:\n{io_error}"), }?, OutputError::Utf8(utf8_error) => write!(f, "Backtick succeeded but stdout was not utf8: {utf8_error}")?, } ChooserInvoke { shell_binary, shell_arguments, chooser, io_error} => { let chooser = chooser.to_string_lossy(); write!(f, "Chooser `{shell_binary} {shell_arguments} {chooser}` invocation failed: {io_error}")?; } ChooserRead { chooser, io_error } => { let chooser = chooser.to_string_lossy(); write!(f, "Failed to read output from chooser `{chooser}`: {io_error}")?; } ChooserStatus { chooser, status } => { let chooser = chooser.to_string_lossy(); write!(f, "Chooser `{chooser}` failed: {status}")?; } ChooserWrite { chooser, io_error } => { let chooser = chooser.to_string_lossy(); write!(f, "Failed to write to chooser `{chooser}`: {io_error}")?; } CircularImport { current, import } => { let import = import.display(); let current = current.display(); write!(f, "Import `{import}` in `{current}` is circular")?; } Code { recipe, line_number, code, .. } => { if let Some(n) = line_number { write!(f, "Recipe `{recipe}` failed on line {n} with exit code {code}")?; } else { write!(f, "Recipe `{recipe}` failed with exit code {code}")?; } } CommandInvoke { binary, arguments, io_error } => { let cmd = format_cmd(binary, arguments); write!(f, "Failed to invoke {cmd}: {io_error}")?; } CommandStatus { binary, arguments, status} => { let cmd = format_cmd(binary, arguments); write!(f, "Command {cmd} failed: {status}")?; } Compile { compile_error } => Display::fmt(compile_error, f)?, Config { config_error } => Display::fmt(config_error, f)?, Cygpath { recipe, output_error} => match output_error { OutputError::Code(code) => write!(f, "Cygpath failed with exit code {code} while translating recipe `{recipe}` shebang interpreter path")?, OutputError::Signal(signal) => write!(f, "Cygpath terminated by signal {signal} while translating recipe `{recipe}` shebang interpreter path")?, OutputError::Unknown => write!(f, "Cygpath experienced an unknown failure while translating recipe `{recipe}` shebang interpreter path")?, OutputError::Io(io_error) => { match io_error.kind() { io::ErrorKind::NotFound => write!(f, "Could not find `cygpath` executable to translate recipe `{recipe}` shebang interpreter path:\n{io_error}"), io::ErrorKind::PermissionDenied => write!(f, "Could not run `cygpath` executable to translate recipe `{recipe}` shebang interpreter path:\n{io_error}"), _ => write!(f, "Could not run `cygpath` executable:\n{io_error}"), }?; } OutputError::Utf8(utf8_error) => write!(f, "Cygpath successfully translated recipe `{recipe}` shebang interpreter path, but output was not utf8: {utf8_error}")?, } DefaultRecipeRequiresArguments { recipe, min_arguments} => { let count = Count("argument", *min_arguments); write!(f, "Recipe `{recipe}` cannot be used as default recipe since it requires at least {min_arguments} {count}.")?; } Dotenv { dotenv_error } => { write!(f, "Failed to load environment file: {dotenv_error}")?; } DumpJson { serde_json_error } => { write!(f, "Failed to dump JSON to stdout: {serde_json_error}")?; } EditorInvoke { editor, io_error } => { let editor = editor.to_string_lossy(); write!(f, "Editor `{editor}` invocation failed: {io_error}")?; } EditorStatus { editor, status } => { let editor = editor.to_string_lossy(); write!(f, "Editor `{editor}` failed: {status}")?; } EvalUnknownVariable { variable, suggestion} => { write!(f, "Justfile does not contain variable `{variable}`.")?; if let Some(suggestion) = suggestion { write!(f, "\n{suggestion}")?; } } FormatCheckFoundDiff => { write!(f, "Formatted justfile differs from original.")?; } FunctionCall { function, message } => { let function = function.lexeme(); write!(f, "Call to function `{function}` failed: {message}")?; } GetConfirmation { io_error } => { write!(f, "Failed to read confirmation from stdin: {io_error}")?; } Homedir => { write!(f, "Failed to get homedir")?; } InitExists { justfile } => { write!(f, "Justfile `{}` already exists", justfile.display())?; } Internal { message } => { write!(f, "Internal runtime error, this may indicate a bug in just: {message} \ consider filing an issue: https://github.com/casey/just/issues/new")?; } Io { recipe, io_error } => { match io_error.kind() { io::ErrorKind::NotFound => write!(f, "Recipe `{recipe}` could not be run because just could not find the shell: {io_error}"), io::ErrorKind::PermissionDenied => write!(f, "Recipe `{recipe}` could not be run because just could not run the shell: {io_error}"), _ => write!(f, "Recipe `{recipe}` could not be run because of an IO error while launching the shell: {io_error}"), }?; } Load { io_error, path } => { let path = path.display(); write!(f, "Failed to read justfile at `{path}`: {io_error}")?; } MissingImportFile { .. } => write!(f, "Could not find source file for import.")?, MissingModuleFile { module } => write!(f, "Could not find source file for module `{module}`.")?, NoChoosableRecipes => write!(f, "Justfile contains no choosable recipes.")?, NoDefaultRecipe => write!(f, "Justfile contains no default recipe.")?, NoRecipes => write!(f, "Justfile contains no recipes.")?, NotConfirmed { recipe } => { write!(f, "Recipe `{recipe}` was not confirmed")?; } RegexCompile { source } => write!(f, "{source}")?, Search { search_error } => Display::fmt(search_error, f)?, Shebang { recipe, command, argument, io_error} => { if let Some(argument) = argument { write!(f, "Recipe `{recipe}` with shebang `#!{command} {argument}` execution error: {io_error}")?; } else { write!(f, "Recipe `{recipe}` with shebang `#!{command}` execution error: {io_error}")?; } } Signal { recipe, line_number, signal } => { if let Some(n) = line_number { write!(f, "Recipe `{recipe}` was terminated on line {n} by signal {signal}")?; } else { write!(f, "Recipe `{recipe}` was terminated by signal {signal}")?; } } TmpdirIo { recipe, io_error } => { write!(f, "Recipe `{recipe}` could not be run because of an IO error while trying to create a temporary \ directory or write a file to that directory`:{io_error}")?; } Unknown { recipe, line_number} => { if let Some(n) = line_number { write!(f, "Recipe `{recipe}` failed on line {n} for an unknown reason")?; } else { write!(f, "Recipe `{recipe}` failed for an unknown reason")?; } } UnknownOverrides { overrides } => { let count = Count("Variable", overrides.len()); let overrides = List::and_ticked(overrides); write!(f, "{count} {overrides} overridden on the command line but not present in justfile")?; } UnknownRecipes { recipes, suggestion } => { let count = Count("recipe", recipes.len()); let recipes = List::or_ticked(recipes); write!(f, "Justfile does not contain {count} {recipes}.")?; if let Some(suggestion) = suggestion { write!(f, "\n{suggestion}")?; } } Unstable { message } => { write!(f, "{message} Invoke `just` with the `--unstable` flag to enable unstable features.")?; } WriteJustfile { justfile, io_error } => { let justfile = justfile.display(); write!(f, "Failed to write justfile to `{justfile}`: {io_error}")?; } } write!(f, "{}", color.message().suffix())?; if let ArgumentCountMismatch { recipe, parameters, .. } = self { writeln!(f)?; write!(f, "{}:\n just {recipe}", color.message().paint("usage"))?; for param in parameters { write!(f, " {}", param.color_display(color))?; } } if let Some(token) = self.context() { writeln!(f)?; write!(f, "{}", token.color_display(color.error()))?; } Ok(()) } } fn format_cmd(binary: &OsString, arguments: &Vec) -> String { iter::once(binary) .chain(arguments) .map(|value| Enclosure::tick(value.to_string_lossy()).to_string()) .collect::>() .join(" ") } just-1.21.0/src/evaluator.rs000064400000000000000000000233231046102023000140340ustar 00000000000000use super::*; pub(crate) struct Evaluator<'src: 'run, 'run> { assignments: Option<&'run Table<'src, Assignment<'src>>>, config: &'run Config, dotenv: &'run BTreeMap, scope: Scope<'src, 'run>, settings: &'run Settings<'run>, search: &'run Search, } impl<'src, 'run> Evaluator<'src, 'run> { pub(crate) fn evaluate_assignments( assignments: &'run Table<'src, Assignment<'src>>, config: &'run Config, dotenv: &'run BTreeMap, overrides: Scope<'src, 'run>, settings: &'run Settings<'run>, search: &'run Search, ) -> RunResult<'src, Scope<'src, 'run>> { let mut evaluator = Self { scope: overrides, assignments: Some(assignments), config, dotenv, settings, search, }; for assignment in assignments.values() { evaluator.evaluate_assignment(assignment)?; } Ok(evaluator.scope) } fn evaluate_assignment(&mut self, assignment: &Assignment<'src>) -> RunResult<'src, &str> { let name = assignment.name.lexeme(); if !self.scope.bound(name) { let value = self.evaluate_expression(&assignment.value)?; self.scope.bind(assignment.export, assignment.name, value); } Ok(self.scope.value(name).unwrap()) } pub(crate) fn evaluate_expression( &mut self, expression: &Expression<'src>, ) -> RunResult<'src, String> { match expression { Expression::Variable { name, .. } => { let variable = name.lexeme(); if let Some(value) = self.scope.value(variable) { Ok(value.to_owned()) } else if let Some(assignment) = self .assignments .and_then(|assignments| assignments.get(variable)) { Ok(self.evaluate_assignment(assignment)?.to_owned()) } else { Err(Error::Internal { message: format!("attempted to evaluate undefined variable `{variable}`"), }) } } Expression::Call { thunk } => { use Thunk::*; let context = FunctionContext { dotenv: self.dotenv, invocation_directory: &self.config.invocation_directory, search: self.search, }; match thunk { Nullary { name, function, .. } => { function(&context).map_err(|message| Error::FunctionCall { function: *name, message, }) } Unary { name, function, arg, .. } => function(&context, &self.evaluate_expression(arg)?).map_err(|message| { Error::FunctionCall { function: *name, message, } }), UnaryOpt { name, function, args: (a, b), .. } => { let a = self.evaluate_expression(a)?; let b = match b.as_ref() { Some(b) => Some(self.evaluate_expression(b)?), None => None, }; function(&context, &a, b.as_deref()).map_err(|message| Error::FunctionCall { function: *name, message, }) } Binary { name, function, args: [a, b], .. } => function( &context, &self.evaluate_expression(a)?, &self.evaluate_expression(b)?, ) .map_err(|message| Error::FunctionCall { function: *name, message, }), BinaryPlus { name, function, args: ([a, b], rest), .. } => { let a = self.evaluate_expression(a)?; let b = self.evaluate_expression(b)?; let mut rest_evaluated = Vec::new(); for arg in rest { rest_evaluated.push(self.evaluate_expression(arg)?); } function(&context, &a, &b, &rest_evaluated).map_err(|message| Error::FunctionCall { function: *name, message, }) } Ternary { name, function, args: [a, b, c], .. } => function( &context, &self.evaluate_expression(a)?, &self.evaluate_expression(b)?, &self.evaluate_expression(c)?, ) .map_err(|message| Error::FunctionCall { function: *name, message, }), } } Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()), Expression::Backtick { contents, token } => { if self.config.dry_run { Ok(format!("`{contents}`")) } else { Ok(self.run_backtick(contents, token)?) } } Expression::Concatenation { lhs, rhs } => { Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(rhs)?) } Expression::Conditional { lhs, rhs, then, otherwise, operator, } => { let lhs_value = self.evaluate_expression(lhs)?; let rhs_value = self.evaluate_expression(rhs)?; let condition = match operator { ConditionalOperator::Equality => lhs_value == rhs_value, ConditionalOperator::Inequality => lhs_value != rhs_value, ConditionalOperator::RegexMatch => Regex::new(&rhs_value) .map_err(|source| Error::RegexCompile { source })? .is_match(&lhs_value), }; if condition { self.evaluate_expression(then) } else { self.evaluate_expression(otherwise) } } Expression::Group { contents } => self.evaluate_expression(contents), Expression::Join { lhs: None, rhs } => Ok("/".to_string() + &self.evaluate_expression(rhs)?), Expression::Join { lhs: Some(lhs), rhs, } => Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?), } } fn run_backtick(&self, raw: &str, token: &Token<'src>) -> RunResult<'src, String> { let mut cmd = self.settings.shell_command(self.config); cmd.arg(raw); cmd.current_dir(&self.search.working_directory); cmd.export(self.settings, self.dotenv, &self.scope); cmd.stdin(Stdio::inherit()); cmd.stderr(if self.config.verbosity.quiet() { Stdio::null() } else { Stdio::inherit() }); InterruptHandler::guard(|| { output(cmd).map_err(|output_error| Error::Backtick { token: *token, output_error, }) }) } pub(crate) fn evaluate_line( &mut self, line: &Line<'src>, continued: bool, ) -> RunResult<'src, String> { let mut evaluated = String::new(); for (i, fragment) in line.fragments.iter().enumerate() { match fragment { Fragment::Text { token } => { let lexeme = token.lexeme().replace("{{{{", "{{"); if i == 0 && continued { evaluated += lexeme.trim_start(); } else { evaluated += &lexeme; } } Fragment::Interpolation { expression } => { evaluated += &self.evaluate_expression(expression)?; } } } Ok(evaluated) } pub(crate) fn evaluate_parameters( config: &'run Config, dotenv: &'run BTreeMap, parameters: &[Parameter<'src>], arguments: &[&str], scope: &'run Scope<'src, 'run>, settings: &'run Settings, search: &'run Search, ) -> RunResult<'src, (Scope<'src, 'run>, Vec)> { let mut evaluator = Self { assignments: None, scope: scope.child(), search, settings, dotenv, config, }; let mut scope = scope.child(); let mut positional = Vec::new(); let mut rest = arguments; for parameter in parameters { let value = if rest.is_empty() { if let Some(ref default) = parameter.default { let value = evaluator.evaluate_expression(default)?; positional.push(value.clone()); value } else if parameter.kind == ParameterKind::Star { String::new() } else { return Err(Error::Internal { message: "missing parameter without default".to_owned(), }); } } else if parameter.kind.is_variadic() { for value in rest { positional.push((*value).to_owned()); } let value = rest.to_vec().join(" "); rest = &[]; value } else { let value = rest[0].to_owned(); positional.push(value.clone()); rest = &rest[1..]; value }; scope.bind(parameter.export, parameter.name, value); } Ok((scope, positional)) } pub(crate) fn recipe_evaluator( config: &'run Config, dotenv: &'run BTreeMap, scope: &'run Scope<'src, 'run>, settings: &'run Settings, search: &'run Search, ) -> Evaluator<'src, 'run> { Self { assignments: None, scope: Scope::child(scope), search, settings, dotenv, config, } } } #[cfg(test)] mod tests { use super::*; run_error! { name: backtick_code, src: " a: echo {{`f() { return 100; }; f`}} ", args: ["a"], error: Error::Backtick { token, output_error: OutputError::Code(code), }, check: { assert_eq!(code, 100); assert_eq!(token.lexeme(), "`f() { return 100; }; f`"); } } run_error! { name: export_assignment_backtick, src: r#" export exported_variable := "A" b := `echo $exported_variable` recipe: echo {{b}} "#, args: ["--quiet", "recipe"], error: Error::Backtick { token, output_error: OutputError::Code(_), }, check: { assert_eq!(token.lexeme(), "`echo $exported_variable`"); } } } just-1.21.0/src/expression.rs000064400000000000000000000077111046102023000142340ustar 00000000000000use super::*; /// An expression. Note that the Just language grammar has both an `expression` /// production of additions (`a + b`) and values, and a `value` production of /// all other value types (for example strings, function calls, and /// parenthetical groups). /// /// The parser parses both values and expressions into `Expression`s. #[derive(PartialEq, Debug, Clone)] pub(crate) enum Expression<'src> { /// `contents` Backtick { contents: String, token: Token<'src>, }, /// `name(arguments)` Call { thunk: Thunk<'src> }, /// `lhs + rhs` Concatenation { lhs: Box>, rhs: Box>, }, /// `if lhs == rhs { then } else { otherwise }` Conditional { lhs: Box>, rhs: Box>, then: Box>, otherwise: Box>, operator: ConditionalOperator, }, /// `(contents)` Group { contents: Box> }, /// `lhs / rhs` Join { lhs: Option>>, rhs: Box>, }, /// `"string_literal"` or `'string_literal'` StringLiteral { string_literal: StringLiteral<'src> }, /// `variable` Variable { name: Name<'src> }, } impl<'src> Expression<'src> { pub(crate) fn variables<'expression>(&'expression self) -> Variables<'expression, 'src> { Variables::new(self) } } impl<'src> Display for Expression<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { match self { Expression::Backtick { token, .. } => write!(f, "{}", token.lexeme()), Expression::Join { lhs: None, rhs } => write!(f, "/ {rhs}"), Expression::Join { lhs: Some(lhs), rhs, } => write!(f, "{lhs} / {rhs}"), Expression::Concatenation { lhs, rhs } => write!(f, "{lhs} + {rhs}"), Expression::Conditional { lhs, rhs, then, otherwise, operator, } => write!( f, "if {lhs} {operator} {rhs} {{ {then} }} else {{ {otherwise} }}" ), Expression::StringLiteral { string_literal } => write!(f, "{string_literal}"), Expression::Variable { name } => write!(f, "{}", name.lexeme()), Expression::Call { thunk } => write!(f, "{thunk}"), Expression::Group { contents } => write!(f, "({contents})"), } } } impl<'src> Serialize for Expression<'src> { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { Self::Backtick { contents, .. } => { let mut seq = serializer.serialize_seq(None)?; seq.serialize_element("evaluate")?; seq.serialize_element(contents)?; seq.end() } Self::Call { thunk } => thunk.serialize(serializer), Self::Concatenation { lhs, rhs } => { let mut seq = serializer.serialize_seq(None)?; seq.serialize_element("concatenate")?; seq.serialize_element(lhs)?; seq.serialize_element(rhs)?; seq.end() } Self::Join { lhs, rhs } => { let mut seq = serializer.serialize_seq(None)?; seq.serialize_element("join")?; seq.serialize_element(lhs)?; seq.serialize_element(rhs)?; seq.end() } Self::Conditional { lhs, rhs, then, otherwise, operator, } => { let mut seq = serializer.serialize_seq(None)?; seq.serialize_element("if")?; seq.serialize_element(&operator.to_string())?; seq.serialize_element(lhs)?; seq.serialize_element(rhs)?; seq.serialize_element(then)?; seq.serialize_element(otherwise)?; seq.end() } Self::Group { contents } => contents.serialize(serializer), Self::StringLiteral { string_literal } => string_literal.serialize(serializer), Self::Variable { name } => { let mut seq = serializer.serialize_seq(None)?; seq.serialize_element("variable")?; seq.serialize_element(name)?; seq.end() } } } } just-1.21.0/src/fragment.rs000064400000000000000000000012671046102023000136400ustar 00000000000000use super::*; /// A line fragment consisting either of… #[derive(PartialEq, Debug, Clone)] pub(crate) enum Fragment<'src> { /// …raw text… Text { token: Token<'src> }, /// …an interpolation containing `expression`. Interpolation { expression: Expression<'src> }, } impl<'src> Serialize for Fragment<'src> { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { Self::Text { token } => serializer.serialize_str(token.lexeme()), Self::Interpolation { expression } => { let mut seq = serializer.serialize_seq(None)?; seq.serialize_element(expression)?; seq.end() } } } } just-1.21.0/src/function.rs000064400000000000000000000305711046102023000136620ustar 00000000000000use { super::*, heck::{ ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase, }, semver::{Version, VersionReq}, Function::*, }; pub(crate) enum Function { Nullary(fn(&FunctionContext) -> Result), Unary(fn(&FunctionContext, &str) -> Result), UnaryOpt(fn(&FunctionContext, &str, Option<&str>) -> Result), Binary(fn(&FunctionContext, &str, &str) -> Result), BinaryPlus(fn(&FunctionContext, &str, &str, &[String]) -> Result), Ternary(fn(&FunctionContext, &str, &str, &str) -> Result), } pub(crate) fn get(name: &str) -> Option { let function = match name { "absolute_path" => Unary(absolute_path), "arch" => Nullary(arch), "capitalize" => Unary(capitalize), "clean" => Unary(clean), "env" => UnaryOpt(env), "env_var" => Unary(env_var), "env_var_or_default" => Binary(env_var_or_default), "error" => Unary(error), "extension" => Unary(extension), "file_name" => Unary(file_name), "file_stem" => Unary(file_stem), "invocation_directory" => Nullary(invocation_directory), "invocation_directory_native" => Nullary(invocation_directory_native), "join" => BinaryPlus(join), "just_executable" => Nullary(just_executable), "justfile" => Nullary(justfile), "justfile_directory" => Nullary(justfile_directory), "kebabcase" => Unary(kebabcase), "lowercamelcase" => Unary(lowercamelcase), "lowercase" => Unary(lowercase), "num_cpus" => Nullary(num_cpus), "os" => Nullary(os), "os_family" => Nullary(os_family), "parent_directory" => Unary(parent_directory), "path_exists" => Unary(path_exists), "quote" => Unary(quote), "replace" => Ternary(replace), "replace_regex" => Ternary(replace_regex), "semver_matches" => Binary(semver_matches), "sha256" => Unary(sha256), "sha256_file" => Unary(sha256_file), "shoutykebabcase" => Unary(shoutykebabcase), "shoutysnakecase" => Unary(shoutysnakecase), "snakecase" => Unary(snakecase), "titlecase" => Unary(titlecase), "trim" => Unary(trim), "trim_end" => Unary(trim_end), "trim_end_match" => Binary(trim_end_match), "trim_end_matches" => Binary(trim_end_matches), "trim_start" => Unary(trim_start), "trim_start_match" => Binary(trim_start_match), "trim_start_matches" => Binary(trim_start_matches), "uppercamelcase" => Unary(uppercamelcase), "uppercase" => Unary(uppercase), "uuid" => Nullary(uuid), "without_extension" => Unary(without_extension), _ => return None, }; Some(function) } impl Function { pub(crate) fn argc(&self) -> Range { match *self { Nullary(_) => 0..0, Unary(_) => 1..1, UnaryOpt(_) => 1..2, Binary(_) => 2..2, BinaryPlus(_) => 2..usize::MAX, Ternary(_) => 3..3, } } } fn absolute_path(context: &FunctionContext, path: &str) -> Result { let abs_path_unchecked = context.search.working_directory.join(path).lexiclean(); match abs_path_unchecked.to_str() { Some(absolute_path) => Ok(absolute_path.to_owned()), None => Err(format!( "Working directory is not valid unicode: {}", context.search.working_directory.display() )), } } fn arch(_context: &FunctionContext) -> Result { Ok(target::arch().to_owned()) } fn capitalize(_context: &FunctionContext, s: &str) -> Result { let mut capitalized = String::new(); for (i, c) in s.chars().enumerate() { if i == 0 { capitalized.extend(c.to_uppercase()); } else { capitalized.extend(c.to_lowercase()); } } Ok(capitalized) } fn clean(_context: &FunctionContext, path: &str) -> Result { Ok(Path::new(path).lexiclean().to_str().unwrap().to_owned()) } fn env_var(context: &FunctionContext, key: &str) -> Result { use std::env::VarError::*; if let Some(value) = context.dotenv.get(key) { return Ok(value.clone()); } match env::var(key) { Err(NotPresent) => Err(format!("environment variable `{key}` not present")), Err(NotUnicode(os_string)) => Err(format!( "environment variable `{key}` not unicode: {os_string:?}" )), Ok(value) => Ok(value), } } fn env_var_or_default( context: &FunctionContext, key: &str, default: &str, ) -> Result { use std::env::VarError::*; if let Some(value) = context.dotenv.get(key) { return Ok(value.clone()); } match env::var(key) { Err(NotPresent) => Ok(default.to_owned()), Err(NotUnicode(os_string)) => Err(format!( "environment variable `{key}` not unicode: {os_string:?}" )), Ok(value) => Ok(value), } } fn env(context: &FunctionContext, key: &str, default: Option<&str>) -> Result { match default { Some(val) => env_var_or_default(context, key, val), None => env_var(context, key), } } fn error(_context: &FunctionContext, message: &str) -> Result { Err(message.to_owned()) } fn extension(_context: &FunctionContext, path: &str) -> Result { Utf8Path::new(path) .extension() .map(str::to_owned) .ok_or_else(|| format!("Could not extract extension from `{path}`")) } fn file_name(_context: &FunctionContext, path: &str) -> Result { Utf8Path::new(path) .file_name() .map(str::to_owned) .ok_or_else(|| format!("Could not extract file name from `{path}`")) } fn file_stem(_context: &FunctionContext, path: &str) -> Result { Utf8Path::new(path) .file_stem() .map(str::to_owned) .ok_or_else(|| format!("Could not extract file stem from `{path}`")) } fn invocation_directory(context: &FunctionContext) -> Result { Platform::convert_native_path( &context.search.working_directory, context.invocation_directory, ) .map_err(|e| format!("Error getting shell path: {e}")) } fn invocation_directory_native(context: &FunctionContext) -> Result { context .invocation_directory .to_str() .map(str::to_owned) .ok_or_else(|| { format!( "Invocation directory is not valid unicode: {}", context.invocation_directory.display() ) }) } fn join( _context: &FunctionContext, base: &str, with: &str, and: &[String], ) -> Result { let mut result = Utf8Path::new(base).join(with); for arg in and { result.push(arg); } Ok(result.to_string()) } fn just_executable(_context: &FunctionContext) -> Result { let exe_path = env::current_exe().map_err(|e| format!("Error getting current executable: {e}"))?; exe_path.to_str().map(str::to_owned).ok_or_else(|| { format!( "Executable path is not valid unicode: {}", exe_path.display() ) }) } fn justfile(context: &FunctionContext) -> Result { context .search .justfile .to_str() .map(str::to_owned) .ok_or_else(|| { format!( "Justfile path is not valid unicode: {}", context.search.justfile.display() ) }) } fn justfile_directory(context: &FunctionContext) -> Result { let justfile_directory = context.search.justfile.parent().ok_or_else(|| { format!( "Could not resolve justfile directory. Justfile `{}` had no parent.", context.search.justfile.display() ) })?; justfile_directory .to_str() .map(str::to_owned) .ok_or_else(|| { format!( "Justfile directory is not valid unicode: {}", justfile_directory.display() ) }) } fn kebabcase(_context: &FunctionContext, s: &str) -> Result { Ok(s.to_kebab_case()) } fn lowercamelcase(_context: &FunctionContext, s: &str) -> Result { Ok(s.to_lower_camel_case()) } fn lowercase(_context: &FunctionContext, s: &str) -> Result { Ok(s.to_lowercase()) } fn num_cpus(_context: &FunctionContext) -> Result { let num = num_cpus::get(); Ok(num.to_string()) } fn os(_context: &FunctionContext) -> Result { Ok(target::os().to_owned()) } fn os_family(_context: &FunctionContext) -> Result { Ok(target::family().to_owned()) } fn parent_directory(_context: &FunctionContext, path: &str) -> Result { Utf8Path::new(path) .parent() .map(Utf8Path::to_string) .ok_or_else(|| format!("Could not extract parent directory from `{path}`")) } fn path_exists(context: &FunctionContext, path: &str) -> Result { Ok( context .search .working_directory .join(path) .exists() .to_string(), ) } fn quote(_context: &FunctionContext, s: &str) -> Result { Ok(format!("'{}'", s.replace('\'', "'\\''"))) } fn replace(_context: &FunctionContext, s: &str, from: &str, to: &str) -> Result { Ok(s.replace(from, to)) } fn replace_regex( _context: &FunctionContext, s: &str, regex: &str, replacement: &str, ) -> Result { Ok( Regex::new(regex) .map_err(|err| err.to_string())? .replace_all(s, replacement) .to_string(), ) } fn sha256(_context: &FunctionContext, s: &str) -> Result { use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); hasher.update(s); let hash = hasher.finalize(); Ok(format!("{hash:x}")) } fn sha256_file(context: &FunctionContext, path: &str) -> Result { use sha2::{Digest, Sha256}; let justpath = context.search.working_directory.join(path); let mut hasher = Sha256::new(); let mut file = fs::File::open(&justpath) .map_err(|err| format!("Failed to open file at `{:?}`: {err}", justpath.to_str()))?; std::io::copy(&mut file, &mut hasher) .map_err(|err| format!("Failed to read file at `{:?}`: {err}", justpath.to_str()))?; let hash = hasher.finalize(); Ok(format!("{hash:x}")) } fn shoutykebabcase(_context: &FunctionContext, s: &str) -> Result { Ok(s.to_shouty_kebab_case()) } fn shoutysnakecase(_context: &FunctionContext, s: &str) -> Result { Ok(s.to_shouty_snake_case()) } fn snakecase(_context: &FunctionContext, s: &str) -> Result { Ok(s.to_snake_case()) } fn titlecase(_context: &FunctionContext, s: &str) -> Result { Ok(s.to_title_case()) } fn trim(_context: &FunctionContext, s: &str) -> Result { Ok(s.trim().to_owned()) } fn trim_end(_context: &FunctionContext, s: &str) -> Result { Ok(s.trim_end().to_owned()) } fn trim_end_match(_context: &FunctionContext, s: &str, pat: &str) -> Result { Ok(s.strip_suffix(pat).unwrap_or(s).to_owned()) } fn trim_end_matches(_context: &FunctionContext, s: &str, pat: &str) -> Result { Ok(s.trim_end_matches(pat).to_owned()) } fn trim_start(_context: &FunctionContext, s: &str) -> Result { Ok(s.trim_start().to_owned()) } fn trim_start_match(_context: &FunctionContext, s: &str, pat: &str) -> Result { Ok(s.strip_prefix(pat).unwrap_or(s).to_owned()) } fn trim_start_matches(_context: &FunctionContext, s: &str, pat: &str) -> Result { Ok(s.trim_start_matches(pat).to_owned()) } fn uppercamelcase(_context: &FunctionContext, s: &str) -> Result { Ok(s.to_upper_camel_case()) } fn uppercase(_context: &FunctionContext, s: &str) -> Result { Ok(s.to_uppercase()) } fn uuid(_context: &FunctionContext) -> Result { Ok(uuid::Uuid::new_v4().to_string()) } fn without_extension(_context: &FunctionContext, path: &str) -> Result { let parent = Utf8Path::new(path) .parent() .ok_or_else(|| format!("Could not extract parent from `{path}`"))?; let file_stem = Utf8Path::new(path) .file_stem() .ok_or_else(|| format!("Could not extract file stem from `{path}`"))?; Ok(parent.join(file_stem).to_string()) } /// Check whether a string processes properly as semver (e.x. "0.1.0") /// and matches a given semver requirement (e.x. ">=0.1.0") fn semver_matches( _context: &FunctionContext, version: &str, requirement: &str, ) -> Result { Ok( requirement .parse::() .map_err(|err| format!("invalid semver requirement: {err}"))? .matches( &version .parse::() .map_err(|err| format!("invalid semver version: {err}"))?, ) .to_string(), ) } just-1.21.0/src/function_context.rs000064400000000000000000000003021046102023000154130ustar 00000000000000use super::*; pub(crate) struct FunctionContext<'run> { pub(crate) dotenv: &'run BTreeMap, pub(crate) invocation_directory: &'run Path, pub(crate) search: &'run Search, } just-1.21.0/src/fuzzing.rs000064400000000000000000000001201046102023000135140ustar 00000000000000use super::*; pub fn compile(text: &str) { let _ = testing::compile(text); } just-1.21.0/src/interrupt_guard.rs000064400000000000000000000004121046102023000152420ustar 00000000000000use super::*; pub(crate) struct InterruptGuard; impl InterruptGuard { pub(crate) fn new() -> Self { InterruptHandler::instance().block(); Self } } impl Drop for InterruptGuard { fn drop(&mut self) { InterruptHandler::instance().unblock(); } } just-1.21.0/src/interrupt_handler.rs000064400000000000000000000034321046102023000155620ustar 00000000000000use super::*; pub(crate) struct InterruptHandler { blocks: u32, interrupted: bool, verbosity: Verbosity, } impl InterruptHandler { pub(crate) fn install(verbosity: Verbosity) -> Result<(), ctrlc::Error> { let mut instance = Self::instance(); instance.verbosity = verbosity; ctrlc::set_handler(|| Self::instance().interrupt()) } pub(crate) fn instance() -> MutexGuard<'static, Self> { static INSTANCE: Mutex = Mutex::new(InterruptHandler::new()); match INSTANCE.lock() { Ok(guard) => guard, Err(poison_error) => { eprintln!( "{}", Error::Internal { message: format!("interrupt handler mutex poisoned: {poison_error}"), } .color_display(Color::auto().stderr()) ); process::exit(EXIT_FAILURE); } } } const fn new() -> Self { Self { blocks: 0, interrupted: false, verbosity: Verbosity::default(), } } fn interrupt(&mut self) { self.interrupted = true; if self.blocks > 0 { return; } Self::exit(); } fn exit() { process::exit(130); } pub(crate) fn block(&mut self) { self.blocks += 1; } pub(crate) fn unblock(&mut self) { if self.blocks == 0 { if self.verbosity.loud() { eprintln!( "{}", Error::Internal { message: "attempted to unblock interrupt handler, but handler was not blocked" .to_owned(), } .color_display(Color::auto().stderr()) ); } process::exit(EXIT_FAILURE); } self.blocks -= 1; if self.interrupted { Self::exit(); } } pub(crate) fn guard T>(function: F) -> T { let _guard = InterruptGuard::new(); function() } } just-1.21.0/src/item.rs000064400000000000000000000026311046102023000127670ustar 00000000000000use super::*; /// A single top-level item #[derive(Debug, Clone)] pub(crate) enum Item<'src> { Alias(Alias<'src, Name<'src>>), Assignment(Assignment<'src>), Comment(&'src str), Import { absolute: Option, optional: bool, path: Token<'src>, relative: StringLiteral<'src>, }, Module { absolute: Option, name: Name<'src>, optional: bool, relative: Option>, }, Recipe(UnresolvedRecipe<'src>), Set(Set<'src>), } impl<'src> Display for Item<'src> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Item::Alias(alias) => write!(f, "{alias}"), Item::Assignment(assignment) => write!(f, "{assignment}"), Item::Comment(comment) => write!(f, "{comment}"), Item::Import { relative, optional, .. } => { write!(f, "import")?; if *optional { write!(f, "?")?; } write!(f, " {relative}") } Item::Module { name, relative, optional, .. } => { write!(f, "mod")?; if *optional { write!(f, "?")?; } write!(f, " {name}")?; if let Some(path) = relative { write!(f, " {path}")?; } Ok(()) } Item::Recipe(recipe) => write!(f, "{}", recipe.color_display(Color::never())), Item::Set(set) => write!(f, "{set}"), } } } just-1.21.0/src/justfile.rs000064400000000000000000000574261046102023000136720ustar 00000000000000use {super::*, serde::Serialize}; #[derive(Debug)] struct Invocation<'src: 'run, 'run> { arguments: &'run [&'run str], recipe: &'run Recipe<'src>, settings: &'run Settings<'src>, scope: &'run Scope<'src, 'run>, } #[derive(Debug, PartialEq, Serialize)] pub(crate) struct Justfile<'src> { pub(crate) aliases: Table<'src, Alias<'src>>, pub(crate) assignments: Table<'src, Assignment<'src>>, #[serde(rename = "first", serialize_with = "keyed::serialize_option")] pub(crate) default: Option>>, #[serde(skip)] pub(crate) loaded: Vec, pub(crate) modules: BTreeMap>, pub(crate) recipes: Table<'src, Rc>>, pub(crate) settings: Settings<'src>, pub(crate) warnings: Vec, } impl<'src> Justfile<'src> { pub(crate) fn suggest_recipe(&self, input: &str) -> Option> { let mut suggestions = self .recipes .keys() .map(|name| { ( edit_distance(name, input), Suggestion { name, target: None }, ) }) .chain(self.aliases.iter().map(|(name, alias)| { ( edit_distance(name, input), Suggestion { name, target: Some(alias.target.name.lexeme()), }, ) })) .filter(|(distance, _suggestion)| distance < &3) .collect::>(); suggestions.sort_by_key(|(distance, _suggestion)| *distance); suggestions .into_iter() .map(|(_distance, suggestion)| suggestion) .next() } pub(crate) fn suggest_variable(&self, input: &str) -> Option> { let mut suggestions = self .assignments .keys() .map(|name| { ( edit_distance(name, input), Suggestion { name, target: None }, ) }) .filter(|(distance, _suggestion)| distance < &3) .collect::>(); suggestions.sort_by_key(|(distance, _suggestion)| *distance); suggestions .into_iter() .map(|(_distance, suggestion)| suggestion) .next() } fn scope<'run>( &'run self, config: &'run Config, dotenv: &'run BTreeMap, search: &'run Search, overrides: &BTreeMap, parent: &'run Scope<'src, 'run>, ) -> RunResult<'src, Scope<'src, 'run>> where 'src: 'run, { let mut scope = parent.child(); let mut unknown_overrides = Vec::new(); for (name, value) in overrides { if let Some(assignment) = self.assignments.get(name) { scope.bind(assignment.export, assignment.name, value.clone()); } else { unknown_overrides.push(name.clone()); } } if !unknown_overrides.is_empty() { return Err(Error::UnknownOverrides { overrides: unknown_overrides, }); } Evaluator::evaluate_assignments( &self.assignments, config, dotenv, scope, &self.settings, search, ) } pub(crate) fn run( &self, config: &Config, search: &Search, overrides: &BTreeMap, arguments: &[String], ) -> RunResult<'src> { let unknown_overrides = overrides .keys() .filter(|name| !self.assignments.contains_key(name.as_str())) .cloned() .collect::>(); if !unknown_overrides.is_empty() { return Err(Error::UnknownOverrides { overrides: unknown_overrides, }); } let dotenv = if config.load_dotenv { load_dotenv(config, &self.settings, &search.working_directory)? } else { BTreeMap::new() }; let root = Scope::new(); let scope = self.scope(config, &dotenv, search, overrides, &root)?; match &config.subcommand { Subcommand::Command { binary, arguments, .. } => { let mut command = if config.shell_command { let mut command = self.settings.shell_command(config); command.arg(binary); command } else { Command::new(binary) }; command.args(arguments); command.current_dir(&search.working_directory); let scope = scope.child(); command.export(&self.settings, &dotenv, &scope); let status = InterruptHandler::guard(|| command.status()).map_err(|io_error| { Error::CommandInvoke { binary: binary.clone(), arguments: arguments.clone(), io_error, } })?; if !status.success() { return Err(Error::CommandStatus { binary: binary.clone(), arguments: arguments.clone(), status, }); }; return Ok(()); } Subcommand::Evaluate { variable, .. } => { if let Some(variable) = variable { if let Some(value) = scope.value(variable) { print!("{value}"); } else { return Err(Error::EvalUnknownVariable { suggestion: self.suggest_variable(variable), variable: variable.clone(), }); } } else { let mut width = 0; for name in scope.names() { width = cmp::max(name.len(), width); } for binding in scope.bindings() { println!( "{0:1$} := \"{2}\"", binding.name.lexeme(), width, binding.value ); } } return Ok(()); } _ => {} } let argvec: Vec<&str> = if !arguments.is_empty() { arguments.iter().map(String::as_str).collect() } else if let Some(recipe) = &self.default { recipe.check_can_be_default_recipe()?; vec![recipe.name()] } else if self.recipes.is_empty() { return Err(Error::NoRecipes); } else { return Err(Error::NoDefaultRecipe); }; let arguments = argvec.as_slice(); let mut missing = Vec::new(); let mut invocations = Vec::new(); let mut remaining = arguments; let mut scopes = BTreeMap::new(); let arena: Arena = Arena::new(); while let Some((first, mut rest)) = remaining.split_first() { if let Some((invocation, consumed)) = self.invocation( 0, &mut Vec::new(), &arena, &mut scopes, config, &dotenv, search, &scope, first, rest, )? { rest = &rest[consumed..]; invocations.push(invocation); } else { missing.push((*first).to_owned()); } remaining = rest; } if !missing.is_empty() { let suggestion = if missing.len() == 1 { self.suggest_recipe(missing.first().unwrap()) } else { None }; return Err(Error::UnknownRecipes { recipes: missing, suggestion, }); } let mut ran = BTreeSet::new(); for invocation in invocations { let context = RecipeContext { settings: invocation.settings, config, scope: invocation.scope, search, }; Self::run_recipe( &context, invocation.recipe, invocation.arguments, &dotenv, search, &mut ran, )?; } Ok(()) } pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias<'src>> { self.aliases.get(name) } pub(crate) fn get_recipe(&self, name: &str) -> Option<&Recipe<'src>> { self .recipes .get(name) .map(Rc::as_ref) .or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref())) } fn invocation<'run>( &'run self, depth: usize, path: &mut Vec<&'run str>, arena: &'run Arena>, scopes: &mut BTreeMap, &'run Scope<'src, 'run>>, config: &'run Config, dotenv: &'run BTreeMap, search: &'run Search, parent: &'run Scope<'src, 'run>, first: &'run str, rest: &'run [&'run str], ) -> RunResult<'src, Option<(Invocation<'src, 'run>, usize)>> { if let Some(module) = self.modules.get(first) { path.push(first); let scope = if let Some(scope) = scopes.get(path) { scope } else { let scope = module.scope(config, dotenv, search, &BTreeMap::new(), parent)?; let scope = arena.alloc(scope); scopes.insert(path.clone(), scope); scopes.get(path).unwrap() }; if rest.is_empty() { if let Some(recipe) = &module.default { recipe.check_can_be_default_recipe()?; return Ok(Some(( Invocation { settings: &module.settings, recipe, arguments: &[], scope, }, depth, ))); } Err(Error::NoDefaultRecipe) } else { module.invocation( depth + 1, path, arena, scopes, config, dotenv, search, scope, rest[0], &rest[1..], ) } } else if let Some(recipe) = self.get_recipe(first) { if recipe.parameters.is_empty() { Ok(Some(( Invocation { arguments: &[], recipe, scope: parent, settings: &self.settings, }, depth, ))) } else { let argument_range = recipe.argument_range(); let argument_count = cmp::min(rest.len(), recipe.max_arguments()); if !argument_range.range_contains(&argument_count) { return Err(Error::ArgumentCountMismatch { recipe: recipe.name(), parameters: recipe.parameters.clone(), found: rest.len(), min: recipe.min_arguments(), max: recipe.max_arguments(), }); } Ok(Some(( Invocation { arguments: &rest[..argument_count], recipe, scope: parent, settings: &self.settings, }, depth + argument_count, ))) } } else { Ok(None) } } fn run_recipe( context: &RecipeContext<'src, '_>, recipe: &Recipe<'src>, arguments: &[&str], dotenv: &BTreeMap, search: &Search, ran: &mut BTreeSet>, ) -> RunResult<'src> { let mut invocation = vec![recipe.name().to_owned()]; for argument in arguments { invocation.push((*argument).to_string()); } if ran.contains(&invocation) { return Ok(()); } if !context.config.yes && !recipe.confirm()? { return Err(Error::NotConfirmed { recipe: recipe.name(), }); } let (outer, positional) = Evaluator::evaluate_parameters( context.config, dotenv, &recipe.parameters, arguments, context.scope, context.settings, search, )?; let scope = outer.child(); let mut evaluator = Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search); for Dependency { recipe, arguments } in recipe.dependencies.iter().take(recipe.priors) { let arguments = arguments .iter() .map(|argument| evaluator.evaluate_expression(argument)) .collect::>>()?; Self::run_recipe( context, recipe, &arguments.iter().map(String::as_ref).collect::>(), dotenv, search, ran, )?; } recipe.run(context, dotenv, scope.child(), search, &positional)?; { let mut ran = BTreeSet::new(); for Dependency { recipe, arguments } in recipe.dependencies.iter().skip(recipe.priors) { let mut evaluated = Vec::new(); for argument in arguments { evaluated.push(evaluator.evaluate_expression(argument)?); } Self::run_recipe( context, recipe, &evaluated.iter().map(String::as_ref).collect::>(), dotenv, search, &mut ran, )?; } } ran.insert(invocation); Ok(()) } pub(crate) fn public_recipes(&self, source_order: bool) -> Vec<&Recipe<'src, Dependency>> { let mut recipes = self .recipes .values() .map(AsRef::as_ref) .filter(|recipe| recipe.public()) .collect::>>(); if source_order { recipes.sort_by_key(|recipe| { ( self .loaded .iter() .position(|path| path == recipe.name.path) .unwrap(), recipe.name.offset, ) }); } recipes } } impl<'src> ColorDisplay for Justfile<'src> { fn fmt(&self, f: &mut Formatter, color: Color) -> Result<(), fmt::Error> { let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len(); for (name, assignment) in &self.assignments { if assignment.export { write!(f, "export ")?; } write!(f, "{name} := {}", assignment.value)?; items -= 1; if items != 0 { write!(f, "\n\n")?; } } for alias in self.aliases.values() { write!(f, "{alias}")?; items -= 1; if items != 0 { write!(f, "\n\n")?; } } for recipe in self.recipes.values() { write!(f, "{}", recipe.color_display(color))?; items -= 1; if items != 0 { write!(f, "\n\n")?; } } Ok(()) } } #[cfg(test)] mod tests { use super::*; use testing::compile; use Error::*; run_error! { name: unknown_recipes, src: "a:\nb:\nc:", args: ["a", "x", "y", "z"], error: UnknownRecipes { recipes, suggestion, }, check: { assert_eq!(recipes, &["x", "y", "z"]); assert_eq!(suggestion, None); } } run_error! { name: unknown_recipes_show_alias_suggestion, src: " foo: echo foo alias z := foo ", args: ["zz"], error: UnknownRecipes { recipes, suggestion, }, check: { assert_eq!(recipes, &["zz"]); assert_eq!(suggestion, Some(Suggestion { name: "z", target: Some("foo"), } )); } } // This test exists to make sure that shebang recipes run correctly. Although // this script is still executed by a shell its behavior depends on the value of // a variable and continuing even though a command fails, whereas in plain // recipes variables are not available in subsequent lines and execution stops // when a line fails. run_error! { name: run_shebang, src: " a: #!/usr/bin/env sh code=200 x() { return $code; } x x ", args: ["a"], error: Code { recipe, line_number, code, print_message, }, check: { assert_eq!(recipe, "a"); assert_eq!(code, 200); assert_eq!(line_number, None); assert!(print_message); } } run_error! { name: code_error, src: " fail: @exit 100 ", args: ["fail"], error: Code { recipe, line_number, code, print_message, }, check: { assert_eq!(recipe, "fail"); assert_eq!(code, 100); assert_eq!(line_number, Some(2)); assert!(print_message); } } run_error! { name: run_args, src: r#" a return code: @x() { {{return}} {{code + "0"}}; }; x "#, args: ["a", "return", "15"], error: Code { recipe, line_number, code, print_message, }, check: { assert_eq!(recipe, "a"); assert_eq!(code, 150); assert_eq!(line_number, Some(2)); assert!(print_message); } } run_error! { name: missing_some_arguments, src: "a b c d:", args: ["a", "b", "c"], error: ArgumentCountMismatch { recipe, parameters, found, min, max, }, check: { let param_names = parameters .iter() .map(|p| p.name.lexeme()) .collect::>(); assert_eq!(recipe, "a"); assert_eq!(param_names, ["b", "c", "d"]); assert_eq!(found, 2); assert_eq!(min, 3); assert_eq!(max, 3); } } run_error! { name: missing_some_arguments_variadic, src: "a b c +d:", args: ["a", "B", "C"], error: ArgumentCountMismatch { recipe, parameters, found, min, max, }, check: { let param_names = parameters .iter() .map(|p| p.name.lexeme()) .collect::>(); assert_eq!(recipe, "a"); assert_eq!(param_names, ["b", "c", "d"]); assert_eq!(found, 2); assert_eq!(min, 3); assert_eq!(max, usize::MAX - 1); } } run_error! { name: missing_all_arguments, src: "a b c d:\n echo {{b}}{{c}}{{d}}", args: ["a"], error: ArgumentCountMismatch { recipe, parameters, found, min, max, }, check: { let param_names = parameters .iter() .map(|p| p.name.lexeme()) .collect::>(); assert_eq!(recipe, "a"); assert_eq!(param_names, ["b", "c", "d"]); assert_eq!(found, 0); assert_eq!(min, 3); assert_eq!(max, 3); } } run_error! { name: missing_some_defaults, src: "a b c d='hello':", args: ["a", "b"], error: ArgumentCountMismatch { recipe, parameters, found, min, max, }, check: { let param_names = parameters .iter() .map(|p| p.name.lexeme()) .collect::>(); assert_eq!(recipe, "a"); assert_eq!(param_names, ["b", "c", "d"]); assert_eq!(found, 1); assert_eq!(min, 2); assert_eq!(max, 3); } } run_error! { name: missing_all_defaults, src: "a b c='r' d='h':", args: ["a"], error: ArgumentCountMismatch { recipe, parameters, found, min, max, }, check: { let param_names = parameters .iter() .map(|p| p.name.lexeme()) .collect::>(); assert_eq!(recipe, "a"); assert_eq!(param_names, ["b", "c", "d"]); assert_eq!(found, 0); assert_eq!(min, 1); assert_eq!(max, 3); } } run_error! { name: unknown_overrides, src: " a: echo {{`f() { return 100; }; f`}} ", args: ["foo=bar", "baz=bob", "a"], error: UnknownOverrides { overrides }, check: { assert_eq!(overrides, &["baz", "foo"]); } } run_error! { name: export_failure, src: r#" export foo := "a" baz := "c" export bar := "b" export abc := foo + bar + baz wut: echo $foo $bar $baz "#, args: ["--quiet", "wut"], error: Code { recipe, line_number, print_message, .. }, check: { assert_eq!(recipe, "wut"); assert_eq!(line_number, Some(7)); assert!(print_message); } } fn case(input: &str, expected: &str) { let justfile = compile(input); let actual = format!("{}", justfile.color_display(Color::never())); assert_eq!(actual, expected); println!("Re-parsing..."); let reparsed = compile(&actual); let redumped = format!("{}", reparsed.color_display(Color::never())); assert_eq!(redumped, actual); } #[test] fn parse_empty() { case( " # hello ", "", ); } #[test] fn parse_string_default() { case( r#" foo a="b\t": "#, r#"foo a="b\t":"#, ); } #[test] fn parse_multiple() { case( r" a: b: ", r"a: b:", ); } #[test] fn parse_variadic() { case( r" foo +a: ", r"foo +a:", ); } #[test] fn parse_variadic_string_default() { case( r#" foo +a="Hello": "#, r#"foo +a="Hello":"#, ); } #[test] fn parse_raw_string_default() { case( r" foo a='b\t': ", r"foo a='b\t':", ); } #[test] fn parse_export() { case( r#" export a := "hello" "#, r#"export a := "hello""#, ); } #[test] fn parse_alias_after_target() { case( r" foo: echo a alias f := foo ", r"alias f := foo foo: echo a", ); } #[test] fn parse_alias_before_target() { case( r" alias f := foo foo: echo a ", r"alias f := foo foo: echo a", ); } #[test] fn parse_alias_with_comment() { case( r" alias f := foo #comment foo: echo a ", r"alias f := foo foo: echo a", ); } #[test] fn parse_complex() { case( " x: y: z: foo := \"xx\" bar := foo goodbye := \"y\" hello a b c : x y z #hello #! blah #blarg {{ foo + bar}}abc{{ goodbye\t + \"x\" }}xyz 1 2 3 ", "bar := foo foo := \"xx\" goodbye := \"y\" hello a b c: x y z #! blah #blarg {{ foo + bar }}abc{{ goodbye + \"x\" }}xyz 1 2 3 x: y: z:", ); } #[test] fn parse_shebang() { case( " practicum := 'hello' install: \t#!/bin/sh \tif [[ -f {{practicum}} ]]; then \t\treturn \tfi ", "practicum := 'hello' install: #!/bin/sh if [[ -f {{ practicum }} ]]; then \treturn fi", ); } #[test] fn parse_simple_shebang() { case("a:\n #!\n print(1)", "a:\n #!\n print(1)"); } #[test] fn parse_assignments() { case( r#"a := "0" c := a + b + a + b b := "1" "#, r#"a := "0" b := "1" c := a + b + a + b"#, ); } #[test] fn parse_assignment_backticks() { case( "a := `echo hello` c := a + b + a + b b := `echo goodbye`", "a := `echo hello` b := `echo goodbye` c := a + b + a + b", ); } #[test] fn parse_interpolation_backticks() { case( r#"a: echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#, r#"a: echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#, ); } #[test] fn eof_test() { case("x:\ny:\nz:\na b c: x y z", "a b c: x y z\n\nx:\n\ny:\n\nz:"); } #[test] fn string_quote_escape() { case(r#"a := "hello\"""#, r#"a := "hello\"""#); } #[test] fn string_escapes() { case(r#"a := "\n\t\r\"\\""#, r#"a := "\n\t\r\"\\""#); } #[test] fn parameters() { case( "a b c: {{b}} {{c}}", "a b c: {{ b }} {{ c }}", ); } #[test] fn unary_functions() { case( " x := arch() a: {{os()}} {{os_family()}} {{num_cpus()}}", "x := arch() a: {{ os() }} {{ os_family() }} {{ num_cpus() }}", ); } #[test] fn env_functions() { case( r#" x := env_var('foo',) a: {{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#, r#"x := env_var('foo') a: {{ env_var_or_default('foo' + 'bar', 'baz') }} {{ env_var(env_var("baz")) }}"#, ); } #[test] fn parameter_default_string() { case( r#" f x="abc": "#, r#"f x="abc":"#, ); } #[test] fn parameter_default_raw_string() { case( r" f x='abc': ", r"f x='abc':", ); } #[test] fn parameter_default_backtick() { case( r" f x=`echo hello`: ", r"f x=`echo hello`:", ); } #[test] fn parameter_default_concatenation_string() { case( r#" f x=(`echo hello` + "foo"): "#, r#"f x=(`echo hello` + "foo"):"#, ); } #[test] fn parameter_default_concatenation_variable() { case( r#" x := "10" f y=(`echo hello` + x) +z="foo": "#, r#"x := "10" f y=(`echo hello` + x) +z="foo":"#, ); } #[test] fn parameter_default_multiple() { case( r#" x := "10" f y=(`echo hello` + x) +z=("foo" + "bar"): "#, r#"x := "10" f y=(`echo hello` + x) +z=("foo" + "bar"):"#, ); } #[test] fn concatenation_in_group() { case("x := ('0' + '1')", "x := ('0' + '1')"); } #[test] fn string_in_group() { case("x := ('0' )", "x := ('0')"); } #[rustfmt::skip] #[test] fn escaped_dos_newlines() { case("@spam:\r \t{ \\\r \t\tfiglet test; \\\r \t\tcargo build --color always 2>&1; \\\r \t\tcargo test --color always -- --color always 2>&1; \\\r \t} | less\r ", "@spam: { \\ \tfiglet test; \\ \tcargo build --color always 2>&1; \\ \tcargo test --color always -- --color always 2>&1; \\ } | less"); } } just-1.21.0/src/keyed.rs000064400000000000000000000011731046102023000131320ustar 00000000000000use super::*; pub(crate) trait Keyed<'key> { fn key(&self) -> &'key str; } impl<'key, T: Keyed<'key>> Keyed<'key> for Rc { fn key(&self) -> &'key str { self.as_ref().key() } } pub(crate) fn serialize<'src, S, K>(keyed: &K, serializer: S) -> Result where S: Serializer, K: Keyed<'src>, { serializer.serialize_str(keyed.key()) } pub(crate) fn serialize_option<'src, S, K>( recipe: &Option, serializer: S, ) -> Result where S: Serializer, K: Keyed<'src>, { match recipe { None => serializer.serialize_none(), Some(keyed) => serialize(keyed, serializer), } } just-1.21.0/src/keyword.rs000064400000000000000000000013121046102023000135100ustar 00000000000000use super::*; #[derive(Debug, Eq, PartialEq, IntoStaticStr, Display, Copy, Clone, EnumString)] #[strum(serialize_all = "kebab_case")] pub(crate) enum Keyword { Alias, AllowDuplicateRecipes, DotenvFilename, DotenvLoad, DotenvPath, Else, Export, Fallback, False, If, IgnoreComments, Import, Mod, PositionalArguments, Set, Shell, Tempdir, True, WindowsPowershell, WindowsShell, } impl Keyword { pub(crate) fn from_lexeme(lexeme: &str) -> Option { lexeme.parse().ok() } pub(crate) fn lexeme(self) -> &'static str { self.into() } } impl<'a> PartialEq<&'a str> for Keyword { fn eq(&self, other: &&'a str) -> bool { self.lexeme() == *other } } just-1.21.0/src/lexer.rs000064400000000000000000001341231046102023000131520ustar 00000000000000use {super::*, CompileErrorKind::*, TokenKind::*}; /// Just language lexer /// /// The lexer proceeds character-by-character, as opposed to using regular /// expressions to lex tokens or semi-tokens at a time. As a result, it is /// verbose and straightforward. Just used to have a regex-based lexer, which /// was slower and generally godawful. However, this should not be taken as a /// slight against regular expressions, the lexer was just idiosyncratically /// bad. pub(crate) struct Lexer<'src> { /// Char iterator chars: Chars<'src>, /// Indentation stack indentation: Vec<&'src str>, /// Interpolation token start stack interpolation_stack: Vec>, /// Next character to be lexed next: Option, /// Current open delimiters open_delimiters: Vec<(Delimiter, usize)>, /// Path to source file path: &'src Path, /// Inside recipe body recipe_body: bool, /// Next indent will start a recipe body recipe_body_pending: bool, /// Source text src: &'src str, /// Tokens tokens: Vec>, /// Current token end token_end: Position, /// Current token start token_start: Position, } impl<'src> Lexer<'src> { /// Lex `src` pub(crate) fn lex(path: &'src Path, src: &'src str) -> CompileResult<'src, Vec>> { Lexer::new(path, src).tokenize() } #[cfg(test)] pub(crate) fn test_lex(src: &'src str) -> CompileResult<'src, Vec>> { Lexer::new("justfile".as_ref(), src).tokenize() } /// Create a new Lexer to lex `src` fn new(path: &'src Path, src: &'src str) -> Lexer<'src> { let mut chars = src.chars(); let next = chars.next(); let start = Position { offset: 0, column: 0, line: 0, }; Lexer { indentation: vec![""], tokens: Vec::new(), token_start: start, token_end: start, recipe_body_pending: false, recipe_body: false, interpolation_stack: Vec::new(), open_delimiters: Vec::new(), chars, next, src, path, } } /// Advance over the character in `self.next`, updating `self.token_end` /// accordingly. fn advance(&mut self) -> CompileResult<'src> { match self.next { Some(c) => { let len_utf8 = c.len_utf8(); self.token_end.offset += len_utf8; self.token_end.column += len_utf8; if c == '\n' { self.token_end.column = 0; self.token_end.line += 1; } self.next = self.chars.next(); Ok(()) } None => Err(self.internal_error("Lexer advanced past end of text")), } } /// Advance over N characters. fn skip(&mut self, n: usize) -> CompileResult<'src> { for _ in 0..n { self.advance()?; } Ok(()) } /// Lexeme of in-progress token fn lexeme(&self) -> &'src str { &self.src[self.token_start.offset..self.token_end.offset] } /// Length of current token fn current_token_length(&self) -> usize { self.token_end.offset - self.token_start.offset } fn accepted(&mut self, c: char) -> CompileResult<'src, bool> { if self.next_is(c) { self.advance()?; Ok(true) } else { Ok(false) } } fn presume(&mut self, c: char) -> CompileResult<'src> { if !self.next_is(c) { return Err(self.internal_error(format!("Lexer presumed character `{c}`"))); } self.advance()?; Ok(()) } fn presume_str(&mut self, s: &str) -> CompileResult<'src> { for c in s.chars() { self.presume(c)?; } Ok(()) } /// Is next character c? fn next_is(&self, c: char) -> bool { self.next == Some(c) } /// Is next character ' ' or '\t'? fn next_is_whitespace(&self) -> bool { self.next_is(' ') || self.next_is('\t') } /// Un-lexed text fn rest(&self) -> &'src str { &self.src[self.token_end.offset..] } /// Check if unlexed text begins with prefix fn rest_starts_with(&self, prefix: &str) -> bool { self.rest().starts_with(prefix) } /// Does rest start with "\n" or "\r\n"? fn at_eol(&self) -> bool { self.next_is('\n') || self.rest_starts_with("\r\n") } /// Are we at end-of-file? fn at_eof(&self) -> bool { self.rest().is_empty() } /// Are we at end-of-line or end-of-file? fn at_eol_or_eof(&self) -> bool { self.at_eol() || self.at_eof() } /// Get current indentation fn indentation(&self) -> &'src str { self.indentation.last().unwrap() } /// Are we currently indented fn indented(&self) -> bool { !self.indentation().is_empty() } /// Create a new token with `kind` whose lexeme is between `self.token_start` /// and `self.token_end` fn token(&mut self, kind: TokenKind) { self.tokens.push(Token { offset: self.token_start.offset, column: self.token_start.column, line: self.token_start.line, src: self.src, length: self.token_end.offset - self.token_start.offset, kind, path: self.path, }); // Set `token_start` to point after the lexed token self.token_start = self.token_end; } /// Create an internal error with `message` fn internal_error(&self, message: impl Into) -> CompileError<'src> { // Use `self.token_end` as the location of the error let token = Token { src: self.src, offset: self.token_end.offset, line: self.token_end.line, column: self.token_end.column, length: 0, kind: Unspecified, path: self.path, }; CompileError::new( token, Internal { message: message.into(), }, ) } /// Create a compilation error with `kind` fn error(&self, kind: CompileErrorKind<'src>) -> CompileError<'src> { // Use the in-progress token span as the location of the error. // The width of the error site to highlight depends on the kind of error: let length = match kind { UnterminatedString | UnterminatedBacktick => { let kind = match StringKind::from_token_start(self.lexeme()) { Some(kind) => kind, None => { return self.internal_error("Lexer::error: expected string or backtick token start") } }; kind.delimiter().len() } // highlight the full token _ => self.lexeme().len(), }; let token = Token { kind: Unspecified, src: self.src, offset: self.token_start.offset, line: self.token_start.line, column: self.token_start.column, length, path: self.path, }; CompileError::new(token, kind) } fn unterminated_interpolation_error(interpolation_start: Token<'src>) -> CompileError<'src> { CompileError::new(interpolation_start, UnterminatedInterpolation) } /// True if `text` could be an identifier pub(crate) fn is_identifier(text: &str) -> bool { if !text.chars().next().map_or(false, Self::is_identifier_start) { return false; } for c in text.chars().skip(1) { if !Self::is_identifier_continue(c) { return false; } } true } /// True if `c` can be the first character of an identifier fn is_identifier_start(c: char) -> bool { matches!(c, 'a'..='z' | 'A'..='Z' | '_') } /// True if `c` can be a continuation character of an identifier fn is_identifier_continue(c: char) -> bool { if Self::is_identifier_start(c) { return true; } matches!(c, '0'..='9' | '-') } /// Consume the text and produce a series of tokens fn tokenize(mut self) -> CompileResult<'src, Vec>> { loop { if self.token_start.column == 0 { self.lex_line_start()?; } match self.next { Some(first) => { if let Some(&interpolation_start) = self.interpolation_stack.last() { self.lex_interpolation(interpolation_start, first)?; } else if self.recipe_body { self.lex_body()?; } else { self.lex_normal(first)?; }; } None => break, } } if let Some(&interpolation_start) = self.interpolation_stack.last() { return Err(Self::unterminated_interpolation_error(interpolation_start)); } while self.indented() { self.lex_dedent(); } self.token(Eof); assert_eq!(self.token_start.offset, self.token_end.offset); assert_eq!(self.token_start.offset, self.src.len()); assert_eq!(self.indentation.len(), 1); Ok(self.tokens) } /// Handle blank lines and indentation fn lex_line_start(&mut self) -> CompileResult<'src> { enum Indentation<'src> { // Line only contains whitespace Blank, // Indentation continues Continue, // Indentation decreases Decrease, // Indentation isn't consistent Inconsistent, // Indentation increases Increase, // Indentation mixes spaces and tabs Mixed { whitespace: &'src str }, } use Indentation::*; let nonblank_index = self .rest() .char_indices() .skip_while(|&(_, c)| c == ' ' || c == '\t') .map(|(i, _)| i) .next() .unwrap_or_else(|| self.rest().len()); let rest = &self.rest()[nonblank_index..]; let whitespace = &self.rest()[..nonblank_index]; let body_whitespace = &whitespace[..whitespace .char_indices() .take(self.indentation().chars().count()) .map(|(i, _c)| i) .next() .unwrap_or(0)]; let spaces = whitespace.chars().any(|c| c == ' '); let tabs = whitespace.chars().any(|c| c == '\t'); let body_spaces = body_whitespace.chars().any(|c| c == ' '); let body_tabs = body_whitespace.chars().any(|c| c == '\t'); #[allow(clippy::if_same_then_else)] let indentation = if rest.starts_with('\n') || rest.starts_with("\r\n") || rest.is_empty() { Blank } else if whitespace == self.indentation() { Continue } else if self.indentation.contains(&whitespace) { Decrease } else if self.recipe_body && whitespace.starts_with(self.indentation()) { Continue } else if self.recipe_body && body_spaces && body_tabs { Mixed { whitespace: body_whitespace, } } else if !self.recipe_body && spaces && tabs { Mixed { whitespace } } else if whitespace.len() < self.indentation().len() { Inconsistent } else if self.recipe_body && body_whitespace.len() >= self.indentation().len() && !body_whitespace.starts_with(self.indentation()) { Inconsistent } else if whitespace.len() >= self.indentation().len() && !whitespace.starts_with(self.indentation()) { Inconsistent } else { Increase }; match indentation { Blank => { if !whitespace.is_empty() { while self.next_is_whitespace() { self.advance()?; } self.token(Whitespace); }; Ok(()) } Continue => { if !self.indentation().is_empty() { for _ in self.indentation().chars() { self.advance()?; } self.token(Whitespace); } Ok(()) } Decrease => { while self.indentation() != whitespace { self.lex_dedent(); } if !whitespace.is_empty() { while self.next_is_whitespace() { self.advance()?; } self.token(Whitespace); } Ok(()) } Mixed { whitespace } => { for _ in whitespace.chars() { self.advance()?; } Err(self.error(MixedLeadingWhitespace { whitespace })) } Inconsistent => { for _ in whitespace.chars() { self.advance()?; } Err(self.error(InconsistentLeadingWhitespace { expected: self.indentation(), found: whitespace, })) } Increase => { while self.next_is_whitespace() { self.advance()?; } if self.open_delimiters() { self.token(Whitespace); } else { let indentation = self.lexeme(); self.indentation.push(indentation); self.token(Indent); if self.recipe_body_pending { self.recipe_body = true; } } Ok(()) } } } /// Lex token beginning with `start` outside of a recipe body fn lex_normal(&mut self, start: char) -> CompileResult<'src> { match start { ' ' | '\t' => self.lex_whitespace(), '!' if self.rest().starts_with("!include") => Err(self.error(Include)), '!' => self.lex_digraph('!', '=', BangEquals), '#' => self.lex_comment(), '$' => self.lex_single(Dollar), '&' => self.lex_digraph('&', '&', AmpersandAmpersand), '(' => self.lex_delimiter(ParenL), ')' => self.lex_delimiter(ParenR), '*' => self.lex_single(Asterisk), '+' => self.lex_single(Plus), ',' => self.lex_single(Comma), '/' => self.lex_single(Slash), ':' => self.lex_colon(), '=' => self.lex_choices('=', &[('=', EqualsEquals), ('~', EqualsTilde)], Equals), '?' => self.lex_single(QuestionMark), '@' => self.lex_single(At), '[' => self.lex_delimiter(BracketL), '\\' => self.lex_escape(), '\n' | '\r' => self.lex_eol(), '\u{feff}' => self.lex_single(ByteOrderMark), ']' => self.lex_delimiter(BracketR), '`' | '"' | '\'' => self.lex_string(), '{' => self.lex_delimiter(BraceL), '}' => self.lex_delimiter(BraceR), _ if Self::is_identifier_start(start) => self.lex_identifier(), _ => { self.advance()?; Err(self.error(UnknownStartOfToken)) } } } /// Lex token beginning with `start` inside an interpolation fn lex_interpolation( &mut self, interpolation_start: Token<'src>, start: char, ) -> CompileResult<'src> { if self.rest_starts_with("}}") { // end current interpolation if self.interpolation_stack.pop().is_none() { self.advance()?; self.advance()?; return Err(self.internal_error( "Lexer::lex_interpolation found `}}` but was called with empty interpolation stack.", )); } // Emit interpolation end token self.lex_double(InterpolationEnd) } else if self.at_eol_or_eof() { // Return unterminated interpolation error that highlights the opening // {{ Err(Self::unterminated_interpolation_error(interpolation_start)) } else { // Otherwise lex as per normal self.lex_normal(start) } } /// Lex token while in recipe body fn lex_body(&mut self) -> CompileResult<'src> { enum Terminator { Newline, NewlineCarriageReturn, Interpolation, EndOfFile, } use Terminator::*; let terminator = loop { if self.rest_starts_with("{{{{") { self.skip(4)?; continue; } if self.rest_starts_with("\n") { break Newline; } if self.rest_starts_with("\r\n") { break NewlineCarriageReturn; } if self.rest_starts_with("{{") { break Interpolation; } if self.at_eof() { break EndOfFile; } self.advance()?; }; // emit text token containing text so far if self.current_token_length() > 0 { self.token(Text); } match terminator { Newline => self.lex_single(Eol), NewlineCarriageReturn => self.lex_double(Eol), Interpolation => { self.lex_double(InterpolationStart)?; self .interpolation_stack .push(self.tokens[self.tokens.len() - 1]); Ok(()) } EndOfFile => Ok(()), } } fn lex_dedent(&mut self) { assert_eq!(self.current_token_length(), 0); self.token(Dedent); self.indentation.pop(); self.recipe_body_pending = false; self.recipe_body = false; } /// Lex a single-character token fn lex_single(&mut self, kind: TokenKind) -> CompileResult<'src> { self.advance()?; self.token(kind); Ok(()) } /// Lex a double-character token fn lex_double(&mut self, kind: TokenKind) -> CompileResult<'src> { self.advance()?; self.advance()?; self.token(kind); Ok(()) } /// Lex a double-character token of kind `then` if the second character of /// that token would be `second`, otherwise lex a single-character token of /// kind `otherwise` fn lex_choices( &mut self, first: char, choices: &[(char, TokenKind)], otherwise: TokenKind, ) -> CompileResult<'src> { self.presume(first)?; for (second, then) in choices { if self.accepted(*second)? { self.token(*then); return Ok(()); } } self.token(otherwise); Ok(()) } /// Lex an opening or closing delimiter fn lex_delimiter(&mut self, kind: TokenKind) -> CompileResult<'src> { use Delimiter::*; match kind { BraceL => self.open_delimiter(Brace), BraceR => self.close_delimiter(Brace)?, BracketL => self.open_delimiter(Bracket), BracketR => self.close_delimiter(Bracket)?, ParenL => self.open_delimiter(Paren), ParenR => self.close_delimiter(Paren)?, _ => { return Err(self.internal_error(format!( "Lexer::lex_delimiter called with non-delimiter token: `{kind}`", ))) } } // Emit the delimiter token self.lex_single(kind) } /// Push a delimiter onto the open delimiter stack fn open_delimiter(&mut self, delimiter: Delimiter) { self .open_delimiters .push((delimiter, self.token_start.line)); } /// Pop a delimiter from the open delimiter stack and error if incorrect type fn close_delimiter(&mut self, close: Delimiter) -> CompileResult<'src> { match self.open_delimiters.pop() { Some((open, _)) if open == close => Ok(()), Some((open, open_line)) => Err(self.error(MismatchedClosingDelimiter { open, close, open_line, })), None => Err(self.error(UnexpectedClosingDelimiter { close })), } } /// Return true if there are any unclosed delimiters fn open_delimiters(&self) -> bool { !self.open_delimiters.is_empty() } /// Lex a two-character digraph fn lex_digraph(&mut self, left: char, right: char, token: TokenKind) -> CompileResult<'src> { self.presume(left)?; if self.accepted(right)? { self.token(token); Ok(()) } else { // Emit an unspecified token to consume the current character, self.token(Unspecified); if self.at_eof() { return Err(self.error(UnexpectedEndOfToken { expected: right })); } // …and advance past another character, self.advance()?; // …so that the error we produce highlights the unexpected character. Err(self.error(UnexpectedCharacter { expected: right })) } } /// Lex a token starting with ':' fn lex_colon(&mut self) -> CompileResult<'src> { self.presume(':')?; if self.accepted('=')? { self.token(ColonEquals); } else { self.token(Colon); self.recipe_body_pending = true; } Ok(()) } /// Lex an token starting with '\' escape fn lex_escape(&mut self) -> CompileResult<'src> { self.presume('\\')?; // Treat newline escaped with \ as whitespace if self.accepted('\n')? { while self.next_is_whitespace() { self.advance()?; } self.token(Whitespace); } else if self.accepted('\r')? { if !self.accepted('\n')? { return Err(self.error(UnpairedCarriageReturn)); } while self.next_is_whitespace() { self.advance()?; } self.token(Whitespace); } else if let Some(character) = self.next { return Err(self.error(InvalidEscapeSequence { character })); } Ok(()) } /// Lex a carriage return and line feed fn lex_eol(&mut self) -> CompileResult<'src> { if self.accepted('\r')? { if !self.accepted('\n')? { return Err(self.error(UnpairedCarriageReturn)); } } else { self.presume('\n')?; } // Emit an eol if there are no open delimiters, otherwise emit a whitespace // token. if self.open_delimiters() { self.token(Whitespace); } else { self.token(Eol); } Ok(()) } /// Lex name: [a-zA-Z_][a-zA-Z0-9_]* fn lex_identifier(&mut self) -> CompileResult<'src> { self.advance()?; while let Some(c) = self.next { if !Self::is_identifier_continue(c) { break; } self.advance()?; } self.token(Identifier); Ok(()) } /// Lex comment: #[^\r\n] fn lex_comment(&mut self) -> CompileResult<'src> { self.presume('#')?; while !self.at_eol_or_eof() { self.advance()?; } self.token(Comment); Ok(()) } /// Lex whitespace: [ \t]+ fn lex_whitespace(&mut self) -> CompileResult<'src> { while self.next_is_whitespace() { self.advance()?; } self.token(Whitespace); Ok(()) } /// Lex a backtick, cooked string, or raw string. /// /// Backtick: ``[^`]*`` /// Cooked string: "[^"]*" # also processes escape sequences /// Raw string: '[^']*' fn lex_string(&mut self) -> CompileResult<'src> { let kind = if let Some(kind) = StringKind::from_token_start(self.rest()) { kind } else { self.advance()?; return Err(self.internal_error("Lexer::lex_string: invalid string start")); }; self.presume_str(kind.delimiter())?; let mut escape = false; loop { if self.next.is_none() { return Err(self.error(kind.unterminated_error_kind())); } else if kind.processes_escape_sequences() && self.next_is('\\') && !escape { escape = true; } else if self.rest_starts_with(kind.delimiter()) && !escape { break; } else { escape = false; } self.advance()?; } self.presume_str(kind.delimiter())?; self.token(kind.token_kind()); Ok(()) } } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; macro_rules! test { { name: $name:ident, text: $text:expr, tokens: ($($kind:ident $(: $lexeme:literal)?),* $(,)?)$(,)? } => { #[test] fn $name() { let kinds: &[TokenKind] = &[$($kind,)* Eof]; let lexemes: &[&str] = &[$(lexeme!($kind $(, $lexeme)?),)* ""]; test($text, true, kinds, lexemes); } }; { name: $name:ident, text: $text:expr, tokens: ($($kind:ident $(: $lexeme:literal)?),* $(,)?)$(,)? unindent: $unindent:expr, } => { #[test] fn $name() { let kinds: &[TokenKind] = &[$($kind,)* Eof]; let lexemes: &[&str] = &[$(lexeme!($kind $(, $lexeme)?),)* ""]; test($text, $unindent, kinds, lexemes); } } } macro_rules! lexeme { { $kind:ident, $lexeme:literal } => { $lexeme }; { $kind:ident } => { default_lexeme($kind) } } fn test(text: &str, unindent_text: bool, want_kinds: &[TokenKind], want_lexemes: &[&str]) { let text = if unindent_text { unindent(text) } else { text.to_owned() }; let have = Lexer::test_lex(&text).unwrap(); let have_kinds = have .iter() .map(|token| token.kind) .collect::>(); let have_lexemes = have.iter().map(Token::lexeme).collect::>(); assert_eq!(have_kinds, want_kinds, "Token kind mismatch"); assert_eq!(have_lexemes, want_lexemes, "Token lexeme mismatch"); let mut roundtrip = String::new(); for lexeme in have_lexemes { roundtrip.push_str(lexeme); } assert_eq!(roundtrip, text, "Roundtrip mismatch"); let mut offset = 0; let mut line = 0; let mut column = 0; for token in have { assert_eq!(token.offset, offset); assert_eq!(token.line, line); assert_eq!(token.lexeme().len(), token.length); assert_eq!(token.column, column); for c in token.lexeme().chars() { if c == '\n' { line += 1; column = 0; } else { column += c.len_utf8(); } } offset += token.length; } } fn default_lexeme(kind: TokenKind) -> &'static str { match kind { // Fixed lexemes AmpersandAmpersand => "&&", Asterisk => "*", At => "@", BangEquals => "!=", BraceL => "{", BraceR => "}", BracketL => "[", BracketR => "]", ByteOrderMark => "\u{feff}", Colon => ":", ColonEquals => ":=", Comma => ",", Dollar => "$", Eol => "\n", Equals => "=", EqualsEquals => "==", EqualsTilde => "=~", Indent => " ", InterpolationEnd => "}}", InterpolationStart => "{{", ParenL => "(", ParenR => ")", Plus => "+", QuestionMark => "?", Slash => "/", Whitespace => " ", // Empty lexemes Dedent | Eof => "", // Variable lexemes Text | StringToken | Backtick | Identifier | Comment | Unspecified => { panic!("Token {kind:?} has no default lexeme") } } } macro_rules! error { ( name: $name:ident, input: $input:expr, offset: $offset:expr, line: $line:expr, column: $column:expr, width: $width:expr, kind: $kind:expr, ) => { #[test] fn $name() { error($input, $offset, $line, $column, $width, $kind); } }; } fn error( src: &str, offset: usize, line: usize, column: usize, length: usize, kind: CompileErrorKind, ) { match Lexer::test_lex(src) { Ok(_) => panic!("Lexing succeeded but expected"), Err(have) => { let want = CompileError { token: Token { kind: have.token.kind, src, offset, line, column, length, path: "justfile".as_ref(), }, kind: Box::new(kind), }; assert_eq!(have, want); } } } test! { name: name_new, text: "foo", tokens: (Identifier:"foo"), } test! { name: comment, text: "# hello", tokens: (Comment:"# hello"), } test! { name: backtick, text: "`echo`", tokens: (Backtick:"`echo`"), } test! { name: backtick_multi_line, text: "`echo\necho`", tokens: (Backtick:"`echo\necho`"), } test! { name: raw_string, text: "'hello'", tokens: (StringToken:"'hello'"), } test! { name: raw_string_multi_line, text: "'hello\ngoodbye'", tokens: (StringToken:"'hello\ngoodbye'"), } test! { name: cooked_string, text: "\"hello\"", tokens: (StringToken:"\"hello\""), } test! { name: cooked_string_multi_line, text: "\"hello\ngoodbye\"", tokens: (StringToken:"\"hello\ngoodbye\""), } test! { name: cooked_multiline_string, text: "\"\"\"hello\ngoodbye\"\"\"", tokens: (StringToken:"\"\"\"hello\ngoodbye\"\"\""), } test! { name: ampersand_ampersand, text: "&&", tokens: (AmpersandAmpersand), } test! { name: equals, text: "=", tokens: (Equals), } test! { name: equals_equals, text: "==", tokens: (EqualsEquals), } test! { name: bang_equals, text: "!=", tokens: (BangEquals), } test! { name: brace_l, text: "{", tokens: (BraceL), } test! { name: brace_r, text: "{}", tokens: (BraceL, BraceR), } test! { name: brace_lll, text: "{{{", tokens: (BraceL, BraceL, BraceL), } test! { name: brace_rrr, text: "{{{}}}", tokens: (BraceL, BraceL, BraceL, BraceR, BraceR, BraceR), } test! { name: dollar, text: "$", tokens: (Dollar), } test! { name: export_concatenation, text: "export foo = 'foo' + 'bar'", tokens: ( Identifier:"export", Whitespace, Identifier:"foo", Whitespace, Equals, Whitespace, StringToken:"'foo'", Whitespace, Plus, Whitespace, StringToken:"'bar'", ) } test! { name: export_complex, text: "export foo = ('foo' + 'bar') + `baz`", tokens: ( Identifier:"export", Whitespace, Identifier:"foo", Whitespace, Equals, Whitespace, ParenL, StringToken:"'foo'", Whitespace, Plus, Whitespace, StringToken:"'bar'", ParenR, Whitespace, Plus, Whitespace, Backtick:"`baz`", ), } test! { name: eol_linefeed, text: "\n", tokens: (Eol), unindent: false, } test! { name: eol_carriage_return_linefeed, text: "\r\n", tokens: (Eol:"\r\n"), unindent: false, } test! { name: indented_line, text: "foo:\n a", tokens: (Identifier:"foo", Colon, Eol, Indent:" ", Text:"a", Dedent), } test! { name: indented_normal, text: " a b c ", tokens: ( Identifier:"a", Eol, Indent:" ", Identifier:"b", Eol, Whitespace:" ", Identifier:"c", Eol, Dedent, ), } test! { name: indented_normal_nonempty_blank, text: "a\n b\n\t\t\n c\n", tokens: ( Identifier:"a", Eol, Indent:" ", Identifier:"b", Eol, Whitespace:"\t\t", Eol, Whitespace:" ", Identifier:"c", Eol, Dedent, ), unindent: false, } test! { name: indented_normal_multiple, text: " a b c ", tokens: ( Identifier:"a", Eol, Indent:" ", Identifier:"b", Eol, Indent:" ", Identifier:"c", Eol, Dedent, Dedent, ), } test! { name: indent_indent_dedent_indent, text: " a b c d e ", tokens: ( Identifier:"a", Eol, Indent:" ", Identifier:"b", Eol, Indent:" ", Identifier:"c", Eol, Dedent, Whitespace:" ", Identifier:"d", Eol, Indent:" ", Identifier:"e", Eol, Dedent, Dedent, ), } test! { name: indent_recipe_dedent_indent, text: " a b: c d e ", tokens: ( Identifier:"a", Eol, Indent:" ", Identifier:"b", Colon, Eol, Indent:" ", Text:"c", Eol, Dedent, Whitespace:" ", Identifier:"d", Eol, Indent:" ", Identifier:"e", Eol, Dedent, Dedent, ), } test! { name: indented_block, text: " foo: a b c ", tokens: ( Identifier:"foo", Colon, Eol, Indent, Text:"a", Eol, Whitespace:" ", Text:"b", Eol, Whitespace:" ", Text:"c", Eol, Dedent, ) } test! { name: brace_escape, text: " foo: {{{{ ", tokens: ( Identifier:"foo", Colon, Eol, Indent, Text:"{{{{", Eol, Dedent, ) } test! { name: indented_block_followed_by_item, text: " foo: a b: ", tokens: ( Identifier:"foo", Colon, Eol, Indent, Text:"a", Eol, Dedent, Identifier:"b", Colon, Eol, ) } test! { name: indented_block_followed_by_blank, text: " foo: a b: ", tokens: ( Identifier:"foo", Colon, Eol, Indent:" ", Text:"a", Eol, Eol, Dedent, Identifier:"b", Colon, Eol, ), } test! { name: indented_line_containing_unpaired_carriage_return, text: "foo:\n \r \n", tokens: ( Identifier:"foo", Colon, Eol, Indent:" ", Text:"\r ", Eol, Dedent, ), unindent: false, } test! { name: indented_blocks, text: " b: a @mv a b a: @touch F @touch a d: c @rm c c: b @mv b c ", tokens: ( Identifier:"b", Colon, Whitespace, Identifier:"a", Eol, Indent, Text:"@mv a b", Eol, Eol, Dedent, Identifier:"a", Colon, Eol, Indent, Text:"@touch F", Eol, Whitespace:" ", Text:"@touch a", Eol, Eol, Dedent, Identifier:"d", Colon, Whitespace, Identifier:"c", Eol, Indent, Text:"@rm c", Eol, Eol, Dedent, Identifier:"c", Colon, Whitespace, Identifier:"b", Eol, Indent, Text:"@mv b c", Eol, Dedent ), } test! { name: interpolation_empty, text: "hello:\n echo {{}}", tokens: ( Identifier:"hello", Colon, Eol, Indent:" ", Text:"echo ", InterpolationStart, InterpolationEnd, Dedent, ), } test! { name: interpolation_expression, text: "hello:\n echo {{`echo hello` + `echo goodbye`}}", tokens: ( Identifier:"hello", Colon, Eol, Indent:" ", Text:"echo ", InterpolationStart, Backtick:"`echo hello`", Whitespace, Plus, Whitespace, Backtick:"`echo goodbye`", InterpolationEnd, Dedent, ), } test! { name: interpolation_raw_multiline_string, text: "hello:\n echo {{'\n'}}", tokens: ( Identifier:"hello", Colon, Eol, Indent:" ", Text:"echo ", InterpolationStart, StringToken:"'\n'", InterpolationEnd, Dedent, ), } test! { name: tokenize_names, text: " foo bar-bob b-bob_asdfAAAA test123 ", tokens: ( Identifier:"foo", Eol, Identifier:"bar-bob", Eol, Identifier:"b-bob_asdfAAAA", Eol, Identifier:"test123", Eol, ), } test! { name: tokenize_indented_line, text: "foo:\n a", tokens: ( Identifier:"foo", Colon, Eol, Indent:" ", Text:"a", Dedent, ), } test! { name: tokenize_indented_block, text: " foo: a b c ", tokens: ( Identifier:"foo", Colon, Eol, Indent, Text:"a", Eol, Whitespace:" ", Text:"b", Eol, Whitespace:" ", Text:"c", Eol, Dedent, ), } test! { name: tokenize_strings, text: r#"a = "'a'" + '"b"' + "'c'" + '"d"'#echo hello"#, tokens: ( Identifier:"a", Whitespace, Equals, Whitespace, StringToken:"\"'a'\"", Whitespace, Plus, Whitespace, StringToken:"'\"b\"'", Whitespace, Plus, Whitespace, StringToken:"\"'c'\"", Whitespace, Plus, Whitespace, StringToken:"'\"d\"'", Comment:"#echo hello", ) } test! { name: tokenize_recipe_interpolation_eol, text: " foo: # some comment {{hello}} ", tokens: ( Identifier:"foo", Colon, Whitespace, Comment:"# some comment", Eol, Indent:" ", InterpolationStart, Identifier:"hello", InterpolationEnd, Eol, Dedent ), } test! { name: tokenize_recipe_interpolation_eof, text: "foo: # more comments {{hello}} # another comment ", tokens: ( Identifier:"foo", Colon, Whitespace, Comment:"# more comments", Eol, Indent:" ", InterpolationStart, Identifier:"hello", InterpolationEnd, Eol, Dedent, Comment:"# another comment", Eol, ), } test! { name: tokenize_recipe_complex_interpolation_expression, text: "foo: #lol\n {{a + b + \"z\" + blarg}}", tokens: ( Identifier:"foo", Colon, Whitespace:" ", Comment:"#lol", Eol, Indent:" ", InterpolationStart, Identifier:"a", Whitespace, Plus, Whitespace, Identifier:"b", Whitespace, Plus, Whitespace, StringToken:"\"z\"", Whitespace, Plus, Whitespace, Identifier:"blarg", InterpolationEnd, Dedent, ), } test! { name: tokenize_recipe_multiple_interpolations, text: "foo:,#ok\n {{a}}0{{b}}1{{c}}", tokens: ( Identifier:"foo", Colon, Comma, Comment:"#ok", Eol, Indent:" ", InterpolationStart, Identifier:"a", InterpolationEnd, Text:"0", InterpolationStart, Identifier:"b", InterpolationEnd, Text:"1", InterpolationStart, Identifier:"c", InterpolationEnd, Dedent, ), } test! { name: tokenize_junk, text: " bob hello blah blah blah : a b c #whatever ", tokens: ( Identifier:"bob", Eol, Eol, Identifier:"hello", Whitespace, Identifier:"blah", Whitespace, Identifier:"blah", Whitespace, Identifier:"blah", Whitespace, Colon, Whitespace, Identifier:"a", Whitespace, Identifier:"b", Whitespace, Identifier:"c", Whitespace, Comment:"#whatever", Eol, ) } test! { name: tokenize_empty_lines, text: " # this does something hello: asdf bsdf csdf dsdf # whatever # yolo ", tokens: ( Eol, Comment:"# this does something", Eol, Identifier:"hello", Colon, Eol, Indent, Text:"asdf", Eol, Whitespace:" ", Text:"bsdf", Eol, Eol, Whitespace:" ", Text:"csdf", Eol, Eol, Whitespace:" ", Text:"dsdf # whatever", Eol, Eol, Dedent, Comment:"# yolo", Eol, ), } test! { name: tokenize_comment_before_variable, text: " # A='1' echo: echo {{A}} ", tokens: ( Comment:"#", Eol, Identifier:"A", Equals, StringToken:"'1'", Eol, Identifier:"echo", Colon, Eol, Indent, Text:"echo ", InterpolationStart, Identifier:"A", InterpolationEnd, Eol, Dedent, ), } test! { name: tokenize_interpolation_backticks, text: "hello:\n echo {{`echo hello` + `echo goodbye`}}", tokens: ( Identifier:"hello", Colon, Eol, Indent:" ", Text:"echo ", InterpolationStart, Backtick:"`echo hello`", Whitespace, Plus, Whitespace, Backtick:"`echo goodbye`", InterpolationEnd, Dedent ), } test! { name: tokenize_empty_interpolation, text: "hello:\n echo {{}}", tokens: ( Identifier:"hello", Colon, Eol, Indent:" ", Text:"echo ", InterpolationStart, InterpolationEnd, Dedent, ), } test! { name: tokenize_assignment_backticks, text: "a = `echo hello` + `echo goodbye`", tokens: ( Identifier:"a", Whitespace, Equals, Whitespace, Backtick:"`echo hello`", Whitespace, Plus, Whitespace, Backtick:"`echo goodbye`", ), } test! { name: tokenize_multiple, text: " hello: a b c d # hello bob: frank \t ", tokens: ( Eol, Identifier:"hello", Colon, Eol, Indent, Text:"a", Eol, Whitespace:" ", Text:"b", Eol, Eol, Whitespace:" ", Text:"c", Eol, Eol, Whitespace:" ", Text:"d", Eol, Eol, Dedent, Comment:"# hello", Eol, Identifier:"bob", Colon, Eol, Indent:" ", Text:"frank", Eol, Eol, Dedent, ), } test! { name: tokenize_comment, text: "a:=#", tokens: ( Identifier:"a", ColonEquals, Comment:"#", ), } test! { name: tokenize_comment_with_bang, text: "a:=#foo!", tokens: ( Identifier:"a", ColonEquals, Comment:"#foo!", ), } test! { name: tokenize_order, text: " b: a @mv a b a: @touch F @touch a d: c @rm c c: b @mv b c ", tokens: ( Identifier:"b", Colon, Whitespace, Identifier:"a", Eol, Indent, Text:"@mv a b", Eol, Eol, Dedent, Identifier:"a", Colon, Eol, Indent, Text:"@touch F", Eol, Whitespace:" ", Text:"@touch a", Eol, Eol, Dedent, Identifier:"d", Colon, Whitespace, Identifier:"c", Eol, Indent, Text:"@rm c", Eol, Eol, Dedent, Identifier:"c", Colon, Whitespace, Identifier:"b", Eol, Indent, Text:"@mv b c", Eol, Dedent, ), } test! { name: tokenize_parens, text: "((())) ()abc(+", tokens: ( ParenL, ParenL, ParenL, ParenR, ParenR, ParenR, Whitespace, ParenL, ParenR, Identifier:"abc", ParenL, Plus, ), } test! { name: crlf_newline, text: "#\r\n#asdf\r\n", tokens: ( Comment:"#", Eol:"\r\n", Comment:"#asdf", Eol:"\r\n", ), } test! { name: multiple_recipes, text: "a:\n foo\nb:", tokens: ( Identifier:"a", Colon, Eol, Indent:" ", Text:"foo", Eol, Dedent, Identifier:"b", Colon, ), } test! { name: brackets, text: "[][]", tokens: (BracketL, BracketR, BracketL, BracketR), } test! { name: open_delimiter_eol, text: "[\n](\n){\n}", tokens: ( BracketL, Whitespace:"\n", BracketR, ParenL, Whitespace:"\n", ParenR, BraceL, Whitespace:"\n", BraceR ), } error! { name: tokenize_space_then_tab, input: "a: 0 1 \t2 ", offset: 9, line: 3, column: 0, width: 1, kind: InconsistentLeadingWhitespace{expected: " ", found: "\t"}, } error! { name: tokenize_tabs_then_tab_space, input: "a: \t\t0 \t\t 1 \t 2 ", offset: 12, line: 3, column: 0, width: 3, kind: InconsistentLeadingWhitespace{expected: "\t\t", found: "\t "}, } error! { name: tokenize_unknown, input: "%", offset: 0, line: 0, column: 0, width: 1, kind: UnknownStartOfToken, } error! { name: unterminated_string_with_escapes, input: r#"a = "\n\t\r\"\\"#, offset: 4, line: 0, column: 4, width: 1, kind: UnterminatedString, } error! { name: unterminated_raw_string, input: "r a='asdf", offset: 4, line: 0, column: 4, width: 1, kind: UnterminatedString, } error! { name: unterminated_interpolation, input: "foo:\n echo {{ ", offset: 11, line: 1, column: 6, width: 2, kind: UnterminatedInterpolation, } error! { name: unterminated_backtick, input: "`echo", offset: 0, line: 0, column: 0, width: 1, kind: UnterminatedBacktick, } error! { name: unpaired_carriage_return, input: "foo\rbar", offset: 3, line: 0, column: 3, width: 1, kind: UnpairedCarriageReturn, } error! { name: invalid_name_start_dash, input: "-foo", offset: 0, line: 0, column: 0, width: 1, kind: UnknownStartOfToken, } error! { name: invalid_name_start_digit, input: "0foo", offset: 0, line: 0, column: 0, width: 1, kind: UnknownStartOfToken, } error! { name: unterminated_string, input: r#"a = ""#, offset: 4, line: 0, column: 4, width: 1, kind: UnterminatedString, } error! { name: mixed_leading_whitespace_recipe, input: "a:\n\t echo hello", offset: 3, line: 1, column: 0, width: 2, kind: MixedLeadingWhitespace{whitespace: "\t "}, } error! { name: mixed_leading_whitespace_normal, input: "a\n\t echo hello", offset: 2, line: 1, column: 0, width: 2, kind: MixedLeadingWhitespace{whitespace: "\t "}, } error! { name: mixed_leading_whitespace_indent, input: "a\n foo\n \tbar", offset: 7, line: 2, column: 0, width: 2, kind: MixedLeadingWhitespace{whitespace: " \t"}, } error! { name: bad_dedent, input: "a\n foo\n bar\n baz", offset: 14, line: 3, column: 0, width: 2, kind: InconsistentLeadingWhitespace{expected: " ", found: " "}, } error! { name: unclosed_interpolation_delimiter, input: "a:\n echo {{ foo", offset: 9, line: 1, column: 6, width: 2, kind: UnterminatedInterpolation, } error! { name: unexpected_character_after_at, input: "@%", offset: 1, line: 0, column: 1, width: 1, kind: UnknownStartOfToken, } error! { name: mismatched_closing_brace, input: "(]", offset: 1, line: 0, column: 1, width: 0, kind: MismatchedClosingDelimiter { open: Delimiter::Paren, close: Delimiter::Bracket, open_line: 0, }, } error! { name: ampersand_eof, input: "&", offset: 1, line: 0, column: 1, width: 0, kind: UnexpectedEndOfToken { expected: '&', }, } error! { name: ampersand_unexpected, input: "&%", offset: 1, line: 0, column: 1, width: 1, kind: UnexpectedCharacter { expected: '&', }, } #[test] fn presume_error() { let compile_error = Lexer::new("justfile".as_ref(), "!") .presume('-') .unwrap_err(); assert_matches!( compile_error.token, Token { offset: 0, line: 0, column: 0, length: 0, src: "!", kind: Unspecified, path: _, } ); assert_matches!(&*compile_error.kind, Internal { ref message } if message == "Lexer presumed character `-`" ); assert_eq!( Error::Compile { compile_error } .color_display(Color::never()) .to_string(), "error: Internal error, this may indicate a bug in just: Lexer presumed character `-` consider filing an issue: https://github.com/casey/just/issues/new ——▶ justfile:1:1 │ 1 │ ! │ ^" ); } } just-1.21.0/src/lib.rs000064400000000000000000000117361046102023000126050ustar 00000000000000#![deny(clippy::all, clippy::pedantic)] #![allow( clippy::enum_glob_use, clippy::let_underscore_untyped, clippy::needless_pass_by_value, clippy::similar_names, clippy::struct_excessive_bools, clippy::struct_field_names, clippy::too_many_arguments, clippy::too_many_lines, clippy::unnecessary_wraps, clippy::wildcard_imports, overlapping_range_endpoints )] pub(crate) use { crate::{ alias::Alias, analyzer::Analyzer, assignment::Assignment, assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding, color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation, compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler, conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError, count::Count, delimiter::Delimiter, dependency::Dependency, dump_format::DumpFormat, enclosure::Enclosure, error::Error, evaluator::Evaluator, expression::Expression, fragment::Fragment, function::Function, function_context::FunctionContext, interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List, load_dotenv::load_dotenv, loader::Loader, name::Name, ordinal::Ordinal, output::output, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform, platform_interface::PlatformInterface, position::Position, positional::Positional, range_ext::RangeExt, recipe::Recipe, recipe_context::RecipeContext, recipe_resolver::RecipeResolver, scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang, shell::Shell, show_whitespace::ShowWhitespace, string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning, }, std::{ cmp, collections::{BTreeMap, BTreeSet, HashMap}, env, ffi::{OsStr, OsString}, fmt::{self, Debug, Display, Formatter}, fs, io::{self, Cursor, Write}, iter::{self, FromIterator}, mem, ops::{Index, Range, RangeInclusive}, path::{self, Path, PathBuf}, process::{self, Command, ExitStatus, Stdio}, rc::Rc, str::{self, Chars}, sync::{Mutex, MutexGuard}, vec, }, { camino::Utf8Path, derivative::Derivative, edit_distance::edit_distance, lexiclean::Lexiclean, libc::EXIT_FAILURE, log::{info, warn}, regex::Regex, serde::{ ser::{SerializeMap, SerializeSeq}, Serialize, Serializer, }, snafu::{ResultExt, Snafu}, strum::{Display, EnumString, IntoStaticStr}, typed_arena::Arena, unicode_width::{UnicodeWidthChar, UnicodeWidthStr}, }, }; #[cfg(test)] pub(crate) use crate::{node::Node, tree::Tree}; pub use crate::run::run; // Used in integration tests. #[doc(hidden)] pub use unindent::unindent; pub(crate) type CompileResult<'a, T = ()> = Result>; pub(crate) type ConfigResult = Result; pub(crate) type RunResult<'a, T = ()> = Result>; pub(crate) type SearchResult = Result; #[cfg(test)] #[macro_use] pub mod testing; #[cfg(test)] #[macro_use] pub mod tree; #[cfg(test)] pub mod node; #[cfg(fuzzing)] pub mod fuzzing; // Used by Janus, https://github.com/casey/janus, a tool // that analyses all public justfiles on GitHub to avoid // breaking changes. #[doc(hidden)] pub mod summary; mod alias; mod analyzer; mod assignment; mod assignment_resolver; mod ast; mod attribute; mod binding; mod color; mod color_display; mod command_ext; mod compilation; mod compile_error; mod compile_error_kind; mod compiler; mod completions; mod conditional_operator; mod config; mod config_error; mod count; mod delimiter; mod dependency; mod dump_format; mod enclosure; mod error; mod evaluator; mod expression; mod fragment; mod function; mod function_context; mod interrupt_guard; mod interrupt_handler; mod item; mod justfile; mod keyed; mod keyword; mod lexer; mod line; mod list; mod load_dotenv; mod loader; mod name; mod ordinal; mod output; mod output_error; mod parameter; mod parameter_kind; mod parser; mod platform; mod platform_interface; mod position; mod positional; mod range_ext; mod recipe; mod recipe_context; mod recipe_resolver; mod run; mod scope; mod search; mod search_config; mod search_error; mod set; mod setting; mod settings; mod shebang; mod shell; mod show_whitespace; mod string_kind; mod string_literal; mod subcommand; mod suggestion; mod table; mod thunk; mod token; mod token_kind; mod unindent; mod unresolved_dependency; mod unresolved_recipe; mod use_color; mod variables; mod verbosity; mod warning; just-1.21.0/src/line.rs000064400000000000000000000024161046102023000127610ustar 00000000000000use super::*; /// A single line in a recipe body, consisting of any number of `Fragment`s. #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(transparent)] pub(crate) struct Line<'src> { pub(crate) fragments: Vec>, } impl<'src> Line<'src> { pub(crate) fn is_empty(&self) -> bool { self.fragments.is_empty() } pub(crate) fn is_comment(&self) -> bool { matches!( self.fragments.first(), Some(Fragment::Text { token }) if token.lexeme().starts_with('#'), ) } pub(crate) fn is_continuation(&self) -> bool { matches!( self.fragments.last(), Some(Fragment::Text { token }) if token.lexeme().ends_with('\\'), ) } pub(crate) fn is_shebang(&self) -> bool { matches!( self.fragments.first(), Some(Fragment::Text { token }) if token.lexeme().starts_with("#!"), ) } pub(crate) fn is_quiet(&self) -> bool { matches!( self.fragments.first(), Some(Fragment::Text { token }) if token.lexeme().starts_with('@') || token.lexeme().starts_with("-@"), ) } pub(crate) fn is_infallible(&self) -> bool { matches!( self.fragments.first(), Some(Fragment::Text { token }) if token.lexeme().starts_with('-') || token.lexeme().starts_with("@-"), ) } } just-1.21.0/src/list.rs000064400000000000000000000062321046102023000130050ustar 00000000000000use super::*; pub struct List + Clone> { conjunction: &'static str, values: I, } impl + Clone> List { pub fn or>(values: II) -> List { List { conjunction: "or", values: values.into_iter(), } } pub fn and>(values: II) -> List { List { conjunction: "and", values: values.into_iter(), } } pub fn or_ticked>( values: II, ) -> List, impl Iterator> + Clone> { List::or(values.into_iter().map(Enclosure::tick)) } pub fn and_ticked>( values: II, ) -> List, impl Iterator> + Clone> { List::and(values.into_iter().map(Enclosure::tick)) } } impl + Clone> Display for List { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let mut values = self.values.clone().fuse(); if let Some(first) = values.next() { write!(f, "{first}")?; } else { return Ok(()); } let second = values.next(); if second.is_none() { return Ok(()); } let third = values.next(); if let (Some(second), None) = (second.as_ref(), third.as_ref()) { write!(f, " {} {second}", self.conjunction)?; return Ok(()); } let mut current = second; let mut next = third; loop { match (current, next) { (Some(c), Some(n)) => { write!(f, ", {c}")?; current = Some(n); next = values.next(); } (Some(c), None) => { write!(f, ", {} {c}", self.conjunction)?; return Ok(()); } _ => unreachable!("Iterator was fused, but returned Some after None"), } } } } #[cfg(test)] mod tests { use super::*; #[test] fn or() { assert_eq!("1", List::or(&[1]).to_string()); assert_eq!("1 or 2", List::or(&[1, 2]).to_string()); assert_eq!("1, 2, or 3", List::or(&[1, 2, 3]).to_string()); assert_eq!("1, 2, 3, or 4", List::or(&[1, 2, 3, 4]).to_string()); } #[test] fn and() { assert_eq!("1", List::and(&[1]).to_string()); assert_eq!("1 and 2", List::and(&[1, 2]).to_string()); assert_eq!("1, 2, and 3", List::and(&[1, 2, 3]).to_string()); assert_eq!("1, 2, 3, and 4", List::and(&[1, 2, 3, 4]).to_string()); } #[test] fn or_ticked() { assert_eq!("`1`", List::or_ticked(&[1]).to_string()); assert_eq!("`1` or `2`", List::or_ticked(&[1, 2]).to_string()); assert_eq!("`1`, `2`, or `3`", List::or_ticked(&[1, 2, 3]).to_string()); assert_eq!( "`1`, `2`, `3`, or `4`", List::or_ticked(&[1, 2, 3, 4]).to_string() ); } #[test] fn and_ticked() { assert_eq!("`1`", List::and_ticked(&[1]).to_string()); assert_eq!("`1` and `2`", List::and_ticked(&[1, 2]).to_string()); assert_eq!( "`1`, `2`, and `3`", List::and_ticked(&[1, 2, 3]).to_string() ); assert_eq!( "`1`, `2`, `3`, and `4`", List::and_ticked(&[1, 2, 3, 4]).to_string() ); } } just-1.21.0/src/load_dotenv.rs000064400000000000000000000022771046102023000143350ustar 00000000000000use super::*; const DEFAULT_DOTENV_FILENAME: &str = ".env"; pub(crate) fn load_dotenv( config: &Config, settings: &Settings, working_directory: &Path, ) -> RunResult<'static, BTreeMap> { let dotenv_filename = config .dotenv_filename .as_ref() .or(settings.dotenv_filename.as_ref()); let dotenv_path = config .dotenv_path .as_ref() .or(settings.dotenv_path.as_ref()); if !settings.dotenv_load.unwrap_or(false) && dotenv_filename.is_none() && dotenv_path.is_none() { return Ok(BTreeMap::new()); } if let Some(path) = dotenv_path { return load_from_file(path); } let filename = dotenv_filename.map_or(DEFAULT_DOTENV_FILENAME, |s| s.as_str()); for directory in working_directory.ancestors() { let path = directory.join(filename); if path.is_file() { return load_from_file(&path); } } Ok(BTreeMap::new()) } fn load_from_file(path: &Path) -> RunResult<'static, BTreeMap> { let iter = dotenvy::from_path_iter(path)?; let mut dotenv = BTreeMap::new(); for result in iter { let (key, value) = result?; if env::var_os(&key).is_none() { dotenv.insert(key, value); } } Ok(dotenv) } just-1.21.0/src/loader.rs000064400000000000000000000012251046102023000132750ustar 00000000000000use super::*; pub(crate) struct Loader { srcs: Arena, paths: Arena, } impl Loader { pub(crate) fn new() -> Self { Loader { srcs: Arena::new(), paths: Arena::new(), } } pub(crate) fn load<'src>( &'src self, root: &Path, path: &Path, ) -> RunResult<(&'src Path, &'src str)> { let src = fs::read_to_string(path).map_err(|io_error| Error::Load { path: path.to_owned(), io_error, })?; let relative = if let Ok(path) = path.strip_prefix(root.parent().unwrap()) { path } else { path }; Ok((self.paths.alloc(relative.into()), self.srcs.alloc(src))) } } just-1.21.0/src/main.rs000064400000000000000000000001231046102023000127470ustar 00000000000000fn main() { if let Err(code) = just::run() { std::process::exit(code); } } just-1.21.0/src/name.rs000064400000000000000000000030311046102023000127440ustar 00000000000000use super::*; /// A name. This is effectively just a `Token` of kind `Identifier`, but we give /// it its own type for clarity. #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] pub(crate) struct Name<'src> { pub(crate) column: usize, pub(crate) length: usize, pub(crate) line: usize, pub(crate) offset: usize, pub(crate) path: &'src Path, pub(crate) src: &'src str, } impl<'src> Name<'src> { /// The name's text contents pub(crate) fn lexeme(&self) -> &'src str { &self.src[self.offset..self.offset + self.length] } /// Turn this name back into a token pub(crate) fn token(&self) -> Token<'src> { Token { column: self.column, kind: TokenKind::Identifier, length: self.length, line: self.line, offset: self.offset, path: self.path, src: self.src, } } pub(crate) fn from_identifier(token: Token<'src>) -> Name { assert_eq!(token.kind, TokenKind::Identifier); Name { column: token.column, length: token.length, line: token.line, offset: token.offset, path: token.path, src: token.src, } } pub(crate) fn error(&self, kind: CompileErrorKind<'src>) -> CompileError<'src> { self.token().error(kind) } } impl Display for Name<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "{}", self.lexeme()) } } impl<'src> Serialize for Name<'src> { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(self.lexeme()) } } just-1.21.0/src/node.rs000064400000000000000000000171451046102023000127640ustar 00000000000000use super::*; /// Methods common to all AST nodes. Currently only used in parser unit tests. pub(crate) trait Node<'src> { /// Construct an untyped tree of atoms representing this Node. This function, /// and `Tree` type, are only used in parser unit tests. fn tree(&self) -> Tree<'src>; } impl<'src> Node<'src> for Ast<'src> { fn tree(&self) -> Tree<'src> { Tree::atom("justfile") .extend(self.items.iter().map(Node::tree)) .extend(self.warnings.iter().map(Node::tree)) } } impl<'src> Node<'src> for Item<'src> { fn tree(&self) -> Tree<'src> { match self { Item::Alias(alias) => alias.tree(), Item::Assignment(assignment) => assignment.tree(), Item::Comment(comment) => comment.tree(), Item::Import { relative, optional, .. } => { let mut tree = Tree::atom("import"); if *optional { tree = tree.push("?"); } tree.push(format!("{relative}")) } Item::Module { name, optional, relative, .. } => { let mut tree = Tree::atom("mod"); if *optional { tree = tree.push("?"); } tree = tree.push(name.lexeme()); if let Some(relative) = relative { tree = tree.push(format!("{relative}")); } tree } Item::Recipe(recipe) => recipe.tree(), Item::Set(set) => set.tree(), } } } impl<'src> Node<'src> for Alias<'src, Name<'src>> { fn tree(&self) -> Tree<'src> { Tree::atom(Keyword::Alias.lexeme()) .push(self.name.lexeme()) .push(self.target.lexeme()) } } impl<'src> Node<'src> for Assignment<'src> { fn tree(&self) -> Tree<'src> { if self.export { Tree::atom("assignment") .push("#") .push(Keyword::Export.lexeme()) } else { Tree::atom("assignment") } .push(self.name.lexeme()) .push(self.value.tree()) } } impl<'src> Node<'src> for Expression<'src> { fn tree(&self) -> Tree<'src> { match self { Expression::Concatenation { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()), Expression::Conditional { lhs, rhs, then, otherwise, operator, } => { let mut tree = Tree::atom(Keyword::If.lexeme()); tree.push_mut(lhs.tree()); tree.push_mut(operator.to_string()); tree.push_mut(rhs.tree()); tree.push_mut(then.tree()); tree.push_mut(otherwise.tree()); tree } Expression::Call { thunk } => { use Thunk::*; let mut tree = Tree::atom("call"); match thunk { Nullary { name, .. } => tree.push_mut(name.lexeme()), Unary { name, arg, .. } => { tree.push_mut(name.lexeme()); tree.push_mut(arg.tree()); } UnaryOpt { name, args: (a, b), .. } => { tree.push_mut(name.lexeme()); tree.push_mut(a.tree()); if let Some(b) = b.as_ref() { tree.push_mut(b.tree()); } } Binary { name, args: [a, b], .. } => { tree.push_mut(name.lexeme()); tree.push_mut(a.tree()); tree.push_mut(b.tree()); } BinaryPlus { name, args: ([a, b], rest), .. } => { tree.push_mut(name.lexeme()); tree.push_mut(a.tree()); tree.push_mut(b.tree()); for arg in rest { tree.push_mut(arg.tree()); } } Ternary { name, args: [a, b, c], .. } => { tree.push_mut(name.lexeme()); tree.push_mut(a.tree()); tree.push_mut(b.tree()); tree.push_mut(c.tree()); } } tree } Expression::Variable { name } => Tree::atom(name.lexeme()), Expression::StringLiteral { string_literal: StringLiteral { cooked, .. }, } => Tree::string(cooked), Expression::Backtick { contents, .. } => Tree::atom("backtick").push(Tree::string(contents)), Expression::Group { contents } => Tree::List(vec![contents.tree()]), Expression::Join { lhs: None, rhs } => Tree::atom("/").push(rhs.tree()), Expression::Join { lhs: Some(lhs), rhs, } => Tree::atom("/").push(lhs.tree()).push(rhs.tree()), } } } impl<'src> Node<'src> for UnresolvedRecipe<'src> { fn tree(&self) -> Tree<'src> { let mut t = Tree::atom("recipe"); if self.quiet { t.push_mut("#"); t.push_mut("quiet"); } if let Some(doc) = self.doc { t.push_mut(Tree::string(doc)); } t.push_mut(self.name.lexeme()); if !self.parameters.is_empty() { let mut params = Tree::atom("params"); for parameter in &self.parameters { if let Some(prefix) = parameter.kind.prefix() { params.push_mut(prefix); } params.push_mut(parameter.tree()); } t.push_mut(params); } if !self.dependencies.is_empty() { let mut dependencies = Tree::atom("deps"); let mut subsequents = Tree::atom("sups"); for (i, dependency) in self.dependencies.iter().enumerate() { let mut d = Tree::atom(dependency.recipe.lexeme()); for argument in &dependency.arguments { d.push_mut(argument.tree()); } if i < self.priors { dependencies.push_mut(d); } else { subsequents.push_mut(d); } } if let Tree::List(_) = dependencies { t.push_mut(dependencies); } if let Tree::List(_) = subsequents { t.push_mut(subsequents); } } if !self.body.is_empty() { t.push_mut(Tree::atom("body").extend(self.body.iter().map(Node::tree))); } t } } impl<'src> Node<'src> for Parameter<'src> { fn tree(&self) -> Tree<'src> { let mut children = vec![Tree::atom(self.name.lexeme())]; if let Some(default) = &self.default { children.push(default.tree()); } Tree::List(children) } } impl<'src> Node<'src> for Line<'src> { fn tree(&self) -> Tree<'src> { Tree::list(self.fragments.iter().map(Node::tree)) } } impl<'src> Node<'src> for Fragment<'src> { fn tree(&self) -> Tree<'src> { match self { Fragment::Text { token } => Tree::string(token.lexeme()), Fragment::Interpolation { expression } => Tree::List(vec![expression.tree()]), } } } impl<'src> Node<'src> for Set<'src> { fn tree(&self) -> Tree<'src> { let mut set = Tree::atom(Keyword::Set.lexeme()); set.push_mut(self.name.lexeme().replace('-', "_")); match &self.value { Setting::AllowDuplicateRecipes(value) | Setting::DotenvLoad(value) | Setting::Export(value) | Setting::Fallback(value) | Setting::PositionalArguments(value) | Setting::WindowsPowerShell(value) | Setting::IgnoreComments(value) => { set.push_mut(value.to_string()); } Setting::Shell(Shell { command, arguments }) | Setting::WindowsShell(Shell { command, arguments }) => { set.push_mut(Tree::string(&command.cooked)); for argument in arguments { set.push_mut(Tree::string(&argument.cooked)); } } Setting::DotenvFilename(value) | Setting::DotenvPath(value) | Setting::Tempdir(value) => { set.push_mut(Tree::string(value)); } } set } } impl<'src> Node<'src> for Warning { fn tree(&self) -> Tree<'src> { unreachable!() } } impl<'src> Node<'src> for str { fn tree(&self) -> Tree<'src> { Tree::atom("comment").push(["\"", self, "\""].concat()) } } just-1.21.0/src/ordinal.rs000064400000000000000000000003041046102023000134540ustar 00000000000000pub(crate) trait Ordinal { /// Convert an index starting at 0 to an ordinal starting at 1 fn ordinal(self) -> Self; } impl Ordinal for usize { fn ordinal(self) -> Self { self + 1 } } just-1.21.0/src/output.rs000064400000000000000000000017531046102023000133750ustar 00000000000000use super::*; /// Run a command and return the data it wrote to stdout as a string pub(crate) fn output(mut command: Command) -> Result { match command.output() { Ok(output) => { if let Some(code) = output.status.code() { if code != 0 { return Err(OutputError::Code(code)); } } else { let signal = Platform::signal_from_exit_status(output.status); return Err(match signal { Some(signal) => OutputError::Signal(signal), None => OutputError::Unknown, }); } match str::from_utf8(&output.stdout) { Err(error) => Err(OutputError::Utf8(error)), Ok(utf8) => Ok( if utf8.ends_with('\n') { &utf8[0..utf8.len() - 1] } else if utf8.ends_with("\r\n") { &utf8[0..utf8.len() - 2] } else { utf8 } .to_owned(), ), } } Err(io_error) => Err(OutputError::Io(io_error)), } } just-1.21.0/src/output_error.rs000064400000000000000000000014301046102023000145760ustar 00000000000000use super::*; #[derive(Debug)] pub(crate) enum OutputError { /// Non-zero exit code Code(i32), /// IO error Io(io::Error), /// Terminated by signal Signal(i32), /// Unknown failure Unknown, /// Stdout not UTF-8 Utf8(str::Utf8Error), } impl Display for OutputError { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { match *self { Self::Code(code) => write!(f, "Process exited with status code {code}"), Self::Io(ref io_error) => write!(f, "Error executing process: {io_error}"), Self::Signal(signal) => write!(f, "Process terminated by signal {signal}"), Self::Unknown => write!(f, "Process experienced an unknown failure"), Self::Utf8(ref err) => write!(f, "Could not convert process stdout to UTF-8: {err}"), } } } just-1.21.0/src/parameter.rs000064400000000000000000000016021046102023000140060ustar 00000000000000use super::*; /// A single function parameter #[derive(PartialEq, Debug, Clone, Serialize)] pub(crate) struct Parameter<'src> { /// An optional default expression pub(crate) default: Option>, /// Export parameter as environment variable pub(crate) export: bool, /// The kind of parameter pub(crate) kind: ParameterKind, /// The parameter name pub(crate) name: Name<'src>, } impl<'src> ColorDisplay for Parameter<'src> { fn fmt(&self, f: &mut Formatter, color: Color) -> Result<(), fmt::Error> { if let Some(prefix) = self.kind.prefix() { write!(f, "{}", color.annotation().paint(prefix))?; } if self.export { write!(f, "$")?; } write!(f, "{}", color.parameter().paint(self.name.lexeme()))?; if let Some(ref default) = self.default { write!(f, "={}", color.string().paint(&default.to_string()))?; } Ok(()) } } just-1.21.0/src/parameter_kind.rs000064400000000000000000000011611046102023000150130ustar 00000000000000use super::*; /// Parameters can either be… #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub(crate) enum ParameterKind { /// …singular, accepting a single argument Singular, /// …variadic, accepting one or more arguments Plus, /// …variadic, accepting zero or more arguments Star, } impl ParameterKind { pub(crate) fn prefix(self) -> Option<&'static str> { match self { Self::Singular => None, Self::Plus => Some("+"), Self::Star => Some("*"), } } pub(crate) fn is_variadic(self) -> bool { self != Self::Singular } } just-1.21.0/src/parser.rs000064400000000000000000001604411046102023000133310ustar 00000000000000use {super::*, TokenKind::*}; /// Just language parser /// /// The parser is a (hopefully) straightforward recursive descent parser. /// /// It uses a few tokens of lookahead to disambiguate different constructs. /// /// The `expect_*` and `presume_`* methods are similar in that they assert the /// type of unparsed tokens and consume them. However, upon encountering an /// unexpected token, the `expect_*` methods return an unexpected token error, /// whereas the `presume_*` tokens return an internal error. /// /// The `presume_*` methods are used when the token stream has been inspected in /// some other way, and thus encountering an unexpected token is a bug in Just, /// and not a syntax error. /// /// All methods starting with `parse_*` parse and return a language construct. /// /// The parser tracks an expected set of tokens as it parses. This set contains /// all tokens which would have been accepted at the current point in the parse. /// Whenever the parser tests for a token that would be accepted, but does not /// find it, it adds that token to the set. When the parser accepts a token, the /// set is cleared. If the parser finds a token which is unexpected, the /// contents of the set is printed in the resultant error message. pub(crate) struct Parser<'tokens, 'src> { /// Source tokens tokens: &'tokens [Token<'src>], /// Index of the next un-parsed token next: usize, /// Current expected tokens expected: BTreeSet, /// Current recursion depth depth: usize, /// Path to the file being parsed path: PathBuf, /// Depth of submodule being parsed submodule: u32, } impl<'tokens, 'src> Parser<'tokens, 'src> { /// Parse `tokens` into an `Ast` pub(crate) fn parse( submodule: u32, path: &Path, tokens: &'tokens [Token<'src>], ) -> CompileResult<'src, Ast<'src>> { Parser { depth: 0, expected: BTreeSet::new(), next: 0, path: path.into(), submodule, tokens, } .parse_ast() } fn error(&self, kind: CompileErrorKind<'src>) -> CompileResult<'src, CompileError<'src>> { Ok(self.next()?.error(kind)) } /// Construct an unexpected token error with the token returned by /// `Parser::next` fn unexpected_token(&self) -> CompileResult<'src, CompileError<'src>> { self.error(CompileErrorKind::UnexpectedToken { expected: self .expected .iter() .copied() .filter(|kind| *kind != ByteOrderMark) .collect::>(), found: self.next()?.kind, }) } fn internal_error(&self, message: impl Into) -> CompileResult<'src, CompileError<'src>> { self.error(CompileErrorKind::Internal { message: message.into(), }) } /// An iterator over the remaining significant tokens fn rest(&self) -> impl Iterator> + 'tokens { self.tokens[self.next..] .iter() .copied() .filter(|token| token.kind != Whitespace) } /// The next significant token fn next(&self) -> CompileResult<'src, Token<'src>> { if let Some(token) = self.rest().next() { Ok(token) } else { Err(self.internal_error("`Parser::next()` called after end of token stream")?) } } /// Check if the next significant token is of kind `kind` fn next_is(&mut self, kind: TokenKind) -> bool { self.next_are(&[kind]) } /// Check if the next significant tokens are of kinds `kinds` /// /// The first token in `kinds` will be added to the expected token set. fn next_are(&mut self, kinds: &[TokenKind]) -> bool { if let Some(&kind) = kinds.first() { self.expected.insert(kind); } let mut rest = self.rest(); for kind in kinds { match rest.next() { Some(token) => { if token.kind != *kind { return false; } } None => return false, } } true } /// Advance past one significant token, clearing the expected token set. fn advance(&mut self) -> CompileResult<'src, Token<'src>> { self.expected.clear(); for skipped in &self.tokens[self.next..] { self.next += 1; if skipped.kind != Whitespace { return Ok(*skipped); } } Err(self.internal_error("`Parser::advance()` advanced past end of token stream")?) } /// Return the next token if it is of kind `expected`, otherwise, return an /// unexpected token error fn expect(&mut self, expected: TokenKind) -> CompileResult<'src, Token<'src>> { if let Some(token) = self.accept(expected)? { Ok(token) } else { Err(self.unexpected_token()?) } } /// Return an unexpected token error if the next token is not an EOL fn expect_eol(&mut self) -> CompileResult<'src> { self.accept(Comment)?; if self.next_is(Eof) { return Ok(()); } self.expect(Eol).map(|_| ()) } fn expect_keyword(&mut self, expected: Keyword) -> CompileResult<'src> { let found = self.advance()?; if found.kind == Identifier && expected == found.lexeme() { Ok(()) } else { Err(found.error(CompileErrorKind::ExpectedKeyword { expected: vec![expected], found, })) } } /// Return an internal error if the next token is not of kind `Identifier` /// with lexeme `lexeme`. fn presume_keyword(&mut self, keyword: Keyword) -> CompileResult<'src> { let next = self.advance()?; if next.kind != Identifier { Err(self.internal_error(format!( "Presumed next token would have kind {Identifier}, but found {}", next.kind ))?) } else if keyword == next.lexeme() { Ok(()) } else { Err(self.internal_error(format!( "Presumed next token would have lexeme \"{keyword}\", but found \"{}\"", next.lexeme(), ))?) } } /// Return an internal error if the next token is not of kind `kind`. fn presume(&mut self, kind: TokenKind) -> CompileResult<'src, Token<'src>> { let next = self.advance()?; if next.kind == kind { Ok(next) } else { Err(self.internal_error(format!( "Presumed next token would have kind {kind:?}, but found {:?}", next.kind ))?) } } /// Return an internal error if the next token is not one of kinds `kinds`. fn presume_any(&mut self, kinds: &[TokenKind]) -> CompileResult<'src, Token<'src>> { let next = self.advance()?; if kinds.contains(&next.kind) { Ok(next) } else { Err(self.internal_error(format!( "Presumed next token would be {}, but found {}", List::or(kinds), next.kind ))?) } } /// Accept and return a token of kind `kind` fn accept(&mut self, kind: TokenKind) -> CompileResult<'src, Option>> { if self.next_is(kind) { Ok(Some(self.advance()?)) } else { Ok(None) } } /// Return an error if the next token is of kind `forbidden` fn forbid(&self, forbidden: TokenKind, error: F) -> CompileResult<'src> where F: FnOnce(Token) -> CompileError, { let next = self.next()?; if next.kind == forbidden { Err(error(next)) } else { Ok(()) } } /// Accept a token of kind `Identifier` and parse into a `Name` fn accept_name(&mut self) -> CompileResult<'src, Option>> { if self.next_is(Identifier) { Ok(Some(self.parse_name()?)) } else { Ok(None) } } fn accepted_keyword(&mut self, keyword: Keyword) -> CompileResult<'src, bool> { let next = self.next()?; if next.kind == Identifier && next.lexeme() == keyword.lexeme() { self.advance()?; Ok(true) } else { Ok(false) } } /// Accept a dependency fn accept_dependency(&mut self) -> CompileResult<'src, Option>> { if let Some(recipe) = self.accept_name()? { Ok(Some(UnresolvedDependency { arguments: Vec::new(), recipe, })) } else if self.accepted(ParenL)? { let recipe = self.parse_name()?; let mut arguments = Vec::new(); while !self.accepted(ParenR)? { arguments.push(self.parse_expression()?); } Ok(Some(UnresolvedDependency { recipe, arguments })) } else { Ok(None) } } /// Accept and return `true` if next token is of kind `kind` fn accepted(&mut self, kind: TokenKind) -> CompileResult<'src, bool> { Ok(self.accept(kind)?.is_some()) } /// Parse a justfile, consumes self fn parse_ast(mut self) -> CompileResult<'src, Ast<'src>> { fn pop_doc_comment<'src>( items: &mut Vec>, eol_since_last_comment: bool, ) -> Option<&'src str> { if !eol_since_last_comment { if let Some(Item::Comment(contents)) = items.last() { let doc = Some(contents[1..].trim_start()); items.pop(); return doc; } } None } let mut items = Vec::new(); let mut eol_since_last_comment = false; self.accept(ByteOrderMark)?; loop { let next = self.next()?; if let Some(comment) = self.accept(Comment)? { items.push(Item::Comment(comment.lexeme().trim_end())); self.expect_eol()?; eol_since_last_comment = false; } else if self.accepted(Eol)? { eol_since_last_comment = true; } else if self.accepted(Eof)? { break; } else if self.next_is(Identifier) { match Keyword::from_lexeme(next.lexeme()) { Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => { items.push(Item::Alias(self.parse_alias(BTreeSet::new())?)); } Some(Keyword::Export) if self.next_are(&[Identifier, Identifier, ColonEquals]) => { self.presume_keyword(Keyword::Export)?; items.push(Item::Assignment(self.parse_assignment(true)?)); } Some(Keyword::Import) if self.next_are(&[Identifier, StringToken]) || self.next_are(&[Identifier, QuestionMark]) => { self.presume_keyword(Keyword::Import)?; let optional = self.accepted(QuestionMark)?; let (path, relative) = self.parse_string_literal_token()?; items.push(Item::Import { absolute: None, optional, path, relative, }); } Some(Keyword::Mod) if self.next_are(&[Identifier, Identifier, StringToken]) || self.next_are(&[Identifier, Identifier, Eof]) || self.next_are(&[Identifier, Identifier, Eol]) || self.next_are(&[Identifier, QuestionMark]) => { self.presume_keyword(Keyword::Mod)?; let optional = self.accepted(QuestionMark)?; let name = self.parse_name()?; let relative = if self.next_is(StringToken) { Some(self.parse_string_literal()?) } else { None }; items.push(Item::Module { absolute: None, name, optional, relative, }); } Some(Keyword::Set) if self.next_are(&[Identifier, Identifier, ColonEquals]) || self.next_are(&[Identifier, Identifier, Comment, Eof]) || self.next_are(&[Identifier, Identifier, Comment, Eol]) || self.next_are(&[Identifier, Identifier, Eof]) || self.next_are(&[Identifier, Identifier, Eol]) => { items.push(Item::Set(self.parse_set()?)); } _ => { if self.next_are(&[Identifier, ColonEquals]) { items.push(Item::Assignment(self.parse_assignment(false)?)); } else { let doc = pop_doc_comment(&mut items, eol_since_last_comment); items.push(Item::Recipe(self.parse_recipe( doc, false, BTreeSet::new(), )?)); } } } } else if self.accepted(At)? { let doc = pop_doc_comment(&mut items, eol_since_last_comment); items.push(Item::Recipe(self.parse_recipe( doc, true, BTreeSet::new(), )?)); } else if let Some(attributes) = self.parse_attributes()? { let next_keyword = Keyword::from_lexeme(self.next()?.lexeme()); match next_keyword { Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => { items.push(Item::Alias(self.parse_alias(attributes)?)); } _ => { let quiet = self.accepted(At)?; let doc = pop_doc_comment(&mut items, eol_since_last_comment); items.push(Item::Recipe(self.parse_recipe(doc, quiet, attributes)?)); } } } else { return Err(self.unexpected_token()?); } } if self.next == self.tokens.len() { Ok(Ast { warnings: Vec::new(), items, }) } else { Err(self.internal_error(format!( "Parse completed with {} unparsed tokens", self.tokens.len() - self.next, ))?) } } /// Parse an alias, e.g `alias name := target` fn parse_alias( &mut self, attributes: BTreeSet, ) -> CompileResult<'src, Alias<'src, Name<'src>>> { self.presume_keyword(Keyword::Alias)?; let name = self.parse_name()?; self.presume_any(&[Equals, ColonEquals])?; let target = self.parse_name()?; self.expect_eol()?; Ok(Alias { attributes, name, target, }) } /// Parse an assignment, e.g. `foo := bar` fn parse_assignment(&mut self, export: bool) -> CompileResult<'src, Assignment<'src>> { let name = self.parse_name()?; self.presume_any(&[Equals, ColonEquals])?; let value = self.parse_expression()?; self.expect_eol()?; Ok(Assignment { export, name, value, }) } /// Parse an expression, e.g. `1 + 2` fn parse_expression(&mut self) -> CompileResult<'src, Expression<'src>> { if self.depth == if cfg!(windows) { 48 } else { 256 } { let token = self.next()?; return Err(CompileError::new( token, CompileErrorKind::ParsingRecursionDepthExceeded, )); } self.depth += 1; let expression = if self.accepted_keyword(Keyword::If)? { self.parse_conditional()? } else if self.accepted(Slash)? { let lhs = None; let rhs = Box::new(self.parse_expression()?); Expression::Join { lhs, rhs } } else { let value = self.parse_value()?; if self.accepted(Slash)? { let lhs = Some(Box::new(value)); let rhs = Box::new(self.parse_expression()?); Expression::Join { lhs, rhs } } else if self.accepted(Plus)? { let lhs = Box::new(value); let rhs = Box::new(self.parse_expression()?); Expression::Concatenation { lhs, rhs } } else { value } }; self.depth -= 1; Ok(expression) } /// Parse a conditional, e.g. `if a == b { "foo" } else { "bar" }` fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> { let lhs = self.parse_expression()?; let operator = if self.accepted(BangEquals)? { ConditionalOperator::Inequality } else if self.accepted(EqualsTilde)? { ConditionalOperator::RegexMatch } else { self.expect(EqualsEquals)?; ConditionalOperator::Equality }; let rhs = self.parse_expression()?; self.expect(BraceL)?; let then = self.parse_expression()?; self.expect(BraceR)?; self.expect_keyword(Keyword::Else)?; let otherwise = if self.accepted_keyword(Keyword::If)? { self.parse_conditional()? } else { self.expect(BraceL)?; let otherwise = self.parse_expression()?; self.expect(BraceR)?; otherwise }; Ok(Expression::Conditional { lhs: Box::new(lhs), rhs: Box::new(rhs), then: Box::new(then), otherwise: Box::new(otherwise), operator, }) } /// Parse a value, e.g. `(bar)` fn parse_value(&mut self) -> CompileResult<'src, Expression<'src>> { if self.next_is(StringToken) { Ok(Expression::StringLiteral { string_literal: self.parse_string_literal()?, }) } else if self.next_is(Backtick) { let next = self.next()?; let kind = StringKind::from_string_or_backtick(next)?; let contents = &next.lexeme()[kind.delimiter_len()..next.lexeme().len() - kind.delimiter_len()]; let token = self.advance()?; let contents = if kind.indented() { unindent(contents) } else { contents.to_owned() }; if contents.starts_with("#!") { return Err(next.error(CompileErrorKind::BacktickShebang)); } Ok(Expression::Backtick { contents, token }) } else if self.next_is(Identifier) { let name = self.parse_name()?; if self.next_is(ParenL) { let arguments = self.parse_sequence()?; Ok(Expression::Call { thunk: Thunk::resolve(name, arguments)?, }) } else { Ok(Expression::Variable { name }) } } else if self.next_is(ParenL) { self.presume(ParenL)?; let contents = Box::new(self.parse_expression()?); self.expect(ParenR)?; Ok(Expression::Group { contents }) } else { Err(self.unexpected_token()?) } } /// Parse a string literal, e.g. `"FOO"`, returning the string literal and the string token fn parse_string_literal_token( &mut self, ) -> CompileResult<'src, (Token<'src>, StringLiteral<'src>)> { let token = self.expect(StringToken)?; let kind = StringKind::from_string_or_backtick(token)?; let delimiter_len = kind.delimiter_len(); let raw = &token.lexeme()[delimiter_len..token.lexeme().len() - delimiter_len]; let unindented = if kind.indented() { unindent(raw) } else { raw.to_owned() }; let cooked = if kind.processes_escape_sequences() { let mut cooked = String::new(); let mut escape = false; for c in unindented.chars() { if escape { match c { 'n' => cooked.push('\n'), 'r' => cooked.push('\r'), 't' => cooked.push('\t'), '\\' => cooked.push('\\'), '\n' => {} '"' => cooked.push('"'), other => { return Err( token.error(CompileErrorKind::InvalidEscapeSequence { character: other }), ); } } escape = false; } else if c == '\\' { escape = true; } else { cooked.push(c); } } cooked } else { unindented }; Ok((token, StringLiteral { kind, raw, cooked })) } /// Parse a string literal, e.g. `"FOO"` fn parse_string_literal(&mut self) -> CompileResult<'src, StringLiteral<'src>> { let (_token, string_literal) = self.parse_string_literal_token()?; Ok(string_literal) } /// Parse a name from an identifier token fn parse_name(&mut self) -> CompileResult<'src, Name<'src>> { self.expect(Identifier).map(Name::from_identifier) } /// Parse sequence of comma-separated expressions fn parse_sequence(&mut self) -> CompileResult<'src, Vec>> { self.presume(ParenL)?; let mut elements = Vec::new(); while !self.next_is(ParenR) { elements.push(self.parse_expression()?); if !self.accepted(Comma)? { break; } } self.expect(ParenR)?; Ok(elements) } /// Parse a recipe fn parse_recipe( &mut self, doc: Option<&'src str>, quiet: bool, attributes: BTreeSet, ) -> CompileResult<'src, UnresolvedRecipe<'src>> { let name = self.parse_name()?; let mut positional = Vec::new(); while self.next_is(Identifier) || self.next_is(Dollar) { positional.push(self.parse_parameter(ParameterKind::Singular)?); } let kind = if self.accepted(Plus)? { ParameterKind::Plus } else if self.accepted(Asterisk)? { ParameterKind::Star } else { ParameterKind::Singular }; let variadic = if kind.is_variadic() { let variadic = self.parse_parameter(kind)?; self.forbid(Identifier, |token| { token.error(CompileErrorKind::ParameterFollowsVariadicParameter { parameter: token.lexeme(), }) })?; Some(variadic) } else { None }; self.expect(Colon)?; let mut dependencies = Vec::new(); while let Some(dependency) = self.accept_dependency()? { dependencies.push(dependency); } let priors = dependencies.len(); if self.accepted(AmpersandAmpersand)? { let mut subsequents = Vec::new(); while let Some(subsequent) = self.accept_dependency()? { subsequents.push(subsequent); } if subsequents.is_empty() { return Err(self.unexpected_token()?); } dependencies.append(&mut subsequents); } self.expect_eol()?; let body = self.parse_body()?; Ok(Recipe { shebang: body.first().map_or(false, Line::is_shebang), attributes, body, dependencies, doc, name, parameters: positional.into_iter().chain(variadic).collect(), path: self.path.clone(), priors, private: name.lexeme().starts_with('_'), quiet, depth: self.submodule, }) } /// Parse a recipe parameter fn parse_parameter(&mut self, kind: ParameterKind) -> CompileResult<'src, Parameter<'src>> { let export = self.accepted(Dollar)?; let name = self.parse_name()?; let default = if self.accepted(Equals)? { Some(self.parse_value()?) } else { None }; Ok(Parameter { default, export, kind, name, }) } /// Parse the body of a recipe fn parse_body(&mut self) -> CompileResult<'src, Vec>> { let mut lines = Vec::new(); if self.accepted(Indent)? { while !self.accepted(Dedent)? { let line = if self.accepted(Eol)? { Line { fragments: Vec::new(), } } else { let mut fragments = Vec::new(); while !(self.accepted(Eol)? || self.next_is(Dedent)) { if let Some(token) = self.accept(Text)? { fragments.push(Fragment::Text { token }); } else if self.accepted(InterpolationStart)? { fragments.push(Fragment::Interpolation { expression: self.parse_expression()?, }); self.expect(InterpolationEnd)?; } else { return Err(self.unexpected_token()?); } } Line { fragments } }; lines.push(line); } } while lines.last().map_or(false, Line::is_empty) { lines.pop(); } Ok(lines) } /// Parse a boolean setting value fn parse_set_bool(&mut self) -> CompileResult<'src, bool> { if !self.accepted(ColonEquals)? { return Ok(true); } let identifier = self.expect(Identifier)?; let value = if Keyword::True == identifier.lexeme() { true } else if Keyword::False == identifier.lexeme() { false } else { return Err(identifier.error(CompileErrorKind::ExpectedKeyword { expected: vec![Keyword::True, Keyword::False], found: identifier, })); }; Ok(value) } /// Parse a setting fn parse_set(&mut self) -> CompileResult<'src, Set<'src>> { self.presume_keyword(Keyword::Set)?; let name = Name::from_identifier(self.presume(Identifier)?); let lexeme = name.lexeme(); let Some(keyword) = Keyword::from_lexeme(lexeme) else { return Err(name.error(CompileErrorKind::UnknownSetting { setting: name.lexeme(), })); }; let set_bool = match keyword { Keyword::AllowDuplicateRecipes => { Some(Setting::AllowDuplicateRecipes(self.parse_set_bool()?)) } Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)), Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)), Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)), Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)), Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)), Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)), _ => None, }; if let Some(value) = set_bool { return Ok(Set { name, value }); } self.expect(ColonEquals)?; let set_value = match keyword { Keyword::DotenvFilename => Some(Setting::DotenvFilename(self.parse_string_literal()?.cooked)), Keyword::DotenvPath => Some(Setting::DotenvPath(self.parse_string_literal()?.cooked)), Keyword::Shell => Some(Setting::Shell(self.parse_shell()?)), Keyword::Tempdir => Some(Setting::Tempdir(self.parse_string_literal()?.cooked)), Keyword::WindowsShell => Some(Setting::WindowsShell(self.parse_shell()?)), _ => None, }; if let Some(value) = set_value { return Ok(Set { name, value }); } Err(name.error(CompileErrorKind::UnknownSetting { setting: name.lexeme(), })) } /// Parse a shell setting value fn parse_shell(&mut self) -> CompileResult<'src, Shell<'src>> { self.expect(BracketL)?; let command = self.parse_string_literal()?; let mut arguments = Vec::new(); if self.accepted(Comma)? { while !self.next_is(BracketR) { arguments.push(self.parse_string_literal()?); if !self.accepted(Comma)? { break; } } } self.expect(BracketR)?; Ok(Shell { arguments, command }) } /// Parse recipe attributes fn parse_attributes(&mut self) -> CompileResult<'src, Option>> { let mut attributes = BTreeMap::new(); while self.accepted(BracketL)? { loop { let name = self.parse_name()?; let attribute = Attribute::from_name(name).ok_or_else(|| { name.error(CompileErrorKind::UnknownAttribute { attribute: name.lexeme(), }) })?; if let Some(line) = attributes.get(&attribute) { return Err(name.error(CompileErrorKind::DuplicateAttribute { attribute: name.lexeme(), first: *line, })); } attributes.insert(attribute, name.line); if !self.accepted(Comma)? { break; } } self.expect(BracketR)?; self.expect_eol()?; } if attributes.is_empty() { Ok(None) } else { Ok(Some(attributes.into_keys().collect())) } } } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; use CompileErrorKind::*; macro_rules! test { { name: $name:ident, text: $text:expr, tree: $tree:tt, } => { #[test] fn $name() { let text: String = $text.into(); let want = tree!($tree); test(&text, want); } } } fn test(text: &str, want: Tree) { let unindented = unindent(text); let tokens = Lexer::test_lex(&unindented).expect("lexing failed"); let justfile = Parser::parse(0, &PathBuf::new(), &tokens).expect("parsing failed"); let have = justfile.tree(); if have != want { println!("parsed text: {unindented}"); println!("expected: {want}"); println!("but got: {have}"); println!("tokens: {tokens:?}"); panic!(); } } macro_rules! error { ( name: $name:ident, input: $input:expr, offset: $offset:expr, line: $line:expr, column: $column:expr, width: $width:expr, kind: $kind:expr, ) => { #[test] fn $name() { error($input, $offset, $line, $column, $width, $kind); } }; } fn error( src: &str, offset: usize, line: usize, column: usize, length: usize, kind: CompileErrorKind, ) { let tokens = Lexer::test_lex(src).expect("Lexing failed in parse test..."); match Parser::parse(0, &PathBuf::new(), &tokens) { Ok(_) => panic!("Parsing unexpectedly succeeded"), Err(have) => { let want = CompileError { token: Token { kind: have.token.kind, src, offset, line, column, length, path: "justfile".as_ref(), }, kind: Box::new(kind), }; assert_eq!(have, want); } } } test! { name: empty, text: "", tree: (justfile), } test! { name: empty_multiline, text: " ", tree: (justfile), } test! { name: whitespace, text: " ", tree: (justfile), } test! { name: alias_single, text: "alias t := test", tree: (justfile (alias t test)), } test! { name: alias_with_attribute, text: "[private]\nalias t := test", tree: (justfile (alias t test)), } test! { name: aliases_multiple, text: "alias t := test\nalias b := build", tree: ( justfile (alias t test) (alias b build) ), } test! { name: alias_equals, text: "alias t := test", tree: (justfile (alias t test) ), } test! { name: recipe_named_alias, text: r" [private] alias: echo 'echoing alias' ", tree: (justfile (recipe alias (body ("echo 'echoing alias'"))) ), } test! { name: export, text: r#"export x := "hello""#, tree: (justfile (assignment #export x "hello")), } test! { name: export_equals, text: r#"export x := "hello""#, tree: (justfile (assignment #export x "hello") ), } test! { name: assignment, text: r#"x := "hello""#, tree: (justfile (assignment x "hello")), } test! { name: assignment_equals, text: r#"x := "hello""#, tree: (justfile (assignment x "hello") ), } test! { name: backtick, text: "x := `hello`", tree: (justfile (assignment x (backtick "hello"))), } test! { name: variable, text: "x := y", tree: (justfile (assignment x y)), } test! { name: group, text: "x := (y)", tree: (justfile (assignment x (y))), } test! { name: addition_single, text: "x := a + b", tree: (justfile (assignment x (+ a b))), } test! { name: addition_chained, text: "x := a + b + c", tree: (justfile (assignment x (+ a (+ b c)))), } test! { name: call_one_arg, text: "x := env_var(y)", tree: (justfile (assignment x (call env_var y))), } test! { name: call_multiple_args, text: "x := env_var_or_default(y, z)", tree: (justfile (assignment x (call env_var_or_default y z))), } test! { name: call_trailing_comma, text: "x := env_var(y,)", tree: (justfile (assignment x (call env_var y))), } test! { name: recipe, text: "foo:", tree: (justfile (recipe foo)), } test! { name: recipe_multiple, text: " foo: bar: baz: ", tree: (justfile (recipe foo) (recipe bar) (recipe baz)), } test! { name: recipe_quiet, text: "@foo:", tree: (justfile (recipe #quiet foo)), } test! { name: recipe_parameter_single, text: "foo bar:", tree: (justfile (recipe foo (params (bar)))), } test! { name: recipe_parameter_multiple, text: "foo bar baz:", tree: (justfile (recipe foo (params (bar) (baz)))), } test! { name: recipe_default_single, text: r#"foo bar="baz":"#, tree: (justfile (recipe foo (params (bar "baz")))), } test! { name: recipe_default_multiple, text: r#"foo bar="baz" bob="biz":"#, tree: (justfile (recipe foo (params (bar "baz") (bob "biz")))), } test! { name: recipe_plus_variadic, text: r"foo +bar:", tree: (justfile (recipe foo (params +(bar)))), } test! { name: recipe_star_variadic, text: r"foo *bar:", tree: (justfile (recipe foo (params *(bar)))), } test! { name: recipe_variadic_string_default, text: r#"foo +bar="baz":"#, tree: (justfile (recipe foo (params +(bar "baz")))), } test! { name: recipe_variadic_variable_default, text: r"foo +bar=baz:", tree: (justfile (recipe foo (params +(bar baz)))), } test! { name: recipe_variadic_addition_group_default, text: r"foo +bar=(baz + bob):", tree: (justfile (recipe foo (params +(bar ((+ baz bob)))))), } test! { name: recipe_dependency_single, text: "foo: bar", tree: (justfile (recipe foo (deps bar))), } test! { name: recipe_dependency_multiple, text: "foo: bar baz", tree: (justfile (recipe foo (deps bar baz))), } test! { name: recipe_dependency_parenthesis, text: "foo: (bar)", tree: (justfile (recipe foo (deps bar))), } test! { name: recipe_dependency_argument_string, text: "foo: (bar 'baz')", tree: (justfile (recipe foo (deps (bar "baz")))), } test! { name: recipe_dependency_argument_identifier, text: "foo: (bar baz)", tree: (justfile (recipe foo (deps (bar baz)))), } test! { name: recipe_dependency_argument_concatenation, text: "foo: (bar 'a' + 'b' 'c' + 'd')", tree: (justfile (recipe foo (deps (bar (+ 'a' 'b') (+ 'c' 'd'))))), } test! { name: recipe_subsequent, text: "foo: && bar", tree: (justfile (recipe foo (sups bar))), } test! { name: recipe_line_single, text: "foo:\n bar", tree: (justfile (recipe foo (body ("bar")))), } test! { name: recipe_line_multiple, text: "foo:\n bar\n baz\n {{\"bob\"}}biz", tree: (justfile (recipe foo (body ("bar") ("baz") (("bob") "biz")))), } test! { name: recipe_line_interpolation, text: "foo:\n bar{{\"bob\"}}biz", tree: (justfile (recipe foo (body ("bar" ("bob") "biz")))), } test! { name: comment, text: "# foo", tree: (justfile (comment "# foo")), } test! { name: comment_before_alias, text: "# foo\nalias x := y", tree: (justfile (comment "# foo") (alias x y)), } test! { name: comment_after_alias, text: "alias x := y # foo", tree: (justfile (alias x y)), } test! { name: comment_assignment, text: "x := y # foo", tree: (justfile (assignment x y)), } test! { name: comment_export, text: "export x := y # foo", tree: (justfile (assignment #export x y)), } test! { name: comment_recipe, text: "foo: # bar", tree: (justfile (recipe foo)), } test! { name: comment_recipe_dependencies, text: "foo: bar # baz", tree: (justfile (recipe foo (deps bar))), } test! { name: doc_comment_single, text: " # foo bar: ", tree: (justfile (recipe "foo" bar)), } test! { name: doc_comment_recipe_clear, text: " # foo bar: baz: ", tree: (justfile (recipe "foo" bar) (recipe baz)), } test! { name: doc_comment_middle, text: " bar: # foo baz: ", tree: (justfile (recipe bar) (recipe "foo" baz)), } test! { name: doc_comment_assignment_clear, text: " # foo x := y bar: ", tree: (justfile (comment "# foo") (assignment x y) (recipe bar)), } test! { name: doc_comment_empty_line_clear, text: " # foo bar: ", tree: (justfile (comment "# foo") (recipe bar)), } test! { name: string_escape_tab, text: r#"x := "foo\tbar""#, tree: (justfile (assignment x "foo\tbar")), } test! { name: string_escape_newline, text: r#"x := "foo\nbar""#, tree: (justfile (assignment x "foo\nbar")), } test! { name: string_escape_suppress_newline, text: r#" x := "foo\ bar" "#, tree: (justfile (assignment x "foobar")), } test! { name: string_escape_carriage_return, text: r#"x := "foo\rbar""#, tree: (justfile (assignment x "foo\rbar")), } test! { name: string_escape_slash, text: r#"x := "foo\\bar""#, tree: (justfile (assignment x "foo\\bar")), } test! { name: string_escape_quote, text: r#"x := "foo\"bar""#, tree: (justfile (assignment x "foo\"bar")), } test! { name: indented_string_raw_with_dedent, text: " x := ''' foo\\t bar\\n ''' ", tree: (justfile (assignment x "foo\\t\nbar\\n\n")), } test! { name: indented_string_raw_no_dedent, text: " x := ''' foo\\t bar\\n ''' ", tree: (justfile (assignment x "foo\\t\n bar\\n\n")), } test! { name: indented_string_cooked, text: r#" x := """ \tfoo\t \tbar\n """ "#, tree: (justfile (assignment x "\tfoo\t\n\tbar\n\n")), } test! { name: indented_string_cooked_no_dedent, text: r#" x := """ \tfoo\t \tbar\n """ "#, tree: (justfile (assignment x "\tfoo\t\n \tbar\n\n")), } test! { name: indented_backtick, text: r" x := ``` \tfoo\t \tbar\n ``` ", tree: (justfile (assignment x (backtick "\\tfoo\\t\n\\tbar\\n\n"))), } test! { name: indented_backtick_no_dedent, text: r" x := ``` \tfoo\t \tbar\n ``` ", tree: (justfile (assignment x (backtick "\\tfoo\\t\n \\tbar\\n\n"))), } test! { name: recipe_variadic_with_default_after_default, text: r" f a=b +c=d: ", tree: (justfile (recipe f (params (a b) +(c d)))), } test! { name: parameter_default_concatenation_variable, text: r#" x := "10" f y=(`echo hello` + x) +z="foo": "#, tree: (justfile (assignment x "10") (recipe f (params (y ((+ (backtick "echo hello") x))) +(z "foo"))) ), } test! { name: parameter_default_multiple, text: r#" x := "10" f y=(`echo hello` + x) +z=("foo" + "bar"): "#, tree: (justfile (assignment x "10") (recipe f (params (y ((+ (backtick "echo hello") x))) +(z ((+ "foo" "bar"))))) ), } test! { name: parse_raw_string_default, text: r" foo a='b\t': ", tree: (justfile (recipe foo (params (a "b\\t")))), } test! { name: parse_alias_after_target, text: r" foo: echo a alias f := foo ", tree: (justfile (recipe foo (body ("echo a"))) (alias f foo) ), } test! { name: parse_alias_before_target, text: " alias f := foo foo: echo a ", tree: (justfile (alias f foo) (recipe foo (body ("echo a"))) ), } test! { name: parse_alias_with_comment, text: " alias f := foo #comment foo: echo a ", tree: (justfile (alias f foo) (recipe foo (body ("echo a"))) ), } test! { name: parse_assignment_with_comment, text: " f := foo #comment foo: echo a ", tree: (justfile (assignment f foo) (recipe foo (body ("echo a"))) ), } test! { name: parse_complex, text: " x: y: z: foo := \"xx\" bar := foo goodbye := \"y\" hello a b c : x y z #hello #! blah #blarg {{ foo + bar}}abc{{ goodbye\t + \"x\" }}xyz 1 2 3 ", tree: (justfile (recipe x) (recipe y) (recipe z) (assignment foo "xx") (assignment bar foo) (assignment goodbye "y") (recipe hello (params (a) (b) (c)) (deps x y z) (body ("#! blah") ("#blarg") (((+ foo bar)) "abc" ((+ goodbye "x")) "xyz") ("1") ("2") ("3") ) ) ), } test! { name: parse_shebang, text: " practicum := 'hello' install: \t#!/bin/sh \tif [[ -f {{practicum}} ]]; then \t\treturn \tfi ", tree: (justfile (assignment practicum "hello") (recipe install (body ("#!/bin/sh") ("if [[ -f " (practicum) " ]]; then") ("\treturn") ("fi") ) ) ), } test! { name: parse_simple_shebang, text: "a:\n #!\n print(1)", tree: (justfile (recipe a (body ("#!") (" print(1)"))) ), } test! { name: parse_assignments, text: r#" a := "0" c := a + b + a + b b := "1" "#, tree: (justfile (assignment a "0") (assignment c (+ a (+ b (+ a b)))) (assignment b "1") ), } test! { name: parse_assignment_backticks, text: " a := `echo hello` c := a + b + a + b b := `echo goodbye` ", tree: (justfile (assignment a (backtick "echo hello")) (assignment c (+ a (+ b (+ a b)))) (assignment b (backtick "echo goodbye")) ), } test! { name: parse_interpolation_backticks, text: r#" a: echo {{ `echo hello` + "blarg" }} {{ `echo bob` }} "#, tree: (justfile (recipe a (body ("echo " ((+ (backtick "echo hello") "blarg")) " " ((backtick "echo bob")))) ) ), } test! { name: eof_test, text: "x:\ny:\nz:\na b c: x y z", tree: (justfile (recipe x) (recipe y) (recipe z) (recipe a (params (b) (c)) (deps x y z)) ), } test! { name: string_quote_escape, text: r#"a := "hello\"""#, tree: (justfile (assignment a "hello\"") ), } test! { name: string_escapes, text: r#"a := "\n\t\r\"\\""#, tree: (justfile (assignment a "\n\t\r\"\\")), } test! { name: parameters, text: " a b c: {{b}} {{c}} ", tree: (justfile (recipe a (params (b) (c)) (body ((b) " " (c))))), } test! { name: unary_functions, text: " x := arch() a: {{os()}} {{os_family()}} ", tree: (justfile (assignment x (call arch)) (recipe a (body (((call os)) " " ((call os_family))))) ), } test! { name: env_functions, text: r#" x := env_var('foo',) a: {{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}} "#, tree: (justfile (assignment x (call env_var "foo")) (recipe a (body ( ((call env_var_or_default (+ "foo" "bar") "baz")) " " ((call env_var (call env_var "baz"))) ) ) ) ), } test! { name: parameter_default_string, text: r#" f x="abc": "#, tree: (justfile (recipe f (params (x "abc")))), } test! { name: parameter_default_raw_string, text: r" f x='abc': ", tree: (justfile (recipe f (params (x "abc")))), } test! { name: parameter_default_backtick, text: " f x=`echo hello`: ", tree: (justfile (recipe f (params (x (backtick "echo hello")))) ), } test! { name: parameter_default_concatenation_string, text: r#" f x=(`echo hello` + "foo"): "#, tree: (justfile (recipe f (params (x ((+ (backtick "echo hello") "foo")))))), } test! { name: concatenation_in_group, text: "x := ('0' + '1')", tree: (justfile (assignment x ((+ "0" "1")))), } test! { name: string_in_group, text: "x := ('0' )", tree: (justfile (assignment x ("0"))), } test! { name: escaped_dos_newlines, text: " @spam:\r \t{ \\\r \t\tfiglet test; \\\r \t\tcargo build --color always 2>&1; \\\r \t\tcargo test --color always -- --color always 2>&1; \\\r \t} | less\r ", tree: (justfile (recipe #quiet spam (body ("{ \\") ("\tfiglet test; \\") ("\tcargo build --color always 2>&1; \\") ("\tcargo test --color always -- --color always 2>&1; \\") ("} | less") ) ) ), } test! { name: empty_body, text: "a:", tree: (justfile (recipe a)), } test! { name: single_line_body, text: "a:\n foo", tree: (justfile (recipe a (body ("foo")))), } test! { name: trimmed_body, text: "a:\n foo\n \n \n \nb:\n ", tree: (justfile (recipe a (body ("foo"))) (recipe b)), } test! { name: set_export_implicit, text: "set export", tree: (justfile (set export true)), } test! { name: set_export_true, text: "set export := true", tree: (justfile (set export true)), } test! { name: set_export_false, text: "set export := false", tree: (justfile (set export false)), } test! { name: set_dotenv_load_implicit, text: "set dotenv-load", tree: (justfile (set dotenv_load true)), } test! { name: set_allow_duplicate_recipes_implicit, text: "set allow-duplicate-recipes", tree: (justfile (set allow_duplicate_recipes true)), } test! { name: set_dotenv_load_true, text: "set dotenv-load := true", tree: (justfile (set dotenv_load true)), } test! { name: set_dotenv_load_false, text: "set dotenv-load := false", tree: (justfile (set dotenv_load false)), } test! { name: set_positional_arguments_implicit, text: "set positional-arguments", tree: (justfile (set positional_arguments true)), } test! { name: set_positional_arguments_true, text: "set positional-arguments := true", tree: (justfile (set positional_arguments true)), } test! { name: set_positional_arguments_false, text: "set positional-arguments := false", tree: (justfile (set positional_arguments false)), } test! { name: set_shell_no_arguments, text: "set shell := ['tclsh']", tree: (justfile (set shell "tclsh")), } test! { name: set_shell_no_arguments_cooked, text: "set shell := [\"tclsh\"]", tree: (justfile (set shell "tclsh")), } test! { name: set_shell_no_arguments_trailing_comma, text: "set shell := ['tclsh',]", tree: (justfile (set shell "tclsh")), } test! { name: set_shell_with_one_argument, text: "set shell := ['bash', '-cu']", tree: (justfile (set shell "bash" "-cu")), } test! { name: set_shell_with_one_argument_trailing_comma, text: "set shell := ['bash', '-cu',]", tree: (justfile (set shell "bash" "-cu")), } test! { name: set_shell_with_two_arguments, text: "set shell := ['bash', '-cu', '-l']", tree: (justfile (set shell "bash" "-cu" "-l")), } test! { name: set_windows_powershell_implicit, text: "set windows-powershell", tree: (justfile (set windows_powershell true)), } test! { name: set_windows_powershell_true, text: "set windows-powershell := true", tree: (justfile (set windows_powershell true)), } test! { name: set_windows_powershell_false, text: "set windows-powershell := false", tree: (justfile (set windows_powershell false)), } test! { name: conditional, text: "a := if b == c { d } else { e }", tree: (justfile (assignment a (if b == c d e))), } test! { name: conditional_inverted, text: "a := if b != c { d } else { e }", tree: (justfile (assignment a (if b != c d e))), } test! { name: conditional_concatenations, text: "a := if b0 + b1 == c0 + c1 { d0 + d1 } else { e0 + e1 }", tree: (justfile (assignment a (if (+ b0 b1) == (+ c0 c1) (+ d0 d1) (+ e0 e1)))), } test! { name: conditional_nested_lhs, text: "a := if if b == c { d } else { e } == c { d } else { e }", tree: (justfile (assignment a (if (if b == c d e) == c d e))), } test! { name: conditional_nested_rhs, text: "a := if c == if b == c { d } else { e } { d } else { e }", tree: (justfile (assignment a (if c == (if b == c d e) d e))), } test! { name: conditional_nested_then, text: "a := if b == c { if b == c { d } else { e } } else { e }", tree: (justfile (assignment a (if b == c (if b == c d e) e))), } test! { name: conditional_nested_otherwise, text: "a := if b == c { d } else { if b == c { d } else { e } }", tree: (justfile (assignment a (if b == c d (if b == c d e)))), } test! { name: import, text: "import \"some/file/path.txt\" \n", tree: (justfile (import "some/file/path.txt")), } test! { name: optional_import, text: "import? \"some/file/path.txt\" \n", tree: (justfile (import ? "some/file/path.txt")), } test! { name: module_with, text: "mod foo", tree: (justfile (mod foo )), } test! { name: optional_module, text: "mod? foo", tree: (justfile (mod ? foo)), } test! { name: module_with_path, text: "mod foo \"some/file/path.txt\" \n", tree: (justfile (mod foo "some/file/path.txt")), } test! { name: optional_module_with_path, text: "mod? foo \"some/file/path.txt\" \n", tree: (justfile (mod ? foo "some/file/path.txt")), } error! { name: alias_syntax_multiple_rhs, input: "alias foo := bar baz", offset: 17, line: 0, column: 17, width: 3, kind: UnexpectedToken { expected: vec![Comment, Eof, Eol], found: Identifier }, } error! { name: alias_syntax_no_rhs, input: "alias foo := \n", offset: 13, line: 0, column: 13, width: 1, kind: UnexpectedToken {expected: vec![Identifier], found:Eol}, } error! { name: missing_colon, input: "a b c\nd e f", offset: 5, line: 0, column: 5, width: 1, kind: UnexpectedToken{ expected: vec![Asterisk, Colon, Dollar, Equals, Identifier, Plus], found: Eol }, } error! { name: missing_default_eol, input: "hello arg=\n", offset: 10, line: 0, column: 10, width: 1, kind: UnexpectedToken { expected: vec![ Backtick, Identifier, ParenL, StringToken, ], found: Eol }, } error! { name: missing_default_eof, input: "hello arg=", offset: 10, line: 0, column: 10, width: 0, kind: UnexpectedToken { expected: vec![ Backtick, Identifier, ParenL, StringToken, ], found: Eof, }, } error! { name: missing_eol, input: "a b c: z =", offset: 9, line: 0, column: 9, width: 1, kind: UnexpectedToken{ expected: vec![AmpersandAmpersand, Comment, Eof, Eol, Identifier, ParenL], found: Equals }, } error! { name: unexpected_brace, input: "{{", offset: 0, line: 0, column: 0, width: 1, kind: UnexpectedToken { expected: vec![At, BracketL, Comment, Eof, Eol, Identifier], found: BraceL, }, } error! { name: unclosed_parenthesis_in_expression, input: "x := foo(", offset: 9, line: 0, column: 9, width: 0, kind: UnexpectedToken{ expected: vec![ Backtick, Identifier, ParenL, ParenR, Slash, StringToken, ], found: Eof, }, } error! { name: unclosed_parenthesis_in_interpolation, input: "a:\n echo {{foo(}}", offset: 15, line: 1, column: 12, width: 2, kind: UnexpectedToken{ expected: vec![ Backtick, Identifier, ParenL, ParenR, Slash, StringToken, ], found: InterpolationEnd, }, } error! { name: plus_following_parameter, input: "a b c+:", offset: 6, line: 0, column: 6, width: 1, kind: UnexpectedToken{expected: vec![Dollar, Identifier], found: Colon}, } error! { name: invalid_escape_sequence, input: r#"foo := "\b""#, offset: 7, line: 0, column: 7, width: 4, kind: InvalidEscapeSequence{character: 'b'}, } error! { name: bad_export, input: "export a", offset: 8, line: 0, column: 8, width: 0, kind: UnexpectedToken { expected: vec![Asterisk, Colon, Dollar, Equals, Identifier, Plus], found: Eof }, } error! { name: parameter_follows_variadic_parameter, input: "foo +a b:", offset: 7, line: 0, column: 7, width: 1, kind: ParameterFollowsVariadicParameter{parameter: "b"}, } error! { name: parameter_after_variadic, input: "foo +a bbb:", offset: 7, line: 0, column: 7, width: 3, kind: ParameterFollowsVariadicParameter{parameter: "bbb"}, } error! { name: concatenation_in_default, input: "foo a=c+d e:", offset: 10, line: 0, column: 10, width: 1, kind: ParameterFollowsVariadicParameter{parameter: "e"}, } error! { name: set_shell_empty, input: "set shell := []", offset: 14, line: 0, column: 14, width: 1, kind: UnexpectedToken { expected: vec![ StringToken, ], found: BracketR, }, } error! { name: set_shell_non_literal_first, input: "set shell := ['bar' + 'baz']", offset: 20, line: 0, column: 20, width: 1, kind: UnexpectedToken { expected: vec![BracketR, Comma], found: Plus, }, } error! { name: set_shell_non_literal_second, input: "set shell := ['biz', 'bar' + 'baz']", offset: 27, line: 0, column: 27, width: 1, kind: UnexpectedToken { expected: vec![BracketR, Comma], found: Plus, }, } error! { name: set_shell_bad_comma, input: "set shell := ['bash',", offset: 21, line: 0, column: 21, width: 0, kind: UnexpectedToken { expected: vec![ BracketR, StringToken, ], found: Eof, }, } error! { name: set_shell_bad, input: "set shell := ['bash'", offset: 20, line: 0, column: 20, width: 0, kind: UnexpectedToken { expected: vec![BracketR, Comma], found: Eof, }, } error! { name: empty_attribute, input: "[]\nsome_recipe:\n @exit 3", offset: 1, line: 0, column: 1, width: 1, kind: UnexpectedToken { expected: vec![Identifier], found: BracketR, }, } error! { name: unknown_attribute, input: "[unknown]\nsome_recipe:\n @exit 3", offset: 1, line: 0, column: 1, width: 7, kind: UnknownAttribute { attribute: "unknown" }, } error! { name: set_unknown, input: "set shall := []", offset: 4, line: 0, column: 4, width: 5, kind: UnknownSetting { setting: "shall", }, } error! { name: set_shell_non_string, input: "set shall := []", offset: 4, line: 0, column: 4, width: 5, kind: UnknownSetting { setting: "shall", }, } error! { name: unknown_function, input: "a := foo()", offset: 5, line: 0, column: 5, width: 3, kind: UnknownFunction{function: "foo"}, } error! { name: unknown_function_in_interpolation, input: "a:\n echo {{bar()}}", offset: 11, line: 1, column: 8, width: 3, kind: UnknownFunction{function: "bar"}, } error! { name: unknown_function_in_default, input: "a f=baz():", offset: 4, line: 0, column: 4, width: 3, kind: UnknownFunction{function: "baz"}, } error! { name: function_argument_count_nullary, input: "x := arch('foo')", offset: 5, line: 0, column: 5, width: 4, kind: FunctionArgumentCountMismatch { function: "arch", found: 1, expected: 0..0, }, } error! { name: function_argument_count_unary, input: "x := env_var()", offset: 5, line: 0, column: 5, width: 7, kind: FunctionArgumentCountMismatch { function: "env_var", found: 0, expected: 1..1, }, } error! { name: function_argument_count_too_high_unary_opt, input: "x := env('foo', 'foo', 'foo')", offset: 5, line: 0, column: 5, width: 3, kind: FunctionArgumentCountMismatch { function: "env", found: 3, expected: 1..2, }, } error! { name: function_argument_count_too_low_unary_opt, input: "x := env()", offset: 5, line: 0, column: 5, width: 3, kind: FunctionArgumentCountMismatch { function: "env", found: 0, expected: 1..2, }, } error! { name: function_argument_count_binary, input: "x := env_var_or_default('foo')", offset: 5, line: 0, column: 5, width: 18, kind: FunctionArgumentCountMismatch { function: "env_var_or_default", found: 1, expected: 2..2, }, } error! { name: function_argument_count_binary_plus, input: "x := join('foo')", offset: 5, line: 0, column: 5, width: 4, kind: FunctionArgumentCountMismatch { function: "join", found: 1, expected: 2..usize::MAX, }, } error! { name: function_argument_count_ternary, input: "x := replace('foo')", offset: 5, line: 0, column: 5, width: 7, kind: FunctionArgumentCountMismatch { function: "replace", found: 1, expected: 3..3, }, } } just-1.21.0/src/platform.rs000064400000000000000000000062661046102023000136650ustar 00000000000000use super::*; pub(crate) struct Platform; #[cfg(unix)] impl PlatformInterface for Platform { fn make_shebang_command( path: &Path, working_directory: Option<&Path>, _shebang: Shebang, ) -> Result { // shebang scripts can be executed directly on unix let mut cmd = Command::new(path); if let Some(working_directory) = working_directory { cmd.current_dir(working_directory); } Ok(cmd) } fn set_execute_permission(path: &Path) -> Result<(), io::Error> { use std::os::unix::fs::PermissionsExt; // get current permissions let mut permissions = fs::metadata(path)?.permissions(); // set the execute bit let current_mode = permissions.mode(); permissions.set_mode(current_mode | 0o100); // set the new permissions fs::set_permissions(path, permissions) } fn signal_from_exit_status(exit_status: ExitStatus) -> Option { use std::os::unix::process::ExitStatusExt; exit_status.signal() } fn convert_native_path(_working_directory: &Path, path: &Path) -> Result { path .to_str() .map(str::to_string) .ok_or_else(|| String::from("Error getting current directory: unicode decode error")) } } #[cfg(windows)] impl PlatformInterface for Platform { fn make_shebang_command( path: &Path, working_directory: Option<&Path>, shebang: Shebang, ) -> Result { use std::borrow::Cow; // If the path contains forward slashes… let command = if shebang.interpreter.contains('/') { // …translate path to the interpreter from unix style to windows style. let mut cygpath = Command::new("cygpath"); if let Some(working_directory) = working_directory { cygpath.current_dir(working_directory); } cygpath.arg("--windows"); cygpath.arg(shebang.interpreter); Cow::Owned(output(cygpath)?) } else { // …otherwise use it as-is. Cow::Borrowed(shebang.interpreter) }; let mut cmd = Command::new(command.as_ref()); if let Some(working_directory) = working_directory { cmd.current_dir(working_directory); } if let Some(argument) = shebang.argument { cmd.arg(argument); } cmd.arg(path); Ok(cmd) } fn set_execute_permission(_path: &Path) -> Result<(), io::Error> { // it is not necessary to set an execute permission on a script on windows, so // this is a nop Ok(()) } fn signal_from_exit_status(_exit_status: process::ExitStatus) -> Option { // The rust standard library does not expose a way to extract a signal from a // windows process exit status, so just return None None } fn convert_native_path(working_directory: &Path, path: &Path) -> Result { // Translate path from windows style to unix style let mut cygpath = Command::new("cygpath"); cygpath.current_dir(working_directory); cygpath.arg("--unix"); cygpath.arg(path); match output(cygpath) { Ok(shell_path) => Ok(shell_path), Err(_) => path .to_str() .map(str::to_string) .ok_or_else(|| String::from("Error getting current directory: unicode decode error")), } } } just-1.21.0/src/platform_interface.rs000064400000000000000000000014011046102023000156670ustar 00000000000000use super::*; pub(crate) trait PlatformInterface { /// Construct a command equivalent to running the script at `path` with the /// shebang line `shebang` fn make_shebang_command( path: &Path, working_directory: Option<&Path>, shebang: Shebang, ) -> Result; /// Set the execute permission on the file pointed to by `path` fn set_execute_permission(path: &Path) -> Result<(), io::Error>; /// Extract the signal from a process exit status, if it was terminated by a /// signal fn signal_from_exit_status(exit_status: ExitStatus) -> Option; /// Translate a path from a "native" path to a path the interpreter expects fn convert_native_path(working_directory: &Path, path: &Path) -> Result; } just-1.21.0/src/position.rs000064400000000000000000000002561046102023000136760ustar 00000000000000/// Source position #[derive(Copy, Clone, PartialEq, Debug)] pub(crate) struct Position { pub(crate) offset: usize, pub(crate) column: usize, pub(crate) line: usize, } just-1.21.0/src/positional.rs000064400000000000000000000137741046102023000142240ustar 00000000000000use super::*; /// A struct containing the parsed representation of positional command-line /// arguments, i.e. arguments that are not flags, options, or the subcommand. /// /// The DSL of positional arguments is fairly complex and mostly accidental. /// There are three possible components: overrides, a search directory, and the /// rest: /// /// - Overrides are of the form `NAME=.*` /// /// - After overrides comes a single optional search directory argument. This is /// either '.', '..', or an argument that contains a `/`. /// /// If the argument contains a `/`, everything before and including the slash /// is the search directory, and everything after is added to the rest. /// /// - Everything else is an argument. /// /// Overrides set the values of top-level variables in the justfile being /// invoked and are a convenient way to override settings. /// /// For modes that do not take other arguments, the search directory argument /// determines where to begin searching for the justfile. This allows command /// lines like `just -l ..` and `just ../build` to find the same justfile. /// /// For modes that do take other arguments, the search argument is simply /// prepended to rest. #[cfg_attr(test, derive(PartialEq, Eq, Debug))] pub struct Positional { /// Overrides from values of the form `[a-zA-Z_][a-zA-Z0-9_-]*=.*` pub overrides: Vec<(String, String)>, /// An argument equal to '.', '..', or ending with `/` pub search_directory: Option, /// Everything else pub arguments: Vec, } impl Positional { pub fn from_values<'values>(values: Option>) -> Self { let mut overrides = Vec::new(); let mut search_directory = None; let mut arguments = Vec::new(); if let Some(values) = values { for value in values { if search_directory.is_none() && arguments.is_empty() { if let Some(o) = Self::override_from_value(value) { overrides.push(o); } else if value == "." || value == ".." { search_directory = Some(value.to_owned()); } else if let Some(i) = value.rfind('/') { let (dir, tail) = value.split_at(i + 1); search_directory = Some(dir.to_owned()); if !tail.is_empty() { arguments.push(tail.to_owned()); } } else { arguments.push(value.to_owned()); } } else { arguments.push(value.to_owned()); } } } Self { overrides, search_directory, arguments, } } /// Parse an override from a value of the form `NAME=.*`. fn override_from_value(value: &str) -> Option<(String, String)> { let equals = value.find('=')?; let (identifier, equals_value) = value.split_at(equals); // exclude `=` from value let value = &equals_value[1..]; if Lexer::is_identifier(identifier) { Some((identifier.to_owned(), value.to_owned())) } else { None } } } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; macro_rules! test { { name: $name:ident, values: $vals:expr, overrides: $overrides:expr, search_directory: $search_directory:expr, arguments: $arguments:expr, } => { #[test] fn $name() { assert_eq! ( Positional::from_values(Some($vals.iter().cloned())), Positional { overrides: $overrides .iter() .cloned() .map(|(key, value): (&str, &str)| (key.to_owned(), value.to_owned())) .collect(), search_directory: $search_directory.map(str::to_owned), arguments: $arguments.iter().cloned().map(str::to_owned).collect(), }, ) } } } test! { name: no_values, values: [], overrides: [], search_directory: None, arguments: [], } test! { name: arguments_only, values: ["foo", "bar"], overrides: [], search_directory: None, arguments: ["foo", "bar"], } test! { name: all_overrides, values: ["foo=bar", "bar=foo"], overrides: [("foo", "bar"), ("bar", "foo")], search_directory: None, arguments: [], } test! { name: override_not_name, values: ["foo=bar", "bar.=foo"], overrides: [("foo", "bar")], search_directory: None, arguments: ["bar.=foo"], } test! { name: no_overrides, values: ["the-dir/", "baz", "bzzd"], overrides: [], search_directory: Some("the-dir/"), arguments: ["baz", "bzzd"], } test! { name: no_search_directory, values: ["foo=bar", "bar=foo", "baz", "bzzd"], overrides: [("foo", "bar"), ("bar", "foo")], search_directory: None, arguments: ["baz", "bzzd"], } test! { name: no_arguments, values: ["foo=bar", "bar=foo", "the-dir/"], overrides: [("foo", "bar"), ("bar", "foo")], search_directory: Some("the-dir/"), arguments: [], } test! { name: all_dot, values: ["foo=bar", "bar=foo", ".", "garnor"], overrides: [("foo", "bar"), ("bar", "foo")], search_directory: Some("."), arguments: ["garnor"], } test! { name: all_dot_dot, values: ["foo=bar", "bar=foo", "..", "garnor"], overrides: [("foo", "bar"), ("bar", "foo")], search_directory: Some(".."), arguments: ["garnor"], } test! { name: all_slash, values: ["foo=bar", "bar=foo", "/", "garnor"], overrides: [("foo", "bar"), ("bar", "foo")], search_directory: Some("/"), arguments: ["garnor"], } test! { name: search_directory_after_argument, values: ["foo=bar", "bar=foo", "baz", "bzzd", "bar/"], overrides: [("foo", "bar"), ("bar", "foo")], search_directory: None, arguments: ["baz", "bzzd", "bar/"], } test! { name: override_after_search_directory, values: ["..", "a=b"], overrides: [], search_directory: Some(".."), arguments: ["a=b"], } test! { name: override_after_argument, values: ["a", "a=b"], overrides: [], search_directory: None, arguments: ["a", "a=b"], } } just-1.21.0/src/range_ext.rs000064400000000000000000000037511046102023000140110ustar 00000000000000use super::*; pub(crate) trait RangeExt { fn range_contains(&self, i: &T) -> bool; fn display(&self) -> DisplayRange<&Self> { DisplayRange(self) } } pub(crate) struct DisplayRange(T); impl Display for DisplayRange<&Range> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { if self.0.start == self.0.end { write!(f, "{}", self.0.start)?; } else if self.0.end == usize::MAX { write!(f, "{} or more", self.0.start)?; } else { write!(f, "{} to {}", self.0.start, self.0.end)?; } Ok(()) } } impl RangeExt for Range where T: PartialOrd, { fn range_contains(&self, i: &T) -> bool { i >= &self.start && i < &self.end } } impl RangeExt for RangeInclusive where T: PartialOrd, { fn range_contains(&self, i: &T) -> bool { i >= self.start() && i <= self.end() } } #[cfg(test)] mod tests { use super::*; #[test] fn exclusive() { assert!(!(0..0).range_contains(&0)); assert!(!(0..0).range_contains(&0)); assert!(!(1..10).range_contains(&0)); assert!(!(1..10).range_contains(&10)); assert!(!(1..10).range_contains(&0)); assert!(!(1..10).range_contains(&10)); assert!((0..1).range_contains(&0)); assert!((0..1).range_contains(&0)); assert!((10..20).range_contains(&15)); assert!((10..20).range_contains(&15)); } #[test] fn inclusive() { assert!(!(0..=10).range_contains(&11)); assert!(!(1..=10).range_contains(&0)); assert!(!(5..=10).range_contains(&4)); assert!((0..=0).range_contains(&0)); assert!((0..=1).range_contains(&0)); assert!((0..=10).range_contains(&0)); assert!((0..=10).range_contains(&10)); assert!((0..=10).range_contains(&7)); assert!((1..=10).range_contains(&10)); assert!((10..=20).range_contains(&15)); } #[test] fn display() { assert_eq!((1..1).display().to_string(), "1"); assert_eq!((1..2).display().to_string(), "1 to 2"); assert_eq!((1..usize::MAX).display().to_string(), "1 or more"); } } just-1.21.0/src/recipe.rs000064400000000000000000000304061046102023000133010ustar 00000000000000use { super::*, std::process::{ExitStatus, Stdio}, }; /// Return a `Error::Signal` if the process was terminated by a signal, /// otherwise return an `Error::UnknownFailure` fn error_from_signal(recipe: &str, line_number: Option, exit_status: ExitStatus) -> Error { match Platform::signal_from_exit_status(exit_status) { Some(signal) => Error::Signal { recipe, line_number, signal, }, None => Error::Unknown { recipe, line_number, }, } } /// A recipe, e.g. `foo: bar baz` #[derive(PartialEq, Debug, Clone, Serialize)] pub(crate) struct Recipe<'src, D = Dependency<'src>> { pub(crate) attributes: BTreeSet, pub(crate) body: Vec>, pub(crate) dependencies: Vec, pub(crate) doc: Option<&'src str>, pub(crate) name: Name<'src>, pub(crate) parameters: Vec>, #[serde(skip)] pub(crate) path: PathBuf, pub(crate) priors: usize, pub(crate) private: bool, pub(crate) quiet: bool, pub(crate) shebang: bool, #[serde(skip)] pub(crate) depth: u32, } impl<'src, D> Recipe<'src, D> { pub(crate) fn argument_range(&self) -> RangeInclusive { self.min_arguments()..=self.max_arguments() } pub(crate) fn min_arguments(&self) -> usize { self .parameters .iter() .filter(|p| p.default.is_none() && p.kind != ParameterKind::Star) .count() } pub(crate) fn max_arguments(&self) -> usize { if self.parameters.iter().any(|p| p.kind.is_variadic()) { usize::MAX - 1 } else { self.parameters.len() } } pub(crate) fn name(&self) -> &'src str { self.name.lexeme() } pub(crate) fn line_number(&self) -> usize { self.name.line } pub(crate) fn confirm(&self) -> RunResult<'src, bool> { if self.attributes.contains(&Attribute::Confirm) { eprint!("Run recipe `{}`? ", self.name); let mut line = String::new(); std::io::stdin() .read_line(&mut line) .map_err(|io_error| Error::GetConfirmation { io_error })?; let line = line.trim().to_lowercase(); Ok(line == "y" || line == "yes") } else { Ok(true) } } pub(crate) fn check_can_be_default_recipe(&self) -> RunResult<'src, ()> { let min_arguments = self.min_arguments(); if min_arguments > 0 { return Err(Error::DefaultRecipeRequiresArguments { recipe: self.name.lexeme(), min_arguments, }); } Ok(()) } pub(crate) fn public(&self) -> bool { !self.private && !self.attributes.contains(&Attribute::Private) } pub(crate) fn change_directory(&self) -> bool { !self.attributes.contains(&Attribute::NoCd) } pub(crate) fn enabled(&self) -> bool { let windows = self.attributes.contains(&Attribute::Windows); let linux = self.attributes.contains(&Attribute::Linux); let macos = self.attributes.contains(&Attribute::Macos); let unix = self.attributes.contains(&Attribute::Unix); (!windows && !linux && !macos && !unix) || (cfg!(target_os = "windows") && windows) || (cfg!(target_os = "linux") && (linux || unix)) || (cfg!(target_os = "macos") && (macos || unix)) || (cfg!(windows) && windows) || (cfg!(unix) && unix) } fn print_exit_message(&self) -> bool { !self.attributes.contains(&Attribute::NoExitMessage) } pub(crate) fn run<'run>( &self, context: &RecipeContext<'src, 'run>, dotenv: &BTreeMap, scope: Scope<'src, 'run>, search: &'run Search, positional: &[String], ) -> RunResult<'src, ()> { let config = &context.config; if config.verbosity.loquacious() { let color = config.color.stderr().banner(); eprintln!( "{}===> Running recipe `{}`...{}", color.prefix(), self.name, color.suffix() ); } let evaluator = Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search); if self.shebang { self.run_shebang(context, dotenv, &scope, positional, config, evaluator) } else { self.run_linewise(context, dotenv, &scope, positional, config, evaluator) } } fn run_linewise<'run>( &self, context: &RecipeContext<'src, 'run>, dotenv: &BTreeMap, scope: &Scope<'src, 'run>, positional: &[String], config: &Config, mut evaluator: Evaluator<'src, 'run>, ) -> RunResult<'src, ()> { let mut lines = self.body.iter().peekable(); let mut line_number = self.line_number() + 1; loop { if lines.peek().is_none() { return Ok(()); } let mut evaluated = String::new(); let mut continued = false; let quiet_command = lines.peek().map_or(false, |line| line.is_quiet()); let infallible_command = lines.peek().map_or(false, |line| line.is_infallible()); let comment_line = context.settings.ignore_comments && lines.peek().map_or(false, |line| line.is_comment()); loop { if lines.peek().is_none() { break; } let line = lines.next().unwrap(); line_number += 1; if !comment_line { evaluated += &evaluator.evaluate_line(line, continued)?; } if line.is_continuation() && !comment_line { continued = true; evaluated.pop(); } else { break; } } if comment_line { continue; } let mut command = evaluated.as_str(); if quiet_command { command = &command[1..]; } if infallible_command { command = &command[1..]; } if command.is_empty() { continue; } if config.dry_run || config.verbosity.loquacious() || !((quiet_command ^ self.quiet) || config.verbosity.quiet()) { let color = if config.highlight { config.color.command(config.command_color) } else { config.color }; eprintln!("{}", color.stderr().paint(command)); } if config.dry_run { continue; } let mut cmd = context.settings.shell_command(config); if self.change_directory() { cmd.current_dir(if self.depth > 0 { self.path.parent().unwrap() } else { &context.search.working_directory }); } cmd.arg(command); if context.settings.positional_arguments { cmd.arg(self.name.lexeme()); cmd.args(positional); } if config.verbosity.quiet() { cmd.stderr(Stdio::null()); cmd.stdout(Stdio::null()); } cmd.export(context.settings, dotenv, scope); match InterruptHandler::guard(|| cmd.status()) { Ok(exit_status) => { if let Some(code) = exit_status.code() { if code != 0 && !infallible_command { return Err(Error::Code { recipe: self.name(), line_number: Some(line_number), code, print_message: self.print_exit_message(), }); } } else { return Err(error_from_signal( self.name(), Some(line_number), exit_status, )); } } Err(io_error) => { return Err(Error::Io { recipe: self.name(), io_error, }); } }; } } pub(crate) fn run_shebang<'run>( &self, context: &RecipeContext<'src, 'run>, dotenv: &BTreeMap, scope: &Scope<'src, 'run>, positional: &[String], config: &Config, mut evaluator: Evaluator<'src, 'run>, ) -> RunResult<'src, ()> { let mut evaluated_lines = vec![]; for line in &self.body { evaluated_lines.push(evaluator.evaluate_line(line, false)?); } if config.verbosity.loud() && (config.dry_run || self.quiet) { for line in &evaluated_lines { eprintln!("{line}"); } } if config.dry_run { return Ok(()); } let shebang_line = evaluated_lines.first().ok_or_else(|| Error::Internal { message: "evaluated_lines was empty".to_owned(), })?; let shebang = Shebang::new(shebang_line).ok_or_else(|| Error::Internal { message: format!("bad shebang line: {shebang_line}"), })?; let mut tempdir_builder = tempfile::Builder::new(); tempdir_builder.prefix("just"); let tempdir = match &context.settings.tempdir { Some(tempdir) => tempdir_builder.tempdir_in(context.search.working_directory.join(tempdir)), None => tempdir_builder.tempdir(), } .map_err(|error| Error::TmpdirIo { recipe: self.name(), io_error: error, })?; let mut path = tempdir.path().to_path_buf(); path.push(shebang.script_filename(self.name())); { let mut f = fs::File::create(&path).map_err(|error| Error::TmpdirIo { recipe: self.name(), io_error: error, })?; let mut text = String::new(); if shebang.include_shebang_line() { text += &evaluated_lines[0]; } else { text += "\n"; } text += "\n"; // add blank lines so that lines in the generated script have the same line // number as the corresponding lines in the justfile for _ in 1..(self.line_number() + 2) { text += "\n"; } for line in &evaluated_lines[1..] { text += line; text += "\n"; } if config.verbosity.grandiloquent() { eprintln!("{}", config.color.doc().stderr().paint(&text)); } f.write_all(text.as_bytes()) .map_err(|error| Error::TmpdirIo { recipe: self.name(), io_error: error, })?; } // make the script executable Platform::set_execute_permission(&path).map_err(|error| Error::TmpdirIo { recipe: self.name(), io_error: error, })?; // create a command to run the script let mut command = Platform::make_shebang_command( &path, if self.change_directory() { if self.depth > 0 { Some(self.path.parent().unwrap()) } else { Some(&context.search.working_directory) } } else { None }, shebang, ) .map_err(|output_error| Error::Cygpath { recipe: self.name(), output_error, })?; if context.settings.positional_arguments { command.args(positional); } command.export(context.settings, dotenv, scope); // run it! match InterruptHandler::guard(|| command.status()) { Ok(exit_status) => exit_status.code().map_or_else( || Err(error_from_signal(self.name(), None, exit_status)), |code| { if code == 0 { Ok(()) } else { Err(Error::Code { recipe: self.name(), line_number: None, code, print_message: self.print_exit_message(), }) } }, ), Err(io_error) => Err(Error::Shebang { recipe: self.name(), command: shebang.interpreter.to_owned(), argument: shebang.argument.map(String::from), io_error, }), } } } impl<'src, D: Display> ColorDisplay for Recipe<'src, D> { fn fmt(&self, f: &mut Formatter, color: Color) -> Result<(), fmt::Error> { if let Some(doc) = self.doc { writeln!(f, "# {doc}")?; } for attribute in &self.attributes { writeln!(f, "[{}]", attribute.to_str())?; } if self.quiet { write!(f, "@{}", self.name)?; } else { write!(f, "{}", self.name)?; } for parameter in &self.parameters { write!(f, " {}", parameter.color_display(color))?; } write!(f, ":")?; for (i, dependency) in self.dependencies.iter().enumerate() { if i == self.priors { write!(f, " &&")?; } write!(f, " {dependency}")?; } for (i, line) in self.body.iter().enumerate() { if i == 0 { writeln!(f)?; } for (j, fragment) in line.fragments.iter().enumerate() { if j == 0 { write!(f, " ")?; } match fragment { Fragment::Text { token } => write!(f, "{}", token.lexeme())?, Fragment::Interpolation { expression, .. } => write!(f, "{{{{ {expression} }}}}")?, } } if i + 1 < self.body.len() { writeln!(f)?; } } Ok(()) } } impl<'src, D> Keyed<'src> for Recipe<'src, D> { fn key(&self) -> &'src str { self.name.lexeme() } } just-1.21.0/src/recipe_context.rs000064400000000000000000000003451046102023000150440ustar 00000000000000use super::*; pub(crate) struct RecipeContext<'src: 'run, 'run> { pub(crate) config: &'run Config, pub(crate) scope: &'run Scope<'src, 'run>, pub(crate) search: &'run Search, pub(crate) settings: &'run Settings<'src>, } just-1.21.0/src/recipe_resolver.rs000064400000000000000000000120721046102023000152210ustar 00000000000000use {super::*, CompileErrorKind::*}; pub(crate) struct RecipeResolver<'src: 'run, 'run> { unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>, resolved_recipes: Table<'src, Rc>>, assignments: &'run Table<'src, Assignment<'src>>, } impl<'src: 'run, 'run> RecipeResolver<'src, 'run> { pub(crate) fn resolve_recipes( unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>, assignments: &'run Table<'src, Assignment<'src>>, ) -> CompileResult<'src, Table<'src, Rc>>> { let mut resolver = Self { resolved_recipes: Table::new(), unresolved_recipes, assignments, }; while let Some(unresolved) = resolver.unresolved_recipes.pop() { resolver.resolve_recipe(&mut Vec::new(), unresolved)?; } for recipe in resolver.resolved_recipes.values() { for parameter in &recipe.parameters { if let Some(expression) = ¶meter.default { for variable in expression.variables() { resolver.resolve_variable(&variable, &[])?; } } } for dependency in &recipe.dependencies { for argument in &dependency.arguments { for variable in argument.variables() { resolver.resolve_variable(&variable, &recipe.parameters)?; } } } for line in &recipe.body { for fragment in &line.fragments { if let Fragment::Interpolation { expression, .. } = fragment { for variable in expression.variables() { resolver.resolve_variable(&variable, &recipe.parameters)?; } } } } } Ok(resolver.resolved_recipes) } fn resolve_variable( &self, variable: &Token<'src>, parameters: &[Parameter], ) -> CompileResult<'src> { let name = variable.lexeme(); let undefined = !self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name); if undefined { return Err(variable.error(UndefinedVariable { variable: name })); } Ok(()) } fn resolve_recipe( &mut self, stack: &mut Vec<&'src str>, recipe: UnresolvedRecipe<'src>, ) -> CompileResult<'src, Rc>> { if let Some(resolved) = self.resolved_recipes.get(recipe.name()) { return Ok(Rc::clone(resolved)); } stack.push(recipe.name()); let mut dependencies: Vec> = Vec::new(); for dependency in &recipe.dependencies { let name = dependency.recipe.lexeme(); if let Some(resolved) = self.resolved_recipes.get(name) { // dependency already resolved dependencies.push(Rc::clone(resolved)); } else if stack.contains(&name) { let first = stack[0]; stack.push(first); return Err( dependency.recipe.error(CircularRecipeDependency { recipe: recipe.name(), circle: stack .iter() .skip_while(|name| **name != dependency.recipe.lexeme()) .copied() .collect(), }), ); } else if let Some(unresolved) = self.unresolved_recipes.remove(name) { // resolve unresolved dependency dependencies.push(self.resolve_recipe(stack, unresolved)?); } else { // dependency is unknown return Err(dependency.recipe.error(UnknownDependency { recipe: recipe.name(), unknown: name, })); } } stack.pop(); let resolved = Rc::new(recipe.resolve(dependencies)?); self.resolved_recipes.insert(Rc::clone(&resolved)); Ok(resolved) } } #[cfg(test)] mod tests { use super::*; analysis_error! { name: circular_recipe_dependency, input: "a: b\nb: a", offset: 8, line: 1, column: 3, width: 1, kind: CircularRecipeDependency{recipe: "b", circle: vec!["a", "b", "a"]}, } analysis_error! { name: self_recipe_dependency, input: "a: a", offset: 3, line: 0, column: 3, width: 1, kind: CircularRecipeDependency{recipe: "a", circle: vec!["a", "a"]}, } analysis_error! { name: unknown_dependency, input: "a: b", offset: 3, line: 0, column: 3, width: 1, kind: UnknownDependency{recipe: "a", unknown: "b"}, } analysis_error! { name: unknown_interpolation_variable, input: "x:\n {{ hello}}", offset: 9, line: 1, column: 6, width: 5, kind: UndefinedVariable{variable: "hello"}, } analysis_error! { name: unknown_second_interpolation_variable, input: "wtf:=\"x\"\nx:\n echo\n foo {{wtf}} {{ lol }}", offset: 34, line: 3, column: 16, width: 3, kind: UndefinedVariable{variable: "lol"}, } analysis_error! { name: unknown_variable_in_default, input: "a f=foo:", offset: 4, line: 0, column: 4, width: 3, kind: UndefinedVariable{variable: "foo"}, } analysis_error! { name: unknown_variable_in_dependency_argument, input: "bar x:\nfoo: (bar baz)", offset: 17, line: 1, column: 10, width: 3, kind: UndefinedVariable{variable: "baz"}, } } just-1.21.0/src/run.rs000064400000000000000000000016601046102023000126360ustar 00000000000000use super::*; /// Main entry point into just binary. #[allow(clippy::missing_errors_doc)] pub fn run() -> Result<(), i32> { #[cfg(windows)] ansi_term::enable_ansi_support().ok(); env_logger::Builder::from_env( env_logger::Env::new() .filter("JUST_LOG") .write_style("JUST_LOG_STYLE"), ) .init(); let app = Config::app(); info!("Parsing command line arguments…"); let matches = app.get_matches(); let config = Config::from_matches(&matches).map_err(Error::from); let (color, verbosity) = config .as_ref() .map(|config| (config.color, config.verbosity)) .unwrap_or((Color::auto(), Verbosity::default())); let loader = Loader::new(); config .and_then(|config| config.run(&loader)) .map_err(|error| { if !verbosity.quiet() && error.print_message() { eprintln!("{}", error.color_display(color.stderr())); } error.code().unwrap_or(EXIT_FAILURE) }) } just-1.21.0/src/scope.rs000064400000000000000000000023031046102023000131360ustar 00000000000000use super::*; #[derive(Debug)] pub(crate) struct Scope<'src: 'run, 'run> { parent: Option<&'run Scope<'src, 'run>>, bindings: Table<'src, Binding<'src, String>>, } impl<'src, 'run> Scope<'src, 'run> { pub(crate) fn child(&'run self) -> Scope<'src, 'run> { Self { parent: Some(self), bindings: Table::new(), } } pub(crate) fn new() -> Scope<'src, 'run> { Self { parent: None, bindings: Table::new(), } } pub(crate) fn bind(&mut self, export: bool, name: Name<'src>, value: String) { self.bindings.insert(Binding { export, name, value, }); } pub(crate) fn bound(&self, name: &str) -> bool { self.bindings.contains_key(name) } pub(crate) fn value(&self, name: &str) -> Option<&str> { if let Some(binding) = self.bindings.get(name) { Some(binding.value.as_ref()) } else { self.parent?.value(name) } } pub(crate) fn bindings(&self) -> impl Iterator> { self.bindings.values() } pub(crate) fn names(&self) -> impl Iterator { self.bindings.keys().copied() } pub(crate) fn parent(&self) -> Option<&'run Scope<'src, 'run>> { self.parent } } just-1.21.0/src/search.rs000064400000000000000000000234211046102023000132760ustar 00000000000000use {super::*, std::path::Component}; const DEFAULT_JUSTFILE_NAME: &str = JUSTFILE_NAMES[0]; pub(crate) const JUSTFILE_NAMES: &[&str] = &["justfile", ".justfile"]; const PROJECT_ROOT_CHILDREN: &[&str] = &[".bzr", ".git", ".hg", ".svn", "_darcs"]; pub(crate) struct Search { pub(crate) justfile: PathBuf, pub(crate) working_directory: PathBuf, } impl Search { pub(crate) fn find( search_config: &SearchConfig, invocation_directory: &Path, ) -> SearchResult { match search_config { SearchConfig::FromInvocationDirectory => Self::find_next(invocation_directory), SearchConfig::FromSearchDirectory { search_directory } => { let search_directory = Self::clean(invocation_directory, search_directory); let justfile = Self::justfile(&search_directory)?; let working_directory = Self::working_directory_from_justfile(&justfile)?; Ok(Self { justfile, working_directory, }) } SearchConfig::WithJustfile { justfile } => { let justfile = Self::clean(invocation_directory, justfile); let working_directory = Self::working_directory_from_justfile(&justfile)?; Ok(Self { justfile, working_directory, }) } SearchConfig::WithJustfileAndWorkingDirectory { justfile, working_directory, } => Ok(Self { justfile: Self::clean(invocation_directory, justfile), working_directory: Self::clean(invocation_directory, working_directory), }), } } pub(crate) fn find_next(starting_dir: &Path) -> SearchResult { let justfile = Self::justfile(starting_dir)?; let working_directory = Self::working_directory_from_justfile(&justfile)?; Ok(Self { justfile, working_directory, }) } pub(crate) fn init( search_config: &SearchConfig, invocation_directory: &Path, ) -> SearchResult { match search_config { SearchConfig::FromInvocationDirectory => { let working_directory = Self::project_root(invocation_directory)?; let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME); Ok(Self { justfile, working_directory, }) } SearchConfig::FromSearchDirectory { search_directory } => { let search_directory = Self::clean(invocation_directory, search_directory); let working_directory = Self::project_root(&search_directory)?; let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME); Ok(Self { justfile, working_directory, }) } SearchConfig::WithJustfile { justfile } => { let justfile = Self::clean(invocation_directory, justfile); let working_directory = Self::working_directory_from_justfile(&justfile)?; Ok(Self { justfile, working_directory, }) } SearchConfig::WithJustfileAndWorkingDirectory { justfile, working_directory, } => Ok(Self { justfile: Self::clean(invocation_directory, justfile), working_directory: Self::clean(invocation_directory, working_directory), }), } } pub(crate) fn justfile(directory: &Path) -> SearchResult { for directory in directory.ancestors() { let mut candidates = BTreeSet::new(); let entries = fs::read_dir(directory).map_err(|io_error| SearchError::Io { io_error, directory: directory.to_owned(), })?; for entry in entries { let entry = entry.map_err(|io_error| SearchError::Io { io_error, directory: directory.to_owned(), })?; if let Some(name) = entry.file_name().to_str() { for justfile_name in JUSTFILE_NAMES { if name.eq_ignore_ascii_case(justfile_name) { candidates.insert(entry.path()); } } } } match candidates.len() { 0 => {} 1 => return Ok(candidates.into_iter().next().unwrap()), _ => return Err(SearchError::MultipleCandidates { candidates }), } } Err(SearchError::NotFound) } fn clean(invocation_directory: &Path, path: &Path) -> PathBuf { let path = invocation_directory.join(path); let mut clean = Vec::new(); for component in path.components() { if component == Component::ParentDir { if let Some(Component::Normal(_)) = clean.last() { clean.pop(); } } else { clean.push(component); } } clean.into_iter().collect() } fn project_root(directory: &Path) -> SearchResult { for directory in directory.ancestors() { let entries = fs::read_dir(directory).map_err(|io_error| SearchError::Io { io_error, directory: directory.to_owned(), })?; for entry in entries { let entry = entry.map_err(|io_error| SearchError::Io { io_error, directory: directory.to_owned(), })?; for project_root_child in PROJECT_ROOT_CHILDREN.iter().copied() { if entry.file_name() == project_root_child { return Ok(directory.to_owned()); } } } } Ok(directory.to_owned()) } fn working_directory_from_justfile(justfile: &Path) -> SearchResult { Ok( justfile .parent() .ok_or_else(|| SearchError::JustfileHadNoParent { path: justfile.to_path_buf(), })? .to_owned(), ) } } #[cfg(test)] mod tests { use super::*; use temptree::temptree; #[test] fn not_found() { let tmp = testing::tempdir(); match Search::justfile(tmp.path()) { Err(SearchError::NotFound) => {} _ => panic!("No justfile found error was expected"), } } #[test] fn multiple_candidates() { let tmp = testing::tempdir(); let mut path = tmp.path().to_path_buf(); path.push(DEFAULT_JUSTFILE_NAME); fs::write(&path, "default:\n\techo ok").unwrap(); path.pop(); path.push(DEFAULT_JUSTFILE_NAME.to_uppercase()); if fs::File::open(path.as_path()).is_ok() { // We are in case-insensitive file system return; } fs::write(&path, "default:\n\techo ok").unwrap(); path.pop(); match Search::justfile(path.as_path()) { Err(SearchError::MultipleCandidates { .. }) => {} _ => panic!("Multiple candidates error was expected"), } } #[test] fn found() { let tmp = testing::tempdir(); let mut path = tmp.path().to_path_buf(); path.push(DEFAULT_JUSTFILE_NAME); fs::write(&path, "default:\n\techo ok").unwrap(); path.pop(); if let Err(err) = Search::justfile(path.as_path()) { panic!("No errors were expected: {err}"); } } #[test] fn found_spongebob_case() { let tmp = testing::tempdir(); let mut path = tmp.path().to_path_buf(); let spongebob_case = DEFAULT_JUSTFILE_NAME .chars() .enumerate() .map(|(i, c)| { if i % 2 == 0 { c.to_ascii_uppercase() } else { c } }) .collect::(); path.push(spongebob_case); fs::write(&path, "default:\n\techo ok").unwrap(); path.pop(); if let Err(err) = Search::justfile(path.as_path()) { panic!("No errors were expected: {err}"); } } #[test] fn found_from_inner_dir() { let tmp = testing::tempdir(); let mut path = tmp.path().to_path_buf(); path.push(DEFAULT_JUSTFILE_NAME); fs::write(&path, "default:\n\techo ok").unwrap(); path.pop(); path.push("a"); fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory"); path.push("b"); fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory"); if let Err(err) = Search::justfile(path.as_path()) { panic!("No errors were expected: {err}"); } } #[test] fn found_and_stopped_at_first_justfile() { let tmp = testing::tempdir(); let mut path = tmp.path().to_path_buf(); path.push(DEFAULT_JUSTFILE_NAME); fs::write(&path, "default:\n\techo ok").unwrap(); path.pop(); path.push("a"); fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory"); path.push(DEFAULT_JUSTFILE_NAME); fs::write(&path, "default:\n\techo ok").unwrap(); path.pop(); path.push("b"); fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory"); match Search::justfile(path.as_path()) { Ok(found_path) => { path.pop(); path.push(DEFAULT_JUSTFILE_NAME); assert_eq!(found_path, path); } Err(err) => panic!("No errors were expected: {err}"), } } #[test] fn justfile_symlink_parent() { let tmp = temptree! { src: "", sub: {}, }; let src = tmp.path().join("src"); let sub = tmp.path().join("sub"); let justfile = sub.join("justfile"); #[cfg(unix)] std::os::unix::fs::symlink(src, &justfile).unwrap(); #[cfg(windows)] std::os::windows::fs::symlink_file(&src, &justfile).unwrap(); let search_config = SearchConfig::FromInvocationDirectory; let search = Search::find(&search_config, &sub).unwrap(); assert_eq!(search.justfile, justfile); assert_eq!(search.working_directory, sub); } #[test] fn clean() { let cases = &[ ("/", "foo", "/foo"), ("/bar", "/foo", "/foo"), #[cfg(windows)] ("//foo", "bar//baz", "//foo\\bar\\baz"), #[cfg(not(windows))] ("/", "..", "/"), ("/", "/..", "/"), ("/..", "", "/"), ("/../../../..", "../../../", "/"), ("/.", "./", "/"), ("/foo/../", "bar", "/bar"), ("/foo/bar", "..", "/foo"), ("/foo/bar/", "..", "/foo"), ]; for (prefix, suffix, want) in cases { let have = Search::clean(Path::new(prefix), Path::new(suffix)); assert_eq!(have, Path::new(want)); } } } just-1.21.0/src/search_config.rs000064400000000000000000000013611046102023000146220ustar 00000000000000use super::*; /// Controls how `just` will search for the justfile. #[derive(Debug, PartialEq)] pub(crate) enum SearchConfig { /// Recursively search for the justfile upwards from the invocation directory /// to the root, setting the working directory to the directory in which the /// justfile is found. FromInvocationDirectory, /// As in `Invocation`, but start from `search_directory`. FromSearchDirectory { search_directory: PathBuf }, /// Use user-specified justfile, with the working directory set to the /// directory that contains it. WithJustfile { justfile: PathBuf }, /// Use user-specified justfile and working directory. WithJustfileAndWorkingDirectory { justfile: PathBuf, working_directory: PathBuf, }, } just-1.21.0/src/search_error.rs000064400000000000000000000022741046102023000145120ustar 00000000000000use super::*; #[derive(Debug, Snafu)] #[snafu(visibility(pub(crate)))] pub(crate) enum SearchError { #[snafu(display( "I/O error reading directory `{}`: {}", directory.display(), io_error ))] Io { directory: PathBuf, io_error: io::Error, }, #[snafu(display("Justfile path had no parent: {}", path.display()))] JustfileHadNoParent { path: PathBuf }, #[snafu(display( "Multiple candidate justfiles found in `{}`: {}", candidates.iter().next().unwrap().parent().unwrap().display(), List::and_ticked( candidates .iter() .map(|candidate| candidate.file_name().unwrap().to_string_lossy()) ), ))] MultipleCandidates { candidates: BTreeSet }, #[snafu(display("No justfile found"))] NotFound, } #[cfg(test)] mod tests { use super::*; #[test] fn multiple_candidates_formatting() { let error = SearchError::MultipleCandidates { candidates: [Path::new("/foo/justfile"), Path::new("/foo/JUSTFILE")] .iter() .map(|path| path.to_path_buf()) .collect(), }; assert_eq!( error.to_string(), "Multiple candidate justfiles found in `/foo`: `JUSTFILE` and `justfile`" ); } } just-1.21.0/src/set.rs000064400000000000000000000006131046102023000126220ustar 00000000000000use super::*; #[derive(Debug, Clone)] pub(crate) struct Set<'src> { pub(crate) name: Name<'src>, pub(crate) value: Setting<'src>, } impl<'src> Keyed<'src> for Set<'src> { fn key(&self) -> &'src str { self.name.lexeme() } } impl<'src> Display for Set<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { write!(f, "set {} := {}", self.name, self.value) } } just-1.21.0/src/setting.rs000064400000000000000000000017511046102023000135100ustar 00000000000000use super::*; #[derive(Debug, Clone)] pub(crate) enum Setting<'src> { AllowDuplicateRecipes(bool), DotenvFilename(String), DotenvLoad(bool), DotenvPath(String), Export(bool), Fallback(bool), IgnoreComments(bool), PositionalArguments(bool), Shell(Shell<'src>), Tempdir(String), WindowsPowerShell(bool), WindowsShell(Shell<'src>), } impl<'src> Display for Setting<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { match self { Setting::AllowDuplicateRecipes(value) | Setting::DotenvLoad(value) | Setting::Export(value) | Setting::Fallback(value) | Setting::IgnoreComments(value) | Setting::PositionalArguments(value) | Setting::WindowsPowerShell(value) => write!(f, "{value}"), Setting::Shell(shell) | Setting::WindowsShell(shell) => write!(f, "{shell}"), Setting::DotenvFilename(value) | Setting::DotenvPath(value) | Setting::Tempdir(value) => { write!(f, "{value:?}") } } } } just-1.21.0/src/settings.rs000064400000000000000000000146561046102023000137030ustar 00000000000000use super::*; pub(crate) const DEFAULT_SHELL: &str = "sh"; pub(crate) const DEFAULT_SHELL_ARGS: &[&str] = &["-cu"]; pub(crate) const WINDOWS_POWERSHELL_SHELL: &str = "powershell.exe"; pub(crate) const WINDOWS_POWERSHELL_ARGS: &[&str] = &["-NoLogo", "-Command"]; #[derive(Debug, PartialEq, Serialize, Default)] pub(crate) struct Settings<'src> { pub(crate) allow_duplicate_recipes: bool, pub(crate) dotenv_filename: Option, pub(crate) dotenv_load: Option, pub(crate) dotenv_path: Option, pub(crate) export: bool, pub(crate) fallback: bool, pub(crate) ignore_comments: bool, pub(crate) positional_arguments: bool, pub(crate) shell: Option>, pub(crate) tempdir: Option, pub(crate) windows_powershell: bool, pub(crate) windows_shell: Option>, } impl<'src> Settings<'src> { pub(crate) fn from_setting_iter(iter: impl Iterator>) -> Self { let mut settings = Self::default(); for set in iter { match set { Setting::AllowDuplicateRecipes(allow_duplicate_recipes) => { settings.allow_duplicate_recipes = allow_duplicate_recipes; } Setting::DotenvFilename(filename) => { settings.dotenv_filename = Some(filename); } Setting::DotenvLoad(dotenv_load) => { settings.dotenv_load = Some(dotenv_load); } Setting::DotenvPath(path) => { settings.dotenv_path = Some(PathBuf::from(path)); } Setting::Export(export) => { settings.export = export; } Setting::Fallback(fallback) => { settings.fallback = fallback; } Setting::IgnoreComments(ignore_comments) => { settings.ignore_comments = ignore_comments; } Setting::PositionalArguments(positional_arguments) => { settings.positional_arguments = positional_arguments; } Setting::Shell(shell) => { settings.shell = Some(shell); } Setting::WindowsPowerShell(windows_powershell) => { settings.windows_powershell = windows_powershell; } Setting::WindowsShell(windows_shell) => { settings.windows_shell = Some(windows_shell); } Setting::Tempdir(tempdir) => { settings.tempdir = Some(tempdir); } } } settings } pub(crate) fn shell_command(&self, config: &Config) -> Command { let (command, args) = self.shell(config); let mut cmd = Command::new(command); cmd.args(args); cmd } pub(crate) fn shell<'a>(&'a self, config: &'a Config) -> (&'a str, Vec<&'a str>) { match (&config.shell, &config.shell_args) { (Some(shell), Some(shell_args)) => (shell, shell_args.iter().map(String::as_ref).collect()), (Some(shell), None) => (shell, DEFAULT_SHELL_ARGS.to_vec()), (None, Some(shell_args)) => ( DEFAULT_SHELL, shell_args.iter().map(String::as_ref).collect(), ), (None, None) => { if let (true, Some(shell)) = (cfg!(windows), &self.windows_shell) { ( shell.command.cooked.as_ref(), shell .arguments .iter() .map(|argument| argument.cooked.as_ref()) .collect(), ) } else if cfg!(windows) && self.windows_powershell { (WINDOWS_POWERSHELL_SHELL, WINDOWS_POWERSHELL_ARGS.to_vec()) } else if let Some(shell) = &self.shell { ( shell.command.cooked.as_ref(), shell .arguments .iter() .map(|argument| argument.cooked.as_ref()) .collect(), ) } else { (DEFAULT_SHELL, DEFAULT_SHELL_ARGS.to_vec()) } } } } } #[cfg(test)] mod tests { use super::*; #[test] fn default_shell() { let settings = Settings::default(); let config = Config { shell_command: false, ..testing::config(&[]) }; assert_eq!(settings.shell(&config), ("sh", vec!["-cu"])); } #[test] fn default_shell_powershell() { let settings = Settings { windows_powershell: true, ..Default::default() }; let config = Config { shell_command: false, ..testing::config(&[]) }; if cfg!(windows) { assert_eq!( settings.shell(&config), ("powershell.exe", vec!["-NoLogo", "-Command"]) ); } else { assert_eq!(settings.shell(&config), ("sh", vec!["-cu"])); } } #[test] fn overwrite_shell() { let settings = Settings::default(); let config = Config { shell_command: true, shell: Some("lol".to_string()), shell_args: Some(vec!["-nice".to_string()]), ..testing::config(&[]) }; assert_eq!(settings.shell(&config), ("lol", vec!["-nice"])); } #[test] fn overwrite_shell_powershell() { let settings = Settings { windows_powershell: true, ..Default::default() }; let config = Config { shell_command: true, shell: Some("lol".to_string()), shell_args: Some(vec!["-nice".to_string()]), ..testing::config(&[]) }; assert_eq!(settings.shell(&config), ("lol", vec!["-nice"])); } #[test] fn shell_cooked() { let settings = Settings { shell: Some(Shell { command: StringLiteral { kind: StringKind::from_token_start("\"").unwrap(), raw: "asdf.exe", cooked: "asdf.exe".to_string(), }, arguments: vec![StringLiteral { kind: StringKind::from_token_start("\"").unwrap(), raw: "-nope", cooked: "-nope".to_string(), }], }), ..Default::default() }; let config = Config { shell_command: false, ..testing::config(&[]) }; assert_eq!(settings.shell(&config), ("asdf.exe", vec!["-nope"])); } #[test] fn shell_present_but_not_shell_args() { let settings = Settings { windows_powershell: true, ..Default::default() }; let config = Config { shell: Some("lol".to_string()), ..testing::config(&[]) }; assert_eq!(settings.shell(&config).0, "lol"); } #[test] fn shell_args_present_but_not_shell() { let settings = Settings { windows_powershell: true, ..Default::default() }; let config = Config { shell_command: false, shell_args: Some(vec!["-nice".to_string()]), ..testing::config(&[]) }; assert_eq!(settings.shell(&config), ("sh", vec!["-nice"])); } } just-1.21.0/src/shebang.rs000064400000000000000000000115511046102023000134410ustar 00000000000000#[derive(Copy, Clone)] pub(crate) struct Shebang<'line> { pub(crate) interpreter: &'line str, pub(crate) argument: Option<&'line str>, } impl<'line> Shebang<'line> { pub(crate) fn new(line: &'line str) -> Option> { if !line.starts_with("#!") { return None; } let mut pieces = line[2..] .lines() .next() .unwrap_or("") .trim() .splitn(2, |c| c == ' ' || c == '\t'); let interpreter = pieces.next().unwrap_or(""); let argument = pieces.next(); if interpreter.is_empty() { return None; } Some(Shebang { interpreter, argument, }) } fn interpreter_filename(&self) -> &str { self .interpreter .split(|c| matches!(c, '/' | '\\')) .last() .unwrap_or(self.interpreter) } pub(crate) fn script_filename(&self, recipe: &str) -> String { match self.interpreter_filename() { "cmd" | "cmd.exe" => format!("{recipe}.bat"), "powershell" | "powershell.exe" | "pwsh" | "pwsh.exe" => format!("{recipe}.ps1"), _ => recipe.to_owned(), } } pub(crate) fn include_shebang_line(&self) -> bool { !(cfg!(windows) || matches!(self.interpreter_filename(), "cmd" | "cmd.exe")) } } #[cfg(test)] mod tests { use super::Shebang; #[test] fn split_shebang() { fn check(text: &str, expected_split: Option<(&str, Option<&str>)>) { let shebang = Shebang::new(text); assert_eq!( shebang.map(|shebang| (shebang.interpreter, shebang.argument)), expected_split ); } check("#! ", None); check("#!", None); check("#!/bin/bash", Some(("/bin/bash", None))); check("#!/bin/bash ", Some(("/bin/bash", None))); check( "#!/usr/bin/env python", Some(("/usr/bin/env", Some("python"))), ); check( "#!/usr/bin/env python ", Some(("/usr/bin/env", Some("python"))), ); check( "#!/usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x"))), ); check( "#!/usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x"))), ); check( "#!/usr/bin/env python \t-x\t", Some(("/usr/bin/env", Some("python \t-x"))), ); check("#/usr/bin/env python \t-x\t", None); check("#! /bin/bash", Some(("/bin/bash", None))); check("#!\t\t/bin/bash ", Some(("/bin/bash", None))); check( "#! \t\t/usr/bin/env python", Some(("/usr/bin/env", Some("python"))), ); check( "#! /usr/bin/env python ", Some(("/usr/bin/env", Some("python"))), ); check( "#! /usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x"))), ); check( "#! /usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x"))), ); check( "#! /usr/bin/env python \t-x\t", Some(("/usr/bin/env", Some("python \t-x"))), ); check("# /usr/bin/env python \t-x\t", None); } #[test] fn interpreter_filename_with_forward_slash() { assert_eq!( Shebang::new("#!/foo/bar/baz") .unwrap() .interpreter_filename(), "baz" ); } #[test] fn interpreter_filename_with_backslash() { assert_eq!( Shebang::new("#!\\foo\\bar\\baz") .unwrap() .interpreter_filename(), "baz" ); } #[test] fn powershell_script_filename() { assert_eq!( Shebang::new("#!powershell").unwrap().script_filename("foo"), "foo.ps1" ); } #[test] fn pwsh_script_filename() { assert_eq!( Shebang::new("#!pwsh").unwrap().script_filename("foo"), "foo.ps1" ); } #[test] fn powershell_exe_script_filename() { assert_eq!( Shebang::new("#!powershell.exe") .unwrap() .script_filename("foo"), "foo.ps1" ); } #[test] fn pwsh_exe_script_filename() { assert_eq!( Shebang::new("#!pwsh.exe").unwrap().script_filename("foo"), "foo.ps1" ); } #[test] fn cmd_script_filename() { assert_eq!( Shebang::new("#!cmd").unwrap().script_filename("foo"), "foo.bat" ); } #[test] fn cmd_exe_script_filename() { assert_eq!( Shebang::new("#!cmd.exe").unwrap().script_filename("foo"), "foo.bat" ); } #[test] fn plain_script_filename() { assert_eq!(Shebang::new("#!bar").unwrap().script_filename("foo"), "foo"); } #[test] fn dont_include_shebang_line_cmd() { assert!(!Shebang::new("#!cmd").unwrap().include_shebang_line()); } #[test] fn dont_include_shebang_line_cmd_exe() { assert!(!Shebang::new("#!cmd.exe /C").unwrap().include_shebang_line()); } #[test] #[cfg(not(windows))] fn include_shebang_line_other_not_windows() { assert!(Shebang::new("#!foo -c").unwrap().include_shebang_line()); } #[test] #[cfg(windows)] fn include_shebang_line_other_windows() { assert!(!Shebang::new("#!foo -c").unwrap().include_shebang_line()); } } just-1.21.0/src/shell.rs000064400000000000000000000006571046102023000131460ustar 00000000000000use super::*; #[derive(Debug, Clone, PartialEq, Serialize)] pub(crate) struct Shell<'src> { pub(crate) arguments: Vec>, pub(crate) command: StringLiteral<'src>, } impl<'src> Display for Shell<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { write!(f, "[{}", self.command)?; for argument in &self.arguments { write!(f, ", {argument}")?; } write!(f, "]") } } just-1.21.0/src/show_whitespace.rs000064400000000000000000000006421046102023000152250ustar 00000000000000use super::*; /// String wrapper that uses nonblank characters to display spaces and tabs pub struct ShowWhitespace<'str>(pub &'str str); impl<'str> Display for ShowWhitespace<'str> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { for c in self.0.chars() { match c { '\t' => write!(f, "␉")?, ' ' => write!(f, "␠")?, _ => write!(f, "{c}")?, }; } Ok(()) } } just-1.21.0/src/string_kind.rs000064400000000000000000000052571046102023000143530ustar 00000000000000use super::*; #[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)] pub(crate) struct StringKind { delimiter: StringDelimiter, indented: bool, } #[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)] enum StringDelimiter { Backtick, QuoteDouble, QuoteSingle, } impl StringKind { // Indented values must come before un-indented values, or else // `Self::from_token_start` will incorrectly return indented = false // for indented strings. const ALL: &'static [Self] = &[ Self::new(StringDelimiter::Backtick, true), Self::new(StringDelimiter::Backtick, false), Self::new(StringDelimiter::QuoteDouble, true), Self::new(StringDelimiter::QuoteDouble, false), Self::new(StringDelimiter::QuoteSingle, true), Self::new(StringDelimiter::QuoteSingle, false), ]; const fn new(delimiter: StringDelimiter, indented: bool) -> Self { Self { delimiter, indented, } } pub(crate) fn delimiter(self) -> &'static str { match (self.delimiter, self.indented) { (StringDelimiter::Backtick, false) => "`", (StringDelimiter::Backtick, true) => "```", (StringDelimiter::QuoteDouble, false) => "\"", (StringDelimiter::QuoteDouble, true) => "\"\"\"", (StringDelimiter::QuoteSingle, false) => "'", (StringDelimiter::QuoteSingle, true) => "'''", } } pub(crate) fn delimiter_len(self) -> usize { self.delimiter().len() } pub(crate) fn token_kind(self) -> TokenKind { match self.delimiter { StringDelimiter::QuoteDouble | StringDelimiter::QuoteSingle => TokenKind::StringToken, StringDelimiter::Backtick => TokenKind::Backtick, } } pub(crate) fn unterminated_error_kind(self) -> CompileErrorKind<'static> { match self.delimiter { StringDelimiter::QuoteDouble | StringDelimiter::QuoteSingle => { CompileErrorKind::UnterminatedString } StringDelimiter::Backtick => CompileErrorKind::UnterminatedBacktick, } } pub(crate) fn processes_escape_sequences(self) -> bool { match self.delimiter { StringDelimiter::QuoteDouble => true, StringDelimiter::Backtick | StringDelimiter::QuoteSingle => false, } } pub(crate) fn indented(self) -> bool { self.indented } pub(crate) fn from_string_or_backtick(token: Token) -> CompileResult { Self::from_token_start(token.lexeme()).ok_or_else(|| { token.error(CompileErrorKind::Internal { message: "StringKind::from_token: Expected String or Backtick".to_owned(), }) }) } pub(crate) fn from_token_start(token_start: &str) -> Option { Self::ALL .iter() .find(|&&kind| token_start.starts_with(kind.delimiter())) .copied() } } just-1.21.0/src/string_literal.rs000064400000000000000000000011141046102023000150460ustar 00000000000000use super::*; #[derive(PartialEq, Debug, Clone)] pub(crate) struct StringLiteral<'src> { pub(crate) kind: StringKind, pub(crate) raw: &'src str, pub(crate) cooked: String, } impl Display for StringLiteral<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!( f, "{}{}{}", self.kind.delimiter(), self.raw, self.kind.delimiter() ) } } impl<'src> Serialize for StringLiteral<'src> { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.cooked) } } just-1.21.0/src/subcommand.rs000064400000000000000000000405401046102023000141620ustar 00000000000000use super::*; const INIT_JUSTFILE: &str = "default:\n echo 'Hello, world!'\n"; #[derive(PartialEq, Clone, Debug)] pub(crate) enum Subcommand { Changelog, Choose { overrides: BTreeMap, chooser: Option, }, Command { arguments: Vec, binary: OsString, overrides: BTreeMap, }, Completions { shell: String, }, Dump, Edit, Evaluate { overrides: BTreeMap, variable: Option, }, Format, Init, List, Run { arguments: Vec, overrides: BTreeMap, }, Show { name: String, }, Summary, Variables, } impl Subcommand { pub(crate) fn execute<'src>( &self, config: &Config, loader: &'src Loader, ) -> Result<(), Error<'src>> { use Subcommand::*; match self { Changelog => { Self::changelog(); return Ok(()); } Completions { shell } => return Self::completions(shell), Init => return Self::init(config), Run { arguments, overrides, } => return Self::run(config, loader, arguments, overrides), _ => {} } let search = Search::find(&config.search_config, &config.invocation_directory)?; if let Edit = self { return Self::edit(&search); } let compilation = Self::compile(config, loader, &search)?; let justfile = &compilation.justfile; let ast = compilation.root_ast(); let src = compilation.root_src(); match self { Choose { overrides, chooser } => { Self::choose(config, justfile, &search, overrides, chooser.as_deref())?; } Command { overrides, .. } | Evaluate { overrides, .. } => { justfile.run(config, &search, overrides, &[])?; } Dump => Self::dump(config, ast, justfile)?, Format => Self::format(config, &search, src, ast)?, List => Self::list(config, 0, justfile), Show { ref name } => Self::show(config, name, justfile)?, Summary => Self::summary(config, justfile), Variables => Self::variables(justfile), Changelog | Completions { .. } | Edit | Init | Run { .. } => unreachable!(), } Ok(()) } fn run<'src>( config: &Config, loader: &'src Loader, arguments: &[String], overrides: &BTreeMap, ) -> Result<(), Error<'src>> { if matches!( config.search_config, SearchConfig::FromInvocationDirectory | SearchConfig::FromSearchDirectory { .. } ) { let starting_path = match &config.search_config { SearchConfig::FromInvocationDirectory => config.invocation_directory.clone(), SearchConfig::FromSearchDirectory { search_directory } => { env::current_dir().unwrap().join(search_directory) } _ => unreachable!(), }; let mut path = starting_path.clone(); let mut unknown_recipes_errors = None; loop { let search = match Search::find_next(&path) { Err(SearchError::NotFound) => match unknown_recipes_errors { Some(err) => return Err(err), None => return Err(SearchError::NotFound.into()), }, Err(err) => return Err(err.into()), Ok(search) => { if config.verbosity.loquacious() && path != starting_path { eprintln!( "Trying {}", starting_path .strip_prefix(path) .unwrap() .components() .map(|_| path::Component::ParentDir) .collect::() .join(search.justfile.file_name().unwrap()) .display() ); } search } }; match Self::run_inner(config, loader, arguments, overrides, &search) { Err((err @ Error::UnknownRecipes { .. }, true)) => { match search.justfile.parent().unwrap().parent() { Some(parent) => { unknown_recipes_errors.get_or_insert(err); path = parent.into(); } None => return Err(err), } } result => return result.map_err(|(err, _fallback)| err), } } } else { Self::run_inner( config, loader, arguments, overrides, &Search::find(&config.search_config, &config.invocation_directory)?, ) .map_err(|(err, _fallback)| err) } } fn run_inner<'src>( config: &Config, loader: &'src Loader, arguments: &[String], overrides: &BTreeMap, search: &Search, ) -> Result<(), (Error<'src>, bool)> { let compilation = Self::compile(config, loader, search).map_err(|err| (err, false))?; let justfile = &compilation.justfile; justfile .run(config, search, overrides, arguments) .map_err(|err| (err, justfile.settings.fallback)) } fn compile<'src>( config: &Config, loader: &'src Loader, search: &Search, ) -> Result, Error<'src>> { let compilation = Compiler::compile(config.unstable, loader, &search.justfile)?; if config.verbosity.loud() { for warning in &compilation.justfile.warnings { eprintln!("{}", warning.color_display(config.color.stderr())); } } Ok(compilation) } fn changelog() { print!("{}", include_str!("../CHANGELOG.md")); } fn choose<'src>( config: &Config, justfile: &Justfile<'src>, search: &Search, overrides: &BTreeMap, chooser: Option<&str>, ) -> Result<(), Error<'src>> { let recipes = justfile .public_recipes(config.unsorted) .iter() .filter(|recipe| recipe.min_arguments() == 0) .copied() .collect::>>(); if recipes.is_empty() { return Err(Error::NoChoosableRecipes); } let chooser = chooser .map(OsString::from) .or_else(|| env::var_os(config::CHOOSER_ENVIRONMENT_KEY)) .unwrap_or_else(|| config::chooser_default(&search.justfile)); let result = justfile .settings .shell_command(config) .arg(&chooser) .current_dir(&search.working_directory) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn(); let mut child = match result { Ok(child) => child, Err(io_error) => { let (shell_binary, shell_arguments) = justfile.settings.shell(config); return Err(Error::ChooserInvoke { shell_binary: shell_binary.to_owned(), shell_arguments: shell_arguments.join(" "), chooser, io_error, }); } }; for recipe in recipes { if let Err(io_error) = child .stdin .as_mut() .expect("Child was created with piped stdio") .write_all(format!("{}\n", recipe.name).as_bytes()) { return Err(Error::ChooserWrite { io_error, chooser }); } } let output = match child.wait_with_output() { Ok(output) => output, Err(io_error) => { return Err(Error::ChooserRead { io_error, chooser }); } }; if !output.status.success() { return Err(Error::ChooserStatus { status: output.status, chooser, }); } let stdout = String::from_utf8_lossy(&output.stdout); let recipes = stdout .split_whitespace() .map(str::to_owned) .collect::>(); justfile.run(config, search, overrides, &recipes) } fn completions(shell: &str) -> RunResult<'static, ()> { use clap::Shell; fn replace(haystack: &mut String, needle: &str, replacement: &str) -> RunResult<'static, ()> { if let Some(index) = haystack.find(needle) { haystack.replace_range(index..index + needle.len(), replacement); Ok(()) } else { Err(Error::internal(format!( "Failed to find text:\n{needle}\n…in completion script:\n{haystack}" ))) } } let shell = shell .parse::() .expect("Invalid value for clap::Shell"); let buffer = Vec::new(); let mut cursor = Cursor::new(buffer); Config::app().gen_completions_to(env!("CARGO_PKG_NAME"), shell, &mut cursor); let buffer = cursor.into_inner(); let mut script = String::from_utf8(buffer).expect("Clap completion not UTF-8"); match shell { Shell::Bash => { for (needle, replacement) in completions::BASH_COMPLETION_REPLACEMENTS { replace(&mut script, needle, replacement)?; } } Shell::Fish => { script.insert_str(0, completions::FISH_RECIPE_COMPLETIONS); } Shell::PowerShell => { for (needle, replacement) in completions::POWERSHELL_COMPLETION_REPLACEMENTS { replace(&mut script, needle, replacement)?; } } Shell::Zsh => { for (needle, replacement) in completions::ZSH_COMPLETION_REPLACEMENTS { replace(&mut script, needle, replacement)?; } } Shell::Elvish => {} } println!("{}", script.trim()); Ok(()) } fn dump(config: &Config, ast: &Ast, justfile: &Justfile) -> Result<(), Error<'static>> { match config.dump_format { DumpFormat::Json => { serde_json::to_writer(io::stdout(), justfile) .map_err(|serde_json_error| Error::DumpJson { serde_json_error })?; println!(); } DumpFormat::Just => print!("{ast}"), } Ok(()) } fn edit(search: &Search) -> Result<(), Error<'static>> { let editor = env::var_os("VISUAL") .or_else(|| env::var_os("EDITOR")) .unwrap_or_else(|| "vim".into()); let error = Command::new(&editor) .current_dir(&search.working_directory) .arg(&search.justfile) .status(); let status = match error { Err(io_error) => return Err(Error::EditorInvoke { editor, io_error }), Ok(status) => status, }; if !status.success() { return Err(Error::EditorStatus { editor, status }); } Ok(()) } fn format(config: &Config, search: &Search, src: &str, ast: &Ast) -> Result<(), Error<'static>> { config.require_unstable("The `--fmt` command is currently unstable.")?; let formatted = ast.to_string(); if config.check { return if formatted == src { Ok(()) } else { if !config.verbosity.quiet() { use similar::{ChangeTag, TextDiff}; let diff = TextDiff::configure() .algorithm(similar::Algorithm::Patience) .diff_lines(src, &formatted); for op in diff.ops() { for change in diff.iter_changes(op) { let (symbol, color) = match change.tag() { ChangeTag::Delete => ("-", config.color.stdout().diff_deleted()), ChangeTag::Equal => (" ", config.color.stdout()), ChangeTag::Insert => ("+", config.color.stdout().diff_added()), }; print!("{}{symbol}{change}{}", color.prefix(), color.suffix()); } } } Err(Error::FormatCheckFoundDiff) }; } fs::write(&search.justfile, formatted).map_err(|io_error| Error::WriteJustfile { justfile: search.justfile.clone(), io_error, })?; if config.verbosity.loud() { eprintln!("Wrote justfile to `{}`", search.justfile.display()); } Ok(()) } fn init(config: &Config) -> Result<(), Error<'static>> { let search = Search::init(&config.search_config, &config.invocation_directory)?; if search.justfile.is_file() { Err(Error::InitExists { justfile: search.justfile, }) } else if let Err(io_error) = fs::write(&search.justfile, INIT_JUSTFILE) { Err(Error::WriteJustfile { justfile: search.justfile, io_error, }) } else { if config.verbosity.loud() { eprintln!("Wrote justfile to `{}`", search.justfile.display()); } Ok(()) } } fn list(config: &Config, level: usize, justfile: &Justfile) { // Construct a target to alias map. let mut recipe_aliases: BTreeMap<&str, Vec<&str>> = BTreeMap::new(); for alias in justfile.aliases.values() { if alias.is_private() { continue; } if recipe_aliases.contains_key(alias.target.name.lexeme()) { let aliases = recipe_aliases.get_mut(alias.target.name.lexeme()).unwrap(); aliases.push(alias.name.lexeme()); } else { recipe_aliases.insert(alias.target.name.lexeme(), vec![alias.name.lexeme()]); } } let mut line_widths: BTreeMap<&str, usize> = BTreeMap::new(); for (name, recipe) in &justfile.recipes { if recipe.private { continue; } for name in iter::once(name).chain(recipe_aliases.get(name).unwrap_or(&Vec::new())) { let mut line_width = UnicodeWidthStr::width(*name); for parameter in &recipe.parameters { line_width += UnicodeWidthStr::width( format!(" {}", parameter.color_display(Color::never())).as_str(), ); } if line_width <= 30 { line_widths.insert(name, line_width); } } } let max_line_width = cmp::min(line_widths.values().copied().max().unwrap_or(0), 30); let doc_color = config.color.stdout().doc(); if level == 0 { print!("{}", config.list_heading); } for recipe in justfile.public_recipes(config.unsorted) { let name = recipe.name(); for (i, name) in iter::once(&name) .chain(recipe_aliases.get(name).unwrap_or(&Vec::new())) .enumerate() { print!("{}{name}", config.list_prefix.repeat(level + 1)); for parameter in &recipe.parameters { print!(" {}", parameter.color_display(config.color.stdout())); } // Declaring this outside of the nested loops will probably be more efficient, // but it creates all sorts of lifetime issues with variables inside the loops. // If this is inlined like the docs say, it shouldn't make any difference. let print_doc = |doc| { print!( " {:padding$}{} {}", "", doc_color.paint("#"), doc_color.paint(doc), padding = max_line_width .saturating_sub(line_widths.get(name).copied().unwrap_or(max_line_width)) ); }; match (i, recipe.doc) { (0, Some(doc)) => print_doc(doc), (0, None) => (), _ => { let alias_doc = format!("alias for `{}`", recipe.name); print_doc(&alias_doc); } } println!(); } } for (name, module) in &justfile.modules { println!(" {name}:"); Self::list(config, level + 1, module); } } fn show<'src>(config: &Config, name: &str, justfile: &Justfile<'src>) -> Result<(), Error<'src>> { if let Some(alias) = justfile.get_alias(name) { let recipe = justfile.get_recipe(alias.target.name.lexeme()).unwrap(); println!("{alias}"); println!("{}", recipe.color_display(config.color.stdout())); Ok(()) } else if let Some(recipe) = justfile.get_recipe(name) { println!("{}", recipe.color_display(config.color.stdout())); Ok(()) } else { Err(Error::UnknownRecipes { recipes: vec![name.to_owned()], suggestion: justfile.suggest_recipe(name), }) } } fn summary(config: &Config, justfile: &Justfile) { let mut printed = 0; Self::summary_recursive(config, &mut Vec::new(), &mut printed, justfile); println!(); if printed == 0 && config.verbosity.loud() { eprintln!("Justfile contains no recipes."); } } fn summary_recursive<'a>( config: &Config, components: &mut Vec<&'a str>, printed: &mut usize, justfile: &'a Justfile, ) { let path = components.join("::"); for recipe in justfile.public_recipes(config.unsorted) { if *printed > 0 { print!(" "); } if path.is_empty() { print!("{}", recipe.name()); } else { print!("{}::{}", path, recipe.name()); } *printed += 1; } for (name, module) in &justfile.modules { components.push(name); Self::summary_recursive(config, components, printed, module); components.pop(); } } fn variables(justfile: &Justfile) { for (i, (_, assignment)) in justfile.assignments.iter().enumerate() { if i > 0 { print!(" "); } print!("{}", assignment.name); } println!(); } } #[cfg(test)] mod tests { use super::*; #[test] fn init_justfile() { testing::compile(INIT_JUSTFILE); } } just-1.21.0/src/suggestion.rs000064400000000000000000000006521046102023000142210ustar 00000000000000use super::*; #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) struct Suggestion<'src> { pub(crate) name: &'src str, pub(crate) target: Option<&'src str>, } impl<'src> Display for Suggestion<'src> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "Did you mean `{}`", self.name)?; if let Some(target) = self.target { write!(f, ", an alias for `{target}`")?; } write!(f, "?") } } just-1.21.0/src/summary.rs000064400000000000000000000217731046102023000135360ustar 00000000000000//! Justfile summary creation, for testing purposes only. //! //! The contents of this module are not bound by any stability guarantees. //! Breaking changes may be introduced at any time. //! //! The main entry point into this module is the `summary` function, which //! parses a justfile at a given path and produces a `Summary` object, which //! broadly captures the functionality of the parsed justfile, or an error //! message. //! //! This functionality is intended to be used with `janus`, a tool for ensuring //! that changes to just do not inadvertently break or change the interpretation //! of existing justfiles. use { crate::{compiler::Compiler, error::Error, loader::Loader}, std::{collections::BTreeMap, io, path::Path}, }; mod full { pub(crate) use crate::{ assignment::Assignment, conditional_operator::ConditionalOperator, dependency::Dependency, expression::Expression, fragment::Fragment, justfile::Justfile, line::Line, parameter::Parameter, parameter_kind::ParameterKind, recipe::Recipe, thunk::Thunk, }; } pub fn summary(path: &Path) -> Result, io::Error> { let loader = Loader::new(); match Compiler::compile(false, &loader, path) { Ok(compilation) => Ok(Ok(Summary::new(&compilation.justfile))), Err(error) => Ok(Err(if let Error::Compile { compile_error } = error { compile_error.to_string() } else { format!("{error:?}") })), } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Summary { pub assignments: BTreeMap, pub recipes: BTreeMap, } impl Summary { fn new(justfile: &full::Justfile) -> Summary { let mut aliases = BTreeMap::new(); for alias in justfile.aliases.values() { aliases .entry(alias.target.name()) .or_insert_with(Vec::new) .push(alias.name.to_string()); } Summary { recipes: justfile .recipes .iter() .map(|(name, recipe)| { ( (*name).to_string(), Recipe::new(recipe, aliases.remove(name).unwrap_or_default()), ) }) .collect(), assignments: justfile .assignments .iter() .map(|(name, assignment)| ((*name).to_owned(), Assignment::new(assignment))) .collect(), } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Recipe { pub aliases: Vec, pub dependencies: Vec, pub lines: Vec, pub private: bool, pub quiet: bool, pub shebang: bool, pub parameters: Vec, } impl Recipe { fn new(recipe: &full::Recipe, aliases: Vec) -> Recipe { Recipe { private: recipe.private, shebang: recipe.shebang, quiet: recipe.quiet, dependencies: recipe.dependencies.iter().map(Dependency::new).collect(), lines: recipe.body.iter().map(Line::new).collect(), parameters: recipe.parameters.iter().map(Parameter::new).collect(), aliases, } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Parameter { pub kind: ParameterKind, pub name: String, pub default: Option, } impl Parameter { fn new(parameter: &full::Parameter) -> Parameter { Parameter { kind: ParameterKind::new(parameter.kind), name: parameter.name.lexeme().to_owned(), default: parameter.default.as_ref().map(Expression::new), } } } #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] pub enum ParameterKind { Singular, Plus, Star, } impl ParameterKind { fn new(parameter_kind: full::ParameterKind) -> Self { match parameter_kind { full::ParameterKind::Singular => Self::Singular, full::ParameterKind::Plus => Self::Plus, full::ParameterKind::Star => Self::Star, } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Line { pub fragments: Vec, } impl Line { fn new(line: &full::Line) -> Line { Line { fragments: line.fragments.iter().map(Fragment::new).collect(), } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub enum Fragment { Text { text: String }, Expression { expression: Expression }, } impl Fragment { fn new(fragment: &full::Fragment) -> Fragment { match fragment { full::Fragment::Text { token } => Fragment::Text { text: token.lexeme().to_owned(), }, full::Fragment::Interpolation { expression } => Fragment::Expression { expression: Expression::new(expression), }, } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Assignment { pub exported: bool, pub expression: Expression, } impl Assignment { fn new(assignment: &full::Assignment) -> Assignment { Assignment { exported: assignment.export, expression: Expression::new(&assignment.value), } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub enum Expression { Backtick { command: String, }, Call { name: String, arguments: Vec, }, Concatenation { lhs: Box, rhs: Box, }, Conditional { lhs: Box, rhs: Box, then: Box, otherwise: Box, operator: ConditionalOperator, }, Join { lhs: Option>, rhs: Box, }, String { text: String, }, Variable { name: String, }, } impl Expression { fn new(expression: &full::Expression) -> Expression { use full::Expression::*; match expression { Backtick { contents, .. } => Expression::Backtick { command: (*contents).clone(), }, Call { thunk } => match thunk { full::Thunk::Nullary { name, .. } => Expression::Call { name: name.lexeme().to_owned(), arguments: Vec::new(), }, full::Thunk::Unary { name, arg, .. } => Expression::Call { name: name.lexeme().to_owned(), arguments: vec![Expression::new(arg)], }, full::Thunk::UnaryOpt { name, args: (a, opt_b), .. } => { let mut arguments = vec![]; if let Some(b) = opt_b.as_ref() { arguments.push(Expression::new(b)); } arguments.push(Expression::new(a)); Expression::Call { name: name.lexeme().to_owned(), arguments, } } full::Thunk::Binary { name, args: [a, b], .. } => Expression::Call { name: name.lexeme().to_owned(), arguments: vec![Expression::new(a), Expression::new(b)], }, full::Thunk::BinaryPlus { name, args: ([a, b], rest), .. } => { let mut arguments = vec![Expression::new(a), Expression::new(b)]; for arg in rest { arguments.push(Expression::new(arg)); } Expression::Call { name: name.lexeme().to_owned(), arguments, } } full::Thunk::Ternary { name, args: [a, b, c], .. } => Expression::Call { name: name.lexeme().to_owned(), arguments: vec![Expression::new(a), Expression::new(b), Expression::new(c)], }, }, Concatenation { lhs, rhs } => Expression::Concatenation { lhs: Box::new(Expression::new(lhs)), rhs: Box::new(Expression::new(rhs)), }, Join { lhs, rhs } => Expression::Join { lhs: lhs.as_ref().map(|lhs| Box::new(Expression::new(lhs))), rhs: Box::new(Expression::new(rhs)), }, Conditional { lhs, operator, otherwise, rhs, then, } => Expression::Conditional { lhs: Box::new(Expression::new(lhs)), operator: ConditionalOperator::new(*operator), otherwise: Box::new(Expression::new(otherwise)), rhs: Box::new(Expression::new(rhs)), then: Box::new(Expression::new(then)), }, StringLiteral { string_literal } => Expression::String { text: string_literal.cooked.clone(), }, Variable { name, .. } => Expression::Variable { name: name.lexeme().to_owned(), }, Group { contents } => Expression::new(contents), } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub enum ConditionalOperator { Equality, Inequality, RegexMatch, } impl ConditionalOperator { fn new(operator: full::ConditionalOperator) -> Self { match operator { full::ConditionalOperator::Equality => Self::Equality, full::ConditionalOperator::Inequality => Self::Inequality, full::ConditionalOperator::RegexMatch => Self::RegexMatch, } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Dependency { pub recipe: String, pub arguments: Vec, } impl Dependency { fn new(dependency: &full::Dependency) -> Self { Self { recipe: dependency.recipe.name().to_owned(), arguments: dependency.arguments.iter().map(Expression::new).collect(), } } } just-1.21.0/src/table.rs000064400000000000000000000043171046102023000131230ustar 00000000000000use {super::*, std::collections::btree_map}; #[derive(Debug, PartialEq, Serialize)] #[serde(transparent)] pub(crate) struct Table<'key, V: Keyed<'key>> { map: BTreeMap<&'key str, V>, } impl<'key, V: Keyed<'key>> Table<'key, V> { pub(crate) fn new() -> Table<'key, V> { Table { map: BTreeMap::new(), } } pub(crate) fn insert(&mut self, value: V) { self.map.insert(value.key(), value); } pub(crate) fn len(&self) -> usize { self.map.len() } pub(crate) fn get(&self, key: &str) -> Option<&V> { self.map.get(key) } pub(crate) fn is_empty(&self) -> bool { self.map.is_empty() } pub(crate) fn values(&self) -> btree_map::Values<&'key str, V> { self.map.values() } pub(crate) fn contains_key(&self, key: &str) -> bool { self.map.contains_key(key) } pub(crate) fn keys(&self) -> btree_map::Keys<&'key str, V> { self.map.keys() } pub(crate) fn iter(&self) -> btree_map::Iter<&'key str, V> { self.map.iter() } pub(crate) fn pop(&mut self) -> Option { let key = self.map.keys().next().copied()?; self.map.remove(key) } pub(crate) fn remove(&mut self, key: &str) -> Option { self.map.remove(key) } } impl<'key, V: Keyed<'key>> Default for Table<'key, V> { fn default() -> Self { Self::new() } } impl<'key, V: Keyed<'key>> FromIterator for Table<'key, V> { fn from_iter>(iter: I) -> Self { Table { map: iter.into_iter().map(|value| (value.key(), value)).collect(), } } } impl<'key, V: Keyed<'key>> Index<&'key str> for Table<'key, V> { type Output = V; #[inline] fn index(&self, key: &str) -> &V { self.map.get(key).expect("no entry found for key") } } impl<'key, V: Keyed<'key>> IntoIterator for Table<'key, V> { type IntoIter = btree_map::IntoIter<&'key str, V>; type Item = (&'key str, V); fn into_iter(self) -> btree_map::IntoIter<&'key str, V> { self.map.into_iter() } } impl<'table, V: Keyed<'table> + 'table> IntoIterator for &'table Table<'table, V> { type IntoIter = btree_map::Iter<'table, &'table str, V>; type Item = (&'table &'table str, &'table V); fn into_iter(self) -> btree_map::Iter<'table, &'table str, V> { self.map.iter() } } just-1.21.0/src/testing.rs000064400000000000000000000064441046102023000135140ustar 00000000000000use {super::*, pretty_assertions::assert_eq}; pub(crate) fn compile(src: &str) -> Justfile { Compiler::test_compile(src).expect("expected successful compilation") } pub(crate) fn config(args: &[&str]) -> Config { let mut args = Vec::from(args); args.insert(0, "just"); let app = Config::app(); let matches = app.get_matches_from_safe(args).unwrap(); Config::from_matches(&matches).unwrap() } pub(crate) fn search(config: &Config) -> Search { let working_directory = config.invocation_directory.clone(); let justfile = working_directory.join("justfile"); Search { justfile, working_directory, } } pub(crate) fn tempdir() -> tempfile::TempDir { tempfile::Builder::new() .prefix("just-test-tempdir") .tempdir() .expect("failed to create temporary directory") } macro_rules! analysis_error { ( name: $name:ident, input: $input:expr, offset: $offset:expr, line: $line:expr, column: $column:expr, width: $width:expr, kind: $kind:expr, ) => { #[test] fn $name() { $crate::testing::analysis_error($input, $offset, $line, $column, $width, $kind); } }; } pub(crate) fn analysis_error( src: &str, offset: usize, line: usize, column: usize, length: usize, kind: CompileErrorKind, ) { let tokens = Lexer::test_lex(src).expect("Lexing failed in parse test..."); let ast = Parser::parse(0, &PathBuf::new(), &tokens).expect("Parsing failed in analysis test..."); let root = PathBuf::from("justfile"); let mut asts: HashMap = HashMap::new(); asts.insert(root.clone(), ast); let mut paths: HashMap = HashMap::new(); paths.insert("justfile".into(), "justfile".into()); match Analyzer::analyze(&[], &paths, &asts, &root) { Ok(_) => panic!("Analysis unexpectedly succeeded"), Err(have) => { let want = CompileError { token: Token { kind: have.token.kind, src, offset, line, column, length, path: "justfile".as_ref(), }, kind: Box::new(kind), }; assert_eq!(have, want); } } } macro_rules! run_error { { name: $name:ident, src: $src:expr, args: $args:expr, error: $error:pat, check: $check:block $(,)? } => { #[test] fn $name() { let config = $crate::testing::config(&$args); let search = $crate::testing::search(&config); if let Subcommand::Run{ overrides, arguments } = &config.subcommand { match $crate::testing::compile(&$crate::unindent::unindent($src)) .run( &config, &search, &overrides, &arguments, ).expect_err("Expected runtime error") { $error => $check other => { panic!("Unexpected run error: {other:?}"); } } } else { panic!("Unexpected subcommand: {:?}", config.subcommand); } } }; } macro_rules! assert_matches { ($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )?) => { match $expression { $( $pattern )|+ $( if $guard )? => {} left => panic!( "assertion failed: (left ~= right)\n left: `{:?}`\n right: `{}`", left, stringify!($($pattern)|+ $(if $guard)?) ), } } } just-1.21.0/src/thunk.rs000064400000000000000000000132361046102023000131650ustar 00000000000000use super::*; #[derive(Derivative)] #[derivative(Debug, Clone, PartialEq = "feature_allow_slow_enum")] pub(crate) enum Thunk<'src> { Nullary { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] function: fn(&FunctionContext) -> Result, }, Unary { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] function: fn(&FunctionContext, &str) -> Result, arg: Box>, }, UnaryOpt { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] function: fn(&FunctionContext, &str, Option<&str>) -> Result, args: (Box>, Box>>), }, Binary { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] function: fn(&FunctionContext, &str, &str) -> Result, args: [Box>; 2], }, BinaryPlus { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] function: fn(&FunctionContext, &str, &str, &[String]) -> Result, args: ([Box>; 2], Vec>), }, Ternary { name: Name<'src>, #[derivative(Debug = "ignore", PartialEq = "ignore")] function: fn(&FunctionContext, &str, &str, &str) -> Result, args: [Box>; 3], }, } impl<'src> Thunk<'src> { fn name(&self) -> &Name<'src> { match self { Self::Nullary { name, .. } | Self::Unary { name, .. } | Self::UnaryOpt { name, .. } | Self::Binary { name, .. } | Self::BinaryPlus { name, .. } | Self::Ternary { name, .. } => name, } } pub(crate) fn resolve( name: Name<'src>, mut arguments: Vec>, ) -> CompileResult<'src, Thunk<'src>> { function::get(name.lexeme()).map_or( Err(name.error(CompileErrorKind::UnknownFunction { function: name.lexeme(), })), |function| match (function, arguments.len()) { (Function::Nullary(function), 0) => Ok(Thunk::Nullary { function, name }), (Function::Unary(function), 1) => Ok(Thunk::Unary { function, arg: Box::new(arguments.pop().unwrap()), name, }), (Function::UnaryOpt(function), 1..=2) => { let a = Box::new(arguments.remove(0)); let b = match arguments.pop() { Some(value) => Box::new(Some(value)), None => Box::new(None), }; Ok(Thunk::UnaryOpt { function, args: (a, b), name, }) } (Function::Binary(function), 2) => { let b = Box::new(arguments.pop().unwrap()); let a = Box::new(arguments.pop().unwrap()); Ok(Thunk::Binary { function, args: [a, b], name, }) } (Function::BinaryPlus(function), 2..=usize::MAX) => { let rest = arguments.drain(2..).collect(); let b = Box::new(arguments.pop().unwrap()); let a = Box::new(arguments.pop().unwrap()); Ok(Thunk::BinaryPlus { function, args: ([a, b], rest), name, }) } (Function::Ternary(function), 3) => { let c = Box::new(arguments.pop().unwrap()); let b = Box::new(arguments.pop().unwrap()); let a = Box::new(arguments.pop().unwrap()); Ok(Thunk::Ternary { function, args: [a, b, c], name, }) } (function, _) => Err(name.error(CompileErrorKind::FunctionArgumentCountMismatch { function: name.lexeme(), found: arguments.len(), expected: function.argc(), })), }, ) } } impl Display for Thunk<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { use Thunk::*; match self { Nullary { name, .. } => write!(f, "{}()", name.lexeme()), Unary { name, arg, .. } => write!(f, "{}({arg})", name.lexeme()), UnaryOpt { name, args: (a, b), .. } => { if let Some(b) = b.as_ref() { write!(f, "{}({a}, {b})", name.lexeme()) } else { write!(f, "{}({a})", name.lexeme()) } } Binary { name, args: [a, b], .. } => write!(f, "{}({a}, {b})", name.lexeme()), BinaryPlus { name, args: ([a, b], rest), .. } => { write!(f, "{}({a}, {b}", name.lexeme())?; for arg in rest { write!(f, ", {arg}")?; } write!(f, ")") } Ternary { name, args: [a, b, c], .. } => write!(f, "{}({a}, {b}, {c})", name.lexeme()), } } } impl<'src> Serialize for Thunk<'src> { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut seq = serializer.serialize_seq(None)?; seq.serialize_element("call")?; seq.serialize_element(self.name())?; match self { Self::Nullary { .. } => {} Self::Unary { arg, .. } => seq.serialize_element(&arg)?, Self::UnaryOpt { args: (a, opt_b), .. } => { seq.serialize_element(a)?; if let Some(b) = opt_b.as_ref() { seq.serialize_element(b)?; } } Self::Binary { args, .. } => { for arg in args { seq.serialize_element(arg)?; } } Self::BinaryPlus { args, .. } => { for arg in args.0.iter().map(Box::as_ref).chain(&args.1) { seq.serialize_element(arg)?; } } Self::Ternary { args, .. } => { for arg in args { seq.serialize_element(arg)?; } } } seq.end() } } just-1.21.0/src/token.rs000064400000000000000000000054111046102023000131500ustar 00000000000000use super::*; #[derive(Debug, PartialEq, Clone, Copy)] pub(crate) struct Token<'src> { pub(crate) column: usize, pub(crate) kind: TokenKind, pub(crate) length: usize, pub(crate) line: usize, pub(crate) offset: usize, pub(crate) path: &'src Path, pub(crate) src: &'src str, } impl<'src> Token<'src> { pub(crate) fn lexeme(&self) -> &'src str { &self.src[self.offset..self.offset + self.length] } pub(crate) fn error(&self, kind: CompileErrorKind<'src>) -> CompileError<'src> { CompileError::new(*self, kind) } } impl<'src> ColorDisplay for Token<'src> { fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result { let width = if self.length == 0 { 1 } else { self.length }; let line_number = self.line.ordinal(); match self.src.lines().nth(self.line) { Some(line) => { let mut i = 0; let mut space_column = 0; let mut space_line = String::new(); let mut space_width = 0; for c in line.chars() { if c == '\t' { space_line.push_str(" "); if i < self.column { space_column += 4; } if i >= self.column && i < self.column + width { space_width += 4; } } else { if i < self.column { space_column += UnicodeWidthChar::width(c).unwrap_or(0); } if i >= self.column && i < self.column + width { space_width += UnicodeWidthChar::width(c).unwrap_or(0); } space_line.push(c); } i += c.len_utf8(); } let line_number_width = line_number.to_string().len(); writeln!( f, "{:width$}{} {}:{}:{}", "", color.context().paint("——▶"), self.path.display(), line_number, self.column.ordinal(), width = line_number_width )?; writeln!( f, "{:width$} {}", "", color.context().paint("│"), width = line_number_width )?; writeln!( f, "{} {space_line}", color.context().paint(&format!("{line_number} │")) )?; write!( f, "{:width$} {}", "", color.context().paint("│"), width = line_number_width )?; write!( f, " {0:1$}{2}{3:^<4$}{5}", "", space_column, color.prefix(), "", space_width.max(1), color.suffix() )?; } None => { if self.offset != self.src.len() { write!( f, "internal error: Error has invalid line number: {line_number}" )?; } } } Ok(()) } } just-1.21.0/src/token_kind.rs000064400000000000000000000032711046102023000141570ustar 00000000000000use super::*; #[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)] pub(crate) enum TokenKind { AmpersandAmpersand, Asterisk, At, Backtick, BangEquals, BraceL, BraceR, BracketL, BracketR, ByteOrderMark, Colon, ColonEquals, Comma, Comment, Dedent, Dollar, Eof, Eol, Equals, EqualsEquals, EqualsTilde, Identifier, Indent, InterpolationEnd, InterpolationStart, ParenL, ParenR, Plus, QuestionMark, Slash, StringToken, Text, Unspecified, Whitespace, } impl Display for TokenKind { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { use TokenKind::*; write!( f, "{}", match *self { AmpersandAmpersand => "'&&'", Asterisk => "'*'", At => "'@'", Backtick => "backtick", BangEquals => "'!='", BraceL => "'{'", BraceR => "'}'", BracketL => "'['", BracketR => "']'", ByteOrderMark => "byte order mark", Colon => "':'", ColonEquals => "':='", Comma => "','", Comment => "comment", Dedent => "dedent", Dollar => "'$'", Eof => "end of file", Eol => "end of line", Equals => "'='", EqualsEquals => "'=='", EqualsTilde => "'=~'", Identifier => "identifier", Indent => "indent", InterpolationEnd => "'}}'", InterpolationStart => "'{{'", ParenL => "'('", ParenR => "')'", Plus => "'+'", QuestionMark => "?", Slash => "'/'", StringToken => "string", Text => "command text", Unspecified => "unspecified", Whitespace => "whitespace", } ) } } just-1.21.0/src/tree.rs000064400000000000000000000063541046102023000127760ustar 00000000000000use { super::*, std::{borrow::Cow, mem}, }; /// Construct a `Tree` from a symbolic expression literal. This macro, and the /// Tree type, are only used in the Parser unit tests, providing a concise /// notation for representing the expected results of parsing a given string. macro_rules! tree { { ($($child:tt)*) } => { $crate::tree::Tree::List(vec![$(tree!($child),)*]) }; { $atom:ident } => { $crate::tree::Tree::atom(stringify!($atom)) }; { $atom:literal } => { $crate::tree::Tree::atom(format!("\"{}\"", $atom)) }; { # } => { $crate::tree::Tree::atom("#") }; { ? } => { $crate::tree::Tree::atom("?") }; { + } => { $crate::tree::Tree::atom("+") }; { * } => { $crate::tree::Tree::atom("*") }; { && } => { $crate::tree::Tree::atom("&&") }; { == } => { $crate::tree::Tree::atom("==") }; { != } => { $crate::tree::Tree::atom("!=") }; } /// A `Tree` is either… #[derive(Debug, PartialEq)] pub(crate) enum Tree<'text> { /// …an atom containing text, or… Atom(Cow<'text, str>), /// …a list containing zero or more `Tree`s. List(Vec>), } impl<'text> Tree<'text> { /// Construct an Atom from a text scalar pub(crate) fn atom(text: impl Into>) -> Tree<'text> { Tree::Atom(text.into()) } /// Construct a List from an iterable of trees pub(crate) fn list(children: impl IntoIterator>) -> Tree<'text> { Tree::List(children.into_iter().collect()) } /// Convenience function to create an atom containing quoted text pub(crate) fn string(contents: impl AsRef) -> Tree<'text> { Tree::atom(format!("\"{}\"", contents.as_ref())) } /// Push a child node into self, turning it into a List if it was an Atom pub(crate) fn push(self, tree: impl Into>) -> Tree<'text> { match self { Tree::List(mut children) => { children.push(tree.into()); Tree::List(children) } Tree::Atom(text) => Tree::List(vec![Tree::Atom(text), tree.into()]), } } /// Extend a self with a tail of Trees, turning self into a List if it was an /// Atom pub(crate) fn extend(self, tail: I) -> Tree<'text> where I: IntoIterator, T: Into>, { // Tree::List(children.into_iter().collect()) let mut head = match self { Tree::List(children) => children, Tree::Atom(text) => vec![Tree::Atom(text)], }; for child in tail { head.push(child.into()); } Tree::List(head) } /// Like `push`, but modify self in-place pub(crate) fn push_mut(&mut self, tree: impl Into>) { *self = mem::replace(self, Tree::List(Vec::new())).push(tree.into()); } } impl Display for Tree<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Tree::List(children) => { write!(f, "(")?; for (i, child) in children.iter().enumerate() { if i > 0 { write!(f, " ")?; } write!(f, "{child}")?; } write!(f, ")") } Tree::Atom(text) => write!(f, "{text}"), } } } impl<'text, T> From for Tree<'text> where T: Into>, { fn from(text: T) -> Tree<'text> { Tree::Atom(text.into()) } } just-1.21.0/src/unindent.rs000064400000000000000000000055501046102023000136600ustar 00000000000000#[must_use] pub fn unindent(text: &str) -> String { // find line start and end indices let mut lines = Vec::new(); let mut start = 0; for (i, c) in text.char_indices() { if c == '\n' || i == text.len() - c.len_utf8() { let end = i + c.len_utf8(); lines.push(&text[start..end]); start = end; } } let common_indentation = lines .iter() .filter(|line| !blank(line)) .copied() .map(indentation) .fold( None, |common_indentation, line_indentation| match common_indentation { Some(common_indentation) => Some(common(common_indentation, line_indentation)), None => Some(line_indentation), }, ) .unwrap_or(""); let mut replacements = Vec::with_capacity(lines.len()); for (i, line) in lines.iter().enumerate() { let blank = blank(line); let first = i == 0; let last = i == lines.len() - 1; let replacement = match (blank, first, last) { (true, false, false) => "\n", (true, _, _) => "", (false, _, _) => &line[common_indentation.len()..], }; replacements.push(replacement); } replacements.into_iter().collect() } fn indentation(line: &str) -> &str { let i = line .char_indices() .take_while(|(_, c)| matches!(c, ' ' | '\t')) .map(|(i, _)| i + 1) .last() .unwrap_or(0); &line[..i] } fn blank(line: &str) -> bool { line.chars().all(|c| matches!(c, ' ' | '\t' | '\r' | '\n')) } fn common<'s>(a: &'s str, b: &'s str) -> &'s str { let i = a .char_indices() .zip(b.chars()) .take_while(|((_, ac), bc)| ac == bc) .map(|((i, c), _)| i + c.len_utf8()) .last() .unwrap_or(0); &a[0..i] } #[cfg(test)] mod tests { use super::*; #[test] fn unindents() { assert_eq!(unindent("foo"), "foo"); assert_eq!(unindent("foo\nbar\nbaz\n"), "foo\nbar\nbaz\n"); assert_eq!(unindent(""), ""); assert_eq!(unindent(" foo\n bar"), "foo\nbar"); assert_eq!(unindent(" foo\n bar\n\n"), "foo\nbar\n"); assert_eq!( unindent( " hello bar " ), "hello\nbar\n" ); assert_eq!(unindent("hello\n bar\n foo"), "hello\n bar\n foo"); assert_eq!( unindent( " hello bar " ), "\nhello\nbar\n\n" ); } #[test] fn indentations() { assert_eq!(indentation(""), ""); assert_eq!(indentation("foo"), ""); assert_eq!(indentation(" foo"), " "); assert_eq!(indentation("\t\tfoo"), "\t\t"); assert_eq!(indentation("\t \t foo"), "\t \t "); } #[test] fn blanks() { assert!(blank(" \n")); assert!(!blank(" foo\n")); assert!(blank("\t\t\n")); } #[test] fn commons() { assert_eq!(common("foo", "foobar"), "foo"); assert_eq!(common("foo", "bar"), ""); assert_eq!(common("", ""), ""); assert_eq!(common("", "bar"), ""); } } just-1.21.0/src/unresolved_dependency.rs000064400000000000000000000010261046102023000164120ustar 00000000000000use super::*; #[derive(PartialEq, Debug, Clone)] pub(crate) struct UnresolvedDependency<'src> { pub(crate) recipe: Name<'src>, pub(crate) arguments: Vec>, } impl<'src> Display for UnresolvedDependency<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { if self.arguments.is_empty() { write!(f, "{}", self.recipe) } else { write!(f, "({}", self.recipe)?; for argument in &self.arguments { write!(f, " {argument}")?; } write!(f, ")") } } } just-1.21.0/src/unresolved_recipe.rs000064400000000000000000000031731046102023000155500ustar 00000000000000use super::*; pub(crate) type UnresolvedRecipe<'src> = Recipe<'src, UnresolvedDependency<'src>>; impl<'src> UnresolvedRecipe<'src> { pub(crate) fn resolve( self, resolved: Vec>>, ) -> CompileResult<'src, Recipe<'src>> { assert_eq!( self.dependencies.len(), resolved.len(), "UnresolvedRecipe::resolve: dependency count not equal to resolved count: {} != {}", self.dependencies.len(), resolved.len() ); for (unresolved, resolved) in self.dependencies.iter().zip(&resolved) { assert_eq!(unresolved.recipe.lexeme(), resolved.name.lexeme()); if !resolved .argument_range() .contains(&unresolved.arguments.len()) { return Err( unresolved .recipe .error(CompileErrorKind::DependencyArgumentCountMismatch { dependency: unresolved.recipe.lexeme(), found: unresolved.arguments.len(), min: resolved.min_arguments(), max: resolved.max_arguments(), }), ); } } let dependencies = self .dependencies .into_iter() .zip(resolved) .map(|(unresolved, resolved)| Dependency { recipe: resolved, arguments: unresolved.arguments, }) .collect(); Ok(Recipe { attributes: self.attributes, body: self.body, dependencies, depth: self.depth, doc: self.doc, name: self.name, parameters: self.parameters, path: self.path, priors: self.priors, private: self.private, quiet: self.quiet, shebang: self.shebang, }) } } just-1.21.0/src/use_color.rs000064400000000000000000000001411046102023000140150ustar 00000000000000#[derive(Copy, Clone, Debug, PartialEq)] pub(crate) enum UseColor { Auto, Always, Never, } just-1.21.0/src/variables.rs000064400000000000000000000043201046102023000137760ustar 00000000000000use super::*; pub(crate) struct Variables<'expression, 'src> { stack: Vec<&'expression Expression<'src>>, } impl<'expression, 'src> Variables<'expression, 'src> { pub(crate) fn new(root: &'expression Expression<'src>) -> Variables<'expression, 'src> { Variables { stack: vec![root] } } } impl<'expression, 'src> Iterator for Variables<'expression, 'src> { type Item = Token<'src>; fn next(&mut self) -> Option> { loop { match self.stack.pop()? { Expression::StringLiteral { .. } | Expression::Backtick { .. } => {} Expression::Call { thunk } => match thunk { Thunk::Nullary { .. } => {} Thunk::Unary { arg, .. } => self.stack.push(arg), Thunk::UnaryOpt { args: (a, opt_b), .. } => { self.stack.push(a); if let Some(b) = opt_b.as_ref() { self.stack.push(b); } } Thunk::Binary { args, .. } => { for arg in args.iter().rev() { self.stack.push(arg); } } Thunk::BinaryPlus { args: ([a, b], rest), .. } => { let first: &[&Expression] = &[a, b]; for arg in first.iter().copied().chain(rest).rev() { self.stack.push(arg); } } Thunk::Ternary { args, .. } => { for arg in args.iter().rev() { self.stack.push(arg); } } }, Expression::Conditional { lhs, rhs, then, otherwise, .. } => { self.stack.push(otherwise); self.stack.push(then); self.stack.push(rhs); self.stack.push(lhs); } Expression::Variable { name, .. } => return Some(name.token()), Expression::Concatenation { lhs, rhs } => { self.stack.push(rhs); self.stack.push(lhs); } Expression::Join { lhs, rhs } => { self.stack.push(rhs); if let Some(lhs) = lhs { self.stack.push(lhs); } } Expression::Group { contents } => { self.stack.push(contents); } } } } } just-1.21.0/src/verbosity.rs000064400000000000000000000014631046102023000140610ustar 00000000000000use Verbosity::*; #[derive(Copy, Clone, Debug, PartialEq)] pub(crate) enum Verbosity { Quiet, Taciturn, Loquacious, Grandiloquent, } impl Verbosity { pub(crate) fn from_flag_occurrences(flag_occurrences: u64) -> Self { match flag_occurrences { 0 => Taciturn, 1 => Loquacious, _ => Grandiloquent, } } pub(crate) fn quiet(self) -> bool { matches!(self, Quiet) } pub(crate) fn loud(self) -> bool { !self.quiet() } pub(crate) fn loquacious(self) -> bool { match self { Quiet | Taciturn => false, Loquacious | Grandiloquent => true, } } pub(crate) fn grandiloquent(self) -> bool { match self { Quiet | Taciturn | Loquacious => false, Grandiloquent => true, } } pub const fn default() -> Self { Taciturn } } just-1.21.0/src/warning.rs000064400000000000000000000015671046102023000135050ustar 00000000000000use super::*; #[derive(Clone, Debug, PartialEq)] pub(crate) enum Warning {} impl Warning { #[allow(clippy::unused_self)] fn context(&self) -> Option<&Token> { None } } impl ColorDisplay for Warning { fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result { let warning = color.warning(); let message = color.message(); write!(f, "{} {}", warning.paint("warning:"), message.prefix())?; write!(f, "{}", message.suffix())?; if let Some(token) = self.context() { writeln!(f)?; write!(f, "{}", token.color_display(color))?; } Ok(()) } } impl Serialize for Warning { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut map = serializer.serialize_map(None)?; map.serialize_entry("message", &self.color_display(Color::never()).to_string())?; map.end() } } just-1.21.0/tests/allow_duplicate_recipes.rs000064400000000000000000000010651046102023000172660ustar 00000000000000use super::*; #[test] fn allow_duplicate_recipes() { Test::new() .justfile( " b: echo foo b: echo bar set allow-duplicate-recipes ", ) .stdout("bar\n") .stderr("echo bar\n") .run(); } #[test] fn allow_duplicate_recipes_with_args() { Test::new() .justfile( " b a: echo foo b c d: echo bar {{c}} {{d}} set allow-duplicate-recipes ", ) .args(["b", "one", "two"]) .stdout("bar one two\n") .stderr("echo bar one two\n") .run(); } just-1.21.0/tests/assert_stdout.rs000064400000000000000000000002651046102023000153100ustar 00000000000000use super::*; pub(crate) fn assert_stdout(output: &std::process::Output, stdout: &str) { assert_success(output); assert_eq!(String::from_utf8_lossy(&output.stdout), stdout); } just-1.21.0/tests/assert_success.rs000064400000000000000000000004211046102023000154300ustar 00000000000000pub(crate) fn assert_success(output: &std::process::Output) { if !output.status.success() { eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr)); eprintln!("stdout: {}", String::from_utf8_lossy(&output.stdout)); panic!("{}", output.status); } } just-1.21.0/tests/assignment.rs000064400000000000000000000010521046102023000145500ustar 00000000000000use super::*; test! { name: set_export_parse_error, justfile: " set export := fals ", stdout: "", stderr: " error: Expected keyword `true` or `false` but found identifier `fals` | 1 | set export := fals | ^^^^ ", status: EXIT_FAILURE, } test! { name: set_export_parse_error_EOL, justfile: " set export := fals ", stdout: "", stderr: " error: Expected keyword `true` or `false` but found `end of line` | 1 | set export := | ^ ", status: EXIT_FAILURE, }just-1.21.0/tests/attributes.rs000064400000000000000000000033321046102023000145710ustar 00000000000000use super::*; #[test] fn all() { Test::new() .justfile( " [macos] [windows] [linux] [unix] [no-exit-message] foo: exit 1 ", ) .stderr("exit 1\n") .status(1) .run(); } #[test] fn duplicate_attributes_are_disallowed() { Test::new() .justfile( " [no-exit-message] [no-exit-message] foo: echo bar ", ) .stderr( " error: Recipe attribute `no-exit-message` first used on line 1 is duplicated on line 2 ——▶ justfile:2:2 │ 2 │ [no-exit-message] │ ^^^^^^^^^^^^^^^ ", ) .status(1) .run(); } #[test] fn multiple_attributes_one_line() { Test::new() .justfile( " [macos, windows,linux] [no-exit-message] foo: exit 1 ", ) .stderr("exit 1\n") .status(1) .run(); } #[test] fn multiple_attributes_one_line_error_message() { Test::new() .justfile( " [macos, windows linux] [no-exit-message] foo: exit 1 ", ) .stderr( " error: Expected ']' or ',', but found identifier ——▶ justfile:1:17 │ 1 │ [macos, windows linux] │ ^^^^^ ", ) .status(1) .run(); } #[test] fn multiple_attributes_one_line_duplicate_check() { Test::new() .justfile( " [macos, windows, linux] [linux] foo: exit 1 ", ) .stderr( " error: Recipe attribute `linux` first used on line 1 is duplicated on line 2 ——▶ justfile:2:2 │ 2 │ [linux] │ ^^^^^ ", ) .status(1) .run(); } just-1.21.0/tests/byte_order_mark.rs000064400000000000000000000016651046102023000155620ustar 00000000000000use super::*; #[test] fn ignore_leading_byte_order_mark() { Test::new() .justfile( " \u{feff}foo: echo bar ", ) .stderr("echo bar\n") .stdout("bar\n") .run(); } #[test] fn non_leading_byte_order_mark_produces_error() { Test::new() .justfile( " foo: echo bar \u{feff} ", ) .stderr( " error: Expected \'@\', \'[\', comment, end of file, end of line, or identifier, but found byte order mark ——▶ justfile:3:1 │ 3 │ \u{feff} │ ^ ") .status(EXIT_FAILURE) .run(); } #[test] fn dont_mention_byte_order_mark_in_errors() { Test::new() .justfile("{") .stderr( " error: Expected '@', '[', comment, end of file, end of line, or identifier, but found '{' ——▶ justfile:1:1 │ 1 │ { │ ^ ", ) .status(EXIT_FAILURE) .run(); } just-1.21.0/tests/changelog.rs000064400000000000000000000002361046102023000143320ustar 00000000000000use super::*; #[test] fn print_changelog() { Test::new() .args(["--changelog"]) .stdout(fs::read_to_string("CHANGELOG.md").unwrap()) .run(); } just-1.21.0/tests/choose.rs000064400000000000000000000075211046102023000136670ustar 00000000000000use super::*; #[test] fn env() { Test::new() .arg("--choose") .env("JUST_CHOOSER", "head -n1") .justfile( " foo: echo foo bar: echo bar ", ) .stderr("echo bar\n") .stdout("bar\n") .run(); } #[test] fn chooser() { Test::new() .arg("--choose") .arg("--chooser") .arg("head -n1") .justfile( " foo: echo foo bar: echo bar ", ) .stderr("echo bar\n") .stdout("bar\n") .run(); } #[test] fn override_variable() { Test::new() .arg("--choose") .arg("baz=B") .env("JUST_CHOOSER", "head -n1") .justfile( " baz := 'A' foo: echo foo bar: echo {{baz}} ", ) .stderr("echo B\n") .stdout("B\n") .run(); } #[test] fn skip_private_recipes() { Test::new() .arg("--choose") .env("JUST_CHOOSER", "head -n1") .justfile( " foo: echo foo _bar: echo bar ", ) .stderr("echo foo\n") .stdout("foo\n") .run(); } #[test] fn skip_recipes_that_require_arguments() { Test::new() .arg("--choose") .env("JUST_CHOOSER", "head -n1") .justfile( " foo: echo foo bar BAR: echo {{BAR}} ", ) .stderr("echo foo\n") .stdout("foo\n") .run(); } #[test] fn no_choosable_recipes() { Test::new() .arg("--choose") .justfile( " _foo: echo foo bar BAR: echo {{BAR}} ", ) .status(EXIT_FAILURE) .stderr("error: Justfile contains no choosable recipes.\n") .stdout("") .run(); } #[test] #[ignore] fn multiple_recipes() { Test::new() .arg("--choose") .arg("--chooser") .arg("echo foo bar") .justfile( " foo: echo foo bar: echo bar ", ) .stderr("echo foo\necho bar\n") .stdout("foo\nbar\n") .run(); } #[test] fn invoke_error_function() { Test::new() .justfile( " foo: echo foo bar: echo bar ", ) .stderr_regex( r#"error: Chooser `/ -cu fzf --multi --preview 'just --unstable --color always --justfile ".*justfile" --show \{\}'` invocation failed: .*\n"#, ) .status(EXIT_FAILURE) .shell(false) .args(["--shell", "/", "--choose"]) .run(); } #[test] #[cfg(not(windows))] fn status_error() { let tmp = temptree! { justfile: "foo:\n echo foo\nbar:\n echo bar\n", "exit-2": "#!/usr/bin/env bash\nexit 2\n", }; ("chmod", "+x", tmp.path().join("exit-2")).run(); let path = env::join_paths( iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())), ) .unwrap(); let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--choose") .arg("--chooser") .arg("exit-2") .env("PATH", path) .output() .unwrap(); assert!( Regex::new("^error: Chooser `exit-2` failed: exit (code|status): 2\n$") .unwrap() .is_match(str::from_utf8(&output.stderr).unwrap()) ); assert_eq!(output.status.code().unwrap(), 2); } #[test] fn default() { let tmp = temptree! { justfile: "foo:\n echo foo\n", }; let cat = which("cat").unwrap(); let fzf = tmp.path().join(format!("fzf{EXE_SUFFIX}")); #[cfg(unix)] std::os::unix::fs::symlink(cat, fzf).unwrap(); #[cfg(windows)] std::os::windows::fs::symlink_file(cat, fzf).unwrap(); let path = env::join_paths( iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())), ) .unwrap(); let output = Command::new(executable_path("just")) .arg("--choose") .arg("--chooser=fzf") .current_dir(tmp.path()) .env("PATH", path) .output() .unwrap(); assert_stdout(&output, "foo\n"); } just-1.21.0/tests/command.rs000064400000000000000000000055451046102023000140310ustar 00000000000000use super::*; test! { name: long, justfile: " x: echo XYZ ", args: ("--command", "printf", "foo"), stdout: "foo", } test! { name: short, justfile: " x: echo XYZ ", args: ("-c", "printf", "foo"), stdout: "foo", } test! { name: command_color, justfile: " x: echo XYZ ", args: ("--color", "always", "--command-color", "cyan"), stdout: "XYZ\n", stderr: "\u{1b}[1;36mecho XYZ\u{1b}[0m\n", status: EXIT_SUCCESS, } test! { name: no_binary, justfile: " x: echo XYZ ", args: ("--command"), stderr: &format!(" error: The argument '--command ' requires a value but none was supplied USAGE: just{EXE_SUFFIX} --color --dump-format --shell \ <--changelog|--choose|--command |--completions |--dump|--edit|\ --evaluate|--fmt|--init|--list|--show |--summary|--variables> For more information try --help "), status: EXIT_FAILURE, } test! { name: env_is_loaded, justfile: " set dotenv-load x: echo XYZ ", args: ("--command", "sh", "-c", "printf $DOTENV_KEY"), stdout: "dotenv-value", } test! { name: exports_are_available, justfile: " export FOO := 'bar' x: echo XYZ ", args: ("--command", "sh", "-c", "printf $FOO"), stdout: "bar", } test! { name: set_overrides_work, justfile: " export FOO := 'bar' x: echo XYZ ", args: ("--set", "FOO", "baz", "--command", "sh", "-c", "printf $FOO"), stdout: "baz", } test! { name: run_in_shell, justfile: " set shell := ['printf'] ", args: ("--shell-command", "--command", "bar baz"), stdout: "bar baz", shell: false, } test! { name: exit_status, justfile: " x: echo XYZ ", args: ("--command", "false"), stderr_regex: "error: Command `false` failed: exit (code|status): 1\n", status: EXIT_FAILURE, } #[test] fn working_directory_is_correct() { let tmp = tempdir(); fs::write(tmp.path().join("justfile"), "").unwrap(); fs::write(tmp.path().join("bar"), "baz").unwrap(); fs::create_dir(tmp.path().join("foo")).unwrap(); let output = Command::new(executable_path("just")) .args(["--command", "cat", "bar"]) .current_dir(tmp.path().join("foo")) .output() .unwrap(); assert_eq!(str::from_utf8(&output.stderr).unwrap(), ""); assert!(output.status.success()); assert_eq!(str::from_utf8(&output.stdout).unwrap(), "baz"); } #[test] fn command_not_found() { let tmp = tempdir(); fs::write(tmp.path().join("justfile"), "").unwrap(); let output = Command::new(executable_path("just")) .args(["--command", "asdfasdfasdfasdfadfsadsfadsf", "bar"]) .output() .unwrap(); assert!(str::from_utf8(&output.stderr) .unwrap() .starts_with("error: Failed to invoke `asdfasdfasdfasdfadfsadsfadsf` `bar`:")); assert!(!output.status.success()); } just-1.21.0/tests/completions/just.bash000075500000000000000000000027711046102023000162260ustar 00000000000000#!/usr/bin/env bash # --- Shared functions --- reply_equals() { local reply=$(declare -p COMPREPLY) local expected=$1 if [ "$reply" = "$expected" ]; then echo "${FUNCNAME[1]}: ok" else exit_code=1 echo >&2 "${FUNCNAME[1]}: failed! Completion for \`${COMP_WORDS[*]}\` does not match." echo diff -U3 --label expected <(echo "$expected") --label actual <(echo "$reply") >&2 echo fi } # --- Initial Setup --- source ./completions/just.bash cd tests/completions cargo build PATH="$(git rev-parse --show-toplevel)/target/debug:$PATH" exit_code=0 # --- Tests --- test_complete_all_recipes() { COMP_WORDS=(just) COMP_CWORD=1 _just just reply_equals 'declare -a COMPREPLY=([0]="deploy" [1]="install" [2]="publish" [3]="push" [4]="test")' } test_complete_all_recipes test_complete_recipes_starting_with_i() { COMP_WORDS=(just i) COMP_CWORD=1 _just just reply_equals 'declare -a COMPREPLY=([0]="install")' } test_complete_recipes_starting_with_i test_complete_recipes_starting_with_p() { COMP_WORDS=(just p) COMP_CWORD=1 _just just reply_equals 'declare -a COMPREPLY=([0]="publish" [1]="push")' } test_complete_recipes_starting_with_p test_complete_recipes_from_subdirs() { COMP_WORDS=(just subdir/) COMP_CWORD=1 _just just reply_equals 'declare -a COMPREPLY=([0]="subdir/special" [1]="subdir/surprise")' } test_complete_recipes_from_subdirs # --- Conclusion --- if [ $exit_code = 0 ]; then echo "All tests passed." else echo "Some test[s] failed." fi exit $exit_code just-1.21.0/tests/completions/justfile000075500000000000000000000000461046102023000161430ustar 00000000000000install: test: deploy: push: publish: just-1.21.0/tests/completions/subdir/justfile000075500000000000000000000000231046102023000174260ustar 00000000000000special: surprise: just-1.21.0/tests/completions.rs000064400000000000000000000005501046102023000147360ustar 00000000000000use super::*; #[test] fn output() { let tempdir = tempdir(); let output = Command::new(executable_path("just")) .arg("--completions") .arg("bash") .current_dir(tempdir.path()) .output() .unwrap(); assert!(output.status.success()); let text = String::from_utf8_lossy(&output.stdout); assert!(text.starts_with("_just() {")); } just-1.21.0/tests/conditional.rs000064400000000000000000000074201046102023000147100ustar 00000000000000use super::*; test! { name: then_branch_unevaluated, justfile: " foo: echo {{ if 'a' == 'b' { `exit 1` } else { 'otherwise' } }} ", stdout: "otherwise\n", stderr: "echo otherwise\n", } test! { name: otherwise_branch_unevaluated, justfile: " foo: echo {{ if 'a' == 'a' { 'then' } else { `exit 1` } }} ", stdout: "then\n", stderr: "echo then\n", } test! { name: otherwise_branch_unevaluated_inverted, justfile: " foo: echo {{ if 'a' != 'b' { 'then' } else { `exit 1` } }} ", stdout: "then\n", stderr: "echo then\n", } test! { name: then_branch_unevaluated_inverted, justfile: " foo: echo {{ if 'a' != 'a' { `exit 1` } else { 'otherwise' } }} ", stdout: "otherwise\n", stderr: "echo otherwise\n", } test! { name: complex_expressions, justfile: " foo: echo {{ if 'a' + 'b' == `echo ab` { 'c' + 'd' } else { 'e' + 'f' } }} ", stdout: "cd\n", stderr: "echo cd\n", } test! { name: undefined_lhs, justfile: " a := if b == '' { '' } else { '' } foo: echo {{ a }} ", stdout: "", stderr: " error: Variable `b` not defined ——▶ justfile:1:9 │ 1 │ a := if b == '' { '' } else { '' } │ ^ ", status: EXIT_FAILURE, } test! { name: undefined_rhs, justfile: " a := if '' == b { '' } else { '' } foo: echo {{ a }} ", stdout: "", stderr: " error: Variable `b` not defined ——▶ justfile:1:15 │ 1 │ a := if '' == b { '' } else { '' } │ ^ ", status: EXIT_FAILURE, } test! { name: undefined_then, justfile: " a := if '' == '' { b } else { '' } foo: echo {{ a }} ", stdout: "", stderr: " error: Variable `b` not defined ——▶ justfile:1:20 │ 1 │ a := if '' == '' { b } else { '' } │ ^ ", status: EXIT_FAILURE, } test! { name: undefined_otherwise, justfile: " a := if '' == '' { '' } else { b } foo: echo {{ a }} ", stdout: "", stderr: " error: Variable `b` not defined ——▶ justfile:1:32 │ 1 │ a := if '' == '' { '' } else { b } │ ^ ", status: EXIT_FAILURE, } test! { name: unexpected_op, justfile: " a := if '' a '' { '' } else { b } foo: echo {{ a }} ", stdout: "", stderr: " error: Expected '!=', '==', '=~', '+', or '/', but found identifier ——▶ justfile:1:12 │ 1 │ a := if '' a '' { '' } else { b } │ ^ ", status: EXIT_FAILURE, } test! { name: dump, justfile: " a := if '' == '' { '' } else { '' } foo: echo {{ a }} ", args: ("--dump"), stdout: " a := if '' == '' { '' } else { '' } foo: echo {{ a }} ", } test! { name: if_else, justfile: " x := if '0' == '1' { 'a' } else if '0' == '0' { 'b' } else { 'c' } foo: echo {{ x }} ", stdout: "b\n", stderr: "echo b\n", } test! { name: missing_else, justfile: " TEST := if path_exists('/bin/bash') == 'true' {'yes'} ", stdout: "", stderr: " error: Expected keyword `else` but found `end of line` ——▶ justfile:1:54 │ 1 │ TEST := if path_exists('/bin/bash') == 'true' {'yes'} │ ^ ", status: EXIT_FAILURE, } test! { name: incorrect_else_identifier, justfile: " TEST := if path_exists('/bin/bash') == 'true' {'yes'} els {'no'} ", stdout: "", stderr: " error: Expected keyword `else` but found identifier `els` ——▶ justfile:1:55 │ 1 │ TEST := if path_exists('/bin/bash') == 'true' {'yes'} els {'no'} │ ^^^ ", status: EXIT_FAILURE, } just-1.21.0/tests/confirm.rs000064400000000000000000000040511046102023000140370ustar 00000000000000use super::*; #[test] fn confirm_recipe_arg() { Test::new() .arg("--yes") .justfile( " [confirm] requires_confirmation: echo confirmed ", ) .stderr("echo confirmed\n") .stdout("confirmed\n") .run(); } #[test] fn recipe_with_confirm_recipe_dependency_arg() { Test::new() .arg("--yes") .justfile( " dep_confirmation: requires_confirmation echo confirmed2 [confirm] requires_confirmation: echo confirmed ", ) .stderr("echo confirmed\necho confirmed2\n") .stdout("confirmed\nconfirmed2\n") .run(); } #[test] fn confirm_recipe() { Test::new() .justfile( " [confirm] requires_confirmation: echo confirmed ", ) .stderr("Run recipe `requires_confirmation`? echo confirmed\n") .stdout("confirmed\n") .stdin("y") .run(); } #[test] fn recipe_with_confirm_recipe_dependency() { Test::new() .justfile( " dep_confirmation: requires_confirmation echo confirmed2 [confirm] requires_confirmation: echo confirmed ", ) .stderr("Run recipe `requires_confirmation`? echo confirmed\necho confirmed2\n") .stdout("confirmed\nconfirmed2\n") .stdin("y") .run(); } #[test] fn do_not_confirm_recipe() { Test::new() .justfile( " [confirm] requires_confirmation: echo confirmed ", ) .stderr("Run recipe `requires_confirmation`? error: Recipe `requires_confirmation` was not confirmed\n") .stdout("") .status(1) .run(); } #[test] fn do_not_confirm_recipe_with_confirm_recipe_dependency() { Test::new() .justfile( " dep_confirmation: requires_confirmation echo mistake [confirm] requires_confirmation: echo confirmed ", ) .stderr("Run recipe `requires_confirmation`? error: Recipe `requires_confirmation` was not confirmed\n") .status(1) .run(); } just-1.21.0/tests/delimiters.rs000064400000000000000000000027361046102023000145530ustar 00000000000000use super::*; test! { name: mismatched_delimiter, justfile: "(]", stderr: " error: Mismatched closing delimiter `]`. (Did you mean to close the `(` on line 1?) ——▶ justfile:1:2 │ 1 │ (] │ ^ ", status: EXIT_FAILURE, } test! { name: unexpected_delimiter, justfile: "]", stderr: " error: Unexpected closing delimiter `]` ——▶ justfile:1:1 │ 1 │ ] │ ^ ", status: EXIT_FAILURE, } test! { name: paren_continuation, justfile: " x := ( 'a' + 'b' ) foo: echo {{x}} ", stdout: "ab\n", stderr: "echo ab\n", } test! { name: brace_continuation, justfile: " x := if '' == '' { 'a' } else { 'b' } foo: echo {{x}} ", stdout: "a\n", stderr: "echo a\n", } test! { name: bracket_continuation, justfile: " set shell := [ 'sh', '-cu', ] foo: echo foo ", stdout: "foo\n", stderr: "echo foo\n", } test! { name: dependency_continuation, justfile: " foo: ( bar 'bar' ) echo foo bar x: echo {{x}} ", stdout: "bar\nfoo\n", stderr: "echo bar\necho foo\n", } test! { name: no_interpolation_continuation, justfile: " foo: echo {{ ( 'a' + 'b')}} ", stdout: "", stderr: " error: Unterminated interpolation ——▶ justfile:2:8 │ 2 │ echo {{ ( │ ^^ ", status: EXIT_FAILURE, } just-1.21.0/tests/dotenv.rs000064400000000000000000000103031046102023000136760ustar 00000000000000use super::*; #[test] fn dotenv() { Test::new() .write(".env", "KEY=ROOT") .write("sub/.env", "KEY=SUB") .write("sub/justfile", "default:\n\techo KEY=${KEY:-unset}") .args(["sub/default"]) .stdout("KEY=unset\n") .stderr("echo KEY=${KEY:-unset}\n") .run(); } test! { name: set_false, justfile: r#" set dotenv-load := false foo: if [ -n "${DOTENV_KEY+1}" ]; then echo defined; else echo undefined; fi "#, stdout: "undefined\n", stderr: "if [ -n \"${DOTENV_KEY+1}\" ]; then echo defined; else echo undefined; fi\n", } test! { name: set_implicit, justfile: r#" set dotenv-load foo: echo $DOTENV_KEY "#, stdout: "dotenv-value\n", stderr: "echo $DOTENV_KEY\n", } test! { name: set_true, justfile: r#" set dotenv-load := true foo: echo $DOTENV_KEY "#, stdout: "dotenv-value\n", stderr: "echo $DOTENV_KEY\n", } #[test] fn no_warning() { Test::new() .justfile( " foo: echo ${DOTENV_KEY:-unset} ", ) .stdout("unset\n") .stderr("echo ${DOTENV_KEY:-unset}\n") .run(); } #[test] fn path_not_found() { Test::new() .justfile( " foo: echo $NAME ", ) .args(["--dotenv-path", ".env.prod"]) .stderr(if cfg!(windows) { "error: Failed to load environment file: The system cannot find the file specified. (os \ error 2)\n" } else { "error: Failed to load environment file: No such file or directory (os error 2)\n" }) .status(EXIT_FAILURE) .run(); } #[test] fn path_resolves() { Test::new() .justfile( " foo: @echo $NAME ", ) .tree(tree! { subdir: { ".env": "NAME=bar" } }) .args(["--dotenv-path", "subdir/.env"]) .stdout("bar\n") .status(EXIT_SUCCESS) .run(); } #[test] fn filename_resolves() { Test::new() .justfile( " foo: @echo $NAME ", ) .tree(tree! { ".env.special": "NAME=bar" }) .args(["--dotenv-filename", ".env.special"]) .stdout("bar\n") .status(EXIT_SUCCESS) .run(); } #[test] fn filename_flag_overwrites_no_load() { Test::new() .justfile( " set dotenv-load := false foo: @echo $NAME ", ) .tree(tree! { ".env.special": "NAME=bar" }) .args(["--dotenv-filename", ".env.special"]) .stdout("bar\n") .status(EXIT_SUCCESS) .run(); } #[test] fn path_flag_overwrites_no_load() { Test::new() .justfile( " set dotenv-load := false foo: @echo $NAME ", ) .tree(tree! { subdir: { ".env": "NAME=bar" } }) .args(["--dotenv-path", "subdir/.env"]) .stdout("bar\n") .status(EXIT_SUCCESS) .run(); } #[test] fn can_set_dotenv_filename_from_justfile() { Test::new() .justfile( r#" set dotenv-filename := ".env.special" foo: @echo $NAME "#, ) .tree(tree! { ".env.special": "NAME=bar" }) .stdout("bar\n") .status(EXIT_SUCCESS) .run(); } #[test] fn can_set_dotenv_path_from_justfile() { Test::new() .justfile( r#" set dotenv-path:= "subdir/.env" foo: @echo $NAME "#, ) .tree(tree! { subdir: { ".env": "NAME=bar" } }) .stdout("bar\n") .status(EXIT_SUCCESS) .run(); } #[test] fn program_argument_has_priority_for_dotenv_filename() { Test::new() .justfile( r#" set dotenv-filename := ".env.special" foo: @echo $NAME "#, ) .tree(tree! { ".env.special": "NAME=bar", ".env.superspecial": "NAME=baz" }) .args(["--dotenv-filename", ".env.superspecial"]) .stdout("baz\n") .status(EXIT_SUCCESS) .run(); } #[test] fn program_argument_has_priority_for_dotenv_path() { Test::new() .justfile( r#" set dotenv-path:= "subdir/.env" foo: @echo $NAME "#, ) .tree(tree! { subdir: { ".env": "NAME=bar", ".env.special": "NAME=baz" } }) .args(["--dotenv-path", "subdir/.env.special"]) .stdout("baz\n") .status(EXIT_SUCCESS) .run(); } just-1.21.0/tests/edit.rs000064400000000000000000000075301046102023000133340ustar 00000000000000use super::*; const JUSTFILE: &str = "Yooooooo, hopefully this never becomes valid syntax."; /// Test that --edit doesn't require a valid justfile #[test] fn invalid_justfile() { let tmp = temptree! { justfile: JUSTFILE, }; let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .output() .unwrap(); assert!(!output.status.success()); let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--edit") .env("VISUAL", "cat") .output() .unwrap(); assert_stdout(&output, JUSTFILE); } #[test] fn invoke_error() { let tmp = temptree! { justfile: JUSTFILE, }; let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .output() .unwrap(); assert!(!output.status.success()); let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--edit") .env("VISUAL", "/") .output() .unwrap(); assert_eq!( String::from_utf8_lossy(&output.stderr), if cfg!(windows) { "error: Editor `/` invocation failed: program path has no file name\n" } else { "error: Editor `/` invocation failed: Permission denied (os error 13)\n" } ); } #[test] #[cfg(not(windows))] fn status_error() { let tmp = temptree! { justfile: JUSTFILE, "exit-2": "#!/usr/bin/env bash\nexit 2\n", }; ("chmod", "+x", tmp.path().join("exit-2")).run(); let path = env::join_paths( iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())), ) .unwrap(); let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--edit") .env("PATH", path) .env("VISUAL", "exit-2") .output() .unwrap(); assert!( Regex::new("^error: Editor `exit-2` failed: exit (code|status): 2\n$") .unwrap() .is_match(str::from_utf8(&output.stderr).unwrap()) ); assert_eq!(output.status.code().unwrap(), 2); } /// Test that editor is $VISUAL, $EDITOR, or "vim" in that order #[test] fn editor_precedence() { let tmp = temptree! { justfile: JUSTFILE, }; let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--edit") .env("VISUAL", "cat") .env("EDITOR", "this-command-doesnt-exist") .output() .unwrap(); assert_stdout(&output, JUSTFILE); let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--edit") .env_remove("VISUAL") .env("EDITOR", "cat") .output() .unwrap(); assert_stdout(&output, JUSTFILE); let cat = which("cat").unwrap(); let vim = tmp.path().join(format!("vim{EXE_SUFFIX}")); #[cfg(unix)] std::os::unix::fs::symlink(cat, vim).unwrap(); #[cfg(windows)] std::os::windows::fs::symlink_file(cat, vim).unwrap(); let path = env::join_paths( iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())), ) .unwrap(); let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--edit") .env("PATH", path) .env_remove("VISUAL") .env_remove("EDITOR") .output() .unwrap(); assert_stdout(&output, JUSTFILE); } /// Test that editor working directory is the same as edited justfile #[cfg(unix)] #[test] fn editor_working_directory() { let tmp = temptree! { justfile: JUSTFILE, child: {}, editor: "#!/usr/bin/env sh\ncat $1\npwd", }; let editor = tmp.path().join("editor"); let permissions = std::os::unix::fs::PermissionsExt::from_mode(0o700); fs::set_permissions(&editor, permissions).unwrap(); let output = Command::new(executable_path("just")) .current_dir(tmp.path().join("child")) .arg("--edit") .env("VISUAL", &editor) .output() .unwrap(); let want = format!( "{JUSTFILE}{}\n", tmp.path().canonicalize().unwrap().display() ); assert_stdout(&output, &want); } just-1.21.0/tests/equals.rs000064400000000000000000000006001046102023000136700ustar 00000000000000use super::*; #[test] fn export_recipe() { Test::new() .justfile( " export foo='bar': echo {{foo}} ", ) .stdout("bar\n") .stderr("echo bar\n") .run(); } #[test] fn alias_recipe() { Test::new() .justfile( " alias foo='bar': echo {{foo}} ", ) .stdout("bar\n") .stderr("echo bar\n") .run(); } just-1.21.0/tests/error_messages.rs000064400000000000000000000042351046102023000154260ustar 00000000000000use super::*; test! { name: invalid_alias_attribute, justfile: "[private]\n[linux]\nalias t := test\n\ntest:\n", stderr: " error: Alias t has an invalid attribute `linux` ——▶ justfile:3:7 │ 3 │ alias t := test │ ^ ", status: EXIT_FAILURE, } test! { name: expected_keyword, justfile: "foo := if '' == '' { '' } arlo { '' }", stderr: " error: Expected keyword `else` but found identifier `arlo` ——▶ justfile:1:27 │ 1 │ foo := if '' == '' { '' } arlo { '' } │ ^^^^ ", status: EXIT_FAILURE, } test! { name: unexpected_character, justfile: "&~", stderr: " error: Expected character `&` ——▶ justfile:1:2 │ 1 │ &~ │ ^ ", status: EXIT_FAILURE, } #[test] fn argument_count_mismatch() { Test::new() .justfile("foo a b:") .args(["foo"]) .stderr( " error: Recipe `foo` got 0 arguments but takes 2 usage: just foo a b ", ) .status(EXIT_FAILURE) .run(); } #[test] fn file_path_is_indented_if_justfile_is_long() { Test::new() .justfile("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nfoo") .status(EXIT_FAILURE) .stderr( " error: Expected '*', ':', '$', identifier, or '+', but found end of file ——▶ justfile:20:4 │ 20 │ foo │ ^ ", ) .run(); } #[test] fn file_paths_are_relative() { Test::new() .justfile("import 'foo/bar.just'") .write("foo/bar.just", "baz") .status(EXIT_FAILURE) .stderr(format!( " error: Expected '*', ':', '$', identifier, or '+', but found end of file ——▶ foo{}bar.just:1:4 │ 1 │ baz │ ^ ", MAIN_SEPARATOR )) .run(); } #[test] #[cfg(not(windows))] fn file_paths_not_in_subdir_are_absolute() { Test::new() .write("foo/justfile", "import '../bar.just'") .write("bar.just", "baz") .no_justfile() .args(["--justfile", "foo/justfile"]) .status(EXIT_FAILURE) .stderr_regex( r"error: Expected '\*', ':', '\$', identifier, or '\+', but found end of file ——▶ /.*/bar.just:1:4 │ 1 │ baz │ \^ ", ) .run(); } just-1.21.0/tests/evaluate.rs000064400000000000000000000023211046102023000142060ustar 00000000000000use super::*; test! { name: evaluate, justfile: r#" foo := "a\t" hello := "c" bar := "b\t" ab := foo + bar + hello wut: touch /this/is/not/a/file "#, args: ("--evaluate"), stdout: r#"ab := "a b c" bar := "b " foo := "a " hello := "c" "#, } test! { name: evaluate_empty, justfile: " a := 'foo' ", args: ("--evaluate"), stdout: r#" a := "foo" "#, } test! { name: evaluate_multiple, justfile: " a := 'x' b := 'y' c := 'z' ", args: ("--evaluate", "a", "c"), stderr: "error: `--evaluate` used with unexpected argument: `c`\n", status: EXIT_FAILURE, } test! { name: evaluate_single_free, justfile: " a := 'x' b := 'y' c := 'z' ", args: ("--evaluate", "b"), stdout: "y", } test! { name: evaluate_no_suggestion, justfile: " abc := 'x' ", args: ("--evaluate", "aby"), stderr: " error: Justfile does not contain variable `aby`. Did you mean `abc`? ", status: EXIT_FAILURE, } test! { name: evaluate_suggestion, justfile: " hello := 'x' ", args: ("--evaluate", "goodbye"), stderr: " error: Justfile does not contain variable `goodbye`. ", status: EXIT_FAILURE, } just-1.21.0/tests/examples.rs000064400000000000000000000006101046102023000142150ustar 00000000000000use super::*; #[test] fn examples() { for result in fs::read_dir("examples").unwrap() { let entry = result.unwrap(); let path = entry.path(); println!("Parsing `{}`…", path.display()); let output = Command::new(executable_path("just")) .arg("--justfile") .arg(&path) .arg("--dump") .output() .unwrap(); assert_success(&output); } } just-1.21.0/tests/export.rs000064400000000000000000000062351046102023000137310ustar 00000000000000test! { name: success, justfile: r#" export FOO := "a" baz := "c" export BAR := "b" export ABC := FOO + BAR + baz wut: echo $FOO $BAR $ABC "#, stdout: "a b abc\n", stderr: "echo $FOO $BAR $ABC\n", } test! { name: parameter, justfile: r#" wut $FOO='a' BAR='b': echo $FOO echo {{BAR}} if [ -n "${BAR+1}" ]; then echo defined; else echo undefined; fi "#, stdout: "a\nb\nundefined\n", stderr: "echo $FOO\necho b\nif [ -n \"${BAR+1}\" ]; then echo defined; else echo undefined; fi\n", } test! { name: parameter_not_visible_to_backtick, justfile: r#" wut $FOO BAR=`if [ -n "${FOO+1}" ]; then echo defined; else echo undefined; fi`: echo $FOO echo {{BAR}} "#, args: ("wut", "bar"), stdout: "bar\nundefined\n", stderr: "echo $FOO\necho undefined\n", } test! { name: override_variable, justfile: r#" export FOO := "a" baz := "c" export BAR := "b" export ABC := FOO + "-" + BAR + "-" + baz wut: echo $FOO $BAR $ABC "#, args: ("--set", "BAR", "bye", "FOO=hello"), stdout: "hello bye hello-bye-c\n", stderr: "echo $FOO $BAR $ABC\n", } test! { name: shebang, justfile: r#" export FOO := "a" baz := "c" export BAR := "b" export ABC := FOO + BAR + baz wut: #!/bin/sh echo $FOO $BAR $ABC "#, stdout: "a b abc\n", } test! { name: recipe_backtick, justfile: r#" export EXPORTED_VARIABLE := "A-IS-A" recipe: echo {{`echo recipe $EXPORTED_VARIABLE`}} "#, stdout: "recipe A-IS-A\n", stderr: "echo recipe A-IS-A\n", } test! { name: setting_implicit, justfile: " set export A := 'hello' foo B C=`echo $A`: echo $A echo $B echo $C ", args: ("foo", "goodbye"), stdout: "hello\ngoodbye\nhello\n", stderr: "echo $A\necho $B\necho $C\n", } test! { name: setting_true, justfile: " set export := true A := 'hello' foo B C=`echo $A`: echo $A echo $B echo $C ", args: ("foo", "goodbye"), stdout: "hello\ngoodbye\nhello\n", stderr: "echo $A\necho $B\necho $C\n", } test! { name: setting_false, justfile: r#" set export := false A := 'hello' foo: if [ -n "${A+1}" ]; then echo defined; else echo undefined; fi "#, stdout: "undefined\n", stderr: "if [ -n \"${A+1}\" ]; then echo defined; else echo undefined; fi\n", } test! { name: setting_shebang, justfile: " set export A := 'hello' foo B: #!/bin/sh echo $A echo $B ", args: ("foo", "goodbye"), stdout: "hello\ngoodbye\n", stderr: "", } test! { name: setting_override_undefined, justfile: r#" set export A := 'hello' B := `if [ -n "${A+1}" ]; then echo defined; else echo undefined; fi` foo C='goodbye' D=`if [ -n "${C+1}" ]; then echo defined; else echo undefined; fi`: echo $B echo $D "#, args: ("A=zzz", "foo"), stdout: "undefined\nundefined\n", stderr: "echo $B\necho $D\n", } test! { name: setting_variable_not_visible, justfile: r#" export A := 'hello' export B := `if [ -n "${A+1}" ]; then echo defined; else echo undefined; fi` foo: echo $B "#, args: ("A=zzz"), stdout: "undefined\n", stderr: "echo $B\n", } just-1.21.0/tests/fallback.rs000064400000000000000000000127151046102023000141470ustar 00000000000000use super::*; #[test] fn fallback_from_subdir_bugfix() { Test::new() .write( "sub/justfile", unindent( " set fallback @default: echo foo ", ), ) .args(["sub/default"]) .stdout("foo\n") .run(); } #[test] fn fallback_from_subdir_message() { Test::new() .justfile("bar:\n echo bar") .write( "sub/justfile", unindent( " set fallback @foo: echo foo ", ), ) .args(["sub/bar"]) .stderr(path("echo bar\n")) .stdout("bar\n") .run(); } #[test] fn fallback_from_subdir_verbose_message() { Test::new() .justfile("bar:\n echo bar") .write( "sub/justfile", unindent( " set fallback @foo: echo foo ", ), ) .args(["--verbose", "sub/bar"]) .stderr(path( " Trying ../justfile ===> Running recipe `bar`... echo bar ", )) .stdout("bar\n") .run(); } #[test] fn runs_recipe_in_parent_if_not_found_in_current() { Test::new() .tree(tree! { bar: { justfile: " set fallback := true baz: echo subdir " } }) .justfile( " foo: echo root ", ) .args(["foo"]) .current_dir("bar") .stderr( " echo root ", ) .stdout("root\n") .run(); } #[test] fn setting_accepts_value() { Test::new() .tree(tree! { bar: { justfile: " set fallback := true baz: echo subdir " } }) .justfile( " foo: echo root ", ) .args(["foo"]) .current_dir("bar") .stderr( " echo root ", ) .stdout("root\n") .run(); } #[test] fn print_error_from_parent_if_recipe_not_found_in_current() { Test::new() .tree(tree! { bar: { justfile: " set fallback := true baz: echo subdir " } }) .justfile("foo:\n echo {{bar}}") .args(["foo"]) .current_dir("bar") .stderr( " error: Variable `bar` not defined ——▶ justfile:2:9 │ 2 │ echo {{bar}} │ ^^^ ", ) .status(EXIT_FAILURE) .run(); } #[test] fn requires_setting() { Test::new() .tree(tree! { bar: { justfile: " baz: echo subdir " } }) .justfile( " foo: echo root ", ) .args(["foo"]) .current_dir("bar") .status(EXIT_FAILURE) .stderr("error: Justfile does not contain recipe `foo`.\n") .run(); } #[test] fn works_with_provided_search_directory() { Test::new() .tree(tree! { bar: { justfile: " set fallback := true baz: echo subdir " } }) .justfile( " foo: echo root ", ) .args(["./foo"]) .stdout("root\n") .stderr( " echo root ", ) .current_dir("bar") .run(); } #[test] fn doesnt_work_with_justfile() { Test::new() .tree(tree! { bar: { justfile: " baz: echo subdir " } }) .justfile( " foo: echo root ", ) .args(["--justfile", "justfile", "foo"]) .current_dir("bar") .status(EXIT_FAILURE) .stderr("error: Justfile does not contain recipe `foo`.\n") .run(); } #[test] fn doesnt_work_with_justfile_and_working_directory() { Test::new() .tree(tree! { bar: { justfile: " baz: echo subdir " } }) .justfile( " foo: echo root ", ) .args(["--justfile", "justfile", "--working-directory", ".", "foo"]) .current_dir("bar") .status(EXIT_FAILURE) .stderr("error: Justfile does not contain recipe `foo`.\n") .run(); } #[test] fn prints_correct_error_message_when_recipe_not_found() { Test::new() .tree(tree! { bar: { justfile: " set fallback := true bar: echo subdir " } }) .justfile( " bar: echo root ", ) .args(["foo"]) .current_dir("bar") .status(EXIT_FAILURE) .stderr( " error: Justfile does not contain recipe `foo`. ", ) .run(); } #[test] fn multiple_levels_of_fallback_work() { Test::new() .tree(tree! { a: { b: { justfile: " set fallback := true foo: echo subdir " }, justfile: " set fallback := true bar: echo subdir " } }) .justfile( " baz: echo root ", ) .args(["baz"]) .current_dir("a/b") .stdout("root\n") .stderr( " echo root ", ) .run(); } #[test] fn stop_fallback_when_fallback_is_false() { Test::new() .tree(tree! { a: { b: { justfile: " set fallback := true foo: echo subdir " }, justfile: " bar: echo subdir " } }) .justfile( " baz: echo root ", ) .args(["baz"]) .current_dir("a/b") .stderr( " error: Justfile does not contain recipe `baz`. Did you mean `bar`? ", ) .status(EXIT_FAILURE) .run(); } just-1.21.0/tests/fmt.rs000064400000000000000000000342221046102023000131730ustar 00000000000000use super::*; test! { name: unstable_not_passed, justfile: "", args: ("--fmt"), stderr: " error: The `--fmt` command is currently unstable. \ Invoke `just` with the `--unstable` flag to enable unstable features. ", status: EXIT_FAILURE, } test! { name: check_without_fmt, justfile: "", args: ("--check"), stderr_regex: "error: The following required arguments were not provided: --fmt (.|\\n)+", status: EXIT_FAILURE, } test! { name: check_ok, justfile: r#" # comment with spaces export x := `backtick with lines` recipe: deps echo "$x" deps: echo {{ x }} echo '$x' "#, args: ("--unstable", "--fmt", "--check"), status: EXIT_SUCCESS, } test! { name: check_found_diff, justfile: "x:=``\n", args: ("--unstable", "--fmt", "--check"), stdout: " -x:=`` +x := `` ", stderr: " error: Formatted justfile differs from original. ", status: EXIT_FAILURE, } test! { name: check_found_diff_quiet, justfile: "x:=``\n", args: ("--unstable", "--fmt", "--check", "--quiet"), stderr: "", status: EXIT_FAILURE, } test! { name: check_diff_color, justfile: "x:=``\n", args: ("--unstable", "--fmt", "--check", "--color", "always"), stdout: " \u{1b}[31m-x:=`` \u{1b}[0m\u{1b}[32m+x := `` \u{1b}[0m", stderr: " \u{1b}[1;31merror\u{1b}[0m: \u{1b}[1mFormatted justfile differs from original.\u{1b}[0m ", status: EXIT_FAILURE, } #[test] fn unstable_passed() { let tmp = tempdir(); let justfile = tmp.path().join("justfile"); fs::write(&justfile, "x := 'hello' ").unwrap(); let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--fmt") .arg("--unstable") .output() .unwrap(); if !output.status.success() { eprintln!("{}", String::from_utf8_lossy(&output.stderr)); eprintln!("{}", String::from_utf8_lossy(&output.stdout)); panic!("justfile failed with status: {}", output.status); } assert_eq!(fs::read_to_string(&justfile).unwrap(), "x := 'hello'\n"); } #[test] fn write_error() { let tempdir = temptree! { justfile: "x := 'hello' ", }; let test = Test::with_tempdir(tempdir) .no_justfile() .args(["--fmt", "--unstable"]) .status(EXIT_FAILURE) .stderr_regex(if cfg!(windows) { r"error: Failed to write justfile to `.*`: Access is denied. \(os error 5\)\n" } else { r"error: Failed to write justfile to `.*`: Permission denied \(os error 13\)\n" }); let justfile_path = test.justfile_path(); ("chmod", "400", &justfile_path).run(); let _tempdir = test.run(); assert_eq!( fs::read_to_string(&justfile_path).unwrap(), "x := 'hello' " ); } test! { name: alias_good, justfile: " alias f := foo foo: echo foo ", args: ("--dump"), stdout: " alias f := foo foo: echo foo ", } test! { name: alias_fix_indent, justfile: " alias f:= foo foo: echo foo ", args: ("--dump"), stdout: " alias f := foo foo: echo foo ", } test! { name: assignment_singlequote, justfile: " foo := 'foo' ", args: ("--dump"), stdout: " foo := 'foo' ", } test! { name: assignment_doublequote, justfile: r#" foo := "foo" "#, args: ("--dump"), stdout: r#" foo := "foo" "#, } test! { name: assignment_indented_singlequote, justfile: " foo := ''' foo ''' ", args: ("--dump"), stdout: r" foo := ''' foo ''' ", } test! { name: assignment_indented_doublequote, justfile: r#" foo := """ foo """ "#, args: ("--dump"), stdout: r#" foo := """ foo """ "#, } test! { name: assignment_backtick, justfile: " foo := `foo` ", args: ("--dump"), stdout: " foo := `foo` ", } test! { name: assignment_indented_backtick, justfile: " foo := ``` foo ``` ", args: ("--dump"), stdout: " foo := ``` foo ``` ", } test! { name: assignment_name, justfile: " bar := 'bar' foo := bar ", args: ("--dump"), stdout: " bar := 'bar' foo := bar ", } test! { name: assignment_parenthesized_expression, justfile: " foo := ('foo') ", args: ("--dump"), stdout: " foo := ('foo') ", } test! { name: assignment_export, justfile: " export foo := 'foo' ", args: ("--dump"), stdout: " export foo := 'foo' ", } test! { name: assignment_concat_values, justfile: " foo := 'foo' + 'bar' ", args: ("--dump"), stdout: " foo := 'foo' + 'bar' ", } test! { name: assignment_if_oneline, justfile: " foo := if 'foo' == 'foo' { 'foo' } else { 'bar' } ", args: ("--dump"), stdout: " foo := if 'foo' == 'foo' { 'foo' } else { 'bar' } ", } test! { name: assignment_if_multiline, justfile: " foo := if 'foo' != 'foo' { 'foo' } else { 'bar' } ", args: ("--dump"), stdout: " foo := if 'foo' != 'foo' { 'foo' } else { 'bar' } ", } test! { name: assignment_nullary_function, justfile: " foo := arch() ", args: ("--dump"), stdout: " foo := arch() ", } test! { name: assignment_unary_function, justfile: " foo := env_var('foo') ", args: ("--dump"), stdout: " foo := env_var('foo') ", } test! { name: assignment_binary_function, justfile: " foo := env_var_or_default('foo', 'bar') ", args: ("--dump"), stdout: " foo := env_var_or_default('foo', 'bar') ", } test! { name: assignment_path_functions, justfile: " foo := without_extension('foo/bar.baz') foo2 := file_stem('foo/bar.baz') foo3 := parent_directory('foo/bar.baz') foo4 := file_name('foo/bar.baz') foo5 := extension('foo/bar.baz') ", args: ("--dump"), stdout: " foo := without_extension('foo/bar.baz') foo2 := file_stem('foo/bar.baz') foo3 := parent_directory('foo/bar.baz') foo4 := file_name('foo/bar.baz') foo5 := extension('foo/bar.baz') ", } test! { name: recipe_ordinary, justfile: " foo: echo bar ", args: ("--dump"), stdout: " foo: echo bar ", } test! { name: recipe_with_docstring, justfile: " # bar foo: echo bar ", args: ("--dump"), stdout: " # bar foo: echo bar ", } test! { name: recipe_with_comments_in_body, justfile: " foo: # bar echo bar ", args: ("--dump"), stdout: " foo: # bar echo bar ", } test! { name: recipe_body_is_comment, justfile: " foo: # bar ", args: ("--dump"), stdout: " foo: # bar ", } test! { name: recipe_several_commands, justfile: " foo: echo bar echo baz ", args: ("--dump"), stdout: " foo: echo bar echo baz ", } test! { name: recipe_quiet, justfile: " @foo: echo bar ", args: ("--dump"), stdout: " @foo: echo bar ", } test! { name: recipe_quiet_command, justfile: " foo: @echo bar ", args: ("--dump"), stdout: " foo: @echo bar ", } test! { name: recipe_quiet_comment, justfile: " foo: @# bar ", args: ("--dump"), stdout: " foo: @# bar ", } test! { name: recipe_ignore_errors, justfile: " foo: -echo foo ", args: ("--dump"), stdout: " foo: -echo foo ", } test! { name: recipe_parameter, justfile: " foo BAR: echo foo ", args: ("--dump"), stdout: " foo BAR: echo foo ", } test! { name: recipe_parameter_default, justfile: " foo BAR='bar': echo foo ", args: ("--dump"), stdout: " foo BAR='bar': echo foo ", } test! { name: recipe_parameter_envar, justfile: " foo $BAR: echo foo ", args: ("--dump"), stdout: " foo $BAR: echo foo ", } test! { name: recipe_parameter_default_envar, justfile: " foo $BAR='foo': echo foo ", args: ("--dump"), stdout: " foo $BAR='foo': echo foo ", } test! { name: recipe_parameter_concat, justfile: " foo BAR=('bar' + 'baz'): echo foo ", args: ("--dump"), stdout: " foo BAR=('bar' + 'baz'): echo foo ", } test! { name: recipe_parameters, justfile: " foo BAR BAZ: echo foo ", args: ("--dump"), stdout: " foo BAR BAZ: echo foo ", } test! { name: recipe_parameters_envar, justfile: " foo $BAR $BAZ: echo foo ", args: ("--dump"), stdout: " foo $BAR $BAZ: echo foo ", } test! { name: recipe_variadic_plus, justfile: " foo +BAR: echo foo ", args: ("--dump"), stdout: " foo +BAR: echo foo ", } test! { name: recipe_variadic_star, justfile: " foo *BAR: echo foo ", args: ("--dump"), stdout: " foo *BAR: echo foo ", } test! { name: recipe_positional_variadic, justfile: " foo BAR *BAZ: echo foo ", args: ("--dump"), stdout: " foo BAR *BAZ: echo foo ", } test! { name: recipe_variadic_default, justfile: " foo +BAR='bar': echo foo ", args: ("--dump"), stdout: " foo +BAR='bar': echo foo ", } test! { name: recipe_parameter_in_body, justfile: " foo BAR: echo {{ BAR }} ", args: ("--dump"), stdout: " foo BAR: echo {{ BAR }} ", } test! { name: recipe_parameter_conditional, justfile: " foo BAR: echo {{ if 'foo' == 'foo' { 'foo' } else { 'bar' } }} ", args: ("--dump"), stdout: " foo BAR: echo {{ if 'foo' == 'foo' { 'foo' } else { 'bar' } }} ", } test! { name: recipe_escaped_braces, justfile: " foo BAR: echo '{{{{BAR}}}}' ", args: ("--dump"), stdout: " foo BAR: echo '{{{{BAR}}}}' ", } test! { name: recipe_assignment_in_body, justfile: " bar := 'bar' foo: echo $bar ", args: ("--dump"), stdout: " bar := 'bar' foo: echo $bar ", } test! { name: recipe_dependency, justfile: " bar: echo bar foo: bar echo foo ", args: ("--dump"), stdout: " bar: echo bar foo: bar echo foo ", } test! { name: recipe_dependency_param, justfile: " bar BAR: echo bar foo: (bar 'bar') echo foo ", args: ("--dump"), stdout: " bar BAR: echo bar foo: (bar 'bar') echo foo ", } test! { name: recipe_dependency_params, justfile: " bar BAR BAZ: echo bar foo: (bar 'bar' 'baz') echo foo ", args: ("--dump"), stdout: " bar BAR BAZ: echo bar foo: (bar 'bar' 'baz') echo foo ", } test! { name: recipe_dependencies, justfile: " bar: echo bar baz: echo baz foo: baz bar echo foo ", args: ("--dump"), stdout: " bar: echo bar baz: echo baz foo: baz bar echo foo ", } test! { name: recipe_dependencies_params, justfile: " bar BAR: echo bar baz BAZ: echo baz foo: (baz 'baz') (bar 'bar') echo foo ", args: ("--dump"), stdout: " bar BAR: echo bar baz BAZ: echo baz foo: (baz 'baz') (bar 'bar') echo foo ", } test! { name: set_true_explicit, justfile: " set export := true ", args: ("--dump"), stdout: " set export := true ", } test! { name: set_true_implicit, justfile: " set export ", args: ("--dump"), stdout: " set export := true ", } test! { name: set_false, justfile: " set export := false ", args: ("--dump"), stdout: " set export := false ", } test! { name: set_shell, justfile: r#" set shell := ['sh', "-c"] "#, args: ("--dump"), stdout: r#" set shell := ['sh', "-c"] "#, } test! { name: comment, justfile: " # foo ", args: ("--dump"), stdout: " # foo ", } test! { name: comment_multiline, justfile: " # foo # bar ", args: ("--dump"), stdout: " # foo # bar ", } test! { name: comment_leading, justfile: " # foo foo := 'bar' ", args: ("--dump"), stdout: " # foo foo := 'bar' ", } test! { name: comment_trailing, justfile: " foo := 'bar' # foo ", args: ("--dump"), stdout: " foo := 'bar' # foo ", } test! { name: comment_before_recipe, justfile: " # foo foo: echo foo ", args: ("--dump"), stdout: " # foo foo: echo foo ", } test! { name: comment_before_docstring_recipe, justfile: " # bar # foo foo: echo foo ", args: ("--dump"), stdout: " # bar # foo foo: echo foo ", } test! { name: group_recipes, justfile: " foo: echo foo bar: echo bar ", args: ("--dump"), stdout: " foo: echo foo bar: echo bar ", } test! { name: group_aliases, justfile: " alias f := foo alias b := bar foo: echo foo bar: echo bar ", args: ("--dump"), stdout: " alias f := foo alias b := bar foo: echo foo bar: echo bar ", } test! { name: group_assignments, justfile: " foo := 'foo' bar := 'bar' ", args: ("--dump"), stdout: " foo := 'foo' bar := 'bar' ", } test! { name: group_sets, justfile: " set export := true set positional-arguments := true ", args: ("--dump"), stdout: " set export := true set positional-arguments := true ", } test! { name: group_comments, justfile: " # foo # bar ", args: ("--dump"), stdout: " # foo # bar ", } test! { name: separate_recipes_aliases, justfile: " alias f := foo foo: echo foo ", args: ("--dump"), stdout: " alias f := foo foo: echo foo ", } test! { name: no_trailing_newline, justfile: " foo: echo foo", args: ("--dump"), stdout: " foo: echo foo ", } test! { name: subsequent, justfile: " bar: foo: && bar echo foo", args: ("--dump"), stdout: " bar: foo: && bar echo foo ", } #[test] fn exported_parameter() { Test::new() .justfile("foo +$f:") .args(["--dump"]) .stdout("foo +$f:\n") .run(); } just-1.21.0/tests/functions.rs000064400000000000000000000344261046102023000144230ustar 00000000000000use super::*; test! { name: test_os_arch_functions_in_interpolation, justfile: r#" foo: echo {{arch()}} {{os()}} {{os_family()}} {{num_cpus()}} "#, stdout: format!("{} {} {} {}\n", target::arch(), target::os(), target::family(), num_cpus::get()).as_str(), stderr: format!("echo {} {} {} {}\n", target::arch(), target::os(), target::family(), num_cpus::get()).as_str(), } test! { name: test_os_arch_functions_in_expression, justfile: r#" a := arch() o := os() f := os_family() n := num_cpus() foo: echo {{a}} {{o}} {{f}} {{n}} "#, stdout: format!("{} {} {} {}\n", target::arch(), target::os(), target::family(), num_cpus::get()).as_str(), stderr: format!("echo {} {} {} {}\n", target::arch(), target::os(), target::family(), num_cpus::get()).as_str(), } #[cfg(not(windows))] test! { name: env_var_functions, justfile: r#" p := env_var('USER') b := env_var_or_default('ZADDY', 'HTAP') x := env_var_or_default('XYZ', 'ABC') foo: /bin/echo '{{p}}' '{{b}}' '{{x}}' "#, stdout: format!("{} HTAP ABC\n", env::var("USER").unwrap()).as_str(), stderr: format!("/bin/echo '{}' 'HTAP' 'ABC'\n", env::var("USER").unwrap()).as_str(), } #[cfg(not(windows))] test! { name: path_functions, justfile: r#" we := without_extension('/foo/bar/baz.hello') fs := file_stem('/foo/bar/baz.hello') fn := file_name('/foo/bar/baz.hello') dir := parent_directory('/foo/bar/baz.hello') ext := extension('/foo/bar/baz.hello') jn := join('a', 'b') foo: /bin/echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}' '{{jn}}' "#, stdout: "/foo/bar/baz baz baz.hello /foo/bar hello a/b\n", stderr: "/bin/echo '/foo/bar/baz' 'baz' 'baz.hello' '/foo/bar' 'hello' 'a/b'\n", } #[cfg(not(windows))] test! { name: path_functions2, justfile: r#" we := without_extension('/foo/bar/baz') fs := file_stem('/foo/bar/baz.hello.ciao') fn := file_name('/bar/baz.hello.ciao') dir := parent_directory('/foo/') ext := extension('/foo/bar/baz.hello.ciao') foo: /bin/echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}' "#, stdout: "/foo/bar/baz baz.hello baz.hello.ciao / ciao\n", stderr: "/bin/echo '/foo/bar/baz' 'baz.hello' 'baz.hello.ciao' '/' 'ciao'\n", } #[cfg(not(windows))] test! { name: broken_without_extension_function, justfile: r#" we := without_extension('') foo: /bin/echo '{{we}}' "#, stdout: "", stderr: format!("{} {}\n{}\n{}\n{}\n{}\n", "error: Call to function `without_extension` failed:", "Could not extract parent from ``", " ——▶ justfile:1:8", " │", "1 │ we := without_extension(\'\')", " │ ^^^^^^^^^^^^^^^^^").as_str(), status: EXIT_FAILURE, } #[cfg(not(windows))] test! { name: broken_extension_function, justfile: r#" we := extension('') foo: /bin/echo '{{we}}' "#, stdout: "", stderr: format!("{}\n{}\n{}\n{}\n{}\n", "error: Call to function `extension` failed: Could not extract extension from ``", " ——▶ justfile:1:8", " │", "1 │ we := extension(\'\')", " │ ^^^^^^^^^").as_str(), status: EXIT_FAILURE, } #[cfg(not(windows))] test! { name: broken_extension_function2, justfile: r#" we := extension('foo') foo: /bin/echo '{{we}}' "#, stdout: "", stderr: format!("{}\n{}\n{}\n{}\n{}\n", "error: Call to function `extension` failed: Could not extract extension from `foo`", " ——▶ justfile:1:8", " │", "1 │ we := extension(\'foo\')", " │ ^^^^^^^^^").as_str(), status: EXIT_FAILURE, } #[cfg(not(windows))] test! { name: broken_file_stem_function, justfile: r#" we := file_stem('') foo: /bin/echo '{{we}}' "#, stdout: "", stderr: format!("{}\n{}\n{}\n{}\n{}\n", "error: Call to function `file_stem` failed: Could not extract file stem from ``", " ——▶ justfile:1:8", " │", "1 │ we := file_stem(\'\')", " │ ^^^^^^^^^").as_str(), status: EXIT_FAILURE, } #[cfg(not(windows))] test! { name: broken_file_name_function, justfile: r#" we := file_name('') foo: /bin/echo '{{we}}' "#, stdout: "", stderr: format!("{}\n{}\n{}\n{}\n{}\n", "error: Call to function `file_name` failed: Could not extract file name from ``", " ——▶ justfile:1:8", " │", "1 │ we := file_name(\'\')", " │ ^^^^^^^^^").as_str(), status: EXIT_FAILURE, } #[cfg(not(windows))] test! { name: broken_directory_function, justfile: r#" we := parent_directory('') foo: /bin/echo '{{we}}' "#, stdout: "", stderr: format!("{} {}\n{}\n{}\n{}\n{}\n", "error: Call to function `parent_directory` failed:", "Could not extract parent directory from ``", " ——▶ justfile:1:8", " │", "1 │ we := parent_directory(\'\')", " │ ^^^^^^^^^^^^^^^^").as_str(), status: EXIT_FAILURE, } #[cfg(not(windows))] test! { name: broken_directory_function2, justfile: r#" we := parent_directory('/') foo: /bin/echo '{{we}}' "#, stdout: "", stderr: format!("{} {}\n{}\n{}\n{}\n{}\n", "error: Call to function `parent_directory` failed:", "Could not extract parent directory from `/`", " ——▶ justfile:1:8", " │", "1 │ we := parent_directory(\'/\')", " │ ^^^^^^^^^^^^^^^^").as_str(), status: EXIT_FAILURE, } #[cfg(windows)] test! { name: env_var_functions, justfile: r#" p := env_var('USERNAME') b := env_var_or_default('ZADDY', 'HTAP') x := env_var_or_default('XYZ', 'ABC') foo: /bin/echo '{{p}}' '{{b}}' '{{x}}' "#, stdout: format!("{} HTAP ABC\n", env::var("USERNAME").unwrap()).as_str(), stderr: format!("/bin/echo '{}' 'HTAP' 'ABC'\n", env::var("USERNAME").unwrap()).as_str(), } test! { name: env_var_failure, justfile: "a:\n echo {{env_var('ZADDY')}}", args: ("a"), stdout: "", stderr: "error: Call to function `env_var` failed: environment variable `ZADDY` not present ——▶ justfile:2:10 │ 2 │ echo {{env_var('ZADDY')}} │ ^^^^^^^ ", status: EXIT_FAILURE, } test! { name: test_just_executable_function, justfile: " a: @printf 'Executable path is: %s\\n' '{{ just_executable() }}' ", args: ("a"), stdout: format!("Executable path is: {}\n", executable_path("just").to_str().unwrap()).as_str(), stderr: "", status: EXIT_SUCCESS, } test! { name: test_os_arch_functions_in_default, justfile: r#" foo a=arch() o=os() f=os_family() n=num_cpus(): echo {{a}} {{o}} {{f}} {{n}} "#, stdout: format!("{} {} {} {}\n", target::arch(), target::os(), target::family(), num_cpus::get()).as_str(), stderr: format!("echo {} {} {} {}\n", target::arch(), target::os(), target::family(), num_cpus::get()).as_str(), } test! { name: clean, justfile: " foo: echo {{ clean('a/../b') }} ", stdout: "b\n", stderr: "echo b\n", } test! { name: uppercase, justfile: " foo: echo {{ uppercase('bar') }} ", stdout: "BAR\n", stderr: "echo BAR\n", } test! { name: lowercase, justfile: " foo: echo {{ lowercase('BAR') }} ", stdout: "bar\n", stderr: "echo bar\n", } test! { name: uppercamelcase, justfile: " foo: echo {{ uppercamelcase('foo bar') }} ", stdout: "FooBar\n", stderr: "echo FooBar\n", } test! { name: lowercamelcase, justfile: " foo: echo {{ lowercamelcase('foo bar') }} ", stdout: "fooBar\n", stderr: "echo fooBar\n", } test! { name: snakecase, justfile: " foo: echo {{ snakecase('foo bar') }} ", stdout: "foo_bar\n", stderr: "echo foo_bar\n", } test! { name: kebabcase, justfile: " foo: echo {{ kebabcase('foo bar') }} ", stdout: "foo-bar\n", stderr: "echo foo-bar\n", } test! { name: shoutysnakecase, justfile: " foo: echo {{ shoutysnakecase('foo bar') }} ", stdout: "FOO_BAR\n", stderr: "echo FOO_BAR\n", } test! { name: titlecase, justfile: " foo: echo {{ titlecase('foo bar') }} ", stdout: "Foo Bar\n", stderr: "echo Foo Bar\n", } test! { name: shoutykebabcase, justfile: " foo: echo {{ shoutykebabcase('foo bar') }} ", stdout: "FOO-BAR\n", stderr: "echo FOO-BAR\n", } test! { name: trim, justfile: " foo: echo {{ trim(' bar ') }} ", stdout: "bar\n", stderr: "echo bar\n", } test! { name: replace, justfile: " foo: echo {{ replace('barbarbar', 'bar', 'foo') }} ", stdout: "foofoofoo\n", stderr: "echo foofoofoo\n", } test! { name: replace_regex, justfile: " foo: echo {{ replace_regex('123bar123bar123bar', '\\d+bar', 'foo') }} ", stdout: "foofoofoo\n", stderr: "echo foofoofoo\n", } test! { name: invalid_replace_regex, justfile: " foo: echo {{ replace_regex('barbarbar', 'foo\\', 'foo') }} ", stderr: "error: Call to function `replace_regex` failed: regex parse error: foo\\ ^ error: incomplete escape sequence, reached end of pattern prematurely ——▶ justfile:2:11 │ 2 │ echo {{ replace_regex('barbarbar', 'foo\\', 'foo') }} │ ^^^^^^^^^^^^^ ", status: EXIT_FAILURE, } test! { name: capitalize, justfile: " foo: echo {{ capitalize('BAR') }} ", stdout: "Bar\n", stderr: "echo Bar\n", } #[test] fn semver_matches() { Test::new() .justfile( " foo: echo {{ semver_matches('0.1.0', '>=0.1.0') }} echo {{ semver_matches('0.1.0', '=0.0.1') }} ", ) .stdout("true\nfalse\n") .stderr("echo true\necho false\n") .run(); } fn assert_eval_eq(expression: &str, result: &str) { Test::new() .justfile(format!("x := {expression}")) .args(["--evaluate", "x"]) .stdout(result) .unindent_stdout(false) .run(); } #[test] fn trim_end_matches() { assert_eval_eq("trim_end_matches('foo', 'o')", "f"); assert_eval_eq("trim_end_matches('fabab', 'ab')", "f"); assert_eval_eq("trim_end_matches('fbaabab', 'ab')", "fba"); } #[test] fn trim_end_match() { assert_eval_eq("trim_end_match('foo', 'o')", "fo"); assert_eval_eq("trim_end_match('fabab', 'ab')", "fab"); } #[test] fn trim_start_matches() { assert_eval_eq("trim_start_matches('oof', 'o')", "f"); assert_eval_eq("trim_start_matches('ababf', 'ab')", "f"); assert_eval_eq("trim_start_matches('ababbaf', 'ab')", "baf"); } #[test] fn trim_start_match() { assert_eval_eq("trim_start_match('oof', 'o')", "of"); assert_eval_eq("trim_start_match('ababf', 'ab')", "abf"); } #[test] fn trim_start() { assert_eval_eq("trim_start(' f ')", "f "); } #[test] fn trim_end() { assert_eval_eq("trim_end(' f ')", " f"); } #[test] #[cfg(not(windows))] fn join() { assert_eval_eq("join('a', 'b', 'c', 'd')", "a/b/c/d"); assert_eval_eq("join('a', '/b', 'c', 'd')", "/b/c/d"); assert_eval_eq("join('a', '/b', '/c', 'd')", "/c/d"); assert_eval_eq("join('a', '/b', '/c', '/d')", "/d"); } #[test] #[cfg(windows)] fn join() { assert_eval_eq("join('a', 'b', 'c', 'd')", "a\\b\\c\\d"); assert_eval_eq("join('a', '\\b', 'c', 'd')", "\\b\\c\\d"); assert_eval_eq("join('a', '\\b', '\\c', 'd')", "\\c\\d"); assert_eval_eq("join('a', '\\b', '\\c', '\\d')", "\\d"); } #[test] fn join_argument_count_error() { Test::new() .justfile("x := join('a')") .args(["--evaluate"]) .stderr( " error: Function `join` called with 1 argument but takes 2 or more ——▶ justfile:1:6 │ 1 │ x := join(\'a\') │ ^^^^ ", ) .status(EXIT_FAILURE) .run(); } #[test] fn test_path_exists_filepath_exist() { Test::new() .tree(tree! { testfile: "" }) .justfile("x := path_exists('testfile')") .args(["--evaluate", "x"]) .stdout("true") .run(); } #[test] fn test_path_exists_filepath_doesnt_exist() { Test::new() .justfile("x := path_exists('testfile')") .args(["--evaluate", "x"]) .stdout("false") .run(); } #[test] fn error_errors_with_message() { Test::new() .justfile("x := error ('Thing Not Supported')") .args(["--evaluate"]) .status(1) .stderr( " error: Call to function `error` failed: Thing Not Supported ——▶ justfile:1:6 │ 1 │ x := error ('Thing Not Supported') │ ^^^^^ ", ) .run(); } #[test] fn test_absolute_path_resolves() { let test_object = Test::new() .justfile("path := absolute_path('./test_file')") .args(["--evaluate", "path"]); let mut tempdir = test_object.tempdir.path().to_owned(); // Just retrieves the current directory via env::current_dir(), which // does the moral equivalent of canonicalize, which will remove symlinks. // So, we have to canonicalize here, so that we can match it. if cfg!(unix) { tempdir = tempdir.canonicalize().unwrap(); } test_object .stdout(tempdir.join("test_file").to_str().unwrap().to_owned()) .run(); } #[test] fn test_absolute_path_resolves_parent() { let test_object = Test::new() .justfile("path := absolute_path('../test_file')") .args(["--evaluate", "path"]); let mut tempdir = test_object.tempdir.path().to_owned(); // Just retrieves the current directory via env::current_dir(), which // does the moral equivalent of canonicalize, which will remove symlinks. // So, we have to canonicalize here, so that we can match it. if cfg!(unix) { tempdir = tempdir.canonicalize().unwrap(); } test_object .stdout( tempdir .parent() .unwrap() .join("test_file") .to_str() .unwrap() .to_owned(), ) .run(); } #[test] fn path_exists_subdir() { Test::new() .tree(tree! { foo: "", bar: { } }) .justfile("x := path_exists('foo')") .current_dir("bar") .args(["--evaluate", "x"]) .stdout("true") .run(); } #[test] fn uuid() { Test::new() .justfile("x := uuid()") .args(["--evaluate", "x"]) .stdout_regex("........-....-....-....-............") .run(); } #[test] fn sha256() { Test::new() .justfile("x := sha256('5943ee37-0000-1000-8000-010203040506')") .args(["--evaluate", "x"]) .stdout("2330d7f5eb94a820b54fed59a8eced236f80b633a504289c030b6a65aef58871") .run(); } #[test] fn sha256_file() { Test::new() .justfile("x := sha256_file('sub/shafile')") .tree(tree! { sub: { shafile: "just is great\n", } }) .current_dir("sub") .args(["--evaluate", "x"]) .stdout("177b3d79aaafb53a7a4d7aaba99a82f27c73370e8cb0295571aade1e4fea1cd2") .run(); } just-1.21.0/tests/ignore_comments.rs000064400000000000000000000034121046102023000155720ustar 00000000000000use super::*; #[test] fn ignore_comments_in_recipe() { Test::new() .justfile( " set ignore-comments some_recipe: # A recipe-internal comment echo something-useful ", ) .stdout("something-useful\n") .stderr("echo something-useful\n") .run(); } #[test] fn dont_ignore_comments_in_recipe_by_default() { Test::new() .justfile( " some_recipe: # A recipe-internal comment echo something-useful ", ) .stdout("something-useful\n") .stderr("# A recipe-internal comment\necho something-useful\n") .run(); } #[test] fn ignore_recipe_comments_with_shell_setting() { Test::new() .justfile( " set shell := ['echo', '-n'] set ignore-comments some_recipe: # Alternate shells still ignore comments echo something-useful ", ) .stdout("something-useful\n") .stderr("echo something-useful\n") .run(); } #[test] fn continuations_iwth_echo_comments_false() { Test::new() .justfile( " set ignore-comments some_recipe: # Comment lines ignore line continuations \\ echo something-useful ", ) .stdout("something-useful\n") .stderr("echo something-useful\n") .run(); } #[test] fn continuations_with_echo_comments_true() { Test::new() .justfile( " set ignore-comments := false some_recipe: # comment lines can be continued \\ echo something-useful ", ) .stdout("") .stderr("# comment lines can be continued echo something-useful\n") .run(); } #[test] fn dont_evaluate_comments() { Test::new() .justfile( " set ignore-comments some_recipe: # {{ error('foo') }} ", ) .run(); } just-1.21.0/tests/imports.rs000064400000000000000000000100061046102023000140740ustar 00000000000000use super::*; #[test] fn import_succeeds() { Test::new() .tree(tree! { "import.justfile": " b: @echo B ", }) .justfile( " import './import.justfile' a: b @echo A ", ) .test_round_trip(false) .arg("a") .stdout("B\nA\n") .run(); } #[test] fn missing_import_file_error() { Test::new() .justfile( " import './import.justfile' a: @echo A ", ) .test_round_trip(false) .arg("a") .status(EXIT_FAILURE) .stderr( " error: Could not find source file for import. ——▶ justfile:1:8 │ 1 │ import './import.justfile' │ ^^^^^^^^^^^^^^^^^^^ ", ) .run(); } #[test] fn missing_optional_imports_are_ignored() { Test::new() .justfile( " import? './import.justfile' a: @echo A ", ) .test_round_trip(false) .arg("a") .stdout("A\n") .run(); } #[test] fn trailing_spaces_after_import_are_ignored() { Test::new() .tree(tree! { "import.justfile": "", }) .justfile( " import './import.justfile'\x20 a: @echo A ", ) .test_round_trip(false) .stdout("A\n") .run(); } #[test] fn import_after_recipe() { Test::new() .tree(tree! { "import.justfile": " a: @echo A ", }) .justfile( " b: a import './import.justfile' ", ) .test_round_trip(false) .stdout("A\n") .run(); } #[test] fn circular_import() { Test::new() .justfile("import 'a'") .tree(tree! { a: "import 'b'", b: "import 'a'", }) .status(EXIT_FAILURE) .stderr_regex(path_for_regex( "error: Import `.*/a` in `.*/b` is circular\n", )) .run(); } #[test] fn import_recipes_are_not_default() { Test::new() .tree(tree! { "import.justfile": "bar:", }) .justfile("import './import.justfile'") .test_round_trip(false) .status(EXIT_FAILURE) .stderr("error: Justfile contains no default recipe.\n") .run(); } #[test] fn listed_recipes_in_imports_are_in_load_order() { Test::new() .justfile( " import './import.justfile' foo: ", ) .write("import.justfile", "bar:") .args(["--list", "--unsorted"]) .test_round_trip(false) .stdout( " Available recipes: foo bar ", ) .run(); } #[test] fn include_error() { Test::new() .justfile("!include foo") .status(EXIT_FAILURE) .stderr( " error: The `!include` directive has been stabilized as `import` ——▶ justfile:1:1 │ 1 │ !include foo │ ^ ", ) .run(); } #[test] fn recipes_in_import_are_overridden_by_recipes_in_parent() { Test::new() .tree(tree! { "import.justfile": " a: @echo IMPORT ", }) .justfile( " import './import.justfile' set allow-duplicate-recipes a: @echo ROOT ", ) .test_round_trip(false) .arg("a") .stdout("ROOT\n") .run(); } #[cfg(not(windows))] #[test] fn import_paths_beginning_with_tilde_are_expanded_to_homdir() { Test::new() .write("foobar/mod.just", "foo:\n @echo FOOBAR") .justfile( " import '~/mod.just' ", ) .test_round_trip(false) .arg("foo") .stdout("FOOBAR\n") .env("HOME", "foobar") .run(); } #[test] fn imports_dump_correctly() { Test::new() .write("import.justfile", "") .justfile( " import './import.justfile' ", ) .test_round_trip(false) .arg("--dump") .stdout("import './import.justfile'\n") .run(); } #[test] fn optional_imports_dump_correctly() { Test::new() .write("import.justfile", "") .justfile( " import? './import.justfile' ", ) .test_round_trip(false) .arg("--dump") .stdout("import? './import.justfile'\n") .run(); } just-1.21.0/tests/init.rs000064400000000000000000000074141046102023000133530ustar 00000000000000use super::*; const EXPECTED: &str = "default:\n echo 'Hello, world!'\n"; #[test] fn current_dir() { let tmp = tempdir(); let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--init") .output() .unwrap(); assert!(output.status.success()); assert_eq!( fs::read_to_string(tmp.path().join("justfile")).unwrap(), EXPECTED ); } #[test] fn exists() { let output = Test::new() .no_justfile() .arg("--init") .stderr_regex("Wrote justfile to `.*`\n") .run(); Test::with_tempdir(output.tempdir) .no_justfile() .arg("--init") .status(EXIT_FAILURE) .stderr_regex("error: Justfile `.*` already exists\n") .run(); } #[test] fn write_error() { let test = Test::new(); let justfile_path = test.justfile_path(); fs::create_dir(justfile_path).unwrap(); test .no_justfile() .args(["--init"]) .status(EXIT_FAILURE) .stderr_regex(if cfg!(windows) { r"error: Failed to write justfile to `.*`: Access is denied. \(os error 5\)\n" } else { r"error: Failed to write justfile to `.*`: Is a directory \(os error 21\)\n" }) .run(); } #[test] fn invocation_directory() { let tmp = temptree! { ".git": {}, }; let test = Test::with_tempdir(tmp); let justfile_path = test.justfile_path(); let _tmp = test .no_justfile() .stderr_regex("Wrote justfile to `.*`\n") .arg("--init") .run(); assert_eq!(fs::read_to_string(justfile_path).unwrap(), EXPECTED); } #[test] fn parent_dir() { let tmp = temptree! { ".git": {}, sub: {}, }; let output = Command::new(executable_path("just")) .current_dir(tmp.path().join("sub")) .arg("--init") .output() .unwrap(); assert!(output.status.success()); assert_eq!( fs::read_to_string(tmp.path().join("justfile")).unwrap(), EXPECTED ); } #[test] fn alternate_marker() { let tmp = temptree! { "_darcs": {}, }; let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--init") .output() .unwrap(); assert!(output.status.success()); assert_eq!( fs::read_to_string(tmp.path().join("justfile")).unwrap(), EXPECTED ); } #[test] fn search_directory() { let tmp = temptree! { sub: { ".git": {}, }, }; let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--init") .arg("sub/") .output() .unwrap(); assert!(output.status.success()); assert_eq!( fs::read_to_string(tmp.path().join("sub/justfile")).unwrap(), EXPECTED ); } #[test] fn justfile() { let tmp = temptree! { sub: { ".git": {}, }, }; let output = Command::new(executable_path("just")) .current_dir(tmp.path().join("sub")) .arg("--init") .arg("--justfile") .arg(tmp.path().join("justfile")) .output() .unwrap(); assert!(output.status.success()); assert_eq!( fs::read_to_string(tmp.path().join("justfile")).unwrap(), EXPECTED ); } #[test] fn justfile_and_working_directory() { let tmp = temptree! { sub: { ".git": {}, }, }; let output = Command::new(executable_path("just")) .current_dir(tmp.path().join("sub")) .arg("--init") .arg("--justfile") .arg(tmp.path().join("justfile")) .arg("--working-directory") .arg("/") .output() .unwrap(); assert!(output.status.success()); assert_eq!( fs::read_to_string(tmp.path().join("justfile")).unwrap(), EXPECTED ); } #[test] fn fmt_compatibility() { let output = Test::new() .no_justfile() .arg("--init") .stderr_regex("Wrote justfile to `.*`\n") .run(); Test::with_tempdir(output.tempdir) .no_justfile() .arg("--unstable") .arg("--check") .arg("--fmt") .status(EXIT_SUCCESS) .run(); } just-1.21.0/tests/interrupts.rs000064400000000000000000000027271046102023000146310ustar 00000000000000use { super::*, std::time::{Duration, Instant}, }; fn kill(process_id: u32) { unsafe { libc::kill(process_id as i32, libc::SIGINT); } } fn interrupt_test(arguments: &[&str], justfile: &str) { let tmp = tempdir(); let mut justfile_path = tmp.path().to_path_buf(); justfile_path.push("justfile"); fs::write(justfile_path, unindent(justfile)).unwrap(); let start = Instant::now(); let mut child = Command::new(executable_path("just")) .current_dir(&tmp) .args(arguments) .spawn() .expect("just invocation failed"); while start.elapsed() < Duration::from_millis(500) {} kill(child.id()); let status = child.wait().unwrap(); let elapsed = start.elapsed(); if elapsed > Duration::from_secs(2) { panic!("process returned too late: {elapsed:?}"); } if elapsed < Duration::from_millis(100) { panic!("process returned too early : {elapsed:?}"); } assert_eq!(status.code(), Some(130)); } #[test] #[ignore] fn interrupt_shebang() { interrupt_test( &[], " default: #!/usr/bin/env sh sleep 1 ", ); } #[test] #[ignore] fn interrupt_line() { interrupt_test( &[], " default: @sleep 1 ", ); } #[test] #[ignore] fn interrupt_backtick() { interrupt_test( &[], " foo := `sleep 1` default: @echo {{foo}} ", ); } #[test] #[ignore] fn interrupt_command() { interrupt_test(&["--command", "sleep", "1"], ""); } just-1.21.0/tests/invocation_directory.rs000064400000000000000000000046471046102023000166520ustar 00000000000000use super::*; #[cfg(unix)] fn convert_native_path(path: &Path) -> String { fs::canonicalize(path) .expect("canonicalize failed") .to_str() .map(str::to_string) .expect("unicode decode failed") } #[cfg(windows)] fn convert_native_path(path: &Path) -> String { // Translate path from windows style to unix style let mut cygpath = Command::new("cygpath"); cygpath.arg("--unix"); cygpath.arg(path); let output = cygpath.output().expect("executing cygpath failed"); assert!(output.status.success()); let stdout = str::from_utf8(&output.stdout).expect("cygpath output was not utf8"); if stdout.ends_with('\n') { &stdout[0..stdout.len() - 1] } else if stdout.ends_with("\r\n") { &stdout[0..stdout.len() - 2] } else { stdout } .to_owned() } #[test] fn test_invocation_directory() { let tmp = tempdir(); let mut justfile_path = tmp.path().to_path_buf(); justfile_path.push("justfile"); fs::write( justfile_path, "default:\n @cd {{invocation_directory()}}\n @echo {{invocation_directory()}}", ) .unwrap(); let mut subdir = tmp.path().to_path_buf(); subdir.push("subdir"); fs::create_dir(&subdir).unwrap(); let output = Command::new(executable_path("just")) .current_dir(&subdir) .args(["--shell", "sh"]) .output() .expect("just invocation failed"); let mut failure = false; let expected_status = 0; let expected_stdout = convert_native_path(&subdir) + "\n"; let expected_stderr = ""; let status = output.status.code().unwrap(); if status != expected_status { println!("bad status: {status} != {expected_status}"); failure = true; } let stdout = str::from_utf8(&output.stdout).unwrap(); if stdout != expected_stdout { println!("bad stdout:\ngot:\n{stdout:?}\n\nexpected:\n{expected_stdout:?}"); failure = true; } let stderr = str::from_utf8(&output.stderr).unwrap(); if stderr != expected_stderr { println!("bad stderr:\ngot:\n{stderr:?}\n\nexpected:\n{expected_stderr:?}"); failure = true; } if failure { panic!("test failed"); } } #[test] fn invocation_directory_native() { let Output { stdout, tempdir } = Test::new() .justfile("x := invocation_directory_native()") .args(["--evaluate", "x"]) .stdout_regex(".*") .run(); if cfg!(windows) { assert_eq!(Path::new(&stdout), tempdir.path()); } else { assert_eq!(Path::new(&stdout), tempdir.path().canonicalize().unwrap()); } } just-1.21.0/tests/json.rs000064400000000000000000000530461046102023000133630ustar 00000000000000use super::*; fn case(justfile: &str, value: Value) { Test::new() .justfile(justfile) .args(["--dump", "--dump-format", "json", "--unstable"]) .stdout(format!("{}\n", serde_json::to_string(&value).unwrap())) .run(); } #[test] fn alias() { case( " alias f := foo foo: ", json!({ "first": "foo", "aliases": { "f": { "name": "f", "target": "foo", "attributes": [], } }, "assignments": {}, "modules": {}, "recipes": { "foo": { "attributes": [], "body": [], "dependencies": [], "doc": null, "name": "foo", "parameters": [], "priors": 0, "private": false, "quiet": false, "shebang": false, } }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "positional_arguments": false, "shell": null, "tempdir" : null, "ignore_comments": false, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn assignment() { case( "foo := 'bar'", json!({ "aliases": {}, "assignments": { "foo": { "export": false, "name": "foo", "value": "bar", } }, "first": null, "modules": {}, "recipes": {}, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, "tempdir" : null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn body() { case( " foo: bar abc{{ 'xyz' }}def ", json!({ "aliases": {}, "assignments": {}, "first": "foo", "modules": {}, "recipes": { "foo": { "attributes": [], "body": [ ["bar"], ["abc", ["xyz"], "def"], ], "dependencies": [], "doc": null, "name": "foo", "parameters": [], "priors": 0, "private": false, "quiet": false, "shebang": false, } }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, "tempdir" : null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn dependencies() { case( " foo: bar: foo ", json!({ "aliases": {}, "assignments": {}, "first": "foo", "modules": {}, "recipes": { "bar": { "attributes": [], "doc": null, "name": "bar", "body": [], "dependencies": [{ "arguments": [], "recipe": "foo" }], "parameters": [], "priors": 1, "private": false, "quiet": false, "shebang": false, }, "foo": { "body": [], "dependencies": [], "doc": null, "name": "foo", "parameters": [], "priors": 0, "private": false, "quiet": false, "shebang": false, "attributes": [], } }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, "tempdir" : null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn dependency_argument() { case( " x := 'foo' foo *args: bar: ( foo 'baz' ('baz') ('a' + 'b') `echo` x if 'a' == 'b' { 'c' } else { 'd' } arch() env_var('foo') join('a', 'b') replace('a', 'b', 'c') ) ", json!({ "aliases": {}, "first": "foo", "assignments": { "x": { "export": false, "name": "x", "value": "foo", }, }, "modules": {}, "recipes": { "bar": { "doc": null, "name": "bar", "body": [], "dependencies": [{ "arguments": [ "baz", "baz", ["concatenate", "a", "b"], ["evaluate", "echo"], ["variable", "x"], ["if", "==", "a", "b", "c", "d"], ["call", "arch"], ["call", "env_var", "foo"], ["call", "join", "a", "b"], ["call", "replace", "a", "b", "c"], ], "recipe": "foo" }], "parameters": [], "priors": 1, "private": false, "quiet": false, "shebang": false, "attributes": [], }, "foo": { "body": [], "dependencies": [], "doc": null, "name": "foo", "parameters": [ { "name": "args", "export": false, "default": null, "kind": "star", } ], "priors": 0, "private": false, "quiet": false, "shebang": false, "attributes": [], } }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, "tempdir" : null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn duplicate_recipes() { case( " set allow-duplicate-recipes alias f := foo foo: foo bar: ", json!({ "first": "foo", "aliases": { "f": { "attributes": [], "name": "f", "target": "foo", } }, "assignments": {}, "modules": {}, "recipes": { "foo": { "body": [], "dependencies": [], "doc": null, "name": "foo", "parameters": [ { "name": "bar", "export": false, "default": null, "kind": "singular", }, ], "priors": 0, "private": false, "quiet": false, "shebang": false, "attributes": [], } }, "settings": { "allow_duplicate_recipes": true, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, "tempdir" : null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn doc_comment() { case( "# hello\nfoo:", json!({ "aliases": {}, "first": "foo", "assignments": {}, "modules": {}, "recipes": { "foo": { "body": [], "dependencies": [], "doc": "hello", "name": "foo", "parameters": [], "priors": 0, "private": false, "quiet": false, "shebang": false, "attributes": [], } }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, "tempdir" : null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn empty_justfile() { case( "", json!({ "aliases": {}, "assignments": {}, "first": null, "modules": {}, "recipes": {}, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, "tempdir" : null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn parameters() { case( " a: b x: c x='y': d +x: e *x: f $x: ", json!({ "aliases": {}, "first": "a", "assignments": {}, "modules": {}, "recipes": { "a": { "attributes": [], "body": [], "dependencies": [], "doc": null, "name": "a", "parameters": [], "priors": 0, "private": false, "quiet": false, "shebang": false, }, "b": { "body": [], "dependencies": [], "doc": null, "name": "b", "parameters": [ { "name": "x", "export": false, "default": null, "kind": "singular", }, ], "priors": 0, "private": false, "quiet": false, "shebang": false, "attributes": [], }, "c": { "body": [], "dependencies": [], "doc": null, "name": "c", "parameters": [ { "name": "x", "export": false, "default": "y", "kind": "singular", } ], "priors": 0, "private": false, "quiet": false, "shebang": false, "attributes": [], }, "d": { "body": [], "dependencies": [], "doc": null, "name": "d", "parameters": [ { "name": "x", "export": false, "default": null, "kind": "plus", } ], "priors": 0, "private": false, "quiet": false, "shebang": false, "attributes": [], }, "e": { "body": [], "dependencies": [], "doc": null, "name": "e", "parameters": [ { "name": "x", "export": false, "default": null, "kind": "star", } ], "priors": 0, "private": false, "quiet": false, "shebang": false, "attributes": [], }, "f": { "body": [], "dependencies": [], "doc": null, "name": "f", "parameters": [ { "name": "x", "export": true, "default": null, "kind": "singular", } ], "priors": 0, "private": false, "quiet": false, "shebang": false, "attributes": [], }, }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, "tempdir" : null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn priors() { case( " a: b: a && c c: ", json!({ "aliases": {}, "assignments": {}, "first": "a", "modules": {}, "recipes": { "a": { "body": [], "dependencies": [], "doc": null, "name": "a", "parameters": [], "priors": 0, "private": false, "quiet": false, "shebang": false, "attributes": [], }, "b": { "body": [], "dependencies": [ { "arguments": [], "recipe": "a", }, { "arguments": [], "recipe": "c", } ], "doc": null, "name": "b", "private": false, "quiet": false, "shebang": false, "attributes": [], "parameters": [], "priors": 1, }, "c": { "body": [], "dependencies": [], "doc": null, "name": "c", "parameters": [], "private": false, "quiet": false, "shebang": false, "attributes": [], "parameters": [], "priors": 0, }, }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, "tempdir" : null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn private() { case( "_foo:", json!({ "aliases": {}, "assignments": {}, "first": "_foo", "modules": {}, "recipes": { "_foo": { "body": [], "dependencies": [], "doc": null, "name": "_foo", "parameters": [], "priors": 0, "private": true, "quiet": false, "shebang": false, "attributes": [], } }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, "tempdir" : null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn quiet() { case( "@foo:", json!({ "aliases": {}, "assignments": {}, "first": "foo", "modules": {}, "recipes": { "foo": { "body": [], "dependencies": [], "doc": null, "name": "foo", "parameters": [], "priors": 0, "private": false, "quiet": true, "shebang": false, "attributes": [], } }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, "tempdir" : null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn settings() { case( " set dotenv-load set dotenv-filename := \"filename\" set dotenv-path := \"path\" set export set fallback set positional-arguments set ignore-comments set shell := ['a', 'b', 'c'] foo: #!bar ", json!({ "aliases": {}, "assignments": {}, "first": "foo", "modules": {}, "recipes": { "foo": { "body": [["#!bar"]], "dependencies": [], "doc": null, "name": "foo", "parameters": [], "priors": 0, "private": false, "quiet": false, "shebang": true, "attributes": [], } }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": "filename", "dotenv_load": true, "dotenv_path": "path", "export": true, "fallback": true, "ignore_comments": true, "positional_arguments": true, "shell": { "arguments": ["b", "c"], "command": "a", }, "tempdir": null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn shebang() { case( " foo: #!bar ", json!({ "aliases": {}, "assignments": {}, "first": "foo", "modules": {}, "recipes": { "foo": { "body": [["#!bar"]], "dependencies": [], "doc": null, "name": "foo", "parameters": [], "priors": 0, "private": false, "quiet": false, "shebang": true, "attributes": [], } }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, "tempdir": null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn simple() { case( "foo:", json!({ "aliases": {}, "assignments": {}, "first": "foo", "modules": {}, "recipes": { "foo": { "body": [], "dependencies": [], "doc": null, "name": "foo", "parameters": [], "priors": 0, "private": false, "quiet": false, "shebang": false, "attributes": [], } }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "ignore_comments": false, "positional_arguments": false, "shell": null, "tempdir": null, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn attribute() { case( " [no-exit-message] foo: ", json!({ "aliases": {}, "assignments": {}, "first": "foo", "modules": {}, "recipes": { "foo": { "attributes": ["no-exit-message"], "body": [], "dependencies": [], "doc": null, "name": "foo", "parameters": [], "priors": 0, "private": false, "quiet": false, "shebang": false, } }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "positional_arguments": false, "shell": null, "tempdir" : null, "ignore_comments": false, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }), ); } #[test] fn module() { Test::new() .justfile( " mod foo ", ) .tree(tree! { "foo.just": "bar:", }) .args(["--dump", "--dump-format", "json", "--unstable"]) .test_round_trip(false) .stdout(format!( "{}\n", serde_json::to_string(&json!({ "aliases": {}, "assignments": {}, "first": null, "modules": { "foo": { "aliases": {}, "assignments": {}, "first": "bar", "modules": {}, "recipes": { "bar": { "attributes": [], "body": [], "dependencies": [], "doc": null, "name": "bar", "parameters": [], "priors": 0, "private": false, "quiet": false, "shebang": false, } }, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "positional_arguments": false, "shell": null, "tempdir" : null, "ignore_comments": false, "windows_powershell": false, "windows_shell": null, }, "warnings": [], }, }, "recipes": {}, "settings": { "allow_duplicate_recipes": false, "dotenv_filename": null, "dotenv_load": null, "dotenv_path": null, "export": false, "fallback": false, "positional_arguments": false, "shell": null, "tempdir" : null, "ignore_comments": false, "windows_powershell": false, "windows_shell": null, }, "warnings": [], })) .unwrap() )) .run(); } just-1.21.0/tests/lib.rs000064400000000000000000000035441046102023000131560ustar 00000000000000pub(crate) use { crate::{ assert_stdout::assert_stdout, assert_success::assert_success, tempdir::tempdir, test::{Output, Test}, }, cradle::input::Input, executable_path::executable_path, just::unindent, libc::{EXIT_FAILURE, EXIT_SUCCESS}, pretty_assertions::Comparison, regex::Regex, serde_json::{json, Value}, std::{ collections::BTreeMap, env::{self, consts::EXE_SUFFIX}, error::Error, fmt::Debug, fs, io::Write, iter, path::{Path, PathBuf, MAIN_SEPARATOR}, process::{Command, Stdio}, str, }, tempfile::TempDir, temptree::{temptree, tree, Tree}, which::which, }; #[macro_use] mod test; mod allow_duplicate_recipes; mod assert_stdout; mod assert_success; mod attributes; mod byte_order_mark; mod changelog; mod choose; mod command; mod completions; mod conditional; mod confirm; mod delimiters; mod dotenv; mod edit; mod equals; mod error_messages; mod evaluate; mod examples; mod export; mod fallback; mod fmt; mod functions; mod ignore_comments; mod imports; mod init; #[cfg(unix)] mod interrupts; mod invocation_directory; mod json; mod line_prefixes; mod misc; mod modules; mod multibyte_char; mod newline_escape; mod no_cd; mod no_exit_message; mod os_attributes; mod parser; mod positional_arguments; mod private; mod quiet; mod quote; mod readme; mod recursion_limit; mod regexes; mod run; mod search; mod search_arguments; mod shadowing_parameters; mod shebang; mod shell; mod show; mod slash_operator; mod string; mod subsequents; mod summary; mod tempdir; mod undefined_variables; mod unstable; #[cfg(target_family = "windows")] mod windows_shell; mod working_directory; fn path(s: &str) -> String { if cfg!(windows) { s.replace('/', "\\") } else { s.into() } } fn path_for_regex(s: &str) -> String { if cfg!(windows) { s.replace('/', "\\\\") } else { s.into() } } just-1.21.0/tests/line_prefixes.rs000064400000000000000000000004401046102023000152340ustar 00000000000000use super::*; #[test] fn infallible_after_quiet() { Test::new() .justfile( " foo: @-exit 1 ", ) .run(); } #[test] fn quiet_after_infallible() { Test::new() .justfile( " foo: -@exit 1 ", ) .run(); } just-1.21.0/tests/misc.rs000064400000000000000000001143031046102023000133370ustar 00000000000000use super::*; test! { name: alias_listing, justfile: " foo: echo foo alias f := foo ", args: ("--list"), stdout: " Available recipes: foo f # alias for `foo` ", } test! { name: alias_listing_multiple_aliases, justfile: "foo:\n echo foo\nalias f := foo\nalias fo := foo", args: ("--list"), stdout: " Available recipes: foo f # alias for `foo` fo # alias for `foo` ", } test! { name: alias_listing_parameters, justfile: "foo PARAM='foo':\n echo {{PARAM}}\nalias f := foo", args: ("--list"), stdout: " Available recipes: foo PARAM='foo' f PARAM='foo' # alias for `foo` ", } test! { name: alias_listing_private, justfile: "foo PARAM='foo':\n echo {{PARAM}}\nalias _f := foo", args: ("--list"), stdout: " Available recipes: foo PARAM='foo' ", } test! { name: alias, justfile: "foo:\n echo foo\nalias f := foo", args: ("f"), stdout: "foo\n", stderr: "echo foo\n", } test! { name: alias_with_parameters, justfile: "foo value='foo':\n echo {{value}}\nalias f := foo", args: ("f", "bar"), stdout: "bar\n", stderr: "echo bar\n", } test! { name: bad_setting, justfile: " set foo ", stderr: " error: Unknown setting `foo` ——▶ justfile:1:5 │ 1 │ set foo │ ^^^ ", status: EXIT_FAILURE, } test! { name: bad_setting_with_keyword_name, justfile: " set if := 'foo' ", stderr: " error: Unknown setting `if` ——▶ justfile:1:5 │ 1 │ set if := 'foo' │ ^^ ", status: EXIT_FAILURE, } test! { name: alias_with_dependencies, justfile: "foo:\n echo foo\nbar: foo\nalias b := bar", args: ("b"), stdout: "foo\n", stderr: "echo foo\n", } test! { name: duplicate_alias, justfile: "alias foo := bar\nalias foo := baz\n", stderr: " error: Alias `foo` first defined on line 1 is redefined on line 2 ——▶ justfile:2:7 │ 2 │ alias foo := baz │ ^^^ ", status: EXIT_FAILURE, } test! { name: unknown_alias_target, justfile: "alias foo := bar\n", stderr: " error: Alias `foo` has an unknown target `bar` ——▶ justfile:1:7 │ 1 │ alias foo := bar │ ^^^ ", status: EXIT_FAILURE, } test! { name: alias_shadows_recipe, justfile: "bar:\n echo bar\nalias foo := bar\nfoo:\n echo foo", stderr: " error: Alias `foo` defined on line 3 is redefined as a recipe on line 4 ——▶ justfile:4:1 │ 4 │ foo: │ ^^^ ", status: EXIT_FAILURE, } test! { name: default, justfile: "default:\n echo hello\nother: \n echo bar", stdout: "hello\n", stderr: "echo hello\n", } test! { name: quiet, justfile: "default:\n @echo hello", stdout: "hello\n", } test! { name: verbose, justfile: "default:\n @echo hello", args: ("--verbose"), stdout: "hello\n", stderr: "===> Running recipe `default`...\necho hello\n", } test! { name: order, justfile: " b: a echo b @mv a b a: echo a @touch F @touch a d: c echo d @rm c c: b echo c @mv b c", args: ("a", "d"), stdout: "a\nb\nc\nd\n", stderr: "echo a\necho b\necho c\necho d\n", } test! { name: select, justfile: "b: @echo b a: @echo a d: @echo d c: @echo c", args: ("d", "c"), stdout: "d\nc\n", } test! { name: print, justfile: "b: echo b a: echo a d: echo d c: echo c", args: ("d", "c"), stdout: "d\nc\n", stderr: "echo d\necho c\n", } test! { name: status_passthrough, justfile: " hello: recipe: @exit 100", args: ("recipe"), stderr: "error: Recipe `recipe` failed on line 5 with exit code 100\n", status: 100, } test! { name: unknown_dependency, justfile: "bar:\nhello:\nfoo: bar baaaaaaaz hello", stderr: " error: Recipe `foo` has unknown dependency `baaaaaaaz` ——▶ justfile:3:10 │ 3 │ foo: bar baaaaaaaz hello │ ^^^^^^^^^ ", status: EXIT_FAILURE, } test! { name: backtick_success, justfile: "a := `printf Hello,`\nbar:\n printf '{{a + `printf ' world.'`}}'", stdout: "Hello, world.", stderr: "printf 'Hello, world.'\n", } test! { name: backtick_trimming, justfile: "a := `echo Hello,`\nbar:\n echo '{{a + `echo ' world.'`}}'", stdout: "Hello, world.\n", stderr: "echo 'Hello, world.'\n", } test! { name: backtick_code_assignment, justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'", stderr: " error: Backtick failed with exit code 100 ——▶ justfile:2:6 │ 2 │ a := `exit 100` │ ^^^^^^^^^^ ", status: 100, } test! { name: backtick_code_interpolation, justfile: "b := a\na := `echo hello`\nbar:\n echo '{{`exit 200`}}'", stderr: " error: Backtick failed with exit code 200 ——▶ justfile:4:10 │ 4 │ echo '{{`exit 200`}}' │ ^^^^^^^^^^ ", status: 200, } test! { name: backtick_code_interpolation_mod, justfile: "f:\n 無{{`exit 200`}}", stderr: " error: Backtick failed with exit code 200 ——▶ justfile:2:7 │ 2 │ 無{{`exit 200`}} │ ^^^^^^^^^^ ", status: 200, } test! { name: backtick_code_interpolation_tab, justfile: " backtick-fail: \techo {{`exit 200`}} ", stderr: " error: Backtick failed with exit code 200 ——▶ justfile:2:9 │ 2 │ echo {{`exit 200`}} │ ^^^^^^^^^^ ", status: 200, } test! { name: backtick_code_interpolation_tabs, justfile: " backtick-fail: \techo {{\t`exit 200`}} ", stderr: "error: Backtick failed with exit code 200 ——▶ justfile:2:10 │ 2 │ echo {{ `exit 200`}} │ ^^^^^^^^^^ ", status: 200, } test! { name: backtick_code_interpolation_inner_tab, justfile: " backtick-fail: \techo {{\t`exit\t\t200`}} ", stderr: " error: Backtick failed with exit code 200 ——▶ justfile:2:10 │ 2 │ echo {{ `exit 200`}} │ ^^^^^^^^^^^^^^^^^ ", status: 200, } test! { name: backtick_code_interpolation_leading_emoji, justfile: " backtick-fail: \techo 😬{{`exit 200`}} ", stderr: " error: Backtick failed with exit code 200 ——▶ justfile:2:13 │ 2 │ echo 😬{{`exit 200`}} │ ^^^^^^^^^^ ", status: 200, } test! { name: backtick_code_interpolation_unicode_hell, justfile: " backtick-fail: \techo \t\t\t😬鎌鼬{{\t\t`exit 200 # \t\t\tabc`}}\t\t\t😬鎌鼬 ", stderr: " error: Backtick failed with exit code 200 ——▶ justfile:2:24 │ 2 │ echo 😬鎌鼬{{ `exit 200 # abc`}} 😬鎌鼬 │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ", status: 200, } test! { name: backtick_code_long, justfile: " b := a a := `echo hello` bar: echo '{{`exit 200`}}' ", stderr: " error: Backtick failed with exit code 200 ——▶ justfile:10:10 │ 10 │ echo '{{`exit 200`}}' │ ^^^^^^^^^^ ", status: 200, } test! { name: shebang_backtick_failure, justfile: "foo: #!/bin/sh echo hello echo {{`exit 123`}}", stdout: "", stderr: " error: Backtick failed with exit code 123 ——▶ justfile:4:9 │ 4 │ echo {{`exit 123`}} │ ^^^^^^^^^^ ", status: 123, } test! { name: command_backtick_failure, justfile: "foo: echo hello echo {{`exit 123`}}", stdout: "hello\n", stderr: " echo hello error: Backtick failed with exit code 123 ——▶ justfile:3:9 │ 3 │ echo {{`exit 123`}} │ ^^^^^^^^^^ ", status: 123, } test! { name: assignment_backtick_failure, justfile: "foo: echo hello echo {{`exit 111`}} a := `exit 222`", stdout: "", stderr: " error: Backtick failed with exit code 222 ——▶ justfile:4:6 │ 4 │ a := `exit 222` │ ^^^^^^^^^^ ", status: 222, } test! { name: unknown_override_options, justfile: "foo: echo hello echo {{`exit 111`}} a := `exit 222`", args: ("--set", "foo", "bar", "--set", "baz", "bob", "--set", "a", "b", "a", "b"), stderr: "error: Variables `baz` and `foo` overridden on the command line but not present \ in justfile\n", status: EXIT_FAILURE, } test! { name: unknown_override_args, justfile: "foo: echo hello echo {{`exit 111`}} a := `exit 222`", args: ("foo=bar", "baz=bob", "a=b", "a", "b"), stderr: "error: Variables `baz` and `foo` overridden on the command line but not present \ in justfile\n", status: EXIT_FAILURE, } test! { name: unknown_override_arg, justfile: "foo: echo hello echo {{`exit 111`}} a := `exit 222`", args: ("foo=bar", "a=b", "a", "b"), stderr: "error: Variable `foo` overridden on the command line but not present in justfile\n", status: EXIT_FAILURE, } test! { name: overrides_first, justfile: r#" foo := "foo" a := "a" baz := "baz" recipe arg: echo arg={{arg}} echo {{foo + a + baz}}"#, args: ("foo=bar", "a=b", "recipe", "baz=bar"), stdout: "arg=baz=bar\nbarbbaz\n", stderr: "echo arg=baz=bar\necho barbbaz\n", } test! { name: overrides_not_evaluated, justfile: r#" foo := `exit 1` a := "a" baz := "baz" recipe arg: echo arg={{arg}} echo {{foo + a + baz}}"#, args: ("foo=bar", "a=b", "recipe", "baz=bar"), stdout: "arg=baz=bar\nbarbbaz\n", stderr: "echo arg=baz=bar\necho barbbaz\n", } test! { name: dry_run, justfile: r#" var := `echo stderr 1>&2; echo backtick` command: @touch /this/is/not/a/file {{var}} echo {{`echo command interpolation`}} shebang: #!/bin/sh touch /this/is/not/a/file {{var}} echo {{`echo shebang interpolation`}}"#, args: ("--dry-run", "shebang", "command"), stdout: "", stderr: "#!/bin/sh touch /this/is/not/a/file `echo stderr 1>&2; echo backtick` echo `echo shebang interpolation` touch /this/is/not/a/file `echo stderr 1>&2; echo backtick` echo `echo command interpolation` ", } test! { name: line_error_spacing, justfile: r#" ^^^ "#, stdout: "", stderr: "error: Unknown start of token: ——▶ justfile:10:1 │ 10 │ ^^^ │ ^ ", status: EXIT_FAILURE, } test! { name: argument_single, justfile: " foo A: echo {{A}} ", args: ("foo", "ARGUMENT"), stdout: "ARGUMENT\n", stderr: "echo ARGUMENT\n", } test! { name: argument_multiple, justfile: " foo A B: echo A:{{A}} B:{{B}} ", args: ("foo", "ONE", "TWO"), stdout: "A:ONE B:TWO\n", stderr: "echo A:ONE B:TWO\n", } test! { name: argument_mismatch_more, justfile: " foo A B: echo A:{{A}} B:{{B}} ", args: ("foo", "ONE", "TWO", "THREE"), stdout: "", stderr: "error: Justfile does not contain recipe `THREE`.\n", status: EXIT_FAILURE, } test! { name: argument_mismatch_fewer, justfile: " foo A B: echo A:{{A}} B:{{B}} ", args: ("foo", "ONE"), stdout: "", stderr: "error: Recipe `foo` got 1 argument but takes 2\nusage:\n just foo A B\n", status: EXIT_FAILURE, } test! { name: argument_mismatch_more_with_default, justfile: " foo A B='B': echo A:{{A}} B:{{B}} ", args: ("foo", "ONE", "TWO", "THREE"), stdout: "", stderr: "error: Justfile does not contain recipe `THREE`.\n", status: EXIT_FAILURE, } test! { name: argument_mismatch_fewer_with_default, justfile: " foo A B C='C': echo A:{{A}} B:{{B}} C:{{C}} ", args: ("foo", "bar"), stdout: "", stderr: " error: Recipe `foo` got 1 argument but takes at least 2 usage: just foo A B C='C' ", status: EXIT_FAILURE, } test! { name: unknown_recipe, justfile: "hello:", args: ("foo"), stdout: "", stderr: "error: Justfile does not contain recipe `foo`.\n", status: EXIT_FAILURE, } test! { name: unknown_recipes, justfile: "hello:", args: ("foo", "bar"), stdout: "", stderr: "error: Justfile does not contain recipes `foo` or `bar`.\n", status: EXIT_FAILURE, } test! { name: color_always, justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'", args: ("--color", "always"), stdout: "", 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", status: 100, } test! { name: color_never, justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'", args: ("--color", "never"), stdout: "", stderr: "error: Backtick failed with exit code 100 ——▶ justfile:2:6 │ 2 │ a := `exit 100` │ ^^^^^^^^^^ ", status: 100, } test! { name: color_auto, justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'", args: ("--color", "auto"), stdout: "", stderr: "error: Backtick failed with exit code 100 ——▶ justfile:2:6 │ 2 │ a := `exit 100` │ ^^^^^^^^^^ ", status: 100, } test! { name: colors_no_context, justfile: " recipe: @exit 100", args: ("--color=always"), stdout: "", stderr: "\u{1b}[1;31merror\u{1b}[0m: \u{1b}[1m\ Recipe `recipe` failed on line 2 with exit code 100\u{1b}[0m\n", status: 100, } test! { name: dump, justfile: r#" # this recipe does something recipe a b +d: @exit 100"#, args: ("--dump"), stdout: "# this recipe does something recipe a b +d: @exit 100 ", } test! { name: mixed_whitespace, justfile: "bar:\n\t echo hello", stdout: "", stderr: "error: Found a mix of tabs and spaces in leading whitespace: `␉␠` Leading whitespace may consist of tabs or spaces, but not both ——▶ justfile:2:1 │ 2 │ echo hello │ ^^^^^ ", status: EXIT_FAILURE, } test! { name: extra_leading_whitespace, justfile: "bar:\n\t\techo hello\n\t\t\techo goodbye", stdout: "", stderr: "error: Recipe line has extra leading whitespace ——▶ justfile:3:3 │ 3 │ echo goodbye │ ^^^^^^^^^^^^^^^^ ", status: EXIT_FAILURE, } test! { name: inconsistent_leading_whitespace, justfile: "bar:\n\t\techo hello\n\t echo goodbye", stdout: "", stderr: "error: Recipe line has inconsistent leading whitespace. \ Recipe started with `␉␉` but found line with `␉␠` ——▶ justfile:3:1 │ 3 │ echo goodbye │ ^^^^^ ", status: EXIT_FAILURE, } test! { name: required_after_default, justfile: "bar:\nhello baz arg='foo' bar:", stdout: "", stderr: "error: Non-default parameter `bar` follows default parameter ——▶ justfile:2:21 │ 2 │ hello baz arg='foo' bar: │ ^^^ ", status: EXIT_FAILURE, } test! { name: required_after_plus_variadic, justfile: "bar:\nhello baz +arg bar:", stdout: "", stderr: "error: Parameter `bar` follows variadic parameter ——▶ justfile:2:16 │ 2 │ hello baz +arg bar: │ ^^^ ", status: EXIT_FAILURE, } test! { name: required_after_star_variadic, justfile: "bar:\nhello baz *arg bar:", stdout: "", stderr: "error: Parameter `bar` follows variadic parameter ——▶ justfile:2:16 │ 2 │ hello baz *arg bar: │ ^^^ ", status: EXIT_FAILURE, } test! { name: use_string_default, justfile: r#" bar: hello baz arg="XYZ\t\" ": echo '{{baz}}...{{arg}}' "#, args: ("hello", "ABC"), stdout: "ABC...XYZ\t\"\t\n", stderr: "echo 'ABC...XYZ\t\"\t'\n", } test! { name: use_raw_string_default, justfile: r#" bar: hello baz arg='XYZ" ': printf '{{baz}}...{{arg}}' "#, args: ("hello", "ABC"), stdout: "ABC...XYZ\"\t", stderr: "printf 'ABC...XYZ\"\t'\n", } test! { name: supply_use_default, justfile: r#" hello a b='B' c='C': echo {{a}} {{b}} {{c}} "#, args: ("hello", "0", "1"), stdout: "0 1 C\n", stderr: "echo 0 1 C\n", } test! { name: supply_defaults, justfile: r#" hello a b='B' c='C': echo {{a}} {{b}} {{c}} "#, args: ("hello", "0", "1", "2"), stdout: "0 1 2\n", stderr: "echo 0 1 2\n", } test! { name: list, justfile: r#" # this does a thing hello a b='B ' c='C': echo {{a}} {{b}} {{c}} # this comment will be ignored a Z="\t z": # this recipe will not appear _private-recipe: "#, args: ("--list"), stdout: r#" Available recipes: a Z="\t z" hello a b='B ' c='C' # this does a thing "#, } test! { name: list_alignment, justfile: r#" # this does a thing hello a b='B ' c='C': echo {{a}} {{b}} {{c}} # something else a Z="\t z": # this recipe will not appear _private-recipe: "#, args: ("--list"), stdout: r#" Available recipes: a Z="\t z" # something else hello a b='B ' c='C' # this does a thing "#, } test! { name: list_alignment_long, justfile: r#" # this does a thing hello a b='B ' c='C': echo {{a}} {{b}} {{c}} # this does another thing x a b='B ' c='C': echo {{a}} {{b}} {{c}} # something else this-recipe-is-very-very-very-important Z="\t z": # this recipe will not appear _private-recipe: "#, args: ("--list"), stdout: r#" Available recipes: hello a b='B ' c='C' # this does a thing this-recipe-is-very-very-very-important Z="\t z" # something else x a b='B ' c='C' # this does another thing "#, } test! { name: list_sorted, justfile: r#" alias c := b b: a: "#, args: ("--list"), stdout: r#" Available recipes: a b c # alias for `b` "#, } test! { name: list_unsorted, justfile: r#" alias c := b b: a: "#, args: ("--list", "--unsorted"), stdout: r#" Available recipes: b c # alias for `b` a "#, } test! { name: list_heading, justfile: r#" a: b: "#, args: ("--list", "--list-heading", "Cool stuff…\n"), stdout: r#" Cool stuff… a b "#, } test! { name: list_prefix, justfile: r#" a: b: "#, args: ("--list", "--list-prefix", "····"), stdout: r#" Available recipes: ····a ····b "#, } test! { name: list_empty_prefix_and_heading, justfile: r#" a: b: "#, args: ("--list", "--list-heading", "", "--list-prefix", ""), stdout: r#" a b "#, } test! { name: run_suggestion, justfile: r#" hello a b='B ' c='C': echo {{a}} {{b}} {{c}} a Z="\t z": "#, args: ("hell"), stdout: "", stderr: "error: Justfile does not contain recipe `hell`.\nDid you mean `hello`?\n", status: EXIT_FAILURE, } test! { name: line_continuation_with_space, justfile: r" foo: echo a\ b \ c ", stdout: "ab c\n", stderr: "echo ab c\n", } test! { name: line_continuation_with_quoted_space, justfile: r" foo: echo 'a\ b \ c' ", stdout: "ab c\n", stderr: "echo 'ab c'\n", } test! { name: line_continuation_no_space, justfile: r" foo: echo a\ b\ c ", stdout: "abc\n", stderr: "echo abc\n", } test! { name: infallible_command, justfile: r#" infallible: -exit 101 "#, stderr: "exit 101\n", status: EXIT_SUCCESS, } test! { name: infallible_with_failing, justfile: r#" infallible: -exit 101 exit 202 "#, stderr: r#"exit 101 exit 202 error: Recipe `infallible` failed on line 3 with exit code 202 "#, status: 202, } test! { name: quiet_recipe, justfile: r#" @quiet: # a # b @echo c "#, stdout: "c\n", stderr: "echo c\n", } test! { name: quiet_shebang_recipe, justfile: r#" @quiet: #!/bin/sh echo hello "#, stdout: "hello\n", stderr: "#!/bin/sh\necho hello\n", } #[cfg(not(windows))] test! { name: shebang_line_numbers, justfile: r#" quiet: #!/usr/bin/env cat a b c "#, stdout: " #!/usr/bin/env cat a b c ", } #[cfg(windows)] test! { name: shebang_line_numbers, justfile: r#" quiet: #!/usr/bin/env cat a b c "#, stdout: " a b c ", } test! { name: complex_dependencies, justfile: r#" a: b b: c: b a "#, args: ("b"), stdout: "", } test! { name: unknown_function_in_assignment, justfile: r#"foo := foo() + "hello" bar:"#, args: ("bar"), stdout: "", stderr: r#"error: Call to unknown function `foo` ——▶ justfile:1:8 │ 1 │ foo := foo() + "hello" │ ^^^ "#, status: EXIT_FAILURE, } test! { name: dependency_takes_arguments_exact, justfile: " a FOO: b: a ", args: ("b"), stdout: "", stderr: "error: Dependency `a` got 0 arguments but takes 1 argument ——▶ justfile:2:4 │ 2 │ b: a │ ^ ", status: EXIT_FAILURE, } test! { name: dependency_takes_arguments_at_least, justfile: " a FOO LUZ='hello': b: a ", args: ("b"), stdout: "", stderr: "error: Dependency `a` got 0 arguments but takes at least 1 argument ——▶ justfile:2:4 │ 2 │ b: a │ ^ ", status: EXIT_FAILURE, } test! { name: dependency_takes_arguments_at_most, justfile: " a FOO LUZ='hello': b: (a '0' '1' '2') ", args: ("b"), stdout: "", stderr: "error: Dependency `a` got 3 arguments but takes at most 2 arguments ——▶ justfile:2:5 │ 2 │ b: (a '0' '1' '2') │ ^ ", status: EXIT_FAILURE, } test! { name: duplicate_parameter, justfile: "a foo foo:", args: ("a"), stdout: "", stderr: "error: Recipe `a` has duplicate parameter `foo` ——▶ justfile:1:7 │ 1 │ a foo foo: │ ^^^ ", status: EXIT_FAILURE, } test! { name: duplicate_recipe, justfile: "b:\nb:", args: ("b"), stdout: "", stderr: "error: Recipe `b` first defined on line 1 is redefined on line 2 ——▶ justfile:2:1 │ 2 │ b: │ ^ ", status: EXIT_FAILURE, } test! { name: duplicate_variable, justfile: "a := 'hello'\na := 'hello'\nfoo:", args: ("foo"), stdout: "", stderr: "error: Variable `a` has multiple definitions ——▶ justfile:2:1 │ 2 │ a := 'hello' │ ^ ", status: EXIT_FAILURE, } test! { name: unexpected_token_in_dependency_position, justfile: "foo: 'bar'", args: ("foo"), stdout: "", stderr: "error: Expected '&&', comment, end of file, end of line, \ identifier, or '(', but found string ——▶ justfile:1:6 │ 1 │ foo: 'bar' │ ^^^^^ ", status: EXIT_FAILURE, } test! { name: unexpected_token_after_name, justfile: "foo 'bar'", args: ("foo"), stdout: "", stderr: "error: Expected '*', ':', '$', identifier, or '+', but found string ——▶ justfile:1:5 │ 1 │ foo 'bar' │ ^^^^^ ", status: EXIT_FAILURE, } test! { name: self_dependency, justfile: "a: a", args: ("a"), stdout: "", stderr: "error: Recipe `a` depends on itself ——▶ justfile:1:4 │ 1 │ a: a │ ^ ", status: EXIT_FAILURE, } test! { name: long_circular_recipe_dependency, justfile: "a: b\nb: c\nc: d\nd: a", args: ("a"), stdout: "", stderr: "error: Recipe `d` has circular dependency `a -> b -> c -> d -> a` ——▶ justfile:4:4 │ 4 │ d: a │ ^ ", status: EXIT_FAILURE, } test! { name: variable_self_dependency, justfile: "z := z\na:", args: ("a"), stdout: "", stderr: "error: Variable `z` is defined in terms of itself ——▶ justfile:1:1 │ 1 │ z := z │ ^ ", status: EXIT_FAILURE, } test! { name: variable_circular_dependency, justfile: "x := y\ny := z\nz := x\na:", args: ("a"), stdout: "", stderr: "error: Variable `x` depends on its own value: `x -> y -> z -> x` ——▶ justfile:1:1 │ 1 │ x := y │ ^ ", status: EXIT_FAILURE, } test! { name: variable_circular_dependency_with_additional_variable, justfile: " a := '' x := y y := x a: ", args: ("a"), stdout: "", stderr: "error: Variable `x` depends on its own value: `x -> y -> x` ——▶ justfile:2:1 │ 2 │ x := y │ ^ ", status: EXIT_FAILURE, } test! { name: plus_variadic_recipe, justfile: " a x y +z: echo {{x}} {{y}} {{z}} ", args: ("a", "0", "1", "2", "3", " 4 "), stdout: "0 1 2 3 4\n", stderr: "echo 0 1 2 3 4 \n", } test! { name: plus_variadic_ignore_default, justfile: " a x y +z='HELLO': echo {{x}} {{y}} {{z}} ", args: ("a", "0", "1", "2", "3", " 4 "), stdout: "0 1 2 3 4\n", stderr: "echo 0 1 2 3 4 \n", } test! { name: plus_variadic_use_default, justfile: " a x y +z='HELLO': echo {{x}} {{y}} {{z}} ", args: ("a", "0", "1"), stdout: "0 1 HELLO\n", stderr: "echo 0 1 HELLO\n", } test! { name: plus_variadic_too_few, justfile: " a x y +z: echo {{x}} {{y}} {{z}} ", args: ("a", "0", "1"), stdout: "", stderr: "error: Recipe `a` got 2 arguments but takes at least 3\nusage:\n just a x y +z\n", status: EXIT_FAILURE, } test! { name: star_variadic_recipe, justfile: " a x y *z: echo {{x}} {{y}} {{z}} ", args: ("a", "0", "1", "2", "3", " 4 "), stdout: "0 1 2 3 4\n", stderr: "echo 0 1 2 3 4 \n", } test! { name: star_variadic_none, justfile: " a x y *z: echo {{x}} {{y}} {{z}} ", args: ("a", "0", "1"), stdout: "0 1\n", stderr: "echo 0 1 \n", } test! { name: star_variadic_ignore_default, justfile: " a x y *z='HELLO': echo {{x}} {{y}} {{z}} ", args: ("a", "0", "1", "2", "3", " 4 "), stdout: "0 1 2 3 4\n", stderr: "echo 0 1 2 3 4 \n", } test! { name: star_variadic_use_default, justfile: " a x y *z='HELLO': echo {{x}} {{y}} {{z}} ", args: ("a", "0", "1"), stdout: "0 1 HELLO\n", stderr: "echo 0 1 HELLO\n", } test! { name: star_then_plus_variadic, justfile: " foo *a +b: echo {{a}} {{b}} ", stdout: "", stderr: "error: Expected \':\' or \'=\', but found \'+\' ——▶ justfile:1:8 │ 1 │ foo *a +b: │ ^ ", status: EXIT_FAILURE, } test! { name: plus_then_star_variadic, justfile: " foo +a *b: echo {{a}} {{b}} ", stdout: "", stderr: "error: Expected \':\' or \'=\', but found \'*\' ——▶ justfile:1:8 │ 1 │ foo +a *b: │ ^ ", status: EXIT_FAILURE, } test! { name: argument_grouping, justfile: " FOO A B='blarg': echo foo: {{A}} {{B}} BAR X: echo bar: {{X}} BAZ +Z: echo baz: {{Z}} ", args: ("BAR", "0", "FOO", "1", "2", "BAZ", "3", "4", "5"), stdout: "bar: 0\nfoo: 1 2\nbaz: 3 4 5\n", stderr: "echo bar: 0\necho foo: 1 2\necho baz: 3 4 5\n", } test! { name: missing_second_dependency, justfile: " x: a: x y ", stdout: "", stderr: "error: Recipe `a` has unknown dependency `y` ——▶ justfile:3:6 │ 3 │ a: x y │ ^ ", status: EXIT_FAILURE, } test! { name: list_colors, justfile: " # comment a B C +D='hello': echo {{B}} {{C}} {{D}} ", args: ("--color", "always", "--list"), stdout: " Available recipes: a \ \u{1b}[36mB\u{1b}[0m \u{1b}[36mC\u{1b}[0m \u{1b}[35m+\ \u{1b}[0m\u{1b}[36mD\u{1b}[0m=\u{1b}[32m'hello'\u{1b}[0m \ \u{1b}[34m#\u{1b}[0m \u{1b}[34mcomment\u{1b}[0m ", } test! { name: run_colors, justfile: " # comment a: echo hi ", args: ("--color", "always", "--highlight", "--verbose"), stdout: "hi\n", stderr: "\u{1b}[1;36m===> Running recipe `a`...\u{1b}[0m\n\u{1b}[1mecho hi\u{1b}[0m\n", } test! { name: no_highlight, justfile: " # comment a: echo hi ", args: ("--color", "always", "--highlight", "--no-highlight", "--verbose"), stdout: "hi\n", stderr: "\u{1b}[1;36m===> Running recipe `a`...\u{1b}[0m\necho hi\n", } test! { name: trailing_flags, justfile: " echo A B C: echo {{A}} {{B}} {{C}} ", args: ("echo", "--some", "--awesome", "--flags"), stdout: "--some --awesome --flags\n", stderr: "echo --some --awesome --flags\n", } test! { name: comment_before_variable, justfile: " # A:='1' echo: echo {{A}} ", args: ("echo"), stdout: "1\n", stderr: "echo 1\n", } test! { name: dotenv_variable_in_recipe, justfile: " # set dotenv-load echo: echo $DOTENV_KEY ", stdout: "dotenv-value\n", stderr: "echo $DOTENV_KEY\n", } test! { name: dotenv_variable_in_backtick, justfile: " # set dotenv-load X:=`echo $DOTENV_KEY` echo: echo {{X}} ", stdout: "dotenv-value\n", stderr: "echo dotenv-value\n", } test! { name: dotenv_variable_in_function_in_recipe, justfile: " # set dotenv-load echo: echo {{env_var_or_default('DOTENV_KEY', 'foo')}} echo {{env_var('DOTENV_KEY')}} ", stdout: "dotenv-value\ndotenv-value\n", stderr: "echo dotenv-value\necho dotenv-value\n", } test! { name: dotenv_variable_in_function_in_backtick, justfile: " # set dotenv-load X:=env_var_or_default('DOTENV_KEY', 'foo') Y:=env_var('DOTENV_KEY') echo: echo {{X}} echo {{Y}} ", stdout: "dotenv-value\ndotenv-value\n", stderr: "echo dotenv-value\necho dotenv-value\n", } test! { name: no_dotenv, justfile: " # X:=env_var_or_default('DOTENV_KEY', 'DEFAULT') echo: echo {{X}} ", args: ("--no-dotenv"), stdout: "DEFAULT\n", stderr: "echo DEFAULT\n", } test! { name: dotenv_env_var_override, justfile: " # echo: echo $DOTENV_KEY ", env: {"DOTENV_KEY": "not-the-dotenv-value",}, stdout: "not-the-dotenv-value\n", stderr: "echo $DOTENV_KEY\n", } test! { name: invalid_escape_sequence_message, justfile: r#" X := "\'" "#, stdout: "", stderr: r#"error: `\'` is not a valid escape sequence ——▶ justfile:1:6 │ 1 │ X := "\'" │ ^^^^ "#, status: EXIT_FAILURE, } test! { name: unknown_variable_in_default, justfile: " foo x=bar: ", stdout: "", stderr: r#"error: Variable `bar` not defined ——▶ justfile:1:7 │ 1 │ foo x=bar: │ ^^^ "#, status: EXIT_FAILURE, } test! { name: unknown_function_in_default, justfile: " foo x=bar(): ", stdout: "", stderr: r#"error: Call to unknown function `bar` ——▶ justfile:1:7 │ 1 │ foo x=bar(): │ ^^^ "#, status: EXIT_FAILURE, } test! { name: default_string, justfile: " foo x='bar': echo {{x}} ", stdout: "bar\n", stderr: "echo bar\n", } test! { name: default_concatenation, justfile: " foo x=(`echo foo` + 'bar'): echo {{x}} ", stdout: "foobar\n", stderr: "echo foobar\n", } test! { name: default_backtick, justfile: " foo x=`echo foo`: echo {{x}} ", stdout: "foo\n", stderr: "echo foo\n", } test! { name: default_variable, justfile: " y := 'foo' foo x=y: echo {{x}} ", stdout: "foo\n", stderr: "echo foo\n", } test! { name: unterminated_interpolation_eol, justfile: " foo: echo {{ ", stderr: r#" error: Unterminated interpolation ——▶ justfile:2:8 │ 2 │ echo {{ │ ^^ "#, status: EXIT_FAILURE, } test! { name: unterminated_interpolation_eof, justfile: " foo: echo {{ ", stderr: r#" error: Unterminated interpolation ——▶ justfile:2:8 │ 2 │ echo {{ │ ^^ "#, status: EXIT_FAILURE, } test! { name: unknown_start_of_token, justfile: " assembly_source_files = %(wildcard src/arch/$(arch)/*.s) ", stderr: r#" error: Unknown start of token: ——▶ justfile:1:25 │ 1 │ assembly_source_files = %(wildcard src/arch/$(arch)/*.s) │ ^ "#, status: EXIT_FAILURE, } test! { name: backtick_variable_cat, justfile: " stdin := `cat` default: echo {{stdin}} ", stdin: "STDIN", stdout: "STDIN\n", stderr: "echo STDIN\n", } test! { name: backtick_default_cat_stdin, justfile: " default stdin = `cat`: echo {{stdin}} ", stdin: "STDIN", stdout: "STDIN\n", stderr: "echo STDIN\n", } test! { name: backtick_default_cat_justfile, justfile: " default stdin = `cat justfile`: echo '{{stdin}}' ", stdout: " default stdin = `cat justfile`: echo {{stdin}} ", stderr: " echo 'default stdin = `cat justfile`: echo '{{stdin}}'' ", } test! { name: backtick_variable_read_single, justfile: " password := `read PW && echo $PW` default: echo {{password}} ", stdin: "foobar\n", stdout: "foobar\n", stderr: "echo foobar\n", } test! { name: backtick_variable_read_multiple, justfile: " a := `read A && echo $A` b := `read B && echo $B` default: echo {{a}} echo {{b}} ", stdin: "foo\nbar\n", stdout: "foo\nbar\n", stderr: "echo foo\necho bar\n", } test! { name: backtick_default_read_multiple, justfile: " default a=`read A && echo $A` b=`read B && echo $B`: echo {{a}} echo {{b}} ", stdin: "foo\nbar\n", stdout: "foo\nbar\n", stderr: "echo foo\necho bar\n", } test! { name: old_equals_assignment_syntax_produces_error, justfile: " foo = 'bar' default: echo {{foo}} ", stderr: " error: Expected '*', ':', '$', identifier, or '+', but found '=' ——▶ justfile:1:5 │ 1 │ foo = 'bar' │ ^ ", status: EXIT_FAILURE, } test! { name: dependency_argument_string, justfile: " release: (build 'foo') (build 'bar') build target: echo 'Building {{target}}...' ", args: (), stdout: "Building foo...\nBuilding bar...\n", stderr: "echo 'Building foo...'\necho 'Building bar...'\n", shell: false, } test! { name: dependency_argument_parameter, justfile: " default: (release '1.0') release version: (build 'foo' version) (build 'bar' version) build target version: echo 'Building {{target}}@{{version}}...' ", args: (), stdout: "Building foo@1.0...\nBuilding bar@1.0...\n", stderr: "echo 'Building foo@1.0...'\necho 'Building bar@1.0...'\n", shell: false, } test! { name: dependency_argument_function, justfile: " foo: (bar env_var_or_default('x', 'y')) bar arg: echo {{arg}} ", args: (), stdout: "y\n", stderr: "echo y\n", shell: false, } test! { name: env_function_as_env_var, justfile: " foo: (bar env('x')) bar arg: echo {{arg}} ", args: (), env: { "x": "z", }, stdout: "z\n", stderr: "echo z\n", shell: false, } test! { name: env_function_as_env_var_or_default, justfile: " foo: (bar env('x', 'y')) bar arg: echo {{arg}} ", args: (), env: { "x": "z", }, stdout: "z\n", stderr: "echo z\n", shell: false, } test! { name: env_function_as_env_var_with_existing_env_var, justfile: " foo: (bar env('x')) bar arg: echo {{arg}} ", args: (), env: { "x": "z", }, stdout: "z\n", stderr: "echo z\n", shell: false, } test! { name: env_function_as_env_var_or_default_with_existing_env_var, justfile: " foo: (bar env('x', 'y')) bar arg: echo {{arg}} ", args: (), env: { "x": "z", }, stdout: "z\n", stderr: "echo z\n", shell: false, } test! { name: dependency_argument_backtick, justfile: " export X := 'X' foo: (bar `echo $X`) bar arg: echo {{arg}} echo $X ", args: (), stdout: "X\nX\n", stderr: "echo X\necho $X\n", shell: false, } test! { name: dependency_argument_assignment, justfile: " v := '1.0' default: (release v) release version: echo Release {{version}}... ", args: (), stdout: "Release 1.0...\n", stderr: "echo Release 1.0...\n", shell: false, } test! { name: dependency_argument_plus_variadic, justfile: " foo: (bar 'A' 'B' 'C') bar +args: echo {{args}} ", args: (), stdout: "A B C\n", stderr: "echo A B C\n", shell: false, } test! { name: duplicate_dependency_no_args, justfile: " foo: bar bar bar bar bar: echo BAR ", args: (), stdout: "BAR\n", stderr: "echo BAR\n", shell: false, } test! { name: duplicate_dependency_argument, justfile: " foo: (bar 'BAR') (bar `echo BAR`) bar bar: echo {{bar}} ", args: (), stdout: "BAR\n", stderr: "echo BAR\n", shell: false, } test! { name: parameter_cross_reference_error, justfile: " foo: bar a b=a: ", args: (), stdout: "", stderr: " error: Variable `a` not defined ——▶ justfile:3:9 │ 3 │ bar a b=a: │ ^ ", status: EXIT_FAILURE, shell: false, } #[cfg(windows)] test! { name: pwsh_invocation_directory, justfile: r#" set shell := ["pwsh", "-NoProfile", "-c"] pwd: @Test-Path {{invocation_directory()}} > result.txt "#, args: (), stdout: "", stderr: "", status: EXIT_SUCCESS, shell: false, } test! { name: variables, justfile: " z := 'a' a := 'z' ", args: ("--variables"), stdout: "a z\n", stderr: "", shell: false, } test! { name: interpolation_evaluation_ignore_quiet, justfile: r#" foo: {{"@echo foo 2>/dev/null"}} "#, args: (), stdout: "", stderr: " @echo foo 2>/dev/null error: Recipe `foo` failed on line 2 with exit code 127 ", status: 127, shell: false, } test! { name: interpolation_evaluation_ignore_quiet_continuation, justfile: r#" foo: {{""}}\ @echo foo 2>/dev/null "#, args: (), stdout: "", stderr: " @echo foo 2>/dev/null error: Recipe `foo` failed on line 3 with exit code 127 ", status: 127, shell: false, } test! { name: brace_escape, justfile: " foo: echo '{{{{' ", stdout: "{{\n", stderr: " echo '{{' ", } test! { name: brace_escape_extra, justfile: " foo: echo '{{{{{' ", stdout: "{{{\n", stderr: " echo '{{{' ", } test! { name: multi_line_string_in_interpolation, justfile: " foo: echo {{'a echo b echo c'}}z echo baz ", stdout: "a\nb\ncz\nbaz\n", stderr: "echo a\n echo b\n echo cz\necho baz\n", } #[cfg(windows)] test! { name: windows_interpreter_path_no_base, justfile: r#" foo: #!powershell exit 0 "#, args: (), } just-1.21.0/tests/modules.rs000064400000000000000000000253571046102023000140660ustar 00000000000000use super::*; #[test] fn modules_are_unstable() { Test::new() .justfile( " mod foo ", ) .arg("foo") .arg("foo") .stderr( "error: Modules are currently unstable. \ Invoke `just` with the `--unstable` flag to enable unstable features.\n", ) .status(EXIT_FAILURE) .run(); } #[test] fn default_recipe_in_submodule_must_have_no_arguments() { Test::new() .write("foo.just", "foo bar:\n @echo FOO") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .stderr("error: Recipe `foo` cannot be used as default recipe since it requires at least 1 argument.\n") .status(EXIT_FAILURE) .run(); } #[test] fn module_recipes_can_be_run_as_subcommands() { Test::new() .write("foo.just", "foo:\n @echo FOO") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("foo") .stdout("FOO\n") .run(); } #[test] fn assignments_are_evaluated_in_modules() { Test::new() .write("foo.just", "bar := 'CHILD'\nfoo:\n @echo {{bar}}") .justfile( " mod foo bar := 'PARENT' ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("foo") .stdout("CHILD\n") .run(); } #[test] fn module_subcommand_runs_default_recipe() { Test::new() .write("foo.just", "foo:\n @echo FOO") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .stdout("FOO\n") .run(); } #[test] fn modules_can_contain_other_modules() { Test::new() .write("bar.just", "baz:\n @echo BAZ") .write("foo.just", "mod bar") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("bar") .arg("baz") .stdout("BAZ\n") .run(); } #[test] fn circular_module_imports_are_detected() { Test::new() .write("bar.just", "mod foo") .write("foo.just", "mod bar") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("bar") .arg("baz") .stderr_regex(path_for_regex( "error: Import `.*/foo.just` in `.*/bar.just` is circular\n", )) .status(EXIT_FAILURE) .run(); } #[test] fn modules_use_module_settings() { Test::new() .write( "foo.just", "set allow-duplicate-recipes\nfoo:\nfoo:\n @echo FOO\n", ) .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("foo") .stdout("FOO\n") .run(); Test::new() .write("foo.just", "\nfoo:\nfoo:\n @echo FOO\n") .justfile( " mod foo set allow-duplicate-recipes ", ) .test_round_trip(false) .status(EXIT_FAILURE) .arg("--unstable") .arg("foo") .arg("foo") .stderr( " error: Recipe `foo` first defined on line 2 is redefined on line 3 ——▶ foo.just:3:1 │ 3 │ foo: │ ^^^ ", ) .run(); } #[test] fn modules_conflict_with_recipes() { Test::new() .write("foo.just", "") .justfile( " mod foo foo: ", ) .stderr( " error: Module `foo` defined on line 1 is redefined as a recipe on line 2 ——▶ justfile:2:1 │ 2 │ foo: │ ^^^ ", ) .test_round_trip(false) .status(EXIT_FAILURE) .arg("--unstable") .run(); } #[test] fn modules_conflict_with_aliases() { Test::new() .write("foo.just", "") .justfile( " mod foo bar: alias foo := bar ", ) .stderr( " error: Module `foo` defined on line 1 is redefined as an alias on line 3 ——▶ justfile:3:7 │ 3 │ alias foo := bar │ ^^^ ", ) .test_round_trip(false) .status(EXIT_FAILURE) .arg("--unstable") .run(); } #[test] fn modules_conflict_with_other_modules() { Test::new() .write("foo.just", "") .justfile( " mod foo mod foo bar: ", ) .test_round_trip(false) .status(EXIT_FAILURE) .stderr( " error: Module `foo` first defined on line 1 is redefined on line 2 ——▶ justfile:2:5 │ 2 │ mod foo │ ^^^ ", ) .arg("--unstable") .run(); } #[test] fn modules_are_dumped_correctly() { Test::new() .write("foo.just", "foo:\n @echo FOO") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("--dump") .stdout("mod foo\n") .run(); } #[test] fn optional_modules_are_dumped_correctly() { Test::new() .write("foo.just", "foo:\n @echo FOO") .justfile( " mod? foo ", ) .test_round_trip(false) .arg("--unstable") .arg("--dump") .stdout("mod? foo\n") .run(); } #[test] fn modules_can_be_in_subdirectory() { Test::new() .write("foo/mod.just", "foo:\n @echo FOO") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("foo") .stdout("FOO\n") .run(); } #[test] fn modules_in_subdirectory_can_be_named_justfile() { Test::new() .write("foo/justfile", "foo:\n @echo FOO") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("foo") .stdout("FOO\n") .run(); } #[test] fn modules_in_subdirectory_can_be_named_justfile_with_any_case() { Test::new() .write("foo/JUSTFILE", "foo:\n @echo FOO") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("foo") .stdout("FOO\n") .run(); } #[test] fn modules_in_subdirectory_can_have_leading_dot() { Test::new() .write("foo/.justfile", "foo:\n @echo FOO") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("foo") .stdout("FOO\n") .run(); } #[test] fn modules_require_unambiguous_file() { Test::new() .write("foo/justfile", "foo:\n @echo FOO") .write("foo.just", "foo:\n @echo FOO") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .status(EXIT_FAILURE) .stderr( " error: Found multiple source files for module `foo`: `foo.just` and `foo/justfile` ——▶ justfile:1:5 │ 1 │ mod foo │ ^^^ ", ) .run(); } #[test] fn missing_module_file_error() { Test::new() .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .status(EXIT_FAILURE) .stderr( " error: Could not find source file for module `foo`. ——▶ justfile:1:5 │ 1 │ mod foo │ ^^^ ", ) .run(); } #[test] fn missing_optional_modules_do_not_trigger_error() { Test::new() .justfile( " mod? foo bar: @echo BAR ", ) .test_round_trip(false) .arg("--unstable") .stdout("BAR\n") .run(); } #[test] fn missing_optional_modules_do_not_conflict() { Test::new() .justfile( " mod? foo mod? foo mod foo 'baz.just' ", ) .write("baz.just", "baz:\n @echo BAZ") .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("baz") .stdout("BAZ\n") .run(); } #[test] fn list_displays_recipes_in_submodules() { Test::new() .write("foo.just", "bar:\n @echo FOO") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("--list") .stdout( " Available recipes: foo: bar ", ) .run(); } #[test] fn root_dotenv_is_available_to_submodules() { Test::new() .write("foo.just", "foo:\n @echo $DOTENV_KEY") .justfile( " set dotenv-load mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("foo") .stdout("dotenv-value\n") .run(); } #[test] fn dotenv_settings_in_submodule_are_ignored() { Test::new() .write( "foo.just", "set dotenv-load := false\nfoo:\n @echo $DOTENV_KEY", ) .justfile( " set dotenv-load mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("foo") .stdout("dotenv-value\n") .run(); } #[test] fn modules_may_specify_path() { Test::new() .write("commands/foo.just", "foo:\n @echo FOO") .justfile( " mod foo 'commands/foo.just' ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("foo") .stdout("FOO\n") .run(); } #[test] fn modules_with_paths_are_dumped_correctly() { Test::new() .write("commands/foo.just", "foo:\n @echo FOO") .justfile( " mod foo 'commands/foo.just' ", ) .test_round_trip(false) .arg("--unstable") .arg("--dump") .stdout("mod foo 'commands/foo.just'\n") .run(); } #[test] fn optional_modules_with_paths_are_dumped_correctly() { Test::new() .write("commands/foo.just", "foo:\n @echo FOO") .justfile( " mod? foo 'commands/foo.just' ", ) .test_round_trip(false) .arg("--unstable") .arg("--dump") .stdout("mod? foo 'commands/foo.just'\n") .run(); } #[test] fn recipes_may_be_named_mod() { Test::new() .justfile( " mod foo: @echo FOO ", ) .test_round_trip(false) .arg("mod") .arg("bar") .stdout("FOO\n") .run(); } #[test] fn submodule_linewise_recipes_run_in_submodule_directory() { Test::new() .write("foo/bar", "BAR") .write("foo/mod.just", "foo:\n @cat bar") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("foo") .stdout("BAR") .run(); } #[test] fn submodule_shebang_recipes_run_in_submodule_directory() { Test::new() .write("foo/bar", "BAR") .write("foo/mod.just", "foo:\n #!/bin/sh\n cat bar") .justfile( " mod foo ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("foo") .stdout("BAR") .run(); } #[cfg(not(windows))] #[test] fn module_paths_beginning_with_tilde_are_expanded_to_homdir() { Test::new() .write("foobar/mod.just", "foo:\n @echo FOOBAR") .justfile( " mod foo '~/mod.just' ", ) .test_round_trip(false) .arg("--unstable") .arg("foo") .arg("foo") .stdout("FOOBAR\n") .env("HOME", "foobar") .run(); } just-1.21.0/tests/multibyte_char.rs000064400000000000000000000001341046102023000154130ustar 00000000000000use super::*; #[test] fn bugfix() { Test::new().justfile("foo:\nx := '''ǩ'''").run(); } just-1.21.0/tests/newline_escape.rs000064400000000000000000000030331046102023000153620ustar 00000000000000use super::*; #[test] fn newline_escape_deps() { Test::new() .justfile( " default: a \\ b \\ c a: echo a b: echo b c: echo c ", ) .stdout("a\nb\nc\n") .stderr("echo a\necho b\necho c\n") .run(); } #[test] fn newline_escape_deps_no_indent() { Test::new() .justfile( " default: a\\ b\\ c a: echo a b: echo b c: echo c ", ) .stdout("a\nb\nc\n") .stderr("echo a\necho b\necho c\n") .run(); } #[test] fn newline_escape_deps_linefeed() { Test::new() .justfile( " default: a\\\r b a: echo a b: echo b ", ) .stdout("a\nb\n") .stderr("echo a\necho b\n") .run(); } #[test] fn newline_escape_deps_invalid_esc() { Test::new() .justfile( " default: a\\ b ", ) .stdout("") .stderr( " error: `\\ ` is not a valid escape sequence ——▶ justfile:1:11 │ 1 │ default: a\\ b │ ^ ", ) .status(EXIT_FAILURE) .run(); } #[test] fn newline_escape_unpaired_linefeed() { Test::new() .justfile( " default:\\\ra", ) .stdout("") .stderr( " error: Unpaired carriage return ——▶ justfile:1:9 │ 1 │ default:\\\ra │ ^ ", ) .status(EXIT_FAILURE) .run(); } just-1.21.0/tests/no_cd.rs000064400000000000000000000010361046102023000134640ustar 00000000000000use super::*; #[test] fn linewise() { Test::new() .justfile( " [no-cd] foo: cat bar ", ) .current_dir("foo") .tree(tree! { foo: { bar: "hello", } }) .stderr("cat bar\n") .stdout("hello") .run(); } #[test] fn shebang() { Test::new() .justfile( " [no-cd] foo: #!/bin/sh cat bar ", ) .current_dir("foo") .tree(tree! { foo: { bar: "hello", } }) .stdout("hello") .run(); } just-1.21.0/tests/no_exit_message.rs000064400000000000000000000037351046102023000155630ustar 00000000000000use super::*; test! { name: recipe_exit_message_suppressed, justfile: r#" # This is a doc comment [no-exit-message] hello: @echo "Hello, World!" @exit 100 "#, stdout: "Hello, World!\n", stderr: "", status: 100, } test! { name: silent_recipe_exit_message_suppressed, justfile: r#" # This is a doc comment [no-exit-message] @hello: echo "Hello, World!" exit 100 "#, stdout: "Hello, World!\n", stderr: "", status: 100, } test! { name: recipe_has_doc_comment, justfile: r#" # This is a doc comment [no-exit-message] hello: @exit 100 "#, args: ("--list"), stdout: " Available recipes: hello # This is a doc comment ", } test! { name: unknown_attribute, justfile: r#" # This is a doc comment [unknown-attribute] hello: @exit 100 "#, stderr: r#" error: Unknown attribute `unknown-attribute` ——▶ justfile:2:2 │ 2 │ [unknown-attribute] │ ^^^^^^^^^^^^^^^^^ "#, status: EXIT_FAILURE, } test! { name: empty_attribute, justfile: r#" # This is a doc comment [] hello: @exit 100 "#, stderr: r#" error: Expected identifier, but found ']' ——▶ justfile:2:2 │ 2 │ [] │ ^ "#, status: EXIT_FAILURE, } test! { name: unattached_attribute_before_comment, justfile: r#" [no-exit-message] # This is a doc comment hello: @exit 100 "#, stderr: r#" error: Expected '@', '[', or identifier, but found comment ——▶ justfile:2:1 │ 2 │ # This is a doc comment │ ^^^^^^^^^^^^^^^^^^^^^^^ "#, status: EXIT_FAILURE, } test! { name: unattached_attribute_before_empty_line, justfile: r#" [no-exit-message] hello: @exit 100 "#, stderr: "error: Expected '@', '[', or identifier, but found end of line\n ——▶ justfile:2:1\n │\n2 │ \n │ ^\n", status: EXIT_FAILURE, } test! { name: shebang_exit_message_suppressed, justfile: r#" [no-exit-message] hello: #!/usr/bin/env bash echo 'Hello, World!' exit 100 "#, stdout: "Hello, World!\n", stderr: "", status: 100, } just-1.21.0/tests/os_attributes.rs000064400000000000000000000027611046102023000152770ustar 00000000000000use super::*; #[test] fn os_family() { Test::new() .justfile( " [unix] foo: echo bar [windows] foo: echo baz ", ) .stdout(if cfg!(unix) { "bar\n" } else if cfg!(windows) { "baz\n" } else { panic!("unexpected os family") }) .stderr(if cfg!(unix) { "echo bar\n" } else if cfg!(windows) { "echo baz\n" } else { panic!("unexpected os family") }) .run(); } #[test] fn os() { Test::new() .justfile( " [macos] foo: echo bar [windows] foo: echo baz [linux] foo: echo quxx ", ) .stdout(if cfg!(target_os = "macos") { "bar\n" } else if cfg!(windows) { "baz\n" } else if cfg!(target_os = "linux") { "quxx\n" } else { panic!("unexpected os family") }) .stderr(if cfg!(target_os = "macos") { "echo bar\n" } else if cfg!(windows) { "echo baz\n" } else if cfg!(target_os = "linux") { "echo quxx\n" } else { panic!("unexpected os family") }) .run(); } #[test] fn all() { Test::new() .justfile( " [macos] [windows] [linux] [unix] foo: echo bar ", ) .stdout("bar\n") .stderr("echo bar\n") .run(); } #[test] fn none() { Test::new() .justfile( " foo: echo bar ", ) .stdout("bar\n") .stderr("echo bar\n") .run(); } just-1.21.0/tests/parser.rs000064400000000000000000000002421046102023000136740ustar 00000000000000use super::*; #[test] fn dont_run_duplicate_recipes() { Test::new() .justfile( " set dotenv-load # foo bar: ", ) .run(); } just-1.21.0/tests/positional_arguments.rs000064400000000000000000000031511046102023000166500ustar 00000000000000test! { name: linewise, justfile: r#" set positional-arguments foo bar baz: echo $0 echo $1 echo $2 echo "$@" "#, args: ("foo", "hello", "goodbye"), stdout: " foo hello goodbye hello goodbye ", stderr: r#" echo $0 echo $1 echo $2 echo "$@" "#, } test! { name: variadic_linewise, justfile: r#" set positional-arguments foo *bar: echo $1 echo "$@" "#, args: ("foo", "a", "b", "c"), stdout: "a\na b c\n", stderr: "echo $1\necho \"$@\"\n", } test! { name: shebang, justfile: " set positional-arguments foo bar: #!/bin/sh echo $1 ", args: ("foo", "hello"), stdout: "hello\n", } test! { name: variadic_shebang, justfile: r#" set positional-arguments foo *bar: #!/bin/sh echo $1 echo "$@" "#, args: ("foo", "a", "b", "c"), stdout: "a\na b c\n", } test! { name: default_arguments, justfile: r#" set positional-arguments foo bar='baz': echo $1 "#, args: (), stdout: "baz\n", stderr: "echo $1\n", } test! { name: empty_variadic_is_undefined, justfile: r#" set positional-arguments foo *bar: if [ -n "${1+1}" ]; then echo defined; else echo undefined; fi "#, args: (), stdout: "undefined\n", stderr: "if [ -n \"${1+1}\" ]; then echo defined; else echo undefined; fi\n", } test! { name: variadic_arguments_are_separate, justfile: r#" set positional-arguments foo *bar: echo $1 echo $2 "#, args: ("foo", "a", "b"), stdout: "a\nb\n", stderr: "echo $1\necho $2\n", } just-1.21.0/tests/private.rs000064400000000000000000000007531046102023000140610ustar 00000000000000use super::*; #[test] fn private_attribute_for_recipe() { Test::new() .justfile( " [private] foo: ", ) .args(["--list"]) .stdout( " Available recipes: ", ) .run(); } #[test] fn private_attribute_for_alias() { Test::new() .justfile( " [private] alias f := foo foo: ", ) .args(["--list"]) .stdout( " Available recipes: foo ", ) .run(); } just-1.21.0/tests/quiet.rs000064400000000000000000000035631046102023000135400ustar 00000000000000use super::*; test! { name: no_stdout, justfile: r#" default: @echo hello "#, args: ("--quiet"), stdout: "", } test! { name: stderr, justfile: r#" default: @echo hello 1>&2 "#, args: ("--quiet"), stdout: "", } test! { name: command_echoing, justfile: r#" default: exit "#, args: ("--quiet"), stdout: "", } test! { name: error_messages, justfile: r#" default: exit 100 "#, args: ("--quiet"), stdout: "", status: 100, } test! { name: assignment_backtick_stderr, justfile: r#" a := `echo hello 1>&2` default: exit 100 "#, args: ("--quiet"), stdout: "", status: 100, } test! { name: interpolation_backtick_stderr, justfile: r#" default: echo `echo hello 1>&2` exit 100 "#, args: ("--quiet"), stdout: "", status: 100, } test! { name: choose_none, justfile: "", args: ("--choose", "--quiet"), status: EXIT_FAILURE, } test! { name: choose_invocation, justfile: "foo:", args: ("--choose", "--quiet", "--shell", "asdfasdfasfdasdfasdfadsf"), status: EXIT_FAILURE, shell: false, } test! { name: choose_status, justfile: "foo:", args: ("--choose", "--quiet", "--chooser", "/usr/bin/env false"), status: EXIT_FAILURE, } test! { name: edit_invocation, justfile: "foo:", args: ("--edit", "--quiet"), env: { "VISUAL": "adsfasdfasdfadsfadfsaf", }, status: EXIT_FAILURE, } test! { name: edit_status, justfile: "foo:", args: ("--edit", "--quiet"), env: { "VISUAL": "false", }, status: EXIT_FAILURE, } test! { name: init_exists, justfile: "foo:", args: ("--init", "--quiet"), status: EXIT_FAILURE, } test! { name: show_missing, justfile: "foo:", args: ("--show", "bar", "--quiet"), status: EXIT_FAILURE, } test! { name: quiet_shebang, justfile: " @foo: #!/bin/sh ", args: ("--quiet"), } just-1.21.0/tests/quote.rs000064400000000000000000000011721046102023000135400ustar 00000000000000use super::*; #[test] fn single_quotes_are_prepended_and_appended() { Test::new() .justfile( " x := quote('abc') ", ) .args(["--evaluate", "x"]) .stdout("'abc'") .run(); } #[test] fn quotes_are_escaped() { Test::new() .justfile( r#" x := quote("'") "#, ) .args(["--evaluate", "x"]) .stdout(r"''\'''") .run(); } #[test] fn quoted_strings_can_be_used_as_arguments() { Test::new() .justfile( r#" file := quote("foo ' bar") @foo: touch {{ file }} ls -1 "#, ) .stdout("foo ' bar\njustfile\n") .run(); } just-1.21.0/tests/readme.rs000064400000000000000000000014311046102023000136360ustar 00000000000000use super::*; #[test] fn readme() { let mut justfiles = vec![]; let mut current = None; for line in fs::read_to_string("README.md").unwrap().lines() { if let Some(mut justfile) = current { if line == "```" { justfiles.push(justfile); current = None; } else { justfile += line; justfile += "\n"; current = Some(justfile); } } else if line == "```just" { current = Some(String::new()); } } for justfile in justfiles { let tmp = tempdir(); let path = tmp.path().join("justfile"); fs::write(path, justfile).unwrap(); let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--dump") .output() .unwrap(); assert_success(&output); } } just-1.21.0/tests/recursion_limit.rs000064400000000000000000000035321046102023000156140ustar 00000000000000use super::*; #[test] fn bugfix() { let mut justfile = String::from("foo: (x "); for _ in 0..500 { justfile.push('('); } Test::new() .justfile(&justfile) .stderr(RECURSION_LIMIT_REACHED) .status(EXIT_FAILURE) .run(); } #[cfg(not(windows))] const RECURSION_LIMIT_REACHED: &str = " error: Parsing recursion depth exceeded ——▶ justfile:1:265 │ 1 │ foo: (x (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((( │ ^ "; #[cfg(windows)] const RECURSION_LIMIT_REACHED: &str = " error: Parsing recursion depth exceeded ——▶ justfile:1:57 │ 1 │ foo: (x (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((( │ ^ "; just-1.21.0/tests/regexes.rs000064400000000000000000000017741046102023000140550ustar 00000000000000use super::*; #[test] fn match_succeeds_evaluates_to_first_branch() { Test::new() .justfile( " foo := if 'abbbc' =~ 'ab+c' { 'yes' } else { 'no' } default: echo {{ foo }} ", ) .stderr("echo yes\n") .stdout("yes\n") .run(); } #[test] fn match_fails_evaluates_to_second_branch() { Test::new() .justfile( " foo := if 'abbbc' =~ 'ab{4}c' { 'yes' } else { 'no' } default: echo {{ foo }} ", ) .stderr("echo no\n") .stdout("no\n") .run(); } #[test] fn bad_regex_fails_at_runtime() { Test::new() .justfile( " default: echo before echo {{ if '' =~ '(' { 'a' } else { 'b' } }} echo after ", ) .stderr( " echo before error: regex parse error: ( ^ error: unclosed group ", ) .stdout("before\n") .status(EXIT_FAILURE) .run(); } just-1.21.0/tests/run.rs000064400000000000000000000003321046102023000132040ustar 00000000000000use super::*; #[test] fn dont_run_duplicate_recipes() { Test::new() .justfile( " foo: # foo ", ) .args(["foo", "foo"]) .stderr( " # foo ", ) .run(); } just-1.21.0/tests/search.rs000064400000000000000000000064711046102023000136570ustar 00000000000000use super::*; fn search_test>(path: P, args: &[&str]) { let binary = executable_path("just"); let output = Command::new(binary) .current_dir(path) .args(args) .output() .expect("just invocation failed"); assert_eq!(output.status.code().unwrap(), 0); let stdout = str::from_utf8(&output.stdout).unwrap(); assert_eq!(stdout, "ok\n"); let stderr = str::from_utf8(&output.stderr).unwrap(); assert_eq!(stderr, "echo ok\n"); } #[test] fn test_justfile_search() { let tmp = temptree! { justfile: "default:\n\techo ok", a: { b: { c: { d: {}, }, }, }, }; search_test(tmp.path().join("a/b/c/d"), &[]); } #[test] fn test_capitalized_justfile_search() { let tmp = temptree! { Justfile: "default:\n\techo ok", a: { b: { c: { d: {}, }, }, }, }; search_test(tmp.path().join("a/b/c/d"), &[]); } #[test] fn test_upwards_path_argument() { let tmp = temptree! { justfile: "default:\n\techo ok", a: { justfile: "default:\n\techo bad", }, }; search_test(tmp.path().join("a"), &["../"]); search_test(tmp.path().join("a"), &["../default"]); } #[test] fn test_downwards_path_argument() { let tmp = temptree! { justfile: "default:\n\techo bad", a: { justfile: "default:\n\techo ok", }, }; let path = tmp.path(); search_test(path, &["a/"]); search_test(path, &["a/default"]); search_test(path, &["./a/"]); search_test(path, &["./a/default"]); search_test(path, &["./a/"]); search_test(path, &["./a/default"]); } #[test] fn test_upwards_multiple_path_argument() { let tmp = temptree! { justfile: "default:\n\techo ok", a: { b: { justfile: "default:\n\techo bad", }, }, }; let path = tmp.path().join("a").join("b"); search_test(&path, &["../../"]); search_test(&path, &["../../default"]); } #[test] fn test_downwards_multiple_path_argument() { let tmp = temptree! { justfile: "default:\n\techo bad", a: { b: { justfile: "default:\n\techo ok", }, }, }; let path = tmp.path(); search_test(path, &["a/b/"]); search_test(path, &["a/b/default"]); search_test(path, &["./a/b/"]); search_test(path, &["./a/b/default"]); search_test(path, &["./a/b/"]); search_test(path, &["./a/b/default"]); } #[test] fn single_downwards() { let tmp = temptree! { justfile: "default:\n\techo ok", child: {}, }; let path = tmp.path(); search_test(path, &["child/"]); } #[test] fn single_upwards() { let tmp = temptree! { justfile: "default:\n\techo ok", child: {}, }; let path = tmp.path().join("child"); search_test(path, &["../"]); } #[test] fn find_dot_justfile() { Test::new() .justfile( " foo: echo bad ", ) .tree(tree! { dir: { ".justfile": " foo: echo ok " } }) .current_dir("dir") .stderr("echo ok\n") .stdout("ok\n") .run(); } #[test] fn dot_justfile_conflicts_with_justfile() { Test::new() .justfile( " foo: ", ) .tree(tree! { ".justfile": " foo: ", }) .stderr_regex("error: Multiple candidate justfiles found in `.*`: `.justfile` and `justfile`\n") .status(EXIT_FAILURE) .run(); } just-1.21.0/tests/search_arguments.rs000064400000000000000000000010261046102023000157330ustar 00000000000000use super::*; #[test] fn argument_with_different_path_prefix_is_allowed() { Test::new() .justfile("foo bar:") .args(["./foo", "../bar"]) .run(); } #[test] fn passing_dot_as_argument_is_allowed() { Test::new() .justfile( " say ARG: echo {{ARG}} ", ) .write( "child/justfile", "say ARG:\n '{{just_executable()}}' ../say {{ARG}}", ) .current_dir("child") .args(["say", "."]) .stdout(".\n") .stderr_regex("'.*' ../say .\necho .\n") .run(); } just-1.21.0/tests/shadowing_parameters.rs000064400000000000000000000011301046102023000166030ustar 00000000000000test! { name: parameter_may_shadow_variable, justfile: "FOO := 'hello'\na FOO:\n echo {{FOO}}\n", args: ("a", "bar"), stdout: "bar\n", stderr: "echo bar\n", } test! { name: shadowing_parameters_do_not_change_environment, justfile: "export FOO := 'hello'\na FOO:\n echo $FOO\n", args: ("a", "bar"), stdout: "hello\n", stderr: "echo $FOO\n", } test! { name: exporting_shadowing_parameters_does_change_environment, justfile: "export FOO := 'hello'\na $FOO:\n echo $FOO\n", args: ("a", "bar"), stdout: "bar\n", stderr: "echo $FOO\n", } just-1.21.0/tests/shebang.rs000064400000000000000000000013611046102023000140120ustar 00000000000000use super::*; #[cfg(windows)] test! { name: powershell, justfile: r#" default: #!powershell Write-Host Hello-World "#, stdout: "Hello-World\n", } #[cfg(windows)] test! { name: powershell_exe, justfile: r#" default: #!powershell.exe Write-Host Hello-World "#, stdout: "Hello-World\n", } #[cfg(windows)] test! { name: cmd, justfile: r#" default: #!cmd /c @echo Hello-World "#, stdout: "Hello-World\r\n", } #[cfg(windows)] test! { name: cmd_exe, justfile: r#" default: #!cmd.exe /c @echo Hello-World "#, stdout: "Hello-World\r\n", } #[test] fn simple() { Test::new() .justfile( " foo: #!/bin/sh echo bar ", ) .stdout("bar\n") .run(); } just-1.21.0/tests/shell.rs000064400000000000000000000052031046102023000135110ustar 00000000000000use super::*; const JUSTFILE: &str = " expression := `EXPRESSION` recipe default=`DEFAULT`: {{expression}} {{default}} RECIPE "; /// Test that --shell correctly sets the shell #[test] #[cfg_attr(windows, ignore)] fn flag() { let tmp = temptree! { justfile: JUSTFILE, shell: "#!/usr/bin/env bash\necho \"$@\"", }; let shell = tmp.path().join("shell"); #[cfg(not(windows))] { let permissions = std::os::unix::fs::PermissionsExt::from_mode(0o700); fs::set_permissions(&shell, permissions).unwrap(); } let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--shell") .arg(shell) .output() .unwrap(); let stdout = "-cu -cu EXPRESSION\n-cu -cu DEFAULT\n-cu RECIPE\n"; assert_stdout(&output, stdout); } const JUSTFILE_CMD: &str = r#" set shell := ["cmd.exe", "/C"] x := `Echo` recipe: REM foo Echo "{{x}}" "#; /// Test that we can use `set shell` to use cmd.exe on windows #[test] #[cfg_attr(unix, ignore)] fn cmd() { let tmp = temptree! { justfile: JUSTFILE_CMD, }; let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .output() .unwrap(); let stdout = "\\\"ECHO is on.\\\"\r\n"; assert_stdout(&output, stdout); } const JUSTFILE_POWERSHELL: &str = r#" set shell := ["powershell.exe", "-c"] x := `Write-Host "Hello, world!"` recipe: For ($i=0; $i -le 10; $i++) { Write-Host $i } Write-Host "{{x}}" "#; /// Test that we can use `set shell` to use cmd.exe on windows #[test] #[cfg_attr(unix, ignore)] fn powershell() { let tmp = temptree! { justfile: JUSTFILE_POWERSHELL, }; let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .output() .unwrap(); let stdout = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\nHello, world!\n"; assert_stdout(&output, stdout); } test! { name: shell_args, justfile: " default: echo A${foo}A ", args: ("--shell-arg", "-c"), stdout: "AA\n", stderr: "echo A${foo}A\n", shell: false, } test! { name: shell_override, justfile: " set shell := ['foo-bar-baz'] default: echo hello ", args: ("--shell", "bash"), stdout: "hello\n", stderr: "echo hello\n", shell: false, } test! { name: shell_arg_override, justfile: " set shell := ['foo-bar-baz'] default: echo hello ", args: ("--shell-arg", "-cu"), stdout: "hello\n", stderr: "echo hello\n", shell: false, } test! { name: set_shell, justfile: " set shell := ['echo', '-n'] x := `bar` foo: echo {{x}} echo foo ", args: (), stdout: "echo barecho foo", stderr: "echo bar\necho foo\n", shell: false, } just-1.21.0/tests/show.rs000064400000000000000000000034721046102023000133700ustar 00000000000000use super::*; test! { name: show, justfile: r#"hello := "foo" bar := hello + hello recipe: echo {{hello + "bar" + bar}}"#, args: ("--show", "recipe"), stdout: r#" recipe: echo {{ hello + "bar" + bar }} "#, } test! { name: alias_show, justfile: "foo:\n bar\nalias f := foo", args: ("--show", "f"), stdout: " alias f := foo foo: bar ", } test! { name: alias_show_missing_target, justfile: "alias f := foo", args: ("--show", "f"), stderr: " error: Alias `f` has an unknown target `foo` ——▶ justfile:1:7 │ 1 │ alias f := foo │ ^ ", status: EXIT_FAILURE, } test! { name: show_suggestion, justfile: r#" hello a b='B ' c='C': echo {{a}} {{b}} {{c}} a Z="\t z": "#, args: ("--show", "hell"), stdout: "", stderr: "error: Justfile does not contain recipe `hell`.\nDid you mean `hello`?\n", status: EXIT_FAILURE, } test! { name: show_alias_suggestion, justfile: r#" hello a b='B ' c='C': echo {{a}} {{b}} {{c}} alias foo := hello a Z="\t z": "#, args: ("--show", "fo"), stdout: "", stderr: " error: Justfile does not contain recipe `fo`. Did you mean `foo`, an alias for `hello`? ", status: EXIT_FAILURE, } test! { name: show_no_suggestion, justfile: r#" helloooooo a b='B ' c='C': echo {{a}} {{b}} {{c}} a Z="\t z": "#, args: ("--show", "hell"), stdout: "", stderr: "error: Justfile does not contain recipe `hell`.\n", status: EXIT_FAILURE, } test! { name: show_no_alias_suggestion, justfile: r#" hello a b='B ' c='C': echo {{a}} {{b}} {{c}} alias foo := hello a Z="\t z": "#, args: ("--show", "fooooooo"), stdout: "", stderr: "error: Justfile does not contain recipe `fooooooo`.\n", status: EXIT_FAILURE, } just-1.21.0/tests/slash_operator.rs000064400000000000000000000041141046102023000154270ustar 00000000000000use super::*; #[test] fn once() { Test::new() .justfile("x := 'a' / 'b'") .args(["--evaluate", "x"]) .stdout("a/b") .run(); } #[test] fn twice() { Test::new() .justfile("x := 'a' / 'b' / 'c'") .args(["--evaluate", "x"]) .stdout("a/b/c") .run(); } #[test] fn no_lhs_once() { Test::new() .justfile("x := / 'a'") .args(["--evaluate", "x"]) .stdout("/a") .run(); } #[test] fn no_lhs_twice() { Test::new() .justfile("x := / 'a' / 'b'") .args(["--evaluate", "x"]) .stdout("/a/b") .run(); Test::new() .justfile("x := // 'a'") .args(["--evaluate", "x"]) .stdout("//a") .run(); } #[test] fn no_rhs_once() { Test::new() .justfile("x := 'a' /") .stderr( " error: Expected backtick, identifier, '(', '/', or string, but found end of file ——▶ justfile:1:11 │ 1 │ x := 'a' / │ ^ ", ) .status(EXIT_FAILURE) .run(); } #[test] fn default_un_parenthesized() { Test::new() .justfile( " foo x='a' / 'b': echo {{x}} ", ) .stderr( " error: Expected '*', ':', '$', identifier, or '+', but found '/' ——▶ justfile:1:11 │ 1 │ foo x='a' / 'b': │ ^ ", ) .status(EXIT_FAILURE) .run(); } #[test] fn no_lhs_un_parenthesized() { Test::new() .justfile( " foo x=/ 'a' / 'b': echo {{x}} ", ) .stderr( " error: Expected backtick, identifier, '(', or string, but found '/' ——▶ justfile:1:7 │ 1 │ foo x=/ 'a' / 'b': │ ^ ", ) .status(EXIT_FAILURE) .run(); } #[test] fn default_parenthesized() { Test::new() .justfile( " foo x=('a' / 'b'): echo {{x}} ", ) .stderr("echo a/b\n") .stdout("a/b\n") .run(); } #[test] fn no_lhs_parenthesized() { Test::new() .justfile( " foo x=(/ 'a' / 'b'): echo {{x}} ", ) .stderr("echo /a/b\n") .stdout("/a/b\n") .run(); } just-1.21.0/tests/string.rs000064400000000000000000000130221046102023000137060ustar 00000000000000use super::*; test! { name: raw_string, justfile: r#" export EXPORTED_VARIABLE := '\z' recipe: printf "$EXPORTED_VARIABLE" "#, stdout: "\\z", stderr: "printf \"$EXPORTED_VARIABLE\"\n", } test! { name: multiline_raw_string, justfile: " string := 'hello whatever' a: echo '{{string}}' ", args: ("a"), stdout: "hello whatever ", stderr: "echo 'hello whatever' ", } test! { name: multiline_backtick, justfile: " string := `echo hello echo goodbye ` a: echo '{{string}}' ", args: ("a"), stdout: "hello\ngoodbye\n", stderr: "echo 'hello goodbye' ", } test! { name: multiline_cooked_string, justfile: r#" string := "hello whatever" a: echo '{{string}}' "#, args: ("a"), stdout: "hello whatever ", stderr: "echo 'hello whatever' ", } test! { name: cooked_string_suppress_newline, justfile: r#" a := """ foo\ bar """ @default: printf %s '{{a}}' "#, stdout: " foobar ", } test! { name: invalid_escape_sequence, justfile: r#"x := "\q" a:"#, args: ("a"), stdout: "", stderr: "error: `\\q` is not a valid escape sequence ——▶ justfile:1:6 │ 1 │ x := \"\\q\" │ ^^^^ ", status: EXIT_FAILURE, } test! { name: error_line_after_multiline_raw_string, justfile: " string := 'hello whatever' + 'yo' a: echo '{{foo}}' ", args: ("a"), stdout: "", stderr: "error: Variable `foo` not defined ——▶ justfile:6:11 │ 6 │ echo '{{foo}}' │ ^^^ ", status: EXIT_FAILURE, } test! { name: error_column_after_multiline_raw_string, justfile: " string := 'hello whatever' + bar a: echo '{{string}}' ", args: ("a"), stdout: "", stderr: "error: Variable `bar` not defined ——▶ justfile:3:13 │ 3 │ whatever' + bar │ ^^^ ", status: EXIT_FAILURE, } test! { name: multiline_raw_string_in_interpolation, justfile: r#" a: echo '{{"a" + ' ' + "b"}}' "#, args: ("a"), stdout: " a b ", stderr: " echo 'a b' ", } test! { name: error_line_after_multiline_raw_string_in_interpolation, justfile: r#" a: echo '{{"a" + ' ' + "b"}}' echo {{b}} "#, args: ("a"), stdout: "", stderr: "error: Variable `b` not defined ——▶ justfile:5:10 │ 5 │ echo {{b}} │ ^ ", status: EXIT_FAILURE, } test! { name: unterminated_raw_string, justfile: " a b= ': ", args: ("a"), stdout: "", stderr: " error: Unterminated string ——▶ justfile:1:6 │ 1 │ a b= ': │ ^ ", status: EXIT_FAILURE, } test! { name: unterminated_string, justfile: r#" a b= ": "#, args: ("a"), stdout: "", stderr: r#" error: Unterminated string ——▶ justfile:1:6 │ 1 │ a b= ": │ ^ "#, status: EXIT_FAILURE, } test! { name: unterminated_backtick, justfile: " foo a=\t`echo blaaaaaah: echo {{a}} ", stderr: r#" error: Unterminated backtick ——▶ justfile:1:8 │ 1 │ foo a= `echo blaaaaaah: │ ^ "#, status: EXIT_FAILURE, } test! { name: unterminated_indented_raw_string, justfile: " a b= ''': ", args: ("a"), stdout: "", stderr: " error: Unterminated string ——▶ justfile:1:6 │ 1 │ a b= ''': │ ^^^ ", status: EXIT_FAILURE, } test! { name: unterminated_indented_string, justfile: r#" a b= """: "#, args: ("a"), stdout: "", stderr: r#" error: Unterminated string ——▶ justfile:1:6 │ 1 │ a b= """: │ ^^^ "#, status: EXIT_FAILURE, } test! { name: unterminated_indented_backtick, justfile: " foo a=\t```echo blaaaaaah: echo {{a}} ", stderr: r#" error: Unterminated backtick ——▶ justfile:1:8 │ 1 │ foo a= ```echo blaaaaaah: │ ^^^ "#, status: EXIT_FAILURE, } test! { name: indented_raw_string_contents_indentation_removed, justfile: " a := ''' foo bar ''' @default: printf '{{a}}' ", stdout: " foo bar ", } test! { name: indented_cooked_string_contents_indentation_removed, justfile: r#" a := """ foo bar """ @default: printf '{{a}}' "#, stdout: " foo bar ", } test! { name: indented_backtick_string_contents_indentation_removed, justfile: r#" a := ``` printf ' foo bar ' ``` @default: printf '{{a}}' "#, stdout: "\n\nfoo\nbar", } test! { name: indented_raw_string_escapes, justfile: r" a := ''' foo\n bar ''' @default: printf %s '{{a}}' ", stdout: r" foo\n bar ", } test! { name: indented_cooked_string_escapes, justfile: r#" a := """ foo\n bar """ @default: printf %s '{{a}}' "#, stdout: " foo bar ", } test! { name: indented_backtick_string_escapes, justfile: r" a := ``` printf %s ' foo\n bar ' ``` @default: printf %s '{{a}}' ", stdout: "\n\nfoo\\n\nbar", } test! { name: shebang_backtick, justfile: " x := `#!/usr/bin/env sh` ", stderr: " error: Backticks may not start with `#!` ——▶ justfile:1:6 │ 1 │ x := `#!/usr/bin/env sh` │ ^^^^^^^^^^^^^^^^^^^ ", status: EXIT_FAILURE, } just-1.21.0/tests/subsequents.rs000064400000000000000000000034541046102023000147710ustar 00000000000000use super::*; test! { name: success, justfile: " foo: && bar echo foo bar: echo bar ", stdout: " foo bar ", stderr: " echo foo echo bar ", } test! { name: failure, justfile: " foo: && bar echo foo false bar: echo bar ", stdout: " foo ", stderr: " echo foo false error: Recipe `foo` failed on line 3 with exit code 1 ", status: EXIT_FAILURE, } test! { name: circular_dependency, justfile: " foo: && foo ", stderr: " error: Recipe `foo` depends on itself ——▶ justfile:1:9 │ 1 │ foo: && foo │ ^^^ ", status: EXIT_FAILURE, } test! { name: unknown, justfile: " foo: && bar ", stderr: " error: Recipe `foo` has unknown dependency `bar` ——▶ justfile:1:9 │ 1 │ foo: && bar │ ^^^ ", status: EXIT_FAILURE, } test! { name: unknown_argument, justfile: " bar x: foo: && (bar y) ", stderr: " error: Variable `y` not defined ——▶ justfile:3:14 │ 3 │ foo: && (bar y) │ ^ ", status: EXIT_FAILURE, } test! { name: argument, justfile: " foo: && (bar 'hello') bar x: echo {{ x }} ", stdout: " hello ", stderr: " echo hello ", } test! { name: duplicate_subsequents_dont_run, justfile: " a: && b c echo a b: d echo b c: d echo c d: echo d ", stdout: " a d b c ", stderr: " echo a echo d echo b echo c ", } test! { name: subsequents_run_even_if_already_ran_as_prior, justfile: " a: b && b echo a b: echo b ", stdout: " b a b ", stderr: " echo b echo a echo b ", } just-1.21.0/tests/summary.rs000064400000000000000000000020671046102023000141040ustar 00000000000000use super::*; test! { name: summary, justfile: "b: a a: d: c c: b _z: _y _y: ", args: ("--summary"), stdout: "a b c d\n", } test! { name: summary_sorted, justfile: " b: c: a: ", args: ("--summary"), stdout: "a b c\n", } test! { name: summary_unsorted, justfile: " b: c: a: ", args: ("--summary", "--unsorted"), stdout: "b c a\n", } test! { name: summary_none, justfile: "", args: ("--summary", "--quiet"), stdout: "\n\n\n", } #[test] fn no_recipes() { Test::new() .arg("--summary") .stderr("Justfile contains no recipes.\n") .stdout("\n\n\n") .run(); } #[test] fn submodule_recipes() { Test::new() .write("foo.just", "mod bar\nfoo:") .write("bar.just", "mod baz\nbar:") .write("baz.just", "mod biz\nbaz:") .write("biz.just", "biz:") .justfile( " mod foo bar: ", ) .test_round_trip(false) .arg("--unstable") .arg("--summary") .stdout("bar foo::foo foo::bar::bar foo::bar::baz::baz foo::bar::baz::biz::biz\n") .run(); } just-1.21.0/tests/tempdir.rs000064400000000000000000000011501046102023000140430ustar 00000000000000use super::*; pub(crate) fn tempdir() -> TempDir { tempfile::Builder::new() .prefix("just-test-tempdir") .tempdir() .expect("failed to create temporary directory") } #[test] fn test_tempdir_is_set() { Test::new() .justfile( " set tempdir := '.' foo: #!/usr/bin/env bash cat just*/foo ", ) .shell(false) .tree(tree! { foo: { } }) .current_dir("foo") .stdout(if cfg!(windows) { " cat just*/foo " } else { " #!/usr/bin/env bash cat just*/foo " }) .run(); } just-1.21.0/tests/test.rs000064400000000000000000000174421046102023000133710ustar 00000000000000use {super::*, pretty_assertions::assert_eq}; macro_rules! test { { name: $name:ident, $(justfile: $justfile:expr,)? $(args: ($($arg:tt),*),)? $(env: { $($env_key:literal : $env_value:literal,)* },)? $(stdin: $stdin:expr,)? $(stdout: $stdout:expr,)? $(stdout_regex: $stdout_regex:expr,)? $(stderr: $stderr:expr,)? $(stderr_regex: $stderr_regex:expr,)? $(status: $status:expr,)? $(shell: $shell:expr,)? } => { #[test] fn $name() { let test = crate::test::Test::new(); $($(let test = test.arg($arg);)*)? $($(let test = test.env($env_key, $env_value);)*)? $(let test = test.justfile($justfile);)? $(let test = test.shell($shell);)? $(let test = test.status($status);)? $(let test = test.stderr($stderr);)? $(let test = test.stderr_regex($stderr_regex);)? $(let test = test.stdin($stdin);)? $(let test = test.stdout($stdout);)? $(let test = test.stdout_regex($stdout_regex);)? test.run(); } } } pub(crate) struct Output { pub(crate) stdout: String, pub(crate) tempdir: TempDir, } #[must_use] pub(crate) struct Test { pub(crate) args: Vec, pub(crate) current_dir: PathBuf, pub(crate) env: BTreeMap, pub(crate) justfile: Option, pub(crate) shell: bool, pub(crate) status: i32, pub(crate) stderr: String, pub(crate) stderr_regex: Option, pub(crate) stdin: String, pub(crate) stdout: String, pub(crate) stdout_regex: Option, pub(crate) tempdir: TempDir, pub(crate) test_round_trip: bool, pub(crate) unindent_stdout: bool, } impl Test { pub(crate) fn new() -> Self { Self::with_tempdir(tempdir()) } pub(crate) fn with_tempdir(tempdir: TempDir) -> Self { Self { args: Vec::new(), current_dir: PathBuf::new(), env: BTreeMap::new(), justfile: Some(String::new()), shell: true, status: EXIT_SUCCESS, stderr: String::new(), stderr_regex: None, stdin: String::new(), stdout: String::new(), stdout_regex: None, tempdir, test_round_trip: true, unindent_stdout: true, } } pub(crate) fn arg(mut self, val: &str) -> Self { self.args.push(val.to_owned()); self } pub(crate) fn args<'a>(mut self, args: impl AsRef<[&'a str]>) -> Self { for arg in args.as_ref() { self = self.arg(arg); } self } pub(crate) fn current_dir(mut self, path: impl AsRef) -> Self { self.current_dir = path.as_ref().to_owned(); self } pub(crate) fn env(mut self, key: &str, val: &str) -> Self { self.env.insert(key.to_string(), val.to_string()); self } pub(crate) fn justfile(mut self, justfile: impl Into) -> Self { self.justfile = Some(justfile.into()); self } pub(crate) fn justfile_path(&self) -> PathBuf { self.tempdir.path().join("justfile") } pub(crate) fn no_justfile(mut self) -> Self { self.justfile = None; self } pub(crate) fn shell(mut self, shell: bool) -> Self { self.shell = shell; self } pub(crate) fn status(mut self, exit_status: i32) -> Self { self.status = exit_status; self } pub(crate) fn stderr(mut self, stderr: impl Into) -> Self { self.stderr = stderr.into(); self } pub(crate) fn stderr_regex(mut self, stderr_regex: impl AsRef) -> Self { self.stderr_regex = Some(Regex::new(&format!("^{}$", stderr_regex.as_ref())).unwrap()); self } pub(crate) fn stdin(mut self, stdin: impl Into) -> Self { self.stdin = stdin.into(); self } pub(crate) fn stdout(mut self, stdout: impl Into) -> Self { self.stdout = stdout.into(); self } pub(crate) fn stdout_regex(mut self, stdout_regex: impl AsRef) -> Self { self.stdout_regex = Some(Regex::new(&format!("^{}$", stdout_regex.as_ref())).unwrap()); self } pub(crate) fn test_round_trip(mut self, test_round_trip: bool) -> Self { self.test_round_trip = test_round_trip; self } pub(crate) fn tree(self, mut tree: Tree) -> Self { tree.map(|_name, content| unindent(content)); tree.instantiate(self.tempdir.path()).unwrap(); self } pub(crate) fn unindent_stdout(mut self, unindent_stdout: bool) -> Self { self.unindent_stdout = unindent_stdout; self } pub(crate) fn write(self, path: impl AsRef, content: impl AsRef<[u8]>) -> Self { let path = self.tempdir.path().join(path); fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(path, content).unwrap(); self } } impl Test { pub(crate) fn run(self) -> Output { if let Some(justfile) = &self.justfile { let justfile = unindent(justfile); fs::write(self.justfile_path(), justfile).unwrap(); } let stdout = if self.unindent_stdout { unindent(&self.stdout) } else { self.stdout }; let stderr = unindent(&self.stderr); let mut dotenv_path = self.tempdir.path().to_path_buf(); dotenv_path.push(".env"); fs::write(dotenv_path, "DOTENV_KEY=dotenv-value").unwrap(); let mut command = Command::new(executable_path("just")); if self.shell { command.args(["--shell", "bash"]); } let mut child = command .args(self.args) .envs(&self.env) .current_dir(self.tempdir.path().join(self.current_dir)) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .expect("just invocation failed"); { let mut stdin_handle = child.stdin.take().expect("failed to unwrap stdin handle"); stdin_handle .write_all(self.stdin.as_bytes()) .expect("failed to write stdin to just process"); } let output = child .wait_with_output() .expect("failed to wait for just process"); fn compare(name: &str, have: T, want: T) -> bool { let equal = have == want; if !equal { eprintln!("Bad {name}: {}", Comparison::new(&have, &want)); } equal } let output_stdout = str::from_utf8(&output.stdout).unwrap(); let output_stderr = str::from_utf8(&output.stderr).unwrap(); if let Some(ref stdout_regex) = self.stdout_regex { if !stdout_regex.is_match(output_stdout) { panic!("Stdout regex mismatch:\n{output_stdout:?}\n!~=\n/{stdout_regex:?}/"); } } if let Some(ref stderr_regex) = self.stderr_regex { if !stderr_regex.is_match(output_stderr) { panic!("Stderr regex mismatch:\n{output_stderr:?}\n!~=\n/{stderr_regex:?}/"); } } if !compare("status", output.status.code().unwrap(), self.status) | (self.stdout_regex.is_none() && !compare("stdout", output_stdout, &stdout)) | (self.stderr_regex.is_none() && !compare("stderr", output_stderr, &stderr)) { panic!("Output mismatch."); } if self.test_round_trip && self.status == EXIT_SUCCESS { test_round_trip(self.tempdir.path()); } Output { tempdir: self.tempdir, stdout: output_stdout.into(), } } } fn test_round_trip(tmpdir: &Path) { println!("Reparsing..."); let output = Command::new(executable_path("just")) .current_dir(tmpdir) .arg("--dump") .output() .expect("just invocation failed"); if !output.status.success() { panic!("dump failed: {}", output.status); } let dumped = String::from_utf8(output.stdout).unwrap(); let reparsed_path = tmpdir.join("reparsed.just"); fs::write(&reparsed_path, &dumped).unwrap(); let output = Command::new(executable_path("just")) .current_dir(tmpdir) .arg("--justfile") .arg(&reparsed_path) .arg("--dump") .output() .expect("just invocation failed"); if !output.status.success() { panic!("reparse failed: {}", output.status); } let reparsed = String::from_utf8(output.stdout).unwrap(); assert_eq!(reparsed, dumped, "reparse mismatch"); } just-1.21.0/tests/undefined_variables.rs000064400000000000000000000033461046102023000164010ustar 00000000000000use super::*; #[test] fn parameter_default_unknown_variable_in_expression() { Test::new() .justfile("foo a=(b+''):") .stderr( " error: Variable `b` not defined ——▶ justfile:1:8 │ 1 │ foo a=(b+''): │ ^ ", ) .status(EXIT_FAILURE) .run(); } #[test] fn unknown_variable_in_unary_call() { Test::new() .justfile( " foo x=env_var(a): ", ) .stderr( " error: Variable `a` not defined ——▶ justfile:1:15 │ 1 │ foo x=env_var(a): │ ^ ", ) .status(EXIT_FAILURE) .run(); } #[test] fn unknown_first_variable_in_binary_call() { Test::new() .justfile( " foo x=env_var_or_default(a, b): ", ) .stderr( " error: Variable `a` not defined ——▶ justfile:1:26 │ 1 │ foo x=env_var_or_default(a, b): │ ^ ", ) .status(EXIT_FAILURE) .run(); } #[test] fn unknown_second_variable_in_binary_call() { Test::new() .justfile( " foo x=env_var_or_default('', b): ", ) .stderr( " error: Variable `b` not defined ——▶ justfile:1:30 │ 1 │ foo x=env_var_or_default('', b): │ ^ ", ) .status(EXIT_FAILURE) .run(); } #[test] fn unknown_variable_in_ternary_call() { Test::new() .justfile( " foo x=replace(a, b, c): ", ) .stderr( " error: Variable `a` not defined ——▶ justfile:1:15 │ 1 │ foo x=replace(a, b, c): │ ^ ", ) .status(EXIT_FAILURE) .run(); } just-1.21.0/tests/unstable.rs000064400000000000000000000021171046102023000142200ustar 00000000000000use super::*; #[test] fn set_unstable_true_with_env_var() { let justfile = r#" default: echo 'foo' "#; for val in ["true", "some-arbitrary-string"] { Test::new() .justfile(justfile) .args(["--fmt"]) .env("JUST_UNSTABLE", val) .status(EXIT_SUCCESS) .stderr_regex("Wrote justfile to `.*`\n") .run(); } } #[test] fn set_unstable_false_with_env_var() { let justfile = r#" default: echo 'foo' "#; for val in ["0", "", "false"] { Test::new() .justfile(justfile) .args(["--fmt"]) .env("JUST_UNSTABLE", val) .status(EXIT_FAILURE) .stderr("error: The `--fmt` command is currently unstable. Invoke `just` with the `--unstable` flag to enable unstable features.\n") .run(); } } #[test] fn set_unstable_false_with_env_var_unset() { let justfile = r#" default: echo 'foo' "#; Test::new() .justfile(justfile) .args(["--fmt"]) .status(EXIT_FAILURE) .stderr("error: The `--fmt` command is currently unstable. Invoke `just` with the `--unstable` flag to enable unstable features.\n") .run(); } just-1.21.0/tests/windows_shell.rs000064400000000000000000000016261046102023000152700ustar 00000000000000use super::*; #[test] fn windows_shell_setting() { Test::new() .justfile( r#" set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"] set shell := ["asdfasdfasdfasdf"] foo: Write-Output bar "#, ) .shell(false) .stdout("bar\r\n") .stderr("Write-Output bar\n") .run(); } #[test] fn windows_powershell_setting_uses_powershell_set_shell() { Test::new() .justfile( r#" set windows-powershell set shell := ["asdfasdfasdfasdf"] foo: Write-Output bar "#, ) .shell(false) .stdout("bar\r\n") .stderr("Write-Output bar\n") .run(); } #[test] fn windows_powershell_setting_uses_powershell() { Test::new() .justfile( r#" set windows-powershell foo: Write-Output bar "#, ) .shell(false) .stdout("bar\r\n") .stderr("Write-Output bar\n") .run(); } just-1.21.0/tests/working_directory.rs000064400000000000000000000101161046102023000161450ustar 00000000000000use super::*; const JUSTFILE: &str = r#" foo := `cat data` linewise bar=`cat data`: shebang echo expression: {{foo}} echo default: {{bar}} echo linewise: `cat data` shebang: #!/usr/bin/env sh echo "shebang:" `cat data` "#; const DATA: &str = "OK"; const WANT: &str = "shebang: OK\nexpression: OK\ndefault: OK\nlinewise: OK\n"; /// Test that just runs with the correct working directory when invoked with /// `--justfile` but not `--working-directory` #[test] fn justfile_without_working_directory() -> Result<(), Box> { let tmp = temptree! { justfile: JUSTFILE, data: DATA, }; let output = Command::new(executable_path("just")) .arg("--justfile") .arg(&tmp.path().join("justfile")) .output()?; if !output.status.success() { eprintln!("{:?}", String::from_utf8_lossy(&output.stderr)); panic!(); } let stdout = String::from_utf8(output.stdout).unwrap(); assert_eq!(stdout, WANT); Ok(()) } /// Test that just runs with the correct working directory when invoked with /// `--justfile` but not `--working-directory`, and justfile path has no parent #[test] fn justfile_without_working_directory_relative() -> Result<(), Box> { let tmp = temptree! { justfile: JUSTFILE, data: DATA, }; let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("--justfile") .arg("justfile") .output()?; if !output.status.success() { eprintln!("{:?}", String::from_utf8_lossy(&output.stderr)); panic!(); } let stdout = String::from_utf8(output.stdout).unwrap(); assert_eq!(stdout, WANT); Ok(()) } /// Test that just invokes commands from the directory in which the justfile is /// found #[test] fn change_working_directory_to_search_justfile_parent() -> Result<(), Box> { let tmp = temptree! { justfile: JUSTFILE, data: DATA, subdir: {}, }; let output = Command::new(executable_path("just")) .current_dir(tmp.path().join("subdir")) .output()?; if !output.status.success() { eprintln!("{:?}", String::from_utf8_lossy(&output.stderr)); panic!(); } let stdout = String::from_utf8_lossy(&output.stdout); assert_eq!(stdout, WANT); Ok(()) } /// Test that just runs with the correct working directory when invoked with /// `--justfile` but not `--working-directory` #[test] fn justfile_and_working_directory() -> Result<(), Box> { let tmp = temptree! { justfile: JUSTFILE, sub: { data: DATA, }, }; let output = Command::new(executable_path("just")) .arg("--justfile") .arg(&tmp.path().join("justfile")) .arg("--working-directory") .arg(&tmp.path().join("sub")) .output()?; if !output.status.success() { eprintln!("{:?}", String::from_utf8_lossy(&output.stderr)); panic!(); } let stdout = String::from_utf8(output.stdout).unwrap(); assert_eq!(stdout, WANT); Ok(()) } /// Test that just runs with the correct working directory when invoked with /// `--justfile` but not `--working-directory` #[test] fn search_dir_child() -> Result<(), Box> { let tmp = temptree! { child: { justfile: JUSTFILE, data: DATA, }, }; let output = Command::new(executable_path("just")) .current_dir(tmp.path()) .arg("child/") .output()?; if !output.status.success() { eprintln!("{:?}", String::from_utf8_lossy(&output.stderr)); panic!(); } let stdout = String::from_utf8(output.stdout).unwrap(); assert_eq!(stdout, WANT); Ok(()) } /// Test that just runs with the correct working directory when invoked with /// `--justfile` but not `--working-directory` #[test] fn search_dir_parent() -> Result<(), Box> { let tmp = temptree! { child: { }, justfile: JUSTFILE, data: DATA, }; let output = Command::new(executable_path("just")) .current_dir(&tmp.path().join("child")) .arg("../") .output()?; if !output.status.success() { eprintln!("{:?}", String::from_utf8_lossy(&output.stderr)); panic!(); } let stdout = String::from_utf8(output.stdout).unwrap(); assert_eq!(stdout, WANT); Ok(()) }