├── .github └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── completions ├── runme.bash ├── runme.fish ├── runme.ps1 └── runme.zsh ├── src └── main.rs └── tests ├── cli.rs ├── compgen.rs ├── create.rs ├── fixtures.rs ├── interrupt.rs ├── macros.rs ├── runmefile.rs ├── snapshots ├── integration__compgen__compgen.snap ├── integration__compgen__compgen_choice_fn.snap ├── integration__compgen__compgen_help.snap ├── integration__compgen__compgen_help_tag.snap ├── integration__compgen__compgen_help_tag2.snap ├── integration__compgen__compgen_option_choices.snap ├── integration__compgen__compgen_option_choices2.snap ├── integration__compgen__compgen_option_choices3.snap ├── integration__compgen__compgen_positional.snap ├── integration__compgen__compgen_positional_arg.snap ├── integration__compgen__compgen_positional_arg2.snap ├── integration__compgen__compgen_positional_choices.snap └── integration__compgen__compgen_subcommand.snap ├── spec.sh └── tests.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - main 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | jobs: 16 | all: 17 | name: All 18 | 19 | strategy: 20 | matrix: 21 | os: 22 | - ubuntu-latest 23 | - macos-latest 24 | - windows-latest 25 | 26 | runs-on: ${{matrix.os}} 27 | 28 | env: 29 | RUSTFLAGS: --deny warnings 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | 34 | - name: Install Rust Toolchain Components 35 | uses: actions-rs/toolchain@v1 36 | with: 37 | components: clippy, rustfmt 38 | override: true 39 | toolchain: stable 40 | 41 | - uses: Swatinem/rust-cache@v1 42 | 43 | - name: Clippy 44 | run: cargo clippy --all --all-targets 45 | 46 | - name: Format 47 | run: cargo fmt --all --check 48 | 49 | - name: Test 50 | run: cargo test --all -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v[0-9]+.[0-9]+.[0-9]+* 7 | 8 | jobs: 9 | release: 10 | name: Publish to Github Releases 11 | permissions: 12 | contents: write 13 | outputs: 14 | rc: ${{ steps.check-tag.outputs.rc }} 15 | 16 | strategy: 17 | matrix: 18 | include: 19 | - target: aarch64-unknown-linux-musl 20 | os: ubuntu-latest 21 | use-cross: true 22 | cargo-flags: "" 23 | - target: aarch64-apple-darwin 24 | os: macos-latest 25 | use-cross: true 26 | cargo-flags: "" 27 | - target: aarch64-pc-windows-msvc 28 | os: windows-latest 29 | use-cross: true 30 | cargo-flags: "" 31 | - target: x86_64-apple-darwin 32 | os: macos-latest 33 | cargo-flags: "" 34 | - target: x86_64-pc-windows-msvc 35 | os: windows-latest 36 | cargo-flags: "" 37 | - target: x86_64-unknown-linux-musl 38 | os: ubuntu-latest 39 | use-cross: true 40 | cargo-flags: "" 41 | - target: i686-unknown-linux-musl 42 | os: ubuntu-latest 43 | use-cross: true 44 | cargo-flags: "" 45 | - target: i686-pc-windows-msvc 46 | os: windows-latest 47 | use-cross: true 48 | cargo-flags: "" 49 | runs-on: ${{matrix.os}} 50 | 51 | steps: 52 | - uses: actions/checkout@v2 53 | 54 | - name: Check Tag 55 | id: check-tag 56 | shell: bash 57 | run: | 58 | tag=${GITHUB_REF##*/} 59 | echo "::set-output name=version::$tag" 60 | if [[ "$tag" =~ [0-9]+.[0-9]+.[0-9]+$ ]]; then 61 | echo "::set-output name=rc::false" 62 | else 63 | echo "::set-output name=rc::true" 64 | fi 65 | 66 | 67 | - name: Install Rust Toolchain Components 68 | uses: actions-rs/toolchain@v1 69 | with: 70 | override: true 71 | target: ${{ matrix.target }} 72 | toolchain: stable 73 | profile: minimal # minimal component installation (ie, no documentation) 74 | 75 | - name: Show Version Information (Rust, cargo, GCC) 76 | shell: bash 77 | run: | 78 | gcc --version || true 79 | rustup -V 80 | rustup toolchain list 81 | rustup default 82 | cargo -V 83 | rustc -V 84 | 85 | - name: Build 86 | uses: actions-rs/cargo@v1 87 | with: 88 | use-cross: ${{ matrix.use-cross }} 89 | command: build 90 | args: --locked --release --target=${{ matrix.target }} ${{ matrix.cargo-flags }} 91 | 92 | - name: Build Archive 93 | shell: bash 94 | id: package 95 | env: 96 | target: ${{ matrix.target }} 97 | version: ${{ steps.check-tag.outputs.version }} 98 | run: | 99 | set -euxo pipefail 100 | 101 | bin=${GITHUB_REPOSITORY##*/} 102 | src=`pwd` 103 | dist=$src/dist 104 | name=$bin-$version-$target 105 | executable=target/$target/release/$bin 106 | 107 | if [[ "$RUNNER_OS" == "Windows" ]]; then 108 | executable=$executable.exe 109 | fi 110 | 111 | mkdir $dist 112 | cp $executable $dist 113 | cd $dist 114 | 115 | if [[ "$RUNNER_OS" == "Windows" ]]; then 116 | archive=$dist/$name.zip 117 | 7z a $archive * 118 | echo "::set-output name=archive::`pwd -W`/$name.zip" 119 | else 120 | archive=$dist/$name.tar.gz 121 | tar czf $archive * 122 | echo "::set-output name=archive::$archive" 123 | fi 124 | 125 | - name: Publish Archive 126 | uses: softprops/action-gh-release@v0.1.5 127 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 128 | with: 129 | draft: false 130 | files: ${{ steps.package.outputs.archive }} 131 | prerelease: ${{ steps.check-tag.outputs.rc == 'true' }} 132 | env: 133 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 134 | 135 | publish-crate: 136 | name: Publish to crates.io 137 | if: ${{ needs.release.outputs.rc == 'false' }} 138 | runs-on: ubuntu-latest 139 | needs: release 140 | steps: 141 | - uses: actions/checkout@v2 142 | - uses: actions-rs/toolchain@v1 143 | with: 144 | profile: minimal 145 | toolchain: stable 146 | - name: Publish 147 | 148 | env: 149 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_API_TOKEN }} 150 | run: cargo publish -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.20" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.69" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" 19 | 20 | [[package]] 21 | name = "argc" 22 | version = "0.14.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8cfe402590572c3fd2eecfcca5ef8620c33693fe70eb6594ae917a9d5e432bd7" 25 | dependencies = [ 26 | "anyhow", 27 | "clap", 28 | "convert_case", 29 | "either", 30 | "indexmap", 31 | "nom", 32 | ] 33 | 34 | [[package]] 35 | name = "assert_cmd" 36 | version = "2.0.8" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "9834fcc22e0874394a010230586367d4a3e9f11b560f469262678547e1d2575e" 39 | dependencies = [ 40 | "bstr", 41 | "doc-comment", 42 | "predicates", 43 | "predicates-core", 44 | "predicates-tree", 45 | "wait-timeout", 46 | ] 47 | 48 | [[package]] 49 | name = "assert_fs" 50 | version = "1.0.10" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "d94b2a3f3786ff2996a98afbd6b4e5b7e890d685ccf67577f508ee2342c71cc9" 53 | dependencies = [ 54 | "doc-comment", 55 | "globwalk", 56 | "predicates", 57 | "predicates-core", 58 | "predicates-tree", 59 | "tempfile", 60 | ] 61 | 62 | [[package]] 63 | name = "autocfg" 64 | version = "1.1.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 67 | 68 | [[package]] 69 | name = "bitflags" 70 | version = "1.3.2" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 73 | 74 | [[package]] 75 | name = "bstr" 76 | version = "1.3.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "5ffdb39cb703212f3c11973452c2861b972f757b021158f3516ba10f2fa8b2c1" 79 | dependencies = [ 80 | "memchr", 81 | "once_cell", 82 | "regex-automata", 83 | "serde", 84 | ] 85 | 86 | [[package]] 87 | name = "cc" 88 | version = "1.0.79" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 91 | 92 | [[package]] 93 | name = "cfg-if" 94 | version = "1.0.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 97 | 98 | [[package]] 99 | name = "clap" 100 | version = "4.1.8" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" 103 | dependencies = [ 104 | "bitflags", 105 | "clap_lex", 106 | "is-terminal", 107 | "strsim", 108 | "termcolor", 109 | ] 110 | 111 | [[package]] 112 | name = "clap_lex" 113 | version = "0.3.2" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" 116 | dependencies = [ 117 | "os_str_bytes", 118 | ] 119 | 120 | [[package]] 121 | name = "console" 122 | version = "0.15.5" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" 125 | dependencies = [ 126 | "encode_unicode", 127 | "lazy_static", 128 | "libc", 129 | "windows-sys 0.42.0", 130 | ] 131 | 132 | [[package]] 133 | name = "convert_case" 134 | version = "0.6.0" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" 137 | dependencies = [ 138 | "unicode-segmentation", 139 | ] 140 | 141 | [[package]] 142 | name = "ctrlc" 143 | version = "3.2.5" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" 146 | dependencies = [ 147 | "nix", 148 | "windows-sys 0.45.0", 149 | ] 150 | 151 | [[package]] 152 | name = "difflib" 153 | version = "0.4.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 156 | 157 | [[package]] 158 | name = "doc-comment" 159 | version = "0.3.3" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 162 | 163 | [[package]] 164 | name = "either" 165 | version = "1.8.1" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 168 | 169 | [[package]] 170 | name = "encode_unicode" 171 | version = "0.3.6" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 174 | 175 | [[package]] 176 | name = "errno" 177 | version = "0.2.8" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 180 | dependencies = [ 181 | "errno-dragonfly", 182 | "libc", 183 | "winapi", 184 | ] 185 | 186 | [[package]] 187 | name = "errno-dragonfly" 188 | version = "0.1.2" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 191 | dependencies = [ 192 | "cc", 193 | "libc", 194 | ] 195 | 196 | [[package]] 197 | name = "fastrand" 198 | version = "1.9.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 201 | dependencies = [ 202 | "instant", 203 | ] 204 | 205 | [[package]] 206 | name = "float-cmp" 207 | version = "0.9.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 210 | dependencies = [ 211 | "num-traits", 212 | ] 213 | 214 | [[package]] 215 | name = "fnv" 216 | version = "1.0.7" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 219 | 220 | [[package]] 221 | name = "futures" 222 | version = "0.3.27" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" 225 | dependencies = [ 226 | "futures-channel", 227 | "futures-core", 228 | "futures-executor", 229 | "futures-io", 230 | "futures-sink", 231 | "futures-task", 232 | "futures-util", 233 | ] 234 | 235 | [[package]] 236 | name = "futures-channel" 237 | version = "0.3.27" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" 240 | dependencies = [ 241 | "futures-core", 242 | "futures-sink", 243 | ] 244 | 245 | [[package]] 246 | name = "futures-core" 247 | version = "0.3.27" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" 250 | 251 | [[package]] 252 | name = "futures-executor" 253 | version = "0.3.27" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" 256 | dependencies = [ 257 | "futures-core", 258 | "futures-task", 259 | "futures-util", 260 | ] 261 | 262 | [[package]] 263 | name = "futures-io" 264 | version = "0.3.27" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" 267 | 268 | [[package]] 269 | name = "futures-macro" 270 | version = "0.3.27" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" 273 | dependencies = [ 274 | "proc-macro2", 275 | "quote", 276 | "syn", 277 | ] 278 | 279 | [[package]] 280 | name = "futures-sink" 281 | version = "0.3.27" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" 284 | 285 | [[package]] 286 | name = "futures-task" 287 | version = "0.3.27" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" 290 | 291 | [[package]] 292 | name = "futures-timer" 293 | version = "3.0.2" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" 296 | 297 | [[package]] 298 | name = "futures-util" 299 | version = "0.3.27" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" 302 | dependencies = [ 303 | "futures-channel", 304 | "futures-core", 305 | "futures-io", 306 | "futures-macro", 307 | "futures-sink", 308 | "futures-task", 309 | "memchr", 310 | "pin-project-lite", 311 | "pin-utils", 312 | "slab", 313 | ] 314 | 315 | [[package]] 316 | name = "globset" 317 | version = "0.4.10" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" 320 | dependencies = [ 321 | "aho-corasick", 322 | "bstr", 323 | "fnv", 324 | "log", 325 | "regex", 326 | ] 327 | 328 | [[package]] 329 | name = "globwalk" 330 | version = "0.8.1" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" 333 | dependencies = [ 334 | "bitflags", 335 | "ignore", 336 | "walkdir", 337 | ] 338 | 339 | [[package]] 340 | name = "hashbrown" 341 | version = "0.12.3" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 344 | 345 | [[package]] 346 | name = "hermit-abi" 347 | version = "0.3.1" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 350 | 351 | [[package]] 352 | name = "ignore" 353 | version = "0.4.20" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" 356 | dependencies = [ 357 | "globset", 358 | "lazy_static", 359 | "log", 360 | "memchr", 361 | "regex", 362 | "same-file", 363 | "thread_local", 364 | "walkdir", 365 | "winapi-util", 366 | ] 367 | 368 | [[package]] 369 | name = "indexmap" 370 | version = "1.9.2" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" 373 | dependencies = [ 374 | "autocfg", 375 | "hashbrown", 376 | ] 377 | 378 | [[package]] 379 | name = "insta" 380 | version = "1.28.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "fea5b3894afe466b4bcf0388630fc15e11938a6074af0cd637c825ba2ec8a099" 383 | dependencies = [ 384 | "console", 385 | "lazy_static", 386 | "linked-hash-map", 387 | "similar", 388 | "yaml-rust", 389 | ] 390 | 391 | [[package]] 392 | name = "instant" 393 | version = "0.1.12" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 396 | dependencies = [ 397 | "cfg-if", 398 | ] 399 | 400 | [[package]] 401 | name = "io-lifetimes" 402 | version = "1.0.6" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" 405 | dependencies = [ 406 | "libc", 407 | "windows-sys 0.45.0", 408 | ] 409 | 410 | [[package]] 411 | name = "is-terminal" 412 | version = "0.4.4" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" 415 | dependencies = [ 416 | "hermit-abi", 417 | "io-lifetimes", 418 | "rustix", 419 | "windows-sys 0.45.0", 420 | ] 421 | 422 | [[package]] 423 | name = "itertools" 424 | version = "0.10.5" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 427 | dependencies = [ 428 | "either", 429 | ] 430 | 431 | [[package]] 432 | name = "lazy_static" 433 | version = "1.4.0" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 436 | 437 | [[package]] 438 | name = "libc" 439 | version = "0.2.140" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" 442 | 443 | [[package]] 444 | name = "linked-hash-map" 445 | version = "0.5.6" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 448 | 449 | [[package]] 450 | name = "linux-raw-sys" 451 | version = "0.1.4" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 454 | 455 | [[package]] 456 | name = "log" 457 | version = "0.4.17" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 460 | dependencies = [ 461 | "cfg-if", 462 | ] 463 | 464 | [[package]] 465 | name = "memchr" 466 | version = "2.5.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 469 | 470 | [[package]] 471 | name = "minimal-lexical" 472 | version = "0.2.1" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 475 | 476 | [[package]] 477 | name = "nix" 478 | version = "0.26.2" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" 481 | dependencies = [ 482 | "bitflags", 483 | "cfg-if", 484 | "libc", 485 | "static_assertions", 486 | ] 487 | 488 | [[package]] 489 | name = "nom" 490 | version = "7.1.3" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 493 | dependencies = [ 494 | "memchr", 495 | "minimal-lexical", 496 | ] 497 | 498 | [[package]] 499 | name = "normalize-line-endings" 500 | version = "0.3.0" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 503 | 504 | [[package]] 505 | name = "num-traits" 506 | version = "0.2.15" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 509 | dependencies = [ 510 | "autocfg", 511 | ] 512 | 513 | [[package]] 514 | name = "once_cell" 515 | version = "1.17.1" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 518 | 519 | [[package]] 520 | name = "os_str_bytes" 521 | version = "6.4.1" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" 524 | 525 | [[package]] 526 | name = "pin-project-lite" 527 | version = "0.2.9" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 530 | 531 | [[package]] 532 | name = "pin-utils" 533 | version = "0.1.0" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 536 | 537 | [[package]] 538 | name = "predicates" 539 | version = "2.1.5" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" 542 | dependencies = [ 543 | "difflib", 544 | "float-cmp", 545 | "itertools", 546 | "normalize-line-endings", 547 | "predicates-core", 548 | "regex", 549 | ] 550 | 551 | [[package]] 552 | name = "predicates-core" 553 | version = "1.0.5" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "72f883590242d3c6fc5bf50299011695fa6590c2c70eac95ee1bdb9a733ad1a2" 556 | 557 | [[package]] 558 | name = "predicates-tree" 559 | version = "1.0.7" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "54ff541861505aabf6ea722d2131ee980b8276e10a1297b94e896dd8b621850d" 562 | dependencies = [ 563 | "predicates-core", 564 | "termtree", 565 | ] 566 | 567 | [[package]] 568 | name = "proc-macro2" 569 | version = "1.0.52" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" 572 | dependencies = [ 573 | "unicode-ident", 574 | ] 575 | 576 | [[package]] 577 | name = "quote" 578 | version = "1.0.24" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "50686e0021c4136d1d453b2dfe059902278681512a34d4248435dc34b6b5c8ec" 581 | dependencies = [ 582 | "proc-macro2", 583 | ] 584 | 585 | [[package]] 586 | name = "redox_syscall" 587 | version = "0.2.16" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 590 | dependencies = [ 591 | "bitflags", 592 | ] 593 | 594 | [[package]] 595 | name = "regex" 596 | version = "1.7.1" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" 599 | dependencies = [ 600 | "aho-corasick", 601 | "memchr", 602 | "regex-syntax", 603 | ] 604 | 605 | [[package]] 606 | name = "regex-automata" 607 | version = "0.1.10" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 610 | 611 | [[package]] 612 | name = "regex-syntax" 613 | version = "0.6.28" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 616 | 617 | [[package]] 618 | name = "rstest" 619 | version = "0.15.0" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "e9c9dc66cc29792b663ffb5269be669f1613664e69ad56441fdb895c2347b930" 622 | dependencies = [ 623 | "futures", 624 | "futures-timer", 625 | "rstest_macros", 626 | "rustc_version", 627 | ] 628 | 629 | [[package]] 630 | name = "rstest_macros" 631 | version = "0.14.0" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "5015e68a0685a95ade3eee617ff7101ab6a3fc689203101ca16ebc16f2b89c66" 634 | dependencies = [ 635 | "cfg-if", 636 | "proc-macro2", 637 | "quote", 638 | "rustc_version", 639 | "syn", 640 | ] 641 | 642 | [[package]] 643 | name = "runme" 644 | version = "0.5.0" 645 | dependencies = [ 646 | "anyhow", 647 | "argc", 648 | "assert_cmd", 649 | "assert_fs", 650 | "clap", 651 | "ctrlc", 652 | "either", 653 | "insta", 654 | "libc", 655 | "predicates", 656 | "rstest", 657 | "which", 658 | ] 659 | 660 | [[package]] 661 | name = "rustc_version" 662 | version = "0.4.0" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 665 | dependencies = [ 666 | "semver", 667 | ] 668 | 669 | [[package]] 670 | name = "rustix" 671 | version = "0.36.9" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" 674 | dependencies = [ 675 | "bitflags", 676 | "errno", 677 | "io-lifetimes", 678 | "libc", 679 | "linux-raw-sys", 680 | "windows-sys 0.45.0", 681 | ] 682 | 683 | [[package]] 684 | name = "same-file" 685 | version = "1.0.6" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 688 | dependencies = [ 689 | "winapi-util", 690 | ] 691 | 692 | [[package]] 693 | name = "semver" 694 | version = "1.0.17" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" 697 | 698 | [[package]] 699 | name = "serde" 700 | version = "1.0.155" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "71f2b4817415c6d4210bfe1c7bfcf4801b2d904cb4d0e1a8fdb651013c9e86b8" 703 | 704 | [[package]] 705 | name = "similar" 706 | version = "2.2.1" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" 709 | 710 | [[package]] 711 | name = "slab" 712 | version = "0.4.8" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" 715 | dependencies = [ 716 | "autocfg", 717 | ] 718 | 719 | [[package]] 720 | name = "static_assertions" 721 | version = "1.1.0" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 724 | 725 | [[package]] 726 | name = "strsim" 727 | version = "0.10.0" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 730 | 731 | [[package]] 732 | name = "syn" 733 | version = "1.0.109" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 736 | dependencies = [ 737 | "proc-macro2", 738 | "quote", 739 | "unicode-ident", 740 | ] 741 | 742 | [[package]] 743 | name = "tempfile" 744 | version = "3.4.0" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" 747 | dependencies = [ 748 | "cfg-if", 749 | "fastrand", 750 | "redox_syscall", 751 | "rustix", 752 | "windows-sys 0.42.0", 753 | ] 754 | 755 | [[package]] 756 | name = "termcolor" 757 | version = "1.2.0" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 760 | dependencies = [ 761 | "winapi-util", 762 | ] 763 | 764 | [[package]] 765 | name = "termtree" 766 | version = "0.4.0" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8" 769 | 770 | [[package]] 771 | name = "thread_local" 772 | version = "1.1.7" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 775 | dependencies = [ 776 | "cfg-if", 777 | "once_cell", 778 | ] 779 | 780 | [[package]] 781 | name = "unicode-ident" 782 | version = "1.0.8" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 785 | 786 | [[package]] 787 | name = "unicode-segmentation" 788 | version = "1.10.1" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 791 | 792 | [[package]] 793 | name = "wait-timeout" 794 | version = "0.2.0" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 797 | dependencies = [ 798 | "libc", 799 | ] 800 | 801 | [[package]] 802 | name = "walkdir" 803 | version = "2.3.2" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 806 | dependencies = [ 807 | "same-file", 808 | "winapi", 809 | "winapi-util", 810 | ] 811 | 812 | [[package]] 813 | name = "which" 814 | version = "4.4.0" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" 817 | dependencies = [ 818 | "either", 819 | "libc", 820 | "once_cell", 821 | ] 822 | 823 | [[package]] 824 | name = "winapi" 825 | version = "0.3.9" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 828 | dependencies = [ 829 | "winapi-i686-pc-windows-gnu", 830 | "winapi-x86_64-pc-windows-gnu", 831 | ] 832 | 833 | [[package]] 834 | name = "winapi-i686-pc-windows-gnu" 835 | version = "0.4.0" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 838 | 839 | [[package]] 840 | name = "winapi-util" 841 | version = "0.1.5" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 844 | dependencies = [ 845 | "winapi", 846 | ] 847 | 848 | [[package]] 849 | name = "winapi-x86_64-pc-windows-gnu" 850 | version = "0.4.0" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 853 | 854 | [[package]] 855 | name = "windows-sys" 856 | version = "0.42.0" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 859 | dependencies = [ 860 | "windows_aarch64_gnullvm", 861 | "windows_aarch64_msvc", 862 | "windows_i686_gnu", 863 | "windows_i686_msvc", 864 | "windows_x86_64_gnu", 865 | "windows_x86_64_gnullvm", 866 | "windows_x86_64_msvc", 867 | ] 868 | 869 | [[package]] 870 | name = "windows-sys" 871 | version = "0.45.0" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 874 | dependencies = [ 875 | "windows-targets", 876 | ] 877 | 878 | [[package]] 879 | name = "windows-targets" 880 | version = "0.42.1" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" 883 | dependencies = [ 884 | "windows_aarch64_gnullvm", 885 | "windows_aarch64_msvc", 886 | "windows_i686_gnu", 887 | "windows_i686_msvc", 888 | "windows_x86_64_gnu", 889 | "windows_x86_64_gnullvm", 890 | "windows_x86_64_msvc", 891 | ] 892 | 893 | [[package]] 894 | name = "windows_aarch64_gnullvm" 895 | version = "0.42.1" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 898 | 899 | [[package]] 900 | name = "windows_aarch64_msvc" 901 | version = "0.42.1" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 904 | 905 | [[package]] 906 | name = "windows_i686_gnu" 907 | version = "0.42.1" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 910 | 911 | [[package]] 912 | name = "windows_i686_msvc" 913 | version = "0.42.1" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 916 | 917 | [[package]] 918 | name = "windows_x86_64_gnu" 919 | version = "0.42.1" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 922 | 923 | [[package]] 924 | name = "windows_x86_64_gnullvm" 925 | version = "0.42.1" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 928 | 929 | [[package]] 930 | name = "windows_x86_64_msvc" 931 | version = "0.42.1" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 934 | 935 | [[package]] 936 | name = "yaml-rust" 937 | version = "0.4.5" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 940 | dependencies = [ 941 | "linked-hash-map", 942 | ] 943 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "runme" 3 | version = "0.5.0" 4 | edition = "2021" 5 | authors = ["sigoden "] 6 | description = "A shell-script based task runner" 7 | license = "MIT OR Apache-2.0" 8 | homepage = "https://github.com/sigoden/runme" 9 | repository = "https://github.com/sigoden/runme" 10 | autotests = false 11 | categories = ["command-line-utilities"] 12 | keywords = ["command-line", "task-runner", "task-automation", "build-tool"] 13 | 14 | [dependencies] 15 | argc = "0.14" 16 | anyhow = "1" 17 | which = "4.2" 18 | either = "1.8" 19 | clap = { version = "4.0", features = ["string"]} 20 | ctrlc = "3.2" 21 | 22 | 23 | [dev-dependencies] 24 | insta = "1.15" 25 | assert_cmd = "2" 26 | assert_fs = "1" 27 | rstest = "0.15" 28 | predicates = "2" 29 | [target.'cfg(unix)'.dev-dependencies] 30 | libc = "0.2" 31 | 32 | [profile.release] 33 | lto = true 34 | strip = true 35 | opt-level = "z" 36 | 37 | [[test]] 38 | name = "integration" 39 | path = "tests/tests.rs" 40 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) sigoden(2022) 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 | # Runme (Deprecated, use [argc](https://github.com/sigoden/argc) instead) 2 | 3 | [![CI](https://github.com/sigoden/runme/actions/workflows/ci.yaml/badge.svg)](https://github.com/sigoden/runme/actions/workflows/ci.yaml) 4 | [![Crates](https://img.shields.io/crates/v/runme.svg)](https://crates.io/crates/runme) 5 | 6 | A shell-script based task runner. 7 | 8 | ![demo](https://user-images.githubusercontent.com/4012553/224712229-fdb08a5b-f04a-4b32-85b5-aae020f87096.gif) 9 | 10 | ## Install 11 | 12 | ### With cargo 13 | 14 | ``` 15 | cargo install --force runme 16 | ``` 17 | 18 | ### Binaries on macOS, Linux, Windows 19 | 20 | Download from [Github Releases](https://github.com/sigoden/runme/releases), unzip and add runme to your $PATH. 21 | 22 | ### GitHub Actions 23 | 24 | [extractions/setup-crate](https://github.com/marketplace/actions/setup-crate) can be used to install just in a GitHub Actions workflow. 25 | 26 | ```yaml 27 | - uses: extractions/setup-crate@v1 28 | with: 29 | owner: sigoden 30 | name: runme 31 | ``` 32 | 33 | ## Get Started 34 | 35 | First, run `runme --runme-create build test` to quickly create boilerplate `Runmefile.sh` 36 | 37 | ```sh 38 | #!/usr/bin/env bash 39 | 40 | # @cmd build project 41 | # @alias b 42 | build() { 43 | echo Run build 44 | } 45 | 46 | # @cmd test project 47 | test() { 48 | echo Run test 49 | } 50 | 51 | eval "$(runme --runme-eval "$0" "$@")" 52 | ``` 53 | 54 | > To define a new task, simply create the bash function and add the [`@cmd`](https://github.com/sigoden/argc#cmd) above it. **Task is just function** 55 | 56 | ``` 57 | $ runme -h 58 | USAGE: Runmefile.sh 59 | 60 | COMMANDS: 61 | build build project [aliases: b] 62 | test test project 63 | 64 | $ runme test 65 | Run test 66 | $ runme b 67 | Run build 68 | ``` 69 | 70 | > Runme uses [`argc`](https://github.com/sigoden/argc) to parse Runmefile. 71 | 72 | ## Features 73 | 74 | ### Cross platform 75 | 76 | `runme` binary is available in linux, macos, and windows. 77 | 78 | `runme` depends on bash which already built into linux/macos. In windows, runme automatically locates and uses bash that comes with **git** by default. 79 | 80 | Gnu tools like `ls`, `rm`, `grep`, `sed`, `awk`... also provided with bash, so you can uses them freely and confidently in the Runmefile. 81 | 82 | ### Task parameters 83 | 84 | Use [comment tags](https://github.com/sigoden/argc#comment-tags) to define task parameters. 85 | 86 | - [`@arg`](https://github.com/sigoden/argc#arg): define positional argument 87 | - [`@option`](https://github.com/sigoden/argc#option): define option argument 88 | - [`@flag`](https://github.com/sigoden/argc#flag): define flag argument 89 | 90 | ```sh 91 | # @cmd Download a file 92 | # @alias d 93 | # @flag -f --force Override existing file 94 | # @option -t --tries Set number of retries to NUM 95 | # @arg source! Url to download from 96 | # @arg target Save file to 97 | download() { 98 | echo "cmd: download" 99 | echo "flag: --force $argc_force" 100 | echo "option: --tries $argc_tries" 101 | echo "arg: source $argc_source" 102 | echo "arg: target $argc_target" 103 | } 104 | ``` 105 | 106 | ``` 107 | $ runme download -h 108 | Download a file 109 | 110 | USAGE: Runmefile.sh download [OPTIONS] [TARGET] 111 | 112 | ARGS: 113 | Url to download from 114 | [TARGET] Save file to 115 | 116 | OPTIONS: 117 | -f, --force Override existing file 118 | -t, --tries Set number of retries to NUM 119 | -h, --help Print help information 120 | ``` 121 | 122 | ``` 123 | $ runme download -f --tries 3 from.txt to.txt 124 | cmd: download 125 | flag: --force 1 126 | option: --tries 3 127 | arg: source from.txt 128 | arg: target to.txt 129 | ``` 130 | 131 | You can also use shell variables to access task parameters. 132 | 133 | ```sh 134 | # @cmd 135 | run() { 136 | echo $2 $1 $# 137 | } 138 | ``` 139 | ``` 140 | $ runme run foo bar 141 | bar foo 2 142 | ``` 143 | 144 | ### Task aliases 145 | 146 | ```sh 147 | # @cmd 148 | # @alias t,tst 149 | test() { 150 | echo "Test..." 151 | } 152 | ``` 153 | 154 | ``` 155 | $ runme t 156 | Test... 157 | ``` 158 | 159 | ### Task dependencies 160 | 161 | Dependencies are established by function calling. 162 | 163 | ```sh 164 | # @cmd 165 | bar() { foo; 166 | echo bar 167 | baz; } 168 | 169 | # @cmd 170 | foo() { 171 | echo foo 172 | } 173 | 174 | # @cmd 175 | baz() { 176 | echo baz 177 | } 178 | ``` 179 | 180 | ``` 181 | $ runme bar 182 | foo 183 | bar 184 | baz 185 | ``` 186 | 187 | ### Task group 188 | 189 | Tasks can be semantically grouped with `_`, `-`, `@`, `.`, `:`. 190 | 191 | ```sh 192 | # @cmd 193 | test@unit() {} 194 | # @cmd 195 | test@bin() {} 196 | 197 | # @cmd 198 | app.build() {} 199 | # @cmd 200 | app.test() {} 201 | ``` 202 | 203 | ### Default task 204 | 205 | When `runme` is invoked without a task name, it runs the `main` function. 206 | If the `main` function does not exist, runme will print help information. 207 | 208 | ```sh 209 | main() { 210 | foo 211 | } 212 | 213 | # @cmd 214 | foo() { 215 | echo foo 216 | } 217 | ``` 218 | 219 | ``` 220 | $ runme 221 | foo 222 | ``` 223 | 224 | ## Advanced Topics 225 | 226 | ### Completions 227 | 228 | [Shell completion scripts](completions) are available for bash/zsh/fish/powershell. 229 | 230 | ### Customize shell path 231 | 232 | You can use environment variable `RUNME_SHELL` to customize shell path. 233 | 234 | ``` 235 | RUNME_SHELL="C:\\Program Files\\Git\\bin\\bash.exe" 236 | ``` 237 | ### Customize script name 238 | 239 | By default, runme searches for runme script file of the following: 240 | 241 | - Runmefile.sh or Runmefile 242 | - runmefile.sh or runmefile 243 | - RUNMEFILE.sh or RUNMEFILE 244 | 245 | You can use environment variable `RUNME_SCRIPT` to custom script name. 246 | 247 | ``` 248 | RUNME_SCRIPT=taskfile.sh 249 | ``` 250 | ## License 251 | 252 | Copyright (c) 2022 runme-developers. 253 | 254 | runme is made available under the terms of either the MIT License or the Apache License 2.0, at your option. 255 | 256 | See the LICENSE-APACHE and LICENSE-MIT files for license details. -------------------------------------------------------------------------------- /completions/runme.bash: -------------------------------------------------------------------------------- 1 | # Bash completion for runme 2 | 3 | _runme_completion() { 4 | cur="${COMP_WORDS[COMP_CWORD]}" 5 | COMPREPLY=() 6 | local scriptfile=$(runme --runme-file 2>/dev/null) 7 | if [[ ! -f "$scriptfile" ]]; then 8 | return 0 9 | fi 10 | local line=${COMP_LINE:${#COMP_WORDS[0]}} 11 | local IFS=$'\n' 12 | local compgen_values=($(runme --runme-compgen "$scriptfile" "$line" 2>/dev/null)) 13 | local option_values=() 14 | local value_kind=0 15 | local candicates=() 16 | for item in ${compgen_values[@]}; do 17 | if [[ "$item" == '-'* ]]; then 18 | option_values+=( "$item" ) 19 | elif [[ "$item" == \`*\` ]]; then 20 | local choices=($(runme "${item:1:-1}" 2>/dev/null)) 21 | candicates=( "${candicates[@]}" "${choices[@]}" ) 22 | elif [[ "$item" == '<'* ]]; then 23 | if echo "$item" | grep -qi '...'; then 24 | value_kind=1 25 | elif echo "$item" | grep -qi '\(file\|path\)>\(\.\.\.\)\?'; then 26 | value_kind=2 27 | elif echo "$item" | grep -qi 'dir>\(\.\.\.\)\?'; then 28 | value_kind=3 29 | else 30 | value_kind=9 31 | fi 32 | else 33 | candicates+=( "$item" ) 34 | fi 35 | done 36 | if [[ "$value_kind" == 0 ]]; then 37 | if [[ "${#candicates[@]}" -eq 0 ]]; then 38 | candicates=( "${option_values[@]}" ) 39 | fi 40 | elif [[ "$value_kind" == 1 ]]; then 41 | if [[ "${#candicates[@]}" -eq 0 ]]; then 42 | candicates=( "${option_values[@]}" ) 43 | fi 44 | if [[ "${#candicates[@]}" -eq 0 ]]; then 45 | _filedir 46 | fi 47 | elif [[ "$value_kind" == 2 ]]; then 48 | _filedir 49 | elif [[ "$value_kind" == 3 ]]; then 50 | _filedir -d 51 | fi 52 | if [[ ${#candicates[@]} -gt 0 ]]; then 53 | candicates=($(compgen -W "${candicates[*]}" -- "${cur}")) 54 | if [ ${#candicates[@]} -gt 0 ]; then 55 | COMPREPLY=( "${COMPREPLY[@]}" $(printf '%q\n' "${candicates[@]}")) 56 | fi 57 | fi 58 | } 59 | 60 | complete -F _runme_completion runme 61 | 62 | # Perform tilde (~) completion 63 | # @return True (0) if completion needs further processing, 64 | # False (> 0) if tilde is followed by a valid username, completions 65 | # are put in COMPREPLY and no further processing is necessary. 66 | _tilde() 67 | { 68 | local result=0 69 | if [[ ${1-} == \~* && $1 != */* ]]; then 70 | # Try generate ~username completions 71 | COMPREPLY=($(compgen -P '~' -u -- "${1#\~}")) 72 | result=${#COMPREPLY[@]} 73 | # 2>/dev/null for direct invocation, e.g. in the _tilde unit test 74 | ((result > 0)) && compopt -o filenames 2>/dev/null 75 | fi 76 | return "$result" 77 | } 78 | 79 | # This function quotes the argument in a way so that readline dequoting 80 | # results in the original argument. This is necessary for at least 81 | # `compgen' which requires its arguments quoted/escaped: 82 | # 83 | # $ ls "a'b/" 84 | # c 85 | # $ compgen -f "a'b/" # Wrong, doesn't return output 86 | # $ compgen -f "a\'b/" # Good 87 | # a\'b/c 88 | # 89 | # See also: 90 | # - https://lists.gnu.org/archive/html/bug-bash/2009-03/msg00155.html 91 | # - https://www.mail-archive.com/bash-completion-devel@lists.alioth.debian.org/msg01944.html 92 | # @param $1 Argument to quote 93 | # @param $2 Name of variable to return result to 94 | _quote_readline_by_ref() 95 | { 96 | if [[ $1 == \'* ]]; then 97 | # Leave out first character 98 | printf -v "$2" %s "${1:1}" 99 | else 100 | printf -v "$2" %q "$1" 101 | 102 | # If result becomes quoted like this: $'string', re-evaluate in order 103 | # to drop the additional quoting. See also: 104 | # https://www.mail-archive.com/bash-completion-devel@lists.alioth.debian.org/msg01942.html 105 | if [[ ${!2} == \$\'*\' ]]; then 106 | local value=${!2:2:-1} # Strip beginning $' and ending '. 107 | value=${value//'%'/%%} # Escape % for printf format. 108 | # shellcheck disable=SC2059 109 | printf -v value "$value" # Decode escape sequences of \.... 110 | local "$2" && _comp_upvars -v "$2" "$value" 111 | fi 112 | fi 113 | } # _quote_readline_by_ref() 114 | 115 | # Assign variables one scope above the caller 116 | # Usage: local varname [varname ...] && 117 | # _comp_upvars [-v varname value] | [-aN varname [value ...]] ... 118 | # Available OPTIONS: 119 | # -aN Assign next N values to varname as array 120 | # -v Assign single value to varname 121 | # @return 1 if error occurs 122 | # @see https://fvue.nl/wiki/Bash:_Passing_variables_by_reference 123 | _comp_upvars() 124 | { 125 | if ! (($#)); then 126 | echo "bash_completion: $FUNCNAME: usage: $FUNCNAME" \ 127 | "[-v varname value] | [-aN varname [value ...]] ..." >&2 128 | return 2 129 | fi 130 | while (($#)); do 131 | case $1 in 132 | -a*) 133 | # Error checking 134 | [[ ${1#-a} ]] || { 135 | echo "bash_completion: $FUNCNAME:" \ 136 | "\`$1': missing number specifier" >&2 137 | return 1 138 | } 139 | printf %d "${1#-a}" &>/dev/null || { 140 | echo bash_completion: \ 141 | "$FUNCNAME: \`$1': invalid number specifier" >&2 142 | return 1 143 | } 144 | # Assign array of -aN elements 145 | # shellcheck disable=SC2015,SC2140 # TODO 146 | [[ $2 ]] && unset -v "$2" && eval "$2"=\(\"\$"{@:3:${1#-a}}"\"\) && 147 | shift $((${1#-a} + 2)) || { 148 | echo bash_completion: \ 149 | "$FUNCNAME: \`$1${2+ }$2': missing argument(s)" \ 150 | >&2 151 | return 1 152 | } 153 | ;; 154 | -v) 155 | # Assign single value 156 | # shellcheck disable=SC2015 # TODO 157 | [[ $2 ]] && unset -v "$2" && eval "$2"=\"\$3\" && 158 | shift 3 || { 159 | echo "bash_completion: $FUNCNAME: $1:" \ 160 | "missing argument(s)" >&2 161 | return 1 162 | } 163 | ;; 164 | *) 165 | echo "bash_completion: $FUNCNAME: $1: invalid option" >&2 166 | return 1 167 | ;; 168 | esac 169 | done 170 | } 171 | 172 | # This function performs file and directory completion. It's better than 173 | # simply using 'compgen -f', because it honours spaces in filenames. 174 | # @param $1 If `-d', complete only on directories. Otherwise filter/pick only 175 | # completions with `.$1' and the uppercase version of it as file 176 | # extension. 177 | # 178 | _filedir() 179 | { 180 | local IFS=$'\n' 181 | 182 | _tilde "${cur-}" || return 183 | 184 | local -a toks 185 | local reset arg=${1-} 186 | 187 | if [[ $arg == -d ]]; then 188 | reset=$(shopt -po noglob) 189 | set -o noglob 190 | toks=($(compgen -d -- "${cur-}")) 191 | IFS=' ' 192 | $reset 193 | IFS=$'\n' 194 | else 195 | local quoted 196 | _quote_readline_by_ref "${cur-}" quoted 197 | 198 | # Munge xspec to contain uppercase version too 199 | # https://lists.gnu.org/archive/html/bug-bash/2010-09/msg00036.html 200 | # news://news.gmane.io/4C940E1C.1010304@case.edu 201 | local xspec=${arg:+"!*.@($arg|${arg^^})"} plusdirs=() 202 | 203 | # Use plusdirs to get dir completions if we have a xspec; if we don't, 204 | # there's no need, dirs come along with other completions. Don't use 205 | # plusdirs quite yet if fallback is in use though, in order to not ruin 206 | # the fallback condition with the "plus" dirs. 207 | local opts=(-f -X "$xspec") 208 | [[ $xspec ]] && plusdirs=(-o plusdirs) 209 | [[ ${BASH_COMPLETION_FILEDIR_FALLBACK-${COMP_FILEDIR_FALLBACK-}} || 210 | ! ${plusdirs-} ]] || 211 | opts+=("${plusdirs[@]}") 212 | 213 | reset=$(shopt -po noglob) 214 | set -o noglob 215 | toks+=($(compgen "${opts[@]}" -- "$quoted")) 216 | IFS=' ' 217 | $reset 218 | IFS=$'\n' 219 | 220 | # Try without filter if it failed to produce anything and configured to 221 | [[ ${BASH_COMPLETION_FILEDIR_FALLBACK-${COMP_FILEDIR_FALLBACK-}} && 222 | $arg && ${#toks[@]} -lt 1 ]] && { 223 | reset=$(shopt -po noglob) 224 | set -o noglob 225 | toks+=($(compgen -f ${plusdirs+"${plusdirs[@]}"} -- "$quoted")) 226 | IFS=' ' 227 | $reset 228 | IFS=$'\n' 229 | } 230 | fi 231 | 232 | if ((${#toks[@]} != 0)); then 233 | # 2>/dev/null for direct invocation, e.g. in the _filedir unit test 234 | compopt -o filenames 2>/dev/null 235 | COMPREPLY+=("${toks[@]}") 236 | fi 237 | } # _filedir() 238 | -------------------------------------------------------------------------------- /completions/runme.fish: -------------------------------------------------------------------------------- 1 | # Fish completion for scripts written with argc 2 | # 3 | # All argc scripts share the same completion function. 4 | # To add completion to a argc script, simply add the script name to $ARGC_SCRIPTS. 5 | 6 | function __fish_complete_runme 7 | set -l tokens (commandline -c | string trim -l | string split " " --) 8 | set -l scriptfile (runme --runme-file 2>/dev/null) 9 | if not test -f $scriptfile 10 | return 0 11 | end 12 | set -l line "$tokens[2..]" 13 | set -l IFS '\n' 14 | set -l compgen_values (runme --runme-compgen "$scriptfile" $line 2>/dev/null) 15 | set -l candicates 16 | set -l option_values 17 | set -l value_kind 0 18 | for item in $compgen_values 19 | if string match -qr -- '^-' "$item" 20 | set -a option_values $item 21 | else if string match -qr '^`[^` ]+`' -- "$item" 22 | set -l name (string sub "$item" -s 2 -e -1) 23 | set -a candicates (runme $name 2>/dev/null) 24 | else if string match -q -- '<*' "$item" 25 | if string match -qi -- '...' "$item" 26 | set value_kind 1 27 | else if string match -qir -- '(file|path)>(\.\.\.)?' "$item" 28 | set value_kind 2 29 | else if string match -qir -- 'dir>(\.\.\.)?' "$item" 30 | set value_kind 3 31 | else 32 | set value_kind 9 33 | end 34 | else 35 | set -a candicates $item 36 | end 37 | end 38 | if [ $value_kind -eq 0 ] 39 | if test -z "$candicates" 40 | set -a candicates $option_values 41 | end 42 | else if [ $value_kind -eq 1 ] 43 | if test -z "$candicates" 44 | set -a candicates $option_values 45 | end 46 | if test -z "$candicates" 47 | __fish_complete_path 48 | end 49 | else if [ $value_kind -eq 2 ] 50 | __fish_complete_path 51 | else if [ $value_kind -eq 3 ] 52 | __fish_complete_directories 53 | end 54 | for item in $candicates 55 | echo $item 56 | end 57 | end 58 | 59 | complete -x -c runme -n 'true' -a "(__fish_complete_runme)" -------------------------------------------------------------------------------- /completions/runme.ps1: -------------------------------------------------------------------------------- 1 | # Powershell completion for runme 2 | 3 | $_runmeCompletion = { 4 | param($wordToComplete, $commandAst, $cursorPosition) 5 | $scriptfile = $(runme --runme-file 2>$null) 6 | if (!$scriptfile) { 7 | $scriptfile = $commandAst.CommandElements[0] 8 | if (-not(Test-Path -Path $scriptfile -PathType Leaf)) { 9 | return; 10 | } 11 | } 12 | if ($wordToComplete.ToString() -eq "") { 13 | $tail = " " 14 | } else { 15 | $tail = "" 16 | } 17 | if ($commandAst.CommandElements.Count -gt 1) { 18 | $line = ($commandAst.CommandElements[1..($commandAst.CommandElements.Count - 1)] -join " ") + $tail 19 | } else { 20 | $line = $tail 21 | } 22 | $compgen_values = (runme --runme-compgen "$scriptfile" "$line" 2>$null).Split("`n") 23 | $candicates = @() 24 | $option_values = @() 25 | $value_kind = 0 26 | foreach ($item in $compgen_values) { 27 | if ($item -match '^-') { 28 | $option_values += $item 29 | } elseif ($item -match '^`[^` ]+`$') { 30 | $choices = (runme $item.Substring(1, $item.Length - 2) 2>$null).Split("`n") 31 | $candicates += $choices 32 | } elseif ($item -match '^<') { 33 | if ($item -imatch "...") { 34 | $value_kind = 1 35 | } elseif ($item -imatch "file|path>(\.\.\.)?") { 36 | $value_kind = 2 37 | } elseif ($item -imatch "dir>(\.\.\.)?") { 38 | $value_kind = 3 39 | } else { 40 | $value_kind = 9 41 | } 42 | } else { 43 | $candicates += $item 44 | } 45 | } 46 | $paths = @() 47 | if ($value_kind -eq 0) { 48 | if ($candicates.Count -eq 0) { 49 | $candicates = $option_values 50 | } 51 | } elseif ($value_kind -eq 1) { 52 | if ($candicates.Count -eq 0) { 53 | $candicates = $option_values 54 | } 55 | if ($candicates.Count -eq 0) { 56 | $paths = (Get-ChildItem -Path "$wordToComplete*" | Select-Object -ExpandProperty Name) 57 | } 58 | } elseif ($value_kind -eq 2) { 59 | $paths = (Get-ChildItem -Path "$wordToComplete*" | Select-Object -ExpandProperty Name) 60 | } elseif ($value_kind -eq 3) { 61 | $paths = (Get-ChildItem -Attributes Directory -Path "$wordToComplete*" | Select-Object -ExpandProperty Name) 62 | } 63 | 64 | $param_value = [System.Management.Automation.CompletionResultType]::ParameterValue 65 | $param_name = [System.Management.Automation.CompletionResultType]::ParameterName 66 | $result = ($candicates | 67 | Where-Object { $_ -like "$wordToComplete*" } | 68 | ForEach-Object { 69 | if ($_.StartsWith("-")) { 70 | $t = $param_name 71 | } else { 72 | $t = $param_value 73 | } 74 | [System.Management.Automation.CompletionResult]::new($_, $_, $t, '-') 75 | }) 76 | 77 | foreach ($path in $paths) { 78 | $result.Add([System.Management.Automation.CompletionResult]::new($path, $path, $param_value, '-')) 79 | } 80 | 81 | return $result 82 | } 83 | 84 | Register-ArgumentCompleter -Native -ScriptBlock $_runmeCompletion -CommandName runme -------------------------------------------------------------------------------- /completions/runme.zsh: -------------------------------------------------------------------------------- 1 | # Zsh completion for runme 2 | 3 | _runme_completion() 4 | { 5 | local scriptfile=$(runme --runme-file 2>/dev/null) 6 | if [[ ! -f "$scriptfile" ]]; then 7 | return 0 8 | fi 9 | local line="${words[2,-1]}" 10 | local IFS=$'\n' 11 | local compgen_values=( $(runme --runme-compgen "$scriptfile" "$line" 2>/dev/null) ) 12 | local candicates=() 13 | local option_values=() 14 | local value_kind=0 15 | for item in ${compgen_values[@]}; do 16 | if [[ "$item" == '-'* ]]; then 17 | option_values+=( "$item" ) 18 | elif [[ "$item" == \`*\` ]]; then 19 | local choices=( $(runme "${item:1:-1}" 2>/dev/null) ) 20 | candicates=( "${candicates[@]}" "${choices[@]}" ) 21 | elif [[ "$item" == '<'* ]]; then 22 | if echo "$item" | grep -qi '...'; then 23 | value_kind=1 24 | elif echo "$item" | grep -qi '\(file\|path\)>\(\.\.\.\)\?'; then 25 | value_kind=2 26 | elif echo "$item" | grep -qi 'dir>\(\.\.\.\)\?'; then 27 | value_kind=3 28 | else 29 | value_kind=9 30 | fi 31 | else 32 | candicates+=( "$item" ) 33 | fi 34 | done 35 | if [[ "$value_kind" == 0 ]]; then 36 | if [[ "${#candicates[@]}" -eq 0 ]]; then 37 | candicates=( "${option_values[@]}" ) 38 | fi 39 | elif [[ "$value_kind" == 1 ]]; then 40 | if [[ "${#candicates[@]}" -eq 0 ]]; then 41 | candicates=( "${option_values[@]}" ) 42 | fi 43 | if [[ "${#candicates[@]}" -eq 0 ]]; then 44 | _path_files 45 | fi 46 | elif [[ "$value_kind" == 2 ]]; then 47 | _path_files 48 | elif [[ "$value_kind" == 3 ]]; then 49 | _path_files -/ 50 | fi 51 | if [[ ${#candicates[@]} -gt 0 ]]; then 52 | compadd -- $candicates[@] 53 | fi 54 | } 55 | 56 | compdef _runme_completion runme -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, Result}; 2 | use clap::{Arg, ArgAction, Command}; 3 | use either::Either; 4 | use std::{ 5 | env, fs, 6 | path::{Path, PathBuf}, 7 | process, 8 | sync::{ 9 | atomic::{AtomicBool, Ordering}, 10 | Arc, 11 | }, 12 | }; 13 | use which::which; 14 | 15 | const SCRIPT_NAMES: [&str; 6] = [ 16 | "Runmefile.sh", 17 | "Runmefile", 18 | "runmefile.sh", 19 | "runmefile", 20 | "RUNMEFILE.sh", 21 | "RUNMEFILE", 22 | ]; 23 | 24 | fn main() { 25 | match run() { 26 | Ok(code) => { 27 | if code != 0 { 28 | process::exit(code); 29 | } 30 | } 31 | Err(err) => { 32 | eprintln!("{err}"); 33 | process::exit(1); 34 | } 35 | } 36 | } 37 | 38 | fn run() -> Result { 39 | let mut runme_args: Vec = vec![]; 40 | let mut script_args: Vec = vec![]; 41 | let mut next_runme_arg = true; 42 | for arg in std::env::args() { 43 | if next_runme_arg { 44 | runme_args.push(arg); 45 | next_runme_arg = false; 46 | continue; 47 | } 48 | if script_args.is_empty() && arg.starts_with("--runme-") { 49 | runme_args.push(arg); 50 | continue; 51 | } 52 | next_runme_arg = false; 53 | script_args.push(arg); 54 | } 55 | let matches = Command::new(env!("CARGO_CRATE_NAME")) 56 | .version(env!("CARGO_PKG_VERSION")) 57 | .author(env!("CARGO_PKG_AUTHORS")) 58 | .override_usage( 59 | r#" 60 | runme --runme-eval SCRIPT [ARGS...] Parse arguments `eval "$(runme --runme-eval "$0" "$@")"` 61 | runme --runme-create [TASKS...] Create a boilerplate runmefile 62 | runme --runme-help Print help information 63 | runme --runme-version Print version information"#, 64 | ) 65 | .help_template(r#"{bin} {version} 66 | {author} 67 | {about} 68 | 69 | USAGE:{usage}"#) 70 | .disable_help_flag(true) 71 | .disable_version_flag(true) 72 | .disable_help_subcommand(true) 73 | .about(concat!( 74 | env!("CARGO_PKG_DESCRIPTION"), 75 | " - ", 76 | env!("CARGO_PKG_REPOSITORY") 77 | )) 78 | .arg(Arg::new("runme-eval").long("runme-eval").action(ArgAction::SetTrue)) 79 | .arg(Arg::new("runme-create").long("runme-create").action(ArgAction::SetTrue)) 80 | .arg( 81 | Arg::new("runme-compgen") 82 | .long("runme-compgen").action(ArgAction::SetTrue)) 83 | .arg( 84 | Arg::new("runme-file") 85 | .long("runme-file").action(ArgAction::SetTrue) 86 | ) 87 | .arg( 88 | Arg::new("runme-shell") 89 | .long("runme-shell").action(ArgAction::SetTrue) 90 | ) 91 | .arg( 92 | Arg::new("runme-version") 93 | .long("runme-version") 94 | .action(ArgAction::Version) 95 | ) 96 | .arg( 97 | Arg::new("runme-help") 98 | .long("runme-help") 99 | .action(ArgAction::Help) 100 | ) 101 | .try_get_matches_from(&runme_args)?; 102 | 103 | if matches.get_flag("runme-eval") { 104 | let (source, cmd_args) = parse_script_args(&script_args)?; 105 | let cmd_args: Vec<&str> = cmd_args.iter().map(|v| v.as_str()).collect(); 106 | match argc::eval(&source, &cmd_args)? { 107 | Either::Left(output) => { 108 | println!("{output}") 109 | } 110 | Either::Right(error) => { 111 | if env::var_os("NO_COLOR").is_some() { 112 | eprintln!("{error}"); 113 | } else { 114 | eprintln!("{}", error.render().ansi()); 115 | } 116 | if error.use_stderr() { 117 | println!("exit 1"); 118 | } else { 119 | println!("exit 0"); 120 | } 121 | } 122 | } 123 | } else if matches.get_flag("runme-create") { 124 | if let Some((_, script_file)) = get_script_path(false) { 125 | bail!("Already exist {}", script_file.display()); 126 | } 127 | let content = generate_boilerplate(&script_args); 128 | let names = candidate_script_names(); 129 | fs::write(&names[0], content).map_err(|err| anyhow!("Failed to create runme.sh, {err}"))?; 130 | } else if matches.get_flag("runme-file") { 131 | let (_, script_file) = 132 | get_script_path(true).ok_or_else(|| anyhow!("Not found script file"))?; 133 | print!("{}", script_file.display()); 134 | } else if matches.get_flag("runme-shell") { 135 | let shell_file = get_shell_path().ok_or_else(|| anyhow!("Not found shell"))?; 136 | print!("{}", shell_file.display()); 137 | } else if matches.get_flag("runme-compgen") { 138 | let (source, cmd_args) = parse_script_args(&script_args)?; 139 | let line = if cmd_args.len() == 1 { 140 | "" 141 | } else { 142 | cmd_args[1].as_str() 143 | }; 144 | let candicates = argc::compgen(&source, line)?; 145 | candicates.into_iter().for_each(|v| println!("{v}")); 146 | } else { 147 | let shell = get_shell_path().ok_or_else(|| anyhow!("Not found shell"))?; 148 | let (script_dir, script_file) = get_script_path(true).ok_or_else(|| { 149 | anyhow!("Not found script file, try `runme --runme-help` to get help.") 150 | })?; 151 | let interrupt = Arc::new(AtomicBool::new(false)); 152 | let interrupt_me = interrupt.clone(); 153 | ctrlc::set_handler(move || interrupt_me.store(true, Ordering::Relaxed)) 154 | .map_err(|err| anyhow!("Failed to set CTRL-C handler: {}", err))?; 155 | let mut command = process::Command::new(shell); 156 | command.arg(&script_file); 157 | command.args(&script_args); 158 | command.current_dir(script_dir); 159 | let status = command 160 | .status() 161 | .map_err(|err| anyhow!("Run `{}` throw {}", script_file.display(), err))?; 162 | if interrupt.load(Ordering::Relaxed) { 163 | return Ok(130); 164 | } 165 | return Ok(status.code().unwrap_or_default()); 166 | } 167 | Ok(0) 168 | } 169 | 170 | fn parse_script_args(args: &[String]) -> Result<(String, Vec)> { 171 | if args.is_empty() { 172 | bail!("No script file"); 173 | } 174 | let script_file = args[0].as_str(); 175 | let args: Vec = args[1..].to_vec(); 176 | let source = fs::read_to_string(script_file) 177 | .map_err(|e| anyhow!("Failed to load '{}', {}", script_file, e))?; 178 | let name = Path::new(script_file) 179 | .file_name() 180 | .and_then(|v| v.to_str()) 181 | .ok_or_else(|| anyhow!("Failed to get script name"))?; 182 | let mut cmd_args = vec![name.to_string()]; 183 | cmd_args.extend(args); 184 | Ok((source, cmd_args)) 185 | } 186 | 187 | fn generate_boilerplate(args: &[String]) -> String { 188 | let tasks = args 189 | .iter() 190 | .map(|cmd| { 191 | format!( 192 | r#" 193 | # @cmd 194 | {cmd}() {{ 195 | echo Run {cmd} 196 | }} 197 | "# 198 | ) 199 | }) 200 | .collect::>() 201 | .join(""); 202 | 203 | format!( 204 | r#"#!/usr/bin/env bash 205 | 206 | set -e 207 | {tasks} 208 | eval "$(runme --runme-eval "$0" "$@")" 209 | "# 210 | ) 211 | } 212 | 213 | fn get_script_path(recursive: bool) -> Option<(PathBuf, PathBuf)> { 214 | let candidates = candidate_script_names(); 215 | let mut dir = env::current_dir().ok()?; 216 | loop { 217 | for name in candidates.iter() { 218 | let path = dir.join(name); 219 | if path.exists() { 220 | return Some((dir, path)); 221 | } 222 | } 223 | if !recursive { 224 | return None; 225 | } 226 | dir = dir.parent()?.to_path_buf(); 227 | } 228 | } 229 | 230 | fn candidate_script_names() -> Vec { 231 | let mut names = vec![]; 232 | if let Ok(name) = env::var("RUNME_SCRIPT") { 233 | names.push(name.clone()); 234 | if !name.ends_with(".sh") { 235 | names.push(format!("{name}.sh")); 236 | } 237 | } 238 | names.extend(SCRIPT_NAMES.into_iter().map(|v| v.to_string())); 239 | names 240 | } 241 | 242 | fn get_shell_path() -> Option { 243 | let shell = match env::var("RUNME_SHELL") { 244 | Ok(v) => Path::new(&v).to_path_buf(), 245 | Err(_) => get_bash_path()?, 246 | }; 247 | if !shell.exists() { 248 | return None; 249 | } 250 | Some(shell) 251 | } 252 | 253 | #[cfg(windows)] 254 | fn get_bash_path() -> Option { 255 | if let Ok(bash) = which("bash") { 256 | if bash.display().to_string().to_lowercase() != "c:\\windows\\system32\\bash.exe" { 257 | return Some(bash); 258 | } 259 | } 260 | let git = which("git").ok()?; 261 | Some(git.parent()?.parent()?.join("bin").join("bash.exe")) 262 | } 263 | 264 | #[cfg(not(windows))] 265 | fn get_bash_path() -> Option { 266 | which("bash").ok() 267 | } 268 | -------------------------------------------------------------------------------- /tests/cli.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::prelude::*; 2 | use std::process::Command; 3 | 4 | #[test] 5 | fn version() { 6 | Command::cargo_bin("runme") 7 | .unwrap() 8 | .arg("--runme-version") 9 | .assert() 10 | .stderr(predicates::str::contains(format!( 11 | "runme {}", 12 | env!("CARGO_PKG_VERSION") 13 | ))) 14 | .failure(); 15 | } 16 | 17 | #[test] 18 | fn help() { 19 | Command::cargo_bin("runme") 20 | .unwrap() 21 | .arg("--runme-help") 22 | .assert() 23 | .stderr(predicates::str::contains(env!("CARGO_PKG_DESCRIPTION"))) 24 | .failure(); 25 | } 26 | -------------------------------------------------------------------------------- /tests/compgen.rs: -------------------------------------------------------------------------------- 1 | use super::SPEC_SCRIPT; 2 | 3 | const HELP_TAG_SCRIPT: &str = r#" 4 | # @help Print help information 5 | 6 | # @cmd 7 | foo() { :; } 8 | 9 | # @cmd 10 | bar() { :; } 11 | "#; 12 | 13 | #[test] 14 | fn test_compgen() { 15 | snapshot_compgen!(SPEC_SCRIPT, ""); 16 | } 17 | 18 | #[test] 19 | fn test_compgen_help() { 20 | snapshot_compgen!(SPEC_SCRIPT, "help"); 21 | } 22 | 23 | #[test] 24 | fn test_compgen_subcommand() { 25 | snapshot_compgen!(SPEC_SCRIPT, "cmd_option_names "); 26 | } 27 | 28 | #[test] 29 | fn test_compgen_option_choices() { 30 | snapshot_compgen!(SPEC_SCRIPT, "cmd_option_names --opt7 "); 31 | } 32 | 33 | #[test] 34 | fn test_compgen_option_choices2() { 35 | snapshot_compgen!(SPEC_SCRIPT, "cmd_option_names --opt7 a"); 36 | } 37 | 38 | #[test] 39 | fn test_compgen_option_choices3() { 40 | snapshot_compgen!(SPEC_SCRIPT, "cmd_option_names --opt7 a "); 41 | } 42 | 43 | #[test] 44 | fn test_compgen_positional() { 45 | snapshot_compgen!(SPEC_SCRIPT, "cmd_positional_requires "); 46 | } 47 | 48 | #[test] 49 | fn test_compgen_positional_arg() { 50 | snapshot_compgen!(SPEC_SCRIPT, "cmd_positional_requires arg1 "); 51 | } 52 | 53 | #[test] 54 | fn test_compgen_positional_arg2() { 55 | snapshot_compgen!(SPEC_SCRIPT, "cmd_positional_requires arg1 arg2 "); 56 | } 57 | 58 | #[test] 59 | fn test_compgen_positional_choices() { 60 | snapshot_compgen!(SPEC_SCRIPT, "cmd_positional_with_choices "); 61 | } 62 | 63 | #[test] 64 | fn test_compgen_help_tag() { 65 | snapshot_compgen!(HELP_TAG_SCRIPT, ""); 66 | } 67 | 68 | #[test] 69 | fn test_compgen_help_tag2() { 70 | snapshot_compgen!(HELP_TAG_SCRIPT, "help"); 71 | } 72 | 73 | #[test] 74 | fn test_compgen_choice_fn() { 75 | snapshot_compgen!(SPEC_SCRIPT, "cmd_option_names --op11 "); 76 | } 77 | -------------------------------------------------------------------------------- /tests/create.rs: -------------------------------------------------------------------------------- 1 | use assert_fs::TempDir; 2 | use rstest::rstest; 3 | 4 | use crate::fixtures::{get_path_env_var, tmpdir2, Error}; 5 | use assert_cmd::prelude::*; 6 | use std::process::Command; 7 | 8 | #[rstest] 9 | fn create(tmpdir2: TempDir) -> Result<(), Error> { 10 | let path_env_var = get_path_env_var(); 11 | Command::cargo_bin("runme")? 12 | .current_dir(tmpdir2.path()) 13 | .env("PATH", path_env_var.clone()) 14 | .arg("--runme-create") 15 | .assert() 16 | .success(); 17 | assert!(tmpdir2.path().join("Runmefile.sh").exists()); 18 | Command::cargo_bin("runme")? 19 | .current_dir(tmpdir2.path()) 20 | .env("PATH", path_env_var) 21 | .assert() 22 | .success(); 23 | Ok(()) 24 | } 25 | 26 | #[rstest] 27 | fn create_with_tasks(tmpdir2: TempDir) -> Result<(), Error> { 28 | let path_env_var = get_path_env_var(); 29 | Command::cargo_bin("runme")? 30 | .current_dir(tmpdir2.path()) 31 | .env("PATH", path_env_var.clone()) 32 | .arg("--runme-create") 33 | .args(["foo", "bar"]) 34 | .assert() 35 | .success(); 36 | Command::cargo_bin("runme")? 37 | .current_dir(tmpdir2.path()) 38 | .env("PATH", path_env_var) 39 | .arg("bar") 40 | .assert() 41 | .stdout(predicates::str::contains("Run bar")) 42 | .success(); 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /tests/fixtures.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::cargo::cargo_bin; 2 | use assert_fs::fixture::{ChildPath, TempDir}; 3 | use assert_fs::prelude::*; 4 | use rstest::fixture; 5 | 6 | #[allow(dead_code)] 7 | pub type Error = Box; 8 | 9 | pub const SCRIPT_PATHS: [&str; 8] = [ 10 | "dir1/Runmefile.sh", 11 | "dir1/subdir1/Runmefile.sh", 12 | "dir1/subdir1/subdirdir1/EMPTY", 13 | "dir2/runmefile.sh", 14 | "dir3/RUNMEFILE.sh", 15 | "dir4/Runmefile", 16 | "dir5/runmefile", 17 | "dir6/RUNMEFILE", 18 | ]; 19 | 20 | /// Test fixture which creates a temporary directory with a few files and directories inside. 21 | /// The directories also contain files. 22 | #[fixture] 23 | #[allow(dead_code)] 24 | pub fn tmpdir() -> TempDir { 25 | let tmpdir = assert_fs::TempDir::new().expect("Couldn't create a temp dir for tests"); 26 | for path in SCRIPT_PATHS { 27 | write_file(&tmpdir, path); 28 | } 29 | tmpdir 30 | } 31 | 32 | #[fixture] 33 | #[allow(dead_code)] 34 | pub fn tmpdir2() -> TempDir { 35 | assert_fs::TempDir::new().expect("Couldn't create a temp dir for tests") 36 | } 37 | 38 | pub fn get_path_env_var() -> String { 39 | let runme_path = cargo_bin("runme"); 40 | let runme_dir = runme_path.parent().unwrap(); 41 | let path_env_var = std::env::var("PATH").unwrap(); 42 | if cfg!(windows) { 43 | format!("{};{}", runme_dir.display(), path_env_var) 44 | } else { 45 | format!("{}:{}", runme_dir.display(), path_env_var) 46 | } 47 | } 48 | 49 | pub fn tmpdir_path(tmpdir: &TempDir, path: &str) -> ChildPath { 50 | let parts: Vec<&str> = path.split('/').collect(); 51 | let cp = tmpdir.child(parts[0]); 52 | parts.iter().skip(1).fold(cp, |acc, part| acc.child(part)) 53 | } 54 | 55 | fn write_file(tmpdir: &TempDir, path: &str) { 56 | let cp = tmpdir_path(tmpdir, path); 57 | if path.ends_with("EMPTY") { 58 | cp.write_str("").unwrap(); 59 | } else { 60 | cp.write_str(&get_script(path)).unwrap(); 61 | } 62 | } 63 | 64 | fn get_script(name: &str) -> String { 65 | format!( 66 | r#"#!/usr/bin/env bash 67 | set -euo pipefail 68 | 69 | main() {{ 70 | echo "{name}" 71 | }} 72 | 73 | # @cmd 74 | task1() {{ 75 | sleep $1 76 | }} 77 | 78 | eval "$(runme --runme-eval "$0" "$@")" 79 | "# 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /tests/interrupt.rs: -------------------------------------------------------------------------------- 1 | use assert_fs::{fixture::PathChild, TempDir}; 2 | use rstest::rstest; 3 | 4 | use crate::fixtures::{get_path_env_var, tmpdir, Error}; 5 | use assert_cmd::prelude::*; 6 | use std::{process::Command, thread, time::Duration}; 7 | 8 | fn kill(process_id: u32) { 9 | unsafe { 10 | libc::kill(process_id as i32, libc::SIGINT); 11 | } 12 | } 13 | 14 | #[rstest] 15 | fn interrupt(tmpdir: TempDir) -> Result<(), Error> { 16 | let path_env_var = get_path_env_var(); 17 | 18 | let mut child = Command::cargo_bin("runme")? 19 | .current_dir(tmpdir.child("dir1").path()) 20 | .env("PATH", path_env_var) 21 | .args(["task1", "2"]) 22 | .spawn() 23 | .expect("runme invocation failed"); 24 | 25 | thread::sleep(Duration::new(1, 0)); 26 | 27 | kill(child.id()); 28 | 29 | let status = child.wait().unwrap(); 30 | 31 | assert_eq!(status.code(), Some(130)); 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /tests/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! snapshot { 3 | ( 4 | $source:expr, 5 | $args:expr, 6 | ) => { 7 | let (stdout, stderr) = match argc::run($source, $args).unwrap() { 8 | either::Either::Left(stdout) => (stdout, String::new()), 9 | either::Either::Right(stderr) => (String::new(), stderr), 10 | }; 11 | 12 | let args = $args.join(" "); 13 | let output = format!( 14 | r###"RUN 15 | {} 16 | 17 | STDOUT 18 | {} 19 | 20 | STDERR 21 | {} 22 | "###, 23 | args, stdout, stderr 24 | ); 25 | insta::assert_snapshot!(output); 26 | }; 27 | } 28 | 29 | #[macro_export] 30 | macro_rules! plain { 31 | ( 32 | $source:expr, 33 | $args:expr, 34 | $(stdout: $stdout:expr,)? 35 | $(stderr: $stderr:expr,)? 36 | ) => { 37 | let result = match argc::run($source, $args).unwrap() { 38 | either::Either::Left(stdout) => (stdout, String::new()), 39 | either::Either::Right(stderr) => (String::new(), stderr), 40 | }; 41 | $({ 42 | assert_eq!(result.0.as_str(), $stdout); 43 | })? 44 | $({ 45 | assert_eq!(result.1.as_str(), $stderr); 46 | })? 47 | }; 48 | } 49 | 50 | #[macro_export] 51 | macro_rules! fatal { 52 | ( 53 | $source:expr, 54 | $args:expr, 55 | $err:expr 56 | ) => { 57 | let err = argc::run($source, $args).unwrap_err(); 58 | assert_eq!(err.to_string().as_str(), $err); 59 | }; 60 | } 61 | 62 | #[macro_export] 63 | macro_rules! snapshot_compgen { 64 | ( 65 | $source:expr, 66 | $args:expr 67 | ) => { 68 | let (stdout, stderr) = match argc::compgen($source, $args) { 69 | Ok(stdout) => (stdout.join(" "), String::new()), 70 | Err(stderr) => (String::new(), stderr.to_string()), 71 | }; 72 | 73 | let output = format!( 74 | r###"RUN 75 | {} 76 | 77 | STDOUT 78 | {} 79 | 80 | STDERR 81 | {} 82 | "###, 83 | $args, stdout, stderr 84 | ); 85 | insta::assert_snapshot!(output); 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /tests/runmefile.rs: -------------------------------------------------------------------------------- 1 | use assert_fs::{fixture::PathChild, TempDir}; 2 | use rstest::rstest; 3 | 4 | use crate::fixtures::{get_path_env_var, tmpdir, tmpdir_path, Error, SCRIPT_PATHS}; 5 | use assert_cmd::prelude::*; 6 | use std::process::Command; 7 | 8 | #[rstest] 9 | fn runmefile(tmpdir: TempDir) -> Result<(), Error> { 10 | let path_env_var = get_path_env_var(); 11 | 12 | for path in SCRIPT_PATHS { 13 | if path.ends_with("EMPTY") { 14 | continue; 15 | } 16 | Command::cargo_bin("runme")? 17 | .current_dir(tmpdir_path(&tmpdir, path).path().parent().unwrap()) 18 | .env("PATH", path_env_var.clone()) 19 | .assert() 20 | .stdout(predicates::str::contains(path)) 21 | .success(); 22 | } 23 | 24 | Command::cargo_bin("runme")? 25 | .current_dir(tmpdir_path(&tmpdir, "dir1/subdir1/subdirdir1")) 26 | .env("PATH", path_env_var) 27 | .assert() 28 | .stdout(predicates::str::contains("dir1/subdir1/Runmefile.sh")) 29 | .success(); 30 | 31 | Ok(()) 32 | } 33 | 34 | #[rstest] 35 | fn runmefile_path(tmpdir: TempDir) -> Result<(), Error> { 36 | Command::cargo_bin("runme")? 37 | .arg("--runme-file") 38 | .current_dir(tmpdir.child("dir1").path()) 39 | .assert() 40 | .stdout(predicates::str::contains( 41 | tmpdir_path(&tmpdir, "dir1/Runmefile.sh") 42 | .display() 43 | .to_string(), 44 | )) 45 | .success(); 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /tests/snapshots/integration__compgen__compgen.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/compgen.rs 3 | expression: output 4 | --- 5 | RUN 6 | 7 | 8 | STDOUT 9 | cmd_preferred cmd_omitted cmd_option_names cmd_option_formats cmd_option_quotes cmd_flag_formats cmd_positional_only cmd_positional_requires cmd_positional_with_choices cmd_positional_with_default cmd_positional_with_default_fn cmd_positional_with_choices_and_default cmd_positional_with_choices_fn cmd_positional_with_choices_and_required cmd_positional_with_choices_fn_and_required cmd_without_any_arg cmd_alias a alias cmd_with_hyphens 10 | 11 | STDERR 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/snapshots/integration__compgen__compgen_choice_fn.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/compgen.rs 3 | expression: output 4 | --- 5 | RUN 6 | cmd_option_names --op11 7 | 8 | STDOUT 9 | `_fn_bars` 10 | 11 | STDERR 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/snapshots/integration__compgen__compgen_help.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/compgen.rs 3 | expression: output 4 | --- 5 | RUN 6 | help 7 | 8 | STDOUT 9 | cmd_preferred cmd_omitted cmd_option_names cmd_option_formats cmd_option_quotes cmd_flag_formats cmd_positional_only cmd_positional_requires cmd_positional_with_choices cmd_positional_with_default cmd_positional_with_default_fn cmd_positional_with_choices_and_default cmd_positional_with_choices_fn cmd_positional_with_choices_and_required cmd_positional_with_choices_fn_and_required cmd_without_any_arg cmd_alias a alias cmd_with_hyphens 10 | 11 | STDERR 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/snapshots/integration__compgen__compgen_help_tag.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/compgen.rs 3 | expression: output 4 | --- 5 | RUN 6 | 7 | 8 | STDOUT 9 | foo bar help 10 | 11 | STDERR 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/snapshots/integration__compgen__compgen_help_tag2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/compgen.rs 3 | expression: output 4 | --- 5 | RUN 6 | help 7 | 8 | STDOUT 9 | foo bar 10 | 11 | STDERR 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/snapshots/integration__compgen__compgen_option_choices.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/compgen.rs 3 | expression: output 4 | --- 5 | RUN 6 | cmd_option_names --opt7 7 | 8 | STDOUT 9 | a b c 10 | 11 | STDERR 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/snapshots/integration__compgen__compgen_option_choices2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/compgen.rs 3 | expression: output 4 | --- 5 | RUN 6 | cmd_option_names --opt7 a 7 | 8 | STDOUT 9 | a b c 10 | 11 | STDERR 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/snapshots/integration__compgen__compgen_option_choices3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/compgen.rs 3 | expression: output 4 | --- 5 | RUN 6 | cmd_option_names --opt7 a 7 | 8 | STDOUT 9 | --opt1 --opt2 --opt3 --opt4 --opt5 --opt6 --opt8 --opt9 --op10 --op11 10 | 11 | STDERR 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/snapshots/integration__compgen__compgen_positional.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/compgen.rs 3 | expression: output 4 | --- 5 | RUN 6 | cmd_positional_requires 7 | 8 | STDOUT 9 | 10 | 11 | STDERR 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/snapshots/integration__compgen__compgen_positional_arg.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/compgen.rs 3 | expression: output 4 | --- 5 | RUN 6 | cmd_positional_requires arg1 7 | 8 | STDOUT 9 | ... 10 | 11 | STDERR 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/snapshots/integration__compgen__compgen_positional_arg2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/compgen.rs 3 | expression: output 4 | --- 5 | RUN 6 | cmd_positional_requires arg1 arg2 7 | 8 | STDOUT 9 | ... 10 | 11 | STDERR 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/snapshots/integration__compgen__compgen_positional_choices.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/compgen.rs 3 | expression: output 4 | --- 5 | RUN 6 | cmd_positional_with_choices 7 | 8 | STDOUT 9 | a b 10 | 11 | STDERR 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/snapshots/integration__compgen__compgen_subcommand.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/compgen.rs 3 | expression: output 4 | --- 5 | RUN 6 | cmd_option_names 7 | 8 | STDOUT 9 | --opt1 --opt2 --opt3 --opt4 --opt5 --opt6 --opt7 --opt8 --opt9 --op10 --op11 10 | 11 | STDERR 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/spec.sh: -------------------------------------------------------------------------------- 1 | # @describe Test all aspects 2 | # @version 0.10 3 | # @author nobody 4 | # @cmd Preferred 5 | # @arg arg1* A positional arg 6 | # @flag -f --flag1 A flag 7 | # @option -o --opt1 A option 8 | cmd_preferred() { 9 | print_argc_vars 10 | } 11 | 12 | # @cmd Omitted 13 | # @arg arg1 14 | # @flag --flag1 15 | # @option --opt1 16 | cmd_omitted() { 17 | print_argc_vars 18 | } 19 | 20 | # @cmd Options all kind of names 21 | # @option --opt1 optional 22 | # @option --opt2! required 23 | # @option --opt3* optional, multiple 24 | # @option --opt4+ required, multiple 25 | # @option --opt5=a optional, default 26 | # @option --opt6[a|b|c] choices 27 | # @option --opt7[=a|b|c] choices, default 28 | # @option --opt8![a|b|c] required, choices 29 | # @option --opt9=`_fn_foo` optional, default from fn 30 | # @option --op10[`_fn_bars`] choices from fn 31 | # @option --op11![`_fn_bars`] required, choices from fn 32 | cmd_option_names() { 33 | print_argc_vars 34 | } 35 | 36 | # @cmd Options all kind of formats 37 | # @option --opt1 38 | # @option -a --opt2 39 | # @option --opt3 40 | # @option --opt4 With description 41 | # @option -b --opt5 With description 42 | # @option -c --opt6 43 | # @option --opt7 With description 44 | cmd_option_formats() { 45 | print_argc_vars 46 | } 47 | 48 | # @cmd Option value quoted 49 | # @option --opt1=a 50 | # @option --opt2="a b" 51 | # @option --opt3[a 3|b|c] 52 | # @option --opt4[=a b|c d|e f] 53 | # @option --opt5[="a|b"|"c]d"|ef] 54 | cmd_option_quotes() { 55 | print_argc_vars 56 | } 57 | 58 | # @cmd All kind of flags 59 | # @flag --foo1 60 | # @flag -a --foo2 61 | # @flag --foo3 With description 62 | # @flag -b --foo4 With description 63 | cmd_flag_formats() { 64 | print_argc_vars 65 | } 66 | 67 | # @cmd Positional one required 68 | # @arg arg1! A required arg 69 | cmd_positional_only() { 70 | print_argc_vars 71 | } 72 | 73 | # @cmd Positional all required 74 | # @arg arg1! A required arg 75 | # @arg arg2+ A required arg, multiple 76 | cmd_positional_requires() { 77 | print_argc_vars 78 | } 79 | 80 | # @cmd Positional with choices 81 | # @arg arg[a|b] A arg with choices 82 | cmd_positional_with_choices() { 83 | print_argc_vars 84 | } 85 | 86 | # @cmd Positional with default value 87 | # @arg arg=a A arg with default value 88 | cmd_positional_with_default() { 89 | print_argc_vars 90 | } 91 | 92 | # @cmd Positional with default value 93 | # @arg arg=`_fn_foo` A arg with default fn 94 | cmd_positional_with_default_fn() { 95 | print_argc_vars 96 | } 97 | 98 | # @cmd Positional with choices and value 99 | # @arg arg[=a|b] A arg with choices and default value 100 | cmd_positional_with_choices_and_default() { 101 | print_argc_vars 102 | } 103 | 104 | # @cmd Positional with choices and value 105 | # @arg arg[`_fn_bars`] A arg with choices fn 106 | cmd_positional_with_choices_fn() { 107 | print_argc_vars 108 | } 109 | 110 | # @cmd Positional with choices and required 111 | # @arg arg![a|b] A arg with choices and required 112 | cmd_positional_with_choices_and_required() { 113 | print_argc_vars 114 | } 115 | 116 | # @cmd Positional with choices and value 117 | # @arg arg![`_fn_bars`] A arg with choices fn and required 118 | cmd_positional_with_choices_fn_and_required() { 119 | print_argc_vars 120 | } 121 | 122 | # @cmd Command without any arg 123 | cmd_without_any_arg() { 124 | print_argc_vars 125 | } 126 | 127 | # @cmd Command with alias 128 | # @alias a,alias 129 | cmd_alias() { 130 | print_argc_vars 131 | } 132 | 133 | # @cmd Command with hyphens 134 | # @arg hyphen-positional 135 | # @flag --hyphen-flag 136 | # @option --hyphen-option 137 | cmd_with_hyphens() { 138 | print_argc_vars 139 | } 140 | 141 | print_argc_vars() { 142 | ( set -o posix ; set ) | grep argc_ 143 | } 144 | 145 | _fn_foo() { 146 | echo "foo" 147 | } 148 | 149 | _fn_bars() { 150 | echo " a1 a2 a3 " 151 | } 152 | 153 | eval "$(argc "$0" "$@")" 154 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | const SPEC_SCRIPT: &str = include_str!("spec.sh"); 2 | 3 | mod fixtures; 4 | 5 | #[macro_use] 6 | mod macros; 7 | mod cli; 8 | mod compgen; 9 | mod create; 10 | #[cfg(unix)] 11 | mod interrupt; 12 | mod runmefile; 13 | --------------------------------------------------------------------------------