├── .github └── workflows │ └── build.yaml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ftdetect ├── just.lua └── just.vim ├── ftplugin └── just.vim ├── indent └── just.vim ├── justfile ├── syntax └── just.vim └── tests ├── Cargo.lock ├── Cargo.toml ├── batch_ftdetect_res.vim ├── cases ├── comment.html ├── comment.just ├── deprecated_obsolete.html ├── deprecated_obsolete.just ├── expressions.html ├── expressions.just ├── ftdetect.yml ├── import.html ├── import.just ├── invalid.html ├── invalid.just ├── kitchen-sink.html ├── kitchen-sink.just ├── line-continuations.html ├── line-continuations.just ├── mod.html ├── mod.just ├── recipes-simple.html ├── recipes-simple.just ├── recipes-with-extras.html ├── recipes-with-extras.just ├── replace_regex-captures.html ├── replace_regex-captures.just ├── set.html ├── set.just ├── tricky.html ├── tricky.just ├── unstable.html └── unstable.just ├── convert-to-html.vim ├── justfile ├── rustfmt.toml └── src ├── common.rs ├── main.rs └── test-ftdetect.rs /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: read 15 | 16 | defaults: 17 | run: 18 | shell: bash 19 | 20 | jobs: 21 | all: 22 | name: All 23 | 24 | runs-on: ubuntu-22.04 25 | 26 | env: 27 | RUSTFLAGS: "--deny warnings" 28 | 29 | defaults: 30 | run: 31 | working-directory: tests 32 | 33 | steps: 34 | - uses: actions/checkout@v5 35 | with: 36 | persist-credentials: false 37 | 38 | # Vim maybe already installed, but this appears to be an undocumented implementation detail. 39 | # Make sure Vim is installed and working, and log Vim version. 40 | - name: Vim 41 | run: | 42 | if ! which vim ;then 43 | sudo apt-get update 44 | sudo apt-get -y install vim 45 | fi 46 | vim --version 47 | 48 | - name: Download Neovim 49 | run: gh -R neovim/neovim release download -p nvim-linux-x86_64.tar.gz 50 | env: 51 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | - name: Setup Neovim 54 | run: | 55 | tar -C "$HOME" -xaf nvim-linux-x86_64.tar.gz 56 | "$HOME/nvim-linux-x86_64/bin/nvim" --version 57 | 58 | - name: Rust Toolchain Info 59 | run: | 60 | rustc --version 61 | cargo --version 62 | cargo clippy --version 63 | cargo fmt --version 64 | 65 | - uses: Swatinem/rust-cache@v2 66 | with: 67 | workspaces: ./tests -> target 68 | 69 | - name: Build 70 | run: cargo build --workspace 71 | 72 | - name: Clippy 73 | run: cargo clippy --all-targets --all-features 74 | 75 | - name: Check Formatting 76 | run: cargo fmt --all --check 77 | 78 | - name: Test Vim 79 | run: | 80 | cargo run 81 | cargo run --bin=test-ftdetect 82 | 83 | - name: Test Neovim 84 | run: | 85 | export TEST_VIM="$HOME/nvim-linux-x86_64/bin/nvim" 86 | cargo run 87 | cargo run --bin=test-ftdetect 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw[a-p] 2 | tests/target 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Thank you for your interest in contributing to `vim-just`! 5 | 6 | ## General 7 | 8 | Any contribution intentionally submitted 9 | for inclusion in the work by you shall be licensed as in [LICENSE](LICENSE), 10 | without any additional terms or conditions. 11 | 12 | ## Pull Requests 13 | 14 | Reviewing a change requires understanding the reason for the change. 15 | To save time and avoid additional back-and-forth, 16 | please include a descriptive rationale when submitting your PR. 17 | If the changes have already been discussed in a `vim-just` issue, 18 | this can simply be a link to the relevant issue. 19 | 20 | We sometimes like to manually apply PRs, so don't be 21 | surprised if we close your PR while pushing your authored commit directly to main. 22 | 23 | Developer Documentation 24 | ======================= 25 | 26 | ## Conformance 27 | 28 | Filetype detection and ftplugin settings follow `just` documentation without deviation. 29 | 30 | Syntax highlighting targets how `just` actually behaves. 31 | This means that if `just` accepts a syntax form, it's considered valid, 32 | even if it's not documented directly. 33 | 34 | ## Test Suite 35 | 36 | `vim-just` includes automated tests of its syntax highlighting and filetype detection. 37 | Running the tests invokes Cargo to build and run the project, which 38 | is a simple test-runner in the `main` fn. 39 | 40 | ### Prerequisites 41 | 42 | * just (of course, lol) 43 | * Rust ([simple/recommended installation instructions](https://www.rust-lang.org/tools/install); for detailed and alternative installation instructions see [here](https://forge.rust-lang.org/infra/other-installation-methods.html)) 44 | 45 | Run `just deps` to install the cargo dev dependencies, which right now is only 46 | [Watchexec CLI](https://crates.io/crates/watchexec-cli). 47 | 48 | ### Running the tests 49 | 50 | To run the syntax highlighting tests, run `just run` in tests/. 51 | If you want to run only a subset of the syntax highlighting tests, 52 | you can pass an optional regex parameter to `just run` matching the filenames you want to include: 53 | 54 | ```bash 55 | # run only the 'kitchen-sink.just' syntax highlighting test 56 | $ just run kitchen-sink 57 | ... 58 | test kitchen-sink… 59 | ok 60 | 61 | # run the syntax highlighting tests with base filenames matching the regex ^\w+$ 62 | $ just run '^\w+$' 63 | ... 64 | test comment… 65 | ok 66 | test deprecated_obsolete… 67 | ok 68 | test invalid… 69 | ok 70 | test set… 71 | ok 72 | test tricky… 73 | ok 74 | ``` 75 | 76 | Note that the `.just` extension is trimmed off before matching against the regex. 77 | 78 | To run the filetype detection tests, run `just ftdetect` in tests/. 79 | 80 | If you're going to do more than a simple change, I recommend calling `just watch` in tests/, 81 | which will watch for any changes in the whole repo and re-run the test suite. 82 | By default, it will run the syntax highlighting tests. 83 | `just watch` accepts positional parameters to customize which tests to watch: 84 | 85 | ```bash 86 | # watch all syntax highlighting tests (default) 87 | $ just watch 88 | 89 | # watch ftdetect tests 90 | $ just watch ftdetect 91 | 92 | # watch all tests 93 | $ just watch ftdetect run 94 | 95 | # watch syntax highlighting tests with filenames matching the regex ^r.*s$ 96 | $ just watch run '^r.*s$' 97 | ``` 98 | 99 | Unless otherwise specified, the test runner will run `vim` to perform the tests. 100 | To test with Neovim or an alternative Vim installation, 101 | the environment variable `TEST_VIM` can be set to the path of the executable: 102 | 103 | ```bash 104 | # test Neovim 105 | export TEST_VIM=nvim 106 | 107 | # one-time run syntax highlighting tests with an alternative Vim executable 108 | TEST_VIM=/opt/vim91/bin/vim just run 109 | ``` 110 | 111 | Vim and Neovim require slightly different invocation. 112 | If the specified executable's 113 | [`file_stem`](https://doc.rust-lang.org/std/path/struct.Path.html#method.file_stem) 114 | contains "nvim", the test runner will treat it as Neovim. 115 | Otherwise, the test runner will treat it as Vim. 116 | 117 | ### How the tests work 118 | 119 | The test runners run Vim with a custom value of the `HOME` environment variable 120 | and create a symlink such that the repository root directory is `~/.vim` to invoked Vim instances, 121 | or `${XDG_CONFIG_HOME}/nvim` to invoked Neovim instances. 122 | 123 | For each `*.just` file in `tests/cases/`, the syntax highlighting test runner opens it in Vim, 124 | runs a Vim script to create HTML representation of the effective highlighting, 125 | normalizes the result, and compares it to the corresponding .html file. 126 | If they don't match, the diff will be printed. 127 | They're effectively "snapshot" tests: at this stage of the syntax files, 128 | this is how the given file should be highlighted. 129 | 130 | The recommended way to update the syntax highlighting tests is 131 | to run them and then copy-paste intentional parts of the 132 | diff output into the relevant html files. 133 | 134 | Filetype detection tests are documented in `tests/cases/ftdetect.yml`. 135 | 136 | ### About test cases 137 | 138 | To assist with accuracy of `vim-just` syntax highlighting, 139 | most of the test case justfiles are designed to be valid and runnable. 140 | 141 | Running `just check-cases` in tests/ performs basic validity checking of all test case justfiles 142 | except for files intentionally flagged as invalid. The exceptions list is in the tests justfile. 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Noah Bogart 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vim Just Syntax 2 | 3 | Vim syntax files for [justfiles](https://github.com/casey/just). 4 | 5 | Works with Vim/GVim >= 8, Neovim >= 0.4. 6 | 7 | ## Installation 8 | 9 | ### [Vim8 Package](https://vimhelp.org/repeat.txt.html#packages) 10 | 11 | ```bash 12 | cd ~/.vim/pack/YOUR-NAMESPACE-HERE/start/ 13 | git clone https://github.com/NoahTheDuke/vim-just.git 14 | ``` 15 | 16 | ### With a plugin manager 17 | 18 | #### [vim-plug](https://github.com/junegunn/vim-plug) 19 | 20 | ```vim 21 | Plug 'NoahTheDuke/vim-just' 22 | ``` 23 | 24 | #### [lazy.nvim](https://github.com/folke/lazy.nvim) 25 | 26 | ```lua 27 | { 28 | "NoahTheDuke/vim-just", 29 | ft = { "just" }, 30 | } 31 | ``` 32 | 33 | ### Third-party packages 34 | 35 | For questions or issues when using these packages, contact the package's maintainer. 36 | 37 | [![Packaging status](https://repology.org/badge/vertical-allrepos/vim:vim-just.svg)](https://repology.org/project/vim:vim-just/versions) 38 | 39 | ## Using alongside `nvim-treesitter` 40 | 41 | If `nvim-treesitter` is installed and has a justfile language parser available, 42 | `nvim-treesitter` will overrule `vim-just` by default. 43 | 44 | To use `vim-just` syntax highlighting with other `nvim-treesitter` features, 45 | configure `nvim-treesitter` not to use its justfile language parser for syntax highlighting: 46 | 47 | ```lua 48 | require("nvim-treesitter.configs").setup({ 49 | highlight = { 50 | enable = true, 51 | disable = { "just" }, 52 | }, 53 | }) 54 | ``` 55 | 56 | For more details or more complex configurations, see 57 | [nvim-treesitter documentation](https://github.com/nvim-treesitter/nvim-treesitter#modules). 58 | 59 | ## Migrating old `git clone` based installations to `main` 60 | 61 | In late March 2023, development was moved from `master` branch to `main` branch, 62 | and `master` is no longer maintained. 63 | Updating installations that used a `git clone` prior to these changes requires some 64 | additional one-time steps: 65 | 66 | ```bash 67 | git fetch 68 | git checkout main 69 | git branch -d master || git branch --unset-upstream master 70 | git remote set-head origin -a 71 | git remote prune origin 72 | ``` 73 | 74 | Now future updates can again be obtained normally. 75 | 76 | ## Contributing & Development 77 | 78 | See [CONTRIBUTING.md](CONTRIBUTING.md). 79 | -------------------------------------------------------------------------------- /ftdetect/just.lua: -------------------------------------------------------------------------------- 1 | -- Neovim filetype plugin 2 | -- Language: Justfile 3 | -- Maintainer: Noah Bogart 4 | -- URL: https://github.com/NoahTheDuke/vim-just.git 5 | -- Last Change: 2025 Feb 05 6 | 7 | if vim.fn.has("nvim-0.8") == 1 then 8 | vim.filetype.add({ 9 | -- Neovim adds start/end anchors to the patterns 10 | pattern = { 11 | ['[Jj][Uu][Ss][Tt][Ff][Ii][Ll][Ee]'] = 'just', 12 | ['.*%.[Jj][Uu][Ss][Tt][Ff][Ii][Ll][Ee]'] = 'just', 13 | ['.*%.[Jj][Uu][Ss][Tt]'] = 'just', 14 | }, 15 | }) 16 | end 17 | -------------------------------------------------------------------------------- /ftdetect/just.vim: -------------------------------------------------------------------------------- 1 | " Vim filetype plugin 2 | " Language: Justfile 3 | " Maintainer: Noah Bogart 4 | " URL: https://github.com/NoahTheDuke/vim-just.git 5 | " Last Change: 2024 May 30 6 | 7 | if !has("nvim-0.8") 8 | au BufNewFile,BufRead \c{,*.}justfile,\c*.just setfiletype just 9 | endif 10 | 11 | au BufNewFile,BufRead * if getline(1) =~# '\v^#!/%(\w|[-/])*/%(env%(\s+-S)?\s+)?just\A' | setfiletype just | endif 12 | -------------------------------------------------------------------------------- /ftplugin/just.vim: -------------------------------------------------------------------------------- 1 | " Vim ftplugin file 2 | " Language: Justfile 3 | " Maintainer: Noah Bogart 4 | " URL: https://github.com/NoahTheDuke/vim-just.git 5 | " Last Change: 2023 Jul 08 6 | 7 | " Only do this when not done yet for this buffer 8 | if exists("b:did_ftplugin") 9 | finish 10 | endif 11 | let b:did_ftplugin = 1 12 | 13 | setlocal iskeyword+=- 14 | setlocal comments=n:# 15 | setlocal commentstring=#\ %s 16 | 17 | let b:undo_ftplugin = "setlocal iskeyword< comments< commentstring<" 18 | -------------------------------------------------------------------------------- /indent/just.vim: -------------------------------------------------------------------------------- 1 | " Vim indent file 2 | " Language: Justfile 3 | " Maintainer: Noah Bogart 4 | " URL: https://github.com/NoahTheDuke/vim-just.git 5 | " Last Change: 2024 Jan 25 6 | 7 | " Only load this indent file when no other was loaded yet. 8 | if exists("b:did_indent") 9 | finish 10 | endif 11 | let b:did_indent = 1 12 | 13 | setlocal indentexpr=GetJustfileIndent() 14 | setlocal indentkeys=0},0),!^F,o,O,0=''',0=\"\"\" 15 | 16 | let b:undo_indent = "setlocal indentexpr< indentkeys<" 17 | 18 | if exists("*GetJustfileIndent") 19 | finish 20 | endif 21 | 22 | function GetJustfileIndent() 23 | if v:lnum < 2 24 | return 0 25 | endif 26 | 27 | let prev_line = getline(v:lnum - 1) 28 | let last_indent = indent(v:lnum - 1) 29 | 30 | if getline(v:lnum) =~ "\\v^\\s+%([})]|'''$|\"\"\"$)" 31 | return last_indent - shiftwidth() 32 | elseif prev_line =~ '\V#' 33 | return last_indent 34 | elseif prev_line =~ "\\v%([:{(]|^.*\\S.*%([^']'''|[^\"]\"\"\"))\\s*$" 35 | return last_indent + shiftwidth() 36 | elseif prev_line =~ '\\$' 37 | if v:lnum == 2 || getline(v:lnum - 2) !~ '\\$' 38 | if prev_line =~ '\v:\=@!' 39 | return last_indent + shiftwidth() + shiftwidth() 40 | else 41 | return last_indent + shiftwidth() 42 | endif 43 | endif 44 | elseif v:lnum > 2 && getline(v:lnum - 2) =~ '\\$' 45 | return last_indent - shiftwidth() 46 | elseif prev_line =~ '\v:\s*%(\h|\()' && prev_line !~ '\V:=' 47 | return last_indent + shiftwidth() 48 | endif 49 | 50 | return last_indent 51 | endfunction 52 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # Do not prevent local external justfile recipes from being run from cwd within the repo 2 | set fallback 3 | 4 | mod test 'tests/justfile' 5 | 6 | justq := quote(just_executable()) + ' -f ' + quote(justfile()) 7 | test_cases := justfile_directory() / 'tests/cases' 8 | 9 | @_default: 10 | {{justq}} --list 11 | 12 | # delete all untracked files from the repository 13 | clean-all: 14 | git clean -fxd 15 | 16 | # revert repository to committed state 17 | reset rev='': && clean-all 18 | git checkout --force {{rev}} 19 | 20 | # show repository status 21 | id: 22 | git show --format=fuller --no-patch HEAD 23 | git status --ignored 24 | 25 | # preview JUSTFILE in Vim with syntax file from this repository 26 | [no-cd] 27 | preview JUSTFILE='': (_preview_common 'vim' JUSTFILE) 28 | 29 | # preview JUSTFILE in GVim with syntax file from this repository 30 | [no-cd] 31 | gpreview JUSTFILE='': (_preview_common 'gvim -f' JUSTFILE) 32 | 33 | # preview JUSTFILE in Neovim with syntax file from this repository 34 | [no-cd] 35 | npreview JUSTFILE='': (_preview_common 'nvim' JUSTFILE) 36 | 37 | [no-cd] 38 | _preview_common vimcmd JUSTFILE: 39 | {{vimcmd}} \ 40 | -c {{quote("let &runtimepath=\"" + justfile_directory() + ",\" . &runtimepath")}} \ 41 | -c {{quote('runtime ftdetect/just.vim | if has("nvim") | runtime ftdetect/just.lua | endif')}} \ 42 | {{if JUSTFILE == '' { '-c "set filetype=just"' } \ 43 | else { \ 44 | "-c " + quote('edit ' + \ 45 | if path_exists(test_cases / JUSTFILE + '.just') == 'true' { \ 46 | test_cases / JUSTFILE + '.just' \ 47 | } else { JUSTFILE }) \ 48 | } }} 49 | 50 | 51 | update-last-changed *force: 52 | #!/bin/bash 53 | for f in $(find * -name just.vim -o -name just.lua);do 54 | gitrev="$(git log -n 1 --format='format:%H' -- "$f")" 55 | if [[ {{quote(force)}} != *"$f"* ]];then 56 | git show "$gitrev" | grep -q -P -i '^\+\S+\s*Last\s+Change:' && continue 57 | fi 58 | lastchange="$(git show -s "$gitrev" --format='%cd' --date='format:%Y %b %d')" 59 | echo -e "$f -> Last Change: $lastchange" 60 | sed --in-place -E -e "s/(^\\S+\\s*Last\\s+Change:\\s+).+$/\\1${lastchange}/g" "$f" 61 | done 62 | 63 | 64 | just_boolean_settings := """ 65 | allow-duplicate-recipes 66 | allow-duplicate-variables 67 | dotenv-load 68 | dotenv-override 69 | dotenv-required 70 | export 71 | fallback 72 | ignore-comments 73 | no-exit-message 74 | positional-arguments 75 | quiet 76 | unstable 77 | windows-powershell 78 | """ 79 | 80 | # Newline-separated list of `just` functions. 81 | # Some functions are intentionally omitted from this list because they're handled as special cases: 82 | # - error 83 | # - env_var 84 | # - env_var_or_default 85 | just_functions := replace_regex(''' 86 | absolute_path 87 | append 88 | arch 89 | blake3 90 | blake3_file 91 | cache_directory 92 | canonicalize 93 | capitalize 94 | choose 95 | clean 96 | config_directory 97 | config_local_directory 98 | data_directory 99 | data_local_directory 100 | datetime 101 | datetime_utc 102 | encode_uri_component 103 | env 104 | executable_directory 105 | extension 106 | file_name 107 | file_stem 108 | home_directory 109 | invocation_directory 110 | invocation_directory_native 111 | is_dependency 112 | join 113 | just_executable 114 | just_pid 115 | justfile 116 | justfile_directory 117 | kebabcase 118 | lowercamelcase 119 | lowercase 120 | module_directory 121 | module_file 122 | num_cpus 123 | os 124 | os_family 125 | parent_directory 126 | path_exists 127 | prepend 128 | quote 129 | read 130 | replace 131 | replace_regex 132 | require 133 | semver_matches 134 | sha256 135 | sha256_file 136 | shell 137 | shoutykebabcase 138 | shoutysnakecase 139 | snakecase 140 | source_directory 141 | source_file 142 | style 143 | titlecase 144 | trim 145 | trim_end 146 | trim_end_match 147 | trim_end_matches 148 | trim_start 149 | trim_start_match 150 | trim_start_matches 151 | uppercamelcase 152 | uppercase 153 | uuid 154 | which 155 | without_extension 156 | ''', '(?m)^(.*)_directory(_native)?$', "${1}_dir$2\n$0") 157 | 158 | @functions: 159 | echo -n {{quote(just_functions)}} | LC_ALL=C sort | tr '\n' ' ' | sed -E -e 's/\s+$/\n/g' 160 | 161 | # generate an optimized Vim-style "very magic" regex snippet from a list of literal strings to match 162 | optrx +strings: 163 | #!/usr/bin/env python3 164 | vparam = """{{strings}}""" 165 | import collections, re 166 | strings_list = vparam.split('|') if '|' in vparam else vparam.strip().split() 167 | vimSpecialChars = tuple('~@$%^&*()+=[]{}\\|<>.?') 168 | def vimEscape(c): 169 | if type(c) is str and len(c) < 2: 170 | return f'\\{c}' if c in vimSpecialChars else c 171 | raise TypeError(f'{c!r} is not a character') 172 | charByPrefix=dict() 173 | for f in strings_list: 174 | if len(f) < 1: continue 175 | g=collections.deque(map(vimEscape, f)) 176 | p=charByPrefix 177 | while len(g): 178 | if g[0] not in p: p[g[0]] = dict() 179 | p=p[g.popleft()] 180 | p[''] = dict() 181 | def recursive_coalesce(d, addGrouping=False): 182 | o='' 183 | l = collections.deque(d.keys()) 184 | # Ensure empty string is at the end 185 | if '' in l and l[-1] != '': 186 | l.remove('') 187 | l.append('') 188 | while len(l): 189 | c = l.popleft() 190 | o += c 191 | if len(d[c]) > 1: 192 | o += recursive_coalesce(d[c], True) 193 | else: 194 | if len(d[c]) == 1: 195 | o += recursive_coalesce(d[c]) 196 | if len(l): 197 | o += '|' 198 | # Do all items in this group have a common suffix? 199 | ss=o.split('|') 200 | if len(ss) > 1: 201 | commonEnd = '' 202 | tryCommonEnd = ss[0][:] 203 | while len(tryCommonEnd): 204 | c=0 205 | for j in ss: 206 | if re.search(r'%\((?:[^)]|\\\))+$', j): 207 | # don't compare suffix in inner group to suffix of outer group 208 | continue 209 | c += int(j.endswith(tryCommonEnd)) 210 | if c == len(ss): 211 | commonEnd = tryCommonEnd 212 | break 213 | tryCommonEnd=tryCommonEnd[1:] 214 | if not(commonEnd in ('', ')?')): 215 | o = '%(' + ('|'.join(map(lambda s: s[:s.rfind(commonEnd)], ss))) + ')' + commonEnd 216 | elif addGrouping: 217 | o = f'%({o})' 218 | return o.replace('|)', ')?') 219 | print(recursive_coalesce(charByPrefix)) 220 | 221 | # run optrx on a variable from this justfile 222 | @optrx_var var: 223 | {{justq}} optrx "$({{justq}} --evaluate {{var}})" 224 | -------------------------------------------------------------------------------- /tests/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.12" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "aho-corasick" 19 | version = "1.1.3" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 22 | dependencies = [ 23 | "memchr", 24 | ] 25 | 26 | [[package]] 27 | name = "allocator-api2" 28 | version = "0.2.21" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 31 | 32 | [[package]] 33 | name = "arraydeque" 34 | version = "0.5.1" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" 37 | 38 | [[package]] 39 | name = "bit-set" 40 | version = "0.8.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" 43 | dependencies = [ 44 | "bit-vec", 45 | ] 46 | 47 | [[package]] 48 | name = "bit-vec" 49 | version = "0.8.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" 52 | 53 | [[package]] 54 | name = "bitflags" 55 | version = "2.9.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 58 | 59 | [[package]] 60 | name = "cfg-if" 61 | version = "1.0.1" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 64 | 65 | [[package]] 66 | name = "cfg_aliases" 67 | version = "0.2.1" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 70 | 71 | [[package]] 72 | name = "crossbeam-deque" 73 | version = "0.8.6" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 76 | dependencies = [ 77 | "crossbeam-epoch", 78 | "crossbeam-utils", 79 | ] 80 | 81 | [[package]] 82 | name = "crossbeam-epoch" 83 | version = "0.9.18" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 86 | dependencies = [ 87 | "crossbeam-utils", 88 | ] 89 | 90 | [[package]] 91 | name = "crossbeam-utils" 92 | version = "0.8.21" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 95 | 96 | [[package]] 97 | name = "ctrlc" 98 | version = "3.4.7" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" 101 | dependencies = [ 102 | "nix", 103 | "windows-sys 0.59.0", 104 | ] 105 | 106 | [[package]] 107 | name = "either" 108 | version = "1.15.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 111 | 112 | [[package]] 113 | name = "encoding_rs" 114 | version = "0.8.35" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 117 | dependencies = [ 118 | "cfg-if", 119 | ] 120 | 121 | [[package]] 122 | name = "errno" 123 | version = "0.3.13" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 126 | dependencies = [ 127 | "libc", 128 | "windows-sys 0.60.2", 129 | ] 130 | 131 | [[package]] 132 | name = "fancy-regex" 133 | version = "0.14.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" 136 | dependencies = [ 137 | "bit-set", 138 | "regex-automata", 139 | "regex-syntax", 140 | ] 141 | 142 | [[package]] 143 | name = "fastrand" 144 | version = "2.3.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 147 | 148 | [[package]] 149 | name = "getrandom" 150 | version = "0.3.3" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 153 | dependencies = [ 154 | "cfg-if", 155 | "libc", 156 | "r-efi", 157 | "wasi", 158 | ] 159 | 160 | [[package]] 161 | name = "hashbrown" 162 | version = "0.14.5" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 165 | dependencies = [ 166 | "ahash", 167 | "allocator-api2", 168 | ] 169 | 170 | [[package]] 171 | name = "hashlink" 172 | version = "0.8.4" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" 175 | dependencies = [ 176 | "hashbrown", 177 | ] 178 | 179 | [[package]] 180 | name = "libc" 181 | version = "0.2.174" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 184 | 185 | [[package]] 186 | name = "linux-raw-sys" 187 | version = "0.9.4" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 190 | 191 | [[package]] 192 | name = "memchr" 193 | version = "2.7.5" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 196 | 197 | [[package]] 198 | name = "nix" 199 | version = "0.30.1" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 202 | dependencies = [ 203 | "bitflags", 204 | "cfg-if", 205 | "cfg_aliases", 206 | "libc", 207 | ] 208 | 209 | [[package]] 210 | name = "once_cell" 211 | version = "1.21.3" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 214 | 215 | [[package]] 216 | name = "ppv-lite86" 217 | version = "0.2.21" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 220 | dependencies = [ 221 | "zerocopy", 222 | ] 223 | 224 | [[package]] 225 | name = "proc-macro2" 226 | version = "1.0.95" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 229 | dependencies = [ 230 | "unicode-ident", 231 | ] 232 | 233 | [[package]] 234 | name = "quote" 235 | version = "1.0.40" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 238 | dependencies = [ 239 | "proc-macro2", 240 | ] 241 | 242 | [[package]] 243 | name = "r-efi" 244 | version = "5.3.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 247 | 248 | [[package]] 249 | name = "rand" 250 | version = "0.9.1" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 253 | dependencies = [ 254 | "rand_chacha", 255 | "rand_core", 256 | ] 257 | 258 | [[package]] 259 | name = "rand_chacha" 260 | version = "0.9.0" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 263 | dependencies = [ 264 | "ppv-lite86", 265 | "rand_core", 266 | ] 267 | 268 | [[package]] 269 | name = "rand_core" 270 | version = "0.9.3" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 273 | dependencies = [ 274 | "getrandom", 275 | ] 276 | 277 | [[package]] 278 | name = "rayon" 279 | version = "1.10.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 282 | dependencies = [ 283 | "either", 284 | "rayon-core", 285 | ] 286 | 287 | [[package]] 288 | name = "rayon-core" 289 | version = "1.12.1" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 292 | dependencies = [ 293 | "crossbeam-deque", 294 | "crossbeam-utils", 295 | ] 296 | 297 | [[package]] 298 | name = "regex-automata" 299 | version = "0.4.9" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 302 | dependencies = [ 303 | "aho-corasick", 304 | "memchr", 305 | "regex-syntax", 306 | ] 307 | 308 | [[package]] 309 | name = "regex-syntax" 310 | version = "0.8.5" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 313 | 314 | [[package]] 315 | name = "rustix" 316 | version = "1.0.7" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 319 | dependencies = [ 320 | "bitflags", 321 | "errno", 322 | "libc", 323 | "linux-raw-sys", 324 | "windows-sys 0.59.0", 325 | ] 326 | 327 | [[package]] 328 | name = "serde" 329 | version = "1.0.219" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 332 | dependencies = [ 333 | "serde_derive", 334 | ] 335 | 336 | [[package]] 337 | name = "serde_derive" 338 | version = "1.0.219" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 341 | dependencies = [ 342 | "proc-macro2", 343 | "quote", 344 | "syn", 345 | ] 346 | 347 | [[package]] 348 | name = "serde_yaml2" 349 | version = "0.1.3" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "63a7808e70b0b09116f01a4730d8976bcab457ea4a5e2e638e9b12ed3e01f27c" 352 | dependencies = [ 353 | "serde", 354 | "thiserror", 355 | "yaml-rust2", 356 | ] 357 | 358 | [[package]] 359 | name = "similar" 360 | version = "2.7.0" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" 363 | 364 | [[package]] 365 | name = "syn" 366 | version = "2.0.104" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 369 | dependencies = [ 370 | "proc-macro2", 371 | "quote", 372 | "unicode-ident", 373 | ] 374 | 375 | [[package]] 376 | name = "tempfile" 377 | version = "3.20.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 380 | dependencies = [ 381 | "fastrand", 382 | "getrandom", 383 | "once_cell", 384 | "rustix", 385 | "windows-sys 0.59.0", 386 | ] 387 | 388 | [[package]] 389 | name = "tests" 390 | version = "0.0.0" 391 | dependencies = [ 392 | "ctrlc", 393 | "fancy-regex", 394 | "rand", 395 | "rayon", 396 | "serde", 397 | "serde_yaml2", 398 | "similar", 399 | "tempfile", 400 | "wait-timeout", 401 | ] 402 | 403 | [[package]] 404 | name = "thiserror" 405 | version = "1.0.69" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 408 | dependencies = [ 409 | "thiserror-impl", 410 | ] 411 | 412 | [[package]] 413 | name = "thiserror-impl" 414 | version = "1.0.69" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 417 | dependencies = [ 418 | "proc-macro2", 419 | "quote", 420 | "syn", 421 | ] 422 | 423 | [[package]] 424 | name = "unicode-ident" 425 | version = "1.0.18" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 428 | 429 | [[package]] 430 | name = "version_check" 431 | version = "0.9.5" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 434 | 435 | [[package]] 436 | name = "wait-timeout" 437 | version = "0.2.1" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" 440 | dependencies = [ 441 | "libc", 442 | ] 443 | 444 | [[package]] 445 | name = "wasi" 446 | version = "0.14.2+wasi-0.2.4" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 449 | dependencies = [ 450 | "wit-bindgen-rt", 451 | ] 452 | 453 | [[package]] 454 | name = "windows-sys" 455 | version = "0.59.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 458 | dependencies = [ 459 | "windows-targets 0.52.6", 460 | ] 461 | 462 | [[package]] 463 | name = "windows-sys" 464 | version = "0.60.2" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 467 | dependencies = [ 468 | "windows-targets 0.53.2", 469 | ] 470 | 471 | [[package]] 472 | name = "windows-targets" 473 | version = "0.52.6" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 476 | dependencies = [ 477 | "windows_aarch64_gnullvm 0.52.6", 478 | "windows_aarch64_msvc 0.52.6", 479 | "windows_i686_gnu 0.52.6", 480 | "windows_i686_gnullvm 0.52.6", 481 | "windows_i686_msvc 0.52.6", 482 | "windows_x86_64_gnu 0.52.6", 483 | "windows_x86_64_gnullvm 0.52.6", 484 | "windows_x86_64_msvc 0.52.6", 485 | ] 486 | 487 | [[package]] 488 | name = "windows-targets" 489 | version = "0.53.2" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" 492 | dependencies = [ 493 | "windows_aarch64_gnullvm 0.53.0", 494 | "windows_aarch64_msvc 0.53.0", 495 | "windows_i686_gnu 0.53.0", 496 | "windows_i686_gnullvm 0.53.0", 497 | "windows_i686_msvc 0.53.0", 498 | "windows_x86_64_gnu 0.53.0", 499 | "windows_x86_64_gnullvm 0.53.0", 500 | "windows_x86_64_msvc 0.53.0", 501 | ] 502 | 503 | [[package]] 504 | name = "windows_aarch64_gnullvm" 505 | version = "0.52.6" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 508 | 509 | [[package]] 510 | name = "windows_aarch64_gnullvm" 511 | version = "0.53.0" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 514 | 515 | [[package]] 516 | name = "windows_aarch64_msvc" 517 | version = "0.52.6" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 520 | 521 | [[package]] 522 | name = "windows_aarch64_msvc" 523 | version = "0.53.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 526 | 527 | [[package]] 528 | name = "windows_i686_gnu" 529 | version = "0.52.6" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 532 | 533 | [[package]] 534 | name = "windows_i686_gnu" 535 | version = "0.53.0" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 538 | 539 | [[package]] 540 | name = "windows_i686_gnullvm" 541 | version = "0.52.6" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 544 | 545 | [[package]] 546 | name = "windows_i686_gnullvm" 547 | version = "0.53.0" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 550 | 551 | [[package]] 552 | name = "windows_i686_msvc" 553 | version = "0.52.6" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 556 | 557 | [[package]] 558 | name = "windows_i686_msvc" 559 | version = "0.53.0" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 562 | 563 | [[package]] 564 | name = "windows_x86_64_gnu" 565 | version = "0.52.6" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 568 | 569 | [[package]] 570 | name = "windows_x86_64_gnu" 571 | version = "0.53.0" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 574 | 575 | [[package]] 576 | name = "windows_x86_64_gnullvm" 577 | version = "0.52.6" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 580 | 581 | [[package]] 582 | name = "windows_x86_64_gnullvm" 583 | version = "0.53.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 586 | 587 | [[package]] 588 | name = "windows_x86_64_msvc" 589 | version = "0.52.6" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 592 | 593 | [[package]] 594 | name = "windows_x86_64_msvc" 595 | version = "0.53.0" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 598 | 599 | [[package]] 600 | name = "wit-bindgen-rt" 601 | version = "0.39.0" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 604 | dependencies = [ 605 | "bitflags", 606 | ] 607 | 608 | [[package]] 609 | name = "yaml-rust2" 610 | version = "0.8.1" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" 613 | dependencies = [ 614 | "arraydeque", 615 | "encoding_rs", 616 | "hashlink", 617 | ] 618 | 619 | [[package]] 620 | name = "zerocopy" 621 | version = "0.8.26" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" 624 | dependencies = [ 625 | "zerocopy-derive", 626 | ] 627 | 628 | [[package]] 629 | name = "zerocopy-derive" 630 | version = "0.8.26" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" 633 | dependencies = [ 634 | "proc-macro2", 635 | "quote", 636 | "syn", 637 | ] 638 | -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tests" 3 | authors = ["Casey Rodarmor ", "Noah Bogart > $OUTPUT | echo @% | set ft? | redir END 5 | 6 | qa 7 | -------------------------------------------------------------------------------- /tests/cases/comment.html: -------------------------------------------------------------------------------- 1 | # foo 2 | 3 | #!/usr/bin/env just --justfile 4 | 5 | # foo 6 | -------------------------------------------------------------------------------- /tests/cases/comment.just: -------------------------------------------------------------------------------- 1 | # foo 2 | 3 | #!/usr/bin/env just --justfile 4 | 5 | # foo 6 | -------------------------------------------------------------------------------- /tests/cases/deprecated_obsolete.html: -------------------------------------------------------------------------------- 1 | !include subdir/some.just 2 | 3 | set windows-powershell 4 | set windows-powershell:=false 5 | set windows-powershell := true 6 | -------------------------------------------------------------------------------- /tests/cases/deprecated_obsolete.just: -------------------------------------------------------------------------------- 1 | !include subdir/some.just 2 | 3 | set windows-powershell 4 | set windows-powershell:=false 5 | set windows-powershell := true 6 | -------------------------------------------------------------------------------- /tests/cases/expressions.html: -------------------------------------------------------------------------------- 1 | xdg_cache := cache_directory() 2 | xdg_config := config_directory() 3 | xdg_config_local := config_local_directory() 4 | xdg_data := data_directory() 5 | xdg_data_local := data_local_directory() 6 | xdg_executable := executable_directory() 7 | xdg_home := home_directory() 8 | 9 | mydir := justfile_dir() 10 | invoked_from := invocation_dir_native() 11 | 12 | random1 := choose('10', HEXLOWER + replace_regex(HEXUPPER, '\d+', '')) 13 | 14 | example_url := 'https://foo.example/?parameter=' + encode_uri_component( 15 | prepend('/', append('/', 'foo bar baz ' + HEX)) 16 | ) 17 | 18 | shell_expanded1 := x'$SHELL -c ' 19 | shell_expanded2 := x"${SHELL:-/bin/sh} -c " 20 | shell_expanded3 := x''' 21 | $_''' 22 | shell_expanded4 := x""" 23 | ${FOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO:-foo\r\n 24 | bar 25 | baz} 26 | $DIS\ 27 | PLAY 28 | Foo 29 | """ 30 | shell_expanded5 := x''' 31 | ${FOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO:-foo\r\n 32 | bar 33 | baz} 34 | ''' 35 | shell_expanded6 := x'$$$$not_an_env_var $$$${also_not_an_env_var}' 36 | shell_expanded7 := x''' 37 | ${FOOOOOOOOOOOOOOOOO:-bar \ 38 | baz 39 | } 40 | ''' 41 | shell_expanded8 := x""" 42 | $\ 43 | $\ 44 | $\ 45 | {\ 46 | F\ 47 | O\ 48 | O\ 49 | :\ 50 | -\ 51 | f\ 52 | o\ 53 | o\ 54 | }\ 55 | """ 56 | 57 | _true := assert('1' != '0', '1 is not 0') 58 | 59 | shell_command := shell('echo "$@"', _true, random1) 60 | 61 | this_source := source_directory() + "\n -> " + source_file() 62 | current_module := module_directory() + "\n -> " + module_file() 63 | 64 | unexport QT_QPA_PLATFORMTHEME 65 | 66 | now := datetime('%Y-%m-%d %H:%M:%S %Z') 67 | now_utc := datetime_utc('%Y-%m-%d %H:%M:%S %Z') 68 | 69 | [private] 70 | hidden := 'foo' 71 | not_hidden := '-' + hidden 72 | 73 | u_escape := "test \u{1F916} test" 74 | u_escape_triple_quoted := """ 75 | test 76 | \u{1f9Ea} 77 | test 78 | """ 79 | u_escape_shell_expanded := x"shell ${FOO:-(\u{1f41a})} shell" 80 | u_escape_shell_expanded_name := x""" 81 | TERM=${\u{000054}\u{00045}\u{0052}\u{04d}}""" 82 | u_escape_in_replace_regex := replace_regex('foo', '^.', "\u{46}") 83 | not_u_escape := '\u{1F916}' 84 | 85 | _regex_mismatch := assert('foo' !~ '^ba', "foo is not bar or baz") 86 | 87 | test_path := PATH_SEP + 'bin' + PATH_VAR_SEP + env("PATH") 88 | -------------------------------------------------------------------------------- /tests/cases/expressions.just: -------------------------------------------------------------------------------- 1 | xdg_cache := cache_directory() 2 | xdg_config := config_directory() 3 | xdg_config_local := config_local_directory() 4 | xdg_data := data_directory() 5 | xdg_data_local := data_local_directory() 6 | xdg_executable := executable_directory() 7 | xdg_home := home_directory() 8 | 9 | mydir := justfile_dir() 10 | invoked_from := invocation_dir_native() 11 | 12 | random1 := choose('10', HEXLOWER + replace_regex(HEXUPPER, '\d+', '')) 13 | 14 | example_url := 'https://foo.example/?parameter=' + encode_uri_component( 15 | prepend('/', append('/', 'foo bar baz ' + HEX)) 16 | ) 17 | 18 | shell_expanded1 := x'$SHELL -c ' 19 | shell_expanded2 := x"${SHELL:-/bin/sh} -c " 20 | shell_expanded3 := x''' 21 | $_''' 22 | shell_expanded4 := x""" 23 | ${FOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO:-foo\r\n 24 | bar 25 | baz} 26 | $DIS\ 27 | PLAY 28 | Foo 29 | """ 30 | shell_expanded5 := x''' 31 | ${FOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO:-foo\r\n 32 | bar 33 | baz} 34 | ''' 35 | shell_expanded6 := x'$$$$not_an_env_var $$$${also_not_an_env_var}' 36 | shell_expanded7 := x''' 37 | ${FOOOOOOOOOOOOOOOOO:-bar \ 38 | baz 39 | } 40 | ''' 41 | shell_expanded8 := x""" 42 | $\ 43 | $\ 44 | $\ 45 | {\ 46 | F\ 47 | O\ 48 | O\ 49 | :\ 50 | -\ 51 | f\ 52 | o\ 53 | o\ 54 | }\ 55 | """ 56 | 57 | _true := assert('1' != '0', '1 is not 0') 58 | 59 | shell_command := shell('echo "$@"', _true, random1) 60 | 61 | this_source := source_directory() + "\n -> " + source_file() 62 | current_module := module_directory() + "\n -> " + module_file() 63 | 64 | unexport QT_QPA_PLATFORMTHEME 65 | 66 | now := datetime('%Y-%m-%d %H:%M:%S %Z') 67 | now_utc := datetime_utc('%Y-%m-%d %H:%M:%S %Z') 68 | 69 | [private] 70 | hidden := 'foo' 71 | not_hidden := '-' + hidden 72 | 73 | u_escape := "test \u{1F916} test" 74 | u_escape_triple_quoted := """ 75 | test 76 | \u{1f9Ea} 77 | test 78 | """ 79 | u_escape_shell_expanded := x"shell ${FOO:-(\u{1f41a})} shell" 80 | u_escape_shell_expanded_name := x""" 81 | TERM=${\u{000054}\u{00045}\u{0052}\u{04d}}""" 82 | u_escape_in_replace_regex := replace_regex('foo', '^.', "\u{46}") 83 | not_u_escape := '\u{1F916}' 84 | 85 | _regex_mismatch := assert('foo' !~ '^ba', "foo is not bar or baz") 86 | 87 | test_path := PATH_SEP + 'bin' + PATH_VAR_SEP + env("PATH") 88 | -------------------------------------------------------------------------------- /tests/cases/ftdetect.yml: -------------------------------------------------------------------------------- 1 | # This file specifies a list of files to generate for `vim-just` filetype detection tests. 2 | # Each list entry can have the following properties: 3 | # 4 | # filename: (string) The name of the file. 5 | # Each occurrence of `*` will be replaced with a 6 | # non-empty random sequence of alphanumeric characters. 7 | # If not specified, the file will have a completely random alphanumeric name. 8 | # 9 | # never: (string) Regex which filename must NOT match for this test case. 10 | # 11 | # content: (string) The file contents. If not specified, the file will be empty. 12 | # 13 | # not_justfile: (boolean) Set this to `true` if the file specified by the other properties 14 | # should **not** be detected as a justfile. 15 | 16 | - filename: justfile 17 | 18 | - filename: Justfile 19 | 20 | - filename: JUSTFILE 21 | 22 | - filename: JuStFiLe 23 | 24 | - filename: .justfile 25 | 26 | - filename: .Justfile 27 | 28 | - filename: .JUSTFILE 29 | 30 | - filename: "*.just" 31 | 32 | - filename: "*.JUST" 33 | 34 | - filename: "*.justfile" 35 | 36 | - filename: "*.Justfile" 37 | 38 | - filename: "*.JUSTFILE" 39 | 40 | - filename: "*.Just" 41 | 42 | - filename: "*justfile" 43 | not_justfile: true 44 | 45 | - filename: .*justfile 46 | not_justfile: true 47 | 48 | - filename: "*.*justfile" 49 | not_justfile: true 50 | 51 | - filename: "*.*just" 52 | not_justfile: true 53 | 54 | - filename: justfile* 55 | not_justfile: true 56 | 57 | - filename: .justfile* 58 | not_justfile: true 59 | 60 | - filename: "*.justfile*" 61 | not_justfile: true 62 | 63 | - filename: "*.just*" 64 | not_justfile: true 65 | 66 | - filename: just 67 | not_justfile: true 68 | 69 | - content: "#!/usr/bin/env -S just -f\n" 70 | 71 | - content: "#!/usr/bin/env just -f\n" 72 | 73 | - content: "#!/usr/local/bin/just -f\n" 74 | 75 | - content: "#!/opt/local/bin-/just -f\n" 76 | 77 | - not_justfile: true 78 | 79 | - content: "#!/usr//bin/env just -f\n" 80 | 81 | - content: "#!//usr/local/bin/just -f\n" 82 | 83 | - content: "#!/usr/bin/env just --justfile\n" 84 | 85 | - content: "#!/usr/bin/env just --working-directory . --justfile\n" 86 | 87 | - content: "#!/usr/bin/env -S just --justfile\n" 88 | -------------------------------------------------------------------------------- /tests/cases/import.html: -------------------------------------------------------------------------------- 1 | import 'recipes-simple.just' 2 | 3 | import\ 4 | 'line-continuations.just' 5 | 6 | import ? """ 7 | a.just""" 8 | 9 | import? x'${FOOOOOOOOO:-.}/foo.just' 10 | import x"${FOOOOOOOOO:-.}/expressions.just" 11 | -------------------------------------------------------------------------------- /tests/cases/import.just: -------------------------------------------------------------------------------- 1 | import 'recipes-simple.just' 2 | 3 | import\ 4 | 'line-continuations.just' 5 | 6 | import ? """ 7 | a.just""" 8 | 9 | import? x'${FOOOOOOOOO:-.}/foo.just' 10 | import x"${FOOOOOOOOO:-.}/expressions.just" 11 | -------------------------------------------------------------------------------- /tests/cases/invalid.html: -------------------------------------------------------------------------------- 1 | !include not-at-beginning-of-line.just 2 | 3 | mod -invalid-mod 4 | 5 | set shell 6 | 7 | bash := 'bash' 8 | c := '-c' 9 | set shell := [bash, c] 10 | 11 | invalid_escapes := "\q \u{} \u{foo}\u{1234123} \u1234" 12 | invalid_escapes_shell_expanded := x"${foo:-z\u{zzz}z}" 13 | 14 | # export is only valid when prefixing an assignment 15 | # just thinks the following line is a malformed attempt to start a recipe called "export" 16 | export FOO 17 | 18 | (the next line is not a recipe body)... 19 | this text should not be highlighted 20 | 21 | badfunc5 := num_cpus(\) 22 | 23 | not_a_recipe_name #comment: 24 | not a recipe body 25 | 26 | nonexistant_func := blahuuid() 27 | nonexistant_func2 := blah_replace_regex('foo', '([^o])', '$1') 28 | 29 | invalid_recipe_body: 30 | echo 'My indentation mixes spaces and tabs!' 31 | echo 'My indentation mixes tabs and spaces!' 32 | echo 'Mixing spaces and tabs again.' 33 | 34 | invalid_recipe_body2: 35 | echo This line is valid 36 | echo but this one is not 37 | invalid_recipe_body3: 38 | echo "valid again 39 | not valid" 40 | invalid_recipe_body4: 41 | echo 'valid again 42 | not valid' 43 | 44 | not_a_recipe_start 45 | not a recipe body 46 | bad_but_not_mixed_indentation: 47 | echo 'tab indent' 48 | echo 'space indent' 49 | bad_but_not_mixed_indentation_shebang: 50 | #!/bin/bash 51 | echo 'space indent, same as first line' 52 | echo 'acceptable space indent' 53 | echo 'tab indent' 54 | 55 | bad_curly_braces: 56 | echo {{{Odd number of opening curly braces is an error.}} 57 | echo {{{{{Odd number of opening curly braces is an error.}} 58 | echo {{{{{{{Odd number of opening curly braces is an error.}} 59 | echo {{{{{{{{{Odd number of opening curly braces is an error.}} 60 | echo '{{{Odd number of opening curly braces is an error.}}' 61 | echo '{{{{{Odd number of opening curly braces is an error.}}' 62 | echo '{{{{{{{Odd number of opening curly braces is an error.}}' 63 | echo '{{{{{{{{{Odd number of opening curly braces is an error.}}' 64 | echo {{{{{{{Odd number of opening curly braces is an error.}}}}} 65 | echo "{{{{{{{{{Odd number of opening curly braces is an error.}}}}}" 66 | 67 | early-interpolation-close foo: 68 | echo {{ if foo == "bar" { "hello" } else { "goodbye" }}} 69 | other-recipe: 70 | echo interpolation ended in last recipe 71 | 72 | variadic-env-wrong-order1 param0 $*PARAM: 73 | env 74 | variadic-env-wrong-order2 $+PARAM="1": 75 | env 76 | variadic-env-wrong-order-with-whitespace $ * PARAM: 77 | env 78 | 79 | non-default-param-after-default param0='value' param1: 80 | /bin/false 81 | non-default-param-after-default2 param0='value' + $ PARAM: 82 | env 83 | non-default-param-after-default3 param0='value' param1 param2 param3 param4=something param5: 84 | /bin/false 85 | non-default-param-after-default4 param0=''' 86 | value 87 | ''' param1 + $ param2: 88 | env 89 | 90 | [confirm()] 91 | argless-attribute1: 92 | /bin/false 93 | 94 | [confirm( )] 95 | argless-attribute2: 96 | /bin/false 97 | 98 | [confirm \ 99 | ( 100 | 101 | \ 102 | ) ] 103 | argless-attribute3: 104 | /bin/false 105 | 106 | invalid_multi_line_interp: 107 | echo {{"foo" + \ 108 | bar }} 109 | echo this line is not part of the recipe body 110 | 111 | [group: group] 112 | invalid-shorthand-attr-value: 113 | 114 | [group: x] 115 | invalid-shorthand-attr-value2: 116 | 117 | [group::-] 118 | invalid-shorthand-attr-value3: 119 | 120 | [group: , unix] 121 | comma-is-not-invalid-attr-value: 122 | 123 | [script: 'bash', "-x", '-c'] 124 | attr-shorthand-takes-only-one-argument: 125 | 126 | 127 | recipe-without-body: 128 | # comment 129 | echo this line is not recipe body 130 | 131 | foo::bar: 132 | foo::bar : 133 | echo recipe names are not module namepaths 134 | 135 | foo bar::baz: 136 | foo bar::baz : 137 | echo recipe parameters are not module namepaths 138 | -------------------------------------------------------------------------------- /tests/cases/invalid.just: -------------------------------------------------------------------------------- 1 | !include not-at-beginning-of-line.just 2 | 3 | mod -invalid-mod 4 | 5 | set shell 6 | 7 | bash := 'bash' 8 | c := '-c' 9 | set shell := [bash, c] 10 | 11 | invalid_escapes := "\q \u{} \u{foo}\u{1234123} \u1234" 12 | invalid_escapes_shell_expanded := x"${foo:-z\u{zzz}z}" 13 | 14 | # export is only valid when prefixing an assignment 15 | # just thinks the following line is a malformed attempt to start a recipe called "export" 16 | export FOO 17 | 18 | (the next line is not a recipe body)... 19 | this text should not be highlighted 20 | 21 | badfunc5 := num_cpus(\) 22 | 23 | not_a_recipe_name #comment: 24 | not a recipe body 25 | 26 | nonexistant_func := blahuuid() 27 | nonexistant_func2 := blah_replace_regex('foo', '([^o])', '$1') 28 | 29 | invalid_recipe_body: 30 | echo 'My indentation mixes spaces and tabs!' 31 | echo 'My indentation mixes tabs and spaces!' 32 | echo 'Mixing spaces and tabs again.' 33 | 34 | invalid_recipe_body2: 35 | echo This line is valid 36 | echo but this one is not 37 | invalid_recipe_body3: 38 | echo "valid again 39 | not valid" 40 | invalid_recipe_body4: 41 | echo 'valid again 42 | not valid' 43 | 44 | not_a_recipe_start 45 | not a recipe body 46 | bad_but_not_mixed_indentation: 47 | echo 'tab indent' 48 | echo 'space indent' 49 | bad_but_not_mixed_indentation_shebang: 50 | #!/bin/bash 51 | echo 'space indent, same as first line' 52 | echo 'acceptable space indent' 53 | echo 'tab indent' 54 | 55 | bad_curly_braces: 56 | echo {{{Odd number of opening curly braces is an error.}} 57 | echo {{{{{Odd number of opening curly braces is an error.}} 58 | echo {{{{{{{Odd number of opening curly braces is an error.}} 59 | echo {{{{{{{{{Odd number of opening curly braces is an error.}} 60 | echo '{{{Odd number of opening curly braces is an error.}}' 61 | echo '{{{{{Odd number of opening curly braces is an error.}}' 62 | echo '{{{{{{{Odd number of opening curly braces is an error.}}' 63 | echo '{{{{{{{{{Odd number of opening curly braces is an error.}}' 64 | echo {{{{{{{Odd number of opening curly braces is an error.}}}}} 65 | echo "{{{{{{{{{Odd number of opening curly braces is an error.}}}}}" 66 | 67 | early-interpolation-close foo: 68 | echo {{ if foo == "bar" { "hello" } else { "goodbye" }}} 69 | other-recipe: 70 | echo interpolation ended in last recipe 71 | 72 | variadic-env-wrong-order1 param0 $*PARAM: 73 | env 74 | variadic-env-wrong-order2 $+PARAM="1": 75 | env 76 | variadic-env-wrong-order-with-whitespace $ * PARAM: 77 | env 78 | 79 | non-default-param-after-default param0='value' param1: 80 | /bin/false 81 | non-default-param-after-default2 param0='value' + $ PARAM: 82 | env 83 | non-default-param-after-default3 param0='value' param1 param2 param3 param4=something param5: 84 | /bin/false 85 | non-default-param-after-default4 param0=''' 86 | value 87 | ''' param1 + $ param2: 88 | env 89 | 90 | [confirm()] 91 | argless-attribute1: 92 | /bin/false 93 | 94 | [confirm( )] 95 | argless-attribute2: 96 | /bin/false 97 | 98 | [confirm \ 99 | ( 100 | 101 | \ 102 | ) ] 103 | argless-attribute3: 104 | /bin/false 105 | 106 | invalid_multi_line_interp: 107 | echo {{"foo" + \ 108 | bar }} 109 | echo this line is not part of the recipe body 110 | 111 | [group: group] 112 | invalid-shorthand-attr-value: 113 | 114 | [group: x] 115 | invalid-shorthand-attr-value2: 116 | 117 | [group::-] 118 | invalid-shorthand-attr-value3: 119 | 120 | [group: , unix] 121 | comma-is-not-invalid-attr-value: 122 | 123 | [script: 'bash', "-x", '-c'] 124 | attr-shorthand-takes-only-one-argument: 125 | 126 | 127 | recipe-without-body: 128 | # comment 129 | echo this line is not recipe body 130 | 131 | foo::bar: 132 | foo::bar : 133 | echo recipe names are not module namepaths 134 | 135 | foo bar::baz: 136 | foo bar::baz : 137 | echo recipe parameters are not module namepaths 138 | -------------------------------------------------------------------------------- /tests/cases/kitchen-sink.html: -------------------------------------------------------------------------------- 1 | # From https://github.com/casey/just/blob/9f03441eef28fd662b33a8f1961e2ee97b60f7ff/examples/kitchen-sink.just 2 | # Original Author: Nick Kocharhook (https://github.com/nk9) 3 | 4 | set shell := ["sh", "-c"] 5 | set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] 6 | set allow-duplicate-recipes 7 | set positional-arguments 8 | set dotenv-load 9 | set export 10 | 11 | alias s := serve 12 | 13 | bt := '0' 14 | 15 | export RUST_BACKTRACE_1 := bt 16 | 17 | log := "warn" 18 | 19 | export JUST_LOG := (log + "ing" + `grep loop /etc/networks | cut -f2`) 20 | 21 | tmpdir := `mktemp` 22 | version := "0.2.7" 23 | tardir := tmpdir / "awesomesauce-" + version 24 | foo1 := / "tmp" 25 | foo2_3 := "a/" 26 | tarball := tardir + ".tar.gz" 27 | 28 | export RUST_BACKTRACE_2 := "1" 29 | string-with-tab := "\t" 30 | string-with-newline := "\n" 31 | string-with-carriage-return := "\r" 32 | string-with-double-quote := "\"" 33 | string-with-slash := "\\" 34 | string-with-no-newline := "\ 35 | " 36 | 37 | # Newlines in variables 38 | single := ' 39 | hello 40 | ' 41 | 42 | double := " 43 | goodbye 44 | " 45 | escapes := '\t\n\r\"\\' 46 | 47 | # this string will evaluate to `foo\nbar\n` 48 | x := ''' 49 | foo 50 | bar 51 | ''' 52 | 53 | # this string will evaluate to `abc\n wuv\nbar\n` 54 | y := """ 55 | abc 56 | wuv 57 | xyz 58 | """ 59 | 60 | for: 61 | for file in `ls .`; do \ 62 | echo $file; \ 63 | done 64 | 65 | serve: 66 | touch {{tmpdir}}/file 67 | 68 | # This backtick evaluates the command `echo foo\necho bar\n`, which produces the value `foo\nbar\n`. 69 | stuff := ``` 70 | echo foo 71 | echo bar 72 | ``` 73 | 74 | 75 | an_arch := trim(lowercase(justfile())) + arch() 76 | trim_end := trim_end("99.99954% ") 77 | home_dir := replace(env_var('HOME') / "yep", 'yep', '') 78 | quoted := quote("some things beyond\"$()^%#@!|-+=_*&'`") 79 | smartphone := trim_end_match('blah.txt', 'txt') 80 | museum := trim_start_match(trim_start(trim_end_matches(' yep_blah.txt.txt', '.txt')), 'yep_') 81 | water := trim_start_matches('ssssssoup.txt', 's') 82 | congress := uppercase(os()) 83 | fam := os_family() 84 | path_1 := absolute_path('test') 85 | path_2 := '/tmp/subcommittee.txt' 86 | ext_z := extension(path_2) 87 | exe_name := file_name(just_executable()) 88 | a_stem := file_stem(path_2) 89 | a_parent := parent_directory(path_2) 90 | sans_ext := without_extension(path_2) 91 | camera := join('tmp', 'dir1', 'dir2', path_2) 92 | cleaned := clean('/tmp/blah/..///thing.txt') 93 | id__path := '/tmp' / sha256('blah') / sha256_file(justfile()) 94 | _another_var := env_var_or_default("HOME", justfile_directory()) 95 | python := `which python` 96 | 97 | exists := if path_exists(just_executable()) =~ '^/User' { uuid() } else { 'yeah' } 98 | 99 | foo := if env_var("_") == "/usr/bin/env" { `touch /tmp/a_file` } else { "dummy-value" } 100 | foo_b := if "hello" == "goodbye" { "xyz" } else { if "no" == "no" { "yep"} else { error("123") } } 101 | foo_c := if "hello" == "goodbye" { 102 | "xyz" 103 | } else if "a" == "a" { 104 | "abc" 105 | } else { 106 | "123" 107 | } 108 | 109 | bar: 110 | @echo {{foo}} 111 | 112 | 113 | bar2 foo_stuff: 114 | echo {{ if foo_stuff == "bar" { "hello" } else { "goodbye" } }} 115 | 116 | executable: 117 | @echo The executable is at: {{just_executable()}} 118 | 119 | 120 | rustfmt: 121 | find {{invocation_directory()}} -name \*.rs -exec rustfmt {} \; 122 | 123 | test: 124 | echo "{{home_dir}}" 125 | 126 | 127 | linewise: 128 | Write-Host "Hello, world!" 129 | 130 | serve2: 131 | @echo "Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…" 132 | 133 | 134 | shebang := if os() == 'windows' { 135 | 'powershell.exe' 136 | } else { 137 | '/usr/bin/env pwsh' 138 | } 139 | 140 | shebang: 141 | #!{{shebang}} 142 | $PSV = $PSVersionTable.PSVersion | % {"$_" -split "\." } 143 | $psver = $PSV[0] + "." + $PSV[1] 144 | if ($PSV[2].Length -lt 4) { 145 | $psver += "." + $PSV[2] + " Core" 146 | } else { 147 | $psver += " Desktop" 148 | } 149 | echo "PowerShell $psver" 150 | 151 | @foo: 152 | echo bar 153 | 154 | @test5 *args='': 155 | bash -c 'while (( "$#" )); do echo - $1; shift; done' -- "$@" 156 | 157 | test2 $RUST_BACKTRACE="1": 158 | # will print a stack trace if it crashes 159 | cargo test 160 | 161 | 162 | notify m="": 163 | keybase chat send --topic-type "chat" --channel <channel> <team> "upd(<repo>): {{m}}" 164 | 165 | # Sample project script 2 166 | script2 *ARGS: 167 | {{ python }} script2.py {{ ARGS }} 168 | 169 | braces: 170 | echo 'I {{{{LOVE}} curly braces!' 171 | 172 | _braces2: 173 | echo '{{'I {{LOVE}} curly braces!'}}' 174 | 175 | _braces3: 176 | echo 'I {{ "{{" }}LOVE}} curly braces!' 177 | 178 | foo2: 179 | -@cat foo 180 | echo 'Done!' 181 | 182 | test3 target tests=path_1: 183 | @echo 'Testing {{target}}:{{tests}}…' 184 | ./test --tests {{tests}} {{target}} 185 | 186 | test4 triple=(an_arch + "-unknown-unknown") input=(an_arch / "input.dat"): 187 | ./test {{triple}} 188 | 189 | variadic $VAR1_1 VAR2 VAR3 VAR4=("a") +$FLAGS='-q': foo2 braces 190 | cargo test {{FLAGS}} 191 | 192 | time: 193 | @-date +"%H:%S" 194 | -cat /tmp/nonexistent_file.txt 195 | @echo "finished" 196 | 197 | justwords: 198 | grep just \ 199 | --text /usr/share/dict/words \ 200 | > /tmp/justwords 201 | 202 | # Subsequent dependencies 203 | # https://just.systems/man/en/chapter_37.html 204 | # To test, run `$ just -f test-suite.just b` 205 | a: 206 | echo 'A!' 207 | 208 | b: a && d 209 | echo 'B start!' 210 | just -f {{justfile()}} c 211 | echo 'B end!' 212 | 213 | c: 214 | echo 'C!' 215 | 216 | d: 217 | echo 'D!' 218 | -------------------------------------------------------------------------------- /tests/cases/kitchen-sink.just: -------------------------------------------------------------------------------- 1 | # From https://github.com/casey/just/blob/9f03441eef28fd662b33a8f1961e2ee97b60f7ff/examples/kitchen-sink.just 2 | # Original Author: Nick Kocharhook (https://github.com/nk9) 3 | 4 | set shell := ["sh", "-c"] 5 | set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] 6 | set allow-duplicate-recipes 7 | set positional-arguments 8 | set dotenv-load 9 | set export 10 | 11 | alias s := serve 12 | 13 | bt := '0' 14 | 15 | export RUST_BACKTRACE_1 := bt 16 | 17 | log := "warn" 18 | 19 | export JUST_LOG := (log + "ing" + `grep loop /etc/networks | cut -f2`) 20 | 21 | tmpdir := `mktemp` 22 | version := "0.2.7" 23 | tardir := tmpdir / "awesomesauce-" + version 24 | foo1 := / "tmp" 25 | foo2_3 := "a/" 26 | tarball := tardir + ".tar.gz" 27 | 28 | export RUST_BACKTRACE_2 := "1" 29 | string-with-tab := "\t" 30 | string-with-newline := "\n" 31 | string-with-carriage-return := "\r" 32 | string-with-double-quote := "\"" 33 | string-with-slash := "\\" 34 | string-with-no-newline := "\ 35 | " 36 | 37 | # Newlines in variables 38 | single := ' 39 | hello 40 | ' 41 | 42 | double := " 43 | goodbye 44 | " 45 | escapes := '\t\n\r\"\\' 46 | 47 | # this string will evaluate to `foo\nbar\n` 48 | x := ''' 49 | foo 50 | bar 51 | ''' 52 | 53 | # this string will evaluate to `abc\n wuv\nbar\n` 54 | y := """ 55 | abc 56 | wuv 57 | xyz 58 | """ 59 | 60 | for: 61 | for file in `ls .`; do \ 62 | echo $file; \ 63 | done 64 | 65 | serve: 66 | touch {{tmpdir}}/file 67 | 68 | # This backtick evaluates the command `echo foo\necho bar\n`, which produces the value `foo\nbar\n`. 69 | stuff := ``` 70 | echo foo 71 | echo bar 72 | ``` 73 | 74 | 75 | an_arch := trim(lowercase(justfile())) + arch() 76 | trim_end := trim_end("99.99954% ") 77 | home_dir := replace(env_var('HOME') / "yep", 'yep', '') 78 | quoted := quote("some things beyond\"$()^%#@!|-+=_*&'`") 79 | smartphone := trim_end_match('blah.txt', 'txt') 80 | museum := trim_start_match(trim_start(trim_end_matches(' yep_blah.txt.txt', '.txt')), 'yep_') 81 | water := trim_start_matches('ssssssoup.txt', 's') 82 | congress := uppercase(os()) 83 | fam := os_family() 84 | path_1 := absolute_path('test') 85 | path_2 := '/tmp/subcommittee.txt' 86 | ext_z := extension(path_2) 87 | exe_name := file_name(just_executable()) 88 | a_stem := file_stem(path_2) 89 | a_parent := parent_directory(path_2) 90 | sans_ext := without_extension(path_2) 91 | camera := join('tmp', 'dir1', 'dir2', path_2) 92 | cleaned := clean('/tmp/blah/..///thing.txt') 93 | id__path := '/tmp' / sha256('blah') / sha256_file(justfile()) 94 | _another_var := env_var_or_default("HOME", justfile_directory()) 95 | python := `which python` 96 | 97 | exists := if path_exists(just_executable()) =~ '^/User' { uuid() } else { 'yeah' } 98 | 99 | foo := if env_var("_") == "/usr/bin/env" { `touch /tmp/a_file` } else { "dummy-value" } 100 | foo_b := if "hello" == "goodbye" { "xyz" } else { if "no" == "no" { "yep"} else { error("123") } } 101 | foo_c := if "hello" == "goodbye" { 102 | "xyz" 103 | } else if "a" == "a" { 104 | "abc" 105 | } else { 106 | "123" 107 | } 108 | 109 | bar: 110 | @echo {{foo}} 111 | 112 | 113 | bar2 foo_stuff: 114 | echo {{ if foo_stuff == "bar" { "hello" } else { "goodbye" } }} 115 | 116 | executable: 117 | @echo The executable is at: {{just_executable()}} 118 | 119 | 120 | rustfmt: 121 | find {{invocation_directory()}} -name \*.rs -exec rustfmt {} \; 122 | 123 | test: 124 | echo "{{home_dir}}" 125 | 126 | 127 | linewise: 128 | Write-Host "Hello, world!" 129 | 130 | serve2: 131 | @echo "Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…" 132 | 133 | 134 | shebang := if os() == 'windows' { 135 | 'powershell.exe' 136 | } else { 137 | '/usr/bin/env pwsh' 138 | } 139 | 140 | shebang: 141 | #!{{shebang}} 142 | $PSV = $PSVersionTable.PSVersion | % {"$_" -split "\." } 143 | $psver = $PSV[0] + "." + $PSV[1] 144 | if ($PSV[2].Length -lt 4) { 145 | $psver += "." + $PSV[2] + " Core" 146 | } else { 147 | $psver += " Desktop" 148 | } 149 | echo "PowerShell $psver" 150 | 151 | @foo: 152 | echo bar 153 | 154 | @test5 *args='': 155 | bash -c 'while (( "$#" )); do echo - $1; shift; done' -- "$@" 156 | 157 | test2 $RUST_BACKTRACE="1": 158 | # will print a stack trace if it crashes 159 | cargo test 160 | 161 | 162 | notify m="": 163 | keybase chat send --topic-type "chat" --channel "upd(): {{m}}" 164 | 165 | # Sample project script 2 166 | script2 *ARGS: 167 | {{ python }} script2.py {{ ARGS }} 168 | 169 | braces: 170 | echo 'I {{{{LOVE}} curly braces!' 171 | 172 | _braces2: 173 | echo '{{'I {{LOVE}} curly braces!'}}' 174 | 175 | _braces3: 176 | echo 'I {{ "{{" }}LOVE}} curly braces!' 177 | 178 | foo2: 179 | -@cat foo 180 | echo 'Done!' 181 | 182 | test3 target tests=path_1: 183 | @echo 'Testing {{target}}:{{tests}}…' 184 | ./test --tests {{tests}} {{target}} 185 | 186 | test4 triple=(an_arch + "-unknown-unknown") input=(an_arch / "input.dat"): 187 | ./test {{triple}} 188 | 189 | variadic $VAR1_1 VAR2 VAR3 VAR4=("a") +$FLAGS='-q': foo2 braces 190 | cargo test {{FLAGS}} 191 | 192 | time: 193 | @-date +"%H:%S" 194 | -cat /tmp/nonexistent_file.txt 195 | @echo "finished" 196 | 197 | justwords: 198 | grep just \ 199 | --text /usr/share/dict/words \ 200 | > /tmp/justwords 201 | 202 | # Subsequent dependencies 203 | # https://just.systems/man/en/chapter_37.html 204 | # To test, run `$ just -f test-suite.just b` 205 | a: 206 | echo 'A!' 207 | 208 | b: a && d 209 | echo 'B start!' 210 | just -f {{justfile()}} c 211 | echo 'B end!' 212 | 213 | c: 214 | echo 'C!' 215 | 216 | d: 217 | echo 'D!' 218 | -------------------------------------------------------------------------------- /tests/cases/line-continuations.html: -------------------------------------------------------------------------------- 1 | set \ 2 | ignore-comments := true 3 | 4 | set \ 5 | fallback \ 6 | := \ 7 | false 8 | 9 | 10 | var \ 11 | :=\ 12 | '1' + '2' \ 13 | \ 14 | + '3' /\ 15 | '456' + \ 16 | if \ 17 | 'a' \ 18 | != \ 19 | 'b' \ 20 | {\ 21 | '\789'} \ 22 | else\ 23 | { '\543'\ 24 | \ 25 | } 26 | 27 | 28 | f1 := \ 29 | uuid\ 30 | \ 31 | \ 32 | (\ 33 | 34 | ) 35 | 36 | f2 := env (\ 37 | 38 | 'FOO_VAR', \ 39 | 40 | '0'\ 41 | 42 | ) 43 | 44 | f3 := replace_regex \ 45 | ( \ 46 | "FOOoo", 47 | "([A-Z])[A-Z]+([^A-Z])", \ 48 | "$1${2}" 49 | \ 50 | ) 51 | 52 | export \ 53 | FOO_ENV \ 54 | := \ 55 | "FOO" 56 | 57 | test: a \ 58 | b 59 | echo '123' 60 | 61 | a: 62 | echo 'A' 63 | b: 64 | echo 'B' 65 | c: 66 | echo 'C' 67 | # ignored line 68 | 69 | test2 \ 70 | param1="foo" param2 \ 71 | = ''' 72 | bar'''\ 73 | param3 = \ 74 | "b\ 75 | a\ 76 | z" \ 77 | \ 78 | * \ 79 | $ \ 80 | param4 \ 81 | = \ 82 | f1 : 83 | echo {{param1}} {{param2}} {{param3}} {{param4}} 84 | 85 | test3: \ 86 | a b \ 87 | c #comment \ 88 | echo d 89 | echo 'Test 3' 90 | 91 | test4 \ 92 | +$PARAM='1': test \ 93 | ( \ 94 | test2 \ 95 | 'bar' 96 | 97 | ) 98 | env | grep -F PARAM 99 | 100 | test5: a \ 101 | \ 102 | \ 103 | \ 104 | b 105 | echo 'Test5' 106 | 107 | test6: a\ 108 | b 109 | echo 'Test6' 110 | 111 | # the trailing space after the closing ] is intentional 112 | [\ 113 | no-cd \ 114 | \ 115 | , unix \ 116 | ] 117 | test7: 118 | pwd 119 | 120 | alias\ 121 | runpwd\ 122 | :=\ 123 | test7 124 | foo \ 125 | := 'Foo!' 126 | 127 | test8 param1 \ 128 | = \ 129 | ( f1 + 'X' + f2) \ 130 | : 131 | echo {{ \ 132 | param1 \ 133 | }} 134 | 135 | test9: 136 | echo {{\ 137 | num_cpus(\ 138 | ) \ 139 | }} 140 | 141 | test10 param1=\ 142 | num_cpus() \ 143 | \ 144 | param2\ 145 | =\ 146 | replace_regex(uuid(), 147 | \ 148 | '^(.+)$', "\"\\\"$1\\\"\"") param3= \ 149 | env\ 150 | ('BAR', 151 | 152 | 153 | 154 | \ 155 | 156 | replace('fghi', 'ghi', \ 157 | 'foo') 158 | 159 | ): \ 160 | # comment \ 161 | echo {{param1\ 162 | / param2 }} 163 | echo . 164 | 165 | test11: a \ 166 | # comment\ 167 | echo Test 11 168 | 169 | test12: \ 170 | 171 | echo Test 12 172 | 173 | test13: a\ 174 | 175 | echo Test13 176 | 177 | continued_interpolation: 178 | echo {{"foo " + \ 179 | f2 \ 180 | }} 181 | -------------------------------------------------------------------------------- /tests/cases/line-continuations.just: -------------------------------------------------------------------------------- 1 | set \ 2 | ignore-comments := true 3 | 4 | set \ 5 | fallback \ 6 | := \ 7 | false 8 | 9 | 10 | var \ 11 | :=\ 12 | '1' + '2' \ 13 | \ 14 | + '3' /\ 15 | '456' + \ 16 | if \ 17 | 'a' \ 18 | != \ 19 | 'b' \ 20 | {\ 21 | '\789'} \ 22 | else\ 23 | { '\543'\ 24 | \ 25 | } 26 | 27 | 28 | f1 := \ 29 | uuid\ 30 | \ 31 | \ 32 | (\ 33 | 34 | ) 35 | 36 | f2 := env (\ 37 | 38 | 'FOO_VAR', \ 39 | 40 | '0'\ 41 | 42 | ) 43 | 44 | f3 := replace_regex \ 45 | ( \ 46 | "FOOoo", 47 | "([A-Z])[A-Z]+([^A-Z])", \ 48 | "$1${2}" 49 | \ 50 | ) 51 | 52 | export \ 53 | FOO_ENV \ 54 | := \ 55 | "FOO" 56 | 57 | test: a \ 58 | b 59 | echo '123' 60 | 61 | a: 62 | echo 'A' 63 | b: 64 | echo 'B' 65 | c: 66 | echo 'C' 67 | # ignored line 68 | 69 | test2 \ 70 | param1="foo" param2 \ 71 | = ''' 72 | bar'''\ 73 | param3 = \ 74 | "b\ 75 | a\ 76 | z" \ 77 | \ 78 | * \ 79 | $ \ 80 | param4 \ 81 | = \ 82 | f1 : 83 | echo {{param1}} {{param2}} {{param3}} {{param4}} 84 | 85 | test3: \ 86 | a b \ 87 | c #comment \ 88 | echo d 89 | echo 'Test 3' 90 | 91 | test4 \ 92 | +$PARAM='1': test \ 93 | ( \ 94 | test2 \ 95 | 'bar' 96 | 97 | ) 98 | env | grep -F PARAM 99 | 100 | test5: a \ 101 | \ 102 | \ 103 | \ 104 | b 105 | echo 'Test5' 106 | 107 | test6: a\ 108 | b 109 | echo 'Test6' 110 | 111 | # the trailing space after the closing ] is intentional 112 | [\ 113 | no-cd \ 114 | \ 115 | , unix \ 116 | ] 117 | test7: 118 | pwd 119 | 120 | alias\ 121 | runpwd\ 122 | :=\ 123 | test7 124 | foo \ 125 | := 'Foo!' 126 | 127 | test8 param1 \ 128 | = \ 129 | ( f1 + 'X' + f2) \ 130 | : 131 | echo {{ \ 132 | param1 \ 133 | }} 134 | 135 | test9: 136 | echo {{\ 137 | num_cpus(\ 138 | ) \ 139 | }} 140 | 141 | test10 param1=\ 142 | num_cpus() \ 143 | \ 144 | param2\ 145 | =\ 146 | replace_regex(uuid(), 147 | \ 148 | '^(.+)$', "\"\\\"$1\\\"\"") param3= \ 149 | env\ 150 | ('BAR', 151 | 152 | 153 | 154 | \ 155 | 156 | replace('fghi', 'ghi', \ 157 | 'foo') 158 | 159 | ): \ 160 | # comment \ 161 | echo {{param1\ 162 | / param2 }} 163 | echo . 164 | 165 | test11: a \ 166 | # comment\ 167 | echo Test 11 168 | 169 | test12: \ 170 | 171 | echo Test 12 172 | 173 | test13: a\ 174 | 175 | echo Test13 176 | 177 | continued_interpolation: 178 | echo {{"foo " + \ 179 | f2 \ 180 | }} 181 | -------------------------------------------------------------------------------- /tests/cases/mod.html: -------------------------------------------------------------------------------- 1 | mod foo 2 | 3 | # the trailing whitespace at the end of the following line is intentional 4 | mod foo 5 | 6 | mod foo "foo/justfile" 7 | 8 | mod _foo-Foo \ 9 | 'foo/foo.just' 10 | 11 | mod? foo 12 | 13 | mod ? foo 'foo/justfile' 14 | 15 | mod foo x'$FOO/justfile' 16 | mod? foo x"$FOO/justfile" 17 | 18 | mod foo # comment 19 | mod foo#comment 20 | mod foo \ 21 | # comment 22 | 23 | mod foo"foo.just" 24 | 25 | # not a shell-expanded string 26 | mod? x"$FOO.just" 27 | 28 | [doc("documented module")] 29 | mod foo 30 | 31 | alias foo := foo::foo 32 | alias foo := foo\ 33 | ::\ 34 | foo 35 | alias foo := foo :: foo 36 | alias foo := foo::foo::foo 37 | 38 | foo: foo::foo foo \ 39 | :: \ 40 | foo (foo::foo::\ 41 | foo foo foo) (foo \ 42 | :: \ 43 | foo foo) 44 | echo foo 45 | -------------------------------------------------------------------------------- /tests/cases/mod.just: -------------------------------------------------------------------------------- 1 | mod foo 2 | 3 | # the trailing whitespace at the end of the following line is intentional 4 | mod foo 5 | 6 | mod foo "foo/justfile" 7 | 8 | mod _foo-Foo \ 9 | 'foo/foo.just' 10 | 11 | mod? foo 12 | 13 | mod ? foo 'foo/justfile' 14 | 15 | mod foo x'$FOO/justfile' 16 | mod? foo x"$FOO/justfile" 17 | 18 | mod foo # comment 19 | mod foo#comment 20 | mod foo \ 21 | # comment 22 | 23 | mod foo"foo.just" 24 | 25 | # not a shell-expanded string 26 | mod? x"$FOO.just" 27 | 28 | [doc("documented module")] 29 | mod foo 30 | 31 | alias foo := foo::foo 32 | alias foo := foo\ 33 | ::\ 34 | foo 35 | alias foo := foo :: foo 36 | alias foo := foo::foo::foo 37 | 38 | foo: foo::foo foo \ 39 | :: \ 40 | foo (foo::foo::\ 41 | foo foo foo) (foo \ 42 | :: \ 43 | foo foo) 44 | echo foo 45 | -------------------------------------------------------------------------------- /tests/cases/recipes-simple.html: -------------------------------------------------------------------------------- 1 | # This file intentionally does not have newline at end of file 2 | 3 | empty: 4 | 5 | tab: 6 | echo 7 | space-indent: 8 | echo foo foo-bar bar 9 | 10 | alias foo := tab 11 | 12 | alias: 13 | echo alias 14 | 15 | simple_dep: dependency5 16 | 17 | simple_multiline: 18 | echo 123 19 | cat comment.just 20 | 21 | simple_multidep: dependency dependency2 dependency3 22 | /bin/true 23 | 24 | 25 | dependency: 26 | echo Dependency1 27 | dependency2: 28 | 29 | 30 | dependency3: 31 | dependency4: 32 | dependency5: 33 | echo Dependency5 line1 34 | echo Dependency5 line2 -------------------------------------------------------------------------------- /tests/cases/recipes-simple.just: -------------------------------------------------------------------------------- 1 | # This file intentionally does not have newline at end of file 2 | 3 | empty: 4 | 5 | tab: 6 | echo 7 | space-indent: 8 | echo foo foo-bar bar 9 | 10 | alias foo := tab 11 | 12 | alias: 13 | echo alias 14 | 15 | simple_dep: dependency5 16 | 17 | simple_multiline: 18 | echo 123 19 | cat comment.just 20 | 21 | simple_multidep: dependency dependency2 dependency3 22 | /bin/true 23 | 24 | 25 | dependency: 26 | echo Dependency1 27 | dependency2: 28 | 29 | 30 | dependency3: 31 | dependency4: 32 | dependency5: 33 | echo Dependency5 line1 34 | echo Dependency5 line2 -------------------------------------------------------------------------------- /tests/cases/recipes-with-extras.html: -------------------------------------------------------------------------------- 1 | foo: # comment 2 | uname -a 3 | 4 | p1 := 'test' 5 | p2 := canonicalize('..') 6 | pdirname := replace_regex(p2, '/([^/]*)$', '$1') 7 | param := semver_matches('0.1.1', '>=0.1.0') 8 | 9 | @foo1 param1 param2="default test" +varparam='default': dependency1 (dependency2 p1 p2) (dependency3 param) 10 | echo many lines 11 | echo to follow 12 | echo {{quote(param1)}} {{quote(param2)}} {{varparam}} 13 | 14 | dependency1: 15 | echo bar # comment 16 | 17 | dependency2 firstparam=("abc\t" / 'def\t\"') secondparam=(""" 18 | ghi 19 | jkl\tmno 20 | """ + `echo pqr ' '` 21 | + uuid()): 22 | /bin/echo {{quote(firstparam)}} 23 | /bin/echo {{quote(secondparam)}} 24 | 25 | dependency3 theparam=('a' + (if env('FOO', blake3_file(justfile())) == '1' { ' With Foo'} else { ' none ' + p1 / p2 }) / 'b'): 26 | echo {{quote(theparam)}} 27 | 28 | dependency4: 29 | echo 'Is Dependency:' {{is_dependency()}} 30 | 31 | test_dependency4: dependency4 32 | 33 | test4: (dependency2 "123" 34 | + '456') 35 | echo Test 36 | 37 | test5: (dependency2 "123" 38 | + '456') # comment 39 | echo Test 40 | 41 | [no-cd] 42 | runpwd: 43 | pwd 44 | 45 | [private] 46 | [confirm] 47 | [no-exit-message] 48 | error: 49 | sh -c 'echo Exit 3;exit 3' 50 | 51 | [ unix ] 52 | [ macos,windows, linux ] 53 | any: 54 | echo foo 55 | 56 | [ private ,no-cd , unix] 57 | [no-exit-message ] 58 | runpwd_then_error: 59 | pwd 60 | sh -c 'exit 2' 61 | echo unreachable 62 | 63 | [ 64 | private 65 | , 66 | 67 | confirm 68 | ] 69 | multiline-attr-no-continuations: 70 | /bin/true 71 | 72 | 73 | [confirm("confirm2: Are you sure?")] 74 | confirm2: 75 | python3 -c pass 76 | 77 | [confirm ( 78 | ''' 79 | confirm3: Are you sure?''' \ 80 | ) 81 | ] 82 | confirm3: 83 | python3 -c pass 84 | 85 | [confirm \ 86 | ( \ 87 | " 88 | confirm4: Are you sure? [y/N] 89 | >" 90 | )] 91 | confirm4: 92 | python3 -c pass 93 | 94 | 95 | somevar := "1" 96 | interp_with_func_with_arg foo: 97 | echo {{quote(foo)}} 98 | echo {{join('test',foo, `echo Bar`, somevar, "2")}} 99 | echo {{quote(blake3(foo+"-"))}} 100 | echo {{quote('x' + blake3(foo+"-"+ foo+'-'+foo) + foo+ 'y')}} 101 | echo {{quote(if foo == '123' { '321' } else { 'foo bar' } )}} 102 | 103 | interp_with_conditional foo: 104 | echo {{if foo != 'foo' { quote(foo) } else { foo / foo } }} 105 | 106 | [no-quiet] 107 | [exit-message] 108 | loud: 109 | echo LOUD 110 | 111 | hex_digits_all: 112 | echo {{HEXUPPER + replace_regex(HEXLOWER, '\d+', "")}} 113 | 114 | [confirm(x"Run this recipe in $PWD ?")] 115 | confirm_shell_expanded: 116 | echo {{x"Running in $PWD"}} 117 | 118 | [group('foo group')] 119 | grouped1: 120 | echo 'grouped' 121 | 122 | # An undocumented recipe 123 | [doc] 124 | not_documented: 125 | echo foo 126 | 127 | [doc(''' 128 | A very documented recipe. 129 | This documentation comment spans multiple lines. 130 | ''')] 131 | long_doc: 132 | echo foo foo 133 | 134 | [positional-arguments] 135 | positional *args: 136 | echo "$@" 137 | 138 | [group: 'foo group'] 139 | attr-colon1: 140 | echo foo 141 | 142 | [group : \ 143 | \ 144 | "foo group" 145 | ] 146 | attr-colon2: 147 | echo foo 148 | 149 | [ 150 | doc: 151 | "Documentation", 152 | 153 | group:'foo group' ] 154 | attr-colon3: 155 | echo foo 156 | 157 | [doc: x"echo value of $$HOME: $HOME"] 158 | attr-colon-shell-expanded-string: 159 | echo "$HOME" 160 | 161 | [extension: '.py'] 162 | ext: 163 | #!/usr/bin/env python3 164 | import sys 165 | print(sys.argv) 166 | 167 | styles: 168 | #!/bin/bash 169 | echo "{{GREEN}}Green{{NORMAL}}" 170 | echo '{{BLUE}}{{BOLD}}Bold Blue{{NORMAL}}' 171 | echo {{UNDERLINE}}{{MAGENTA}}Magenta underlined text{{NORMAL}} 172 | echo "{{style('command')}}echo command{{NORMAL}}" 173 | echo {{quote(style('command') + 'echo command' + NORMAL)}} >&2 174 | 175 | [working-directory('../..')] 176 | attr-working-directory: 177 | pwd 178 | ls -lA 179 | 180 | variadic-star-after-default param1='foo' *ARGS: 181 | echo {{ARGS}} 182 | variadic-star-after-default2 param1='foo' * ARGS: 183 | echo {{ARGS}} 184 | 185 | [metadata: 'foo'] 186 | [metadata('foo bar')] 187 | [metadata("foo", "bar")] 188 | recipe-with-metadata: 189 | echo foo 190 | 191 | test-not-parallel: sleep2 sleep1 192 | [parallel] 193 | test-parallel: sleep2 sleep1 194 | 195 | sleep1: 196 | sleep 1 197 | echo 'sleep1 done' 198 | 199 | sleep2: 200 | sleep 2 201 | echo 'sleep2 done' 202 | 203 | [default] 204 | the-default-recipe: 205 | echo 'default recipe' 206 | -------------------------------------------------------------------------------- /tests/cases/recipes-with-extras.just: -------------------------------------------------------------------------------- 1 | foo: # comment 2 | uname -a 3 | 4 | p1 := 'test' 5 | p2 := canonicalize('..') 6 | pdirname := replace_regex(p2, '/([^/]*)$', '$1') 7 | param := semver_matches('0.1.1', '>=0.1.0') 8 | 9 | @foo1 param1 param2="default test" +varparam='default': dependency1 (dependency2 p1 p2) (dependency3 param) 10 | echo many lines 11 | echo to follow 12 | echo {{quote(param1)}} {{quote(param2)}} {{varparam}} 13 | 14 | dependency1: 15 | echo bar # comment 16 | 17 | dependency2 firstparam=("abc\t" / 'def\t\"') secondparam=(""" 18 | ghi 19 | jkl\tmno 20 | """ + `echo pqr ' '` 21 | + uuid()): 22 | /bin/echo {{quote(firstparam)}} 23 | /bin/echo {{quote(secondparam)}} 24 | 25 | dependency3 theparam=('a' + (if env('FOO', blake3_file(justfile())) == '1' { ' With Foo'} else { ' none ' + p1 / p2 }) / 'b'): 26 | echo {{quote(theparam)}} 27 | 28 | dependency4: 29 | echo 'Is Dependency:' {{is_dependency()}} 30 | 31 | test_dependency4: dependency4 32 | 33 | test4: (dependency2 "123" 34 | + '456') 35 | echo Test 36 | 37 | test5: (dependency2 "123" 38 | + '456') # comment 39 | echo Test 40 | 41 | [no-cd] 42 | runpwd: 43 | pwd 44 | 45 | [private] 46 | [confirm] 47 | [no-exit-message] 48 | error: 49 | sh -c 'echo Exit 3;exit 3' 50 | 51 | [ unix ] 52 | [ macos,windows, linux ] 53 | any: 54 | echo foo 55 | 56 | [ private ,no-cd , unix] 57 | [no-exit-message ] 58 | runpwd_then_error: 59 | pwd 60 | sh -c 'exit 2' 61 | echo unreachable 62 | 63 | [ 64 | private 65 | , 66 | 67 | confirm 68 | ] 69 | multiline-attr-no-continuations: 70 | /bin/true 71 | 72 | 73 | [confirm("confirm2: Are you sure?")] 74 | confirm2: 75 | python3 -c pass 76 | 77 | [confirm ( 78 | ''' 79 | confirm3: Are you sure?''' \ 80 | ) 81 | ] 82 | confirm3: 83 | python3 -c pass 84 | 85 | [confirm \ 86 | ( \ 87 | " 88 | confirm4: Are you sure? [y/N] 89 | >" 90 | )] 91 | confirm4: 92 | python3 -c pass 93 | 94 | 95 | somevar := "1" 96 | interp_with_func_with_arg foo: 97 | echo {{quote(foo)}} 98 | echo {{join('test',foo, `echo Bar`, somevar, "2")}} 99 | echo {{quote(blake3(foo+"-"))}} 100 | echo {{quote('x' + blake3(foo+"-"+ foo+'-'+foo) + foo+ 'y')}} 101 | echo {{quote(if foo == '123' { '321' } else { 'foo bar' } )}} 102 | 103 | interp_with_conditional foo: 104 | echo {{if foo != 'foo' { quote(foo) } else { foo / foo } }} 105 | 106 | [no-quiet] 107 | [exit-message] 108 | loud: 109 | echo LOUD 110 | 111 | hex_digits_all: 112 | echo {{HEXUPPER + replace_regex(HEXLOWER, '\d+', "")}} 113 | 114 | [confirm(x"Run this recipe in $PWD ?")] 115 | confirm_shell_expanded: 116 | echo {{x"Running in $PWD"}} 117 | 118 | [group('foo group')] 119 | grouped1: 120 | echo 'grouped' 121 | 122 | # An undocumented recipe 123 | [doc] 124 | not_documented: 125 | echo foo 126 | 127 | [doc(''' 128 | A very documented recipe. 129 | This documentation comment spans multiple lines. 130 | ''')] 131 | long_doc: 132 | echo foo foo 133 | 134 | [positional-arguments] 135 | positional *args: 136 | echo "$@" 137 | 138 | [group: 'foo group'] 139 | attr-colon1: 140 | echo foo 141 | 142 | [group : \ 143 | \ 144 | "foo group" 145 | ] 146 | attr-colon2: 147 | echo foo 148 | 149 | [ 150 | doc: 151 | "Documentation", 152 | 153 | group:'foo group' ] 154 | attr-colon3: 155 | echo foo 156 | 157 | [doc: x"echo value of $$HOME: $HOME"] 158 | attr-colon-shell-expanded-string: 159 | echo "$HOME" 160 | 161 | [extension: '.py'] 162 | ext: 163 | #!/usr/bin/env python3 164 | import sys 165 | print(sys.argv) 166 | 167 | styles: 168 | #!/bin/bash 169 | echo "{{GREEN}}Green{{NORMAL}}" 170 | echo '{{BLUE}}{{BOLD}}Bold Blue{{NORMAL}}' 171 | echo {{UNDERLINE}}{{MAGENTA}}Magenta underlined text{{NORMAL}} 172 | echo "{{style('command')}}echo command{{NORMAL}}" 173 | echo {{quote(style('command') + 'echo command' + NORMAL)}} >&2 174 | 175 | [working-directory('../..')] 176 | attr-working-directory: 177 | pwd 178 | ls -lA 179 | 180 | variadic-star-after-default param1='foo' *ARGS: 181 | echo {{ARGS}} 182 | variadic-star-after-default2 param1='foo' * ARGS: 183 | echo {{ARGS}} 184 | 185 | [metadata: 'foo'] 186 | [metadata('foo bar')] 187 | [metadata("foo", "bar")] 188 | recipe-with-metadata: 189 | echo foo 190 | 191 | test-not-parallel: sleep2 sleep1 192 | [parallel] 193 | test-parallel: sleep2 sleep1 194 | 195 | sleep1: 196 | sleep 1 197 | echo 'sleep1 done' 198 | 199 | sleep2: 200 | sleep 2 201 | echo 'sleep2 done' 202 | 203 | [default] 204 | the-default-recipe: 205 | echo 'default recipe' 206 | -------------------------------------------------------------------------------- /tests/cases/replace_regex-captures.html: -------------------------------------------------------------------------------- 1 | foo := replace_regex(justfile(), '[^A-Za-z]([A-Za-z]+)\.just$', ' - Test $1') 2 | 3 | foo2 := replace_regex(""" 4 | \tLorem ipsum dolor sit amet, consectetur adipiscing elit, sed 5 | do eiusmod tempor incididunt ut labore et dolore 6 | disputandum putant. Sed ut omittam pericula, labores, 7 | dolorem etiam, quem optimus quisque pro patria et pro suis 8 | suscipit, ut non plus habeat sapiens, quod gaudeat, quam 9 | quod angatur. Optime vero Epicurus, quod exiguam dixit 10 | fortunam intervenire sapienti maximasque ab eo dissentiunt, 11 | sed certe non. 12 | """, '\W(?P<Abc1_>\w{2})\W', " _$Abc1_-") 13 | 14 | foo3 := replace_regex(`echo ${HOME}`, '/(\w+)/(\w+)', '${2}at${1}') 15 | 16 | foo4 := replace_regex('$T$es$t', 17 | '\$([tT])' 18 | , 19 | '$1' 20 | ) 21 | 22 | foo5 := replace_regex("any\n$123", '\$123', """ 23 | \t$$abc$ 24 | 234\ 25 | 345 ${456}567 26 | """) 27 | 28 | foo6 := replace_regex("Some text.", '([^a-z])', '$$$1$$2') 29 | 30 | foo7 := replace_regex("Some text.", '([^a-z])', '$$1') 31 | 32 | foo8 := replace_regex("test123", '\w(\d+)', ''' 33 | \t'$1''') 34 | 35 | foo9 := replace_regex("trailing comma is accepted after the final argument,", 36 | "(\\W+)", \ 37 | "${1}_$1", 38 | ) 39 | 40 | foo10 := replace_regex("_abcdef", 41 | '(\w)[a-z]+', \ 42 | 43 | '${1}' 44 | , \ 45 | ) 46 | 47 | foo11 := replace_regex(HEX, '\d+', "$$$$1") 48 | 49 | not_a_replacement := 'test $1 test ' 50 | not_a_replacement_2 := "test $Abc_ test" 51 | 52 | not_a_regex := replace('Foo Bar', ' ', '$1') 53 | 54 | # special highlighting should only occur in the simplest case 55 | replacement_conditional := replace_regex('123456789', '([2468])', if "$1" == "$2" { '$1_' } else { '_${1}_' }) 56 | replacement_nested_func := replace_regex('987654321', '([13579])', trim_start(' $1 ')) 57 | replacement_split := replace_regex('012034', '(?P<capture>[1-9]+)', "!$cap" + 'ture' + '$captu' / "re") 58 | 59 | replacement_in_interp: 60 | echo '{{replace_regex('A b c d', '\s(\w)', '1$1')}}' 61 | echo {{replace_regex(replacement_conditional, '(\d+)', foo3)}} 62 | -------------------------------------------------------------------------------- /tests/cases/replace_regex-captures.just: -------------------------------------------------------------------------------- 1 | foo := replace_regex(justfile(), '[^A-Za-z]([A-Za-z]+)\.just$', ' - Test $1') 2 | 3 | foo2 := replace_regex(""" 4 | \tLorem ipsum dolor sit amet, consectetur adipiscing elit, sed 5 | do eiusmod tempor incididunt ut labore et dolore 6 | disputandum putant. Sed ut omittam pericula, labores, 7 | dolorem etiam, quem optimus quisque pro patria et pro suis 8 | suscipit, ut non plus habeat sapiens, quod gaudeat, quam 9 | quod angatur. Optime vero Epicurus, quod exiguam dixit 10 | fortunam intervenire sapienti maximasque ab eo dissentiunt, 11 | sed certe non. 12 | """, '\W(?P\w{2})\W', " _$Abc1_-") 13 | 14 | foo3 := replace_regex(`echo ${HOME}`, '/(\w+)/(\w+)', '${2}at${1}') 15 | 16 | foo4 := replace_regex('$T$es$t', 17 | '\$([tT])' 18 | , 19 | '$1' 20 | ) 21 | 22 | foo5 := replace_regex("any\n$123", '\$123', """ 23 | \t$$abc$ 24 | 234\ 25 | 345 ${456}567 26 | """) 27 | 28 | foo6 := replace_regex("Some text.", '([^a-z])', '$$$1$$2') 29 | 30 | foo7 := replace_regex("Some text.", '([^a-z])', '$$1') 31 | 32 | foo8 := replace_regex("test123", '\w(\d+)', ''' 33 | \t'$1''') 34 | 35 | foo9 := replace_regex("trailing comma is accepted after the final argument,", 36 | "(\\W+)", \ 37 | "${1}_$1", 38 | ) 39 | 40 | foo10 := replace_regex("_abcdef", 41 | '(\w)[a-z]+', \ 42 | 43 | '${1}' 44 | , \ 45 | ) 46 | 47 | foo11 := replace_regex(HEX, '\d+', "$$$$1") 48 | 49 | not_a_replacement := 'test $1 test ' 50 | not_a_replacement_2 := "test $Abc_ test" 51 | 52 | not_a_regex := replace('Foo Bar', ' ', '$1') 53 | 54 | # special highlighting should only occur in the simplest case 55 | replacement_conditional := replace_regex('123456789', '([2468])', if "$1" == "$2" { '$1_' } else { '_${1}_' }) 56 | replacement_nested_func := replace_regex('987654321', '([13579])', trim_start(' $1 ')) 57 | replacement_split := replace_regex('012034', '(?P[1-9]+)', "!$cap" + 'ture' + '$captu' / "re") 58 | 59 | replacement_in_interp: 60 | echo '{{replace_regex('A b c d', '\s(\w)', '1$1')}}' 61 | echo {{replace_regex(replacement_conditional, '(\d+)', foo3)}} 62 | -------------------------------------------------------------------------------- /tests/cases/set.html: -------------------------------------------------------------------------------- 1 | set allow-duplicate-variables 2 | set allow-duplicate-variables := true 3 | set allow-duplicate-variables := false 4 | 5 | set dotenv-load 6 | set dotenv-load := true 7 | set dotenv-load := false 8 | 9 | set dotenv-override 10 | set dotenv-override := true 11 | set dotenv-override := false 12 | 13 | set export 14 | set export := true 15 | set export := false 16 | 17 | set positional-arguments 18 | set positional-arguments := true 19 | set positional-arguments := false 20 | 21 | set quiet 22 | set quiet := true 23 | set quiet := false 24 | 25 | set shell := ["foo"] 26 | set shell := ["foo",] 27 | set shell := ["foo", "bar"] 28 | set shell := ["foo", "bar",] 29 | set shell := ['python3', '-c', """ 30 | import sys\nprint(sys.argv[1:]) 31 | """ 32 | 33 | , ''' 34 | prefix''' 35 | ] 36 | set shell:=['bash',"-c"] 37 | set shell := [x"$SHELL"] 38 | 39 | set windows-shell:=["powershell.exe", "-NoLogo", "-Command"] 40 | 41 | set script-interpreter := ['/usr/bin/env', 'python3'] 42 | 43 | set fallback 44 | set fallback := true 45 | set fallback := false 46 | set fallback:=false 47 | set fallback:= true 48 | set fallback :=false 49 | 50 | # the trailing whitespace at the end of the following lines is intentional 51 | set fallback 52 | set fallback := true 53 | set fallback := false 54 | 55 | set fallback # comment 56 | set fallback := true # comment 57 | set fallback := true# comment 58 | 59 | set fallback := true \ 60 | # comment 61 | 62 | set fallback := false\ 63 | #comment 64 | 65 | set tempdir := '/tmp' 66 | set tempdir := """ 67 | /tmp""" 68 | set tempdir:="/tmp" 69 | set tempdir:= "/tmp" 70 | set tempdir :='/tmp' 71 | set tempdir := x'${XDG_CACHE_HOME:-/tmp}' 72 | 73 | set dotenv-filename := "mydotenv" 74 | set dotenv-path := '/tmp/dotenv' 75 | 76 | set dotenv-required 77 | set dotenv-required := true 78 | set dotenv-required := false 79 | 80 | set unstable 81 | set unstable := true 82 | set unstable := false 83 | 84 | set working-directory := '/tmp' 85 | 86 | set no-exit-message 87 | set no-exit-message := true 88 | set no-exit-message := false 89 | -------------------------------------------------------------------------------- /tests/cases/set.just: -------------------------------------------------------------------------------- 1 | set allow-duplicate-variables 2 | set allow-duplicate-variables := true 3 | set allow-duplicate-variables := false 4 | 5 | set dotenv-load 6 | set dotenv-load := true 7 | set dotenv-load := false 8 | 9 | set dotenv-override 10 | set dotenv-override := true 11 | set dotenv-override := false 12 | 13 | set export 14 | set export := true 15 | set export := false 16 | 17 | set positional-arguments 18 | set positional-arguments := true 19 | set positional-arguments := false 20 | 21 | set quiet 22 | set quiet := true 23 | set quiet := false 24 | 25 | set shell := ["foo"] 26 | set shell := ["foo",] 27 | set shell := ["foo", "bar"] 28 | set shell := ["foo", "bar",] 29 | set shell := ['python3', '-c', """ 30 | import sys\nprint(sys.argv[1:]) 31 | """ 32 | 33 | , ''' 34 | prefix''' 35 | ] 36 | set shell:=['bash',"-c"] 37 | set shell := [x"$SHELL"] 38 | 39 | set windows-shell:=["powershell.exe", "-NoLogo", "-Command"] 40 | 41 | set script-interpreter := ['/usr/bin/env', 'python3'] 42 | 43 | set fallback 44 | set fallback := true 45 | set fallback := false 46 | set fallback:=false 47 | set fallback:= true 48 | set fallback :=false 49 | 50 | # the trailing whitespace at the end of the following lines is intentional 51 | set fallback 52 | set fallback := true 53 | set fallback := false 54 | 55 | set fallback # comment 56 | set fallback := true # comment 57 | set fallback := true# comment 58 | 59 | set fallback := true \ 60 | # comment 61 | 62 | set fallback := false\ 63 | #comment 64 | 65 | set tempdir := '/tmp' 66 | set tempdir := """ 67 | /tmp""" 68 | set tempdir:="/tmp" 69 | set tempdir:= "/tmp" 70 | set tempdir :='/tmp' 71 | set tempdir := x'${XDG_CACHE_HOME:-/tmp}' 72 | 73 | set dotenv-filename := "mydotenv" 74 | set dotenv-path := '/tmp/dotenv' 75 | 76 | set dotenv-required 77 | set dotenv-required := true 78 | set dotenv-required := false 79 | 80 | set unstable 81 | set unstable := true 82 | set unstable := false 83 | 84 | set working-directory := '/tmp' 85 | 86 | set no-exit-message 87 | set no-exit-message := true 88 | set no-exit-message := false 89 | -------------------------------------------------------------------------------- /tests/cases/tricky.html: -------------------------------------------------------------------------------- 1 | # This recipe is at the start of this file because `just` inserts extra lines to make shebang script line numbers match the corresponding justfile line numbers. 2 | skip-all-whitespace-lines-shebang: 3 | #!/bin/bash 4 | 5 | 6 | 7 | echo -e -n '\033[1;47;30m' 8 | cat "$0" 9 | echo -e -n '\033[0m' 10 | 11 | set ignore-comments 12 | 13 | raw := ''' 14 | it's two lines \ 15 | ''' 16 | 17 | something := """ 18 | "T E " \ 19 | "S T " \ 20 | """ 21 | 22 | testing := ``` 23 | foo=`echo ' testing '`;echo $foo 123 24 | ``` 25 | 26 | nospace:='ABCabc' 27 | 28 | other_export := '1' 29 | export some_export := '0' + other_export 30 | export SOME_EXPORT2:='1' 31 | 32 | valid_uuid := uuid( ) 33 | 34 | this_justfile := justfile ( 35 | 36 | ) 37 | 38 | not_a_uuid_anymore := replace_regex ( 39 | uuid ( 40 | ), '-4([0-9A-Fa-f]{3})-' 41 | , '_9${1}_' 42 | ) 43 | 44 | conditional_without_indent := if env('SHELL') == '/bin/bash' { 45 | valid_uuid + testing 46 | } else { '' } 47 | 48 | paren_without_indent := ( 49 | valid_uuid + testing 50 | ) 51 | 52 | back2back_interpolations: 53 | echo {{something}}{{testing}} 54 | 55 | interp_immediately_after_string: 56 | #!/usr/bin/env python3 57 | print(''{{quote(something)}}''.split("T")) 58 | 59 | curly_braces_3: 60 | echo 'Four {{{{{{{{{{"""\ 61 | Opening """ + 'Curly ' + "Braces"}}!' 62 | echo }} 63 | 64 | line_continuation: 65 | echo 'This starts with tabs \ 66 | but continues with tabs and spaces' 67 | echo 123 456 \ 68 | 789 69 | 70 | line_continuation2: 71 | echo "This starts with spaces \ 72 | but continues with spaces and tabs" 73 | echo 987 654 \ 74 | 321 75 | 76 | mismatched_but_escaped_quotes: 77 | echo it\'s a single apostrophe 78 | echo "\"single escaped quote" 79 | echo "string \\" ended there 80 | 81 | shebang_mixed_indentation: 82 | #!/usr/bin/env python3 83 | if True: 84 | print('tab indent followed by spaces is allowed in shebang recipe') 85 | 86 | shebang_recipes_dont_do_line_prefixes: 87 | #!/bin/bash 88 | @foo 89 | 90 | multiline-non-paren-final-param param0=valid_uuid *$PARAM=""" 91 | foo 92 | 93 | """: 94 | echo {{param0}} 95 | env | grep -F PARAM 96 | 97 | immediate-comment:#this is valid 98 | echo comment 99 | 100 | spaced_params param1 = valid_uuid * param2 = "x" : 101 | echo {{param1}} {{param2}} 102 | 103 | spaced_params2 param1 = valid_uuid + $ ENV_PARAM2 = 'exported environment variable value': 104 | echo {{param1}} 105 | env | grep -F PARAM2 106 | 107 | spaced_paren_param param1 = (valid_uuid + valid_uuid): 108 | echo {{param1}} 109 | 110 | spaced_paren_param2 param=(valid_uuid ) : 111 | echo {{param}} 112 | 113 | invoke shell: 114 | {{shell}} 115 | 116 | multi count=num_cpus(): 117 | seq -s ' ' 1 {{count}} 118 | 119 | functions2 cpus = num_cpus() foo = env('FOO', just_pid()) : 120 | echo '{{foo}}:x{{cpus}}' 121 | 122 | env_test_export: 123 | env | grep -F -i export 124 | alias printenv:=env_test_export 125 | 126 | parenthesized_dep_param: (multi ('1' + '0')) 127 | 128 | parenthesized_dep_param2 p: (invoke \ 129 | (p + " -c 'echo 123'") \ 130 | ) 131 | 132 | parenthesized_dep_param3: (multi \ 133 | ( '1' + '0' ) \ 134 | ) ( 135 | functions2 valid_uuid) \ 136 | ( 137 | 138 | \ 139 | 140 | unexport 141 | this_justfile 142 | ) 143 | 144 | skip-all-whitespace-lines1: 145 | echo '111' 146 | 147 | echo '222' 148 | 149 | skip-all-whitespace-lines2: 150 | echo start 151 | 152 | 153 | 154 | echo end 155 | 156 | triple-quoted-multiline-interp: 157 | echo {{quote(""" 158 | [package] 159 | name = "foo" 160 | edition = "2021" 161 | 162 | [dependencies] 163 | """)}} > {{quote(cache_directory() / 'Cargo.toml-' + valid_uuid)}} 164 | 165 | not-shell-expanded-string fix: (functions2 fix'$HOME') 166 | not-shell-expanded-string2 _-x: (functions2 _-x"$HOME") 167 | 168 | unexport PARAM \ 169 | : 170 | -env | grep -P {{quote(PARAM)}} 171 | 172 | ignored-comments-in-recipe foo='Foo': 173 | echo recipe 174 | # echo {{ foo }} {{{{test \ 175 | echo recipe 176 | 177 | # documented recipe 1 178 | documented1: 179 | # documented recipe 2 180 | documented2: 181 | 182 | space_after_name_before_colon_no_params : 183 | echo recipe body 184 | -------------------------------------------------------------------------------- /tests/cases/tricky.just: -------------------------------------------------------------------------------- 1 | # This recipe is at the start of this file because `just` inserts extra lines to make shebang script line numbers match the corresponding justfile line numbers. 2 | skip-all-whitespace-lines-shebang: 3 | #!/bin/bash 4 | 5 | 6 | 7 | echo -e -n '\033[1;47;30m' 8 | cat "$0" 9 | echo -e -n '\033[0m' 10 | 11 | set ignore-comments 12 | 13 | raw := ''' 14 | it's two lines \ 15 | ''' 16 | 17 | something := """ 18 | "T E " \ 19 | "S T " \ 20 | """ 21 | 22 | testing := ``` 23 | foo=`echo ' testing '`;echo $foo 123 24 | ``` 25 | 26 | nospace:='ABCabc' 27 | 28 | other_export := '1' 29 | export some_export := '0' + other_export 30 | export SOME_EXPORT2:='1' 31 | 32 | valid_uuid := uuid( ) 33 | 34 | this_justfile := justfile ( 35 | 36 | ) 37 | 38 | not_a_uuid_anymore := replace_regex ( 39 | uuid ( 40 | ), '-4([0-9A-Fa-f]{3})-' 41 | , '_9${1}_' 42 | ) 43 | 44 | conditional_without_indent := if env('SHELL') == '/bin/bash' { 45 | valid_uuid + testing 46 | } else { '' } 47 | 48 | paren_without_indent := ( 49 | valid_uuid + testing 50 | ) 51 | 52 | back2back_interpolations: 53 | echo {{something}}{{testing}} 54 | 55 | interp_immediately_after_string: 56 | #!/usr/bin/env python3 57 | print(''{{quote(something)}}''.split("T")) 58 | 59 | curly_braces_3: 60 | echo 'Four {{{{{{{{{{"""\ 61 | Opening """ + 'Curly ' + "Braces"}}!' 62 | echo }} 63 | 64 | line_continuation: 65 | echo 'This starts with tabs \ 66 | but continues with tabs and spaces' 67 | echo 123 456 \ 68 | 789 69 | 70 | line_continuation2: 71 | echo "This starts with spaces \ 72 | but continues with spaces and tabs" 73 | echo 987 654 \ 74 | 321 75 | 76 | mismatched_but_escaped_quotes: 77 | echo it\'s a single apostrophe 78 | echo "\"single escaped quote" 79 | echo "string \\" ended there 80 | 81 | shebang_mixed_indentation: 82 | #!/usr/bin/env python3 83 | if True: 84 | print('tab indent followed by spaces is allowed in shebang recipe') 85 | 86 | shebang_recipes_dont_do_line_prefixes: 87 | #!/bin/bash 88 | @foo 89 | 90 | multiline-non-paren-final-param param0=valid_uuid *$PARAM=""" 91 | foo 92 | 93 | """: 94 | echo {{param0}} 95 | env | grep -F PARAM 96 | 97 | immediate-comment:#this is valid 98 | echo comment 99 | 100 | spaced_params param1 = valid_uuid * param2 = "x" : 101 | echo {{param1}} {{param2}} 102 | 103 | spaced_params2 param1 = valid_uuid + $ ENV_PARAM2 = 'exported environment variable value': 104 | echo {{param1}} 105 | env | grep -F PARAM2 106 | 107 | spaced_paren_param param1 = (valid_uuid + valid_uuid): 108 | echo {{param1}} 109 | 110 | spaced_paren_param2 param=(valid_uuid ) : 111 | echo {{param}} 112 | 113 | invoke shell: 114 | {{shell}} 115 | 116 | multi count=num_cpus(): 117 | seq -s ' ' 1 {{count}} 118 | 119 | functions2 cpus = num_cpus() foo = env('FOO', just_pid()) : 120 | echo '{{foo}}:x{{cpus}}' 121 | 122 | env_test_export: 123 | env | grep -F -i export 124 | alias printenv:=env_test_export 125 | 126 | parenthesized_dep_param: (multi ('1' + '0')) 127 | 128 | parenthesized_dep_param2 p: (invoke \ 129 | (p + " -c 'echo 123'") \ 130 | ) 131 | 132 | parenthesized_dep_param3: (multi \ 133 | ( '1' + '0' ) \ 134 | ) ( 135 | functions2 valid_uuid) \ 136 | ( 137 | 138 | \ 139 | 140 | unexport 141 | this_justfile 142 | ) 143 | 144 | skip-all-whitespace-lines1: 145 | echo '111' 146 | 147 | echo '222' 148 | 149 | skip-all-whitespace-lines2: 150 | echo start 151 | 152 | 153 | 154 | echo end 155 | 156 | triple-quoted-multiline-interp: 157 | echo {{quote(""" 158 | [package] 159 | name = "foo" 160 | edition = "2021" 161 | 162 | [dependencies] 163 | """)}} > {{quote(cache_directory() / 'Cargo.toml-' + valid_uuid)}} 164 | 165 | not-shell-expanded-string fix: (functions2 fix'$HOME') 166 | not-shell-expanded-string2 _-x: (functions2 _-x"$HOME") 167 | 168 | unexport PARAM \ 169 | : 170 | -env | grep -P {{quote(PARAM)}} 171 | 172 | ignored-comments-in-recipe foo='Foo': 173 | echo recipe 174 | # echo {{ foo }} {{{{test \ 175 | echo recipe 176 | 177 | # documented recipe 1 178 | documented1: 179 | # documented recipe 2 180 | documented2: 181 | 182 | space_after_name_before_colon_no_params : 183 | echo recipe body 184 | -------------------------------------------------------------------------------- /tests/cases/unstable.html: -------------------------------------------------------------------------------- 1 | set unstable 2 | 3 | [script: 'python3'] 4 | python_info: 5 | import platform 6 | print(platform.python_implementation(), platform.python_version()) 7 | 8 | [script('just', '-d', x'${PWD:-.}/', '-f')] 9 | nested_justfile: 10 | foo: 11 | pwd 12 | 13 | foo := 'foo' && '' || 'foo' 14 | fdfind := which('fd') || require('fdfind') 15 | -------------------------------------------------------------------------------- /tests/cases/unstable.just: -------------------------------------------------------------------------------- 1 | set unstable 2 | 3 | [script: 'python3'] 4 | python_info: 5 | import platform 6 | print(platform.python_implementation(), platform.python_version()) 7 | 8 | [script('just', '-d', x'${PWD:-.}/', '-f')] 9 | nested_justfile: 10 | foo: 11 | pwd 12 | 13 | foo := 'foo' && '' || 'foo' 14 | fdfind := which('fd') || require('fdfind') 15 | -------------------------------------------------------------------------------- /tests/convert-to-html.vim: -------------------------------------------------------------------------------- 1 | set nobackup " don't save backup files 2 | set nowritebackup " don't create backup files while editing 3 | setlocal readonly " no need to modify test case justfiles 4 | set nofixeol " don't add or remove trailing newlines 5 | 6 | " define a color for every default syntax class 7 | hi Boolean ctermfg=7* 8 | hi Character ctermfg=7* 9 | hi Comment ctermfg=7* 10 | hi Conditional ctermfg=7* 11 | hi Constant ctermfg=7* 12 | hi Debug ctermfg=7* 13 | hi Define ctermfg=7* 14 | hi Delimiter ctermfg=7* 15 | hi Error ctermfg=7* 16 | hi Exception ctermfg=7* 17 | hi Float ctermfg=7* 18 | hi Function ctermfg=7* 19 | hi Identifier ctermfg=7* 20 | hi Ignore ctermfg=7* 21 | hi Include ctermfg=7* 22 | hi Keyword ctermfg=7* 23 | hi Label ctermfg=7* 24 | hi Macro ctermfg=7* 25 | hi Normal ctermfg=7* 26 | hi Number ctermfg=7* 27 | hi Operator ctermfg=7* 28 | hi PreCondit ctermfg=7* 29 | hi PreProc ctermfg=7* 30 | hi Repeat ctermfg=7* 31 | hi Special ctermfg=7* 32 | hi SpecialChar ctermfg=7* 33 | hi SpecialComment ctermfg=7* 34 | hi Statement ctermfg=7* 35 | hi StorageClass ctermfg=7* 36 | hi String ctermfg=7* 37 | hi Tag ctermfg=7* 38 | hi Todo ctermfg=7* 39 | hi Type ctermfg=7* 40 | hi Typedef ctermfg=7* 41 | hi Underlined ctermfg=7* 42 | 43 | " define a helper function to check whether a syntax group is cleared 44 | " This is so different between Vim and versions of Neovim that we need platform-specific variants. 45 | if exists("*hlget") 46 | " Vim 47 | function s:is_cleared(name) 48 | let l:chr_hlget = hlget(a:name) 49 | return len(l:chr_hlget) > 0 && l:chr_hlget[0]->has_key("cleared") && l:chr_hlget[0].cleared 50 | endfunction 51 | elseif has("nvim") && exists("*nvim_get_hl") 52 | " Neovim >= 0.9 53 | function s:is_cleared(name) 54 | return len(nvim_get_hl(0, {"name": a:name})) == 0 55 | endfunction 56 | else 57 | " old Neovim 58 | " fallback to checking name 59 | " this function is only ever passed canonical group name obtained from result of synIDtrans() 60 | " so if we get a name starting with 'just' it's an unlinked implementation detail of vim-just 61 | function s:is_cleared(name) 62 | return luaeval('string.len(_A) > 4 and string.sub(_A, 1, 4) == "just"', a:name) 63 | endfunction 64 | endif 65 | 66 | " convert justfile to HTML 67 | function s:html(winid) 68 | let l:saved_wid = win_getid() 69 | call win_gotoid(a:winid) 70 | 71 | let l:has_eol = &eol 72 | 73 | let l:output = [] 74 | 75 | let l:line_n = 1 76 | let l:line_max = line('$') 77 | 78 | while l:line_n <= l:line_max 79 | let l:line = getline(l:line_n) 80 | let l:last_name = "" 81 | let l:line_html = "" 82 | 83 | let l:chr_n = 0 84 | let l:chr = l:line[l:chr_n] 85 | let l:last_synid = hlID("cleared") 86 | while !empty(l:chr) 87 | let l:synid = synID(l:line_n, l:chr_n + 1, 1) 88 | let l:name = synIDattr(synIDtrans(l:synid), "name") 89 | if s:is_cleared(l:name) 90 | let l:name = "" 91 | endif 92 | 93 | if l:synid != l:last_synid 94 | if !empty(l:last_name) 95 | let l:line_html .= '' 96 | endif 97 | if !empty(l:name) 98 | let l:line_html .= '' 99 | endif 100 | endif 101 | 102 | if l:chr == '&' 103 | let l:line_html .= '&' 104 | elseif l:chr == '<' 105 | let l:line_html .= '<' 106 | elseif l:chr == '>' 107 | let l:line_html .= '>' 108 | elseif l:chr == "\t" 109 | let l:line_html .= ' ' 110 | else 111 | let l:line_html .= l:chr 112 | endif 113 | 114 | let l:last_name = l:name 115 | let l:last_synid = l:synid 116 | 117 | let l:chr_n += 1 118 | let l:chr = l:line[l:chr_n] 119 | endwhile 120 | 121 | if !empty(l:last_name) 122 | let l:line_html .= '' 123 | endif 124 | 125 | call add(l:output, l:line_html) 126 | let l:line_n += 1 127 | endwhile 128 | 129 | if l:has_eol 130 | call add(l:output, "") 131 | endif 132 | 133 | call win_gotoid(l:saved_wid) 134 | return l:output 135 | endfunction 136 | 137 | " perform HTML conversion in a new buffer and write HTML to output 138 | let s:wid = win_getid() 139 | new | set noeol | call append(1, s:html(s:wid)) | 1d | w! $OUTPUT 140 | 141 | " quit! 142 | qa! 143 | -------------------------------------------------------------------------------- /tests/justfile: -------------------------------------------------------------------------------- 1 | # To enable using preview recipes from cwd inside tests/ 2 | set fallback 3 | 4 | # Newline-separated list of test cases that are _not_ intended to be runnable justfiles. 5 | cases_invalid_justfiles := """ 6 | comment 7 | deprecated_obsolete 8 | invalid 9 | mod 10 | set 11 | """ 12 | 13 | # run syntax highlighting tests 14 | run FILTER='': 15 | cargo run {{ if FILTER == '' { '' } else { '-- ' + quote(FILTER) } }} 16 | 17 | # run tests whenever a file changes 18 | [positional-arguments] 19 | watch *args: 20 | {{if args =~ '(?:^|\s)watch(?:\s|$)' { error('`just watch watch` is redundant') } else { '' } }} 21 | watchexec \ 22 | --clear \ 23 | --shell=none \ 24 | --project-origin {{quote(clean(source_directory() / '..'))}} \ 25 | --watch {{quote(clean(source_directory() / '..'))}} \ 26 | {{quote(just_executable())}} "$@" 27 | 28 | # run ftdetect tests 29 | ftdetect: 30 | cargo run --bin=test-ftdetect 31 | 32 | # install development dependencies 33 | deps: 34 | cargo install watchexec-cli 35 | 36 | # compile test runner without running tests 37 | build: 38 | cargo build 39 | 40 | # for test runner development: strict production-level checks on test runner code 41 | lint: 42 | cargo clippy --all-targets --all-features -- -Dwarnings 43 | cargo fmt --all --check 44 | 45 | # for test case development: verify that test case files are valid justfiles 46 | check-cases: 47 | #!/bin/bash 48 | set -e 49 | declare -rA cases_invalid_justfiles=({{ \ 50 | replace_regex(cases_invalid_justfiles, "([^\n]+)", '["cases/${1}.just"]=1, ') \ 51 | }}) 52 | for case in cases/*.just;do 53 | if [[ -z "${cases_invalid_justfiles["$case"]}" ]];then 54 | {{quote(just_executable())}} --unstable -f "$case" --list >/dev/null 55 | fi 56 | done 57 | -------------------------------------------------------------------------------- /tests/rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | -------------------------------------------------------------------------------- /tests/src/common.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | ffi::OsString, 4 | fs::canonicalize, 5 | io::{self, ErrorKind, prelude::*}, 6 | os::unix::fs as ufs, 7 | path::{Path, PathBuf}, 8 | process::{Command, Stdio}, 9 | sync::{ 10 | Arc, LazyLock, 11 | atomic::{AtomicBool, Ordering::Relaxed}, 12 | }, 13 | time::Duration, 14 | }; 15 | pub use tempfile::{TempDir, tempdir}; 16 | use wait_timeout::ChildExt; 17 | 18 | pub static VIM_BIN: LazyLock = LazyLock::new(|| { 19 | let default_vim = OsString::from("vim"); 20 | let v = env::var_os("TEST_VIM").unwrap_or(default_vim.clone()); 21 | if v.is_empty() { default_vim } else { v } 22 | }); 23 | static TEST_NVIM: LazyLock = LazyLock::new(|| { 24 | PathBuf::from(&*VIM_BIN) 25 | .file_stem() 26 | .unwrap() 27 | .to_str() 28 | .unwrap() 29 | .contains("nvim") 30 | }); 31 | 32 | pub fn test_vim_home() -> TempDir { 33 | let test_home = tempdir().unwrap(); 34 | ufs::symlink( 35 | canonicalize("..").unwrap(), 36 | test_home 37 | .path() 38 | .join(if *TEST_NVIM { "nvim" } else { ".vim" }), 39 | ) 40 | .unwrap(); 41 | test_home 42 | } 43 | 44 | pub fn setup_ctrlc_handler() -> Arc { 45 | let interrupted = Arc::new(AtomicBool::new(false)); 46 | let interrupted_ = Arc::clone(&interrupted); 47 | 48 | ctrlc::set_handler(move || { 49 | interrupted_.store(true, Relaxed); 50 | eprintln!("Received Ctrl+C"); 51 | }) 52 | .unwrap(); 53 | 54 | interrupted 55 | } 56 | 57 | pub fn run_vim( 58 | args: Vec<&str>, 59 | output: &PathBuf, 60 | home: &Path, 61 | interrupted: &Arc, 62 | ) -> io::Result<()> { 63 | let mut vim = match Command::new(&*VIM_BIN) 64 | .arg(if *TEST_NVIM { 65 | "--headless" 66 | } else { 67 | "--not-a-term" 68 | }) 69 | .args(["-i", "NONE", "--cmd", "set noswapfile"]) 70 | .args(args) 71 | .env("OUTPUT", output) 72 | .env("HOME", home) 73 | .env("XDG_CONFIG_HOME", home) 74 | .env("XDG_DATA_HOME", home) 75 | .stdin(Stdio::piped()) 76 | .stdout(Stdio::null()) 77 | .stderr(Stdio::piped()) 78 | .spawn() 79 | { 80 | Ok(o) => o, 81 | Err(e) => { 82 | eprintln!("Failed to start {:?} subprocess", VIM_BIN.to_string_lossy()); 83 | return Err(e); 84 | } 85 | }; 86 | 87 | let mut vim_stdin = vim.stdin.take().unwrap(); 88 | // Prevent stalling on "Press ENTER or type command to continue" 89 | if let Err(e) = vim_stdin.write_all(b"\r") { 90 | // If we return this error, the Vim process will never be waited on in the error case, 91 | // resulting in `clippy::zombie_processes` lint flagging the above call to `.spawn()`. 92 | // To avoid stray processes, don't return this error, try to terminate the Vim process 93 | // but unconditionally fall through to the .wait_timeout() below 94 | // so that the Vim process is always waited on. 95 | eprintln!("Error writing to subprocess stdin: {e}"); 96 | if let Err(e) = vim.kill() { 97 | eprintln!("Error sending signal to subprocess: {e}"); 98 | } 99 | } 100 | 101 | let status = loop { 102 | let poll_interval = Duration::from_millis(200); 103 | match vim.wait_timeout(poll_interval) { 104 | Ok(Some(status)) => break status, 105 | Ok(None) => { 106 | if interrupted.load(Relaxed) { 107 | vim.kill().unwrap(); 108 | return Err(io::Error::new(ErrorKind::Interrupted, "interrupted!")); 109 | } 110 | } 111 | Err(e) => { 112 | return Err(e); 113 | } 114 | } 115 | }; 116 | 117 | if status.success() { 118 | Ok(()) 119 | } else { 120 | Err(io::Error::other(format!( 121 | "{} failed with status: {}", 122 | VIM_BIN.to_string_lossy(), 123 | status 124 | ))) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/src/main.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use crate::common::*; 3 | 4 | use fancy_regex::Regex; 5 | use rayon::prelude::*; 6 | use std::{ 7 | collections::HashMap, 8 | env, 9 | ffi::OsStr, 10 | fs, 11 | io::{self, ErrorKind}, 12 | path::{Path, PathBuf}, 13 | sync::{ 14 | Mutex, 15 | atomic::{AtomicU64, Ordering::Relaxed}, 16 | }, 17 | time::Instant, 18 | }; 19 | 20 | fn main() -> io::Result<()> { 21 | let filter = match env::args().nth(1).map(|s| Regex::new(&s)).transpose() { 22 | Ok(o) => o, 23 | Err(e) => { 24 | return Err(io::Error::other(format!( 25 | "Invalid regex given on command line: {e}" 26 | ))); 27 | } 28 | }; 29 | 30 | let interrupted = setup_ctrlc_handler(); 31 | 32 | let test_home = test_vim_home(); 33 | let tmpdir = tempdir().unwrap(); 34 | 35 | let case_dir = Path::new("cases"); 36 | 37 | let mut test_cases = fs::read_dir(case_dir)? 38 | .filter_map( 39 | |res: io::Result| -> Option> { 40 | match res { 41 | Ok(o) => { 42 | let p = o.path(); 43 | let ext = p.extension(); 44 | if ext != Some(OsStr::new("just")) { 45 | return None; 46 | } 47 | let name = p.file_stem().unwrap().to_str().unwrap().to_owned(); 48 | if let Some(rx) = &filter 49 | && !rx.is_match(&name).unwrap() 50 | { 51 | return None; 52 | } 53 | Some(Ok((name, p))) 54 | } 55 | Err(e) => Some(Err(e)), 56 | } 57 | }, 58 | ) 59 | .collect::, io::Error>>()?; 60 | test_cases.sort_unstable(); 61 | 62 | let total = test_cases.len(); 63 | let mut passed = 0; 64 | 65 | let total_vim_time = AtomicU64::new(0); 66 | 67 | let res = Mutex::new(HashMap::::with_capacity(total)); 68 | test_cases 69 | .par_iter() 70 | .try_for_each(|(name, case)| -> io::Result<()> { 71 | let output = tmpdir.path().join(format!("{name}.output.html")); 72 | 73 | let ts = Instant::now(); 74 | 75 | run_vim( 76 | vec!["-S", "convert-to-html.vim", case.to_str().unwrap()], 77 | &output, 78 | test_home.path(), 79 | &interrupted, 80 | )?; 81 | 82 | let vim_time = ts.elapsed().as_millis() as u64; 83 | total_vim_time.fetch_add(vim_time, Relaxed); 84 | 85 | let html = fs::read_to_string(&output)?; 86 | res.lock().unwrap().insert(name.to_owned(), html); 87 | 88 | Ok(()) 89 | })?; 90 | 91 | eprintln!( 92 | "{} total execution time: {}s.", 93 | VIM_BIN.to_string_lossy(), 94 | total_vim_time.load(Relaxed) as f64 / 1000.0 95 | ); 96 | 97 | let res = res.into_inner().unwrap(); 98 | 99 | for (name, _) in test_cases.iter() { 100 | eprintln!("test {name}…"); 101 | 102 | let output = &res[name]; 103 | 104 | let expected = case_dir.join(format!("{name}.html")); 105 | 106 | if !expected.is_file() { 107 | eprintln!( 108 | "`{}` is missing, output was:\n{}", 109 | expected.display(), 110 | output 111 | ); 112 | continue; 113 | } 114 | 115 | if interrupted.load(Relaxed) { 116 | return Err(io::Error::new(ErrorKind::Interrupted, "interrupted!")); 117 | } 118 | 119 | let expected = fs::read_to_string(expected)?; 120 | 121 | let diff = similar::TextDiff::from_lines(output, &expected) 122 | .unified_diff() 123 | .header("output", "expected") 124 | .to_string(); 125 | if diff.is_empty() { 126 | eprintln!("ok"); 127 | passed += 1; 128 | } else { 129 | eprintln!("syntax highlighting mismatch:"); 130 | for line in diff.lines() { 131 | if line.starts_with(' ') { 132 | eprintln!("{line}"); 133 | continue; 134 | } 135 | let color = if line.starts_with('+') { 136 | "0;32" 137 | } else if line.starts_with('-') { 138 | "0;31" 139 | } else if line.starts_with('@') { 140 | "0;36" 141 | } else if line == "\\ No newline at end of file" { 142 | "0;97" 143 | } else { 144 | unimplemented!("no defined color for line: '{}'", line); 145 | }; 146 | eprintln!("\x1B[{color}m{line}\x1B[0m"); 147 | } 148 | } 149 | } 150 | 151 | if passed == total { 152 | Ok(()) 153 | } else { 154 | Err(io::Error::other(format!( 155 | "{}/{} tests failed.", 156 | total - passed, 157 | total 158 | ))) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /tests/src/test-ftdetect.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use crate::common::*; 3 | 4 | use fancy_regex::Regex; 5 | use rand::{ 6 | Rng, 7 | distr::{Alphanumeric, SampleString}, 8 | rngs::ThreadRng, 9 | }; 10 | use serde::Deserialize; 11 | use std::{ 12 | collections::{HashMap, HashSet}, 13 | fs::{self, File}, 14 | hash::{Hash, Hasher}, 15 | io::{self, prelude::*}, 16 | }; 17 | 18 | #[derive(Clone, Debug, Default, Deserialize, Eq)] 19 | #[serde(deny_unknown_fields)] 20 | struct FtdetectCase { 21 | #[serde(default)] 22 | filename: String, 23 | 24 | #[serde(default)] 25 | never: String, 26 | 27 | #[serde(default)] 28 | content: String, 29 | 30 | #[serde(default)] 31 | not_justfile: bool, 32 | } 33 | 34 | impl PartialEq for FtdetectCase { 35 | fn eq(&self, other: &Self) -> bool { 36 | self.filename == other.filename && self.content == other.content 37 | } 38 | } 39 | 40 | impl Hash for FtdetectCase { 41 | fn hash(&self, state: &mut H) { 42 | self.filename.hash(state); 43 | self.content.hash(state); 44 | } 45 | } 46 | 47 | fn is_justfile_filename(s: &str) -> bool { 48 | let s = s.to_ascii_lowercase(); 49 | s == "justfile" || s.ends_with(".justfile") || s.ends_with(".just") 50 | } 51 | 52 | fn random_alnum(rng: &mut ThreadRng, minlen: u8, maxlen: u8, never_like_justfile: bool) -> String { 53 | let len = rng.random_range(minlen..=maxlen); 54 | loop { 55 | let r = Alphanumeric.sample_string(rng, len.into()); 56 | if !(never_like_justfile && is_justfile_filename(&r)) { 57 | break r; 58 | } 59 | } 60 | } 61 | 62 | fn fuzz_filename>(rng: &mut ThreadRng, filename: T) -> String { 63 | filename 64 | .as_ref() 65 | .split_inclusive('*') 66 | .map(|part| part.replace('*', random_alnum(rng, 3, 8, false).as_str())) 67 | .collect() 68 | } 69 | 70 | fn main() -> io::Result<()> { 71 | let interrupted = setup_ctrlc_handler(); 72 | 73 | let test_home = test_vim_home(); 74 | let mut tempdirs: Vec = vec![tempdir().unwrap()]; 75 | 76 | let mut rng = rand::rng(); 77 | 78 | let cases = fs::read_to_string("cases/ftdetect.yml")?; 79 | let cases = match serde_yaml2::from_str::>(cases.as_str()) { 80 | Ok(o) => o, 81 | Err(e) => return Err(io::Error::other(e)), 82 | }; 83 | 84 | let total = cases.len(); 85 | let mut passed = 0; 86 | 87 | let mut file2case = HashMap::::with_capacity(total); 88 | let mut unique = HashSet::::with_capacity(total); 89 | for case in cases { 90 | if !unique.insert(case.clone()) { 91 | return Err(io::Error::other(format!( 92 | "Duplicate or contradictory test case: {case:?}" 93 | ))); 94 | } 95 | let never_rx = if case.never.is_empty() { 96 | None 97 | } else { 98 | match Regex::new(&case.never) { 99 | Ok(r) => Some(r), 100 | Err(e) => return Err(io::Error::other(e)), 101 | } 102 | }; 103 | let mut tries = 0; 104 | let fname = loop { 105 | let fname_ = if case.filename.is_empty() { 106 | random_alnum(&mut rng, 1, 16, true) 107 | } else { 108 | fuzz_filename(&mut rng, &case.filename) 109 | }; 110 | if (case.not_justfile && is_justfile_filename(&fname_)) 111 | || never_rx 112 | .as_ref() 113 | .is_some_and(|r| r.is_match(&fname_).unwrap()) 114 | { 115 | if tries >= 20 { 116 | return Err(io::Error::other(format!( 117 | "Failed to find a suitable filename for {:?} after {} tries", 118 | &case, tries 119 | ))); 120 | } 121 | tries += 1; 122 | continue; 123 | } 124 | break fname_; 125 | }; 126 | let actual_file = tempdirs 127 | .iter() 128 | .find_map(|t| { 129 | let pth = t.path().join(&fname); 130 | if pth.exists() { None } else { Some(pth) } 131 | }) 132 | .unwrap_or_else(|| { 133 | tempdirs.push(tempdir().unwrap()); 134 | tempdirs[tempdirs.len() - 1].path().join(&fname) 135 | }); 136 | let mut testfile = File::create_new(&actual_file)?; 137 | testfile.write_all(case.content.as_bytes())?; 138 | file2case.insert(actual_file.into_os_string().into_string().unwrap(), case); 139 | } 140 | 141 | let ftdetect_results = tempdirs[0].path().join("ftdetect_results.txt"); 142 | 143 | let mut args = vec!["-R", "-S", "batch_ftdetect_res.vim"]; 144 | args.extend(file2case.keys().map(|s| s.as_str())); 145 | run_vim(args, &ftdetect_results, test_home.path(), &interrupted)?; 146 | 147 | let ftdetections = fs::read_to_string(ftdetect_results)?; 148 | 149 | let mut current_key = ""; 150 | for line in ftdetections.lines() { 151 | if line.is_empty() { 152 | continue; 153 | } else if !current_key.is_empty() { 154 | let filetype = match line.split_once("filetype=") { 155 | Some((_, ft)) => ft, 156 | None => { 157 | return Err(io::Error::other(format!( 158 | "expected to find \"filetype=\" in line: {line:?}" 159 | ))); 160 | } 161 | }; 162 | let case = &file2case[current_key]; 163 | if (filetype == "just" && !case.not_justfile) || (case.not_justfile && filetype != "just") { 164 | passed += 1; 165 | } else { 166 | eprintln!( 167 | "TEST FAILED: {case:?} (file {current_key}): unexpectedly detected as '{filetype}'" 168 | ); 169 | } 170 | current_key = ""; 171 | } else { 172 | current_key = line; 173 | } 174 | } 175 | 176 | if passed == total { 177 | eprintln!("[\u{2713}] {passed}/{total} ftdetect tests passed."); 178 | Ok(()) 179 | } else { 180 | Err(io::Error::other(format!( 181 | "{}/{total} tests failed.", 182 | total - passed 183 | ))) 184 | } 185 | } 186 | --------------------------------------------------------------------------------