├── .editorconfig ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── ci ├── cargo-out-dir ├── macos-install-packages └── ubuntu-install-packages ├── examples ├── bash │ ├── complete │ ├── complete-heredoc │ ├── gh-clone-repos │ ├── gh-repo-list │ ├── naughty-send │ ├── sub │ └── vlen ├── complete.yml ├── elvish │ └── complete ├── fish │ └── complete ├── pwsh │ └── complete ├── simple.yml └── zsh │ ├── complete │ ├── complete-heredoc │ ├── gh-clone-repos │ ├── gh-repo-list │ ├── naughty-send │ ├── sub │ └── vlen └── src ├── app_wrapper.rs ├── config_checker.rs ├── dependencies.rs ├── ident_type.rs ├── main.rs └── shell.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # The way this works is a little weird. But basically, the create-release job 2 | # runs purely to initialize the GitHub release itself. Once done, the upload 3 | # URL of the release is saved as an artifact. 4 | # 5 | # The build-release job runs only once create-release is finished. It gets 6 | # the release upload URL by downloading the corresponding artifact (which was 7 | # uploaded by create-release). It then builds the release executables for each 8 | # supported platform and attaches them as release assets to the previously 9 | # created release. 10 | # 11 | # The key here is that we create the release only once. 12 | 13 | name: release 14 | on: 15 | push: 16 | # Enable when testing release infrastructure on a branch. 17 | # branches: 18 | # - ag/release 19 | tags: 20 | - "[0-9]+.[0-9]+.[0-9]+" 21 | jobs: 22 | create-release: 23 | name: create-release 24 | runs-on: ubuntu-latest 25 | # env: 26 | # Set to force version number, e.g., when no tag exists. 27 | # SLAP_VERSION: TEST-0.0.0 28 | steps: 29 | - name: Create artifacts directory 30 | run: mkdir artifacts 31 | 32 | - name: Get the release version from the tag 33 | if: env.SLAP_VERSION == '' 34 | run: | 35 | # Apparently, this is the right way to get a tag name. Really? 36 | # 37 | # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 38 | echo "::set-env name=SLAP_VERSION::${GITHUB_REF#refs/tags/}" 39 | echo "version is: ${{ env.SLAP_VERSION }}" 40 | - name: Create GitHub release 41 | id: release 42 | uses: actions/create-release@v1 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | with: 46 | tag_name: ${{ env.SLAP_VERSION }} 47 | release_name: ${{ env.SLAP_VERSION }} 48 | 49 | - name: Save release upload URL to artifact 50 | run: echo "${{ steps.release.outputs.upload_url }}" > artifacts/release-upload-url 51 | 52 | - name: Save version number to artifact 53 | run: echo "${{ env.SLAP_VERSION }}" > artifacts/release-version 54 | 55 | - name: Upload artifacts 56 | uses: actions/upload-artifact@v1 57 | with: 58 | name: artifacts 59 | path: artifacts 60 | 61 | build-release: 62 | name: build-release 63 | needs: ["create-release"] 64 | runs-on: ${{ matrix.os }} 65 | env: 66 | # For some builds, we use cross to test on 32-bit and big-endian 67 | # systems. 68 | CARGO: cargo 69 | # When CARGO is set to CROSS, this is set to `--target matrix.target`. 70 | TARGET_FLAGS: 71 | # When CARGO is set to CROSS, TARGET_DIR includes matrix.target. 72 | TARGET_DIR: ./target 73 | # Emit backtraces on panics. 74 | RUST_BACKTRACE: 1 75 | strategy: 76 | matrix: 77 | build: [linux, linux-arm, macos, win-msvc, win-gnu, win32-msvc] 78 | include: 79 | - build: linux 80 | os: ubuntu-18.04 81 | rust: nightly 82 | target: x86_64-unknown-linux-musl 83 | - build: linux-arm 84 | os: ubuntu-18.04 85 | rust: nightly 86 | target: arm-unknown-linux-gnueabihf 87 | - build: macos 88 | os: macos-latest 89 | rust: nightly 90 | target: x86_64-apple-darwin 91 | - build: win-msvc 92 | os: windows-2019 93 | rust: nightly 94 | target: x86_64-pc-windows-msvc 95 | - build: win-gnu 96 | os: windows-2019 97 | rust: nightly-x86_64-gnu 98 | target: x86_64-pc-windows-gnu 99 | - build: win32-msvc 100 | os: windows-2019 101 | rust: nightly 102 | target: i686-pc-windows-msvc 103 | 104 | steps: 105 | - name: Checkout repository 106 | uses: actions/checkout@v2 107 | with: 108 | fetch-depth: 1 109 | 110 | - name: Install packages (Ubuntu) 111 | if: matrix.os == 'ubuntu-18.04' 112 | run: | 113 | ci/ubuntu-install-packages 114 | - name: Install packages (macOS) 115 | if: matrix.os == 'macos-latest' 116 | run: | 117 | ci/macos-install-packages 118 | - name: Install Rust 119 | uses: actions-rs/toolchain@v1 120 | with: 121 | toolchain: ${{ matrix.rust }} 122 | profile: minimal 123 | override: true 124 | target: ${{ matrix.target }} 125 | 126 | - name: Use Cross 127 | # if: matrix.os != 'windows-2019' 128 | run: | 129 | # FIXME: to work around bugs in latest cross release, install master. 130 | # See: https://github.com/rust-embedded/cross/issues/357 131 | cargo install --git https://github.com/rust-embedded/cross 132 | echo "::set-env name=CARGO::cross" 133 | echo "::set-env name=TARGET_FLAGS::--target ${{ matrix.target }}" 134 | echo "::set-env name=TARGET_DIR::./target/${{ matrix.target }}" 135 | - name: Show command used for Cargo 136 | run: | 137 | echo "cargo command is: ${{ env.CARGO }}" 138 | echo "target flag is: ${{ env.TARGET_FLAGS }}" 139 | echo "target dir is: ${{ env.TARGET_DIR }}" 140 | - name: Get release download URL 141 | uses: actions/download-artifact@v1 142 | with: 143 | name: artifacts 144 | path: artifacts 145 | 146 | - name: Set release upload URL and release version 147 | shell: bash 148 | run: | 149 | release_upload_url="$(cat artifacts/release-upload-url)" 150 | echo "::set-env name=RELEASE_UPLOAD_URL::$release_upload_url" 151 | echo "release upload url: $RELEASE_UPLOAD_URL" 152 | release_version="$(cat artifacts/release-version)" 153 | echo "::set-env name=RELEASE_VERSION::$release_version" 154 | echo "release version: $RELEASE_VERSION" 155 | - name: Build release binary 156 | run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }} 157 | 158 | - name: Strip release binary (linux and macos) 159 | if: matrix.build == 'linux' || matrix.build == 'macos' 160 | run: strip "target/${{ matrix.target }}/release/slap" 161 | 162 | - name: Strip release binary (arm) 163 | if: matrix.build == 'linux-arm' 164 | run: | 165 | docker run --rm -v \ 166 | "$PWD/target:/target:Z" \ 167 | rustembedded/cross:arm-unknown-linux-gnueabihf \ 168 | arm-linux-gnueabihf-strip \ 169 | /target/arm-unknown-linux-gnueabihf/release/slap 170 | - name: Build archive 171 | shell: bash 172 | run: | 173 | outdir="$(ci/cargo-out-dir "${{ env.TARGET_DIR }}")" 174 | staging="slap-${{ env.RELEASE_VERSION }}-${{ matrix.target }}" 175 | mkdir -p "$staging" 176 | cp {README.md,LICENSE-APACHE,LICENSE-MIT} "$staging/" 177 | if [ "${{ matrix.os }}" = "windows-2019" ]; then 178 | cp "target/${{ matrix.target }}/release/slap.exe" "$staging/" 179 | 7z a "$staging.zip" "$staging" 180 | echo "::set-env name=ASSET::$staging.zip" 181 | else 182 | cp "target/${{ matrix.target }}/release/slap" "$staging/" 183 | tar czf "$staging.tar.gz" "$staging" 184 | echo "::set-env name=ASSET::$staging.tar.gz" 185 | fi 186 | - name: Upload release archive 187 | uses: actions/upload-release-asset@v1.0.1 188 | env: 189 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 190 | with: 191 | upload_url: ${{ env.RELEASE_UPLOAD_URL }} 192 | asset_path: ${{ env.ASSET }} 193 | asset_name: ${{ env.ASSET }} 194 | asset_content_type: application/octet-stream 195 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | newline_style = "Unix" 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.7.13" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" 8 | dependencies = [ 9 | "memchr", 10 | ] 11 | 12 | [[package]] 13 | name = "ansi_term" 14 | version = "0.11.0" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 17 | dependencies = [ 18 | "winapi", 19 | ] 20 | 21 | [[package]] 22 | name = "anyhow" 23 | version = "1.0.32" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" 26 | 27 | [[package]] 28 | name = "atty" 29 | version = "0.2.14" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 32 | dependencies = [ 33 | "hermit-abi", 34 | "libc", 35 | "winapi", 36 | ] 37 | 38 | [[package]] 39 | name = "bitflags" 40 | version = "1.2.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 43 | 44 | [[package]] 45 | name = "clap" 46 | version = "2.33.3" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 49 | dependencies = [ 50 | "ansi_term", 51 | "atty", 52 | "bitflags", 53 | "strsim", 54 | "textwrap", 55 | "unicode-width", 56 | "vec_map", 57 | "yaml-rust", 58 | ] 59 | 60 | [[package]] 61 | name = "hermit-abi" 62 | version = "0.1.15" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" 65 | dependencies = [ 66 | "libc", 67 | ] 68 | 69 | [[package]] 70 | name = "itoa" 71 | version = "0.4.6" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" 74 | 75 | [[package]] 76 | name = "lazy_static" 77 | version = "1.4.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 80 | 81 | [[package]] 82 | name = "libc" 83 | version = "0.2.76" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3" 86 | 87 | [[package]] 88 | name = "memchr" 89 | version = "2.3.3" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 92 | 93 | [[package]] 94 | name = "proc-macro2" 95 | version = "1.0.20" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "175c513d55719db99da20232b06cda8bab6b83ec2d04e3283edf0213c37c1a29" 98 | dependencies = [ 99 | "unicode-xid", 100 | ] 101 | 102 | [[package]] 103 | name = "quote" 104 | version = "1.0.7" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" 107 | dependencies = [ 108 | "proc-macro2", 109 | ] 110 | 111 | [[package]] 112 | name = "regex" 113 | version = "1.3.9" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" 116 | dependencies = [ 117 | "aho-corasick", 118 | "memchr", 119 | "regex-syntax", 120 | "thread_local", 121 | ] 122 | 123 | [[package]] 124 | name = "regex-syntax" 125 | version = "0.6.18" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" 128 | 129 | [[package]] 130 | name = "ryu" 131 | version = "1.0.5" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 134 | 135 | [[package]] 136 | name = "serde" 137 | version = "1.0.115" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" 140 | 141 | [[package]] 142 | name = "serde_json" 143 | version = "1.0.57" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" 146 | dependencies = [ 147 | "itoa", 148 | "ryu", 149 | "serde", 150 | ] 151 | 152 | [[package]] 153 | name = "slap-cli" 154 | version = "1.3.1" 155 | dependencies = [ 156 | "anyhow", 157 | "atty", 158 | "clap", 159 | "lazy_static", 160 | "regex", 161 | "serde_json", 162 | "termcolor", 163 | "which", 164 | "yaml-rust", 165 | ] 166 | 167 | [[package]] 168 | name = "strsim" 169 | version = "0.8.0" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 172 | 173 | [[package]] 174 | name = "syn" 175 | version = "1.0.39" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9" 178 | dependencies = [ 179 | "proc-macro2", 180 | "quote", 181 | "unicode-xid", 182 | ] 183 | 184 | [[package]] 185 | name = "termcolor" 186 | version = "1.1.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" 189 | dependencies = [ 190 | "winapi-util", 191 | ] 192 | 193 | [[package]] 194 | name = "textwrap" 195 | version = "0.11.0" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 198 | dependencies = [ 199 | "unicode-width", 200 | ] 201 | 202 | [[package]] 203 | name = "thiserror" 204 | version = "1.0.20" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" 207 | dependencies = [ 208 | "thiserror-impl", 209 | ] 210 | 211 | [[package]] 212 | name = "thiserror-impl" 213 | version = "1.0.20" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" 216 | dependencies = [ 217 | "proc-macro2", 218 | "quote", 219 | "syn", 220 | ] 221 | 222 | [[package]] 223 | name = "thread_local" 224 | version = "1.0.1" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 227 | dependencies = [ 228 | "lazy_static", 229 | ] 230 | 231 | [[package]] 232 | name = "unicode-width" 233 | version = "0.1.8" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 236 | 237 | [[package]] 238 | name = "unicode-xid" 239 | version = "0.2.1" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 242 | 243 | [[package]] 244 | name = "vec_map" 245 | version = "0.8.2" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 248 | 249 | [[package]] 250 | name = "which" 251 | version = "4.0.2" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "87c14ef7e1b8b8ecfc75d5eca37949410046e66f15d185c01d70824f1f8111ef" 254 | dependencies = [ 255 | "libc", 256 | "thiserror", 257 | ] 258 | 259 | [[package]] 260 | name = "winapi" 261 | version = "0.3.9" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 264 | dependencies = [ 265 | "winapi-i686-pc-windows-gnu", 266 | "winapi-x86_64-pc-windows-gnu", 267 | ] 268 | 269 | [[package]] 270 | name = "winapi-i686-pc-windows-gnu" 271 | version = "0.4.0" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 274 | 275 | [[package]] 276 | name = "winapi-util" 277 | version = "0.1.5" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 280 | dependencies = [ 281 | "winapi", 282 | ] 283 | 284 | [[package]] 285 | name = "winapi-x86_64-pc-windows-gnu" 286 | version = "0.4.0" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 289 | 290 | [[package]] 291 | name = "yaml-rust" 292 | version = "0.3.5" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "e66366e18dc58b46801afbf2ca7661a9f59cc8c5962c29892b6039b4f86fa992" 295 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "slap-cli" 3 | version = "1.3.1" 4 | authors = ["Matteo G. "] 5 | description = "Painless shell argument parsing and dependency check." 6 | edition = "2018" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/agnipau/slap" 9 | readme = "README.md" 10 | keywords = ["argument", "cli", "parse", "clap", "shell"] 11 | categories = ["command-line-utilities", "command-line-interface"] 12 | exclude = [ 13 | "examples/*", 14 | ] 15 | 16 | [dependencies] 17 | # Crucial to use this version. 18 | clap = { version = "2.33.3", features = ["yaml"] } 19 | # Crucial to use this version, the same that clap 2.33.3 uses. 20 | yaml-rust = "0.3.5" 21 | 22 | anyhow = "1.0.32" 23 | lazy_static = "1.4.0" 24 | regex = "1.3.9" 25 | which = "4.0.2" 26 | serde_json = "1.0.57" 27 | atty = { version = "0.2.14", optional = true } 28 | termcolor = { version = "1.1.0", optional = true } 29 | 30 | [features] 31 | default = ["color"] 32 | color = ["atty", "termcolor"] 33 | 34 | [[bin]] 35 | name = "slap" 36 | path = "src/main.rs" 37 | -------------------------------------------------------------------------------- /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, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the 13 | copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other 16 | entities that control, are controlled by, or are under common control with 17 | that entity. For the purposes of this definition, "control" means (i) the 18 | power, direct or indirect, to cause the direction or management of such 19 | entity, whether by contract or otherwise, or (ii) ownership of fifty percent 20 | (50%) or more of the outstanding shares, or (iii) beneficial ownership of 21 | such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity exercising 24 | 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 source, and 28 | configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical transformation 31 | or translation of a Source form, including but not limited to compiled 32 | object code, generated documentation, and conversions to other media types. 33 | 34 | "Work" shall mean the work of authorship, whether in Source or Object form, 35 | made available under the License, as indicated by a copyright notice that is 36 | included in or attached to the work (an example is provided in the Appendix 37 | below). 38 | 39 | "Derivative Works" shall mean any work, whether in Source or Object form, 40 | that is based on (or derived from) the Work and for which the editorial 41 | revisions, annotations, elaborations, or other modifications represent, as a 42 | whole, an original work of authorship. For the purposes of this License, 43 | Derivative Works shall not include works that remain separable from, or 44 | merely link (or bind by name) to the interfaces of, the Work and Derivative 45 | Works thereof. 46 | 47 | "Contribution" shall mean any work of authorship, including the original 48 | version of the Work and any modifications or additions to that Work or 49 | Derivative Works thereof, that is intentionally submitted to Licensor for 50 | inclusion in the Work by the copyright owner or by an individual or Legal 51 | Entity authorized to submit on behalf of the copyright owner. For the 52 | purposes of this definition, "submitted" means any form of electronic, 53 | verbal, or written communication sent to the Licensor or its 54 | representatives, including but not limited to communication on electronic 55 | mailing lists, source code control systems, and issue tracking systems that 56 | are managed by, or on behalf of, the Licensor for the purpose of discussing 57 | and improving the Work, but excluding communication that is conspicuously 58 | marked or otherwise designated in writing by the copyright owner as "Not a 59 | Contribution." 60 | 61 | "Contributor" shall mean Licensor and any individual or Legal Entity on 62 | behalf of whom a Contribution has been received by Licensor and subsequently 63 | incorporated within the Work. 64 | 65 | 2. Grant of Copyright License. Subject to the terms and conditions of this 66 | License, each Contributor hereby grants to You a perpetual, worldwide, 67 | non-exclusive, no-charge, royalty-free, irrevocable copyright license to 68 | reproduce, prepare Derivative Works of, publicly display, publicly perform, 69 | sublicense, and distribute the Work and such Derivative Works in Source or 70 | Object form. 71 | 72 | 3. Grant of Patent License. Subject to the terms and conditions of this 73 | License, each Contributor hereby grants to You a perpetual, worldwide, 74 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this 75 | section) patent license to make, have made, use, offer to sell, sell, import, 76 | and otherwise transfer the Work, where such license applies only to those 77 | patent claims licensable by such Contributor that are necessarily infringed by 78 | their Contribution(s) alone or by combination of their Contribution(s) with the 79 | Work to which such Contribution(s) was submitted. If You institute patent 80 | litigation against any entity (including a cross-claim or counterclaim in a 81 | lawsuit) alleging that the Work or a Contribution incorporated within the Work 82 | constitutes direct or contributory patent infringement, then any patent 83 | licenses granted to You under this License for that Work shall terminate as of 84 | the date such litigation is filed. 85 | 86 | 4. Redistribution. You may reproduce and distribute copies of the Work or 87 | Derivative Works thereof in any medium, with or without modifications, and in 88 | Source or Object form, provided that You meet the following conditions: 89 | 90 | (a) You must give any other recipients of the Work or Derivative Works a 91 | copy of this License; and 92 | 93 | (b) You must cause any modified files to carry prominent notices stating 94 | that You changed the files; and 95 | 96 | (c) You must retain, in the Source form of any Derivative Works that You 97 | distribute, all copyright, patent, trademark, and attribution notices from 98 | the Source form of the Work, excluding those notices that do not pertain to 99 | any part of the Derivative Works; and 100 | 101 | (d) If the Work includes a "NOTICE" text file as part of its distribution, 102 | then any Derivative Works that You distribute must include a readable copy 103 | of the attribution notices contained within such NOTICE file, excluding 104 | those notices that do not pertain to any part of the Derivative Works, in at 105 | least one of the following places: within a NOTICE text file distributed as 106 | part of the Derivative Works; within the Source form or documentation, if 107 | provided along with the Derivative Works; or, within a display generated by 108 | the Derivative Works, if and wherever such third-party notices normally 109 | appear. The contents of the NOTICE file are for informational purposes only 110 | and do not modify the License. You may add Your own attribution notices 111 | within Derivative Works that You distribute, alongside or as an addendum to 112 | the NOTICE text from the Work, provided that such additional attribution 113 | notices cannot be construed as modifying the License. 114 | 115 | You may add Your own copyright statement to Your modifications and may 116 | provide additional or different license terms and conditions for use, 117 | reproduction, or distribution of Your modifications, or for any such 118 | Derivative Works as a whole, provided Your use, reproduction, and 119 | distribution of the Work otherwise complies with the conditions stated in 120 | this License. 121 | 122 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 123 | Contribution intentionally submitted for inclusion in the Work by You to the 124 | Licensor shall be under the terms and conditions of this License, without any 125 | additional terms or conditions. Notwithstanding the above, nothing herein 126 | shall supersede or modify the terms of any separate license agreement you may 127 | have executed with Licensor regarding such Contributions. 128 | 129 | 6. Trademarks. This License does not grant permission to use the trade names, 130 | trademarks, service marks, or product names of the Licensor, except as required 131 | for reasonable and customary use in describing the origin of the Work and 132 | reproducing the content of the NOTICE file. 133 | 134 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in 135 | writing, Licensor provides the Work (and each Contributor provides its 136 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 137 | KIND, either express or implied, including, without limitation, any warranties 138 | or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 139 | PARTICULAR PURPOSE. You are solely responsible for determining the 140 | appropriateness of using or redistributing the Work and assume any risks 141 | associated with Your exercise of permissions under this License. 142 | 143 | 8. Limitation of Liability. In no event and under no legal theory, whether in 144 | tort (including negligence), contract, or otherwise, unless required by 145 | applicable law (such as deliberate and grossly negligent acts) or agreed to in 146 | writing, shall any Contributor be liable to You for damages, including any 147 | direct, indirect, special, incidental, or consequential damages of any 148 | character arising as a result of this License or out of the use or inability to 149 | use the Work (including but not limited to damages for loss of goodwill, work 150 | stoppage, computer failure or malfunction, or any and all other commercial 151 | damages or losses), even if such Contributor has been advised of the 152 | possibility of such damages. 153 | 154 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or 155 | Derivative Works thereof, You may choose to offer, and charge a fee for, 156 | acceptance of support, warranty, indemnity, or other liability obligations 157 | and/or rights consistent with this License. However, in accepting such 158 | obligations, You may act only on Your own behalf and on Your sole 159 | responsibility, not on behalf of any other Contributor, and only if You agree 160 | to indemnify, defend, and hold each Contributor harmless for any liability 161 | incurred by, or claims asserted against, such Contributor by reason of your 162 | accepting any such warranty or additional liability. 163 | 164 | END OF TERMS AND CONDITIONS 165 | 166 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Matteo Guarda 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # slap 2 | 3 | ![Batman slapping Robin meme](https://raw.githubusercontent.com/agnipau/slap/screenshots/batman-slapping-robin.jpg) 4 | 5 | slap (shell [`clap`][clap]) - painless argument parsing and dependency check. 6 | 7 | ## Why? 8 | 9 | Writing code to parse arguments in a shell scripting language (`bash`, `zsh`, 10 | `fish` etc...) is an extremly verbose, repetitive, error prone, and painful 11 | process. 12 | This program aims to improve that. 13 | 14 | ## Can't I just use `getopt` or something similiar? 15 | 16 | `getopt` and similiar tools are not only language-dependent (slap works with a number of shells), but are also just tools to write your own argument parser. 17 | slap is different, in the sense that it puts at your disposal an already written argument parser. 18 | Good luck in writing in bash, powershell, fish or whatever, a full fledged argument parser (using `getopt` if you want) that takes in consideration things like argument order, possible values, subcommands, argument groups, automatic color-enabled help menus, most similiar argument name suggestions etc. 19 | 20 | ## How? 21 | 22 | You declare your CLI in YAML and pass it to slap's `stdin` and pass all your 23 | script's arguments to slap as arguments. 24 | slap makes sure that the arguments you pass to it conform to your YAML 25 | description, and if not, it exits with an error code and outputs useful error 26 | messages to `stderr`. 27 | In other words slap handles the argument parsing logic and validation, your 28 | script only evalutes the code exported by slap and uses the parsed arguments. 29 | Here is an example bash script: 30 | 31 | ```bash 32 | config="path to your YAML config" 33 | eval "$(slap parse bash -- "$@" <"$config")" 34 | ``` 35 | 36 | The `slap-parse` subcommand, if the passed arguments conform to the YAML 37 | description, outputs code in the language specified, so you can evaluate it to 38 | have access to the variables containing the parsed arguments. 39 | Relax, slap writes to `stdout` ONLY if the YAML config is valid and the 40 | arguments passed conform to it, otherwise it doesn't. 41 | 42 | ## Installation 43 | 44 | If you're an **Arch Linux** user, you can install slap from the [AUR](https://aur.archlinux.org/packages/slap-cli-bin/): 45 | 46 | ```bash 47 | # This will install a binary named `slap`. 48 | yay -S slap-cli-bin 49 | ``` 50 | 51 | If you're a **Rust programmer**, you can install slap with `cargo`. 52 | Make sure to add `~/.cargo/bin` to your `$PATH`. 53 | 54 | ```bash 55 | # This will install a binary named `slap`. 56 | cargo install slap-cli 57 | ``` 58 | 59 | You can also download a pre-compiled binary (for `linux`, `linux-arm`, `macos`, 60 | `win-msvc`, `win-gnu`, `win32-msvc`) from the 61 | [Releases](https://github.com/agnipau/slap/releases). 62 | 63 | ## Supported platforms 64 | 65 | At the moment slap supports `bash`, `zsh`, `fish`, `elvish` and `powershell`. 69 | We are planning to support more shells. 70 | If your favourite shell is not supported, make sure to open an issue. 71 | 72 | ## Completions script generation 73 | 74 | Thanks to [clap][clap], slap's underlying engine, automatic 75 | completions-script generation is supported. 76 | For example in bash: 77 | 78 | ```bash 79 | config="path to your YAML config" 80 | slap completions bash <"$config" >completions.bash 81 | ``` 82 | 83 | `completions.bash` now contains a bash script that provides command 84 | autocompletion for the CLI described in your YAML config file. 85 | 86 | ## Dependency check 87 | 88 | If your script depends on some programs you can check if they are in `$PATH` 89 | with the `deps` subcommand: 90 | 91 | ```bash 92 | slap deps curl jq || exit 1 93 | ``` 94 | 95 | If `curl` and `jq` are found in `$PATH` the script will continue its execution 96 | and nothing will be printed, otherwise an error will be written to `stderr` and 97 | slap will exit with a non-zero exit code. 98 | 99 | ## Absolute path of a script 100 | 101 | slap includes a `path` subcommand that simplifies getting the absolute path of 102 | a script: 103 | 104 | ```bash 105 | # before 106 | abs="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 107 | ``` 108 | 109 | ``` 110 | # with slap 111 | abs="$(slap path -d "${BASH_SOURCE[0]}")" 112 | ``` 113 | 114 | Both the snippets never dereference symlinks. If you want to dereference symlinks use the `-D` option. 115 | 116 | ## Demo 117 | 118 | [![asciicast](https://asciinema.org/a/357515.svg)](https://asciinema.org/a/357515) 119 | 120 | ## Example 121 | 122 | Here are two useful bash scripts: 123 | 124 | ```bash 125 | slap deps curl jq || exit 1 126 | 127 | eval "$(slap parse bash _ -- "$@" <<-EOF 128 | name: gh-repo-list 129 | version: "1.0" 130 | about: Outputs JSON containing useful informations about your GitHub repos. 131 | 132 | settings: 133 | - ArgRequiredElseHelp 134 | - ColorAuto 135 | 136 | global_settings: 137 | - ColoredHelp 138 | 139 | args: 140 | - username: 141 | help: your GitHub username 142 | required: true 143 | - password: 144 | help: your GitHub password 145 | required: true 146 | - iterations: 147 | help: the number of iterations to do. 0 means there is no limit 148 | long: iterations 149 | short: i 150 | default_value: "0" 151 | EOF 152 | )"; [[ -z "${_success}" ]] && exit 1 153 | 154 | page=1 155 | while :; do 156 | data="$(curl -s -X GET \ 157 | -u "${_username_vals}:${_password_vals}" \ 158 | "https://api.github.com/user/repos?page=${page}&per_page100&type=all")" 159 | len="$(printf '%s\n' "${data}" | jq '. | length')" 160 | [[ "${_iterations_vals}" == "0" && "${len}" == 0 ]] && break 161 | printf '%s\n' "${data}" 162 | [[ "${page}" == "${_iterations_vals}" ]] && break 163 | page="$((page + 1))" 164 | done 165 | ``` 166 | 167 | ```bash 168 | slap deps jq git || exit 1 169 | 170 | eval "$(slap parse bash _ -- "$@" <<-EOF 171 | name: gh-clone-repos 172 | version: "1.0" 173 | about: Uses 'gh-repo-list' to clone all your GitHub repos. 174 | 175 | settings: 176 | - ArgRequiredElseHelp 177 | - ColorAuto 178 | 179 | global_settings: 180 | - ColoredHelp 181 | 182 | args: 183 | - username: 184 | help: your GitHub username 185 | required: true 186 | - password: 187 | help: your GitHub password 188 | required: true 189 | - git_options: 190 | help: "additional Git options (for example: --git-options '--depth 1')" 191 | long: git-options 192 | takes_value: true 193 | short: o 194 | allow_hyphen_values: true 195 | EOF 196 | )"; [[ -z "${_success}" ]] && exit 1 197 | 198 | for repo in $(gh-repo-list "${_username_vals}" "${_password_vals}" \ 199 | | jq -r "map(.ssh_url) | join(\"\n\")"); do 200 | if [[ -n "${_git_options_occurs}" ]]; then 201 | eval "git clone ${_git_options_vals} ${repo}" 202 | else 203 | git clone "${repo}" 204 | fi 205 | done 206 | ``` 207 | 208 | ## Learning material 209 | 210 | This YAML config probably contains all the 211 | options you'll ever need. 212 | For additional informations look at [`clap`'s 213 | docs](https://docs.rs/clap/2.33.3/clap). 214 | 215 | For `powershell`, `fish`, `zsh` and other 217 | examples look here. 218 | 219 | ## Elvish 220 | 221 | As of `v0.14.1`, elvish doesn't support `eval` yet, so you can use slap to 222 | generate elvish code, but you can't yet use the generated code inside an 223 | elvish script. 224 | Luckily there is some work going on for this functionality. 225 | 226 | ## Credits 227 | 228 | This program is solely made possible by [clap][clap], so many thanks to its 229 | authors. 230 | 231 | #### License 232 | 233 | 234 | Licensed under either of Apache License, Version 235 | 2.0 or MIT license at your option. 236 | 237 | 238 |
239 | 240 | 241 | Unless you explicitly state otherwise, any contribution intentionally submitted 242 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 243 | be dual licensed as above, without any additional terms or conditions. 244 | 245 | 246 | [clap]: https://github.com/clap-rs/clap 247 | -------------------------------------------------------------------------------- /ci/cargo-out-dir: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Finds Cargo's `OUT_DIR` directory from the most recent build. 4 | # 5 | # This requires one parameter corresponding to the target directory 6 | # to search for the build output. 7 | 8 | if [ $# != 1 ]; then 9 | echo "Usage: $(basename "$0") " >&2 10 | exit 2 11 | fi 12 | 13 | # This works by finding the most recent stamp file, which is produced by 14 | # every slap build. 15 | target_dir="$1" 16 | find "$target_dir" -name slap-stamp -print0 \ 17 | | xargs -0 ls -t \ 18 | | head -n1 \ 19 | | xargs dirname 20 | -------------------------------------------------------------------------------- /ci/macos-install-packages: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | brew install asciidoctor 4 | -------------------------------------------------------------------------------- /ci/ubuntu-install-packages: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sudo apt-get update 4 | sudo apt-get install -y --no-install-recommends \ 5 | asciidoctor \ 6 | zsh xz-utils liblz4-tool musl-tools 7 | -------------------------------------------------------------------------------- /examples/bash/complete: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2154,SC2250 3 | 4 | config="$(cargo r -q -- path -d "${BASH_SOURCE[0]}")/../complete.yml" 5 | eval "$(cargo r -q -- parse bash _ -- "$@" <"$config")" 6 | [[ -z "$_success" ]] && exit 1 7 | 8 | printf '%s\n' \ 9 | "opt = '${_opt_vals[@]}' 10 | pos = '$_pos_vals' 11 | flag = '${_flag_vals[@]}' 12 | mode = '$_mode_vals' 13 | mvals = '$_mvals_vals' 14 | minvals = '${_minvals_vals[@]}' 15 | maxvals = '${_maxvals_vals[@]}' 16 | 17 | subcommand -> '$_subcommand' 18 | subcmd_scopt = '${_subcmd_scopt_vals[@]}' 19 | subcmd_scpos1 = '$_subcmd_scpos1_vals'" 20 | 21 | -------------------------------------------------------------------------------- /examples/bash/complete-heredoc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2154,SC2250 3 | 4 | eval "$(cargo r -q -- parse bash _ -- "$@" <<-EOF 5 | name: yml_app 6 | version: "1.0" 7 | about: An example using a .yml file to build a CLI 8 | author: Kevin K. 9 | 10 | # AppSettings can be defined as a list and are **not** ascii case sensitive 11 | settings: 12 | - ArgRequiredElseHelp 13 | - ColorAuto 14 | 15 | global_settings: 16 | - ColoredHelp 17 | 18 | # All Args must be defined in the 'args:' list where the name of the arg, is the 19 | # key to a Hash object 20 | args: 21 | # The name of this argument, is 'opt' which will be used to access the value 22 | # later in your Rust code 23 | - opt: 24 | help: Example option argument from yaml 25 | short: o 26 | long: option 27 | multiple: true 28 | takes_value: true 29 | - pos: 30 | help: Example positional argument from yaml 31 | index: 1 32 | # A list of possible values can be defined as a list 33 | possible_values: 34 | - fast 35 | - slow 36 | - flag: 37 | help: Demo flag argument 38 | short: F 39 | multiple: true 40 | global: true 41 | # Conflicts, mutual overrides, and requirements can all be defined as a 42 | # list, where the key is the name of the other argument 43 | conflicts_with: 44 | - opt 45 | requires: 46 | - pos 47 | - mode: 48 | long: mode 49 | help: Shows an option with specific values 50 | # possible_values can also be defined in this list format 51 | possible_values: [vi, emacs] 52 | takes_value: true 53 | - mvals: 54 | long: mult-vals 55 | help: Demos an option which has two named values 56 | # value names can be described in a list, where the help will be shown 57 | # --mult-vals 58 | value_names: 59 | - one 60 | - two 61 | - minvals: 62 | long: min-vals 63 | multiple: true 64 | help: You must supply at least two values to satisfy me 65 | min_values: 2 66 | - maxvals: 67 | long: max-vals 68 | multiple: true 69 | help: You can only supply a max of 3 values for me! 70 | max_values: 3 71 | 72 | # All subcommands must be listed in the 'subcommand:' object, where the key to 73 | # the list is the name of the subcommand, and all settings for that command are 74 | # are part of a Hash object 75 | subcommands: 76 | # The name of this subcommand will be 'subcmd' which can be accessed in your 77 | # Rust code later 78 | - subcmd: 79 | about: demos subcommands from yaml 80 | version: "0.1" 81 | author: Kevin K. 82 | # Subcommand args are exactly like App args 83 | args: 84 | - scopt: 85 | short: B 86 | multiple: true 87 | help: Example subcommand option 88 | takes_value: true 89 | - scpos1: 90 | help: Example subcommand positional 91 | index: 1 92 | 93 | # ArgGroups are supported as well, and must be specified in the 'groups:' 94 | # object of this file 95 | groups: 96 | # the name of the ArgGoup is specified here 97 | - min-max-vals: 98 | # All args and groups that are a part of this group are set here 99 | args: 100 | - minvals 101 | - maxvals 102 | # setting conflicts is done the same manner as setting 'args:' 103 | # 104 | # to make this group required, you could set 'required: true' but for 105 | # this example we won't do that. 106 | EOF 107 | )"; [[ -z "$_success" ]] && exit 1 108 | 109 | printf '%s\n' \ 110 | "opt = '${_opt_vals[@]}' 111 | pos = '$_pos_vals' 112 | flag = '${_flag_vals[@]}' 113 | mode = '$_mode_vals' 114 | mvals = '$_mvals_vals' 115 | minvals = '${_minvals_vals[@]}' 116 | maxvals = '${_maxvals_vals[@]}' 117 | 118 | subcommand -> '$_subcommand' 119 | subcmd_scopt = '${_subcmd_scopt_vals[@]}' 120 | subcmd_scpos1 = '$_subcmd_scpos1_vals'" 121 | 122 | -------------------------------------------------------------------------------- /examples/bash/gh-clone-repos: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2154 3 | 4 | cargo r -q -- deps jq git || exit 1 5 | 6 | eval "$(cargo r -q -- parse bash _ -- "$@" <<-EOF 7 | name: gh-clone-repos 8 | version: "1.0" 9 | author: Matteo G. 10 | about: Uses 'gh-repo-list' to clone all your GitHub repos. 11 | 12 | settings: 13 | - ArgRequiredElseHelp 14 | - ColorAuto 15 | 16 | global_settings: 17 | - ColoredHelp 18 | 19 | args: 20 | - username: 21 | help: Your GitHub username 22 | required: true 23 | - password: 24 | help: Your GitHub password 25 | required: true 26 | - git_options: 27 | help: "Additional Git options (for example: --git-options '--depth 1')" 28 | long: git-options 29 | takes_value: true 30 | short: o 31 | allow_hyphen_values: true 32 | EOF 33 | )"; [[ -z "${_success}" ]] && exit 1 34 | 35 | for repo in $(gh-repo-list "${_username_vals}" "${_password_vals}" \ 36 | | jq -r "map(.ssh_url) | join(\"\n\")"); do 37 | if [[ -n "${_git_options_occurs}" ]]; then 38 | eval "git clone ${_git_options_vals} ${repo}" 39 | else 40 | git clone "${repo}" 41 | fi 42 | done 43 | 44 | -------------------------------------------------------------------------------- /examples/bash/gh-repo-list: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2154 3 | 4 | cargo r -q -- deps curl jq || exit 1 5 | 6 | eval "$(cargo r -q -- parse bash _ -- "$@" <<-EOF 7 | name: gh-repo-list 8 | version: "1.0" 9 | author: Matteo G. 10 | about: Outputs JSON containing useful informations about your GitHub repos. 11 | 12 | settings: 13 | - ArgRequiredElseHelp 14 | - ColorAuto 15 | 16 | global_settings: 17 | - ColoredHelp 18 | 19 | args: 20 | - username: 21 | help: Your GitHub username 22 | required: true 23 | - password: 24 | help: Your GitHub password 25 | required: true 26 | - iterations: 27 | help: The number of iterations to do. 0 means there is no limit 28 | long: iterations 29 | short: i 30 | default_value: "0" 31 | EOF 32 | )"; [[ -z "${_success}" ]] && exit 1 33 | 34 | page=1 35 | while :; do 36 | data="$(curl -s -X GET \ 37 | -u "${_username_vals}:${_password_vals}" \ 38 | "https://api.github.com/user/repos?page=${page}&per_page100&type=all")" 39 | len="$(printf '%s\n' "${data}" | jq '. | length')" 40 | [[ "${_iterations_vals}" == "0" && "${len}" == 0 ]] && break 41 | printf '%s\n' "${data}" 42 | [[ "${page}" == "${_iterations_vals}" ]] && break 43 | page="$((page + 1))" 44 | done 45 | 46 | -------------------------------------------------------------------------------- /examples/bash/naughty-send: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2154 3 | # 4 | # See: https://awesomewm.org/doc/api/libraries/naughty.html 5 | 6 | cargo r -q -- deps awsome-client cut || exit 1 7 | 8 | eval "$(cargo r -q -- parse bash _ -- "$@" <<-EOF 9 | name: naughty-send 10 | version: "1.0" 11 | author: Matteo G. 12 | about: "CLI similiar to notify-send that sends notifications to Awesome WM using naughty.notify()" 13 | 14 | settings: 15 | - ArgRequiredElseHelp 16 | - ColorAuto 17 | 18 | global_settings: 19 | - ColoredHelp 20 | 21 | args: 22 | - text: 23 | help: "(string) Text of the notification." 24 | default_value: "" 25 | - title: 26 | help: "(string) Title of the notification. (optional)" 27 | default_value: nil 28 | - timeout: 29 | help: "(int) Time in seconds after which popup expires. Set 0 for no timeout." 30 | default_value: "5" 31 | - hover_timeout: 32 | help: "(int) Delay in seconds after which hovered popup disappears. (optional)" 33 | default_value: nil 34 | - screen: 35 | help: "(integer or screen) Target screen for the notification." 36 | default_value: focused 37 | - position: 38 | help: "(string) Corner of the workarea displaying the popups. (default \"top_right\")" 39 | possible_values: 40 | - top_right 41 | - top_left 42 | - bottom_left 43 | - bottom_right 44 | - top_middle 45 | - bottom_middle 46 | default_value: top_right 47 | - ontop: 48 | help: "(bool) Boolean forcing popups to display on top." 49 | possible_values: 50 | - "true" 51 | - "false" 52 | default_value: "true" 53 | - height: 54 | help: "(int) Popup height. (default \`beautiful.notification_height\` or auto)" 55 | default_value: nil 56 | - width: 57 | help: "(int) Popup width. (default \`beautiful.notification_width\` or auto)" 58 | default_value: nil 59 | - max_height: 60 | help: "(int) Popup maximum height. (default \`beautiful.notification_max_height\` or auto)" 61 | default_value: nil 62 | - max_width: 63 | help: "(int) Popup maximum width. (default \`beautiful.notification_max_width\` or auto)" 64 | default_value: nil 65 | - font: 66 | help: "(string) Notification font. (default \`beautiful.notification_font\` or \`beautiful.font\` or \`awesome.font\`)" 67 | default_value: nil 68 | - icon: 69 | help: "(string) Path to icon. (optional)" 70 | default_value: nil 71 | - icon_size: 72 | help: "(int) Desired icon size in px. (optional)" 73 | default_value: nil 74 | - fg: 75 | help: "(string) Foreground color. (default \`beautiful.notification_fg\` or \`beautiful.fg_focus\` or \`'#ffffff'\`)" 76 | default_value: nil 77 | - bg: 78 | help: "(string) Background color. (default \`beautiful.notification_fg\` or \`beautiful.bg_focus\` or \`'#535d6c'\`)" 79 | default_value: nil 80 | - border_width: 81 | help: "(int) Border width. (default \`beautiful.notification_border_width\` or 1)" 82 | default_value: nil 83 | - border_color: 84 | help: "(string) Border color. (default \`beautiful.notification_border_color\` or \`beautiful.border_focus\` or \`'#535d6c'\`)" 85 | default_value: nil 86 | - shape: 87 | help: "(gears.shape) Widget shape. (default \`beautiful.notification_shape\`)" 88 | default_value: nil 89 | - opacity: 90 | help: "(gears.opacity) Widget opacity. (default \`beautiful.notification_opacity\`)" 91 | default_value: nil 92 | - margin: 93 | help: "(gears.margin) Widget margin. (default \`beautiful.notification_margin\`)" 94 | default_value: nil 95 | - run: 96 | help: "(func) Function to run on left click. The notification object will be passed to it as an argument. You need to call e.g. notification.die(naughty.notificationClosedReason.dismissedByUser) from there to dismiss the notification yourself. (optional)" 97 | default_value: nil 98 | - destroy: 99 | help: "(func) Function to run when notification is destroyed. (optional)" 100 | default_value: nil 101 | - preset: 102 | help: "(table) Table with any of the above parameters. Note: Any parameters specified directly in args will override ones defined in the preset. (optional)" 103 | default_value: nil 104 | - replaces_id: 105 | help: "(int) Replace the notification with the given ID. (optional)" 106 | default_value: nil 107 | - callback: 108 | help: "(func) Function that will be called with all arguments. The notification will only be displayed if the function returns true. Note: this function is only relevant to notifications sent via dbus. (optional)" 109 | default_value: nil 110 | - actions: 111 | help: "(table) Mapping that maps a string to a callback when this action is selected. (optional)" 112 | default_value: nil 113 | - ignore_suspend: 114 | help: "(bool) If set to true this notification will be shown even if notifications are suspended via naughty.suspend." 115 | possible_values: 116 | - "true" 117 | - "false" 118 | default_value: "false" 119 | EOF 120 | )"; [[ -z "${_success}" ]] && exit 1 121 | 122 | quote_or_nil() { 123 | occurs="_$1_occurs" 124 | vals="_$1_vals" 125 | if [[ "${!occurs}" -gt 0 ]]; then 126 | printf '%s' "\"${!vals}\"" 127 | else 128 | if [[ -n "${!vals}" ]]; then 129 | printf '%s\n' "${!vals}" 130 | else 131 | printf nil 132 | fi 133 | fi 134 | } 135 | 136 | printf '%s\n%s\n%s\n' \ 137 | "local naughty = require('naughty')" \ 138 | "local notification = naughty.notify({ 139 | text = $(quote_or_nil text), 140 | title = $(quote_or_nil title), 141 | timeout = ${_timeout_vals}, 142 | hover_timeout = ${_hover_timeout_vals}, 143 | screen = $(quote_or_nil screen), 144 | position = $(quote_or_nil position), 145 | ontop = ${_ontop_vals}, 146 | height = ${_height_vals}, 147 | width = ${_width_vals}, 148 | max_height = ${_max_height_vals}, 149 | max_width = ${_max_width_vals}, 150 | font = $(quote_or_nil font), 151 | icon = $(quote_or_nil icon), 152 | icon_size = ${_icon_size_vals}, 153 | fg = $(quote_or_nil fg), 154 | bg = $(quote_or_nil bg), 155 | border_width = ${_border_width_vals}, 156 | border_color = $(quote_or_nil border_color), 157 | shape = ${_shape_vals}, 158 | opacity = ${_opacity_vals}, 159 | margin = ${_margin_vals}, 160 | run = ${_run_vals}, 161 | destroy = ${_destroy_vals}, 162 | preset = ${_preset_vals}, 163 | replaces_id = ${_replaces_id_vals}, 164 | callback = ${_callback_vals}, 165 | actions = ${_actions_vals}, 166 | ignore_suspend = ${_ignore_suspend_vals} 167 | })" \ 168 | "return notification.id" \ 169 | | awesome-client \ 170 | | cut -d' ' -f5 171 | 172 | 173 | -------------------------------------------------------------------------------- /examples/bash/sub: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2154 3 | 4 | cargo r -q -- deps rg xargs sed || exit 1 5 | 6 | eval "$(cargo r -q -- parse bash _ -- "$@" <<-EOF 7 | name: sub 8 | version: "1.0" 9 | author: Matteo G. 10 | about: Searches for a pattern using ripgrep and substitutes all occurrences with a string. 11 | 12 | settings: 13 | - ArgRequiredElseHelp 14 | - ColorAuto 15 | 16 | global_settings: 17 | - ColoredHelp 18 | 19 | args: 20 | - pattern: 21 | help: The pattern to search for 22 | required: true 23 | - substitute: 24 | help: The text to substitute the matches with 25 | required: true 26 | - dry_run: 27 | help: Instead of modifying the files right away it shows you a preview of the changes 28 | short: d 29 | long: dry-run 30 | EOF 31 | )"; [[ -z "${_success}" ]] && exit 1 32 | 33 | if [[ -n "${_dry_run_occurs}" ]]; then 34 | rg "${_pattern_vals}" | sed "s/${_pattern_vals}/${_substitute_vals}/g" 35 | else 36 | rg "${_pattern_vals}" --files-with-matches \ 37 | | xargs sed -i "s/${_pattern_vals}/${_substitute_vals}/g" 38 | fi 39 | 40 | -------------------------------------------------------------------------------- /examples/bash/vlen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2154 3 | 4 | cargo r -q -- deps fd ffprobe paste bc || exit 1 5 | 6 | eval "$(cargo r -q -- parse bash _ -- "$@" <<-EOF 7 | name: vlen 8 | version: "1.0" 9 | author: Matteo G. 10 | about: Searches for all videos starting from a given directory and outputs the cumulative duration. 11 | 12 | settings: 13 | - ArgRequiredElseHelp 14 | - ColorAuto 15 | 16 | global_settings: 17 | - ColoredHelp 18 | 19 | args: 20 | - dir: 21 | help: The dir where to start searching from 22 | default_value: . 23 | EOF 24 | )"; [[ -z "${_success}" ]] && exit 1 25 | 26 | fd "${_dir_vals}" -e mkv -e mp4 -e webm -x \ 27 | ffprobe -v quiet -of csv=p=0 -show_entries format=duration {} \; \ 28 | | paste -sd+ - \ 29 | | bc 30 | 31 | -------------------------------------------------------------------------------- /examples/complete.yml: -------------------------------------------------------------------------------- 1 | # Example taken from: https://github.com/clap-rs/clap/blob/v2.33.1/examples/17_yaml.yml 2 | 3 | name: yml_app 4 | version: "1.0" 5 | about: An example using a .yml file to build a CLI 6 | author: Kevin K. 7 | 8 | # AppSettings can be defined as a list and are **not** ascii case sensitive 9 | settings: 10 | - ArgRequiredElseHelp 11 | - ColorAuto 12 | 13 | global_settings: 14 | - ColoredHelp 15 | 16 | # All Args must be defined in the 'args:' list where the name of the arg, is the 17 | # key to a Hash object 18 | args: 19 | # The name of this argument, is 'opt' which will be used to access the value 20 | # later in your Rust code 21 | - opt: 22 | help: Example option argument from yaml 23 | short: o 24 | long: option 25 | multiple: true 26 | takes_value: true 27 | - pos: 28 | help: Example positional argument from yaml 29 | index: 1 30 | # A list of possible values can be defined as a list 31 | possible_values: 32 | - fast 33 | - slow 34 | - flag: 35 | help: Demo flag argument 36 | short: F 37 | multiple: true 38 | global: true 39 | # Conflicts, mutual overrides, and requirements can all be defined as a 40 | # list, where the key is the name of the other argument 41 | conflicts_with: 42 | - opt 43 | requires: 44 | - pos 45 | - mode: 46 | long: mode 47 | help: Shows an option with specific values 48 | # possible_values can also be defined in this list format 49 | possible_values: [vi, emacs] 50 | takes_value: true 51 | - mvals: 52 | long: mult-vals 53 | help: Demos an option which has two named values 54 | # value names can be described in a list, where the help will be shown 55 | # --mult-vals 56 | value_names: 57 | - one 58 | - two 59 | - minvals: 60 | long: min-vals 61 | multiple: true 62 | help: You must supply at least two values to satisfy me 63 | min_values: 2 64 | - maxvals: 65 | long: max-vals 66 | multiple: true 67 | help: You can only supply a max of 3 values for me! 68 | max_values: 3 69 | 70 | # All subcommands must be listed in the 'subcommand:' object, where the key to 71 | # the list is the name of the subcommand, and all settings for that command are 72 | # are part of a Hash object 73 | subcommands: 74 | # The name of this subcommand will be 'subcmd' which can be accessed in your 75 | # Rust code later 76 | - subcmd: 77 | about: demos subcommands from yaml 78 | version: "0.1" 79 | author: Kevin K. 80 | # Subcommand args are exactly like App args 81 | args: 82 | - scopt: 83 | short: B 84 | multiple: true 85 | help: Example subcommand option 86 | takes_value: true 87 | - scpos1: 88 | help: Example subcommand positional 89 | index: 1 90 | 91 | # ArgGroups are supported as well, and must be specified in the 'groups:' 92 | # object of this file 93 | groups: 94 | # the name of the ArgGoup is specified here 95 | - min-max-vals: 96 | # All args and groups that are a part of this group are set here 97 | args: 98 | - minvals 99 | - maxvals 100 | # setting conflicts is done the same manner as setting 'args:' 101 | # 102 | # to make this group required, you could set 'required: true' but for 103 | # this example we won't do that. 104 | -------------------------------------------------------------------------------- /examples/elvish/complete: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env elvish 2 | 3 | echo "As of v0.14.1, elvish doesn't support 'eval' yet" 4 | 5 | -------------------------------------------------------------------------------- /examples/fish/complete: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env fish 2 | 3 | set config (printf '%s/../complete.yml' (cargo r -q -- path -d (status -f))) 4 | cargo r -q -- parse fish _ -- $argv <$config | source 5 | [ -z "$_success" ] && exit 1 6 | 7 | printf '%s\n' \ 8 | "opt = '$_opt_vals' 9 | pos = '$_pos_vals' 10 | flag = '$_flag_vals' 11 | mode = '$_mode_vals' 12 | mvals = '$_mvals_vals' 13 | minvals = '$_minvals_vals' 14 | maxvals = '$_maxvals_vals' 15 | 16 | subcommand -> '$_subcommand' 17 | subcmd_scopt = '$_subcmd_scopt_vals' 18 | subcmd_scpos1 = '$_subcmd_scpos1_vals'" 19 | 20 | -------------------------------------------------------------------------------- /examples/pwsh/complete: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | $config = "$($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath('.\'))/../complete.yml" 4 | $code = $(cat $config ` 5 | | cargo r -q -- parse pwsh _ -- $args ` 6 | | Out-String) 7 | if ([string]::IsNullOrWhiteSpace($code)) { 8 | Exit 1 9 | } 10 | Invoke-Expression $code 11 | if ([string]::IsNullOrWhiteSpace($_success)) { 12 | Exit 1 13 | } 14 | 15 | Write-Output ` 16 | "opt = '$_opt_vals' 17 | pos = '$_pos_vals' 18 | flag = '$_flag_vals' 19 | mode = '$_mode_vals' 20 | mvals = '$_mvals_vals' 21 | minvals = '$_minvals_vals' 22 | maxvals = '$_maxvals_vals' 23 | 24 | subcommand -> '$_subcommand' 25 | subcmd_scopt = '$_subcmd_scopt_vals' 26 | subcmd_scpos1 = '$_subcmd_scpos1_vals'" 27 | 28 | -------------------------------------------------------------------------------- /examples/simple.yml: -------------------------------------------------------------------------------- 1 | # Example taken from: https://github.com/clap-rs/clap/blob/v2.33.0/README.md 2 | 3 | name: myapp 4 | version: "1.0" 5 | author: Kevin K. 6 | about: Does awesome things 7 | 8 | global_settings: 9 | - ColoredHelp 10 | 11 | args: 12 | - config: 13 | short: c 14 | long: config 15 | value_name: FILE 16 | help: Sets a custom config file 17 | takes_value: true 18 | - INPUT: 19 | help: Sets the input file to use 20 | required: true 21 | index: 1 22 | - verbose: 23 | short: v 24 | multiple: true 25 | help: Sets the level of verbosity 26 | subcommands: 27 | - test: 28 | about: Controls testing features 29 | version: "1.3" 30 | author: Someone E. 31 | args: 32 | - debug: 33 | short: d 34 | help: print debug information 35 | -------------------------------------------------------------------------------- /examples/zsh/complete: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | config="$(cargo r -q -- path -d "${(%):-%N}")/../complete.yml" 4 | eval "$(cargo r -q -- parse zsh _ -- "$@" <"$config")" 5 | [[ -z "$_success" ]] && exit 1 6 | 7 | printf '%s\n' \ 8 | "opt = '${_opt_vals[@]}' 9 | pos = '$_pos_vals' 10 | flag = '${_flag_vals[@]}' 11 | mode = '$_mode_vals' 12 | mvals = '$_mvals_vals' 13 | minvals = '${_minvals_vals[@]}' 14 | maxvals = '${_maxvals_vals[@]}' 15 | 16 | subcommand -> '$_subcommand' 17 | subcmd_scopt = '${_subcmd_scopt_vals[@]}' 18 | subcmd_scpos1 = '$_subcmd_scpos1_vals'" 19 | 20 | -------------------------------------------------------------------------------- /examples/zsh/complete-heredoc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | eval "$(cargo r -q -- parse zsh _ -- "$@" <<-EOF 4 | name: yml_app 5 | version: "1.0" 6 | about: An example using a .yml file to build a CLI 7 | author: Kevin K. 8 | 9 | # AppSettings can be defined as a list and are **not** ascii case sensitive 10 | settings: 11 | - ArgRequiredElseHelp 12 | - ColorAuto 13 | 14 | global_settings: 15 | - ColoredHelp 16 | 17 | # All Args must be defined in the 'args:' list where the name of the arg, is the 18 | # key to a Hash object 19 | args: 20 | # The name of this argument, is 'opt' which will be used to access the value 21 | # later in your Rust code 22 | - opt: 23 | help: Example option argument from yaml 24 | short: o 25 | long: option 26 | multiple: true 27 | takes_value: true 28 | - pos: 29 | help: Example positional argument from yaml 30 | index: 1 31 | # A list of possible values can be defined as a list 32 | possible_values: 33 | - fast 34 | - slow 35 | - flag: 36 | help: Demo flag argument 37 | short: F 38 | multiple: true 39 | global: true 40 | # Conflicts, mutual overrides, and requirements can all be defined as a 41 | # list, where the key is the name of the other argument 42 | conflicts_with: 43 | - opt 44 | requires: 45 | - pos 46 | - mode: 47 | long: mode 48 | help: Shows an option with specific values 49 | # possible_values can also be defined in this list format 50 | possible_values: [vi, emacs] 51 | takes_value: true 52 | - mvals: 53 | long: mult-vals 54 | help: Demos an option which has two named values 55 | # value names can be described in a list, where the help will be shown 56 | # --mult-vals 57 | value_names: 58 | - one 59 | - two 60 | - minvals: 61 | long: min-vals 62 | multiple: true 63 | help: You must supply at least two values to satisfy me 64 | min_values: 2 65 | - maxvals: 66 | long: max-vals 67 | multiple: true 68 | help: You can only supply a max of 3 values for me! 69 | max_values: 3 70 | 71 | # All subcommands must be listed in the 'subcommand:' object, where the key to 72 | # the list is the name of the subcommand, and all settings for that command are 73 | # are part of a Hash object 74 | subcommands: 75 | # The name of this subcommand will be 'subcmd' which can be accessed in your 76 | # Rust code later 77 | - subcmd: 78 | about: demos subcommands from yaml 79 | version: "0.1" 80 | author: Kevin K. 81 | # Subcommand args are exactly like App args 82 | args: 83 | - scopt: 84 | short: B 85 | multiple: true 86 | help: Example subcommand option 87 | takes_value: true 88 | - scpos1: 89 | help: Example subcommand positional 90 | index: 1 91 | 92 | # ArgGroups are supported as well, and must be specified in the 'groups:' 93 | # object of this file 94 | groups: 95 | # the name of the ArgGoup is specified here 96 | - min-max-vals: 97 | # All args and groups that are a part of this group are set here 98 | args: 99 | - minvals 100 | - maxvals 101 | # setting conflicts is done the same manner as setting 'args:' 102 | # 103 | # to make this group required, you could set 'required: true' but for 104 | # this example we won't do that. 105 | EOF 106 | )"; [[ -z "$_success" ]] && exit 1 107 | 108 | printf '%s\n' \ 109 | "opt = '${_opt_vals[@]}' 110 | pos = '$_pos_vals' 111 | flag = '${_flag_vals[@]}' 112 | mode = '$_mode_vals' 113 | mvals = '$_mvals_vals' 114 | minvals = '${_minvals_vals[@]}' 115 | maxvals = '${_maxvals_vals[@]}' 116 | 117 | subcommand -> '$_subcommand' 118 | subcmd_scopt = '${_subcmd_scopt_vals[@]}' 119 | subcmd_scpos1 = '$_subcmd_scpos1_vals'" 120 | 121 | -------------------------------------------------------------------------------- /examples/zsh/gh-clone-repos: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | cargo r -q -- deps jq git || exit 1 4 | 5 | eval "$(cargo r -q -- parse zsh _ -- "$@" <<-EOF 6 | name: gh-clone-repos 7 | version: "1.0" 8 | author: Matteo G. 9 | about: Uses 'gh-repo-list' to clone all your GitHub repos. 10 | 11 | settings: 12 | - ArgRequiredElseHelp 13 | - ColorAuto 14 | 15 | global_settings: 16 | - ColoredHelp 17 | 18 | args: 19 | - username: 20 | help: Your GitHub username 21 | required: true 22 | - password: 23 | help: Your GitHub password 24 | required: true 25 | - git_options: 26 | help: "Additional Git options (for example: --git-options '--depth 1')" 27 | long: git-options 28 | takes_value: true 29 | short: o 30 | allow_hyphen_values: true 31 | EOF 32 | )"; [[ -z "${_success}" ]] && exit 1 33 | 34 | for repo in $(gh-repo-list "${_username_vals}" "${_password_vals}" \ 35 | | jq -r "map(.ssh_url) | join(\"\n\")"); do 36 | if [[ -n "${_git_options_occurs}" ]]; then 37 | eval "git clone ${_git_options_vals} ${repo}" 38 | else 39 | git clone "${repo}" 40 | fi 41 | done 42 | 43 | -------------------------------------------------------------------------------- /examples/zsh/gh-repo-list: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | cargo r -q -- deps curl jq || exit 1 4 | 5 | eval "$(cargo r -q -- parse zsh _ -- "$@" <<-EOF 6 | name: gh-repo-list 7 | version: "1.0" 8 | author: Matteo G. 9 | about: Outputs JSON containing useful informations about your GitHub repos. 10 | 11 | settings: 12 | - ArgRequiredElseHelp 13 | - ColorAuto 14 | 15 | global_settings: 16 | - ColoredHelp 17 | 18 | args: 19 | - username: 20 | help: Your GitHub username 21 | required: true 22 | - password: 23 | help: Your GitHub password 24 | required: true 25 | - iterations: 26 | help: The number of iterations to do. 0 means there is no limit 27 | long: iterations 28 | short: i 29 | default_value: "0" 30 | EOF 31 | )"; [[ -z "${_success}" ]] && exit 1 32 | 33 | page=1 34 | while :; do 35 | data="$(curl -s -X GET \ 36 | -u "${_username_vals}:${_password_vals}" \ 37 | "https://api.github.com/user/repos?page=${page}&per_page100&type=all")" 38 | len="$(printf '%s\n' "${data}" | jq '. | length')" 39 | [[ "${_iterations_vals}" == "0" && "${len}" == 0 ]] && break 40 | printf '%s\n' "${data}" 41 | [[ "${page}" == "${_iterations_vals}" ]] && break 42 | page="$((page + 1))" 43 | done 44 | 45 | -------------------------------------------------------------------------------- /examples/zsh/naughty-send: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | # See: https://awesomewm.org/doc/api/libraries/naughty.html 3 | 4 | cargo r -q -- deps awsome-client cut || exit 1 5 | 6 | eval "$(cargo r -q -- parse zsh _ -- "$@" <<-EOF 7 | name: naughty-send 8 | version: "1.0" 9 | author: Matteo G. 10 | about: "CLI similiar to notify-send that sends notifications to Awesome WM using naughty.notify()" 11 | 12 | settings: 13 | - ArgRequiredElseHelp 14 | - ColorAuto 15 | 16 | global_settings: 17 | - ColoredHelp 18 | 19 | args: 20 | - text: 21 | help: "(string) Text of the notification." 22 | default_value: "" 23 | - title: 24 | help: "(string) Title of the notification. (optional)" 25 | default_value: nil 26 | - timeout: 27 | help: "(int) Time in seconds after which popup expires. Set 0 for no timeout." 28 | default_value: "5" 29 | - hover_timeout: 30 | help: "(int) Delay in seconds after which hovered popup disappears. (optional)" 31 | default_value: nil 32 | - screen: 33 | help: "(integer or screen) Target screen for the notification." 34 | default_value: focused 35 | - position: 36 | help: "(string) Corner of the workarea displaying the popups. (default \"top_right\")" 37 | possible_values: 38 | - top_right 39 | - top_left 40 | - bottom_left 41 | - bottom_right 42 | - top_middle 43 | - bottom_middle 44 | default_value: top_right 45 | - ontop: 46 | help: "(bool) Boolean forcing popups to display on top." 47 | possible_values: 48 | - "true" 49 | - "false" 50 | default_value: "true" 51 | - height: 52 | help: "(int) Popup height. (default \`beautiful.notification_height\` or auto)" 53 | default_value: nil 54 | - width: 55 | help: "(int) Popup width. (default \`beautiful.notification_width\` or auto)" 56 | default_value: nil 57 | - max_height: 58 | help: "(int) Popup maximum height. (default \`beautiful.notification_max_height\` or auto)" 59 | default_value: nil 60 | - max_width: 61 | help: "(int) Popup maximum width. (default \`beautiful.notification_max_width\` or auto)" 62 | default_value: nil 63 | - font: 64 | help: "(string) Notification font. (default \`beautiful.notification_font\` or \`beautiful.font\` or \`awesome.font\`)" 65 | default_value: nil 66 | - icon: 67 | help: "(string) Path to icon. (optional)" 68 | default_value: nil 69 | - icon_size: 70 | help: "(int) Desired icon size in px. (optional)" 71 | default_value: nil 72 | - fg: 73 | help: "(string) Foreground color. (default \`beautiful.notification_fg\` or \`beautiful.fg_focus\` or \`'#ffffff'\`)" 74 | default_value: nil 75 | - bg: 76 | help: "(string) Background color. (default \`beautiful.notification_fg\` or \`beautiful.bg_focus\` or \`'#535d6c'\`)" 77 | default_value: nil 78 | - border_width: 79 | help: "(int) Border width. (default \`beautiful.notification_border_width\` or 1)" 80 | default_value: nil 81 | - border_color: 82 | help: "(string) Border color. (default \`beautiful.notification_border_color\` or \`beautiful.border_focus\` or \`'#535d6c'\`)" 83 | default_value: nil 84 | - shape: 85 | help: "(gears.shape) Widget shape. (default \`beautiful.notification_shape\`)" 86 | default_value: nil 87 | - opacity: 88 | help: "(gears.opacity) Widget opacity. (default \`beautiful.notification_opacity\`)" 89 | default_value: nil 90 | - margin: 91 | help: "(gears.margin) Widget margin. (default \`beautiful.notification_margin\`)" 92 | default_value: nil 93 | - run: 94 | help: "(func) Function to run on left click. The notification object will be passed to it as an argument. You need to call e.g. notification.die(naughty.notificationClosedReason.dismissedByUser) from there to dismiss the notification yourself. (optional)" 95 | default_value: nil 96 | - destroy: 97 | help: "(func) Function to run when notification is destroyed. (optional)" 98 | default_value: nil 99 | - preset: 100 | help: "(table) Table with any of the above parameters. Note: Any parameters specified directly in args will override ones defined in the preset. (optional)" 101 | default_value: nil 102 | - replaces_id: 103 | help: "(int) Replace the notification with the given ID. (optional)" 104 | default_value: nil 105 | - callback: 106 | help: "(func) Function that will be called with all arguments. The notification will only be displayed if the function returns true. Note: this function is only relevant to notifications sent via dbus. (optional)" 107 | default_value: nil 108 | - actions: 109 | help: "(table) Mapping that maps a string to a callback when this action is selected. (optional)" 110 | default_value: nil 111 | - ignore_suspend: 112 | help: "(bool) If set to true this notification will be shown even if notifications are suspended via naughty.suspend." 113 | possible_values: 114 | - "true" 115 | - "false" 116 | default_value: "false" 117 | EOF 118 | )"; [[ -z "${_success}" ]] && exit 1 119 | 120 | quote_or_nil() { 121 | occurs="_$1_occurs" 122 | vals="_$1_vals" 123 | if [[ "${!occurs}" -gt 0 ]]; then 124 | printf '%s' "\"${!vals}\"" 125 | else 126 | if [[ -n "${!vals}" ]]; then 127 | printf '%s\n' "${!vals}" 128 | else 129 | printf nil 130 | fi 131 | fi 132 | } 133 | 134 | printf '%s\n%s\n%s\n' \ 135 | "local naughty = require('naughty')" \ 136 | "local notification = naughty.notify({ 137 | text = $(quote_or_nil text), 138 | title = $(quote_or_nil title), 139 | timeout = ${_timeout_vals}, 140 | hover_timeout = ${_hover_timeout_vals}, 141 | screen = $(quote_or_nil screen), 142 | position = $(quote_or_nil position), 143 | ontop = ${_ontop_vals}, 144 | height = ${_height_vals}, 145 | width = ${_width_vals}, 146 | max_height = ${_max_height_vals}, 147 | max_width = ${_max_width_vals}, 148 | font = $(quote_or_nil font), 149 | icon = $(quote_or_nil icon), 150 | icon_size = ${_icon_size_vals}, 151 | fg = $(quote_or_nil fg), 152 | bg = $(quote_or_nil bg), 153 | border_width = ${_border_width_vals}, 154 | border_color = $(quote_or_nil border_color), 155 | shape = ${_shape_vals}, 156 | opacity = ${_opacity_vals}, 157 | margin = ${_margin_vals}, 158 | run = ${_run_vals}, 159 | destroy = ${_destroy_vals}, 160 | preset = ${_preset_vals}, 161 | replaces_id = ${_replaces_id_vals}, 162 | callback = ${_callback_vals}, 163 | actions = ${_actions_vals}, 164 | ignore_suspend = ${_ignore_suspend_vals} 165 | })" \ 166 | "return notification.id" \ 167 | | awesome-client \ 168 | | cut -d' ' -f5 169 | 170 | 171 | -------------------------------------------------------------------------------- /examples/zsh/sub: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | cargo r -q -- deps rg xargs sed || exit 1 4 | 5 | eval "$(cargo r -q -- parse zsh _ -- "$@" <<-EOF 6 | name: sub 7 | version: "1.0" 8 | author: Matteo G. 9 | about: Searches for a pattern using ripgrep and substitutes all occurrences with a string. 10 | 11 | settings: 12 | - ArgRequiredElseHelp 13 | - ColorAuto 14 | 15 | global_settings: 16 | - ColoredHelp 17 | 18 | args: 19 | - pattern: 20 | help: The pattern to search for 21 | required: true 22 | - substitute: 23 | help: The text to substitute the matches with 24 | required: true 25 | - dry_run: 26 | help: Instead of modifying the files right away it shows you a preview of the changes 27 | short: d 28 | long: dry-run 29 | EOF 30 | )"; [[ -z "${_success}" ]] && exit 1 31 | 32 | if [[ -n "${_dry_run_occurs}" ]]; then 33 | rg "${_pattern_vals}" | sed "s/${_pattern_vals}/${_substitute_vals}/g" 34 | else 35 | rg "${_pattern_vals}" --files-with-matches \ 36 | | xargs sed -i "s/${_pattern_vals}/${_substitute_vals}/g" 37 | fi 38 | 39 | -------------------------------------------------------------------------------- /examples/zsh/vlen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | cargo r -q -- deps fd ffprobe paste bc || exit 1 4 | 5 | eval "$(cargo r -q -- parse zsh _ -- "$@" <<-EOF 6 | name: vlen 7 | version: "1.0" 8 | author: Matteo G. 9 | about: Searches for all videos starting from a given directory and outputs the cumulative duration. 10 | 11 | settings: 12 | - ArgRequiredElseHelp 13 | - ColorAuto 14 | 15 | global_settings: 16 | - ColoredHelp 17 | 18 | args: 19 | - dir: 20 | help: The dir where to start searching from 21 | default_value: . 22 | EOF 23 | )"; [[ -z "${_success}" ]] && exit 1 24 | 25 | fd "${_dir_vals}" -e mkv -e mp4 -e webm -x \ 26 | ffprobe -v quiet -of csv=p=0 -show_entries format=duration {} \; \ 27 | | paste -sd+ - \ 28 | | bc 29 | 30 | -------------------------------------------------------------------------------- /src/app_wrapper.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::Shell, 3 | clap::{App, AppSettings, Arg}, 4 | std::str, 5 | }; 6 | 7 | #[derive(Clone)] 8 | pub struct AppWrapper<'a, 'b> 9 | where 10 | 'a: 'b, 11 | { 12 | pub app: App<'a, 'b>, 13 | pub help_msg: String, 14 | pub version_msg: String, 15 | } 16 | 17 | impl<'a, 'b> AppWrapper<'a, 'b> 18 | where 19 | 'a: 'b, 20 | { 21 | pub fn new( 22 | app: App<'a, 'b>, 23 | modify_app: impl FnOnce(App<'a, 'b>) -> App<'a, 'b>, 24 | ) -> anyhow::Result { 25 | let app = app 26 | .settings(&[ 27 | AppSettings::DisableHelpFlags, 28 | AppSettings::DisableVersion, 29 | AppSettings::DisableHelpSubcommand, 30 | ]) 31 | .arg( 32 | Arg::with_name("help") 33 | .short("h") 34 | .long("help") 35 | .help("Prints help information"), 36 | ) 37 | .arg( 38 | Arg::with_name("version") 39 | .short("V") 40 | .long("version") 41 | .help("Prints version information"), 42 | ); 43 | let app = modify_app(app); 44 | 45 | let mut help_msg = Vec::new(); 46 | app.write_help(&mut help_msg)?; 47 | let help_msg = str::from_utf8(&help_msg)?; 48 | let mut version_msg = Vec::new(); 49 | app.write_long_version(&mut version_msg)?; 50 | let version_msg = str::from_utf8(&version_msg)?; 51 | 52 | Ok(Self { 53 | app, 54 | help_msg: help_msg.into(), 55 | version_msg: version_msg.into(), 56 | }) 57 | } 58 | 59 | // FIXME: Fix ZSH not generating the code for completion. 60 | pub fn completions_script(&mut self, bin_name: &str, shell: &Shell) -> anyhow::Result { 61 | let mut completions_script = Vec::new(); 62 | self.app 63 | .gen_completions_to(bin_name, shell.into(), &mut completions_script); 64 | Ok(str::from_utf8(&completions_script)?.trim_end().into()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/config_checker.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::bail, 3 | clap::YamlLoader, 4 | lazy_static::lazy_static, 5 | std::collections::{BTreeMap, HashMap}, 6 | yaml_rust::Yaml, 7 | }; 8 | 9 | const REQUIRED_KEYS: [&str; 1] = ["name"]; 10 | 11 | lazy_static! { 12 | static ref BANNED_KEYS: HashMap<&'static str, &'static str> = { 13 | let mut m = HashMap::new(); 14 | m.insert("help", "about"); 15 | m 16 | }; 17 | } 18 | 19 | pub fn required(yaml_config: &BTreeMap) -> anyhow::Result<()> { 20 | // We must check these ourselves because if you don't specify a name in the YAML config, clap 21 | // screws up, probably this is a clap bug. 22 | for key in &REQUIRED_KEYS { 23 | if !yaml_config.contains_key(&YamlLoader::load_from_str(key).unwrap()[0]) { 24 | bail!("YAML config must contain an entry named '{}'", key); 25 | } 26 | } 27 | Ok(()) 28 | } 29 | 30 | pub fn banned(yaml_config: &BTreeMap) -> anyhow::Result<()> { 31 | // If these are present in the YAML config, clap screws up, probably this is a clap bug. 32 | for (bannedk, suggestion) in &*BANNED_KEYS { 33 | if yaml_config.contains_key(&YamlLoader::load_from_str(bannedk).unwrap()[0]) { 34 | bail!( 35 | "YAML config can't contain an entry named '{}', try '{}'", 36 | bannedk, 37 | suggestion 38 | ); 39 | } 40 | } 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /src/dependencies.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::anyhow, 3 | clap::ArgMatches, 4 | std::{ 5 | collections::HashMap, 6 | fmt::{self, Display, Formatter}, 7 | path::PathBuf, 8 | }, 9 | }; 10 | 11 | pub struct Dependencies<'a> { 12 | failed_deps: Vec<&'a str>, 13 | } 14 | 15 | impl<'a> Display for Dependencies<'a> { 16 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 17 | match self.failed_deps.len() { 18 | 0 => {} 19 | 1 => { 20 | writeln!( 21 | f, 22 | "Required dependency '{}' not found in $PATH", 23 | self.failed_deps[0], 24 | )?; 25 | } 26 | _ => { 27 | writeln!(f, "These required dependencies were not found in $PATH:")?; 28 | for dep in self.failed_deps.iter() { 29 | writeln!(f, " {}", dep)?; 30 | } 31 | } 32 | } 33 | Ok(()) 34 | } 35 | } 36 | 37 | impl<'a> Dependencies<'a> { 38 | fn parse(deps: &'_ [&'a str]) -> HashMap<&'a str, Option> { 39 | let mut map = HashMap::new(); 40 | for dep in deps.iter() { 41 | map.insert(*dep, which::which(dep).ok()); 42 | } 43 | map 44 | } 45 | 46 | #[cfg(feature = "color")] 47 | fn print_colored(&self) -> anyhow::Result<()> { 48 | use { 49 | std::io::Write, 50 | termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}, 51 | }; 52 | 53 | let mut stderr = StandardStream::stderr( 54 | if atty::is(atty::Stream::Stdout) || atty::is(atty::Stream::Stderr) { 55 | ColorChoice::Auto 56 | } else { 57 | ColorChoice::Never 58 | }, 59 | ); 60 | let mut color_spec = ColorSpec::new(); 61 | stderr.set_color(color_spec.set_fg(Some(Color::Red)).set_bold(true))?; 62 | 63 | match self.failed_deps.len() { 64 | 0 => {} 65 | 1 => { 66 | write!(&mut stderr, "error: ")?; 67 | stderr.set_color(color_spec.set_fg(None).set_bold(false))?; 68 | write!(&mut stderr, "Required dependency ")?; 69 | stderr.set_color(color_spec.set_fg(Some(Color::Green)).set_bold(true))?; 70 | write!(&mut stderr, "{}", self.failed_deps[0])?; 71 | stderr.set_color(color_spec.set_fg(None).set_bold(false))?; 72 | write!(&mut stderr, " not found in ")?; 73 | stderr.set_color(color_spec.set_fg(Some(Color::Cyan)).set_bold(true))?; 74 | writeln!(&mut stderr, "$PATH")?; 75 | } 76 | _ => { 77 | write!(&mut stderr, "error: ")?; 78 | stderr.set_color(color_spec.set_fg(None).set_bold(false))?; 79 | write!( 80 | &mut stderr, 81 | "These required dependencies were not found in " 82 | )?; 83 | stderr.set_color(color_spec.set_fg(Some(Color::Cyan)).set_bold(true))?; 84 | write!(&mut stderr, "$PATH")?; 85 | stderr.set_color(color_spec.set_fg(None).set_bold(false))?; 86 | writeln!(&mut stderr, ":")?; 87 | stderr.set_color(color_spec.set_fg(Some(Color::Green)).set_bold(true))?; 88 | for dep in self.failed_deps.iter() { 89 | writeln!(&mut stderr, " {}", dep)?; 90 | } 91 | } 92 | } 93 | stderr.set_color(color_spec.set_fg(None).set_bold(false))?; 94 | Ok(()) 95 | } 96 | 97 | fn print(&self) { 98 | eprintln!("{}", self); 99 | } 100 | 101 | pub fn check(matches: &'a ArgMatches) -> Option> { 102 | macro_rules! exit { 103 | ( $x:expr ) => { 104 | return Some(if $x == 0 { 105 | Ok(()) 106 | } else { 107 | Err(anyhow!( 108 | "1 or more required dependencies were not found in $PATH" 109 | )) 110 | }); 111 | }; 112 | } 113 | 114 | if let Some(matches) = matches.subcommand_matches("deps") { 115 | let deps: Vec<&'a str> = matches 116 | .values_of("DEPENDENCIES") 117 | .unwrap() 118 | .collect::>(); 119 | let results = Self::parse(&deps); 120 | 121 | if matches.is_present("succeded") { 122 | let mut failed_deps = 0; 123 | for (_, path) in results { 124 | if let Some(path) = path { 125 | println!("{}", path.display()); 126 | } else { 127 | failed_deps += 1; 128 | } 129 | } 130 | exit!(failed_deps); 131 | } 132 | 133 | if matches.is_present("failed") { 134 | let mut failed_deps = 0; 135 | for (dep, path) in results { 136 | if path.is_none() { 137 | failed_deps += 1; 138 | println!("{}", dep); 139 | } 140 | } 141 | exit!(failed_deps); 142 | } 143 | 144 | if matches.is_present("all") { 145 | let mut succeded = HashMap::new(); 146 | let mut failed = Vec::new(); 147 | for (k, v) in results { 148 | if let Some(path) = v { 149 | succeded.insert(k, path); 150 | } else { 151 | failed.push(k); 152 | } 153 | } 154 | let json_val = serde_json::json!({ 155 | "succeded": succeded, 156 | "failed": failed, 157 | }); 158 | let json_str = if matches.is_present("pretty") { 159 | serde_json::to_string_pretty(&json_val).unwrap() 160 | } else { 161 | json_val.to_string() 162 | }; 163 | println!("{}", json_str); 164 | 165 | exit!(failed.len()); 166 | } 167 | 168 | let mut failed_deps = Vec::new(); 169 | for (k, v) in results { 170 | if v.is_none() { 171 | failed_deps.push(k); 172 | } 173 | } 174 | let len = failed_deps.len(); 175 | 176 | let s = Self { failed_deps }; 177 | if cfg!(feature = "color") { 178 | if let Err(e) = s.print_colored() { 179 | return Some(Err(e)); 180 | } 181 | } else { 182 | s.print(); 183 | } 184 | 185 | exit!(len); 186 | } else { 187 | None 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/ident_type.rs: -------------------------------------------------------------------------------- 1 | use {crate::shell::Shell, lazy_static::lazy_static, regex::Regex}; 2 | 3 | lazy_static! { 4 | static ref CANNOT_START_WITH_NUM_RE: Regex = Regex::new("^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap(); 5 | static ref CAN_START_WITH_NUM_RE: Regex = Regex::new("^[a-zA-Z0-9_]+$").unwrap(); 6 | static ref ANY_RE: Regex = Regex::new("^.+$").unwrap(); 7 | } 8 | 9 | pub enum IdentType { 10 | Head, 11 | Tail, 12 | } 13 | 14 | impl IdentType { 15 | // Regex for validating the (head or tail) identifier. 16 | pub fn re(&self, shell: &Shell) -> &'static Regex { 17 | match self { 18 | Self::Head => match shell { 19 | Shell::Bash | Shell::Zsh => &*CANNOT_START_WITH_NUM_RE, 20 | Shell::Elvish | Shell::Fish => &*CAN_START_WITH_NUM_RE, 21 | Shell::PowerShell => &*ANY_RE, 22 | }, 23 | Self::Tail => match shell { 24 | Shell::Bash | Shell::Zsh | Shell::Elvish | Shell::Fish => &*CAN_START_WITH_NUM_RE, 25 | Shell::PowerShell => &*ANY_RE, 26 | }, 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod app_wrapper; 2 | mod config_checker; 3 | mod dependencies; 4 | mod ident_type; 5 | mod shell; 6 | 7 | pub use {dependencies::Dependencies, shell::Shell}; 8 | 9 | use { 10 | crate::app_wrapper::AppWrapper, 11 | anyhow::{bail, Context}, 12 | clap::{App, AppSettings, Arg, ArgMatches, SubCommand, YamlLoader}, 13 | std::{ 14 | convert::TryFrom, 15 | env, fs, 16 | io::{self, Read}, 17 | path::Path, 18 | process, str, 19 | }, 20 | yaml_rust::Yaml, 21 | }; 22 | 23 | fn this_cli() -> ArgMatches<'static> { 24 | App::new("slap") 25 | .version(clap::crate_version!()) 26 | .author(clap::crate_authors!("\n")) 27 | .about(clap::crate_description!()) 28 | .settings(&[ 29 | AppSettings::ArgRequiredElseHelp, 30 | AppSettings::SubcommandRequiredElseHelp, 31 | AppSettings::ColorAuto, 32 | ]) 33 | .global_settings(&[ 34 | AppSettings::ColoredHelp, 35 | ]) 36 | .subcommand( 37 | SubCommand::with_name("completions") 38 | .about("Output a completions script for the specified shell") 39 | .arg( 40 | Arg::with_name("SHELL") 41 | .help("The target shell") 42 | .index(1) 43 | .required(true) 44 | .possible_values(&Shell::SHELLS), 45 | ) 46 | ) 47 | .subcommand( 48 | SubCommand::with_name("parse") 49 | .about("Check the passed arguments and output code intended to be evaluated by your shell") 50 | .arg( 51 | Arg::with_name("SHELL") 52 | .help("The target shell") 53 | .index(1) 54 | .required(true) 55 | .possible_values(&Shell::SHELLS), 56 | ) 57 | .arg( 58 | Arg::with_name("VAR_PREFIX") 59 | .help("The prefix to use for the exported variables") 60 | .index(2), 61 | ) 62 | .arg( 63 | Arg::with_name("EXTERNAL_ARGS") 64 | .help("Arguments to parse using the YAML config passed to STDIN") 65 | .index(3) 66 | .raw(true) 67 | .allow_hyphen_values(true) 68 | .multiple(true), 69 | ), 70 | ) 71 | .subcommand( 72 | SubCommand::with_name("deps") 73 | .about("Check that your sh script dependencies are present in $PATH") 74 | .arg( 75 | Arg::with_name("DEPENDENCIES") 76 | .help("Your sh script dependencies") 77 | .index(1) 78 | .multiple(true) 79 | .required(true) 80 | ) 81 | .arg( 82 | Arg::with_name("failed") 83 | .help("Lists every dependency not found in $PATH") 84 | .long("failed") 85 | .short("f") 86 | .conflicts_with_all(&["succeded", "all"]) 87 | ) 88 | .arg( 89 | Arg::with_name("succeded") 90 | .help("Lists the absolute path of every dependency found in $PATH") 91 | .long("succeded") 92 | .short("s") 93 | .conflicts_with_all(&["failed", "all"]) 94 | ) 95 | .arg( 96 | Arg::with_name("all") 97 | .help("Outputs a JSON containing succeded and failed dependencies (can easily be parsed using jq)") 98 | .long("all") 99 | .short("a") 100 | .conflicts_with_all(&["failed", "succeded"]) 101 | ) 102 | .arg( 103 | Arg::with_name("pretty") 104 | .help("Pretty print the JSON output") 105 | .long("pretty") 106 | .short("p") 107 | .requires("all") 108 | ), 109 | ) 110 | .subcommand( 111 | SubCommand::with_name("path") 112 | .about("Gives you the absolute path given the relative path of a script") 113 | .arg( 114 | Arg::with_name("SCRIPT_RELATIVE_PATH") 115 | .help("Relative path of your script. For example in bash: `slap path \"${BASH_SOURCE[0]}\"`, in fish: `slap path (status -f)`, in zsh: `slap path \"${(%):-%N}\"`") 116 | .index(1) 117 | .required(true) 118 | ) 119 | .arg( 120 | Arg::with_name("dir_only") 121 | .long("dir-only") 122 | .short("d") 123 | .conflicts_with("dereference") 124 | .help("Gives you the absolute path of the script without including the script name") 125 | ) 126 | .arg( 127 | Arg::with_name("dereference") 128 | .long("dereference") 129 | .short("D") 130 | .conflicts_with("dir_only") 131 | .help("If the path points to a symlink, the dereferenced path will be printed") 132 | ) 133 | ) 134 | .get_matches() 135 | } 136 | 137 | fn path_subcmd(matches: &ArgMatches) -> anyhow::Result<()> { 138 | let relativep = matches.value_of("SCRIPT_RELATIVE_PATH").unwrap(); 139 | let relativep = Path::new(relativep); 140 | let script_name = relativep 141 | .file_name() 142 | .with_context(|| format!("Can't get file name of path '{}'", relativep.display()))?; 143 | 144 | let dirname = relativep 145 | .parent() 146 | .context("Can't get parent path of root (/)")?; 147 | env::set_current_dir(dirname) 148 | .with_context(|| format!("Failed to cd in '{}'", dirname.display()))?; 149 | let mut current_dir = env::current_dir()?; 150 | if !matches.is_present("dir_only") { 151 | current_dir = current_dir.join(script_name); 152 | } 153 | 154 | if matches.is_present("dereference") { 155 | if let Ok(dereferencedp) = fs::read_link(¤t_dir) { 156 | current_dir = dereferencedp; 157 | } 158 | } 159 | 160 | println!("{}", current_dir.display()); 161 | 162 | Ok(()) 163 | } 164 | 165 | // FIXME: Fix ZSH not generating the code for completion. 166 | fn autocompletions_subcmd( 167 | matches: &ArgMatches, 168 | external_app: &mut AppWrapper, 169 | name: &str, 170 | ) -> anyhow::Result<()> { 171 | let shell = Shell::try_from(matches.value_of("SHELL").unwrap()).unwrap(); 172 | let completions_script = external_app.completions_script(name, &shell)?; 173 | println!("{}", completions_script); 174 | Ok(()) 175 | } 176 | 177 | fn parse_subcmd( 178 | matches: &ArgMatches, 179 | name: &str, 180 | external_app: AppWrapper, 181 | external_app_subcommands: &[AppWrapper], 182 | help_msg: &str, 183 | version_msg: &str, 184 | ) -> anyhow::Result<()> { 185 | let shell = Shell::try_from(matches.value_of("SHELL").unwrap()).unwrap(); 186 | let mut external_args = matches 187 | .values_of("EXTERNAL_ARGS") 188 | .map(|x| x.collect::>()) 189 | .unwrap_or_default(); 190 | let var_prefix = matches.value_of("VAR_PREFIX"); 191 | 192 | external_args.insert(0, &name); 193 | let external_matches = external_app.app.get_matches_from(external_args); 194 | 195 | // We can't output help or version messages to stdout. Only to stderr. 196 | // The only thing that we can output to stdout is the code that the user will eval. 197 | if let Some(ref subcmd) = external_matches.subcommand { 198 | let subcmd_matches = &subcmd.matches; 199 | let subcmd_name = &subcmd.name; 200 | 201 | macro_rules! handle_subcmd { 202 | ( $x:ident, $y:ident ) => { 203 | let subcmd = external_app_subcommands 204 | .into_iter() 205 | .find(|x| x.app.get_name() == $y) 206 | .unwrap(); 207 | eprintln!("{}", subcmd.$x); 208 | return Ok(()); 209 | }; 210 | } 211 | 212 | if subcmd_name == "help" { 213 | if subcmd_matches.is_present("help") { 214 | eprintln!("{}", help_msg); 215 | return Ok(()); 216 | } 217 | if subcmd_matches.is_present("version") { 218 | eprintln!("{}", version_msg); 219 | return Ok(()); 220 | } 221 | match subcmd_matches.value_of("SUBCMD") { 222 | Some(help_subcmd) => { 223 | handle_subcmd!(help_msg, help_subcmd); 224 | } 225 | None => { 226 | eprintln!("{}", external_app.help_msg); 227 | return Ok(()); 228 | } 229 | } 230 | } 231 | if subcmd_matches.is_present("help") { 232 | handle_subcmd!(help_msg, subcmd_name); 233 | } 234 | if subcmd_matches.is_present("version") { 235 | handle_subcmd!(version_msg, subcmd_name); 236 | } 237 | } else { 238 | if external_matches.is_present("help") { 239 | eprintln!("{}", external_app.help_msg); 240 | return Ok(()); 241 | } 242 | if external_matches.is_present("version") { 243 | eprintln!("{}", external_app.version_msg); 244 | return Ok(()); 245 | } 246 | } 247 | 248 | let code = shell.parse(external_matches, var_prefix)?; 249 | println!("{}", code); 250 | 251 | Ok(()) 252 | } 253 | 254 | fn main() -> anyhow::Result<()> { 255 | let matches = this_cli(); 256 | 257 | match Dependencies::check(&matches) { 258 | Some(Ok(())) => return Ok(()), 259 | Some(Err(_)) => process::exit(1), 260 | None => {} 261 | } 262 | 263 | if let Some(matches) = matches.subcommand_matches("path") { 264 | return path_subcmd(matches); 265 | } 266 | 267 | let stdin = { 268 | let mut stdin = String::new(); 269 | io::stdin().read_to_string(&mut stdin)?; 270 | if stdin.is_empty() { 271 | bail!("Received an empty string from STDIN. Check that the YAML config file exists") 272 | } 273 | stdin 274 | }; 275 | 276 | let yaml_loader = { 277 | let mut yaml_loader = YamlLoader::load_from_str(&stdin)?; 278 | yaml_loader.remove(0) 279 | }; 280 | let yaml_config = yaml_loader.into_hash().context("Invalid YAML config")?; 281 | config_checker::required(&yaml_config)?; 282 | config_checker::banned(&yaml_config)?; 283 | 284 | let subcommands_key = YamlLoader::load_from_str("subcommands") 285 | .ok() 286 | .map(|mut x| x.remove(0)) 287 | .unwrap(); 288 | 289 | // Clap doesn't let us redirect --help and --version to stderr so we have to do it manually. 290 | // This block of code parses the subcommands into a Vec and removes from the 291 | // YamlLoader the subcommands parts so we can add them manually later to the `external_app` 292 | // clap::App. 293 | let external_app_subcommands = { 294 | if yaml_config.contains_key(&subcommands_key) { 295 | let subcommands = yaml_config.get(&subcommands_key).unwrap(); 296 | let external_app_subcommands = subcommands 297 | .as_vec() 298 | .context("Subcommands object must be an array of maps")? 299 | .iter() 300 | .map(SubCommand::from_yaml) 301 | .map(|x| AppWrapper::new(x, |app| app)); 302 | let mut xs = Vec::new(); 303 | for subcmd in external_app_subcommands { 304 | xs.push(subcmd?); 305 | } 306 | xs 307 | } else { 308 | Default::default() 309 | } 310 | }; 311 | 312 | let yaml_loader = { 313 | #[allow(clippy::redundant_clone)] 314 | let mut yaml_config = yaml_config.clone(); 315 | yaml_config.remove_entry(&subcommands_key); 316 | let new_yaml_content = { 317 | let mut buffer = String::new(); 318 | let mut emitter = yaml_rust::YamlEmitter::new(&mut buffer); 319 | let yaml_hash = Yaml::Hash(yaml_config); 320 | emitter.dump(&yaml_hash).unwrap(); 321 | buffer 322 | }; 323 | let mut loader = YamlLoader::load_from_str(&new_yaml_content)?; 324 | loader.remove(0) 325 | }; 326 | 327 | let external_app_help_subcmd = AppWrapper::new(SubCommand::with_name("help"), |app: App| { 328 | app.arg(Arg::with_name("SUBCMD").required(false)) 329 | .about("Prints this message or the help of the given subcommand(s)") 330 | })?; 331 | let external_app = App::from(&yaml_loader); 332 | let name = external_app.get_name().to_owned(); 333 | let mut external_app = AppWrapper::new(external_app.bin_name(&name), { 334 | let subcommand = external_app_help_subcmd.app; 335 | let subcommands = external_app_subcommands.clone().into_iter().map(|x| x.app); 336 | move |app: App| app.subcommand(subcommand).subcommands(subcommands) 337 | })?; 338 | 339 | if matches.subcommand_matches("completions").is_some() { 340 | return autocompletions_subcmd(&matches, &mut external_app, &name); 341 | } 342 | 343 | if let Some(matches) = matches.subcommand_matches("parse") { 344 | return parse_subcmd( 345 | matches, 346 | &name, 347 | external_app, 348 | &external_app_subcommands, 349 | &external_app_help_subcmd.help_msg, 350 | &external_app_help_subcmd.version_msg, 351 | ); 352 | } 353 | 354 | Ok(()) 355 | } 356 | -------------------------------------------------------------------------------- /src/shell.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::ident_type::IdentType, 3 | anyhow::{bail, Context}, 4 | std::convert::TryFrom, 5 | }; 6 | 7 | #[derive(Clone)] 8 | pub enum Shell { 9 | Bash, 10 | Elvish, 11 | Fish, 12 | PowerShell, 13 | Zsh, 14 | } 15 | 16 | impl Shell { 17 | pub const SHELLS: [&'static str; 5] = ["bash", "elvish", "fish", "pwsh", "zsh"]; 18 | 19 | fn ident_check<'a>(&self, s: &'a str, ident_type: &IdentType) -> anyhow::Result<&'a str> { 20 | let re = ident_type.re(self); 21 | if re.is_match(s) { 22 | Ok(s) 23 | } else { 24 | bail!( 25 | "`{}` is not a valid identifier, it must conform to this regex: `{}`", 26 | s, 27 | re.to_string(), 28 | ) 29 | } 30 | } 31 | 32 | fn str_escape(&self, s: &str) -> String { 33 | let mut s = s.replace( 34 | '\'', 35 | match self { 36 | Self::Fish => "\\'", 37 | Self::Bash | Self::Elvish | Self::Zsh => r#"'"'"'"#, 38 | Self::PowerShell => "''", 39 | }, 40 | ); 41 | s.insert(0, '\''); 42 | s.push('\''); 43 | s 44 | } 45 | 46 | fn array_escape(&self, xs: &[&str]) -> String { 47 | let mut s = match self { 48 | Self::Fish => String::new(), 49 | Self::Bash | Self::Zsh => "(".into(), 50 | Self::Elvish => "[".into(), 51 | Self::PowerShell => "@(".into(), 52 | }; 53 | let len = xs.len(); 54 | for (idx, x) in xs.iter().enumerate() { 55 | s.push_str(&self.str_escape(x)); 56 | if idx < len - 1 { 57 | if let Self::PowerShell = self { 58 | s.push(','); 59 | } 60 | s.push(' '); 61 | } 62 | } 63 | match self { 64 | Self::Bash | Self::PowerShell | Self::Zsh => s.push(')'), 65 | Self::Elvish => s.push(']'), 66 | _ => {} 67 | } 68 | s 69 | } 70 | 71 | fn assignment(&self, var_ident: &str, val: &str) -> String { 72 | match self { 73 | Self::Fish => format!("set {} {}", var_ident, val), 74 | Self::Bash | Self::Zsh => format!("{}={}", var_ident, val), 75 | Self::Elvish => format!("{} = {}", var_ident, val), 76 | Self::PowerShell => format!( 77 | "Set-Variable -Name {} -Value {}", 78 | self.str_escape(var_ident), 79 | val 80 | ), 81 | } 82 | } 83 | 84 | // NOTE: In the future we could add an option to use associative arrays instead of arrays for 85 | // elvish and powershell. 86 | fn parse_( 87 | &self, 88 | matches: &clap::ArgMatches, 89 | var_prefix: Option<&str>, 90 | // Subcommands are recursive, used to mantain the subcommand prefix for variables. 91 | subcommands_prefixes: Option>, 92 | ) -> anyhow::Result { 93 | let vprefix = { 94 | let mut s = String::new(); 95 | if let Some(vprefix) = var_prefix { 96 | s = self.ident_check(vprefix, &IdentType::Head)?.into(); 97 | } 98 | s 99 | }; 100 | 101 | let subcommands_ident = if let Some(ref xs) = subcommands_prefixes { 102 | format!("{}_", xs.join("_")) 103 | } else { 104 | String::new() 105 | }; 106 | let subcommands_ident = if subcommands_ident.is_empty() { 107 | subcommands_ident 108 | } else { 109 | self.ident_check(&subcommands_ident, &IdentType::Tail)? 110 | .into() 111 | }; 112 | 113 | let mut buffer = String::new(); 114 | 115 | if subcommands_prefixes.is_none() { 116 | buffer.push_str( 117 | &self.assignment(&format!("{}success", vprefix), &self.str_escape("true")), 118 | ); 119 | buffer.push('\n'); 120 | } 121 | 122 | if let Some(ref usage) = matches.usage { 123 | let clap_usage = self.str_escape(usage); 124 | buffer.push_str(&self.assignment( 125 | &format!("{}{}usage", vprefix, subcommands_ident), 126 | &clap_usage, 127 | )); 128 | buffer.push('\n'); 129 | } 130 | 131 | if let Some(ref subcommand) = matches.subcommand { 132 | let clap_subcommand = self.str_escape(&subcommand.name); 133 | buffer.push_str(&self.assignment( 134 | &format!("{}{}subcommand", vprefix, subcommands_ident), 135 | &clap_subcommand, 136 | )); 137 | buffer.push('\n'); 138 | 139 | let mut subcommands_prefixes = subcommands_prefixes.unwrap_or_default(); 140 | subcommands_prefixes.push(&subcommand.name); 141 | buffer.push_str(&self.parse_( 142 | &subcommand.matches, 143 | var_prefix, 144 | Some(subcommands_prefixes), 145 | )?) 146 | } 147 | 148 | for (name, arg) in &matches.args { 149 | let arg_name = self.ident_check(name, &IdentType::Tail)?; 150 | 151 | let clap_occurs = self.str_escape(&arg.occurs.to_string()); 152 | buffer.push_str(&self.assignment( 153 | &format!("{}{}{}_occurs", vprefix, subcommands_ident, arg_name), 154 | &clap_occurs, 155 | )); 156 | buffer.push('\n'); 157 | 158 | let clap_indices = arg 159 | .indices 160 | .iter() 161 | .map(|x| x.to_string()) 162 | .collect::>(); 163 | let clap_indices = clap_indices.iter().map(|x| x.as_str()).collect::>(); 164 | let clap_indices = self.array_escape(&clap_indices); 165 | buffer.push_str(&self.assignment( 166 | &format!("{}{}{}_indices", vprefix, subcommands_ident, arg_name), 167 | &clap_indices, 168 | )); 169 | buffer.push('\n'); 170 | 171 | let mut clap_vals = Vec::new(); 172 | for val in &arg.vals { 173 | clap_vals.push(val.to_str().context("String contains invalid UTF-8 data")?); 174 | } 175 | let clap_vals = self.array_escape(&clap_vals); 176 | buffer.push_str(&self.assignment( 177 | &format!("{}{}{}_vals", vprefix, subcommands_ident, arg_name), 178 | &clap_vals, 179 | )); 180 | buffer.push('\n'); 181 | } 182 | 183 | Ok(buffer) 184 | } 185 | 186 | pub fn parse( 187 | &self, 188 | matches: clap::ArgMatches, 189 | var_prefix: Option<&str>, 190 | ) -> anyhow::Result { 191 | Ok(self.parse_(&matches, var_prefix, None)?.trim_end().into()) 192 | } 193 | } 194 | 195 | impl TryFrom<&str> for Shell { 196 | type Error = anyhow::Error; 197 | 198 | fn try_from(s: &str) -> anyhow::Result { 199 | match s { 200 | "bash" => Ok(Shell::Bash), 201 | "elvish" => Ok(Shell::Elvish), 202 | "fish" => Ok(Shell::Fish), 203 | "pwsh" => Ok(Shell::PowerShell), 204 | "zsh" => Ok(Shell::Zsh), 205 | _ => bail!("Shell must be one of {:?}", Shell::SHELLS), 206 | } 207 | } 208 | } 209 | 210 | impl<'a> Into for &'a Shell { 211 | fn into(self) -> clap::Shell { 212 | match *self { 213 | Shell::Bash => clap::Shell::Bash, 214 | Shell::Elvish => clap::Shell::Elvish, 215 | Shell::Fish => clap::Shell::Fish, 216 | Shell::PowerShell => clap::Shell::PowerShell, 217 | Shell::Zsh => clap::Shell::Zsh, 218 | } 219 | } 220 | } 221 | --------------------------------------------------------------------------------