├── .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 |  | v0.1.10
31 | bpaf | 282 KiB | 965ms *(full)*
236ms *(incremental)* | 5ms | Y |  | v0.9.4
32 | bpaf_derive | 276 KiB | 4s *(full)*
238ms *(incremental)* | 5ms | Y |  | v0.9.4
33 | clap | 654 KiB | 3s *(full)*
392ms *(incremental)* | 4ms | Y |  | v4.4.0
34 | clap-minimal | 427 KiB | 2s *(full)*
330ms *(incremental)* | 4ms | Y |  | v4.4.0
35 | clap_derive | 689 KiB | 6s *(full)*
410ms *(incremental)* | 4ms | Y |  | v4.4.0
36 | clap_lex | 27 KiB | 407ms *(full)*
188ms *(incremental)* | 3ms | Y |  | v0.5.1
37 | gumdrop | 37 KiB | 3s *(full)*
198ms *(incremental)* | 3ms | N |  | v0.8.1
38 | lexopt | 34 KiB | 385ms *(full)*
184ms *(incremental)* | 3ms | Y |  | v0.3.0
39 | pico-args | 23 KiB | 384ms *(full)*
185ms *(incremental)* | 3ms | Y |  | v0.5.0
40 | xflags | 22 KiB | 709ms *(full)*
179ms *(incremental)* | 3ms | Y |  | 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 = "".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 | }
--------------------------------------------------------------------------------