├── .github ├── renovate.json5 ├── settings.yml └── workflows │ ├── audit.yml │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── bench.py ├── deny.toml ├── docs └── tradeoffs.md ├── examples ├── argh-app │ ├── Cargo.toml │ └── app.rs ├── bpaf-app │ ├── Cargo.toml │ └── app.rs ├── bpaf_derive-app │ ├── Cargo.toml │ └── app.rs ├── clap-app │ ├── Cargo.toml │ └── app.rs ├── clap-minimal-app │ ├── Cargo.toml │ └── app.rs ├── clap_derive-app │ ├── Cargo.toml │ └── app.rs ├── clap_lex-app │ ├── Cargo.toml │ └── app.rs ├── gumdrop-app │ ├── Cargo.toml │ └── app.rs ├── lexopt-app │ ├── Cargo.toml │ └── app.rs ├── null-app │ ├── Cargo.toml │ └── app.rs ├── pico-args-app │ ├── Cargo.toml │ └── app.rs └── xflags-app │ ├── Cargo.toml │ └── app.rs ├── format.py └── runs ├── 2021-07-21-epage-sc01.json ├── 2021-07-22-epage-sc01.json ├── 2021-10-23-seon.json ├── 2021-12-08-seon.json ├── 2021-12-31-seon.json ├── 2022-03-15-seon.json ├── 2022-04-15-seon.json ├── 2022-06-13-seon.json ├── 2022-09-02-seon.json ├── 2022-09-27-seon.json ├── 2022-09-28-seon.json ├── 2023-03-23-seon.json ├── 2023-03-28-seon.json └── 2023-08-24-seon.json /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | schedule: [ 3 | 'before 5am on the first day of the month', 4 | ], 5 | semanticCommits: 'enabled', 6 | configMigration: true, 7 | dependencyDashboard: true, 8 | packageRules: [ 9 | // Goals: 10 | // - Rollup safe upgrades to reduce CI runner load 11 | // - Have lockfile and manifest in-sync 12 | { 13 | matchManagers: [ 14 | 'cargo', 15 | ], 16 | matchCurrentVersion: '>=0.1.0', 17 | matchUpdateTypes: [ 18 | 'patch', 19 | ], 20 | automerge: true, 21 | groupName: 'compatible', 22 | }, 23 | { 24 | matchManagers: [ 25 | 'cargo', 26 | ], 27 | matchCurrentVersion: '>=1.0.0', 28 | matchUpdateTypes: [ 29 | 'minor', 30 | ], 31 | automerge: true, 32 | groupName: 'compatible', 33 | }, 34 | ], 35 | } 36 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # These settings are synced to GitHub by https://probot.github.io/apps/settings/ 2 | 3 | repository: 4 | description: Comparing argparse APIs 5 | topics: rust argparse cli 6 | has_issues: true 7 | has_projects: false 8 | has_wiki: false 9 | has_downloads: true 10 | default_branch: main 11 | 12 | allow_squash_merge: true 13 | allow_merge_commit: true 14 | allow_rebase_merge: true 15 | 16 | # Manual: allow_auto_merge: true, see https://github.com/probot/settings/issues/402 17 | delete_branch_on_merge: true 18 | 19 | labels: 20 | # Type 21 | - name: bug 22 | color: '#b60205' 23 | description: Not as expected 24 | - name: enhancement 25 | color: '#1d76db' 26 | description: Improve the expected 27 | # Flavor 28 | - name: question 29 | color: "#cc317c" 30 | description: Uncertainty is involved 31 | - name: breaking-change 32 | color: "#e99695" 33 | - name: good first issue 34 | color: '#c2e0c6' 35 | description: Help wanted! 36 | 37 | branches: 38 | - name: main 39 | protection: 40 | required_pull_request_reviews: null 41 | required_conversation_resolution: true 42 | required_status_checks: 43 | # Required. Require branches to be up to date before merging. 44 | strict: false 45 | contexts: ["CI"] 46 | enforce_admins: false 47 | restrictions: null 48 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | paths: 9 | - '**/Cargo.toml' 10 | - '**/Cargo.lock' 11 | push: 12 | branches: 13 | - main 14 | 15 | env: 16 | RUST_BACKTRACE: 1 17 | CARGO_TERM_COLOR: always 18 | CLICOLOR: 1 19 | 20 | jobs: 21 | security_audit: 22 | permissions: 23 | issues: write # to create issues (actions-rs/audit-check) 24 | checks: write # to create check (actions-rs/audit-check) 25 | runs-on: ubuntu-latest 26 | # Prevent sudden announcement of a new advisory from failing ci: 27 | continue-on-error: true 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | - uses: actions-rs/audit-check@v1 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | cargo_deny: 36 | permissions: 37 | issues: write # to create issues (actions-rs/audit-check) 38 | checks: write # to create check (actions-rs/audit-check) 39 | runs-on: ubuntu-latest 40 | strategy: 41 | matrix: 42 | checks: 43 | - bans sources 44 | steps: 45 | - uses: actions/checkout@v4 46 | - uses: EmbarkStudios/cargo-deny-action@v2 47 | with: 48 | command: check ${{ matrix.checks }} 49 | rust-version: stable 50 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | 12 | env: 13 | RUST_BACKTRACE: 1 14 | CARGO_TERM_COLOR: always 15 | CLICOLOR: 1 16 | 17 | jobs: 18 | smoke: 19 | name: Quick Check 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | - name: Install Rust 25 | uses: dtolnay/rust-toolchain@stable 26 | with: 27 | toolchain: stable 28 | - uses: Swatinem/rust-cache@v2 29 | - name: Default features 30 | run: cargo check --workspace --all-targets 31 | rustfmt: 32 | name: rustfmt 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v4 37 | - name: Install Rust 38 | uses: dtolnay/rust-toolchain@stable 39 | with: 40 | toolchain: stable 41 | components: rustfmt 42 | - uses: Swatinem/rust-cache@v2 43 | - name: Check formatting 44 | run: cargo fmt --all -- --check 45 | clippy: 46 | name: clippy 47 | runs-on: ubuntu-latest 48 | permissions: 49 | security-events: write # to upload sarif results 50 | steps: 51 | - name: Checkout repository 52 | uses: actions/checkout@v4 53 | - name: Install Rust 54 | uses: dtolnay/rust-toolchain@stable 55 | with: 56 | toolchain: stable 57 | components: clippy 58 | - uses: Swatinem/rust-cache@v2 59 | - name: Install SARIF tools 60 | run: cargo install clippy-sarif sarif-fmt 61 | - name: Check 62 | run: > 63 | cargo clippy --workspace --all-features --all-targets --message-format=json -- -D warnings --allow deprecated 64 | | clippy-sarif 65 | | tee clippy-results.sarif 66 | | sarif-fmt 67 | continue-on-error: true 68 | - name: Upload 69 | uses: github/codeql-action/upload-sarif@v3 70 | with: 71 | sarif_file: clippy-results.sarif 72 | wait-for-processing: true 73 | - name: Report status 74 | run: cargo clippy --workspace --all-features --all-targets -- -D warnings --allow deprecated 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/*.rs.bk 3 | /.idea 4 | /*.iml 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "utf8parse", 17 | ] 18 | 19 | [[package]] 20 | name = "anstyle" 21 | version = "1.0.8" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 24 | 25 | [[package]] 26 | name = "anstyle-parse" 27 | version = "0.2.0" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 30 | dependencies = [ 31 | "utf8parse", 32 | ] 33 | 34 | [[package]] 35 | name = "anstyle-query" 36 | version = "1.0.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 39 | dependencies = [ 40 | "windows-sys", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle-wincon" 45 | version = "3.0.1" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 48 | dependencies = [ 49 | "anstyle", 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "argh" 55 | version = "0.1.13" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "34ff18325c8a36b82f992e533ece1ec9f9a9db446bd1c14d4f936bac88fcd240" 58 | dependencies = [ 59 | "argh_derive", 60 | "argh_shared", 61 | "rust-fuzzy-search", 62 | ] 63 | 64 | [[package]] 65 | name = "argh-app" 66 | version = "0.0.0" 67 | dependencies = [ 68 | "argh", 69 | ] 70 | 71 | [[package]] 72 | name = "argh_derive" 73 | version = "0.1.13" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "adb7b2b83a50d329d5d8ccc620f5c7064028828538bdf5646acd60dc1f767803" 76 | dependencies = [ 77 | "argh_shared", 78 | "proc-macro2", 79 | "quote", 80 | "syn 2.0.25", 81 | ] 82 | 83 | [[package]] 84 | name = "argh_shared" 85 | version = "0.1.13" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "a464143cc82dedcdc3928737445362466b7674b5db4e2eb8e869846d6d84f4f6" 88 | dependencies = [ 89 | "serde", 90 | ] 91 | 92 | [[package]] 93 | name = "bpaf" 94 | version = "0.9.20" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "473976d7a8620bb1e06dcdd184407c2363fe4fec8e983ee03ed9197222634a31" 97 | dependencies = [ 98 | "bpaf_derive", 99 | ] 100 | 101 | [[package]] 102 | name = "bpaf-app" 103 | version = "0.0.0" 104 | dependencies = [ 105 | "bpaf", 106 | ] 107 | 108 | [[package]] 109 | name = "bpaf_derive" 110 | version = "0.5.17" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "fefb4feeec9a091705938922f26081aad77c64cd2e76cd1c4a9ece8e42e1618a" 113 | dependencies = [ 114 | "proc-macro2", 115 | "quote", 116 | "syn 2.0.25", 117 | ] 118 | 119 | [[package]] 120 | name = "bpaf_derive-app" 121 | version = "0.0.0" 122 | dependencies = [ 123 | "bpaf", 124 | ] 125 | 126 | [[package]] 127 | name = "clap" 128 | version = "4.5.40" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" 131 | dependencies = [ 132 | "clap_builder", 133 | "clap_derive", 134 | ] 135 | 136 | [[package]] 137 | name = "clap-app" 138 | version = "0.0.0" 139 | dependencies = [ 140 | "clap", 141 | ] 142 | 143 | [[package]] 144 | name = "clap-minimal-app" 145 | version = "0.0.0" 146 | dependencies = [ 147 | "clap", 148 | ] 149 | 150 | [[package]] 151 | name = "clap_builder" 152 | version = "4.5.40" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" 155 | dependencies = [ 156 | "anstream", 157 | "anstyle", 158 | "clap_lex", 159 | "strsim", 160 | ] 161 | 162 | [[package]] 163 | name = "clap_derive" 164 | version = "4.5.40" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" 167 | dependencies = [ 168 | "heck", 169 | "proc-macro2", 170 | "quote", 171 | "syn 2.0.25", 172 | ] 173 | 174 | [[package]] 175 | name = "clap_derive-app" 176 | version = "0.0.0" 177 | dependencies = [ 178 | "clap", 179 | ] 180 | 181 | [[package]] 182 | name = "clap_lex" 183 | version = "0.7.5" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 186 | 187 | [[package]] 188 | name = "clap_lex-app" 189 | version = "0.0.0" 190 | dependencies = [ 191 | "clap_lex", 192 | ] 193 | 194 | [[package]] 195 | name = "colorchoice" 196 | version = "1.0.0" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 199 | 200 | [[package]] 201 | name = "gumdrop" 202 | version = "0.8.1" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "5bc700f989d2f6f0248546222d9b4258f5b02a171a431f8285a81c08142629e3" 205 | dependencies = [ 206 | "gumdrop_derive", 207 | ] 208 | 209 | [[package]] 210 | name = "gumdrop-app" 211 | version = "0.0.0" 212 | dependencies = [ 213 | "gumdrop", 214 | ] 215 | 216 | [[package]] 217 | name = "gumdrop_derive" 218 | version = "0.8.1" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "729f9bd3449d77e7831a18abfb7ba2f99ee813dfd15b8c2167c9a54ba20aa99d" 221 | dependencies = [ 222 | "proc-macro2", 223 | "quote", 224 | "syn 1.0.109", 225 | ] 226 | 227 | [[package]] 228 | name = "heck" 229 | version = "0.5.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 232 | 233 | [[package]] 234 | name = "lexopt" 235 | version = "0.3.1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "9fa0e2a1fcbe2f6be6c42e342259976206b383122fc152e872795338b5a3f3a7" 238 | 239 | [[package]] 240 | name = "lexopt-app" 241 | version = "0.0.0" 242 | dependencies = [ 243 | "lexopt", 244 | ] 245 | 246 | [[package]] 247 | name = "null-app" 248 | version = "0.0.0" 249 | 250 | [[package]] 251 | name = "pico-args" 252 | version = "0.5.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" 255 | 256 | [[package]] 257 | name = "pico-args-app" 258 | version = "0.0.0" 259 | dependencies = [ 260 | "pico-args", 261 | ] 262 | 263 | [[package]] 264 | name = "proc-macro2" 265 | version = "1.0.78" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 268 | dependencies = [ 269 | "unicode-ident", 270 | ] 271 | 272 | [[package]] 273 | name = "quote" 274 | version = "1.0.29" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" 277 | dependencies = [ 278 | "proc-macro2", 279 | ] 280 | 281 | [[package]] 282 | name = "rust-fuzzy-search" 283 | version = "0.1.1" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2" 286 | 287 | [[package]] 288 | name = "serde" 289 | version = "1.0.179" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "0a5bf42b8d227d4abf38a1ddb08602e229108a517cd4e5bb28f9c7eaafdce5c0" 292 | dependencies = [ 293 | "serde_derive", 294 | ] 295 | 296 | [[package]] 297 | name = "serde_derive" 298 | version = "1.0.179" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "741e124f5485c7e60c03b043f79f320bff3527f4bbf12cf3831750dc46a0ec2c" 301 | dependencies = [ 302 | "proc-macro2", 303 | "quote", 304 | "syn 2.0.25", 305 | ] 306 | 307 | [[package]] 308 | name = "strsim" 309 | version = "0.11.0" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 312 | 313 | [[package]] 314 | name = "syn" 315 | version = "1.0.109" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 318 | dependencies = [ 319 | "proc-macro2", 320 | "quote", 321 | "unicode-ident", 322 | ] 323 | 324 | [[package]] 325 | name = "syn" 326 | version = "2.0.25" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" 329 | dependencies = [ 330 | "proc-macro2", 331 | "quote", 332 | "unicode-ident", 333 | ] 334 | 335 | [[package]] 336 | name = "unicode-ident" 337 | version = "1.0.10" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" 340 | 341 | [[package]] 342 | name = "utf8parse" 343 | version = "0.2.1" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 346 | 347 | [[package]] 348 | name = "windows-sys" 349 | version = "0.48.0" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 352 | dependencies = [ 353 | "windows-targets", 354 | ] 355 | 356 | [[package]] 357 | name = "windows-targets" 358 | version = "0.48.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 361 | dependencies = [ 362 | "windows_aarch64_gnullvm", 363 | "windows_aarch64_msvc", 364 | "windows_i686_gnu", 365 | "windows_i686_msvc", 366 | "windows_x86_64_gnu", 367 | "windows_x86_64_gnullvm", 368 | "windows_x86_64_msvc", 369 | ] 370 | 371 | [[package]] 372 | name = "windows_aarch64_gnullvm" 373 | version = "0.48.0" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 376 | 377 | [[package]] 378 | name = "windows_aarch64_msvc" 379 | version = "0.48.0" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 382 | 383 | [[package]] 384 | name = "windows_i686_gnu" 385 | version = "0.48.0" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 388 | 389 | [[package]] 390 | name = "windows_i686_msvc" 391 | version = "0.48.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 394 | 395 | [[package]] 396 | name = "windows_x86_64_gnu" 397 | version = "0.48.0" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 400 | 401 | [[package]] 402 | name = "windows_x86_64_gnullvm" 403 | version = "0.48.0" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 406 | 407 | [[package]] 408 | name = "windows_x86_64_msvc" 409 | version = "0.48.0" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 412 | 413 | [[package]] 414 | name = "xflags" 415 | version = "0.3.2" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "7d9e15fbb3de55454b0106e314b28e671279009b363e6f1d8e39fdc3bf048944" 418 | dependencies = [ 419 | "xflags-macros", 420 | ] 421 | 422 | [[package]] 423 | name = "xflags-app" 424 | version = "0.0.0" 425 | dependencies = [ 426 | "xflags", 427 | ] 428 | 429 | [[package]] 430 | name = "xflags-macros" 431 | version = "0.3.2" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "672423d4fea7ffa2f6c25ba60031ea13dc6258070556f125cc4d790007d4a155" 434 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "examples/*", 5 | ] 6 | 7 | [workspace.package] 8 | edition = "2021" 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Evgeniy Reizner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all 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 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust Arg Parsing Benchmarks 2 | 3 | This repo tries to assess Rust arg parsing performance. 4 | 5 | We currently compare: 6 | 7 | Name | Style | Notes 8 | -----------------------------------------------------|-----------------------|------ 9 | No-op | N/A | N/A 10 | [argh](https://github.com/google/argh) | `derive` | 11 | [bpaf](https://github.com/pacak/bpaf) | Combinatoric or `derive` | 12 | [clap_lex](https://github.com/clap-rs/clap) | Imperative | No help generation 13 | [clap](https://github.com/clap-rs/clap) | Builder or `derive` | Color, suggested fixes, completions 14 | [gumdrop](https://github.com/murarth/gumdrop) | `derive` | 15 | [lexopt](https://github.com/blyxxyz/lexopt) | Imperative | No help generation 16 | [pico-args](https://github.com/razrfalcon/pico-args) | Imperative | No help generation 17 | [xflags](https://github.com/matklad/xflags) | proc-macro | 18 | 19 | See also [an examination of design trade offs](docs/tradeoffs.md) 20 | 21 | *Note: any non-performance comparison is meant to provide context for what you 22 | gain/lose with each crate's overhead. For a full comparison, see each parser 23 | docs* 24 | 25 | # Results 26 | 27 | Name | Overhead (release) | Build (debug) | Parse (release) | Invalid UTF-8 | Downloads | Version 28 | -----|--------------------|---------------|-----------------|---------------|-----------|-------- 29 | null | 0 KiB | 234ms *(full)*
172ms *(incremental)* | 3ms | Y | - | - 30 | argh | 38 KiB | 3s *(full)*
203ms *(incremental)* | 4ms | N | ![Download count](https://img.shields.io/crates/dr/argh) | v0.1.10 31 | bpaf | 282 KiB | 965ms *(full)*
236ms *(incremental)* | 5ms | Y | ![Download count](https://img.shields.io/crates/dr/bpaf) | v0.9.4 32 | bpaf_derive | 276 KiB | 4s *(full)*
238ms *(incremental)* | 5ms | Y | ![Download count](https://img.shields.io/crates/dr/bpaf) | v0.9.4 33 | clap | 654 KiB | 3s *(full)*
392ms *(incremental)* | 4ms | Y | ![Download count](https://img.shields.io/crates/dr/clap) | v4.4.0 34 | clap-minimal | 427 KiB | 2s *(full)*
330ms *(incremental)* | 4ms | Y | ![Download count](https://img.shields.io/crates/dr/clap) | v4.4.0 35 | clap_derive | 689 KiB | 6s *(full)*
410ms *(incremental)* | 4ms | Y | ![Download count](https://img.shields.io/crates/dr/clap) | v4.4.0 36 | clap_lex | 27 KiB | 407ms *(full)*
188ms *(incremental)* | 3ms | Y | ![Download count](https://img.shields.io/crates/dr/clap_lex) | v0.5.1 37 | gumdrop | 37 KiB | 3s *(full)*
198ms *(incremental)* | 3ms | N | ![Download count](https://img.shields.io/crates/dr/gumdrop) | v0.8.1 38 | lexopt | 34 KiB | 385ms *(full)*
184ms *(incremental)* | 3ms | Y | ![Download count](https://img.shields.io/crates/dr/lexopt) | v0.3.0 39 | pico-args | 23 KiB | 384ms *(full)*
185ms *(incremental)* | 3ms | Y | ![Download count](https://img.shields.io/crates/dr/pico-args) | v0.5.0 40 | xflags | 22 KiB | 709ms *(full)*
179ms *(incremental)* | 3ms | Y | ![Download count](https://img.shields.io/crates/dr/xflags) | v0.3.1 41 | 42 | *System: Linux 5.4.0-124-generic (x86_64) w/ `-j 8`* 43 | 44 | *rustc: rustc 1.72.0 (5680fa18f 2023-08-23)* 45 | 46 | Notes: 47 | - Overhead will be lower if your application shares dependencies with your argument parsing library. 48 | 49 | # Running the Benchmarks 50 | 51 | ```bash 52 | $ ./bench.py 53 | $ ./format.py 54 | ``` 55 | 56 | To be included, the crate needs meet one of the following criteria: 57 | - 10k+ recent downloads 58 | - Unique API design 59 | 60 | # Special Thanks 61 | 62 | - RazrFalcon for creating the [initial benchmarks](https://github.com/RazrFalcon/pico-args) 63 | - djc for inspiration with [template-benchmarks-rs](https://github.com/djc/template-benchmarks-rs) 64 | - sharkdp for [hyperfine](https://github.com/sharkdp/hyperfine) 65 | -------------------------------------------------------------------------------- /bench.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import copy 4 | import datetime 5 | import json 6 | import multiprocessing 7 | import pathlib 8 | import platform 9 | import subprocess 10 | import sys 11 | import tempfile 12 | 13 | 14 | def main(): 15 | repo_root = pathlib.Path(__name__).parent 16 | 17 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d") 18 | hostname = platform.node() 19 | uname = platform.uname() 20 | cpus = multiprocessing.cpu_count() 21 | rustc = subprocess.run(["rustc", "--version"], check=True, capture_output=True, encoding="utf-8").stdout.strip() 22 | 23 | extension = ".exe" if sys.platform in ("win32", "cygwin") else "" 24 | 25 | runs_root = repo_root / "runs" 26 | runs_root.mkdir(parents=True, exist_ok=True) 27 | raw_run_path = runs_root / "{}-{}.json".format(timestamp, hostname) 28 | if raw_run_path.exists(): 29 | old_raw_run = json.loads(raw_run_path.read_text()) 30 | else: 31 | old_raw_run = {} 32 | 33 | raw_run = { 34 | "timestamp": timestamp, 35 | "hostname": hostname, 36 | "os": uname.system, 37 | "os_ver": uname.release, 38 | "arch": uname.machine, 39 | "cpus": cpus, 40 | "rustc": rustc, 41 | "libs": {}, 42 | } 43 | 44 | with tempfile.TemporaryDirectory() as tmpdir: 45 | for example_path in sorted((repo_root / "examples").glob("*-app")): 46 | manifest_path = example_path / "Cargo.toml" 47 | metadata = harvest_metadata(manifest_path) 48 | 49 | full_build_report_path = pathlib.Path(tmpdir) / f"{example_path.name}-build.json" 50 | if True: 51 | hyperfine_cmd = [ 52 | "hyperfine", 53 | "--warmup=1", 54 | "--min-runs=5", 55 | f"--export-json={full_build_report_path}", 56 | "--prepare=cargo clean", 57 | # Doing debug builds because that is more likely the 58 | # time directly impacting people 59 | f"cargo build -j {cpus} --manifest-path {example_path}/Cargo.toml" 60 | ] 61 | if False: 62 | hyperfine_cmd.append("--show-output") 63 | subprocess.run( 64 | hyperfine_cmd, 65 | cwd=repo_root, 66 | check=True, 67 | ) 68 | full_build_report = json.loads(full_build_report_path.read_text()) 69 | else: 70 | full_build_report = old_raw_run.get("libs", {}).get(str(manifest_path), {}).get("build_inc", None) 71 | 72 | inc_build_report_path = pathlib.Path(tmpdir) / f"{example_path.name}-build.json" 73 | if True: 74 | hyperfine_cmd = [ 75 | "hyperfine", 76 | "--warmup=1", 77 | "--min-runs=5", 78 | f"--export-json={inc_build_report_path}", 79 | f"--prepare=touch {example_path}/app.rs", 80 | # Doing debug builds because that is more likely the 81 | # time directly impacting people 82 | f"cargo build -j {cpus} --manifest-path {example_path}/Cargo.toml" 83 | ] 84 | if False: 85 | hyperfine_cmd.append("--show-output") 86 | subprocess.run( 87 | hyperfine_cmd, 88 | cwd=repo_root, 89 | check=True, 90 | ) 91 | inc_build_report = json.loads(inc_build_report_path.read_text()) 92 | else: 93 | inc_build_report = old_raw_run.get("libs", {}).get(str(manifest_path), {}).get("build_inc", None) 94 | 95 | if True: 96 | # Doing release builds because that is where size probably matters most 97 | subprocess.run(["cargo", "build", "--release", "--package", example_path.name], cwd=repo_root, check=True) 98 | app_path = repo_root / f"target/release/{example_path.name}{extension}" 99 | file_size = app_path.stat().st_size 100 | else: 101 | app_path = None 102 | file_size = old_raw_run.get("libs", {}).get(str(manifest_path), {}).get("size", None) 103 | 104 | xargs_report_path = pathlib.Path(tmpdir) / f"{example_path.name}-xargs.json" 105 | if True and app_path is not None: 106 | # This is intended to see how well the crate handles large number of arguments from 107 | # - Shell glob expansion 108 | # - `find -exec` 109 | # - Piping to `xargs` 110 | large_arg = " ".join(["some/path/that/find/found"] * 1000) 111 | hyperfine_cmd = [ 112 | "hyperfine", 113 | "--warmup=1", 114 | "--min-runs=5", 115 | f"--export-json={xargs_report_path}", 116 | # Doing debug builds because that is more likely the 117 | # time directly impacting people 118 | f"{app_path} --number 42 {large_arg}" 119 | ] 120 | if False: 121 | hyperfine_cmd.append("--show-output") 122 | subprocess.run( 123 | hyperfine_cmd, 124 | cwd=repo_root, 125 | check=True, 126 | ) 127 | xargs_report = json.loads(xargs_report_path.read_text()) 128 | else: 129 | xargs_report = old_raw_run.get("libs", {}).get(str(manifest_path), {}).get("xargs", None) 130 | 131 | p = subprocess.run(["cargo", "run", "--package", example_path.name, "--", "--number", "10", "path"], cwd=repo_root, capture_output=True, encoding="utf-8") 132 | works = p.returncode == 0 133 | 134 | p = subprocess.run(["cargo", "run", "--package", example_path.name, "--", "--number", "10", b"\xe9"], cwd=repo_root, capture_output=True, encoding="utf-8") 135 | basic_osstr = p.returncode == 0 136 | 137 | raw_run["libs"][str(manifest_path)] = { 138 | "name": example_path.name.rsplit("-", 1)[0], 139 | "manifest_path": str(manifest_path), 140 | "crate": metadata["name"], 141 | "version": metadata["version"], 142 | "build_inc": inc_build_report, 143 | "build_full": full_build_report, 144 | "xargs": xargs_report, 145 | "size": file_size, 146 | "works": works, 147 | "osstr_basic": basic_osstr, 148 | } 149 | 150 | raw_run_path.write_text(json.dumps(raw_run, indent=2)) 151 | print(raw_run_path) 152 | 153 | 154 | def harvest_metadata(manifest_path): 155 | p = subprocess.run(["cargo", "tree"], check=True, cwd=manifest_path.parent, capture_output=True, encoding="utf-8") 156 | lines = p.stdout.strip().splitlines() 157 | app_line = lines.pop(0) 158 | if lines: 159 | self_line = lines.pop(0) 160 | name, version = _extract_line(self_line) 161 | unique = sorted(set(_extract_line(line) for line in lines if "(*)" not in line and "[build-dependencies]" not in line)) 162 | else: 163 | name = None 164 | version = None 165 | 166 | return { 167 | "name": name, 168 | "version": version, 169 | } 170 | 171 | 172 | def _extract_line(line): 173 | if line.endswith(" (proc-macro)"): 174 | line = line[0:-len(" (proc-macro)")] 175 | _, name, version = line.rsplit(" ", 2) 176 | return name, version 177 | 178 | 179 | 180 | if __name__ == "__main__": 181 | main() 182 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # Note that all fields that take a lint level have these possible values: 2 | # * deny - An error will be produced and the check will fail 3 | # * warn - A warning will be produced, but the check will not fail 4 | # * allow - No warning or error will be produced, though in some cases a note 5 | # will be 6 | 7 | # This section is considered when running `cargo deny check advisories` 8 | # More documentation for the advisories section can be found here: 9 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 10 | [advisories] 11 | # The lint level for security vulnerabilities 12 | vulnerability = "deny" 13 | # The lint level for unmaintained crates 14 | unmaintained = "warn" 15 | # The lint level for crates that have been yanked from their source registry 16 | yanked = "warn" 17 | # The lint level for crates with security notices. Note that as of 18 | # 2019-12-17 there are no security notice advisories in 19 | # https://github.com/rustsec/advisory-db 20 | notice = "warn" 21 | # A list of advisory IDs to ignore. Note that ignored advisories will still 22 | # output a note when they are encountered. 23 | # 24 | # e.g. "RUSTSEC-0000-0000", 25 | ignore = [ 26 | ] 27 | 28 | # This section is considered when running `cargo deny check licenses` 29 | # More documentation for the licenses section can be found here: 30 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 31 | [licenses] 32 | unlicensed = "deny" 33 | # List of explicitly allowed licenses 34 | # See https://spdx.org/licenses/ for list of possible licenses 35 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 36 | allow = [ 37 | "MIT", 38 | "MIT-0", 39 | "Apache-2.0", 40 | "BSD-3-Clause", 41 | "MPL-2.0", 42 | "Unicode-DFS-2016", 43 | "CC0-1.0", 44 | ] 45 | # List of explicitly disallowed licenses 46 | # See https://spdx.org/licenses/ for list of possible licenses 47 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 48 | deny = [ 49 | ] 50 | # Lint level for licenses considered copyleft 51 | copyleft = "deny" 52 | # Blanket approval or denial for OSI-approved or FSF Free/Libre licenses 53 | # * both - The license will be approved if it is both OSI-approved *AND* FSF 54 | # * either - The license will be approved if it is either OSI-approved *OR* FSF 55 | # * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF 56 | # * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved 57 | # * neither - This predicate is ignored and the default lint level is used 58 | allow-osi-fsf-free = "neither" 59 | # Lint level used when no other predicates are matched 60 | # 1. License isn't in the allow or deny lists 61 | # 2. License isn't copyleft 62 | # 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" 63 | default = "deny" 64 | # The confidence threshold for detecting a license from license text. 65 | # The higher the value, the more closely the license text must be to the 66 | # canonical license text of a valid SPDX license file. 67 | # [possible values: any between 0.0 and 1.0]. 68 | confidence-threshold = 0.8 69 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 70 | # aren't accepted for every possible crate as with the normal allow list 71 | exceptions = [ 72 | # Each entry is the crate and version constraint, and its specific allow 73 | # list 74 | #{ allow = ["Zlib"], name = "adler32", version = "*" }, 75 | ] 76 | 77 | [licenses.private] 78 | # If true, ignores workspace crates that aren't published, or are only 79 | # published to private registries. 80 | # To see how to mark a crate as unpublished (to the official registry), 81 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 82 | ignore = true 83 | 84 | # This section is considered when running `cargo deny check bans`. 85 | # More documentation about the 'bans' section can be found here: 86 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 87 | [bans] 88 | # Lint level for when multiple versions of the same crate are detected 89 | multiple-versions = "warn" 90 | # Lint level for when a crate version requirement is `*` 91 | wildcards = "deny" 92 | # The graph highlighting used when creating dotgraphs for crates 93 | # with multiple versions 94 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 95 | # * simplest-path - The path to the version with the fewest edges is highlighted 96 | # * all - Both lowest-version and simplest-path are used 97 | highlight = "all" 98 | # The default lint level for `default` features for crates that are members of 99 | # the workspace that is being checked. This can be overridden by allowing/denying 100 | # `default` on a crate-by-crate basis if desired. 101 | workspace-default-features = "allow" 102 | # The default lint level for `default` features for external crates that are not 103 | # members of the workspace. This can be overridden by allowing/denying `default` 104 | # on a crate-by-crate basis if desired. 105 | external-default-features = "allow" 106 | # List of crates that are allowed. Use with care! 107 | allow = [ 108 | #{ name = "ansi_term", version = "=0.11.0" }, 109 | ] 110 | # List of crates to deny 111 | deny = [ 112 | # Each entry the name of a crate and a version range. If version is 113 | # not specified, all versions will be matched. 114 | #{ name = "ansi_term", version = "=0.11.0" }, 115 | # 116 | # Wrapper crates can optionally be specified to allow the crate when it 117 | # is a direct dependency of the otherwise banned crate 118 | #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, 119 | ] 120 | 121 | # This section is considered when running `cargo deny check sources`. 122 | # More documentation about the 'sources' section can be found here: 123 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 124 | [sources] 125 | # Lint level for what to happen when a crate from a crate registry that is not 126 | # in the allow list is encountered 127 | unknown-registry = "deny" 128 | # Lint level for what to happen when a crate from a git repository that is not 129 | # in the allow list is encountered 130 | unknown-git = "deny" 131 | # List of URLs for allowed crate registries. Defaults to the crates.io index 132 | # if not specified. If it is specified but empty, no registries are allowed. 133 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 134 | # List of URLs for allowed Git repositories 135 | allow-git = [] 136 | 137 | [sources.allow-org] 138 | # 1 or more github.com organizations to allow git sources for 139 | github = [] 140 | -------------------------------------------------------------------------------- /docs/tradeoffs.md: -------------------------------------------------------------------------------- 1 | # Design Trade-offs 2 | 3 | This will be looking at CLI parser design trade offs from the lens of comparing 4 | [bpaf](https://docs.rs/bpaf) and [clap](https://docs.rs/clap). 5 | 6 | For anyone asking the question "which should I use?", the short answer would be 7 | - `bpaf` would work well for simple cases like example bins for a library 8 | while giving the best build times and a reasonable to understand code with 9 | the `derive` API 10 | - All other cases the answer is "it depends" as the two designs offer different 11 | trade offs. If you want to just choose one with little research that will 12 | cover the most use cases, that will most likely be `clap`. 13 | 14 | Meta: 15 | - This was written as of clap 3.2.21 and bpaf 0.6.0 though the focus was more on design goals 16 | - This was written by the maintainer of clap with [input from the maintainer of bpaf](https://github.com/rosetta-rs/argparse-rosetta-rs/pull/50) 17 | 18 | ## Static vs Dynamic Typing 19 | 20 | `bpaf`'s combinator API uses generics and macros to build a statically-typed 21 | parser, pushing development errors to compile time. This minimizes binary 22 | size and runtime, only paying for what you use. 23 | 24 | The combinator approach tends towards yoda-speak (like 25 | [yoda conditions](https://en.wikipedia.org/wiki/Yoda_conditions)), 26 | though with [careful 27 | structuring by breaking down arguments into 28 | functions](https://github.com/pacak/bpaf/blob/aa6992931bbfbdca6390c87f4a76898f8db0ae47/examples/top_to_bottom.rs), 29 | a more straightforward ordering can be accomplished. 30 | 31 | `clap`'s builder API stores parsed values in a Map-like container where the 32 | keys are the argument IDs and the values are effectively `Box`. 33 | - This allows dynamically created CLIs, like with 34 | [clap_serde](https://docs.rs/clap_serde) or [conditionally-present 35 | flags](https://github.com/sharkdp/bat/blob/6680f65e4b25b0f18c455f7a4639a96e97519dc5/src/bin/bat/clap_app.rs#L556) 36 | - Callers can dynamically iterate over the arguments, like for layered configs. 37 | - `clap` can have ready-to-use, arbitrarily defined defaults and validation 38 | conditioned on argument values (e.g. `default_value_if`). See the section on 39 | Validation for more nuance on the "arbitrarily" part 40 | - While not many errors are at compile-time, `clap` tries to help push as many 41 | errors to early test-time with minimal effort with the `cmd.debug_assert()` 42 | function. Value lookups still requires full coverage of that code to catch 43 | errors. 44 | 45 | Which design approach is faster to build will be dependent on the exact 46 | implementation and the compiler. 47 | 48 | ## Context-sensitive parsing 49 | 50 | Parsing arguments is context sensitive. Consider: 51 | ```console 52 | $ prog --one two --three --four -abcdef 53 | ``` 54 | - When is an argument a subcommand, a positional value, a flag, or a flag's value? 55 | - When is the trailing parts of a short flag an attached value or more shorts flags? 56 | 57 | For example, some possible interpretations of the above could be: 58 | ```console 59 | $ prog --one=two --three=--four -a=bcdef 60 | $ prog two -a -b -c -d -e -f --one --three --four 61 | ``` 62 | 63 | `clap` parses left-to-right using [`clap_lex`](https://docs.rs/clap_lex), with 64 | the output from the previous token hinting how to parse the next one to 65 | prevent ambiguity. This leads to a fairly complicated parser to handle all of 66 | the different cases upfront, including some usability features to help catch 67 | developer mistakes. 68 | 69 | `bpaf` instead does context-free tokenization, storing all flags and values in 70 | a list. 71 | - By default tokens that look like flags can only be considered flags, 72 | unless escaped with `--`. 73 | - An argument is resolved as either a subcommand, positional value, or a flag's 74 | value based on the order that the they are processed (as determined by their 75 | definition order) 76 | - With context manipulation modifiers you can parse a large variety of cases: 77 | `find`'s `--exec rm -rf ;`, `Xorg`'s `+foo -foo`, `dd`'s `count=N if=foo of=bar`, 78 | windows standard `/opt`, sequential command chaining, "option structures", multi 79 | value options, etc: https://docs.rs/bpaf/0.6.0/bpaf/_unusual/index.html 80 | 81 | While `bpaf` supports fewer convenience methods out of the box, it has an overall 82 | simpler parser that then scales up in runtime, build time, and code size based on 83 | the arguments themselves, only paying for what you use. 84 | 85 | 86 | 87 | ## Validation 88 | 89 | Specifically when arguments conflict, override, or require each other. 90 | 91 | `bpaf` uses function/macro combinators to declare argument / group of argument relationships. This 92 | offers a lot of flexibility that, again, you only pay for what you use. 93 | 94 | The downside to this approach is it couples together: 95 | - Parsing disambiguation with positionals 96 | - Validation (in a strict tree structure) 97 | - Help ordering 98 | - Help sections headers 99 | - Code structure building up the combinators 100 | - Data structures as each level is its own `struct` 101 | - Organizing some of the validation rules aren't always as intuitive for people 102 | not steeped in a functional way of thinking 103 | 104 | `clap` provides `ArgGroup` to compose arguments and other groups with 105 | relationships defined in terms of either group or argument IDs. `ArgGroup`s 106 | (and the separate help section header feature) are tags on arguments. This 107 | allows a very flexible directed acyclic graph of relationships. 108 | 109 | The downside to this approach 110 | - Everyone pays the runtime, compile time, and code size cost, no matter which subset they are using 111 | - Developers are limited by what relationships `clap` has predefined 112 | - Even once `clap` opens up to user-provided relationships, the ergonomics for 113 | defining them won't be as nice as it will require implementing a trait that 114 | uses the lower levels of clap's API and then passing it in to the parser 115 | 116 | ## Derive APIs 117 | 118 | Both libraries provide derive APIs that mask over the static and dynamic typing differences. 119 | 120 | In `bpaf`s case, the combinators still show through in terms of requiring the 121 | user to organize their data structures around their validation. Some times 122 | this is good (pushing errors to compile time like if mutually exclusive 123 | arguments are represented in an `enum`) while at other times it has the potential to convolute the code. 124 | 125 | In `clap`s case, it has the challenge of hand-implemented support to express 126 | each case of argument relationships in the type system (which hasn't been done 127 | yet). 128 | 129 | In both cases, some errors are still pushed off from compile time to early test 130 | time through asserts. 131 | 132 | ## Maturity 133 | 134 | While this document is focused on design trade-offs, we understand some users 135 | will look to this to help understand which would work better for them. 136 | 137 | `clap` has been around for many more years and has a lot more users from 138 | varying backgrounds solving different problems and `clap` has taken input and 139 | adapted to help meet a variety of needs. 140 | 141 | `bpaf` is a younger project but it is able to move a lot more quickly because 142 | it aims to provide minimal set of tools for users to create a desired combination 143 | rather than the combinations themselves. `bpaf` already covers 144 | most of the common cases for creating a polished CLI out of the box but can be 145 | used to parse a lot more with some extra manual effort. 146 | 147 | An exact feature-by-feature comparison is out of the scope as `clap` and `bpaf` 148 | are both constantly evolving. 149 | -------------------------------------------------------------------------------- /examples/argh-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "argh-app" 3 | edition.workspace = true 4 | 5 | [[bin]] 6 | name = "argh-app" 7 | path = "app.rs" 8 | 9 | [dependencies] 10 | argh = "0.1" 11 | -------------------------------------------------------------------------------- /examples/argh-app/app.rs: -------------------------------------------------------------------------------- 1 | use argh::FromArgs; 2 | 3 | /// App 4 | #[derive(Debug, FromArgs)] 5 | struct AppArgs { 6 | /// sets number 7 | #[argh(option)] 8 | number: u32, 9 | 10 | /// sets optional number 11 | #[argh(option)] 12 | opt_number: Option, 13 | 14 | /// sets width [default: 10] 15 | #[argh(option, default = "10", from_str_fn(parse_width))] 16 | width: u32, 17 | 18 | /// input 19 | #[argh(positional)] 20 | input: Vec, 21 | } 22 | 23 | fn parse_width(s: &str) -> Result { 24 | let w = s.parse().map_err(|_| "not a number")?; 25 | if w != 0 { 26 | Ok(w) 27 | } else { 28 | Err("width must be positive".to_string()) 29 | } 30 | } 31 | 32 | fn main() { 33 | let args: AppArgs = argh::from_env(); 34 | #[cfg(debug_assertions)] 35 | { 36 | println!("{:#?}", args.number); 37 | println!("{:#?}", args.opt_number); 38 | println!("{:#?}", args.width); 39 | if 10 < args.input.len() { 40 | println!("{:#?}", args.input.len()); 41 | } else { 42 | println!("{:#?}", args); 43 | } 44 | } 45 | std::hint::black_box(args); 46 | } 47 | -------------------------------------------------------------------------------- /examples/bpaf-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bpaf-app" 3 | edition.workspace = true 4 | 5 | [[bin]] 6 | name = "bpaf-app" 7 | path = "app.rs" 8 | 9 | [dependencies] 10 | bpaf = "0.9.12" 11 | -------------------------------------------------------------------------------- /examples/bpaf-app/app.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use bpaf::{construct, long, positional, Parser}; 4 | 5 | #[derive(Debug, Clone)] 6 | struct AppArgs { 7 | number: u32, 8 | opt_number: Option, 9 | width: u32, 10 | input: Vec, 11 | } 12 | 13 | fn as_width(s: String) -> Result { 14 | let w: u32 = s.parse().map_err(|_| "not a number")?; 15 | if w != 0 { 16 | Ok(w) 17 | } else { 18 | Err("width must be positive".to_string()) 19 | } 20 | } 21 | 22 | fn main() { 23 | let number = long("number") 24 | .help("Sets a number") 25 | .argument::("NUMBER"); 26 | let opt_number = long("opt-number") 27 | .help("Sets an optional number") 28 | .argument::("OPT-NUMBER") 29 | .optional(); 30 | let width = long("width") 31 | .help("Sets width") 32 | .argument::("WIDTH") 33 | .parse(as_width) 34 | .fallback(10); 35 | let input = positional::("INPUT").many(); 36 | 37 | let parser = construct!(AppArgs { 38 | number, 39 | opt_number, 40 | width, 41 | input 42 | }) 43 | .to_options() 44 | .descr("App"); 45 | 46 | let args = parser.run(); 47 | 48 | #[cfg(debug_assertions)] 49 | { 50 | println!("{:#?}", args.number); 51 | println!("{:#?}", args.opt_number); 52 | println!("{:#?}", args.width); 53 | if 10 < args.input.len() { 54 | println!("{:#?}", args.input.len()); 55 | } else { 56 | println!("{:#?}", args); 57 | } 58 | } 59 | std::hint::black_box(args); 60 | } 61 | -------------------------------------------------------------------------------- /examples/bpaf_derive-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bpaf_derive-app" 3 | edition.workspace = true 4 | 5 | [[bin]] 6 | name = "bpaf_derive-app" 7 | path = "app.rs" 8 | 9 | [dependencies] 10 | bpaf = { version = "0.9.12", features = ["derive"] } 11 | -------------------------------------------------------------------------------- /examples/bpaf_derive-app/app.rs: -------------------------------------------------------------------------------- 1 | use bpaf::Bpaf; 2 | 3 | #[derive(Debug, Clone, Bpaf)] 4 | #[bpaf(options)] 5 | /// App 6 | struct AppArgs { 7 | /// Sets a number 8 | #[bpaf(argument("NUMBER"))] 9 | number: u32, 10 | 11 | /// Sets an optional number 12 | #[bpaf(argument("OPT-NUMBER"))] 13 | opt_number: Option, 14 | 15 | /// Sets width 16 | #[bpaf( 17 | argument("WIDTH"), 18 | guard(valid_width, "width must be positive"), 19 | fallback(10) 20 | )] 21 | width: u32, 22 | 23 | #[bpaf(positional("INPUT"))] 24 | input: Vec, 25 | } 26 | 27 | fn valid_width(width: &u32) -> bool { 28 | *width > 0 29 | } 30 | 31 | fn main() { 32 | let args = app_args().run(); 33 | 34 | #[cfg(debug_assertions)] 35 | { 36 | println!("{:#?}", args.number); 37 | println!("{:#?}", args.opt_number); 38 | println!("{:#?}", args.width); 39 | if 10 < args.input.len() { 40 | println!("{:#?}", args.input.len()); 41 | } else { 42 | println!("{:#?}", args); 43 | } 44 | } 45 | std::hint::black_box(args); 46 | } 47 | -------------------------------------------------------------------------------- /examples/clap-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clap-app" 3 | edition.workspace = true 4 | 5 | [[bin]] 6 | name = "clap-app" 7 | path = "app.rs" 8 | 9 | [dependencies] 10 | clap = "4.5.4" 11 | -------------------------------------------------------------------------------- /examples/clap-app/app.rs: -------------------------------------------------------------------------------- 1 | use clap::{value_parser, Arg, Command}; 2 | 3 | #[derive(Debug)] 4 | struct AppArgs { 5 | number: u32, 6 | opt_number: Option, 7 | width: u32, 8 | input: Vec, 9 | } 10 | 11 | fn parse_width(s: &str) -> Result { 12 | let w = s.parse().map_err(|_| "not a number")?; 13 | if w != 0 { 14 | Ok(w) 15 | } else { 16 | Err("width must be positive".to_string()) 17 | } 18 | } 19 | 20 | fn main() { 21 | let matches = Command::new("App") 22 | .arg( 23 | Arg::new("number") 24 | .long("number") 25 | .required(true) 26 | .help("Sets a number") 27 | .value_parser(value_parser!(u32)), 28 | ) 29 | .arg( 30 | Arg::new("opt-number") 31 | .long("opt-number") 32 | .help("Sets an optional number") 33 | .value_parser(value_parser!(u32)), 34 | ) 35 | .arg( 36 | Arg::new("width") 37 | .long("width") 38 | .default_value("10") 39 | .value_parser(parse_width) 40 | .help("Sets width"), 41 | ) 42 | .arg( 43 | Arg::new("INPUT") 44 | .num_args(1..) 45 | .value_parser(value_parser!(std::path::PathBuf)), 46 | ) 47 | .get_matches(); 48 | 49 | let args = AppArgs { 50 | number: *matches.get_one::("number").unwrap(), 51 | opt_number: matches.get_one::("opt-number").cloned(), 52 | width: matches.get_one::("width").cloned().unwrap(), 53 | input: matches 54 | .get_many::("INPUT") 55 | .unwrap_or_default() 56 | .cloned() 57 | .collect(), 58 | }; 59 | 60 | #[cfg(debug_assertions)] 61 | { 62 | println!("{:#?}", args.number); 63 | println!("{:#?}", args.opt_number); 64 | println!("{:#?}", args.width); 65 | if 10 < args.input.len() { 66 | println!("{:#?}", args.input.len()); 67 | } else { 68 | println!("{:#?}", args); 69 | } 70 | } 71 | std::hint::black_box(args); 72 | } 73 | -------------------------------------------------------------------------------- /examples/clap-minimal-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clap-minimal-app" 3 | edition.workspace = true 4 | 5 | [[bin]] 6 | name = "clap-minimal-app" 7 | path = "app.rs" 8 | 9 | [dependencies] 10 | clap = { version = "4.5.4", default-features = false, features = ["std"] } 11 | -------------------------------------------------------------------------------- /examples/clap-minimal-app/app.rs: -------------------------------------------------------------------------------- 1 | use clap::{value_parser, Arg, Command}; 2 | 3 | #[derive(Debug)] 4 | struct AppArgs { 5 | number: u32, 6 | opt_number: Option, 7 | width: u32, 8 | input: Vec, 9 | } 10 | 11 | fn parse_width(s: &str) -> Result { 12 | let w = s.parse().map_err(|_| "not a number")?; 13 | if w != 0 { 14 | Ok(w) 15 | } else { 16 | Err("width must be positive".to_string()) 17 | } 18 | } 19 | 20 | fn main() { 21 | let matches = Command::new("App") 22 | .arg( 23 | Arg::new("number") 24 | .long("number") 25 | .required(true) 26 | .help("Sets a number") 27 | .value_parser(value_parser!(u32)), 28 | ) 29 | .arg( 30 | Arg::new("opt-number") 31 | .long("opt-number") 32 | .help("Sets an optional number") 33 | .value_parser(value_parser!(u32)), 34 | ) 35 | .arg( 36 | Arg::new("width") 37 | .long("width") 38 | .default_value("10") 39 | .value_parser(parse_width) 40 | .help("Sets width"), 41 | ) 42 | .arg( 43 | Arg::new("INPUT") 44 | .num_args(1..) 45 | .value_parser(value_parser!(std::path::PathBuf)), 46 | ) 47 | .get_matches(); 48 | 49 | let args = AppArgs { 50 | number: *matches.get_one::("number").unwrap(), 51 | opt_number: matches.get_one::("opt-number").cloned(), 52 | width: matches.get_one::("width").cloned().unwrap(), 53 | input: matches 54 | .get_many::("INPUT") 55 | .unwrap_or_default() 56 | .cloned() 57 | .collect(), 58 | }; 59 | 60 | #[cfg(debug_assertions)] 61 | { 62 | println!("{:#?}", args.number); 63 | println!("{:#?}", args.opt_number); 64 | println!("{:#?}", args.width); 65 | if 10 < args.input.len() { 66 | println!("{:#?}", args.input.len()); 67 | } else { 68 | println!("{:#?}", args); 69 | } 70 | } 71 | std::hint::black_box(args); 72 | } 73 | -------------------------------------------------------------------------------- /examples/clap_derive-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clap_derive-app" 3 | edition.workspace = true 4 | 5 | [[bin]] 6 | name = "clap_derive-app" 7 | path = "app.rs" 8 | 9 | [dependencies] 10 | clap = { version = "4.5.4", features = ["derive"] } 11 | -------------------------------------------------------------------------------- /examples/clap_derive-app/app.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser, Debug)] 4 | struct AppArgs { 5 | /// Sets a number. 6 | #[arg(long)] 7 | number: u32, 8 | 9 | /// Sets an optional number. 10 | #[arg(long)] 11 | opt_number: Option, 12 | 13 | /// Sets width. 14 | #[arg(long, default_value = "10", value_parser = parse_width)] 15 | width: u32, 16 | 17 | input: Vec, 18 | } 19 | 20 | fn parse_width(s: &str) -> Result { 21 | let w = s.parse().map_err(|_| "not a number")?; 22 | if w != 0 { 23 | Ok(w) 24 | } else { 25 | Err("width must be positive".to_string()) 26 | } 27 | } 28 | 29 | fn main() { 30 | let args = AppArgs::parse(); 31 | #[cfg(debug_assertions)] 32 | { 33 | println!("{:#?}", args.number); 34 | println!("{:#?}", args.opt_number); 35 | println!("{:#?}", args.width); 36 | if 10 < args.input.len() { 37 | println!("{:#?}", args.input.len()); 38 | } else { 39 | println!("{:#?}", args); 40 | } 41 | } 42 | std::hint::black_box(args); 43 | } 44 | -------------------------------------------------------------------------------- /examples/clap_lex-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clap_lex-app" 3 | edition.workspace = true 4 | 5 | [[bin]] 6 | name = "clap_lex-app" 7 | path = "app.rs" 8 | 9 | [dependencies] 10 | clap_lex = "0.7.0" 11 | -------------------------------------------------------------------------------- /examples/clap_lex-app/app.rs: -------------------------------------------------------------------------------- 1 | type BoxedError = Box; 2 | 3 | const HELP: &str = "\ 4 | USAGE: app [OPTIONS] --number NUMBER INPUT.. 5 | 6 | OPTIONS: 7 | --number NUMBER Set a number (required) 8 | --opt-number NUMBER Set an optional number 9 | --width WIDTH Set a width (non-zero, default 10) 10 | 11 | ARGS: 12 | Input file 13 | "; 14 | 15 | #[derive(Debug)] 16 | struct AppArgs { 17 | number: u32, 18 | opt_number: Option, 19 | width: u32, 20 | input: Vec, 21 | } 22 | 23 | fn parse_width(s: &str) -> Result { 24 | let w = s.parse().map_err(|_| "not a number")?; 25 | if w != 0 { 26 | Ok(w) 27 | } else { 28 | Err("width must be positive".to_string()) 29 | } 30 | } 31 | 32 | fn main() { 33 | let args = match parse_args() { 34 | Ok(args) => args, 35 | Err(err) => { 36 | eprintln!("Error: {}.", err); 37 | std::process::exit(1); 38 | } 39 | }; 40 | #[cfg(debug_assertions)] 41 | { 42 | println!("{:#?}", args.number); 43 | println!("{:#?}", args.opt_number); 44 | println!("{:#?}", args.width); 45 | if 10 < args.input.len() { 46 | println!("{:#?}", args.input.len()); 47 | } else { 48 | println!("{:#?}", args); 49 | } 50 | } 51 | std::hint::black_box(args); 52 | } 53 | 54 | fn parse_args() -> Result { 55 | let mut number = None; 56 | let mut opt_number = None; 57 | let mut width = 10; 58 | let mut input = Vec::new(); 59 | 60 | let raw = clap_lex::RawArgs::from_args(); 61 | let mut cursor = raw.cursor(); 62 | while let Some(arg) = raw.next(&mut cursor) { 63 | if arg.is_escape() { 64 | input.extend(raw.remaining(&mut cursor).map(std::path::PathBuf::from)); 65 | } else if arg.is_stdio() { 66 | input.push(std::path::PathBuf::from("-")); 67 | } else if let Some((long, value)) = arg.to_long() { 68 | match long { 69 | Ok("help") => { 70 | print!("{}", HELP); 71 | std::process::exit(0); 72 | } 73 | Ok("number") => { 74 | let value = if let Some(value) = value { 75 | value.to_str().ok_or_else(|| { 76 | format!("Value `{}` is not a number", value.to_string_lossy()) 77 | })? 78 | } else { 79 | let value = raw 80 | .next_os(&mut cursor) 81 | .ok_or_else(|| "`--number` is missing a value".to_owned())?; 82 | value.to_str().ok_or_else(|| { 83 | format!("Value `{}` is not a number", value.to_string_lossy()) 84 | })? 85 | }; 86 | number = Some(value.parse()?); 87 | } 88 | Ok("opt-number") => { 89 | let value = if let Some(value) = value { 90 | value.to_str().ok_or_else(|| { 91 | format!("Value `{}` is not a number", value.to_string_lossy()) 92 | })? 93 | } else { 94 | let value = raw 95 | .next_os(&mut cursor) 96 | .ok_or_else(|| "`--number` is missing a value".to_owned())?; 97 | value.to_str().ok_or_else(|| { 98 | format!("Value `{}` is not a number", value.to_string_lossy()) 99 | })? 100 | }; 101 | opt_number = Some(value.parse()?); 102 | } 103 | Ok("width") => { 104 | let value = if let Some(value) = value { 105 | value.to_str().ok_or_else(|| { 106 | format!("Value `{}` is not a number", value.to_string_lossy()) 107 | })? 108 | } else { 109 | let value = raw 110 | .next_os(&mut cursor) 111 | .ok_or_else(|| "`--number` is missing a value".to_owned())?; 112 | value.to_str().ok_or_else(|| { 113 | format!("Value `{}` is not a number", value.to_string_lossy()) 114 | })? 115 | }; 116 | width = parse_width(value)?; 117 | } 118 | _ => { 119 | return Err(format!("Unexpected flag: {}", arg.display()).into()); 120 | } 121 | } 122 | } else if let Some(mut shorts) = arg.to_short() { 123 | #[allow(clippy::never_loop)] // leave this refactor-proof 124 | while let Some(short) = shorts.next_flag() { 125 | match short { 126 | Ok('h') => { 127 | print!("{}", HELP); 128 | std::process::exit(0); 129 | } 130 | Ok(c) => { 131 | return Err(format!("Unexpected flag: -{}", c).into()); 132 | } 133 | Err(e) => { 134 | return Err(format!("Unexpected flag: -{}", e.to_string_lossy()).into()); 135 | } 136 | } 137 | } 138 | } else { 139 | input.push(std::path::PathBuf::from(arg.to_value_os().to_owned())); 140 | } 141 | } 142 | 143 | Ok(AppArgs { 144 | number: number.ok_or("missing required option --number".to_owned())?, 145 | opt_number, 146 | width, 147 | input, 148 | }) 149 | } 150 | -------------------------------------------------------------------------------- /examples/gumdrop-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gumdrop-app" 3 | edition.workspace = true 4 | 5 | [[bin]] 6 | name = "gumdrop-app" 7 | path = "app.rs" 8 | 9 | [dependencies] 10 | gumdrop = "0.8" 11 | -------------------------------------------------------------------------------- /examples/gumdrop-app/app.rs: -------------------------------------------------------------------------------- 1 | use gumdrop::Options; 2 | 3 | #[derive(Debug, Options)] 4 | struct AppArgs { 5 | #[options(help = "Shows help")] 6 | help: bool, 7 | 8 | #[options(no_short, required, help = "Sets a number")] 9 | number: u32, 10 | 11 | #[options(no_short, help = "Sets an optional number")] 12 | opt_number: Option, 13 | 14 | #[options( 15 | no_short, 16 | help = "Sets width", 17 | default = "10", 18 | parse(try_from_str = "parse_width") 19 | )] 20 | width: u32, 21 | 22 | #[options(free, help = "Input file")] 23 | input: Vec, 24 | } 25 | 26 | fn parse_width(s: &str) -> Result { 27 | let w = s.parse().map_err(|_| "not a number")?; 28 | if w != 0 { 29 | Ok(w) 30 | } else { 31 | Err("width must be positive".to_string()) 32 | } 33 | } 34 | 35 | fn main() { 36 | let args = AppArgs::parse_args_default_or_exit(); 37 | #[cfg(debug_assertions)] 38 | { 39 | println!("{:#?}", args.number); 40 | println!("{:#?}", args.opt_number); 41 | println!("{:#?}", args.width); 42 | if 10 < args.input.len() { 43 | println!("{:#?}", args.input.len()); 44 | } else { 45 | println!("{:#?}", args); 46 | } 47 | } 48 | std::hint::black_box(args); 49 | } 50 | -------------------------------------------------------------------------------- /examples/lexopt-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lexopt-app" 3 | edition.workspace = true 4 | 5 | [[bin]] 6 | name = "lexopt-app" 7 | path = "app.rs" 8 | 9 | [dependencies] 10 | lexopt = "0.3" 11 | -------------------------------------------------------------------------------- /examples/lexopt-app/app.rs: -------------------------------------------------------------------------------- 1 | const HELP: &str = "\ 2 | USAGE: app [OPTIONS] --number NUMBER INPUT.. 3 | 4 | OPTIONS: 5 | --number NUMBER Set a number (required) 6 | --opt-number NUMBER Set an optional number 7 | --width WIDTH Set a width (non-zero, default 10) 8 | 9 | ARGS: 10 | Input file 11 | "; 12 | 13 | #[derive(Debug)] 14 | struct AppArgs { 15 | number: u32, 16 | opt_number: Option, 17 | width: u32, 18 | input: Vec, 19 | } 20 | 21 | fn parse_width(s: &str) -> Result { 22 | let w = s.parse().map_err(|_| "not a number")?; 23 | if w != 0 { 24 | Ok(w) 25 | } else { 26 | Err("width must be positive".to_string()) 27 | } 28 | } 29 | 30 | fn main() { 31 | let args = match parse_args() { 32 | Ok(args) => args, 33 | Err(err) => { 34 | eprintln!("Error: {}.", err); 35 | std::process::exit(1); 36 | } 37 | }; 38 | #[cfg(debug_assertions)] 39 | { 40 | println!("{:#?}", args.number); 41 | println!("{:#?}", args.opt_number); 42 | println!("{:#?}", args.width); 43 | if 10 < args.input.len() { 44 | println!("{:#?}", args.input.len()); 45 | } else { 46 | println!("{:#?}", args); 47 | } 48 | } 49 | std::hint::black_box(args); 50 | } 51 | 52 | fn parse_args() -> Result { 53 | use lexopt::prelude::*; 54 | 55 | let mut number = None; 56 | let mut opt_number = None; 57 | let mut width = 10; 58 | let mut input = Vec::new(); 59 | 60 | let mut parser = lexopt::Parser::from_env(); 61 | while let Some(arg) = parser.next()? { 62 | match arg { 63 | Short('h') | Long("help") => { 64 | print!("{}", HELP); 65 | std::process::exit(0); 66 | } 67 | Long("number") => number = Some(parser.value()?.parse()?), 68 | Long("opt-number") => opt_number = Some(parser.value()?.parse()?), 69 | Long("width") => width = parser.value()?.parse_with(parse_width)?, 70 | Value(path) => input.push(path.into()), 71 | _ => return Err(arg.unexpected()), 72 | } 73 | } 74 | Ok(AppArgs { 75 | number: number.ok_or("missing required option --number")?, 76 | opt_number, 77 | width, 78 | input, 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /examples/null-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "null-app" 3 | edition.workspace = true 4 | 5 | [[bin]] 6 | name = "null-app" 7 | path = "app.rs" 8 | -------------------------------------------------------------------------------- /examples/null-app/app.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let args: Vec<_> = std::env::args_os().collect(); 3 | #[cfg(debug_assertions)] 4 | { 5 | if 10 < args.len() { 6 | println!("{:#?}", args.len()); 7 | } else { 8 | println!("{:#?}", args); 9 | } 10 | } 11 | std::hint::black_box(args); 12 | } 13 | -------------------------------------------------------------------------------- /examples/pico-args-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pico-args-app" 3 | edition.workspace = true 4 | 5 | [[bin]] 6 | name = "pico-args-app" 7 | path = "app.rs" 8 | 9 | [dependencies] 10 | pico-args = "0.5" 11 | 12 | [features] 13 | default = ["eq-separator"] 14 | eq-separator = ["pico-args/eq-separator"] 15 | -------------------------------------------------------------------------------- /examples/pico-args-app/app.rs: -------------------------------------------------------------------------------- 1 | const HELP: &str = "\ 2 | App 3 | 4 | USAGE: 5 | app [OPTIONS] --number NUMBER [INPUT].. 6 | 7 | FLAGS: 8 | -h, --help Prints help information 9 | 10 | OPTIONS: 11 | --number NUMBER Sets a number 12 | --opt-number NUMBER Sets an optional number 13 | --width WIDTH Sets width [default: 10] 14 | 15 | ARGS: 16 | 17 | "; 18 | 19 | #[derive(Debug)] 20 | struct AppArgs { 21 | number: u32, 22 | opt_number: Option, 23 | width: u32, 24 | input: Vec, 25 | } 26 | 27 | fn parse_width(s: &str) -> Result { 28 | let w = s.parse().map_err(|_| "not a number")?; 29 | if w != 0 { 30 | Ok(w) 31 | } else { 32 | Err("width must be positive".to_string()) 33 | } 34 | } 35 | 36 | fn parse_path(s: &std::ffi::OsStr) -> Result { 37 | Ok(std::path::PathBuf::from(s)) 38 | } 39 | 40 | fn main() { 41 | let args = match parse_args() { 42 | Ok(v) => v, 43 | Err(e) => { 44 | eprintln!("Error: {}.", e); 45 | std::process::exit(1); 46 | } 47 | }; 48 | 49 | #[cfg(debug_assertions)] 50 | { 51 | println!("{:#?}", args.number); 52 | println!("{:#?}", args.opt_number); 53 | println!("{:#?}", args.width); 54 | if 10 < args.input.len() { 55 | println!("{:#?}", args.input.len()); 56 | } else { 57 | println!("{:#?}", args); 58 | } 59 | } 60 | std::hint::black_box(args); 61 | } 62 | 63 | fn parse_args() -> Result { 64 | let mut pargs = pico_args::Arguments::from_env(); 65 | 66 | if pargs.contains(["-h", "--help"]) { 67 | print!("{}", HELP); 68 | std::process::exit(0); 69 | } 70 | 71 | let mut args = AppArgs { 72 | number: pargs.value_from_str("--number")?, 73 | opt_number: pargs.opt_value_from_str("--opt-number")?, 74 | width: pargs 75 | .opt_value_from_fn("--width", parse_width)? 76 | .unwrap_or(10), 77 | input: Vec::new(), 78 | }; 79 | 80 | while let Some(value) = pargs.opt_free_from_os_str(parse_path)? { 81 | args.input.push(value); 82 | } 83 | 84 | Ok(args) 85 | } 86 | -------------------------------------------------------------------------------- /examples/xflags-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xflags-app" 3 | edition.workspace = true 4 | 5 | [[bin]] 6 | name = "xflags-app" 7 | path = "app.rs" 8 | 9 | [dependencies] 10 | xflags = "0.3.2" 11 | -------------------------------------------------------------------------------- /examples/xflags-app/app.rs: -------------------------------------------------------------------------------- 1 | mod flags { 2 | // xflags! doesn't support `:` in types 3 | use std::path::PathBuf; 4 | 5 | xflags::xflags! { 6 | src "./app.rs" 7 | 8 | cmd app 9 | { 10 | repeated input: PathBuf 11 | /// Sets a number 12 | required --number number: u32 13 | /// Sets an optional number 14 | optional --opt-number opt_number: u32 15 | /// Sets width 16 | optional --width width: u32 17 | } 18 | } 19 | 20 | // generated start 21 | // The following code is generated by `xflags` macro. 22 | // Run `env UPDATE_XFLAGS=1 cargo build` to regenerate. 23 | #[derive(Debug)] 24 | pub struct App { 25 | pub input: Vec, 26 | 27 | pub number: u32, 28 | pub opt_number: Option, 29 | pub width: Option, 30 | } 31 | 32 | impl App { 33 | #[allow(dead_code)] 34 | pub fn from_env_or_exit() -> Self { 35 | Self::from_env_or_exit_() 36 | } 37 | 38 | #[allow(dead_code)] 39 | pub fn from_env() -> xflags::Result { 40 | Self::from_env_() 41 | } 42 | 43 | #[allow(dead_code)] 44 | pub fn from_vec(args: Vec) -> xflags::Result { 45 | Self::from_vec_(args) 46 | } 47 | } 48 | // generated end 49 | 50 | impl App { 51 | pub fn validate(&self) -> xflags::Result<()> { 52 | if let Some(width) = self.width { 53 | if width == 0 { 54 | return Err(xflags::Error::new("width must be positive")); 55 | } 56 | } 57 | Ok(()) 58 | } 59 | } 60 | } 61 | 62 | fn main() { 63 | let args = match flags::App::from_env() { 64 | Ok(args) => args, 65 | Err(err) => { 66 | err.exit(); 67 | } 68 | }; 69 | match args.validate() { 70 | Ok(()) => {} 71 | Err(err) => { 72 | err.exit(); 73 | } 74 | } 75 | #[cfg(debug_assertions)] 76 | { 77 | println!("{:#?}", args.number); 78 | println!("{:#?}", args.opt_number); 79 | println!("{:#?}", args.width); 80 | if 10 < args.input.len() { 81 | println!("{:#?}", args.input.len()); 82 | } else { 83 | println!("{:#?}", args); 84 | } 85 | } 86 | std::hint::black_box(args); 87 | } 88 | -------------------------------------------------------------------------------- /format.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pathlib 4 | import json 5 | import argparse 6 | 7 | 8 | def main(): 9 | repo_root = pathlib.Path(__name__).parent 10 | runs_root = repo_root / "runs" 11 | default_run_path = sorted(runs_root.glob("*.json"))[-1] 12 | 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument("--run", metavar="PATH", type=pathlib.Path, default=default_run_path, help="Default: %(default)s") 15 | args = parser.parse_args() 16 | 17 | data = json.loads(args.run.read_text()) 18 | cases = sorted(data["libs"].values(), key=lambda c: (c["crate"] if c["crate"] else "", c["name"])) 19 | 20 | print("Name | Overhead (release) | Build (debug) | Parse (release) | Invalid UTF-8 | Downloads | Version") 21 | print("-----|--------------------|---------------|-----------------|---------------|-----------|--------") 22 | for case in cases: 23 | if case["name"] != "null": 24 | count_link = "![Download count](https://img.shields.io/crates/dr/{})".format(case["crate"]) 25 | else: 26 | count_link = "-" 27 | row = [ 28 | case["name"], 29 | fmt_size(case, cases[0]), 30 | "{} *(full)*
{} *(incremental)*".format(fmt_time(case, "build_full"), fmt_time(case, "build_inc")), 31 | fmt_time(case, "xargs"), 32 | "Y" if case["osstr_basic"] else "N", 33 | count_link, 34 | case["version"] if case["version"] else "-", 35 | ] 36 | print(" | ".join(row)) 37 | print() 38 | print(f"*System: {data['os']} {data['os_ver']} ({data['arch']}), {data.get('rustc', '')} w/ `-j {data['cpus']}`*") 39 | 40 | 41 | def fmt_time(case, bench): 42 | bench = case[bench] 43 | if bench is None: 44 | return "N/A" 45 | 46 | value = bench["results"][0]["median"] 47 | if value < 1: 48 | value *= 1000 49 | return "{:.0f}ms".format(value) 50 | else: 51 | return "{:.0f}s".format(value) 52 | 53 | 54 | def fmt_size(case, null_case): 55 | delta = (case["size"] - null_case["size"]) / 1024 56 | return "{:,.0f} KiB".format(delta) 57 | 58 | 59 | if __name__ == "__main__": 60 | main() 61 | -------------------------------------------------------------------------------- /runs/2021-07-21-epage-sc01.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-07-21", 3 | "hostname": "epage-sc01", 4 | "os": "Linux", 5 | "os_ver": "4.4.0-19041-Microsoft", 6 | "arch": "x86_64", 7 | "cpus": 8, 8 | "libs": { 9 | "examples/argh-app/Cargo.toml": { 10 | "name": "argh", 11 | "manifest_path": "examples/argh-app/Cargo.toml", 12 | "crate": "argh", 13 | "version": "v0.1.5", 14 | "deps": 8, 15 | "build": { 16 | "results": [ 17 | { 18 | "command": "cargo build -j 8 --package argh-app", 19 | "mean": 11.2265115045, 20 | "stddev": 0.29834693599057327, 21 | "median": 11.3087072645, 22 | "user": 16.665156250000003, 23 | "system": 6.13859375, 24 | "min": 10.7149778645, 25 | "max": 11.487517564500001, 26 | "times": [ 27 | 10.7149778645, 28 | 11.487517564500001, 29 | 11.259059564500001, 30 | 11.3087072645, 31 | 11.3622952645 32 | ] 33 | } 34 | ] 35 | }, 36 | "size": 3304136 37 | }, 38 | "examples/clap-app/Cargo.toml": { 39 | "name": "clap", 40 | "manifest_path": "examples/clap-app/Cargo.toml", 41 | "crate": "clap", 42 | "version": "v2.33.3", 43 | "deps": 8, 44 | "build": { 45 | "results": [ 46 | { 47 | "command": "cargo build -j 8 --package clap-app", 48 | "mean": 9.731063857999999, 49 | "stddev": 0.05327978921104148, 50 | "median": 9.719836198, 51 | "user": 16.431171874999997, 52 | "system": 5.33828125, 53 | "min": 9.668259698, 54 | "max": 9.808399498, 55 | "times": [ 56 | 9.719836198, 57 | 9.703933897999999, 58 | 9.808399498, 59 | 9.668259698, 60 | 9.754889998 61 | ] 62 | } 63 | ] 64 | }, 65 | "size": 3915648 66 | }, 67 | "examples/clap-minimal-app/Cargo.toml": { 68 | "name": "clap-minimal", 69 | "manifest_path": "examples/clap-minimal-app/Cargo.toml", 70 | "crate": "clap", 71 | "version": "v3.0.0-beta.2", 72 | "deps": 8, 73 | "build": { 74 | "results": [ 75 | { 76 | "command": "cargo build -j 8 --package clap-minimal-app", 77 | "mean": 9.8829004605, 78 | "stddev": 0.3458033472576671, 79 | "median": 9.7507360805, 80 | "user": 15.862109375, 81 | "system": 5.538671875, 82 | "min": 9.5529013805, 83 | "max": 10.433972880499999, 84 | "times": [ 85 | 10.433972880499999, 86 | 9.987307980499999, 87 | 9.6895839805, 88 | 9.5529013805, 89 | 9.7507360805 90 | ] 91 | } 92 | ] 93 | }, 94 | "size": 3813240 95 | }, 96 | "examples/clap3-app/Cargo.toml": { 97 | "name": "clap3", 98 | "manifest_path": "examples/clap3-app/Cargo.toml", 99 | "crate": "clap", 100 | "version": "v3.0.0-beta.2", 101 | "deps": 23, 102 | "build": { 103 | "results": [ 104 | { 105 | "command": "cargo build -j 8 --package clap3-app", 106 | "mean": 23.144059515, 107 | "stddev": 0.1820228210343413, 108 | "median": 23.179363215, 109 | "user": 53.67171874999999, 110 | "system": 19.16046875, 111 | "min": 22.878906815, 112 | "max": 23.383042515, 113 | "times": [ 114 | 23.183209415, 115 | 23.095775615, 116 | 23.383042515, 117 | 23.179363215, 118 | 22.878906815 119 | ] 120 | } 121 | ] 122 | }, 123 | "size": 3860880 124 | }, 125 | "examples/clap_derive-app/Cargo.toml": { 126 | "name": "clap_derive", 127 | "manifest_path": "examples/clap_derive-app/Cargo.toml", 128 | "crate": "clap", 129 | "version": "v3.0.0-beta.2", 130 | "deps": 23, 131 | "build": { 132 | "results": [ 133 | { 134 | "command": "cargo build -j 8 --package clap_derive-app", 135 | "mean": 23.336501766, 136 | "stddev": 0.3236057059997127, 137 | "median": 23.393399806, 138 | "user": 53.67140625, 139 | "system": 18.873046875, 140 | "min": 22.957550306, 141 | "max": 23.797438906, 142 | "times": [ 143 | 22.957550306, 144 | 23.797438906, 145 | 23.393399806, 146 | 23.425068806, 147 | 23.109051006 148 | ] 149 | } 150 | ] 151 | }, 152 | "size": 3861240 153 | }, 154 | "examples/gumdrop-app/Cargo.toml": { 155 | "name": "gumdrop", 156 | "manifest_path": "examples/gumdrop-app/Cargo.toml", 157 | "crate": "gumdrop", 158 | "version": "v0.8.0", 159 | "deps": 5, 160 | "build": { 161 | "results": [ 162 | { 163 | "command": "cargo build -j 8 --package gumdrop-app", 164 | "mean": 10.6197106835, 165 | "stddev": 0.21193332418072658, 166 | "median": 10.4862100235, 167 | "user": 13.846640625000001, 168 | "system": 4.659921875, 169 | "min": 10.4382173235, 170 | "max": 10.8514214235, 171 | "times": [ 172 | 10.4719549235, 173 | 10.4382173235, 174 | 10.8507497235, 175 | 10.8514214235, 176 | 10.4862100235 177 | ] 178 | } 179 | ] 180 | }, 181 | "size": 3298032 182 | }, 183 | "examples/lexopt-app/Cargo.toml": { 184 | "name": "lexopt", 185 | "manifest_path": "examples/lexopt-app/Cargo.toml", 186 | "crate": "lexopt", 187 | "version": "v0.1.0", 188 | "deps": 0, 189 | "build": { 190 | "results": [ 191 | { 192 | "command": "cargo build -j 8 --package lexopt-app", 193 | "mean": 1.9246871635000002, 194 | "stddev": 0.034962428869073164, 195 | "median": 1.9185320635, 196 | "user": 1.2120312500000001, 197 | "system": 0.8067968750000001, 198 | "min": 1.8926131635, 199 | "max": 1.9732349635000002, 200 | "times": [ 201 | 1.9732349635000002, 202 | 1.9185320635, 203 | 1.9461053635, 204 | 1.8926131635, 205 | 1.8929502635000002 206 | ] 207 | } 208 | ] 209 | }, 210 | "size": 3295768 211 | }, 212 | "examples/null-app/Cargo.toml": { 213 | "name": "null", 214 | "manifest_path": "examples/null-app/Cargo.toml", 215 | "crate": null, 216 | "version": null, 217 | "deps": 0, 218 | "build": { 219 | "results": [ 220 | { 221 | "command": "cargo build -j 8 --package null-app", 222 | "mean": 1.170488584, 223 | "stddev": 0.05698039564677481, 224 | "median": 1.168403144, 225 | "user": 0.6340625, 226 | "system": 0.50703125, 227 | "min": 1.093298644, 228 | "max": 1.240455444, 229 | "times": [ 230 | 1.240455444, 231 | 1.142849344, 232 | 1.093298644, 233 | 1.207436344, 234 | 1.168403144 235 | ] 236 | } 237 | ] 238 | }, 239 | "size": 3258728 240 | }, 241 | "examples/pico-args-app/Cargo.toml": { 242 | "name": "pico-args", 243 | "manifest_path": "examples/pico-args-app/Cargo.toml", 244 | "crate": "pico-args", 245 | "version": "v0.4.2", 246 | "deps": 0, 247 | "build": { 248 | "results": [ 249 | { 250 | "command": "cargo build -j 8 --package pico-args-app", 251 | "mean": 1.9831927820000002, 252 | "stddev": 0.0784537831814693, 253 | "median": 1.952630582, 254 | "user": 1.28375, 255 | "system": 0.8384375000000001, 256 | "min": 1.907222682, 257 | "max": 2.091755282, 258 | "times": [ 259 | 1.926830582, 260 | 2.091755282, 261 | 1.907222682, 262 | 2.037524782, 263 | 1.952630582 264 | ] 265 | } 266 | ] 267 | }, 268 | "size": 3288360 269 | }, 270 | "examples/structopt-app/Cargo.toml": { 271 | "name": "structopt", 272 | "manifest_path": "examples/structopt-app/Cargo.toml", 273 | "crate": "structopt", 274 | "version": "v0.3.22", 275 | "deps": 20, 276 | "build": { 277 | "results": [ 278 | { 279 | "command": "cargo build -j 8 --package structopt-app", 280 | "mean": 19.5963706155, 281 | "stddev": 0.22304503812149243, 282 | "median": 19.5125861555, 283 | "user": 54.17171874999999, 284 | "system": 17.050234375000002, 285 | "min": 19.3557464555, 286 | "max": 19.9315235555, 287 | "times": [ 288 | 19.4868559555, 289 | 19.9315235555, 290 | 19.5125861555, 291 | 19.6951409555, 292 | 19.3557464555 293 | ] 294 | } 295 | ] 296 | }, 297 | "size": 3916440 298 | }, 299 | "examples/xflags-app/Cargo.toml": { 300 | "name": "xflags", 301 | "manifest_path": "examples/xflags-app/Cargo.toml", 302 | "crate": "xflags", 303 | "version": "v0.2.3", 304 | "deps": 1, 305 | "build": { 306 | "results": [ 307 | { 308 | "command": "cargo build -j 8 --package xflags-app", 309 | "mean": 3.5467443304999997, 310 | "stddev": 0.2088168560198792, 311 | "median": 3.4922354104999997, 312 | "user": 2.893359375, 313 | "system": 1.5074999999999998, 314 | "min": 3.4152494105, 315 | "max": 3.9138285104999997, 316 | "times": [ 317 | 3.4174102105, 318 | 3.4152494105, 319 | 3.4949981104999996, 320 | 3.9138285104999997, 321 | 3.4922354104999997 322 | ] 323 | } 324 | ] 325 | }, 326 | "size": 3288864 327 | } 328 | } 329 | } --------------------------------------------------------------------------------