├── .github └── workflows │ └── test.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── crates ├── palex │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ ├── README.md │ ├── examples │ │ └── parser.rs │ └── src │ │ ├── input.rs │ │ ├── lib.rs │ │ ├── part.rs │ │ ├── tests.rs │ │ └── token_kind.rs └── parkour_derive │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ ├── README.md │ └── src │ ├── attrs.rs │ ├── from_input │ ├── enums.rs │ ├── mod.rs │ └── structs.rs │ ├── from_input_value.rs │ ├── lib.rs │ ├── parse_attrs.rs │ └── utils.rs ├── rustfmt.toml ├── src ├── actions │ ├── bool.rs │ ├── mod.rs │ └── option.rs ├── error.rs ├── from_input.rs ├── help.rs ├── impls │ ├── array.rs │ ├── bool.rs │ ├── char.rs │ ├── list.rs │ ├── mod.rs │ ├── numbers.rs │ ├── string.rs │ ├── tuple.rs │ └── wrappers.rs ├── lib.rs ├── parse.rs └── util.rs └── tests └── it ├── bool_argument.rs ├── macros.rs ├── main.rs ├── optional_argument.rs └── single_argument.rs /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | rustfmt: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: beta 17 | override: true 18 | components: rustfmt 19 | - uses: actions/checkout@v2 20 | - name: Run `cargo fmt` 21 | uses: actions-rs/cargo@v1 22 | with: 23 | command: fmt 24 | args: --all -- --check 25 | 26 | clippy: 27 | needs: rustfmt 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions-rs/toolchain@v1 31 | with: 32 | toolchain: beta 33 | override: true 34 | components: clippy 35 | - name: Checkout 36 | uses: actions/checkout@v2 37 | - name: Run `cargo clippy` 38 | uses: actions-rs/clippy-check@v1 39 | with: 40 | token: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | build: 43 | needs: clippy 44 | runs-on: ${{ matrix.os }} 45 | strategy: 46 | matrix: 47 | os: [ubuntu-latest, macos-latest, windows-latest] 48 | steps: 49 | - uses: actions-rs/toolchain@v1 50 | with: 51 | toolchain: beta 52 | override: true 53 | - name: Checkout 54 | uses: actions/checkout@v2 55 | - name: Build and run tests 56 | run: | 57 | cargo test --verbose 58 | cargo test --verbose --test it --features=dyn_iter 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*"] 3 | 4 | [package] 5 | name = "parkour" 6 | version = "0.1.0" 7 | authors = ["Ludwig Stecher "] 8 | description = "A fast, extensible command-line arguments parser" 9 | edition = "2018" 10 | readme = "README.md" 11 | license = "MIT OR Apache-2.0" 12 | categories = ["command-line-interface"] 13 | documentation = "https://docs.rs/parkour" 14 | repository = "https://github.com/Aloso/parkour" 15 | 16 | [dependencies] 17 | palex = { version = "0.2.0", path = "crates/palex" } 18 | parkour_derive = { version = "0.2.0", path = "crates/parkour_derive", optional = true } 19 | 20 | [features] 21 | derive = ["parkour_derive"] 22 | dyn_iter = ["palex/dyn_iter"] 23 | default = ["derive"] 24 | 25 | [[test]] 26 | name = "it" 27 | path = "tests/it/main.rs" 28 | required-features = ["dyn_iter"] 29 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ludwig Stecher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parkour 2 | 3 | A fast, extensible, command-line arguments parser. 4 | 5 | [![Documentation](https://img.shields.io/badge/documentation-docs.rs-blue?style=flat-square)](https://docs.rs/parkour) 6 | [![Crates.io](https://img.shields.io/crates/v/parkour?style=flat-square)](https://crates.io/crates/parkour) 7 | [![Downloads](https://img.shields.io/crates/d/parkour?style=flat-square)](https://crates.io/crates/parkour) 8 | [![License-MIT]()](./LICENSE-MIT) 9 | [![License-Apache]()](./LICENSE-MIT) 10 | [![Checks](https://flat.badgen.net/github/checks/Aloso/parkour/main)](https://github.com/Aloso/parkour/actions) 11 | 12 | ## Introduction 📚 13 | 14 | The most popular argument parser, `clap`, allows you list all the possible arguments and their constraints, and then gives you a dynamic, stringly-typed object containing all the values. Usually these values are then manually extracted into structs and enums to access the values more conveniently and get the advantages of a static type system ([example](https://github.com/rust-lang/cargo/blob/master/src/bin/cargo/cli.rs)). 15 | 16 | Parkour uses a different approach: Instead of parsing the arguments into an intermediate, stringly-typed object, it parses them directly into the types you want, so there's no cumbersome conversion. For types outside the standard library, you need to implement a trait, but in most cases this can be done with a simple derive macro. 17 | 18 | This has several advantages: 19 | 20 | * It is very flexible: Every aspect of argument parsing can be tailored to your needs. 21 | * It is strongly typed: Many errors can be caught at compile time, so you waste less time debugging. 22 | * It is zero-cost: If you don't need a feature, you don't have to use it. Parkour should also be pretty fast, but don't take my word for it, benchmark it 😉 23 | 24 | ## Status 25 | 26 | Parkour started as an experiment and is very new (about 1 week old at the time of writing). Expect frequent breaking changes. If you like what you see, consider supporting this work by 27 | 28 | * Reading the [docs](https://docs.rs/parkour) 29 | * Trying it out 30 | * Giving feedback in [this issue](https://github.com/Aloso/parkour/issues/1) 31 | * Opening issues or sending PRs 32 | 33 | Right now, parkour lacks some important features, which I intend to implement: 34 | 35 | * Auto-generated help messages 36 | * A DSL to write (sub)commands more ergonomically 37 | * More powerful derive macros 38 | * Error messages with ANSI colors 39 | 40 | ## Example 41 | 42 | ```rust 43 | use parkour::prelude::*; 44 | 45 | #[derive(FromInputValue)] 46 | enum ColorMode { 47 | Always, 48 | Auto, 49 | Never, 50 | } 51 | 52 | struct Command { 53 | color_mode: ColorMode, 54 | file: String, 55 | } 56 | 57 | impl FromInput<'static> for Command { 58 | type Context = (); 59 | 60 | fn from_input(input: &mut ArgsInput, _: &Self::Context) 61 | -> Result { 62 | // discard the first argument 63 | input.bump_argument().unwrap(); 64 | 65 | let mut file = None; 66 | let mut color_mode = None; 67 | 68 | while !input.is_empty() { 69 | if input.parse_long_flag("help") || input.parse_short_flag("h") { 70 | println!("Usage: run [-h,--help] [--color,-c auto|always|never] FILE"); 71 | return Err(parkour::Error::early_exit()); 72 | } 73 | if SetOnce(&mut color_mode) 74 | .apply(input, &Flag::LongShort("color", "c").into())? { 75 | continue; 76 | } 77 | if SetPositional(&mut file).apply(input, &"FILE")? { 78 | continue; 79 | } 80 | input.expect_empty()?; 81 | } 82 | 83 | Ok(Command { 84 | color_mode: color_mode.unwrap_or(ColorMode::Auto), 85 | file: file.ok_or_else(|| parkour::Error::missing_argument("FILE"))?, 86 | }) 87 | } 88 | } 89 | ``` 90 | 91 | In the future, I'd like to support a syntax like this: 92 | 93 | ```rust 94 | use parkour::prelude::*; 95 | 96 | #[derive(FromInput)] 97 | #[parkour(main, help)] 98 | #[arg(long = "version", short = "V", function = print_help)] 99 | struct Command { 100 | #[parkour(default = ColorMode::Auto)] 101 | #[arg(long = "color", short = "c")] 102 | color_mode: ColorMode, 103 | 104 | #[arg(positional)] 105 | subcommand: Option, 106 | } 107 | 108 | #[derive(FromInputValue)] 109 | enum ColorMode { 110 | Always, 111 | Auto, 112 | Never, 113 | } 114 | 115 | #[derive(FromInput)] 116 | enum Subcommand { 117 | Foo(Foo), 118 | Bar(Bar), 119 | } 120 | 121 | #[derive(FromInput)] 122 | #[parkour(subcommand)] 123 | struct Foo { 124 | #[parkour(default = 0, max = 3)] 125 | #[arg(long, short, action = Inc)] 126 | verbose: u8, 127 | 128 | #[arg(positional)] 129 | values: Vec, 130 | } 131 | 132 | #[derive(FromInput)] 133 | #[parkour(subcommand)] 134 | struct Bar { 135 | #[parkour(default = false)] 136 | #[arg(long, short)] 137 | dry_run: bool, 138 | } 139 | 140 | fn print_version(_: &mut ArgsInput) -> parkour::Result<()> { 141 | println!("command {}", env!("CARGO_PKG_VERSION")); 142 | Err(parkour::Error::early_exit()) 143 | } 144 | ``` 145 | 146 | ## Code of Conduct 🤝 147 | 148 | Please be friendly and respectful to others. This should be a place where everyone can feel safe, therefore I intend to enforce the [Rust code of conduct](https://www.rust-lang.org/policies/code-of-conduct). 149 | 150 | ## License 151 | 152 | This project is licensed under either of 153 | 154 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) 155 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 156 | 157 | at your option. 158 | -------------------------------------------------------------------------------- /crates/palex/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "palex" 3 | version = "0.2.0" 4 | authors = ["Ludwig Stecher "] 5 | edition = "2018" 6 | description = "A fast, small, dependency-free crate for lexing command-line arguments" 7 | readme = "README.md" 8 | documentation = "https://docs.rs/palex" 9 | license = "MIT OR Apache-2.0" 10 | categories = ["command-line-interface"] 11 | exclude = ["examples"] 12 | repository = "https://github.com/Aloso/parkour" 13 | 14 | [dependencies] 15 | 16 | [features] 17 | dyn_iter = [] 18 | -------------------------------------------------------------------------------- /crates/palex/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /crates/palex/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ludwig Stecher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/palex/README.md: -------------------------------------------------------------------------------- 1 | # palex 2 | 3 | A fast, small, dependency-free crate for lexing command-line arguments. You 4 | can use this crate if you want to build your own argument parsing library. 5 | 6 | This crate is almost zero-cost, since it parses arguments lazily and avoids 7 | most heap allocations. There's no dynamic dispatch. 8 | 9 | ## Usage 10 | 11 | Palex isn't published on crates.io yet, but it will be soon! 12 | 13 | ## License 14 | 15 | This project is licensed under either of 16 | 17 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) 18 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 19 | 20 | at your option. 21 | -------------------------------------------------------------------------------- /crates/palex/examples/parser.rs: -------------------------------------------------------------------------------- 1 | use palex::ArgsInput; 2 | 3 | #[derive(Debug)] 4 | struct Subcommand { 5 | r: u8, 6 | g: u8, 7 | b: u8, 8 | } 9 | 10 | pub fn main() -> Result<(), String> { 11 | let mut input = ArgsInput::from_args(); 12 | 13 | // Ignore the first argument 14 | input.bump_argument().unwrap(); 15 | 16 | // The values to be parsed 17 | let mut verbose = false; 18 | let mut output: Option = None; 19 | let mut subcommand: Option = None; 20 | 21 | // consume tokens until the input is empty 22 | while !input.is_empty() { 23 | // All methods that start with `eat` work the same: They check if the current 24 | // token matches the argument; if the check succeeds, the token is 25 | // consumed and returned. This is comparable to Iterator::next_if(). 26 | 27 | // When the -h/--help flag is encountered, the help is printed and the program 28 | // is exited 29 | if input.eat_one_dash("h").is_some() || input.eat_two_dashes("help").is_some() { 30 | print_help(); 31 | return Ok(()); 32 | } 33 | 34 | // When the -v/--verbose flag is encountered, `verbose` is set to true 35 | if input.eat_one_dash("v").is_some() || input.eat_two_dashes("verbose").is_some() 36 | { 37 | // This produces an error if the flag is encountered for a second time 38 | if verbose { 39 | return Err("The `--verbose` flag was given multiple times".into()); 40 | } 41 | verbose = true; 42 | continue; 43 | } 44 | 45 | // The -o/--out argument expects a string value. 46 | // This value can be given after a equals sign (e.g. `--out=foo`) or a space 47 | // (e.g. `--out foo`) 48 | if input.eat_one_dash("o").is_some() || input.eat_two_dashes("out").is_some() { 49 | // Here we parse the value, which might start with a dash 50 | if let Some(path) = input.value_allows_leading_dashes() { 51 | // Assign the string to the `output` variable. 52 | 53 | // We convert the string slice to an owned [String] to prevent 54 | // lifetime issues, since the slice borrows the input, but we still 55 | // want to be able to borrow it mutable after this! 56 | output = Some(path.eat().to_string()); 57 | continue; 58 | } else { 59 | return Err("`--out` expects 1 argument, none found".into()); 60 | } 61 | } 62 | 63 | // A subcommand. 64 | if input.eat_no_dash("subcommand").is_some() { 65 | subcommand = parse_subcommand(&mut input)?; 66 | if subcommand.is_none() { 67 | return Ok(()); 68 | } 69 | continue; 70 | } 71 | 72 | // Every branch above ends with a `return` or `continue` statement. 73 | // Therefore, if we reach this point, none of the above arguments 74 | // could be parsed. 75 | if let Some(arg) = input.value_allows_leading_dashes() { 76 | return Err(format!("Unexpected {:?} argument", arg.eat())); 77 | } 78 | } 79 | 80 | if output.is_none() { 81 | return Err("missing `--out` argument".into()); 82 | } 83 | 84 | dbg!(output); 85 | dbg!(subcommand); 86 | 87 | Ok(()) 88 | } 89 | 90 | fn parse_subcommand(input: &mut ArgsInput) -> Result, String> { 91 | let mut subcommand: Option = None; 92 | 93 | while !input.is_empty() { 94 | // A help flag that only applies to the subcommand 95 | if input.eat_one_dash("h").is_some() || input.eat_two_dashes("help").is_some() { 96 | print_help_for_subcommand(); 97 | return Ok(None); 98 | } 99 | 100 | if let Some(sub) = parse_rgb(input)? { 101 | subcommand = Some(sub); 102 | continue; 103 | } 104 | } 105 | // The `subcommand` variable is only set if the required `rgb` 106 | // option was provided 107 | if subcommand.is_none() { 108 | return Err("subcommand is missing the `--rgb` argument".into()); 109 | } 110 | Ok(subcommand) 111 | } 112 | 113 | fn parse_rgb(input: &mut ArgsInput) -> Result, String> { 114 | let mut subcommand: Option = None; 115 | 116 | // Required argument with 3 values. They can be comma-separated, e.g. 117 | // `--rgb=0,70,255`, or appear in the following arguments, e.g. 118 | // `--rgb 0 70 255` 119 | if input.eat_two_dashes("rgb").is_some() { 120 | if input.can_parse_value_no_whitespace() { 121 | let mut rgb = input.value().ok_or("No RGB value")?.eat().split(','); 122 | 123 | let r = rgb.next().ok_or("No red part")?; 124 | let r: u8 = r.parse().map_err(|_| "Invalid red number")?; 125 | 126 | let g = rgb.next().ok_or("No green part")?; 127 | let g: u8 = g.parse().map_err(|_| "Invalid green number")?; 128 | 129 | let b = rgb.next().ok_or("No blue part")?; 130 | let b: u8 = b.parse().map_err(|_| "Invalid blue number")?; 131 | 132 | subcommand = Some(Subcommand { r, g, b }); 133 | } else { 134 | let r = input.value().ok_or("No red part")?.eat(); 135 | let r: u8 = r.parse().map_err(|_| "Invalid red number")?; 136 | 137 | let g = input.value().ok_or("No green part")?.eat(); 138 | let g: u8 = g.parse().map_err(|_| "Invalid green number")?; 139 | 140 | let b = input.value().ok_or("No blue part")?.eat(); 141 | let b: u8 = b.parse().map_err(|_| "Invalid blue number")?; 142 | 143 | subcommand = Some(Subcommand { r, g, b }); 144 | } 145 | } 146 | 147 | Ok(subcommand) 148 | } 149 | 150 | fn print_help() { 151 | println!("Help for the palex example"); 152 | } 153 | 154 | fn print_help_for_subcommand() { 155 | println!("Help for the subcommand"); 156 | } 157 | -------------------------------------------------------------------------------- /crates/palex/src/input.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(any(test, feature = "dyn_iter")))] 2 | use std::env::Args; 3 | 4 | use crate::part::{InputPart, InputPartLd}; 5 | use crate::TokenKind; 6 | 7 | /// The default input type for argument parsing. This is generic over its 8 | /// iterator type and can be used with [`std::env::args`]. See 9 | /// [`ArgsInput::new()`] for more information. 10 | /// 11 | /// Getting the current token and token kind is very cheap. Bumping the token is 12 | /// a bit more expensive, since it involves more complicated logic and might 13 | /// re-allocate. 14 | pub struct ArgsInput { 15 | current: Option<(usize, usize, TokenKind)>, 16 | 17 | #[cfg(any(test, feature = "dyn_iter"))] 18 | iter: Box>, 19 | #[cfg(not(any(test, feature = "dyn_iter")))] 20 | iter: Args, 21 | 22 | buf: String, 23 | ignore_dashes: bool, 24 | } 25 | 26 | #[cfg(any(test, feature = "dyn_iter"))] 27 | impl ArgsInput { 28 | /// Creates a new instance of this input. 29 | /// 30 | /// ### Example: 31 | /// 32 | /// ``` 33 | /// # use palex::ArgsInput; 34 | /// let mut _input = ArgsInput::new(std::env::args()); 35 | /// ``` 36 | /// 37 | /// You probably want to discard the first argument in this case, which is 38 | /// just the path to the executable. 39 | pub fn new + 'static>(iter: I) -> Self { 40 | let mut iter = Box::new(iter); 41 | match iter.next() { 42 | Some(buf) => Self { 43 | current: Some(Self::trim_leading_dashes(false, &buf, 0)), 44 | iter, 45 | buf, 46 | ignore_dashes: false, 47 | }, 48 | None => { 49 | Self { current: None, iter, buf: String::new(), ignore_dashes: false } 50 | } 51 | } 52 | } 53 | } 54 | 55 | #[cfg(any(test, feature = "dyn_iter"))] 56 | impl From<&'static str> for ArgsInput { 57 | fn from(s: &'static str) -> Self { 58 | ArgsInput::new(s.split(' ').map(ToString::to_string)) 59 | } 60 | } 61 | 62 | impl ArgsInput { 63 | /// Creates a new instance from the command-line arguments 64 | /// 65 | /// ### Example: 66 | /// 67 | /// ``` 68 | /// # use palex::ArgsInput; 69 | /// let mut _input = ArgsInput::from_args(); 70 | /// ``` 71 | /// 72 | /// You probably want to discard the first argument in this case, which is 73 | /// just the path to the executable. 74 | pub fn from_args() -> Self { 75 | #[cfg(any(test, feature = "dyn_iter"))] 76 | let mut iter = Box::new(std::env::args()); 77 | #[cfg(not(any(test, feature = "dyn_iter")))] 78 | let mut iter = std::env::args(); 79 | 80 | match iter.next() { 81 | Some(buf) => Self { 82 | current: Some(Self::trim_leading_dashes(false, &buf, 0)), 83 | iter, 84 | buf, 85 | ignore_dashes: false, 86 | }, 87 | None => { 88 | Self { current: None, iter, buf: String::new(), ignore_dashes: false } 89 | } 90 | } 91 | } 92 | 93 | fn trim_leading_dashes( 94 | ignore: bool, 95 | string: &str, 96 | current: usize, 97 | ) -> (usize, usize, TokenKind) { 98 | if ignore { 99 | (current, current, TokenKind::NoDash) 100 | } else if string.starts_with("--") { 101 | (current + 2, current, TokenKind::TwoDashes) 102 | } else if string.starts_with('-') { 103 | (current + 1, current, TokenKind::OneDash) 104 | } else { 105 | (current, current, TokenKind::NoDash) 106 | } 107 | } 108 | 109 | fn trim_equals(&self, current: usize, kind: TokenKind) -> (usize, usize, TokenKind) { 110 | match kind { 111 | TokenKind::NoDash => {} 112 | TokenKind::OneDash => { 113 | if self.buf[current..].starts_with('=') { 114 | return (current + 1, current + 1, TokenKind::AfterEquals); 115 | } else { 116 | return (current, current, TokenKind::AfterOneDash); 117 | } 118 | } 119 | TokenKind::TwoDashes => { 120 | if self.buf[current..].starts_with('=') { 121 | return (current + 1, current + 1, TokenKind::AfterEquals); 122 | } 123 | } 124 | TokenKind::AfterOneDash => { 125 | if self.buf[current..].starts_with('=') { 126 | return (current + 1, current + 1, TokenKind::AfterEquals); 127 | } 128 | } 129 | TokenKind::AfterEquals => {} 130 | } 131 | (current, current, kind) 132 | } 133 | 134 | /// Returns the current token as string slice and the [`TokenKind`] of the 135 | /// current token, or [None] if the input is empty. 136 | /// 137 | /// This function skips the leading dashes of arguments. If you don't want 138 | /// that, use [`ArgsInput::current_str_with_leading_dashes()`] instead. 139 | pub(crate) fn current(&self) -> Option<(&str, TokenKind)> { 140 | self.current.map(|(i, _, kind)| (&self.buf[i..], kind)) 141 | } 142 | 143 | /// Returns the current token (including the leading dashes) as string 144 | /// slice, or [None] if the input is empty. 145 | pub(crate) fn current_str_with_leading_dashes(&self) -> Option<&str> { 146 | self.current.map(|(_, i, _)| &self.buf[i..]) 147 | } 148 | 149 | /// Bumps the current token by `len` bytes. 150 | /// 151 | /// Leading dashes are ignored, e.g. bumping the argument `--foo` by one 152 | /// byte returns `f`; the rest of the token is `oo`. If you don't want 153 | /// this, use [`ArgsInput::bump_with_leading_dashes()`] instead. 154 | /// 155 | /// If the bytes are followed by an equals sign and the current 156 | /// [`TokenKind`] is `OneDash`, `TwoDashes` or `AfterOneDash`, the 157 | /// equals sign is skipped. 158 | /// 159 | /// If afterwards the current argument is empty, a new argument is read and 160 | /// becomes the "current token" 161 | pub(crate) fn bump(&mut self, len: usize) -> &str { 162 | if let Some((current, _, kind)) = &mut self.current { 163 | let current_len = self.buf.len() - *current; 164 | if len > current_len { 165 | panic!("index bumped out of bounds: {} > {}", len, current_len); 166 | } 167 | 168 | let prev_current = *current; 169 | *current += len; 170 | 171 | if current_len == len { 172 | match self.iter.next() { 173 | Some(s) => { 174 | self.buf.push_str(&s); 175 | self.current = Some(Self::trim_leading_dashes( 176 | self.ignore_dashes, 177 | &s, 178 | *current, 179 | )); 180 | } 181 | None => self.current = None, 182 | } 183 | } else { 184 | let (current, kind) = (*current, *kind); 185 | self.current = Some(self.trim_equals(current, kind)); 186 | } 187 | 188 | &self.buf[prev_current..prev_current + len] 189 | } else { 190 | panic!("tried to bump index on empty input by {}", len) 191 | } 192 | } 193 | 194 | /// Bumps the current token (including leading dashes) by `len` bytes. 195 | /// 196 | /// If the bytes are followed by an equals sign and the current 197 | /// [`TokenKind`] is `OneDash`, `TwoDashes` or `AfterOneDash`, the 198 | /// equals sign is skipped. 199 | /// 200 | /// If afterwards the current argument is empty, a new argument is read and 201 | /// becomes the "current token" 202 | pub(crate) fn bump_with_leading_dashes(&mut self, len: usize) -> &str { 203 | if let Some((current, cwd, kind)) = &mut self.current { 204 | let current_len = self.buf.len() - *cwd; 205 | if len > current_len { 206 | panic!("index bumped out of bounds: {} > {}", len, current_len); 207 | } 208 | 209 | let prev_current = *cwd; 210 | *current += len; 211 | *cwd += len; 212 | 213 | if current_len == len { 214 | match self.iter.next() { 215 | Some(s) => { 216 | self.buf.push_str(&s); 217 | self.current = 218 | Some(Self::trim_leading_dashes(self.ignore_dashes, &s, *cwd)); 219 | } 220 | None => self.current = None, 221 | } 222 | } else { 223 | let (current, kind) = (*current, *kind); 224 | self.current = Some(self.trim_equals(current, kind)); 225 | } 226 | 227 | &self.buf[prev_current..prev_current + len] 228 | } else { 229 | panic!("tried to bump index on empty input by {}", len) 230 | } 231 | } 232 | 233 | /// Bumps the current argument (including leading dashes) completely. 234 | pub fn bump_argument(&mut self) -> Option<&str> { 235 | if let Some((i, _, _)) = self.current { 236 | let len = self.buf.len() - i; 237 | Some(self.bump(len)) 238 | } else { 239 | None 240 | } 241 | } 242 | 243 | /// Sets the parsing mode. When `true`, all arguments are considered 244 | /// positional, i.e. leading dashes are ignored. 245 | pub fn set_ignore_dashes(&mut self, ignore: bool) { 246 | self.ignore_dashes = ignore; 247 | if let Some((current, cwd, kind)) = &mut self.current { 248 | if ignore { 249 | *current = *cwd; 250 | *kind = TokenKind::NoDash; 251 | } else { 252 | self.current = 253 | Some(Self::trim_leading_dashes(ignore, &self.buf[*current..], *cwd)); 254 | } 255 | } 256 | } 257 | 258 | /// Returns the parsing mode. When `true`, all arguments are considered 259 | /// positional, i.e. leading dashes are ignored. 260 | pub fn ignore_dashes(&self) -> bool { 261 | self.ignore_dashes 262 | } 263 | 264 | /// Returns `true` if the input is empty. This means that all arguments have 265 | /// been fully parsed. 266 | pub fn is_empty(&self) -> bool { 267 | self.current().is_none() 268 | } 269 | 270 | /// Returns `true` if the input is not empty. This means that all arguments 271 | /// have been fully parsed. 272 | pub fn is_not_empty(&self) -> bool { 273 | self.current().is_some() 274 | } 275 | 276 | /// Returns `true` if a value within the same argument is expected. Or in 277 | /// other words, if we just consumed a single-dash flag or an equals sign 278 | /// and there are remaining bytes in the same argument. 279 | pub fn can_parse_value_no_whitespace(&self) -> bool { 280 | if let Some((_, current)) = self.current() { 281 | matches!(current, TokenKind::AfterOneDash | TokenKind::AfterEquals) 282 | } else { 283 | false 284 | } 285 | } 286 | 287 | /// Returns `true` if the current token can be parsed as a flag or named 288 | /// argument (e.g. `-h`, `--help=config`). 289 | pub fn can_parse_dash_argument(&self) -> bool { 290 | if let Some((_, current)) = self.current() { 291 | matches!( 292 | current, 293 | TokenKind::OneDash | TokenKind::TwoDashes | TokenKind::AfterOneDash 294 | ) 295 | } else { 296 | false 297 | } 298 | } 299 | 300 | /// Eat the current token if the argument doesn't start with dashes and 301 | /// matches `token` exactly. 302 | pub fn eat_no_dash<'a>(&mut self, token: &'a str) -> Option<&str> { 303 | if let Some((s, TokenKind::NoDash)) = self.current() { 304 | if token == s { 305 | return Some(self.bump(token.len())); 306 | } 307 | } 308 | None 309 | } 310 | 311 | /// Eat the current token if the argument starts with a single dash, and the 312 | /// current token starts with `token`. 313 | /// 314 | /// Does not work if the token appears after an equals sign has already been 315 | /// parsed. 316 | pub fn eat_one_dash<'a>(&mut self, token: &'a str) -> Option<&str> { 317 | if let Some((s, TokenKind::OneDash)) | Some((s, TokenKind::AfterOneDash)) = 318 | self.current() 319 | { 320 | if s.starts_with(token) { 321 | return Some(self.bump(token.len())); 322 | } 323 | } 324 | None 325 | } 326 | 327 | /// Eat the current token if the argument starts with (at least) two dashes, 328 | /// and the current token either matches `token` exactly, or starts with 329 | /// `token` followed by an equals sign. 330 | /// 331 | /// Does not work if the token appears after an equals sign has already been 332 | /// parsed. 333 | pub fn eat_two_dashes<'a>(&mut self, token: &'a str) -> Option<&str> { 334 | if let Some((s, TokenKind::TwoDashes)) = self.current() { 335 | if let Some(rest) = s.strip_prefix(token) { 336 | if rest.is_empty() || rest.starts_with('=') { 337 | return Some(self.bump(token.len())); 338 | } 339 | } 340 | } 341 | None 342 | } 343 | 344 | /// Eat the current token if it matches `token` exactly. 345 | /// 346 | /// This method only works if the current [`TokenKind`] is either `NoDash`, 347 | /// `AfterOneDash` or `AfterEquals`. 348 | pub fn eat_value<'a>(&mut self, token: &'a str) -> Option<&str> { 349 | if let Some((s, kind)) = self.current() { 350 | match kind { 351 | TokenKind::TwoDashes | TokenKind::OneDash => return None, 352 | 353 | | TokenKind::NoDash 354 | | TokenKind::AfterOneDash 355 | | TokenKind::AfterEquals => { 356 | if let Some(rest) = s.strip_prefix(token) { 357 | if rest.is_empty() { 358 | return Some(self.bump(token.len())); 359 | } 360 | } 361 | } 362 | } 363 | } 364 | None 365 | } 366 | 367 | /// Eat the current token (including any leading dashes) if it matches 368 | /// `token` exactly. 369 | pub fn eat_value_allows_leading_dashes<'a>( 370 | &mut self, 371 | token: &'a str, 372 | ) -> Option<&str> { 373 | if let Some(s) = self.current_str_with_leading_dashes() { 374 | if let Some(rest) = s.strip_prefix(token) { 375 | if rest.is_empty() { 376 | return Some(self.bump_with_leading_dashes(token.len())); 377 | } 378 | } 379 | } 380 | None 381 | } 382 | 383 | /// If the argument doesn't start with dashes, returns a helper struct for 384 | /// obtaining, validating and eating the next token. 385 | pub fn no_dash(&mut self) -> Option> 386 | where 387 | Self: Sized, 388 | { 389 | match self.current() { 390 | Some((s, TokenKind::NoDash)) => Some(InputPart::new(s.len(), self)), 391 | _ => None, 392 | } 393 | } 394 | 395 | /// If the argument starts with a single dash, returns a helper struct for 396 | /// obtaining, validating and eating the next token. 397 | pub fn one_dash(&mut self) -> Option> 398 | where 399 | Self: Sized, 400 | { 401 | match self.current() { 402 | Some((s, TokenKind::OneDash)) => Some(InputPart::new(s.len(), self)), 403 | _ => None, 404 | } 405 | } 406 | 407 | /// If the argument starts with two (or more) dashes, returns a helper 408 | /// struct for obtaining, validating and eating the next token. 409 | pub fn two_dashes(&mut self) -> Option> 410 | where 411 | Self: Sized, 412 | { 413 | match self.current() { 414 | Some((s, TokenKind::TwoDashes)) => Some(InputPart::new(s.len(), self)), 415 | _ => None, 416 | } 417 | } 418 | 419 | /// Returns a helper struct for obtaining, validating and eating the next 420 | /// token. Works only if the current [`TokenKind`] is either `NoDash`, 421 | /// `AfterOneDash` or `AfterEquals`. 422 | /// 423 | /// The value is not allowed to start with a dash, unless the dash is not at 424 | /// the start of the current argument. 425 | pub fn value(&mut self) -> Option> 426 | where 427 | Self: Sized, 428 | { 429 | match self.current() { 430 | | Some((s, TokenKind::NoDash)) 431 | | Some((s, TokenKind::AfterOneDash)) 432 | | Some((s, TokenKind::AfterEquals)) => Some(InputPart::new(s.len(), self)), 433 | _ => None, 434 | } 435 | } 436 | 437 | /// Returns a helper struct for obtaining, validating and eating the next 438 | /// token. The value is allowed to start with a dash. 439 | pub fn value_allows_leading_dashes(&mut self) -> Option> 440 | where 441 | Self: Sized, 442 | { 443 | match self.current_str_with_leading_dashes() { 444 | Some(s) => Some(InputPartLd::new(s.len(), self)), 445 | None => None, 446 | } 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /crates/palex/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | #![warn(missing_docs)] 3 | 4 | //! A fast, small, dependency-free crate for lexing command-line arguments. You 5 | //! can use this crate if you want to build your own argument parsing library. 6 | //! 7 | //! This crate is almost zero-cost, since it parses arguments lazily and avoids 8 | //! most heap allocations. There's no dynamic dispatch. 9 | //! 10 | //! Check the `examples` folder for examples. 11 | 12 | pub use input::ArgsInput; 13 | pub use token_kind::TokenKind; 14 | 15 | mod input; 16 | mod token_kind; 17 | 18 | #[cfg(test)] 19 | mod tests; 20 | 21 | pub mod part; 22 | -------------------------------------------------------------------------------- /crates/palex/src/part.rs: -------------------------------------------------------------------------------- 1 | //! Helper structs for checking if the next token matches your expectations and 2 | //! consuming the token thereupon. 3 | 4 | use crate::ArgsInput; 5 | 6 | /// A helper struct for checking if the next token matches your expectations and 7 | /// consuming the token thereupon. Instances of this type can be created with 8 | /// the following methods: 9 | /// 10 | /// - [`ArgsInput::no_dash`] 11 | /// - [`ArgsInput::one_dash`] 12 | /// - [`ArgsInput::two_dashes`] 13 | /// - [`ArgsInput::value`] 14 | pub struct InputPart<'a> { 15 | input: &'a mut ArgsInput, 16 | len: usize, 17 | } 18 | 19 | impl<'a> InputPart<'a> { 20 | pub(super) fn new(len: usize, input: &'a mut ArgsInput) -> Self { 21 | Self { input, len } 22 | } 23 | } 24 | 25 | impl<'a> InputPart<'a> { 26 | /// Returns the string slice of the token currently looked at 27 | pub fn as_str(&self) -> &str { 28 | &self.input.current().unwrap().0[..self.len] 29 | } 30 | 31 | /// Returns whether the token currently looked at is empty, i.e. has a 32 | /// length of 0 33 | pub fn is_empty(&self) -> bool { 34 | self.len == 0 35 | } 36 | 37 | /// Returns the length of the current token in bytes 38 | pub fn len(&self) -> usize { 39 | self.len 40 | } 41 | 42 | /// If the token is longer than `len` bytes, use only the first `len` bytes 43 | /// of this token. The rest of the string is considered part of the next 44 | /// token. 45 | pub fn take(self, len: usize) -> InputPart<'a> { 46 | InputPart { len, ..self } 47 | } 48 | 49 | /// Ignore everything but the first [char] of the token. The rest of the 50 | /// string is considered part of the next token. 51 | /// 52 | /// This returns `None` if the token is empty. 53 | pub fn take_char(self) -> Option> { 54 | let len = self.as_str().chars().next()?.len_utf8(); 55 | Some(InputPart { len, ..self }) 56 | } 57 | 58 | /// If the token contains `c`, use only the part of the token before the 59 | /// first occurrence of `c`. The rest of the string is considered part 60 | /// of the next token. 61 | pub fn take_until(self, c: char) -> InputPart<'a> { 62 | let len = self.as_str().find(c).unwrap_or(self.len); 63 | InputPart { len, ..self } 64 | } 65 | 66 | /// Consumes and returns the token as string slice. 67 | pub fn eat(self) -> &'a str { 68 | self.input.bump(self.len) 69 | } 70 | } 71 | 72 | impl<'a> AsRef for InputPart<'a> { 73 | fn as_ref(&self) -> &str { 74 | self.as_str() 75 | } 76 | } 77 | 78 | /// A helper struct for checking if the next token matches your expectations and 79 | /// consuming the token thereupon. Instances of this type can be created with 80 | /// the following method: 81 | /// 82 | /// - [`ArgsInput::value_allows_leading_dashes`] 83 | pub struct InputPartLd<'a> { 84 | input: &'a mut ArgsInput, 85 | len: usize, 86 | } 87 | 88 | impl<'a> InputPartLd<'a> { 89 | pub(super) fn new(len: usize, input: &'a mut ArgsInput) -> Self { 90 | Self { input, len } 91 | } 92 | } 93 | 94 | impl<'a> InputPartLd<'a> { 95 | /// Returns the string slice of the token currently looked at 96 | pub fn as_str(&self) -> &str { 97 | &self.input.current_str_with_leading_dashes().unwrap()[..self.len] 98 | } 99 | 100 | /// Returns whether the token currently looked at is empty, i.e. has a 101 | /// length of 0 102 | pub fn is_empty(&self) -> bool { 103 | self.len == 0 104 | } 105 | 106 | /// Returns the length of the current token in bytes 107 | pub fn len(&self) -> usize { 108 | self.len 109 | } 110 | 111 | /// If the token is longer than `len` bytes, use only the first `len` bytes 112 | /// of this token. The rest of the string is considered part of the next 113 | /// token. 114 | pub fn take(self, len: usize) -> InputPartLd<'a> { 115 | InputPartLd { len, ..self } 116 | } 117 | 118 | /// Ignore everything but the first [char] of the token. The rest of the 119 | /// string is considered part of the next token. 120 | /// 121 | /// This returns `None` if the token is empty. 122 | pub fn take_char(self) -> Option> { 123 | let len = self.as_str().chars().next()?.len_utf8(); 124 | Some(InputPartLd { len, ..self }) 125 | } 126 | 127 | /// If the token contains `c`, use only the part of the token before the 128 | /// first occurrence of `c`. The rest of the string is considered part 129 | /// of the next token. 130 | pub fn take_until(self, c: char) -> InputPartLd<'a> { 131 | let len = self.as_str().find(c).unwrap_or(self.len); 132 | InputPartLd { len, ..self } 133 | } 134 | 135 | /// Consumes and returns the token as string slice. 136 | pub fn eat(self) -> &'a str { 137 | self.input.bump_with_leading_dashes(self.len) 138 | } 139 | } 140 | 141 | impl<'a> AsRef for InputPartLd<'a> { 142 | fn as_ref(&self) -> &str { 143 | self.as_str() 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /crates/palex/src/tests.rs: -------------------------------------------------------------------------------- 1 | use std::vec::IntoIter; 2 | 3 | use crate::ArgsInput; 4 | 5 | fn input(s: &'static str) -> IntoIter { 6 | let v: Vec = s.split(' ').map(ToString::to_string).collect(); 7 | v.into_iter() 8 | } 9 | 10 | #[test] 11 | fn test_no_dash_1() { 12 | let mut input = ArgsInput::new(input("ab c def")); 13 | assert_eq!(input.eat_no_dash("ab"), Some("ab")); 14 | assert_eq!(input.eat_no_dash("cd"), None); 15 | assert_eq!(input.eat_no_dash("c"), Some("c")); 16 | assert_eq!(input.eat_no_dash("de"), None); 17 | assert_eq!(input.eat_no_dash("def"), Some("def")); 18 | assert_eq!(input.eat_no_dash(""), None); 19 | assert!(input.is_empty()); 20 | } 21 | 22 | #[test] 23 | fn test_no_dash_2() { 24 | let mut input = ArgsInput::new(input("ab c-d=e -fg")); 25 | assert_eq!(input.eat_no_dash("ab"), Some("ab")); 26 | assert_eq!(input.eat_no_dash("c-d=e"), Some("c-d=e")); 27 | assert_eq!(input.eat_no_dash("fg"), None); 28 | assert_eq!(input.eat_no_dash("-fg"), None); 29 | } 30 | 31 | #[test] 32 | fn test_no_dash_3() { 33 | let mut input = ArgsInput::new(input("ab --cd=e -fg")); 34 | input.bump(1); 35 | assert_eq!(input.eat_no_dash("b"), Some("b")); 36 | assert_eq!(input.eat_two_dashes("cd"), Some("cd")); 37 | assert_eq!(input.eat_no_dash("e"), None); 38 | assert_eq!(input.eat_value("e"), Some("e")); 39 | assert_eq!(input.eat_one_dash("f"), Some("f")); 40 | assert_eq!(input.eat_no_dash("g"), None); 41 | } 42 | 43 | #[test] 44 | fn test_one_dash_1() { 45 | let mut input = ArgsInput::new(input("-cde=f -gh= - --")); 46 | assert_eq!(input.eat_one_dash("c"), Some("c")); 47 | assert_eq!(input.eat_one_dash("de"), Some("de")); 48 | assert_eq!(input.eat_value("f"), Some("f")); 49 | assert_eq!(input.eat_one_dash("gh"), Some("gh")); 50 | assert_eq!(input.eat_one_dash(""), None); 51 | assert_eq!(input.eat_value(""), Some("")); 52 | assert_eq!(input.eat_one_dash(""), Some("")); 53 | assert_eq!(input.eat_one_dash("-"), None); 54 | assert_eq!(input.eat_two_dashes(""), Some("")); 55 | assert_eq!(input.eat_one_dash(""), None); 56 | } 57 | 58 | #[test] 59 | fn test_one_dash_2() { 60 | let mut input = ArgsInput::new(input("-a-b=c -d=e")); 61 | assert_eq!(input.eat_one_dash("a"), Some("a")); 62 | assert_eq!(input.eat_one_dash("-b"), Some("-b")); 63 | assert_eq!(input.eat_one_dash("="), None); 64 | assert_eq!(input.eat_value("c"), Some("c")); 65 | assert_eq!(input.eat_one_dash("d=e"), Some("d=e")); 66 | assert!(input.is_empty()); 67 | } 68 | 69 | #[test] 70 | fn test_one_dash_3() { 71 | let mut input = ArgsInput::new(input("--abc=-def -g=h i")); 72 | assert_eq!(input.eat_one_dash("-"), None); 73 | assert_eq!(input.eat_one_dash("a"), None); 74 | assert_eq!(input.eat_two_dashes("abc"), Some("abc")); 75 | assert_eq!(input.eat_one_dash("d"), None); 76 | assert_eq!(input.eat_one_dash("-def"), None); 77 | assert_eq!(input.eat_value("-def"), Some("-def")); 78 | assert_eq!(input.eat_one_dash("g"), Some("g")); 79 | assert_eq!(input.eat_one_dash("=h"), None); 80 | assert_eq!(input.eat_one_dash("h"), None); 81 | assert_eq!(input.eat_value("h"), Some("h")); 82 | assert_eq!(input.eat_one_dash("i"), None); 83 | } 84 | 85 | #[test] 86 | fn test_two_dashes_1() { 87 | let mut input = ArgsInput::new(input("-- --abc --d=e --f=g")); 88 | assert_eq!(input.eat_two_dashes(""), Some("")); 89 | assert_eq!(input.eat_two_dashes("ab"), None); 90 | assert_eq!(input.eat_two_dashes("abc"), Some("abc")); 91 | assert_eq!(input.eat_two_dashes("d=e"), Some("d=e")); 92 | assert_eq!(input.eat_two_dashes("f"), Some("f")); 93 | assert_eq!(input.eat_value("g"), Some("g")); 94 | assert_eq!(input.eat_two_dashes(""), None); 95 | assert!(input.is_empty()); 96 | } 97 | 98 | #[test] 99 | fn test_two_dashes_2() { 100 | let mut input = ArgsInput::new(input("--a=b c--d -e--f")); 101 | assert_eq!(input.eat_two_dashes("a"), Some("a")); 102 | assert_eq!(input.eat_two_dashes("b"), None); 103 | assert_eq!(input.eat_value("b"), Some("b")); 104 | input.bump(1); 105 | assert_eq!(input.eat_two_dashes("d"), None); 106 | assert_eq!(input.eat_value("--d"), Some("--d")); 107 | assert_eq!(input.eat_one_dash("e"), Some("e")); 108 | assert_eq!(input.eat_two_dashes("f"), None); 109 | } 110 | 111 | #[test] 112 | fn test_value() { 113 | let mut input = ArgsInput::new(input("ab -cde fg -hi --jk --l=-m -n=--o")); 114 | assert_eq!(input.eat_value("ab"), Some("ab")); 115 | assert_eq!(input.eat_one_dash("c"), Some("c")); 116 | assert_eq!(input.eat_value("de"), Some("de")); 117 | assert_eq!(input.eat_value("fg"), Some("fg")); 118 | assert_eq!(input.eat_value("-hi"), None); 119 | assert_eq!(input.eat_one_dash("hi"), Some("hi")); 120 | assert_eq!(input.eat_value("--jk"), None); 121 | assert_eq!(input.eat_two_dashes("jk"), Some("jk")); 122 | assert_eq!(input.eat_two_dashes("l"), Some("l")); 123 | assert_eq!(input.eat_value("-m"), Some("-m")); 124 | assert_eq!(input.eat_one_dash("n"), Some("n")); 125 | assert_eq!(input.eat_value("--o"), Some("--o")); 126 | assert!(input.is_empty()); 127 | } 128 | 129 | #[test] 130 | fn test_value_allows_leading_dashes() { 131 | let mut input = ArgsInput::new(input("ab -cde fg -hi --jk --l=-m -n=--o")); 132 | assert_eq!(input.eat_value_allows_leading_dashes("ab"), Some("ab")); 133 | assert_eq!(input.eat_value_allows_leading_dashes("-c"), None); 134 | assert_eq!(input.eat_value_allows_leading_dashes("-cde"), Some("-cde")); 135 | assert_eq!(input.eat_value_allows_leading_dashes("fg"), Some("fg")); 136 | assert_eq!(input.eat_value_allows_leading_dashes("-hi"), Some("-hi")); 137 | assert_eq!(input.eat_value_allows_leading_dashes("--jk"), Some("--jk")); 138 | assert_eq!(input.eat_two_dashes("l"), Some("l")); 139 | assert_eq!(input.eat_value_allows_leading_dashes("-m"), Some("-m")); 140 | assert_eq!(input.eat_one_dash("n"), Some("n")); 141 | assert_eq!(input.eat_value_allows_leading_dashes("--o"), Some("--o")); 142 | assert!(input.is_empty()); 143 | } 144 | 145 | #[test] 146 | fn test_modes() { 147 | { 148 | let mut input = ArgsInput::new(input("-a --b c")); 149 | assert_eq!(input.eat_no_dash("-a"), None); 150 | assert_eq!(input.eat_one_dash("a"), Some("a")); 151 | assert_eq!(input.eat_no_dash("--b"), None); 152 | assert_eq!(input.eat_two_dashes("b"), Some("b")); 153 | assert_eq!(input.eat_no_dash("c"), Some("c")); 154 | } 155 | { 156 | let mut input = ArgsInput::new(input("-a --b c")); 157 | input.set_ignore_dashes(true); 158 | assert_eq!(input.eat_one_dash("a"), None); 159 | assert_eq!(input.eat_no_dash("-a"), Some("-a")); 160 | assert_eq!(input.eat_two_dashes("b"), None); 161 | assert_eq!(input.eat_no_dash("--b"), Some("--b")); 162 | assert_eq!(input.eat_no_dash("c"), Some("c")); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /crates/palex/src/token_kind.rs: -------------------------------------------------------------------------------- 1 | /// The kind of the current token. 2 | /// 3 | /// This enum acts as the state of the currently parsed argument; this is 4 | /// necessary because an argument can consist of multiple tokens. 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 6 | pub enum TokenKind { 7 | /// An argument that doesn't start with dashes, e.g. `program`. 8 | NoDash, 9 | 10 | /// An argument that starts with exactly 1 dash, e.g. `-foo`, `-V`, 11 | /// `-h=config`. 12 | OneDash, 13 | 14 | /// An argument that starts with 2 or more dashes, e.g. `--version` or 15 | /// `--help=config`. 16 | TwoDashes, 17 | 18 | /// An option or value of a single-dash argument, after an option has been 19 | /// eaten. 20 | /// 21 | /// ### Example when parsing `-abcd=efg,hij` 22 | /// 23 | /// ```text 24 | /// abcd=efg # OneDash 25 | /// bcd=efg # AfterOneDash 26 | /// cd=efg # AfterOneDash 27 | /// efg # AfterEquals 28 | /// ``` 29 | AfterOneDash, 30 | 31 | /// A value of an argument after the `=`, after the name of the argument has 32 | /// been eaten. 33 | /// 34 | /// ### Example when parsing `--abcd=efg,hij` 35 | /// 36 | /// ```text 37 | /// abcd=efg,hij # TwoDashes 38 | /// efg,hij # AfterEquals 39 | /// hij # AfterEquals 40 | /// ``` 41 | AfterEquals, 42 | } 43 | -------------------------------------------------------------------------------- /crates/parkour_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "parkour_derive" 3 | version = "0.2.0" 4 | authors = ["Ludwig Stecher "] 5 | edition = "2018" 6 | description = "Derive macros for parkour" 7 | readme = "README.md" 8 | license = "MIT OR Apache-2.0" 9 | categories = ["command-line-interface"] 10 | repository = "https://github.com/Aloso/parkour" 11 | documentation = "https://docs.rs/parkour_derive" 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | quote = "1.0" 18 | proc-macro2 = "1.0" 19 | 20 | [dependencies.syn] 21 | version = "1.0" 22 | default-features = false 23 | features = ["derive", "parsing", "printing", "proc-macro", "full"] 24 | -------------------------------------------------------------------------------- /crates/parkour_derive/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /crates/parkour_derive/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ludwig Stecher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/parkour_derive/README.md: -------------------------------------------------------------------------------- 1 | # parkour_derive 2 | 3 | Derive macros for [parkour](https://github.com/Aloso/parkour). 4 | 5 | ## License 6 | 7 | This project is licensed under either of 8 | 9 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) 10 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 11 | 12 | at your option. 13 | -------------------------------------------------------------------------------- /crates/parkour_derive/src/attrs.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use syn::spanned::Spanned; 3 | use syn::{Attribute, Expr, ExprLit, Lit, Result}; 4 | 5 | use crate::parse_attrs; 6 | 7 | pub enum Attr { 8 | Parkour(Parkour), 9 | Arg(Arg), 10 | } 11 | 12 | pub enum Parkour { 13 | Main, 14 | Default(Option>), 15 | Subcommand(Option), 16 | } 17 | 18 | #[derive(PartialEq, Eq)] 19 | pub enum Arg { 20 | Named { long: Vec>, short: Vec> }, 21 | Positional { name: Option }, 22 | } 23 | 24 | pub fn parse(attrs: &[Attribute]) -> Result> { 25 | let mut result = Vec::new(); 26 | 27 | for a in attrs { 28 | if let Some(ident) = a.path.get_ident() { 29 | if *ident == "parkour" { 30 | parse_parkour_attrs(&a.tokens, &mut result)?; 31 | } else if *ident == "arg" { 32 | result.push((Attr::Arg(parse_arg_attrs(&a.tokens)?), ident.span())); 33 | } 34 | } 35 | } 36 | Ok(result) 37 | } 38 | 39 | fn parse_parkour_attrs(tokens: &TokenStream, buf: &mut Vec<(Attr, Span)>) -> Result<()> { 40 | let values = parse_attrs::parse(tokens)?; 41 | 42 | for (id, v) in values { 43 | match (id.to_string().as_str(), v) { 44 | ("main", None) => { 45 | buf.push((Attr::Parkour(Parkour::Main), id.span())); 46 | } 47 | ("subcommand", Some(t)) => { 48 | let s = parse_string(&t)?; 49 | buf.push((Attr::Parkour(Parkour::Subcommand(Some(s))), id.span())); 50 | } 51 | ("subcommand", None) => { 52 | buf.push((Attr::Parkour(Parkour::Subcommand(None)), id.span())); 53 | } 54 | ("default", Some(t)) => { 55 | buf.push((Attr::Parkour(Parkour::Default(Some(Box::new(t)))), id.span())); 56 | } 57 | ("default", None) => { 58 | buf.push((Attr::Parkour(Parkour::Default(None)), id.span())); 59 | } 60 | (s, _) => bail!(id.span(), "unexpected key {:?}", s), 61 | } 62 | } 63 | Ok(()) 64 | } 65 | 66 | fn parse_arg_attrs(tokens: &TokenStream) -> Result { 67 | let mut long = Vec::new(); 68 | let mut short = Vec::new(); 69 | let mut positional = None; 70 | 71 | let span = tokens.span(); 72 | let values = parse_attrs::parse(tokens)?; 73 | for (id, v) in values { 74 | match (id.to_string().as_str(), v) { 75 | ("long", None) => { 76 | long.push(None); 77 | } 78 | ("long", Some(t)) => { 79 | long.push(Some(parse_string(&t)?)); 80 | } 81 | ("short", None) => { 82 | short.push(None); 83 | } 84 | ("short", Some(t)) => { 85 | short.push(Some(parse_string(&t)?)); 86 | } 87 | ("positional", None) => { 88 | err_on_duplicate(positional.is_some(), id.span())?; 89 | positional = Some(None); 90 | } 91 | ("positional", Some(p)) => { 92 | err_on_duplicate(positional.is_some(), id.span())?; 93 | positional = Some(Some(parse_string(&p)?)); 94 | } 95 | (s, _) => bail!(id.span(), "unexpected key {:?}", s), 96 | } 97 | } 98 | 99 | if positional.is_some() && !(long.is_empty() && short.is_empty()) { 100 | bail!( 101 | span, 102 | "`arg(positional)` can't be used together with `arg(long)` or `arg(short)`", 103 | ); 104 | } 105 | if let Some(name) = positional { 106 | Ok(Arg::Positional { name }) 107 | } else { 108 | Ok(Arg::Named { long, short }) 109 | } 110 | } 111 | 112 | fn parse_string(t: &Expr) -> Result { 113 | match t { 114 | Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) => Ok(s.value()), 115 | _ => bail!(t.span(), "invalid token: expected string literal"), 116 | } 117 | } 118 | 119 | fn err_on_duplicate(b: bool, span: Span) -> Result<()> { 120 | if b { 121 | bail!(span, "key exists multiple times"); 122 | } else { 123 | Ok(()) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /crates/parkour_derive/src/from_input/enums.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::spanned::Spanned; 4 | use syn::{Attribute, DataEnum, Ident, Result, Variant}; 5 | 6 | use crate::attrs::{Attr, Parkour}; 7 | use crate::{attrs, utils}; 8 | 9 | pub fn enums(name: &Ident, e: DataEnum, attrs: Vec) -> Result { 10 | let variants: Vec = e.variants.into_iter().collect(); 11 | 12 | if let Some(v) = variants.iter().find(|&v| utils::field_len(&v.fields) > 1) { 13 | bail!( 14 | v.fields.span(), 15 | "The FromInput derive macro doesn't support variants with more than 1 field", 16 | ) 17 | } 18 | 19 | let empty_idents = utils::get_empty_variant_idents(&variants); 20 | let empty_ident_strs = utils::get_lowercase_ident_strs(&empty_idents); 21 | let (inner_types, inner_type_ctors) = utils::get_variant_types_and_ctors(&variants)?; 22 | 23 | let attrs = attrs::parse(&attrs)?; 24 | let is_main = attrs.iter().any(|(a, _)| matches!(a, Attr::Parkour(Parkour::Main))); 25 | 26 | let start_bump = if is_main { 27 | quote! { input.bump_argument().unwrap(); } 28 | } else { 29 | quote! {} 30 | }; 31 | 32 | let gen = quote! { 33 | #[automatically_derived] 34 | impl parkour::FromInput<'static> for #name { 35 | type Context = (); 36 | 37 | fn from_input(input: &mut parkour::ArgsInput, _: &Self::Context) 38 | -> parkour::Result 39 | { 40 | #start_bump 41 | 42 | if input.parse_long_flag("") { 43 | input.set_ignore_dashes(true); 44 | } 45 | 46 | #( 47 | if input.parse_command(#empty_ident_strs) { 48 | // TODO: Parse -h and --help by default 49 | input.expect_empty()?; 50 | return Ok(#name::#empty_idents {}); 51 | } 52 | )* 53 | 54 | #( 55 | match <#inner_types as parkour::FromInput>::from_input(input, &Default::default()) { 56 | Ok(__v) => return Ok( #name::#inner_type_ctors ), 57 | Err(e) if e.is_no_value() => {}, 58 | Err(e) => { 59 | return Err(e); 60 | }, 61 | } 62 | )* 63 | Err(parkour::Error::no_value()) 64 | } 65 | } 66 | }; 67 | Ok(gen) 68 | } 69 | -------------------------------------------------------------------------------- /crates/parkour_derive/src/from_input/mod.rs: -------------------------------------------------------------------------------- 1 | mod enums; 2 | mod structs; 3 | 4 | pub use enums::enums; 5 | pub use structs::structs; 6 | -------------------------------------------------------------------------------- /crates/parkour_derive/src/from_input/structs.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::quote; 3 | use syn::{ 4 | Attribute, Fields, GenericArgument, Ident, PathArguments, Result, Type, TypePath, 5 | }; 6 | 7 | use crate::attrs::{Arg, Attr, Parkour}; 8 | use crate::{attrs, utils}; 9 | 10 | pub fn structs( 11 | name: &Ident, 12 | s: syn::DataStruct, 13 | attr: Vec, 14 | ) -> Result { 15 | let attrs = attrs::parse(&attr)?; 16 | 17 | let subcommands = get_subcommand_names(&attrs, name)?; 18 | 19 | let is_main = attrs.iter().any(|(a, _)| matches!(a, Attr::Parkour(Parkour::Main))); 20 | if is_main && !subcommands.is_empty() { 21 | bail!( 22 | Span::call_site(), 23 | "`parkour(main)` and `parkour(subcommand)` can't be combined", 24 | ); 25 | } else if !is_main && subcommands.is_empty() { 26 | bail!( 27 | Span::call_site(), 28 | "The FromInput derive macro requires a `parkour(main)` or \ 29 | `parkour(subcommand)` attribute", 30 | ); 31 | } 32 | 33 | let main_condition = if is_main { 34 | quote! { input.bump_argument().is_some() } 35 | } else { 36 | quote! { #( input.parse_command(#subcommands) )||* } 37 | }; 38 | 39 | let field_len = utils::field_len(&s.fields); 40 | 41 | if field_len > 0 { 42 | if let Fields::Unnamed(_) = s.fields { 43 | bail!( 44 | Span::call_site(), 45 | "The FromInput derive macro doesn't support tuple structs", 46 | ); 47 | } 48 | } 49 | 50 | let mut field_idents = Vec::new(); 51 | let mut field_initials = Vec::new(); 52 | let mut field_getters = Vec::new(); 53 | let mut contexts = Vec::new(); 54 | 55 | for field in &s.fields { 56 | let attrs = attrs::parse(&field.attrs)?; 57 | let ident = field.ident.as_ref().expect("a field has no ident"); 58 | 59 | let ty = parse_my_type(&field.ty); 60 | 61 | let mut field_str = None; 62 | 63 | let mut args = Vec::new(); 64 | for (attr, span) in attrs { 65 | if let Attr::Arg(a) = attr { 66 | args.push(match a { 67 | Arg::Named { long, short } => { 68 | if long.is_empty() && short.is_empty() { 69 | bail!(span, "no flags specified"); 70 | } 71 | 72 | let main_flag = long 73 | .iter() 74 | .find_map(|f| f.as_deref().map(ToString::to_string)) 75 | .unwrap_or_else(|| utils::ident_to_flag_string(ident)); 76 | 77 | if field_str.is_none() { 78 | field_str = Some(format!("--{}", &main_flag)); 79 | } 80 | 81 | let (long, short) = 82 | flatten_flags(span, &main_flag, &long, &short)?; 83 | generate_flag_context(&long, &short) 84 | } 85 | 86 | Arg::Positional { name: None } => { 87 | if field_str.is_none() { 88 | field_str = Some(ident.to_string()); 89 | } 90 | 91 | quote! { todo!() } 92 | } 93 | Arg::Positional { name: Some(_p) } => { 94 | if field_str.is_none() { 95 | field_str = Some(ident.to_string()); 96 | } 97 | 98 | quote! { todo!() } 99 | } 100 | }) 101 | } else if let Attr::Parkour(_) = attr { 102 | bail!(span, "this key is not yet implemented!"); 103 | } 104 | } 105 | 106 | if args.is_empty() { 107 | bail!(ident.span(), "This field is missing a `arg` attribute"); 108 | } 109 | contexts.push(args); 110 | 111 | field_idents.push(ident); 112 | 113 | field_initials.push(match ty { 114 | MyType::Bool => quote! { false }, 115 | _ => quote! { None }, 116 | }); 117 | 118 | let field_str = field_str.expect("a field has no string"); 119 | field_getters.push(match ty { 120 | MyType::Bool | MyType::Option(_) => quote! {}, 121 | MyType::Other(_) => quote! { 122 | .ok_or_else(|| { 123 | parkour::Error::missing_argument(#field_str) 124 | })? 125 | }, 126 | }); 127 | } 128 | 129 | let gen = quote! { 130 | #[automatically_derived] 131 | impl parkour::FromInput<'static> for #name { 132 | type Context = (); 133 | 134 | fn from_input(input: &mut parkour::ArgsInput, _: &Self::Context) 135 | -> parkour::Result 136 | { 137 | if #main_condition { 138 | #( 139 | let mut #field_idents = #field_initials; 140 | )* 141 | while input.is_not_empty() { 142 | if input.parse_long_flag("") { 143 | input.set_ignore_dashes(true); 144 | } 145 | 146 | #( 147 | #( 148 | if parkour::actions::SetOnce(&mut #field_idents) 149 | .apply(input, &#contexts)? 150 | { 151 | input.expect_end_of_argument()?; 152 | continue; 153 | } 154 | )* 155 | )* 156 | 157 | input.expect_empty()?; 158 | } 159 | Ok(#name { 160 | #( 161 | #field_idents: #field_idents #field_getters, 162 | )* 163 | }) 164 | } else { 165 | Err(parkour::Error::no_value()) 166 | } 167 | } 168 | } 169 | }; 170 | Ok(gen) 171 | } 172 | 173 | enum MyType<'a> { 174 | Bool, 175 | Option(&'a Type), 176 | Other(&'a Type), 177 | } 178 | 179 | fn is_bool(path: &TypePath) -> bool { 180 | if path.qself.is_none() { 181 | if let Some(ident) = path.path.get_ident() { 182 | return ident == "bool"; 183 | } 184 | } 185 | false 186 | } 187 | 188 | fn parse_my_type(ty: &Type) -> MyType<'_> { 189 | if let Type::Path(path) = ty { 190 | if is_bool(&path) { 191 | return MyType::Bool; 192 | } else if path.qself.is_none() { 193 | let segments = path.path.segments.iter().collect::>(); 194 | 195 | let is_option = (segments.len() == 1 && segments[0].ident == "Option") 196 | || (segments.len() == 3 197 | && (segments[0].ident == "std" || segments[0].ident == "core") 198 | && segments[0].arguments.is_empty() 199 | && segments[1].ident == "option" 200 | && segments[1].arguments.is_empty() 201 | && segments[2].ident == "Option"); 202 | 203 | if is_option { 204 | if let PathArguments::AngleBracketed(a) = 205 | &segments[segments.len() - 1].arguments 206 | { 207 | if let Some(GenericArgument::Type(t)) = a.args.iter().next() { 208 | return MyType::Option(t); 209 | } 210 | } 211 | } 212 | } 213 | } 214 | MyType::Other(ty) 215 | } 216 | 217 | fn generate_flag_context(long: &[&str], short: &[&str]) -> TokenStream { 218 | match (long.len(), short.len()) { 219 | (1, 1) => { 220 | let long = long[0]; 221 | let short = short[0]; 222 | quote! { parkour::util::Flag::LongShort(#long, #short).into() } 223 | } 224 | (0, 1) => { 225 | let short = short[0]; 226 | quote! { parkour::util::Flag::Short(#short).into() } 227 | } 228 | (1, 0) => { 229 | let long = long[0]; 230 | quote! { parkour::util::Flag::Long(#long).into() } 231 | } 232 | (_, _) => quote! { 233 | parkour::util::Flag::Many(vec![ 234 | #( parkour::util::Flag::Long(#long), )* 235 | #( parkour::util::Flag::Short(#short), )* 236 | ]).into() 237 | }, 238 | } 239 | } 240 | 241 | fn flatten_flags<'a>( 242 | span: Span, 243 | main_flag: &'a str, 244 | long: &'a [Option], 245 | short: &'a [Option], 246 | ) -> Result<(Vec<&'a str>, Vec<&'a str>)> { 247 | let main_short = utils::first_char(span, main_flag)?; 248 | 249 | let mut long: Vec<&str> = 250 | long.iter().map(|o| o.as_deref().unwrap_or(main_flag)).collect(); 251 | let mut short: Vec<&str> = 252 | short.iter().map(|o| o.as_deref().unwrap_or(main_short)).collect(); 253 | 254 | long.sort_unstable(); 255 | short.sort_unstable(); 256 | 257 | if let Some(w) = long.windows(2).find(|pair| pair[0] == pair[1]) { 258 | bail!(span, "long flag {:?} is specified twice", w[0]); 259 | } 260 | if let Some(w) = short.windows(2).find(|pair| pair[0] == pair[1]) { 261 | bail!(span, "short flag {:?} is specified twice", w[0]); 262 | } 263 | 264 | Ok((long, short)) 265 | } 266 | 267 | fn get_subcommand_names(attrs: &[(Attr, Span)], name: &Ident) -> Result> { 268 | let mut subcommands: Vec = attrs 269 | .iter() 270 | .filter_map(|(a, _)| match a { 271 | Attr::Parkour(Parkour::Subcommand(s)) => { 272 | Some(s.clone().unwrap_or_else(|| name.to_string().to_lowercase())) 273 | } 274 | _ => None, 275 | }) 276 | .collect(); 277 | subcommands.sort_unstable(); 278 | 279 | if let Some(w) = subcommands.windows(2).find(|&pair| pair[0] == pair[1]) { 280 | bail!(Span::call_site(), "subcommand {:?} is specified twice", w[0]); 281 | } 282 | Ok(subcommands) 283 | } 284 | -------------------------------------------------------------------------------- /crates/parkour_derive/src/from_input_value.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::spanned::Spanned; 4 | use syn::{DataEnum, Ident, Result, Variant}; 5 | 6 | use crate::utils; 7 | 8 | pub fn enums(name: &Ident, e: DataEnum) -> Result { 9 | let variants: Vec = e.variants.into_iter().collect(); 10 | 11 | if let Some(v) = variants.iter().find(|&v| utils::field_len(&v.fields) > 1) { 12 | bail!( 13 | v.fields.span(), 14 | "The FromInput derive macro doesn't support variants with more than 1 field", 15 | ) 16 | } 17 | 18 | let empty_idents = utils::get_empty_variant_idents(&variants); 19 | let empty_ident_strs = utils::get_lowercase_ident_strs(&empty_idents); 20 | let (inner_types, inner_type_ctors) = utils::get_variant_types_and_ctors(&variants)?; 21 | 22 | let empty_ident_comparisons = empty_ident_strs.iter().map(|s| { 23 | if s.chars().all(|c| c.is_ascii()) { 24 | quote! { v if v.eq_ignore_ascii_case(#s) } 25 | } else { 26 | quote! { v if v.to_lowercase() == #s } 27 | } 28 | }); 29 | 30 | let from_input_value = quote! { 31 | fn from_input_value(value: &str, context: &Self::Context) -> parkour::Result { 32 | match value { 33 | #( 34 | #empty_ident_comparisons => Ok(#name::#empty_idents {}), 35 | )* 36 | v => { 37 | #[allow(unused_mut)] 38 | let mut source = None::; 39 | #( 40 | match <#inner_types as parkour::FromInputValue>::from_input_value( 41 | value, 42 | &Default::default() 43 | ) { 44 | Ok(__v) => return Ok( #name::#inner_type_ctors ), 45 | Err(e) if e.is_no_value() => {}, 46 | Err(e) => { 47 | source = Some(e); 48 | }, 49 | } 50 | )* 51 | match source { 52 | Some(s) => Err( 53 | parkour::Error::unexpected_value(v, Self::possible_values(context)) 54 | .with_source(s), 55 | ), 56 | None => Err(parkour::Error::unexpected_value(v, Self::possible_values(context))), 57 | } 58 | } 59 | } 60 | } 61 | }; 62 | 63 | let possible_values = quote! { 64 | #[allow(unused_mut)] 65 | fn possible_values(context: &Self::Context) -> Option { 66 | let mut values = vec![ 67 | #( 68 | parkour::help::PossibleValues::String(#empty_ident_strs.to_string()) 69 | ),* 70 | ]; 71 | #( 72 | if let Some(v) = <#inner_types as parkour::FromInputValue>::possible_values(context) { 73 | values.push(v); 74 | } 75 | ),* 76 | Some(parkour::help::PossibleValues::OneOf(values)) 77 | } 78 | }; 79 | 80 | let gen = quote! { 81 | #[automatically_derived] 82 | impl parkour::FromInputValue<'static> for #name { 83 | type Context = (); 84 | 85 | #from_input_value 86 | 87 | #possible_values 88 | } 89 | }; 90 | Ok(gen) 91 | } 92 | -------------------------------------------------------------------------------- /crates/parkour_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro::TokenStream; 4 | 5 | use syn::spanned::Spanned; 6 | use syn::{Data, DeriveInput}; 7 | 8 | #[macro_use] 9 | mod utils; 10 | mod attrs; 11 | mod parse_attrs; 12 | 13 | mod from_input; 14 | mod from_input_value; 15 | 16 | #[proc_macro_derive(FromInputValue)] 17 | pub fn from_input_value_derive(input: TokenStream) -> TokenStream { 18 | let ast = syn::parse_macro_input!(input as DeriveInput); 19 | let name = &ast.ident; 20 | let generics = &ast.generics; 21 | 22 | if generics.type_params().next().is_some() { 23 | bail_main!( 24 | generics.span(), 25 | "The FromInputValue derive macro currently doesn't support generics", 26 | ); 27 | } 28 | 29 | match ast.data { 30 | Data::Enum(e) => match from_input_value::enums(name, e) { 31 | Ok(stream) => stream.into(), 32 | Err(err) => err.into_compile_error().into(), 33 | }, 34 | Data::Struct(s) => bail_main!( 35 | s.struct_token.span(), 36 | "The FromInputValue derive macro only supports enums, not structs", 37 | ), 38 | Data::Union(u) => bail_main!( 39 | u.union_token.span(), 40 | "The FromInputValue derive macro only supports enums, not unions", 41 | ), 42 | } 43 | } 44 | 45 | #[proc_macro_derive(FromInput, attributes(parkour, arg))] 46 | pub fn from_input_derive(input: TokenStream) -> TokenStream { 47 | let ast = syn::parse_macro_input!(input as DeriveInput); 48 | let name = &ast.ident; 49 | let generics = &ast.generics; 50 | 51 | if generics.type_params().next().is_some() { 52 | bail_main!( 53 | generics.span(), 54 | "The FromInput derive macro currently doesn't support generics", 55 | ); 56 | } 57 | 58 | let result = match ast.data { 59 | Data::Enum(e) => from_input::enums(name, e, ast.attrs), 60 | Data::Struct(s) => from_input::structs(name, s, ast.attrs), 61 | Data::Union(u) => bail_main!( 62 | u.union_token.span(), 63 | "The FromInput derive macro only supports enums, not unions", 64 | ), 65 | }; 66 | match result { 67 | Ok(stream) => stream.into(), 68 | Err(e) => e.into_compile_error().into(), 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crates/parkour_derive/src/parse_attrs.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use syn::parse::{Parse, ParseStream}; 3 | use syn::punctuated::Punctuated; 4 | use syn::spanned::Spanned; 5 | use syn::{Expr, ExprPath, Ident, Result}; 6 | 7 | pub fn parse(tokens: &TokenStream) -> Result)>> { 8 | let AttrMap(values) = syn::parse2(tokens.clone())?; 9 | Ok(values) 10 | } 11 | 12 | struct AttrMap(Vec<(Ident, Option)>); 13 | 14 | impl Parse for AttrMap { 15 | fn parse(input: ParseStream) -> Result { 16 | let mut res = Vec::new(); 17 | 18 | if input.is_empty() { 19 | return Ok(AttrMap(res)); 20 | } 21 | 22 | let exprs = match input.parse::()? { 23 | Expr::Paren(p) => { 24 | if !p.attrs.is_empty() { 25 | bail!(p.span(), "Illegal attribute"); 26 | } 27 | let mut punct = Punctuated::new(); 28 | punct.push_value(*p.expr); 29 | punct 30 | } 31 | Expr::Tuple(t) => { 32 | if !t.attrs.is_empty() { 33 | bail!(t.span(), "Illegal attribute"); 34 | } 35 | t.elems 36 | } 37 | expr => bail!(expr.span(), "expected parentheses"), 38 | }; 39 | 40 | for expr in exprs { 41 | match expr { 42 | Expr::Assign(a) => { 43 | if !a.attrs.is_empty() { 44 | bail!(a.span(), "Illegal attribute"); 45 | } 46 | if let Expr::Path(left) = *a.left { 47 | res.push((parse_ident(left)?, Some(*a.right))); 48 | } else { 49 | bail!(a.span(), "invalid token: expected identifier"); 50 | } 51 | } 52 | Expr::Path(p) => { 53 | res.push((parse_ident(p)?, None)); 54 | } 55 | _ => bail!(expr.span(), "unsupported expression"), 56 | } 57 | } 58 | 59 | Ok(AttrMap(res)) 60 | } 61 | } 62 | 63 | fn parse_ident(p: ExprPath) -> Result { 64 | if p.qself.is_none() && p.attrs.is_empty() { 65 | if let Some(id) = p.path.get_ident() { 66 | return Ok(id.clone()); 67 | } 68 | } 69 | bail!(p.span(), "invalid token: expected identifier") 70 | } 71 | -------------------------------------------------------------------------------- /crates/parkour_derive/src/utils.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::{quote, ToTokens}; 3 | use syn::spanned::Spanned; 4 | use syn::{Field, Fields, Ident, Result, Type, Variant}; 5 | 6 | macro_rules! bail_main { 7 | ($span:expr, $s:literal $(,)?) => {{ 8 | return syn::Error::new($span, $s).into_compile_error().into(); 9 | }}; 10 | ($span:expr, $($rest:tt)*) => {{ 11 | return syn::Error::new($span, format!($($rest)*)) 12 | .into_compile_error() 13 | .into(); 14 | }}; 15 | } 16 | 17 | macro_rules! bail { 18 | ($span:expr, $s:literal $(,)?) => {{ 19 | return Err(syn::Error::new($span, $s)); 20 | }}; 21 | ($span:expr, $($rest:tt)*) => {{ 22 | return Err(syn::Error::new($span, format!($($rest)*))); 23 | }}; 24 | } 25 | 26 | pub fn field_len(fields: &Fields) -> usize { 27 | match fields { 28 | Fields::Named(n) => n.named.len(), 29 | Fields::Unnamed(n) => n.unnamed.len(), 30 | Fields::Unit => 0, 31 | } 32 | } 33 | 34 | pub fn first_char(span: Span, s: &str) -> Result<&str> { 35 | match s.char_indices().nth(1) { 36 | Some((i, _)) => Ok(&s[0..i]), 37 | None if !s.is_empty() => Ok(s), 38 | None => bail!(span, "flag is empty"), 39 | } 40 | } 41 | 42 | pub fn ident_to_flag_string(ident: &Ident) -> String { 43 | ident.to_string().trim_matches('_').replace('_', "-") 44 | } 45 | 46 | pub fn get_empty_variant_idents(variants: &[Variant]) -> Vec<&Ident> { 47 | variants.iter().filter(|&v| field_len(&v.fields) == 0).map(|v| &v.ident).collect() 48 | } 49 | 50 | pub fn get_lowercase_ident_strs(idents: &[&Ident]) -> Vec { 51 | idents 52 | .iter() 53 | .map(|&i| { 54 | let mut s = format!("{}", i); 55 | s.make_ascii_lowercase(); 56 | s 57 | }) 58 | .collect() 59 | } 60 | 61 | pub fn get_field(variant: &Variant) -> Option<&Field> { 62 | match &variant.fields { 63 | Fields::Named(f) => f.named.first(), 64 | Fields::Unnamed(f) => f.unnamed.first(), 65 | Fields::Unit => None, 66 | } 67 | } 68 | 69 | pub fn get_variant_types_and_ctors( 70 | variants: &[Variant], 71 | ) -> Result<(Vec<&Type>, Vec)> { 72 | let mut inner_types = Vec::new(); 73 | let mut inner_types_string = Vec::new(); 74 | let mut inner_type_ctors = Vec::new(); 75 | 76 | for v in variants { 77 | if let Some(field) = get_field(v) { 78 | let var_name = &v.ident; 79 | let field_ty_string = field.ty.to_token_stream().to_string(); 80 | if inner_types_string.contains(&field_ty_string) { 81 | bail!( 82 | field.span(), 83 | "The FromInputValue derive macro doesn't support multiple variants \ 84 | with the same type", 85 | ); 86 | } 87 | inner_types_string.push(field_ty_string); 88 | inner_types.push(&field.ty); 89 | 90 | if let Some(ident) = &field.ident { 91 | inner_type_ctors.push(quote! { #var_name { #ident: __v } }); 92 | } else { 93 | inner_type_ctors.push(quote! { #var_name(__v) }); 94 | } 95 | } 96 | } 97 | 98 | Ok((inner_types, inner_type_ctors)) 99 | } 100 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 90 2 | use_small_heuristics = "Max" 3 | wrap_comments = true 4 | match_arm_leading_pipes = "Preserve" 5 | imports_granularity = "Module" 6 | format_strings = true 7 | -------------------------------------------------------------------------------- /src/actions/bool.rs: -------------------------------------------------------------------------------- 1 | use palex::ArgsInput; 2 | 3 | use crate::util::Flag; 4 | use crate::ErrorInner; 5 | 6 | use super::{Action, ApplyResult, Reset, Set, SetOnce, Unset}; 7 | 8 | impl<'a> Action> for Set<'_, bool> { 9 | fn apply(self, input: &mut ArgsInput, context: &Flag<'a>) -> ApplyResult { 10 | if Flag::from_input(input, context)? { 11 | *self.0 = true; 12 | Ok(true) 13 | } else { 14 | Ok(false) 15 | } 16 | } 17 | } 18 | 19 | impl<'a> Action> for Reset<'_, bool> { 20 | fn apply(self, input: &mut ArgsInput, context: &Flag<'a>) -> ApplyResult { 21 | if Flag::from_input(input, context)? { 22 | *self.0 = false; 23 | Ok(true) 24 | } else { 25 | Ok(false) 26 | } 27 | } 28 | } 29 | 30 | impl<'a> Action> for SetOnce<'_, bool> { 31 | fn apply(self, input: &mut ArgsInput, context: &Flag<'a>) -> ApplyResult { 32 | if Flag::from_input(input, context)? { 33 | if *self.0 { 34 | return Err(ErrorInner::TooManyArgOccurrences { 35 | arg: context.first_to_string(), 36 | max: Some(1), 37 | } 38 | .into()); 39 | } 40 | *self.0 = true; 41 | Ok(true) 42 | } else { 43 | Ok(false) 44 | } 45 | } 46 | } 47 | 48 | impl<'a> Action> for Unset<'_, bool> { 49 | fn apply(self, input: &mut ArgsInput, context: &Flag<'a>) -> ApplyResult { 50 | if Flag::from_input(input, context)? { 51 | if !*self.0 { 52 | return Err(ErrorInner::TooManyArgOccurrences { 53 | arg: context.first_to_string(), 54 | max: None, 55 | } 56 | .into()); 57 | } 58 | *self.0 = false; 59 | Ok(true) 60 | } else { 61 | Ok(false) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/actions/mod.rs: -------------------------------------------------------------------------------- 1 | //! Actions are used to store the parsed command-line arguments in local 2 | //! variables. Actions can make sure that arguments are specified at most once. 3 | //! 4 | //! The structs [SetOnce], [Set], [Unset], [Reset], [Inc], [Dec], [Append], 5 | //! [SetPositional] and [SetSubcommand] implement the [Action] trait. Each 6 | //! struct has a different strategy of updating the local variable, and is 7 | //! implemented for different types. For example, [Inc] and [Dec] are only 8 | //! implemented for integer types, whereas [Set] is implemented for all types. 9 | //! 10 | //! ## Usage 11 | //! 12 | //! Make sure the [Action] trait is in scope, e.g. 13 | //! ```rust 14 | //! use parkour::actions::Action; 15 | //! // or 16 | //! use parkour::actions::Action as _; 17 | //! // or 18 | //! use parkour::prelude::*; 19 | //! ``` 20 | 21 | use palex::ArgsInput; 22 | 23 | use crate::{Error, FromInput, FromInputValue, Parse}; 24 | 25 | mod bool; 26 | mod option; 27 | 28 | /// The result of [`Action::apply`] 29 | pub type ApplyResult = Result; 30 | 31 | /// The trait for _actions_. Actions are used to store the parsed command-line 32 | /// arguments in local variables. Actions can make sure that arguments are 33 | /// specified at most once. 34 | pub trait Action { 35 | /// Perform the action. 36 | fn apply(self, input: &mut ArgsInput, context: &C) -> ApplyResult; 37 | } 38 | 39 | /// Set the parsed value, ensuring that it is set at most once. When the action 40 | /// is performed and the value is not in its initial state (e.g. `None`), an 41 | /// error is returned. 42 | pub struct SetOnce<'a, T>(pub &'a mut T); 43 | 44 | /// Set the value to it's initial state, e.g. `None`. This returns an error if 45 | /// the value is still in its initial state. 46 | pub struct Unset<'a, T>(pub &'a mut T); 47 | 48 | /// Set the parsed value. When this action is performed multiple times, only the 49 | /// last value is preserved. 50 | pub struct Set<'a, T>(pub &'a mut T); 51 | 52 | /// Reset the value to it's initial state, e.g. `None`. If it is already in its 53 | /// initial state, nothing happens. 54 | pub struct Reset<'a, T>(pub &'a mut T); 55 | 56 | /// Increments the value. 57 | pub struct Inc<'a, T>(pub &'a mut T); 58 | 59 | /// Decrements the value. 60 | pub struct Dec<'a, T>(pub &'a mut T); 61 | 62 | /// Appends the parsed value(s) to the existing ones. 63 | pub struct Append<'a, T>(pub &'a mut T); 64 | 65 | /// Like [`Set`], but works for positional arguments. 66 | pub struct SetPositional<'a, T>(pub &'a mut T); 67 | 68 | /// Like [`Set`], but works for subcommands. 69 | pub struct SetSubcommand<'a, T>(pub &'a mut T); 70 | 71 | impl<'a, T: FromInputValue<'a>> Action for SetPositional<'_, T> { 72 | fn apply(self, input: &mut ArgsInput, context: &T::Context) -> ApplyResult { 73 | if let Some(s) = input.try_parse_value(context)? { 74 | *self.0 = s; 75 | Ok(true) 76 | } else { 77 | Ok(false) 78 | } 79 | } 80 | } 81 | 82 | impl<'a, T: FromInput<'a>> Action for SetSubcommand<'_, T> { 83 | fn apply(self, input: &mut ArgsInput, context: &T::Context) -> ApplyResult { 84 | if let Some(s) = input.try_parse(context)? { 85 | *self.0 = s; 86 | Ok(true) 87 | } else { 88 | Ok(false) 89 | } 90 | } 91 | } 92 | 93 | impl<'a, T: FromInput<'a>> Action for Set<'_, T> { 94 | fn apply(self, input: &mut ArgsInput, context: &T::Context) -> ApplyResult { 95 | if let Some(s) = T::try_from_input(input, context)? { 96 | *self.0 = s; 97 | Ok(true) 98 | } else { 99 | Ok(false) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/actions/option.rs: -------------------------------------------------------------------------------- 1 | use palex::ArgsInput; 2 | 3 | use crate::util::{ArgCtx, Flag, PosCtx}; 4 | use crate::{ErrorInner, FromInput, FromInputValue, Parse}; 5 | 6 | use super::{ 7 | Action, ApplyResult, Reset, Set, SetOnce, SetPositional, SetSubcommand, Unset, 8 | }; 9 | 10 | impl<'a, V: FromInputValue<'a>> Action> for Set<'_, Option> { 11 | fn apply( 12 | self, 13 | input: &mut ArgsInput, 14 | context: &ArgCtx<'a, V::Context>, 15 | ) -> ApplyResult { 16 | match input.try_parse(context).map_err(|e| { 17 | e.chain(ErrorInner::InArgument(context.flag.first_to_string())) 18 | })? { 19 | Some(s) => { 20 | *self.0 = Some(s); 21 | Ok(true) 22 | } 23 | None => Ok(false), 24 | } 25 | } 26 | } 27 | 28 | impl<'a, V: FromInputValue<'a>> Action> 29 | for SetOnce<'_, Option> 30 | { 31 | fn apply( 32 | self, 33 | input: &mut ArgsInput, 34 | context: &ArgCtx<'a, V::Context>, 35 | ) -> ApplyResult { 36 | match input.try_parse(context).map_err(|e| { 37 | e.chain(ErrorInner::InArgument(context.flag.first_to_string())) 38 | })? { 39 | Some(s) => { 40 | if self.0.is_some() { 41 | return Err(ErrorInner::TooManyArgOccurrences { 42 | arg: context.flag.first_to_string(), 43 | max: Some(1), 44 | } 45 | .into()); 46 | } 47 | *self.0 = Some(s); 48 | Ok(true) 49 | } 50 | None => Ok(false), 51 | } 52 | } 53 | } 54 | 55 | impl<'a, V: FromInputValue<'a>> Action> for Reset<'_, Option> { 56 | fn apply(self, input: &mut ArgsInput, context: &Flag<'a>) -> ApplyResult { 57 | if Flag::from_input(input, context)? { 58 | *self.0 = None; 59 | Ok(true) 60 | } else { 61 | Ok(false) 62 | } 63 | } 64 | } 65 | 66 | impl<'a, V: FromInputValue<'a>> Action> for Unset<'_, Option> { 67 | fn apply(self, input: &mut ArgsInput, context: &Flag<'a>) -> ApplyResult { 68 | if Flag::from_input(input, context)? { 69 | if self.0.is_none() { 70 | return Err(ErrorInner::TooManyArgOccurrences { 71 | arg: context.first_to_string(), 72 | max: None, 73 | } 74 | .into()); 75 | } 76 | *self.0 = None; 77 | Ok(true) 78 | } else { 79 | Ok(false) 80 | } 81 | } 82 | } 83 | 84 | impl<'a, T: FromInputValue<'a>> Action> 85 | for SetPositional<'_, Option> 86 | { 87 | fn apply( 88 | self, 89 | input: &mut ArgsInput, 90 | context: &PosCtx<'a, T::Context>, 91 | ) -> ApplyResult { 92 | if let Some(s) = input.try_parse_value(&context.inner)? { 93 | if self.0.is_some() { 94 | return Err(ErrorInner::TooManyArgOccurrences { 95 | arg: context.name.to_string(), 96 | max: None, 97 | } 98 | .into()); 99 | } 100 | *self.0 = Some(s); 101 | Ok(true) 102 | } else { 103 | Ok(false) 104 | } 105 | } 106 | } 107 | 108 | impl<'a, T: FromInput<'a>> Action for SetSubcommand<'_, Option> { 109 | fn apply(self, input: &mut ArgsInput, context: &T::Context) -> ApplyResult { 110 | if let Some(s) = input.try_parse(context)? { 111 | if self.0.is_some() { 112 | return Err(ErrorInner::TooManyArgOccurrences { 113 | arg: "subcommand".to_string(), 114 | max: None, 115 | } 116 | .into()); 117 | } 118 | *self.0 = Some(s); 119 | Ok(true) 120 | } else { 121 | Ok(false) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::num::{ParseFloatError, ParseIntError}; 3 | 4 | use crate::help::PossibleValues; 5 | use crate::util::Flag; 6 | 7 | /// The error type when parsing command-line arguments. You can create an 8 | /// `Error` by creating an `ErrorInner` and converting it with `.into()`. 9 | /// 10 | /// This error type supports an error source for attaching context to the error. 11 | #[derive(Debug)] 12 | pub struct Error { 13 | inner: ErrorInner, 14 | source: Option>, 15 | } 16 | 17 | impl Error { 18 | /// Attach context to the error. Note that this overwrites the current 19 | /// source, if there is one. 20 | /// 21 | /// ### Usage 22 | /// 23 | /// ``` 24 | /// use parkour::{Error, util::Flag}; 25 | /// 26 | /// Error::missing_value() 27 | /// .with_source(Error::in_subcommand("test")) 28 | /// # ; 29 | /// ``` 30 | /// 31 | /// This could produce the following output: 32 | /// ```text 33 | /// missing value 34 | /// source: in subcommand `test` 35 | /// ``` 36 | pub fn with_source( 37 | self, 38 | source: impl std::error::Error + Sync + Send + 'static, 39 | ) -> Self { 40 | Error { source: Some(Box::new(source)), ..self } 41 | } 42 | 43 | /// Attach context to the error. This function ensures that an already 44 | /// attached source isn't discarded, but appended to the the new source. The 45 | /// sources therefore form a singly linked list. 46 | /// 47 | /// ### Usage 48 | /// 49 | /// ``` 50 | /// use parkour::{Error, ErrorInner, util::Flag}; 51 | /// 52 | /// Error::missing_value() 53 | /// .chain(ErrorInner::IncompleteValue(1)) 54 | /// # ; 55 | /// ``` 56 | pub fn chain(mut self, source: ErrorInner) -> Self { 57 | let mut new = Self::from(source); 58 | new.source = self.source.take(); 59 | Error { source: Some(Box::new(new)), ..self } 60 | } 61 | 62 | /// Create a `NoValue` error 63 | pub fn no_value() -> Self { 64 | ErrorInner::NoValue.into() 65 | } 66 | 67 | /// Returns `true` if this is a `NoValue` error 68 | pub fn is_no_value(&self) -> bool { 69 | matches!(self.inner, ErrorInner::NoValue) 70 | } 71 | 72 | /// Create a `MissingValue` error 73 | pub fn missing_value() -> Self { 74 | ErrorInner::MissingValue.into() 75 | } 76 | 77 | /// Returns the [`ErrorInner`] of this error 78 | pub fn inner(&self) -> &ErrorInner { 79 | &self.inner 80 | } 81 | 82 | /// Create a `EarlyExit` error 83 | pub fn early_exit() -> Self { 84 | ErrorInner::EarlyExit.into() 85 | } 86 | 87 | /// Returns `true` if this is a `EarlyExit` error 88 | pub fn is_early_exit(&self) -> bool { 89 | matches!(self.inner, ErrorInner::EarlyExit) 90 | } 91 | 92 | /// Create a `UnexpectedValue` error 93 | pub fn unexpected_value( 94 | got: impl ToString, 95 | expected: Option, 96 | ) -> Self { 97 | ErrorInner::InvalidValue { got: got.to_string(), expected }.into() 98 | } 99 | 100 | /// Create a `MissingArgument` error 101 | pub fn missing_argument(arg: impl ToString) -> Self { 102 | ErrorInner::MissingArgument { arg: arg.to_string() }.into() 103 | } 104 | 105 | /// Create a `InArgument` error 106 | pub fn in_argument(flag: &Flag) -> Self { 107 | ErrorInner::InArgument(flag.first_to_string()).into() 108 | } 109 | 110 | /// Create a `InSubcommand` error 111 | pub fn in_subcommand(cmd: impl ToString) -> Self { 112 | ErrorInner::InSubcommand(cmd.to_string()).into() 113 | } 114 | 115 | /// Create a `TooManyArgOccurrences` error 116 | pub fn too_many_arg_occurrences(arg: impl ToString, max: Option) -> Self { 117 | ErrorInner::TooManyArgOccurrences { arg: arg.to_string(), max }.into() 118 | } 119 | } 120 | 121 | impl From for Error { 122 | fn from(inner: ErrorInner) -> Self { 123 | Error { inner, source: None } 124 | } 125 | } 126 | 127 | /// The error type when parsing command-line arguments 128 | #[derive(Debug, PartialEq)] 129 | pub enum ErrorInner { 130 | /// The argument you tried to parse wasn't present at the current position. 131 | /// Has a similar purpose as `Option::None` 132 | NoValue, 133 | 134 | /// The argument you tried to parse wasn't present at the current position, 135 | /// but was required 136 | MissingValue, 137 | 138 | /// The argument you tried to parse was only partly present 139 | IncompleteValue(usize), 140 | 141 | /// Used when an argument should abort argument parsing, like --help 142 | EarlyExit, 143 | 144 | /// Indicates that the error originated in the specified argument. This 145 | /// should be used as the source for another error 146 | InArgument(String), 147 | 148 | /// Indicates that the error originated in the specified subcommand. This 149 | /// should be used as the source for another error 150 | InSubcommand(String), 151 | 152 | /// The parsed value doesn't meet our expectations 153 | InvalidValue { 154 | /// The value we tried to parse 155 | got: String, 156 | /// The expectation that was violated. For example, this string can 157 | /// contain a list of accepted values. 158 | expected: Option, 159 | }, 160 | 161 | /// The parsed list contains more items than allowed 162 | TooManyValues { 163 | /// The maximum number of items 164 | max: usize, 165 | /// The number of items that was parsed 166 | count: usize, 167 | }, 168 | 169 | /// The parsed array has the wrong length 170 | WrongNumberOfValues { 171 | /// The length of the array 172 | expected: usize, 173 | /// The number of items that was parsed 174 | got: usize, 175 | }, 176 | 177 | /// A required argument was not provided 178 | MissingArgument { 179 | /// The name of the argument that is missing 180 | arg: String, 181 | }, 182 | 183 | /// An unknown argument was provided 184 | UnexpectedArgument { 185 | /// The (full) argument that wasn't expected 186 | arg: String, 187 | }, 188 | 189 | /// The argument has a value, but no value was expected 190 | UnexpectedValue { 191 | /// The value of the argument 192 | value: String, 193 | }, 194 | 195 | /// An argument was provided more often than allowed 196 | TooManyArgOccurrences { 197 | /// The name of the argument that was provided too many times 198 | arg: String, 199 | /// The maximum number of times the argument may be provided 200 | max: Option, 201 | }, 202 | 203 | /// Parsing an integer failed 204 | ParseIntError(ParseIntError), 205 | 206 | /// Parsing a floating-point number failed 207 | ParseFloatError(ParseFloatError), 208 | } 209 | 210 | impl From for Error { 211 | fn from(e: ParseIntError) -> Self { 212 | ErrorInner::ParseIntError(e).into() 213 | } 214 | } 215 | impl From for Error { 216 | fn from(e: ParseFloatError) -> Self { 217 | ErrorInner::ParseFloatError(e).into() 218 | } 219 | } 220 | 221 | impl std::error::Error for Error { 222 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 223 | match &self.source { 224 | Some(source) => Some(&**source as &(dyn std::error::Error + 'static)), 225 | None => None, 226 | } 227 | } 228 | } 229 | 230 | impl fmt::Display for Error { 231 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 232 | match &self.inner { 233 | ErrorInner::NoValue => write!(f, "no value"), 234 | ErrorInner::MissingValue => write!(f, "missing value"), 235 | ErrorInner::IncompleteValue(part) => { 236 | write!(f, "missing part {} of value", part) 237 | } 238 | ErrorInner::EarlyExit => write!(f, "early exit"), 239 | ErrorInner::InArgument(opt) => write!(f, "in `{}`", opt.escape_debug()), 240 | ErrorInner::InSubcommand(cmd) => { 241 | write!(f, "in subcommand {}", cmd.escape_debug()) 242 | } 243 | ErrorInner::InvalidValue { expected, got } => { 244 | if let Some(expected) = expected { 245 | write!( 246 | f, 247 | "unexpected value `{}`, expected {}", 248 | got.escape_debug(), 249 | expected, 250 | ) 251 | } else { 252 | write!(f, "unexpected value `{}`", got.escape_debug()) 253 | } 254 | } 255 | ErrorInner::UnexpectedArgument { arg } => { 256 | write!(f, "unexpected argument `{}`", arg.escape_debug()) 257 | } 258 | ErrorInner::UnexpectedValue { value } => { 259 | write!(f, "unexpected value `{}`", value.escape_debug()) 260 | } 261 | ErrorInner::TooManyValues { max, count } => { 262 | write!(f, "too many values, expected at most {}, got {}", max, count) 263 | } 264 | ErrorInner::WrongNumberOfValues { expected, got } => { 265 | write!(f, "wrong number of values, expected {}, got {}", expected, got) 266 | } 267 | ErrorInner::MissingArgument { arg } => { 268 | write!(f, "required {} was not provided", arg) 269 | } 270 | ErrorInner::TooManyArgOccurrences { arg, max } => { 271 | if let Some(max) = max { 272 | write!( 273 | f, 274 | "{} was used too often, it can be used at most {} times", 275 | arg, max 276 | ) 277 | } else { 278 | write!(f, "{} was used too often", arg) 279 | } 280 | } 281 | 282 | ErrorInner::ParseIntError(e) => write!(f, "{}", e), 283 | ErrorInner::ParseFloatError(e) => write!(f, "{}", e), 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/from_input.rs: -------------------------------------------------------------------------------- 1 | use palex::ArgsInput; 2 | 3 | use crate::help::PossibleValues; 4 | use crate::util::{ArgCtx, Flag}; 5 | use crate::{Error, ErrorInner, Parse}; 6 | 7 | /// Trait for extracting information from the command-line input. This is 8 | /// implemented for flags, positional and named arguments, subcommands, etc. 9 | /// 10 | /// ### Implementation 11 | /// 12 | /// * Return `Err(parkour::Error::no_value())` if the parsed value isn't present 13 | /// * The lifetime parameter is for the context. If the context type doesn't 14 | /// have a lifetime, use `'static`. 15 | /// * Make sure that you consume exactly as much text as you need. Most methods 16 | /// from [`Parse`] should take care of this automatically. Avoid using 17 | /// lower-level functions, such as [`parkour::ArgsInput::current`] or 18 | /// [`parkour::ArgsInput::bump`], which might not advance the input correctly. 19 | /// 20 | /// ### Example 21 | /// 22 | /// ``` 23 | /// # use parkour::prelude::*; 24 | /// use parkour::help::PossibleValues; 25 | /// 26 | /// // The struct we want to crate from a positional number argument 27 | /// struct Foo(usize); 28 | /// 29 | /// // Information that is available while parsing. When `even` is true, 30 | /// // we only accept even numbers. Otherwise we only accept odd numbers. 31 | /// struct FooCtx { 32 | /// even: bool, 33 | /// } 34 | /// 35 | /// impl FromInput<'static> for Foo { 36 | /// type Context = FooCtx; 37 | /// 38 | /// fn from_input(input: &mut ArgsInput, context: &FooCtx) -> parkour::Result { 39 | /// let num: usize = input.parse_value(&Default::default())?; 40 | /// 41 | /// if context.even && num % 2 != 0 { 42 | /// Err(parkour::Error::unexpected_value( 43 | /// num, 44 | /// Some(PossibleValues::Other("even number".into())), 45 | /// )) 46 | /// } else if !context.even && num % 2 == 0 { 47 | /// Err(parkour::Error::unexpected_value( 48 | /// num, 49 | /// Some(PossibleValues::Other("odd number".into())), 50 | /// )) 51 | /// } else { 52 | /// Ok(Foo(num)) 53 | /// } 54 | /// } 55 | /// } 56 | /// ``` 57 | pub trait FromInput<'a>: Sized { 58 | /// Information that is available while parsing 59 | type Context: 'a; 60 | 61 | /// Extract information from the command-line input. 62 | fn from_input(input: &mut ArgsInput, context: &Self::Context) -> Result; 63 | 64 | /// Extract information from the command-line input, but convert 65 | /// [`Error::no_value`] to [`Option::None`]. This is useful when you want to 66 | /// bubble up all errors except for [`Error::no_value`]: 67 | /// 68 | /// ```no_run 69 | /// # use parkour::prelude::*; 70 | /// # let input: &mut parkour::ArgsInput = todo!(); 71 | /// if let Some(value) = bool::try_from_input(input, &Flag::Short("b").into())? { 72 | /// // do something with value 73 | /// } 74 | /// # Ok::<(), parkour::Error>(()) 75 | /// ``` 76 | fn try_from_input( 77 | input: &mut ArgsInput, 78 | context: &Self::Context, 79 | ) -> Result, Error> { 80 | match Self::from_input(input, context) { 81 | Ok(value) => Ok(Some(value)), 82 | Err(e) if e.is_no_value() => Ok(None), 83 | Err(e) => Err(e), 84 | } 85 | } 86 | } 87 | 88 | /// Trait for parsing a _value_. A value can be 89 | /// - a positional argument 90 | /// - a string following a flag; e.g in `--foo bar` or `--foo=bar`, the `bar` 91 | /// part is a value. A flag can be followed by multiple values, e.g. `--foo 92 | /// bar baz` or `--foo=bar,baz` 93 | /// 94 | /// To parse values, they are first converted to a string slice. By default, an 95 | /// argument that starts with a dash can't be parsed as a value, unless you 96 | /// implement the `allow_leading_dashes` method. 97 | /// 98 | /// The lifetime parameter is for the context. If the context type doesn't have 99 | /// a lifetime, use `'static`. 100 | pub trait FromInputValue<'a>: Sized { 101 | /// Information that is available while parsing 102 | type Context: 'a; 103 | 104 | /// The function that parses the string. This function is usually not 105 | /// invoked directly. Instead you can use [`Parse::parse_value`] and 106 | /// [`Parse::try_parse_value`]: 107 | /// 108 | /// ```no_run 109 | /// # use parkour::prelude::*; 110 | /// let mut input = parkour::parser(); 111 | /// let n: i32 = input.parse_value(&NumberCtx { min: -1000, max: 1000 })?; 112 | /// # Ok::<(), parkour::Error>(()) 113 | /// ``` 114 | fn from_input_value(value: &str, context: &Self::Context) -> Result; 115 | 116 | /// This function specifies whether this argument may start with leading 117 | /// dashes. For example, this returns `true` for numbers that can be 118 | /// negative. The default is `false`. 119 | fn allow_leading_dashes(_: &Self::Context) -> bool { 120 | false 121 | } 122 | 123 | /// Returns a list or short description of all the accepted values 124 | fn possible_values(context: &Self::Context) -> Option; 125 | } 126 | 127 | impl<'a, T: FromInputValue<'a>> FromInput<'a> for T 128 | where 129 | T::Context: 'a, 130 | { 131 | type Context = ArgCtx<'a, T::Context>; 132 | 133 | fn from_input(input: &mut ArgsInput, context: &Self::Context) -> Result { 134 | if Flag::from_input(input, &context.flag)? { 135 | match input.parse_value(&context.inner) { 136 | Ok(value) => Ok(value), 137 | Err(e) if e.is_no_value() => Err(Error::missing_value() 138 | .chain(ErrorInner::InArgument(context.flag.first_to_string()))), 139 | Err(e) => Err(e), 140 | } 141 | } else { 142 | Err(Error::no_value()) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/help.rs: -------------------------------------------------------------------------------- 1 | //! This module provides functionality for automatically generated help 2 | //! messages. 3 | 4 | use std::fmt; 5 | use std::iter::FusedIterator; 6 | 7 | /// This struct defines the possible values of a type representing a _value_. 8 | /// See the [`crate::FromInputValue`] trait for more information. 9 | #[derive(Debug)] 10 | pub enum PossibleValues { 11 | /// A literal value. For example, use `String("1")` if the value `1` is 12 | /// accepted. 13 | String(String), 14 | 15 | /// A string describing the kind of accepted values. For example, use 16 | /// `Other("positive number")` if all positive numbers are accepted. 17 | Other(String), 18 | 19 | /// A list of possible values. For example: 20 | /// 21 | /// ``` 22 | /// # use parkour::help::PossibleValues; 23 | /// 24 | /// PossibleValues::OneOf(vec![ 25 | /// PossibleValues::String("yes".into()), 26 | /// PossibleValues::String("no".into()), 27 | /// PossibleValues::Other("number".into()), 28 | /// ]); 29 | /// ``` 30 | /// 31 | /// This variant allows nesting lists of possible values. When displaying 32 | /// them, they should be flattened automatically. 33 | OneOf(Vec), 34 | } 35 | 36 | /// This struct defines a possible value of a type representing a _value_. 37 | /// See the [`crate::FromInputValue`] trait for more information. 38 | /// 39 | /// This struct is used only as item when iterating over [`PossibleValues`]. 40 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 41 | pub enum PossibleValue<'a> { 42 | /// A literal value. For example, `String("1")` means the value `1` is 43 | /// accepted. 44 | String(&'a str), 45 | /// A string describing the kind of accepted values. For example, 46 | /// `Other("positive number")` means all positive numbers are accepted. 47 | Other(&'a str), 48 | } 49 | 50 | impl PartialEq for PossibleValues { 51 | fn eq(&self, other: &Self) -> bool { 52 | use PossibleValues::*; 53 | 54 | match (self, other) { 55 | (String(a), String(b)) | (Other(a), Other(b)) => a == b, 56 | (String(_), Other(_)) | (Other(_), String(_)) => false, 57 | (_, _) => self.iter().eq(other.iter()), 58 | } 59 | } 60 | } 61 | 62 | impl Eq for PossibleValues {} 63 | 64 | impl fmt::Display for PossibleValues { 65 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 66 | let mut iter = self.iter().peekable(); 67 | match iter.next() { 68 | Some(v) => { 69 | write!(f, "{}", v)?; 70 | while let Some(next) = iter.next() { 71 | if iter.peek().is_some() { 72 | f.write_str(", ")?; 73 | } else { 74 | f.write_str(" or ")?; 75 | } 76 | write!(f, "{}", next)?; 77 | } 78 | Ok(()) 79 | } 80 | None => f.write_str("nothing"), 81 | } 82 | } 83 | } 84 | 85 | impl fmt::Display for PossibleValue<'_> { 86 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 87 | match *self { 88 | PossibleValue::String(s) => write!(f, "`{}`", s.escape_debug()), 89 | PossibleValue::Other(o) => f.write_str(o), 90 | } 91 | } 92 | } 93 | 94 | impl PossibleValues { 95 | /// Returns an iterator over all the possible values. This iterator flattens 96 | /// [`PossibleValues::OneOf`]. 97 | pub fn iter(&self) -> PossibleValueIter<'_> { 98 | PossibleValueIter { values: Some(self), index: 0, then: None } 99 | } 100 | } 101 | 102 | /// Iterator over possible values that flattens [`PossibleValues::OneOf`]. 103 | pub struct PossibleValueIter<'a> { 104 | values: Option<&'a PossibleValues>, 105 | index: usize, 106 | then: Option>>, 107 | } 108 | 109 | impl<'a> Iterator for PossibleValueIter<'a> { 110 | type Item = PossibleValue<'a>; 111 | 112 | fn next(&mut self) -> Option { 113 | match self.values { 114 | Some(PossibleValues::String(s)) => { 115 | advance(self); 116 | Some(PossibleValue::String(s)) 117 | } 118 | Some(PossibleValues::Other(o)) => { 119 | advance(self); 120 | Some(PossibleValue::Other(o)) 121 | } 122 | 123 | Some(PossibleValues::OneOf(o)) => { 124 | let next = &o[self.index]; 125 | if self.index + 1 >= o.len() { 126 | advance(self); 127 | } else { 128 | self.index += 1; 129 | } 130 | 131 | let then = std::mem::replace( 132 | self, 133 | PossibleValueIter { values: Some(next), index: 0, then: None }, 134 | ); 135 | self.then = Some(Box::new(then)); 136 | self.next() 137 | } 138 | None => None, 139 | } 140 | } 141 | 142 | fn size_hint(&self) -> (usize, Option) { 143 | match self.values { 144 | Some(PossibleValues::String(_)) => (1, Some(1)), 145 | Some(PossibleValues::Other(_)) => (1, Some(1)), 146 | Some(PossibleValues::OneOf(v)) => (v.len(), None), 147 | None => (0, None), 148 | } 149 | } 150 | } 151 | 152 | impl FusedIterator for PossibleValueIter<'_> {} 153 | 154 | fn advance(iter: &mut PossibleValueIter) { 155 | if let Some(then) = iter.then.take() { 156 | *iter = *then; 157 | } else { 158 | iter.values = None; 159 | } 160 | } 161 | 162 | #[test] 163 | fn test_values_iterator() { 164 | use PossibleValues::*; 165 | 166 | let values = OneOf(vec![ 167 | OneOf(vec![String("A".into())]), 168 | OneOf(vec![OneOf(vec![String("B".into())])]), 169 | OneOf(vec![OneOf(vec![String("C".into()), String("D".into())])]), 170 | OneOf(vec![OneOf(vec![String("E".into()), OneOf(vec![String("F".into())])])]), 171 | String("G".into()), 172 | String("H".into()), 173 | OneOf(vec![OneOf(vec![OneOf(vec![String("I".into())])])]), 174 | OneOf(vec![OneOf(vec![OneOf(vec![String("J".into()), String("K".into())])])]), 175 | ]); 176 | let collected: Vec<_> = values.iter().collect(); 177 | assert_eq!( 178 | collected, 179 | vec![ 180 | PossibleValue::String("A"), 181 | PossibleValue::String("B"), 182 | PossibleValue::String("C"), 183 | PossibleValue::String("D"), 184 | PossibleValue::String("E"), 185 | PossibleValue::String("F"), 186 | PossibleValue::String("G"), 187 | PossibleValue::String("H"), 188 | PossibleValue::String("I"), 189 | PossibleValue::String("J"), 190 | PossibleValue::String("K"), 191 | ] 192 | ); 193 | } 194 | -------------------------------------------------------------------------------- /src/impls/array.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use crate::help::PossibleValues; 4 | use crate::{Error, ErrorInner, FromInputValue}; 5 | 6 | #[derive(Debug)] 7 | pub struct ArrayCtx { 8 | pub delimiter: Option, 9 | pub inner: C, 10 | } 11 | 12 | impl ArrayCtx { 13 | pub fn new(delimiter: Option, inner: C) -> Self { 14 | Self { delimiter, inner } 15 | } 16 | } 17 | 18 | impl Default for ArrayCtx { 19 | fn default() -> Self { 20 | ArrayCtx { delimiter: Some(','), inner: C::default() } 21 | } 22 | } 23 | 24 | impl<'a, T: FromInputValue<'a>, const N: usize> FromInputValue<'a> for [T; N] { 25 | type Context = ArrayCtx; 26 | 27 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 28 | if let Some(delim) = context.delimiter { 29 | let values = value 30 | .split(delim) 31 | .map(|s| T::from_input_value(s, &context.inner)) 32 | .collect::, _>>()?; 33 | 34 | let len = values.len(); 35 | match values.try_into() { 36 | Ok(values) => Ok(values), 37 | Err(_) => { 38 | Err(ErrorInner::WrongNumberOfValues { expected: N, got: len }.into()) 39 | } 40 | } 41 | } else { 42 | Err(ErrorInner::WrongNumberOfValues { expected: N, got: 1 }.into()) 43 | } 44 | } 45 | 46 | fn possible_values(context: &Self::Context) -> Option { 47 | T::possible_values(&context.inner) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/impls/bool.rs: -------------------------------------------------------------------------------- 1 | use crate::help::PossibleValues; 2 | use crate::{Error, FromInputValue}; 3 | 4 | impl FromInputValue<'static> for bool { 5 | type Context = (); 6 | 7 | fn from_input_value(value: &str, context: &()) -> Result { 8 | match value { 9 | "1" => Ok(true), 10 | "0" => Ok(false), 11 | s if s.eq_ignore_ascii_case("y") => Ok(true), 12 | s if s.eq_ignore_ascii_case("n") => Ok(false), 13 | s if s.eq_ignore_ascii_case("yes") => Ok(true), 14 | s if s.eq_ignore_ascii_case("no") => Ok(false), 15 | s if s.eq_ignore_ascii_case("true") => Ok(true), 16 | s if s.eq_ignore_ascii_case("false") => Ok(false), 17 | _ => Err(Error::unexpected_value(value, Self::possible_values(context))), 18 | } 19 | } 20 | 21 | fn possible_values(_: &Self::Context) -> Option { 22 | Some(PossibleValues::OneOf(vec![ 23 | PossibleValues::String("yes".into()), 24 | PossibleValues::String("no".into()), 25 | ])) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/impls/char.rs: -------------------------------------------------------------------------------- 1 | use crate::help::PossibleValues; 2 | use crate::{Error, FromInputValue}; 3 | 4 | impl FromInputValue<'static> for char { 5 | type Context = (); 6 | 7 | fn from_input_value(value: &str, context: &()) -> Result { 8 | let mut chars = value.chars(); 9 | let next = chars 10 | .next() 11 | .ok_or_else(|| Error::unexpected_value("", Self::possible_values(context)))?; 12 | 13 | if chars.next().is_some() { 14 | return Err(Error::unexpected_value(value, Self::possible_values(context))); 15 | } 16 | Ok(next) 17 | } 18 | 19 | fn possible_values(_: &Self::Context) -> Option { 20 | Some(PossibleValues::Other("character".into())) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/impls/list.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeSet, HashSet, LinkedList, VecDeque}; 2 | use std::hash::Hash; 3 | use std::iter::FromIterator; 4 | 5 | use palex::ArgsInput; 6 | 7 | use crate::actions::{Action, Set}; 8 | use crate::util::Flag; 9 | use crate::{Error, ErrorInner, FromInput, FromInputValue, Parse, Result}; 10 | 11 | use super::StringCtx; 12 | 13 | /// The parsing context for list-like types. This is used by the following types 14 | /// from the standard library: 15 | /// 16 | /// - [`Vec`] 17 | /// - [`std::collections::VecDeque`] 18 | /// - [`std::collections::HashSet`] 19 | /// - [`std::collections::BTreeSet`] 20 | /// - [`std::collections::LinkedList`] 21 | /// 22 | /// This can parse argument lists like the following: 23 | /// 24 | /// 1. `-f a b c d` 25 | /// 2. `-f=a,b,c,d` 26 | /// 3. `-f a -f b -f c -f d` 27 | /// 28 | /// If you want to allow the third syntax, use [`crate::actions::Append`] 29 | /// action, to make sure that all values are saved. 30 | #[derive(Debug)] 31 | pub struct ListCtx<'a, C> { 32 | /// The flag after which the values should be parsed. 33 | pub flag: Flag<'a>, 34 | /// The maximum number of items that can be parsed at once. The default is 35 | /// `usize::MAX`. 36 | pub max_items: usize, 37 | /// The delimiter that is used when the `-f=a,b,c,d` syntax is used. The 38 | /// default is a comma. 39 | pub delimiter: Option, 40 | /// The context of the values we want to parse 41 | pub inner: C, 42 | /// When `greedy` is set to true, the parser will greedily try to parse as 43 | /// many values as possible (up to `max_items`) at once, except when the 44 | /// 2nd syntax is used. This defaults to `false`, so the 1st syntax is 45 | /// unavailable by default. 46 | /// 47 | /// Note that setting `greedy` to `true` is less problematic if the values 48 | /// can't start with a dash, because then it will stop consuming arguments 49 | /// as soon as it encounters an argument starting with a dash. 50 | pub greedy: bool, 51 | } 52 | 53 | impl<'a, C: Default> From> for ListCtx<'a, C> { 54 | fn from(flag: Flag<'a>) -> Self { 55 | ListCtx { 56 | flag, 57 | max_items: usize::MAX, 58 | delimiter: Some(','), 59 | inner: C::default(), 60 | greedy: false, 61 | } 62 | } 63 | } 64 | 65 | impl<'a, T, C: 'a> FromInput<'a> for Vec 66 | where 67 | T: FromInputValue<'a, Context = C>, 68 | { 69 | type Context = ListCtx<'a, C>; 70 | 71 | fn from_input(input: &mut ArgsInput, context: &Self::Context) -> Result { 72 | let mut flag_set = false; 73 | Set(&mut flag_set).apply(input, &context.flag)?; 74 | 75 | if flag_set { 76 | if input.can_parse_value_no_whitespace() || context.delimiter.is_some() { 77 | parse_list_no_ws(input, context) 78 | } else { 79 | parse_list_with_ws(input, context) 80 | } 81 | } else { 82 | Err(Error::no_value()) 83 | } 84 | } 85 | } 86 | 87 | impl<'a, T, C: 'a> FromInput<'a> for VecDeque 88 | where 89 | T: FromInputValue<'a, Context = C>, 90 | { 91 | type Context = ListCtx<'a, C>; 92 | 93 | fn from_input(input: &mut ArgsInput, context: &Self::Context) -> Result { 94 | let mut flag_set = false; 95 | Set(&mut flag_set).apply(input, &context.flag)?; 96 | 97 | if flag_set { 98 | if input.can_parse_value_no_whitespace() || context.delimiter.is_some() { 99 | parse_list_no_ws(input, context) 100 | } else { 101 | parse_list_with_ws(input, context) 102 | } 103 | } else { 104 | Err(Error::no_value()) 105 | } 106 | } 107 | } 108 | 109 | impl<'a, T, C: 'a> FromInput<'a> for LinkedList 110 | where 111 | T: FromInputValue<'a, Context = C>, 112 | { 113 | type Context = ListCtx<'a, C>; 114 | 115 | fn from_input(input: &mut ArgsInput, context: &Self::Context) -> Result { 116 | let mut flag_set = false; 117 | Set(&mut flag_set).apply(input, &context.flag)?; 118 | 119 | if flag_set { 120 | if input.can_parse_value_no_whitespace() || context.delimiter.is_some() { 121 | parse_list_no_ws(input, context) 122 | } else { 123 | parse_list_with_ws(input, context) 124 | } 125 | } else { 126 | Err(Error::no_value()) 127 | } 128 | } 129 | } 130 | 131 | impl<'a, T, C: 'a> FromInput<'a> for BTreeSet 132 | where 133 | T: FromInputValue<'a, Context = C> + Ord, 134 | { 135 | type Context = ListCtx<'a, C>; 136 | 137 | fn from_input(input: &mut ArgsInput, context: &Self::Context) -> Result { 138 | let mut flag_set = false; 139 | Set(&mut flag_set).apply(input, &context.flag)?; 140 | 141 | if flag_set { 142 | if input.can_parse_value_no_whitespace() || context.delimiter.is_some() { 143 | parse_list_no_ws(input, context) 144 | } else { 145 | parse_list_with_ws(input, context) 146 | } 147 | } else { 148 | Err(Error::no_value()) 149 | } 150 | } 151 | } 152 | 153 | impl<'a, T, C: 'a> FromInput<'a> for HashSet 154 | where 155 | T: FromInputValue<'a, Context = C> + Hash + Eq, 156 | { 157 | type Context = ListCtx<'a, C>; 158 | 159 | fn from_input(input: &mut ArgsInput, context: &Self::Context) -> Result { 160 | let mut flag_set = false; 161 | Set(&mut flag_set).apply(input, &context.flag)?; 162 | 163 | if flag_set { 164 | if input.can_parse_value_no_whitespace() || context.delimiter.is_some() { 165 | parse_list_no_ws(input, context) 166 | } else { 167 | parse_list_with_ws(input, context) 168 | } 169 | } else { 170 | Err(Error::no_value()) 171 | } 172 | } 173 | } 174 | 175 | fn parse_list_no_ws<'a, L: List, T: FromInputValue<'a>>( 176 | input: &mut ArgsInput, 177 | context: &ListCtx<'a, T::Context>, 178 | ) -> Result { 179 | let inner = &context.inner; 180 | 181 | let value: String = input.parse_value( 182 | &StringCtx::default().allow_leading_dashes(T::allow_leading_dashes(inner)), 183 | )?; 184 | 185 | if let Some(delim) = context.delimiter { 186 | let values: L = value 187 | .split(delim) 188 | .map(|s| T::from_input_value(s, inner)) 189 | .enumerate() 190 | .map(|(i, r)| r.map_err(|e| e.chain(ErrorInner::IncompleteValue(i)))) 191 | .collect::>()?; 192 | 193 | let count = values.len(); 194 | if count <= context.max_items { 195 | Ok(values) 196 | } else { 197 | Err(ErrorInner::TooManyValues { max: context.max_items, count }.into()) 198 | } 199 | } else { 200 | let value = T::from_input_value(&value, inner)?; 201 | let mut list = L::default(); 202 | list.add(value); 203 | Ok(list) 204 | } 205 | } 206 | 207 | fn parse_list_with_ws<'a, L: List, T: FromInputValue<'a>>( 208 | input: &mut ArgsInput, 209 | context: &ListCtx<'a, T::Context>, 210 | ) -> Result { 211 | let first = input 212 | .parse_value(&context.inner) 213 | .map_err(|e| e.chain(ErrorInner::IncompleteValue(0)))?; 214 | let mut list = L::default(); 215 | list.add(first); 216 | 217 | for i in 1..context.max_items { 218 | if let Some(value) = input 219 | .try_parse_value(&context.inner) 220 | .map_err(|e| e.chain(ErrorInner::IncompleteValue(i)))? 221 | { 222 | list.add(value); 223 | } else { 224 | break; 225 | } 226 | } 227 | 228 | Ok(list) 229 | } 230 | 231 | trait List: Default + FromIterator { 232 | fn add(&mut self, value: T); 233 | fn len(&self) -> usize; 234 | } 235 | 236 | impl List for Vec { 237 | fn add(&mut self, value: T) { 238 | self.push(value) 239 | } 240 | 241 | fn len(&self) -> usize { 242 | self.len() 243 | } 244 | } 245 | 246 | impl List for VecDeque { 247 | fn add(&mut self, value: T) { 248 | self.push_back(value) 249 | } 250 | 251 | fn len(&self) -> usize { 252 | self.len() 253 | } 254 | } 255 | 256 | impl List for LinkedList { 257 | fn add(&mut self, value: T) { 258 | self.push_back(value) 259 | } 260 | 261 | fn len(&self) -> usize { 262 | self.len() 263 | } 264 | } 265 | 266 | impl List for BTreeSet { 267 | fn add(&mut self, value: T) { 268 | self.insert(value); 269 | } 270 | 271 | fn len(&self) -> usize { 272 | self.len() 273 | } 274 | } 275 | 276 | impl List for HashSet { 277 | fn add(&mut self, value: T) { 278 | self.insert(value); 279 | } 280 | 281 | fn len(&self) -> usize { 282 | self.len() 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/impls/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the implementations of the [`crate::FromInput`] and 2 | //! [`crate::FromInputValue`] traits. 3 | 4 | mod array; 5 | mod bool; 6 | mod char; 7 | mod list; 8 | mod numbers; 9 | mod string; 10 | mod tuple; 11 | mod wrappers; 12 | 13 | pub use list::ListCtx; 14 | pub use numbers::NumberCtx; 15 | pub use string::StringCtx; 16 | -------------------------------------------------------------------------------- /src/impls/numbers.rs: -------------------------------------------------------------------------------- 1 | use std::num::*; 2 | 3 | use crate::help::PossibleValues; 4 | use crate::{Error, FromInputValue}; 5 | 6 | /// The parsing context for numeric types. 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 8 | pub struct NumberCtx { 9 | /// The smallest accepted number 10 | pub min: T, 11 | /// The largest accepted number 12 | pub max: T, 13 | } 14 | 15 | impl NumberCtx 16 | where 17 | T: Copy + PartialOrd + FromInputValue<'static, Context = Self> + std::fmt::Display, 18 | { 19 | fn must_include(&self, n: T) -> Result { 20 | if n >= self.min && n <= self.max { 21 | Ok(n) 22 | } else { 23 | Err(Error::unexpected_value( 24 | format!("number {}", n), 25 | T::possible_values(self), 26 | )) 27 | } 28 | } 29 | } 30 | 31 | macro_rules! default_impl { 32 | ($( $t:ident ),*) => { 33 | $( 34 | impl Default for NumberCtx<$t> { 35 | fn default() -> Self { 36 | NumberCtx { min: $t::MIN, max: $t::MAX } 37 | } 38 | } 39 | )* 40 | }; 41 | } 42 | 43 | macro_rules! from_input_value { 44 | (signed -> $( $t:ident ),*) => { 45 | $( 46 | impl FromInputValue<'static> for $t { 47 | type Context = NumberCtx<$t>; 48 | 49 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 50 | context.must_include(value.parse()?) 51 | } 52 | 53 | fn allow_leading_dashes(context: &Self::Context) -> bool { 54 | context.min.is_negative() 55 | } 56 | 57 | fn possible_values(context: &Self::Context) -> Option { 58 | Some(PossibleValues::Other( 59 | match (context.min, context.max) { 60 | ($t::MIN, $t::MAX) => "integer".into(), 61 | ($t::MIN, max) => format!("integer at most {}", max), 62 | (min, $t::MAX) => format!("integer at least {}", min), 63 | (min, max) => format!("integer between {} and {}", min, max), 64 | } 65 | )) 66 | } 67 | } 68 | )* 69 | }; 70 | (signed_nonzero -> $( $t:ident ),*) => { 71 | $( 72 | impl FromInputValue<'static> for $t { 73 | type Context = NumberCtx<$t>; 74 | 75 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 76 | context.must_include(value.parse()?) 77 | } 78 | 79 | fn allow_leading_dashes(context: &Self::Context) -> bool { 80 | context.min.get().is_negative() 81 | } 82 | 83 | fn possible_values(context: &Self::Context) -> Option { 84 | Some(PossibleValues::Other( 85 | format!("integer between {} and {}", context.min, context.max), 86 | )) 87 | } 88 | } 89 | )* 90 | }; 91 | (unsigned -> $( $t:ident ),*) => { 92 | $( 93 | impl FromInputValue<'static> for $t { 94 | type Context = NumberCtx<$t>; 95 | 96 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 97 | context.must_include(value.parse()?) 98 | } 99 | 100 | fn allow_leading_dashes(_: &Self::Context) -> bool { false } 101 | 102 | fn possible_values(context: &Self::Context) -> Option { 103 | Some(PossibleValues::Other( 104 | format!("integer between {} and {}", context.min, context.max), 105 | )) 106 | } 107 | } 108 | )* 109 | }; 110 | (float -> $( $t:ident ),*) => { 111 | $( 112 | impl FromInputValue<'static> for $t { 113 | type Context = NumberCtx<$t>; 114 | 115 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 116 | context.must_include(value.parse()?) 117 | } 118 | 119 | fn allow_leading_dashes(context: &Self::Context) -> bool { 120 | context.min.is_sign_negative() 121 | } 122 | 123 | fn possible_values(context: &Self::Context) -> Option { 124 | Some(PossibleValues::Other( 125 | match (context.min, context.max) { 126 | (min, max) if min == $t::MIN && max == $t::MAX => "number".into(), 127 | (min, max) if min == $t::MIN => format!("number at most {}", max), 128 | (min, max) if max == $t::MAX => format!("number at least {}", min), 129 | (min, max) => format!("number between {} and {}", min, max), 130 | } 131 | )) 132 | } 133 | } 134 | )* 135 | }; 136 | } 137 | 138 | default_impl!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64); 139 | 140 | from_input_value! { signed -> i8, i16, i32, i64, i128, isize } 141 | from_input_value! { signed_nonzero -> 142 | NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize 143 | } 144 | from_input_value! { unsigned -> 145 | u8, u16, u32, u64, u128, usize, 146 | NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, NonZeroUsize 147 | } 148 | from_input_value! { float -> f32, f64 } 149 | -------------------------------------------------------------------------------- /src/impls/string.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::ffi::OsString; 3 | use std::path::PathBuf; 4 | 5 | use crate::help::PossibleValues; 6 | use crate::{Error, FromInputValue}; 7 | 8 | /// The parsing context for strings 9 | pub struct StringCtx { 10 | /// The minimum length of the string in bytes 11 | pub min_length: usize, 12 | /// The maximum length of the string in bytes 13 | pub max_length: usize, 14 | /// Whether or not the string may start with dashes 15 | pub allow_leading_dashes: bool, 16 | } 17 | 18 | impl Default for StringCtx { 19 | fn default() -> Self { 20 | StringCtx { min_length: 0, max_length: usize::MAX, allow_leading_dashes: false } 21 | } 22 | } 23 | 24 | impl StringCtx { 25 | /// Create a new `StringCtx` that doesn't accept strings starting with 26 | /// leading dashes 27 | pub fn new(min_length: usize, max_length: usize) -> Self { 28 | StringCtx { min_length, max_length, allow_leading_dashes: false } 29 | } 30 | 31 | /// Sets `allow_leading_dashes` to true 32 | pub fn allow_leading_dashes(mut self, x: bool) -> Self { 33 | self.allow_leading_dashes = x; 34 | self 35 | } 36 | } 37 | 38 | impl FromInputValue<'static> for String { 39 | type Context = StringCtx; 40 | 41 | fn from_input_value(value: &str, context: &StringCtx) -> Result { 42 | if value.len() < context.min_length || value.len() > context.max_length { 43 | Err(Error::unexpected_value( 44 | format!("string with length {}", value.len()), 45 | Self::possible_values(context), 46 | )) 47 | } else { 48 | Ok(value.to_string()) 49 | } 50 | } 51 | 52 | fn allow_leading_dashes(context: &Self::Context) -> bool { 53 | context.allow_leading_dashes 54 | } 55 | 56 | fn possible_values(context: &Self::Context) -> Option { 57 | Some(PossibleValues::Other(match (context.min_length, context.max_length) { 58 | (0, usize::MAX) => "string".into(), 59 | (1, usize::MAX) => "non-empty string".into(), 60 | (min, usize::MAX) => format!("string with at least {} bytes", min), 61 | (0, max) => format!("string with at most {} bytes", max), 62 | (min, max) => format!("string with {} to {} bytes", min, max), 63 | })) 64 | } 65 | } 66 | 67 | impl FromInputValue<'static> for OsString { 68 | type Context = StringCtx; 69 | 70 | fn from_input_value(value: &str, context: &StringCtx) -> Result { 71 | if value.len() < context.min_length || value.len() > context.max_length { 72 | Err(Error::unexpected_value( 73 | format!("string with length {}", value.len()), 74 | Self::possible_values(context), 75 | )) 76 | } else { 77 | Ok(value.into()) 78 | } 79 | } 80 | 81 | fn allow_leading_dashes(context: &Self::Context) -> bool { 82 | context.allow_leading_dashes 83 | } 84 | 85 | fn possible_values(context: &Self::Context) -> Option { 86 | Some(PossibleValues::Other(match (context.min_length, context.max_length) { 87 | (0, usize::MAX) => "string".into(), 88 | (1, usize::MAX) => "non-empty string".into(), 89 | (min, usize::MAX) => format!("string with at least {} bytes", min), 90 | (0, max) => format!("string with at most {} bytes", max), 91 | (min, max) => format!("string with {} to {} bytes", min, max), 92 | })) 93 | } 94 | } 95 | 96 | impl FromInputValue<'static> for PathBuf { 97 | type Context = StringCtx; 98 | 99 | fn from_input_value(value: &str, context: &StringCtx) -> Result { 100 | if value.len() < context.min_length || value.len() > context.max_length { 101 | Err(Error::unexpected_value( 102 | format!("string with length {}", value.len()), 103 | Self::possible_values(context), 104 | )) 105 | } else { 106 | Ok(value.into()) 107 | } 108 | } 109 | 110 | fn allow_leading_dashes(context: &Self::Context) -> bool { 111 | context.allow_leading_dashes 112 | } 113 | 114 | fn possible_values(context: &Self::Context) -> Option { 115 | Some(PossibleValues::Other(match (context.min_length, context.max_length) { 116 | (0, usize::MAX) => "path".into(), 117 | (1, usize::MAX) => "non-empty path".into(), 118 | (min, usize::MAX) => format!("path with at least {} bytes", min), 119 | (0, max) => format!("path with at most {} bytes", max), 120 | (min, max) => format!("path with {} to {} bytes", min, max), 121 | })) 122 | } 123 | } 124 | 125 | impl FromInputValue<'static> for Cow<'static, str> { 126 | type Context = StringCtx; 127 | 128 | fn from_input_value(value: &str, context: &StringCtx) -> Result { 129 | if value.len() < context.min_length || value.len() > context.max_length { 130 | Err(Error::unexpected_value( 131 | format!("string with length {}", value.len()), 132 | Self::possible_values(context), 133 | )) 134 | } else { 135 | Ok(Cow::Owned(value.into())) 136 | } 137 | } 138 | 139 | fn allow_leading_dashes(context: &Self::Context) -> bool { 140 | context.allow_leading_dashes 141 | } 142 | 143 | fn possible_values(context: &Self::Context) -> Option { 144 | Some(PossibleValues::Other(match (context.min_length, context.max_length) { 145 | (0, usize::MAX) => "string".into(), 146 | (1, usize::MAX) => "non-empty string".into(), 147 | (min, usize::MAX) => format!("string with at least {} bytes", min), 148 | (0, max) => format!("string with at most {} bytes", max), 149 | (min, max) => format!("string with {} to {} bytes", min, max), 150 | })) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/impls/tuple.rs: -------------------------------------------------------------------------------- 1 | use crate::help::PossibleValues; 2 | use crate::{Error, ErrorInner, FromInputValue}; 3 | 4 | #[derive(Debug)] 5 | pub struct TupleCtx { 6 | pub delimiter: char, 7 | pub inner: C, 8 | } 9 | 10 | impl TupleCtx { 11 | pub fn new(delimiter: char, inner: C) -> Self { 12 | Self { delimiter, inner } 13 | } 14 | } 15 | 16 | impl Default for TupleCtx { 17 | fn default() -> Self { 18 | TupleCtx { delimiter: ',', inner: C::default() } 19 | } 20 | } 21 | 22 | macro_rules! impl_tuple { 23 | ($( $t:ident $v:ident $i:tt ),* $(,)?) => { 24 | impl<'a, $( $t: FromInputValue<'a> ),*> FromInputValue<'a> for ($( $t ),* ,) { 25 | type Context = TupleCtx<($( $t::Context ),* ,)>; 26 | 27 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 28 | let mut iter = value.split(context.delimiter); 29 | 30 | $( 31 | let $v = $t::from_input_value( 32 | iter.next().ok_or_else(|| ErrorInner::IncompleteValue($i + 1))?, 33 | &context.inner.$i, 34 | )?; 35 | )* 36 | 37 | Ok(($( $v ),* ,)) 38 | } 39 | 40 | fn allow_leading_dashes(context: &Self::Context) -> bool { 41 | T1::allow_leading_dashes(&context.inner.0) 42 | } 43 | 44 | fn possible_values(context: &Self::Context) -> Option { 45 | T1::possible_values(&context.inner.0) 46 | } 47 | } 48 | }; 49 | } 50 | 51 | impl_tuple!( 52 | T1 v1 0, 53 | ); 54 | impl_tuple!( 55 | T1 v1 0, 56 | T2 v2 1, 57 | ); 58 | impl_tuple!( 59 | T1 v1 0, 60 | T2 v2 1, 61 | T3 v3 2, 62 | ); 63 | impl_tuple!( 64 | T1 v1 0, 65 | T2 v2 1, 66 | T3 v3 2, 67 | T4 v4 3, 68 | ); 69 | impl_tuple!( 70 | T1 v1 0, 71 | T2 v2 1, 72 | T3 v3 2, 73 | T4 v4 3, 74 | T5 v5 4, 75 | ); 76 | impl_tuple!( 77 | T1 v1 0, 78 | T2 v2 1, 79 | T3 v3 2, 80 | T4 v4 3, 81 | T5 v5 4, 82 | T6 v6 5, 83 | ); 84 | impl_tuple!( 85 | T1 v1 0, 86 | T2 v2 1, 87 | T3 v3 2, 88 | T4 v4 3, 89 | T5 v5 4, 90 | T6 v6 5, 91 | T7 v7 6, 92 | ); 93 | impl_tuple!( 94 | T1 v1 0, 95 | T2 v2 1, 96 | T3 v3 2, 97 | T4 v4 3, 98 | T5 v5 4, 99 | T6 v6 5, 100 | T7 v7 6, 101 | T8 v8 7, 102 | ); 103 | impl_tuple!( 104 | T1 v1 0, 105 | T2 v2 1, 106 | T3 v3 2, 107 | T4 v4 3, 108 | T5 v5 4, 109 | T6 v6 5, 110 | T7 v7 6, 111 | T8 v8 7, 112 | T9 v9 8, 113 | ); 114 | impl_tuple!( 115 | T1 v1 0, 116 | T2 v2 1, 117 | T3 v3 2, 118 | T4 v4 3, 119 | T5 v5 4, 120 | T6 v6 5, 121 | T7 v7 6, 122 | T8 v8 7, 123 | T9 v9 8, 124 | T10 v10 9, 125 | ); 126 | impl_tuple!( 127 | T1 v1 0, 128 | T2 v2 1, 129 | T3 v3 2, 130 | T4 v4 3, 131 | T5 v5 4, 132 | T6 v6 5, 133 | T7 v7 6, 134 | T8 v8 7, 135 | T9 v9 8, 136 | T10 v10 9, 137 | T11 v11 10, 138 | ); 139 | impl_tuple!( 140 | T1 v1 0, 141 | T2 v2 1, 142 | T3 v3 2, 143 | T4 v4 3, 144 | T5 v5 4, 145 | T6 v6 5, 146 | T7 v7 6, 147 | T8 v8 7, 148 | T9 v9 8, 149 | T10 v10 9, 150 | T11 v11 10, 151 | T12 v12 11, 152 | ); 153 | -------------------------------------------------------------------------------- /src/impls/wrappers.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::cell::{Cell, RefCell, UnsafeCell}; 3 | use std::mem::ManuallyDrop; 4 | use std::rc::Rc; 5 | use std::sync::{Arc, Mutex, RwLock}; 6 | 7 | use crate::help::PossibleValues; 8 | use crate::{Error, FromInputValue}; 9 | 10 | impl<'a, T: FromInputValue<'a>> FromInputValue<'a> for Box { 11 | type Context = T::Context; 12 | 13 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 14 | T::from_input_value(value, context).map(Self::new) 15 | } 16 | 17 | fn allow_leading_dashes(context: &Self::Context) -> bool { 18 | T::allow_leading_dashes(context) 19 | } 20 | 21 | fn possible_values(context: &Self::Context) -> Option { 22 | T::possible_values(context) 23 | } 24 | } 25 | 26 | impl<'a, T: FromInputValue<'a>> FromInputValue<'a> for Rc { 27 | type Context = T::Context; 28 | 29 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 30 | T::from_input_value(value, context).map(Self::new) 31 | } 32 | 33 | fn allow_leading_dashes(context: &Self::Context) -> bool { 34 | T::allow_leading_dashes(context) 35 | } 36 | 37 | fn possible_values(context: &Self::Context) -> Option { 38 | T::possible_values(context) 39 | } 40 | } 41 | 42 | impl<'a, T: FromInputValue<'a>> FromInputValue<'a> for Arc { 43 | type Context = T::Context; 44 | 45 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 46 | T::from_input_value(value, context).map(Self::new) 47 | } 48 | 49 | fn allow_leading_dashes(context: &Self::Context) -> bool { 50 | T::allow_leading_dashes(context) 51 | } 52 | 53 | fn possible_values(context: &Self::Context) -> Option { 54 | T::possible_values(context) 55 | } 56 | } 57 | 58 | impl<'a, T: FromInputValue<'a>> FromInputValue<'a> for Cell { 59 | type Context = T::Context; 60 | 61 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 62 | T::from_input_value(value, context).map(Self::new) 63 | } 64 | 65 | fn allow_leading_dashes(context: &Self::Context) -> bool { 66 | T::allow_leading_dashes(context) 67 | } 68 | 69 | fn possible_values(context: &Self::Context) -> Option { 70 | T::possible_values(context) 71 | } 72 | } 73 | 74 | impl<'a, T: FromInputValue<'a>> FromInputValue<'a> for RefCell { 75 | type Context = T::Context; 76 | 77 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 78 | T::from_input_value(value, context).map(Self::new) 79 | } 80 | 81 | fn allow_leading_dashes(context: &Self::Context) -> bool { 82 | T::allow_leading_dashes(context) 83 | } 84 | 85 | fn possible_values(context: &Self::Context) -> Option { 86 | T::possible_values(context) 87 | } 88 | } 89 | 90 | impl<'a, T: FromInputValue<'a>> FromInputValue<'a> for UnsafeCell { 91 | type Context = T::Context; 92 | 93 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 94 | T::from_input_value(value, context).map(Self::new) 95 | } 96 | 97 | fn allow_leading_dashes(context: &Self::Context) -> bool { 98 | T::allow_leading_dashes(context) 99 | } 100 | 101 | fn possible_values(context: &Self::Context) -> Option { 102 | T::possible_values(context) 103 | } 104 | } 105 | 106 | impl<'a, T: FromInputValue<'a>> FromInputValue<'a> for Mutex { 107 | type Context = T::Context; 108 | 109 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 110 | T::from_input_value(value, context).map(Self::new) 111 | } 112 | 113 | fn allow_leading_dashes(context: &Self::Context) -> bool { 114 | T::allow_leading_dashes(context) 115 | } 116 | 117 | fn possible_values(context: &Self::Context) -> Option { 118 | T::possible_values(context) 119 | } 120 | } 121 | 122 | impl<'a, T: FromInputValue<'a>> FromInputValue<'a> for RwLock { 123 | type Context = T::Context; 124 | 125 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 126 | T::from_input_value(value, context).map(Self::new) 127 | } 128 | 129 | fn allow_leading_dashes(context: &Self::Context) -> bool { 130 | T::allow_leading_dashes(context) 131 | } 132 | 133 | fn possible_values(context: &Self::Context) -> Option { 134 | T::possible_values(context) 135 | } 136 | } 137 | 138 | impl<'a, T: FromInputValue<'a>> FromInputValue<'a> for ManuallyDrop { 139 | type Context = T::Context; 140 | 141 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 142 | T::from_input_value(value, context).map(Self::new) 143 | } 144 | 145 | fn allow_leading_dashes(context: &Self::Context) -> bool { 146 | T::allow_leading_dashes(context) 147 | } 148 | 149 | fn possible_values(context: &Self::Context) -> Option { 150 | T::possible_values(context) 151 | } 152 | } 153 | 154 | impl<'a, T: ToOwned> FromInputValue<'a> for Cow<'static, T> 155 | where 156 | T::Owned: FromInputValue<'a>, 157 | { 158 | type Context = >::Context; 159 | 160 | fn from_input_value(value: &str, context: &Self::Context) -> Result { 161 | let v = ::from_input_value(value, context)?; 162 | Ok(Cow::Owned(v)) 163 | } 164 | 165 | fn allow_leading_dashes(context: &Self::Context) -> bool { 166 | ::allow_leading_dashes(context) 167 | } 168 | 169 | fn possible_values(context: &Self::Context) -> Option { 170 | ::possible_values(context) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A fast, extensible, command-line arguments parser. 2 | //! 3 | //! This library is very new, so expect regular breaking changes. If you find a 4 | //! bug or lacking documentation, don't hesitate to open an 5 | //! [issue](https://github.com/Aloso/parkour/issues) or a pull request. 6 | //! 7 | //! This crate started as an experiment, so I'm not sure yet if I want to 8 | //! maintain it long-term. See [here](https://github.com/Aloso/parkour/issues/1) 9 | //! for more. 10 | //! 11 | //! ## Getting started 12 | //! 13 | //! Parkour requires const generics. The first rust version that supports them 14 | //! is Rust 1.51 (`rustc 1.51.0-beta.2`). You can install it with `rustup 15 | //! default beta`. 16 | //! 17 | //! It's recommended to import the [prelude](./prelude/index.html): 18 | //! 19 | //! ``` 20 | //! use parkour::prelude::*; 21 | //! ``` 22 | //! 23 | //! First, create a struct containing all the data you want to parse. For 24 | //! example: 25 | //! 26 | //! ``` 27 | //! struct Command { 28 | //! color: Option, 29 | //! show: Option, 30 | //! } 31 | //! 32 | //! struct Show { 33 | //! pos1: String, 34 | //! out: ColorSpace, 35 | //! size: u8, 36 | //! } 37 | //! 38 | //! enum ColorSpace { 39 | //! Rgb, 40 | //! Cmy, 41 | //! Cmyk, 42 | //! Hsv, 43 | //! Hsl, 44 | //! CieLab, 45 | //! } 46 | //! ``` 47 | //! 48 | //! `bool`, `u8` and `String` can all be parsed by default. To parse 49 | //! `ColorSpace`, we have to implement the [`FromInputValue`] trait. This 50 | //! easiest by using the derive macro: 51 | //! 52 | //! ``` 53 | //! # use parkour::prelude::*; 54 | //! #[derive(FromInputValue)] 55 | //! enum ColorSpace { 56 | //! Rgb, 57 | //! Cmy, 58 | //! Cmyk, 59 | //! Hsv, 60 | //! Hsl, 61 | //! CieLab, 62 | //! } 63 | //! ``` 64 | //! 65 | //! This parses the names of the enum variants case-insensitively. When an 66 | //! invalid value is provided, the error message will say something like: 67 | //! 68 | //! ```text 69 | //! unexpected value, got `foo`, expected rgb, cmy, cmyk, hsv, hsl or cielab 70 | //! ``` 71 | //! 72 | //! Now let's implement `Show` as a subcommand. Unfortunately, there's no 73 | //! convenient derive macro (yet): 74 | //! 75 | //! ``` 76 | //! # use parkour::prelude::*; 77 | //! # #[derive(FromInputValue)] 78 | //! # enum ColorSpace { Rgb, Cmy, Cmyk, Hsv, Hsl, CieLab } 79 | //! # 80 | //! struct Show { 81 | //! pos1: String, 82 | //! color_space: ColorSpace, 83 | //! size: u8, 84 | //! } 85 | //! 86 | //! impl FromInput<'static> for Show { 87 | //! type Context = (); 88 | //! 89 | //! fn from_input(input: &mut ArgsInput, _: &()) -> parkour::Result { 90 | //! if input.parse_command("show") { 91 | //! let mut pos1 = None; 92 | //! let mut color_space = None; 93 | //! let mut size = None; 94 | //! 95 | //! while !input.is_empty() { 96 | //! if SetOnce(&mut color_space) 97 | //! .apply(input, &Flag::LongShort("color-space", "c").into())? { 98 | //! continue; 99 | //! } 100 | //! 101 | //! if SetOnce(&mut size) 102 | //! .apply(input, &Flag::LongShort("size", "s").into())? { 103 | //! continue; 104 | //! } 105 | //! 106 | //! if pos1.is_none() 107 | //! && SetPositional(&mut pos1).apply(input, &"pos1".into())? { 108 | //! continue; 109 | //! } 110 | //! 111 | //! input.expect_empty()?; 112 | //! } 113 | //! 114 | //! Ok(Show { 115 | //! pos1: pos1.ok_or_else(|| parkour::Error::missing_argument("pos1"))?, 116 | //! color_space: color_space 117 | //! .ok_or_else(|| parkour::Error::missing_argument("--color-space"))?, 118 | //! size: size.unwrap_or(4), 119 | //! }) 120 | //! } else { 121 | //! Err(parkour::Error::no_value()) 122 | //! } 123 | //! } 124 | //! } 125 | //! ``` 126 | //! 127 | //! To parse a subcommand, we implement the [`FromInput`] trait. We first check 128 | //! if the next argument is the word `show`. If that's the case, we iterate over 129 | //! the remaining input, until it is empty. 130 | //! 131 | //! In the subcommand, we expect two named arguments (`--color-space` and 132 | //! `--size`) and a positional argument (`pos`). Therefore, in each iteration, 133 | //! we first check if we can parse the named arguments, and then the positional 134 | //! argument. If none of them succeeds and there is still input left, then 135 | //! `input.expect_empty()?` throws an error. 136 | //! 137 | //! Producing the `Show` struct is rather straightforward (`pos` and 138 | //! `--color-space` are required, `--size` defaults to `4`). However, parsing 139 | //! the values involves some type system magic. `SetOnce` and `SetPositional` 140 | //! are [actions], they check if the referenced types can be parsed, and if so, 141 | //! assign the parsed value to the variable automatically. They also ensure that 142 | //! each argument is parsed at most once. 143 | //! 144 | //! Whenever something is parsed, a _context_ is provided that can contain 145 | //! information about _how_ the value should be parsed. In the above example, 146 | //! `Flag::LongShort("color-space", "c").into()` is a context that instructs the 147 | //! parser to parse the color space after the `--color-space` or the `-c` flag. 148 | //! 149 | //! The main command can be implemented similarly: 150 | //! 151 | //! ``` 152 | //! # use parkour::prelude::*; 153 | //! # enum ColorSpace { Rgb, Cmy, Cmyk, Hsv, Hsl, CieLab } 154 | //! # struct Show { 155 | //! # pos1: String, 156 | //! # color_space: ColorSpace, 157 | //! # size: u8, 158 | //! # } 159 | //! # impl FromInput<'static> for Show { 160 | //! # type Context = (); 161 | //! # fn from_input(input: &mut ArgsInput, _: &()) -> parkour::Result { 162 | //! # todo!() 163 | //! # } 164 | //! # } 165 | //! # 166 | //! struct Command { 167 | //! color: Option, 168 | //! show: Option, 169 | //! } 170 | //! 171 | //! impl FromInput<'static> for Command { 172 | //! type Context = (); 173 | //! 174 | //! fn from_input(input: &mut ArgsInput, _: &()) -> parkour::Result { 175 | //! // discard the first argument, which is the path to the executable 176 | //! input.bump_argument().unwrap(); 177 | //! 178 | //! let mut show = None; 179 | //! let mut color = None; 180 | //! 181 | //! while !input.is_empty() { 182 | //! if SetOnce(&mut color).apply(input, &Flag::LongShort("color", "c").into())? { 183 | //! continue; 184 | //! } 185 | //! 186 | //! if SetSubcommand(&mut show).apply(input, &())? { 187 | //! continue; 188 | //! } 189 | //! 190 | //! input.expect_empty()?; 191 | //! } 192 | //! Ok(Command { show, color }) 193 | //! } 194 | //! } 195 | //! ``` 196 | //! 197 | //! This is pretty self-explanatory. Now let's proceed to the main function: 198 | //! 199 | //! ``` 200 | //! # use parkour::prelude::*; 201 | //! # struct Command { 202 | //! # color: Option, 203 | //! # show: Option<()>, 204 | //! # } 205 | //! # impl FromInput<'static> for Command { 206 | //! # type Context = (); 207 | //! # fn from_input(input: &mut ArgsInput, _: &()) -> parkour::Result { 208 | //! # Ok(Command { color: None, show: None }) 209 | //! # } 210 | //! # } 211 | //! # 212 | //! use std::error::Error; 213 | //! 214 | //! fn main() { 215 | //! match Command::from_input(&mut parkour::parser(), &()) { 216 | //! Ok(command) => { 217 | //! println!("parsed successfully"); 218 | //! } 219 | //! Err(e) if e.is_early_exit() => {} 220 | //! Err(e) => { 221 | //! eprint!("{}", e); 222 | //! let mut source = e.source(); 223 | //! while let Some(s) = source { 224 | //! eprint!(": {}", s); 225 | //! source = s.source(); 226 | //! } 227 | //! eprintln!(); 228 | //! } 229 | //! } 230 | //! } 231 | //! ``` 232 | //! 233 | //! The [`parser`] function creates a new parser instance, which 234 | //! implements [`Parse`]. This is used to parse the `Command`. If it fails, we 235 | //! print the error with its sources. I will implement a more convenient method 236 | //! for this, I just haven't gotten around to it yet. I also plan to implement 237 | //! ANSI color support. 238 | //! 239 | //! What's with the `e.is_early_exit()`, you might wonder? This error is 240 | //! returned when parsing was aborted and can be ignored. This error can be used 241 | //! e.g. when the `--help` flag is encountered: 242 | //! 243 | //! ```no_run 244 | //! # use parkour::prelude::*; 245 | //! # struct Command { 246 | //! # color: Option, 247 | //! # show: Option<()>, 248 | //! # } 249 | //! impl FromInput<'static> for Command { 250 | //! type Context = (); 251 | //! 252 | //! fn from_input(input: &mut ArgsInput, _: &()) -> Result { 253 | //! # let color = None; 254 | //! # let show = None; 255 | //! // 256 | //! while !input.is_empty() { 257 | //! if input.parse_long_flag("help") || input.parse_short_flag("h") { 258 | //! println!("Usage:\n\ 259 | //! my-program [-h,--help]\n\ 260 | //! my-program show POS1 -c,--color-space VALUE [-s,--size N]"); 261 | //! 262 | //! return Err(parkour::Error::early_exit()); 263 | //! } 264 | //! 265 | //! // 266 | //! } 267 | //! Ok(Command { show, color }) 268 | //! } 269 | //! } 270 | //! ``` 271 | //! 272 | //! There is one special case that isn't handled yet: The argument `--` usually 273 | //! causes the remaining tokens to be treated as positional arguments, even if 274 | //! they start with a dash. This is easily implemented: 275 | //! 276 | //! ```no_run 277 | //! # use parkour::prelude::*; 278 | //! # struct Command { 279 | //! # color: Option, 280 | //! # show: Option<()>, 281 | //! # } 282 | //! impl FromInput<'static> for Command { 283 | //! type Context = (); 284 | //! 285 | //! fn from_input(input: &mut ArgsInput, _: &()) -> Result { 286 | //! # let color = None; 287 | //! # let show = None; 288 | //! // 289 | //! while !input.is_empty() { 290 | //! if input.parse_long_flag("") { 291 | //! input.set_ignore_dashes(true); 292 | //! continue; 293 | //! } 294 | //! 295 | //! // 296 | //! } 297 | //! Ok(Command { show, color }) 298 | //! } 299 | //! } 300 | //! ``` 301 | //! 302 | //! Unfortunately, this must be repeated in every subcommand. 303 | 304 | #![forbid(unsafe_code)] 305 | #![warn(missing_docs)] 306 | 307 | pub use error::{Error, ErrorInner}; 308 | pub use from_input::{FromInput, FromInputValue}; 309 | pub use parse::Parse; 310 | 311 | pub use palex::ArgsInput; 312 | 313 | #[cfg(feature = "derive")] 314 | pub use parkour_derive::{FromInput, FromInputValue}; 315 | 316 | pub mod actions; 317 | mod error; 318 | mod from_input; 319 | pub mod help; 320 | pub mod impls; 321 | mod parse; 322 | pub mod util; 323 | 324 | /// A parkour result. 325 | pub type Result = std::result::Result; 326 | 327 | /// Create a new parser, which can be used to parse the 328 | /// command-line arguments of the program. 329 | pub fn parser() -> ArgsInput { 330 | ArgsInput::from_args() 331 | } 332 | 333 | /// A prelude to make it easier to import all the needed types and traits. Use 334 | /// it like this: 335 | /// 336 | /// ``` 337 | /// use parkour::prelude::*; 338 | /// ``` 339 | pub mod prelude { 340 | pub use crate::actions::{ 341 | Action, Append, Dec, Inc, Reset, Set, SetOnce, SetPositional, SetSubcommand, 342 | Unset, 343 | }; 344 | pub use crate::impls::{ListCtx, NumberCtx, StringCtx}; 345 | pub use crate::util::{ArgCtx, Flag, PosCtx}; 346 | pub use crate::{ArgsInput, FromInput, FromInputValue, Parse}; 347 | } 348 | -------------------------------------------------------------------------------- /src/parse.rs: -------------------------------------------------------------------------------- 1 | use palex::ArgsInput; 2 | 3 | use crate::{Error, ErrorInner, FromInput, FromInputValue}; 4 | 5 | /// An extension trait of [`palex::ArgsInput`], the trait for types that can 6 | /// produce tokens from a list of command-line arguments. 7 | /// 8 | /// This trait provides several convenience methods for parsing different 9 | /// things. 10 | pub trait Parse: Sized { 11 | /// Parse something using the [`FromInput`] trait 12 | fn parse<'a, F: FromInput<'a>>(&mut self, context: &F::Context) -> Result; 13 | 14 | /// Parse something using the [`FromInput`] trait, but convert 15 | /// [`Error::no_value`] to [`Option::None`]. This is useful when you want to 16 | /// bubble up all errors except for [`Error::no_value`]: 17 | /// 18 | /// ```no_run 19 | /// # use parkour::prelude::*; 20 | /// # let input: parkour::ArgsInput = todo!(); 21 | /// if let Some(x) = input.try_parse(&Flag::Short("o").into())? { 22 | /// // do something with x 23 | /// # let _: usize = x; 24 | /// } 25 | /// # Ok::<(), parkour::Error>(()) 26 | /// ``` 27 | fn try_parse<'a, F: FromInput<'a>>( 28 | &mut self, 29 | context: &F::Context, 30 | ) -> Result, Error>; 31 | 32 | /// Parse a _value_ using the [`FromInputValue`] trait. 33 | fn parse_value<'a, V: FromInputValue<'a>>( 34 | &mut self, 35 | context: &V::Context, 36 | ) -> Result; 37 | 38 | /// Parse a _value_ using the [`FromInputValue`] trait, but convert 39 | /// [`Error::no_value`] to [`Option::None`]. This is useful when you want to 40 | /// bubble up all errors except for [`Error::no_value`]: 41 | /// 42 | /// ```no_run 43 | /// # use parkour::prelude::*; 44 | /// # let input: parkour::ArgsInput = todo!(); 45 | /// if let Some(value) = input.try_parse_value(&Default::default())? { 46 | /// // do something with value 47 | /// # let _: usize = value; 48 | /// } 49 | /// # Ok::<(), parkour::Error>(()) 50 | /// ``` 51 | #[inline] 52 | fn try_parse_value<'a, V: FromInputValue<'a>>( 53 | &mut self, 54 | context: &V::Context, 55 | ) -> Result, Error> { 56 | match self.parse_value(context) { 57 | Ok(value) => Ok(Some(value)), 58 | Err(e) if e.is_no_value() => Ok(None), 59 | Err(e) => Err(e), 60 | } 61 | } 62 | 63 | /// Convenience function for parsing a flag with a single dash, like `-h` or 64 | /// `-foo`. Returns `true` if it succeeded. 65 | fn parse_short_flag(&mut self, flag: &str) -> bool; 66 | 67 | /// Convenience function for parsing a flag with two dashes, like `--h` or 68 | /// `--foo`. Returns `true` if it succeeded. 69 | fn parse_long_flag(&mut self, flag: &str) -> bool; 70 | 71 | /// Convenience function for parsing a (sub)command, i.e. an argument that 72 | /// doesn't start with a dash. Returns `true` if it succeeded. 73 | fn parse_command(&mut self, command: &str) -> bool; 74 | 75 | /// Returns an error if the input is not yet empty. 76 | fn expect_empty(&mut self) -> Result<(), Error>; 77 | 78 | /// Returns an error if the current argument is only partially consumed. 79 | fn expect_end_of_argument(&mut self) -> Result<(), Error>; 80 | } 81 | 82 | impl Parse for ArgsInput { 83 | #[inline] 84 | fn parse<'a, F: FromInput<'a>>(&mut self, context: &F::Context) -> Result { 85 | F::from_input(self, context) 86 | } 87 | 88 | #[inline] 89 | fn try_parse<'a, F: FromInput<'a>>( 90 | &mut self, 91 | context: &F::Context, 92 | ) -> Result, Error> { 93 | F::try_from_input(self, context) 94 | } 95 | 96 | #[inline] 97 | fn parse_value<'a, V: FromInputValue<'a>>( 98 | &mut self, 99 | context: &V::Context, 100 | ) -> Result { 101 | if V::allow_leading_dashes(&context) { 102 | let value = self.value_allows_leading_dashes().ok_or_else(Error::no_value)?; 103 | let result = V::from_input_value(value.as_str(), context)?; 104 | value.eat(); 105 | Ok(result) 106 | } else { 107 | let value = self.value().ok_or_else(Error::no_value)?; 108 | let result = V::from_input_value(value.as_str(), context)?; 109 | value.eat(); 110 | Ok(result) 111 | } 112 | } 113 | 114 | #[inline] 115 | fn parse_short_flag(&mut self, flag: &str) -> bool { 116 | self.eat_one_dash(flag).is_some() 117 | } 118 | 119 | #[inline] 120 | fn parse_long_flag(&mut self, flag: &str) -> bool { 121 | self.eat_two_dashes(flag).is_some() 122 | } 123 | 124 | #[inline] 125 | fn parse_command(&mut self, command: &str) -> bool { 126 | self.eat_no_dash(command).is_some() 127 | } 128 | 129 | fn expect_empty(&mut self) -> Result<(), Error> { 130 | if !self.is_empty() { 131 | return Err(ErrorInner::UnexpectedArgument { 132 | arg: self.bump_argument().unwrap().to_string(), 133 | } 134 | .into()); 135 | } 136 | Ok(()) 137 | } 138 | 139 | fn expect_end_of_argument(&mut self) -> Result<(), Error> { 140 | if self.can_parse_value_no_whitespace() { 141 | return Err(ErrorInner::UnexpectedValue { 142 | value: self.bump_argument().unwrap().to_string(), 143 | } 144 | .into()); 145 | } 146 | Ok(()) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! Several utility types. 2 | 3 | use std::fmt; 4 | use std::fmt::Write as _; 5 | 6 | use palex::ArgsInput; 7 | 8 | use crate::actions::ApplyResult; 9 | use crate::Parse; 10 | 11 | /// The parsing context for a flag. 12 | /// 13 | /// A flag is either short (i.e. it starts with a single dash) or long (i.e. it 14 | /// starts with two dashes). Note that the dashes should **not** be written in 15 | /// the string, i.e. use `Flag::Long("version")`, not `Flag::Long("--version")`. 16 | /// 17 | /// Arguments can often be specified with a long and a short flag (e.g. `--help` 18 | /// and `-h`); Use `Flag::LongShort("help", "h")` in this case. If an argument 19 | /// has more than 2 flags, use `Flag::Many(vec![...])`. 20 | #[derive(Debug, Clone)] 21 | pub enum Flag<'a> { 22 | /// A short flag, like `-h` 23 | Short(&'a str), 24 | /// A long flag, like `--help` 25 | Long(&'a str), 26 | /// A flag with a long and a short alias, e.g. `-h,--help`. 27 | LongShort(&'a str, &'a str), 28 | /// A flag with multiple aliases 29 | Many(Vec>), 30 | } 31 | 32 | impl Flag<'_> { 33 | /// Returns the first alias of the flag as a [String]. 34 | pub fn first_to_string(&self) -> String { 35 | match self { 36 | &Flag::Short(s) => format!("-{}", s), 37 | &Flag::Long(l) => format!("--{}", l), 38 | &Flag::LongShort(l, _) => format!("--{}", l), 39 | Flag::Many(v) => v[0].first_to_string(), 40 | } 41 | } 42 | 43 | /// Parses a flag from a [`Parse`] instance. 44 | pub fn from_input<'a>(input: &mut ArgsInput, context: &Flag<'a>) -> ApplyResult { 45 | Ok(match context { 46 | &Flag::Short(f) => input.parse_short_flag(f), 47 | &Flag::Long(f) => input.parse_long_flag(f), 48 | &Flag::LongShort(l, s) => { 49 | input.parse_long_flag(l) || input.parse_short_flag(s) 50 | } 51 | Flag::Many(flags) => { 52 | flags.iter().any(|flag| matches!(Self::from_input(input, flag), Ok(true))) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | impl fmt::Display for Flag<'_> { 59 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 60 | match self { 61 | Flag::Short(s) => write!(f, "-{}", s), 62 | Flag::Long(l) => write!(f, "--{}", l), 63 | Flag::LongShort(l, s) => write!(f, "--{},-{}", l, s), 64 | Flag::Many(v) => { 65 | for (i, flag) in v.iter().enumerate() { 66 | if i > 0 { 67 | f.write_char(',')?; 68 | } 69 | fmt::Display::fmt(flag, f)?; 70 | } 71 | Ok(()) 72 | } 73 | } 74 | } 75 | } 76 | 77 | /// The parsing context for a named argument, e.g. `--help=config`. 78 | #[derive(Debug, Clone)] 79 | pub struct ArgCtx<'a, C> { 80 | /// The flag before the argument value(s) 81 | pub flag: Flag<'a>, 82 | /// The context for the argument value(s) 83 | pub inner: C, 84 | } 85 | 86 | impl<'a, C> ArgCtx<'a, C> { 87 | /// Creates a new `ArgCtx` instance 88 | pub fn new(flag: Flag<'a>, inner: C) -> Self { 89 | Self { flag, inner } 90 | } 91 | } 92 | 93 | impl<'a, C: Default> From> for ArgCtx<'a, C> { 94 | fn from(flag: Flag<'a>) -> Self { 95 | ArgCtx { flag, inner: C::default() } 96 | } 97 | } 98 | 99 | /// The parsing context for a positional argument. 100 | #[derive(Debug, Clone)] 101 | pub struct PosCtx<'a, C> { 102 | /// The name of the positional argument, used in error messages 103 | pub name: &'a str, 104 | /// The context for the argument value 105 | pub inner: C, 106 | } 107 | 108 | impl<'a, C> PosCtx<'a, C> { 109 | /// Creates a new `PosCtx` instance 110 | pub fn new(name: &'a str, inner: C) -> Self { 111 | Self { name, inner } 112 | } 113 | } 114 | 115 | impl<'a, C: Default> From<&'a str> for PosCtx<'a, C> { 116 | fn from(name: &'a str) -> Self { 117 | PosCtx { name, inner: C::default() } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/it/bool_argument.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as _; 2 | 3 | use parkour::prelude::*; 4 | 5 | #[derive(FromInput, Debug, PartialEq)] 6 | #[parkour(main)] 7 | struct Command { 8 | #[arg(long, short)] 9 | dry_run: bool, 10 | } 11 | 12 | macro_rules! ok { 13 | ($s:literal, $v:expr) => { 14 | assert_parse!(Command, $s, $v) 15 | }; 16 | } 17 | macro_rules! err { 18 | ($s:literal, $e:literal) => { 19 | assert_parse!(Command, $s, $e) 20 | }; 21 | } 22 | 23 | #[test] 24 | fn successes() { 25 | ok!("$", Command { dry_run: false }); 26 | ok!("$ --dry-run", Command { dry_run: true }); 27 | ok!("$ -d", Command { dry_run: true }); 28 | } 29 | 30 | #[test] 31 | fn failures() { 32 | err!("$ -dYES", "unexpected value `YES`"); 33 | err!("$ -d=yes", "unexpected value `yes`"); 34 | err!("$ --dry-run=", "unexpected value ``"); 35 | err!("$ --dry-run yes", "unexpected argument `yes`"); 36 | err!("$ dry-run", "unexpected argument `dry-run`"); 37 | err!( 38 | "$ --dry-run -d", 39 | "--dry-run was used too often, it can be used at most 1 times" 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /tests/it/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! assert_parse { 2 | ($t:ty, $c:literal, $err:literal) => { 3 | let mut input = parkour::ArgsInput::from($c); 4 | match <$t>::from_input(&mut input, &()) { 5 | Ok(f) => { 6 | panic!("Expected error `{}`, got {:?}", $err.escape_debug(), f); 7 | } 8 | Err(e) => { 9 | use std::fmt::Write; 10 | 11 | let mut buf = String::new(); 12 | write!(&mut buf, "{}", e).unwrap(); 13 | let mut source = e.source(); 14 | while let Some(s) = source { 15 | write!(&mut buf, ": {}", s).unwrap(); 16 | source = s.source(); 17 | } 18 | 19 | assert_eq!(buf, $err); 20 | } 21 | } 22 | }; 23 | ($t:ty, $c:literal, $e:expr) => { 24 | let mut input = parkour::ArgsInput::from($c); 25 | match <$t>::from_input(&mut input, &()) { 26 | Ok(f) => { 27 | assert_eq!(f, $e); 28 | } 29 | Err(e) => { 30 | eprintln!("{}", $c); 31 | eprint!("error: {}", e); 32 | let mut source = e.source(); 33 | while let Some(s) = source { 34 | eprint!(": {}", s); 35 | source = s.source(); 36 | } 37 | eprintln!(); 38 | panic!("error parsing command"); 39 | } 40 | } 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /tests/it/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod macros; 3 | mod bool_argument; 4 | mod optional_argument; 5 | mod single_argument; 6 | -------------------------------------------------------------------------------- /tests/it/optional_argument.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as _; 2 | 3 | use parkour::prelude::*; 4 | 5 | #[derive(FromInput, Debug, PartialEq)] 6 | #[parkour(main)] 7 | struct Command { 8 | #[arg(long = "color", long = "colour", short)] // --color,-c 9 | #[arg(long = "alias", short)] // --alias,-a 10 | mode: Option, 11 | } 12 | 13 | #[derive(FromInputValue, Debug, PartialEq)] 14 | enum ColorMode { 15 | Always, 16 | Auto, 17 | Never, 18 | } 19 | 20 | macro_rules! ok { 21 | ($s:literal, $v:expr) => { 22 | assert_parse!(Command, $s, $v) 23 | }; 24 | } 25 | macro_rules! err { 26 | ($s:literal, $e:literal) => { 27 | assert_parse!(Command, $s, $e) 28 | }; 29 | } 30 | 31 | #[test] 32 | fn successes() { 33 | ok!("$", Command { mode: None }); 34 | ok!("$ -c always", Command { mode: Some(ColorMode::Always) }); 35 | ok!("$ -c=always", Command { mode: Some(ColorMode::Always) }); 36 | ok!("$ -cALwAyS", Command { mode: Some(ColorMode::Always) }); 37 | ok!("$ -a always", Command { mode: Some(ColorMode::Always) }); 38 | ok!("$ --color always", Command { mode: Some(ColorMode::Always) }); 39 | ok!("$ --color=always", Command { mode: Some(ColorMode::Always) }); 40 | ok!("$ --colour=always", Command { mode: Some(ColorMode::Always) }); 41 | ok!("$ --alias always", Command { mode: Some(ColorMode::Always) }); 42 | } 43 | 44 | #[test] 45 | fn failures() { 46 | err!("$ --color", "missing value: in `--color`: in `--color`"); 47 | err!( 48 | "$ --color=", 49 | "unexpected value ``, expected `always`, `auto` or `never`: in `--color`" 50 | ); 51 | err!( 52 | "$ --color a", 53 | "unexpected value `a`, expected `always`, `auto` or `never`: in `--color`" 54 | ); 55 | err!( 56 | "$ -ca", 57 | "unexpected value `a`, expected `always`, `auto` or `never`: in `--color`" 58 | ); 59 | err!("$ -bca", "unexpected argument `bca`"); 60 | err!("$ --colorALWAYS", "unexpected argument `colorALWAYS`"); 61 | err!("$ -cALWAYS d", "unexpected argument `d`"); 62 | err!( 63 | "$ -cALWAYS=d", 64 | "unexpected value `ALWAYS=d`, expected `always`, `auto` or `never`: in `--color`" 65 | ); 66 | err!( 67 | "$ -cALWAYS -aNEVER", 68 | "--alias was used too often, it can be used at most 1 times" 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /tests/it/single_argument.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as _; 2 | 3 | use parkour::prelude::*; 4 | 5 | #[derive(FromInput, Debug, PartialEq)] 6 | #[parkour(main)] 7 | struct Command { 8 | #[arg(long = "color", long = "colour", short)] // --color,-c 9 | #[arg(long = "alias", short)] // --alias,-a 10 | mode: ColorMode, 11 | } 12 | 13 | #[derive(FromInputValue, Debug, PartialEq)] 14 | enum ColorMode { 15 | Always, 16 | Auto, 17 | Never, 18 | } 19 | 20 | macro_rules! ok { 21 | ($s:literal, $v:expr) => { 22 | assert_parse!(Command, $s, $v) 23 | }; 24 | } 25 | macro_rules! err { 26 | ($s:literal, $e:literal) => { 27 | assert_parse!(Command, $s, $e) 28 | }; 29 | } 30 | 31 | #[test] 32 | fn successes() { 33 | ok!("$ -c always", Command { mode: ColorMode::Always }); 34 | ok!("$ -c=always", Command { mode: ColorMode::Always }); 35 | ok!("$ -cALwAyS", Command { mode: ColorMode::Always }); 36 | ok!("$ -a always", Command { mode: ColorMode::Always }); 37 | ok!("$ --color always", Command { mode: ColorMode::Always }); 38 | ok!("$ --color=always", Command { mode: ColorMode::Always }); 39 | ok!("$ --colour=always", Command { mode: ColorMode::Always }); 40 | ok!("$ --alias always", Command { mode: ColorMode::Always }); 41 | } 42 | 43 | #[test] 44 | fn failures() { 45 | err!("$", "required --color was not provided"); 46 | err!("$ --color", "missing value: in `--color`: in `--color`"); 47 | err!( 48 | "$ --color=", 49 | "unexpected value ``, expected `always`, `auto` or `never`: in `--color`" 50 | ); 51 | err!( 52 | "$ --color a", 53 | "unexpected value `a`, expected `always`, `auto` or `never`: in `--color`" 54 | ); 55 | err!( 56 | "$ -ca", 57 | "unexpected value `a`, expected `always`, `auto` or `never`: in `--color`" 58 | ); 59 | err!("$ -bca", "unexpected argument `bca`"); 60 | err!("$ --colorALWAYS", "unexpected argument `colorALWAYS`"); 61 | err!("$ -cALWAYS d", "unexpected argument `d`"); 62 | err!( 63 | "$ -cALWAYS=d", 64 | "unexpected value `ALWAYS=d`, expected `always`, `auto` or `never`: in `--color`" 65 | ); 66 | err!( 67 | "$ -cALWAYS -aNEVER", 68 | "--alias was used too often, it can be used at most 1 times" 69 | ); 70 | } 71 | --------------------------------------------------------------------------------