├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── bors.toml ├── crates ├── xflags-macros │ ├── Cargo.toml │ ├── src │ │ ├── ast.rs │ │ ├── emit.rs │ │ ├── lib.rs │ │ ├── parse.rs │ │ └── update.rs │ └── tests │ │ ├── data │ │ ├── aliases.rs │ │ ├── empty.rs │ │ ├── help.rs │ │ ├── repeated_pos.rs │ │ ├── smoke.rs │ │ └── subcommands.rs │ │ └── it │ │ ├── aliases.rs │ │ ├── empty.rs │ │ ├── help.rs │ │ ├── main.rs │ │ ├── repeated_pos.rs │ │ ├── smoke.rs │ │ └── subcommands.rs └── xflags │ ├── Cargo.toml │ ├── examples │ ├── hello-generated.rs │ ├── hello.rs │ ├── immediate-mode.rs │ ├── longer.rs │ └── non-utf8.rs │ └── src │ ├── lib.rs │ └── rt.rs ├── rustfmt.toml └── xtask ├── Cargo.toml └── src ├── main.rs └── tidy.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: ["master", "staging", "trying"] 6 | 7 | env: 8 | CARGO_INCREMENTAL: 0 9 | CARGO_NET_RETRY: 10 10 | CI: 1 11 | RUST_BACKTRACE: short 12 | RUSTFLAGS: -D warnings 13 | RUSTUP_MAX_RETRIES: 10 14 | 15 | jobs: 16 | test: 17 | name: Rust 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 0 # fetch tags for publish 24 | - uses: Swatinem/rust-cache@6720f05bc48b77f96918929a9019fb2203ff71f8 25 | - run: cargo run -p xtask 26 | env: 27 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.4.0-pre.2 4 | 5 | - Generate `--help` messages. 6 | 7 | ## 0.3.2 8 | 9 | - Support command aliases. 10 | - Fix unused mut warning for empty commands. 11 | 12 | ## 0.3.1 13 | 14 | - Better align with [fuchsia CLI spec](https://fuchsia.dev/fuchsia-src/development/api/cli#command_line_arguments): 15 | 16 | * values can begin with `-`: `--takes-value --i-am-the-value` 17 | * support `--` to delimit positional arguments: `cargo run -- --not-an-option` 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["xtask/", "crates/*"] 4 | 5 | [workspace.package] 6 | version = "0.4.0-pre.2" # NB: update the dep! 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/matklad/xflags" 9 | authors = ["Aleksey Kladov "] 10 | edition = "2021" 11 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![API reference](https://docs.rs/once_cell/badge.svg)](https://docs.rs/xflags/) 2 | 3 | # xflags 4 | 5 | Moderately simple command line arguments parsing: 6 | 7 | ```rust 8 | mod flags { 9 | use std::path::PathBuf; 10 | 11 | xflags::xflags! { 12 | src "./examples/basic.rs" 13 | 14 | cmd my-command { 15 | required path: PathBuf 16 | optional -v, --verbose 17 | } 18 | } 19 | 20 | // generated start 21 | // The following code is generated by `xflags` macro. 22 | // Run `env UPDATE_XFLAGS=1 cargo build` to regenerate. 23 | #[derive(Debug)] 24 | pub struct MyCommand { 25 | pub path: PathBuf, 26 | 27 | pub verbose: bool, 28 | } 29 | 30 | impl MyCommand { 31 | pub const HELP: &'static str = Self::HELP_; 32 | 33 | pub fn from_env() -> xflags::Result { 34 | Self::from_env_() 35 | } 36 | 37 | pub fn from_vec(args: Vec) -> xflags::Result { 38 | Self::from_vec_(args) 39 | } 40 | } 41 | // generated end 42 | } 43 | 44 | fn main() { 45 | let flags = flags::MyCommand::from_env(); 46 | println!("{:#?}", flags); 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = [ "Rust" ] 2 | delete_merged_branches = true 3 | -------------------------------------------------------------------------------- /crates/xflags-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xflags-macros" 3 | description = "Private implementation details of xflags." 4 | version.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | authors.workspace = true 8 | edition.workspace = true 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dev-dependencies] 14 | proc-macro2 = "1" 15 | expect-test = "1" 16 | xflags = { path = "../xflags" } 17 | -------------------------------------------------------------------------------- /crates/xflags-macros/src/ast.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub(crate) struct XFlags { 3 | pub(crate) src: Option, 4 | pub(crate) cmd: Cmd, 5 | } 6 | 7 | impl XFlags { 8 | pub fn is_anon(&self) -> bool { 9 | self.cmd.name.is_empty() 10 | } 11 | } 12 | 13 | #[derive(Debug)] 14 | pub(crate) struct Cmd { 15 | pub(crate) name: String, 16 | pub(crate) aliases: Vec, 17 | pub(crate) doc: Option, 18 | pub(crate) args: Vec, 19 | pub(crate) flags: Vec, 20 | pub(crate) subcommands: Vec, 21 | pub(crate) default: bool, 22 | pub(crate) idx: u8, 23 | } 24 | 25 | #[derive(Debug)] 26 | pub(crate) struct Arg { 27 | pub(crate) arity: Arity, 28 | pub(crate) doc: Option, 29 | pub(crate) val: Val, 30 | } 31 | 32 | #[derive(Debug)] 33 | pub(crate) struct Flag { 34 | pub(crate) arity: Arity, 35 | pub(crate) name: String, 36 | pub(crate) short: Option, 37 | pub(crate) doc: Option, 38 | pub(crate) val: Option, 39 | } 40 | 41 | impl Flag { 42 | pub(crate) fn is_help(&self) -> bool { 43 | self.name == "help" 44 | } 45 | } 46 | 47 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 48 | pub(crate) enum Arity { 49 | Optional, 50 | Required, 51 | Repeated, 52 | } 53 | 54 | #[derive(Debug)] 55 | pub(crate) struct Val { 56 | pub(crate) name: String, 57 | pub(crate) ty: Ty, 58 | } 59 | 60 | #[derive(Debug)] 61 | pub(crate) enum Ty { 62 | PathBuf, 63 | OsString, 64 | FromStr(String), 65 | } 66 | -------------------------------------------------------------------------------- /crates/xflags-macros/src/emit.rs: -------------------------------------------------------------------------------- 1 | use crate::{ast, update}; 2 | 3 | use std::{env, fmt::Write, path::Path}; 4 | 5 | macro_rules! w { 6 | ($($tt:tt)*) => {{ let _ = write!($($tt)*); }}; 7 | } 8 | 9 | pub(crate) fn emit(xflags: &ast::XFlags) -> String { 10 | let mut buf = String::new(); 11 | 12 | if xflags.is_anon() { 13 | w!(buf, "{{\n"); 14 | } 15 | 16 | emit_cmd(&mut buf, &xflags.cmd); 17 | blank_line(&mut buf); 18 | emit_api(&mut buf, xflags); 19 | 20 | if !xflags.is_anon() && env::var("UPDATE_XFLAGS").is_ok() { 21 | if let Some(src) = &xflags.src { 22 | update::in_place(&buf, Path::new(src.as_str())) 23 | } else { 24 | update::stdout(&buf); 25 | } 26 | } 27 | 28 | if xflags.src.is_some() { 29 | buf.clear() 30 | } 31 | 32 | blank_line(&mut buf); 33 | emit_impls(&mut buf, xflags); 34 | emit_help(&mut buf, xflags); 35 | 36 | if xflags.is_anon() { 37 | w!(buf, "Flags::from_env_or_exit()"); 38 | w!(buf, "}}\n"); 39 | } 40 | 41 | buf 42 | } 43 | 44 | fn emit_cmd(buf: &mut String, cmd: &ast::Cmd) { 45 | w!(buf, "#[derive(Debug)]\n"); 46 | w!(buf, "pub struct {}", cmd.ident()); 47 | let flags = cmd.flags.iter().filter(|it| !it.is_help()).collect::>(); 48 | if cmd.args.is_empty() && flags.is_empty() && cmd.subcommands.is_empty() { 49 | w!(buf, ";\n"); 50 | return; 51 | } 52 | w!(buf, " {{\n"); 53 | 54 | for arg in &cmd.args { 55 | let ty = gen_arg_ty(arg.arity, &arg.val.ty); 56 | w!(buf, " pub {}: {ty},\n", arg.val.ident()); 57 | } 58 | 59 | if !cmd.args.is_empty() && !flags.is_empty() { 60 | blank_line(buf); 61 | } 62 | 63 | for flag in &flags { 64 | let ty = gen_flag_ty(flag.arity, flag.val.as_ref().map(|it| &it.ty)); 65 | w!(buf, " pub {}: {ty},\n", flag.ident()); 66 | } 67 | 68 | if cmd.has_subcommands() { 69 | w!(buf, " pub subcommand: {},\n", cmd.cmd_enum_ident()); 70 | } 71 | w!(buf, "}}\n"); 72 | 73 | if cmd.has_subcommands() { 74 | blank_line(buf); 75 | w!(buf, "#[derive(Debug)]\n"); 76 | w!(buf, "pub enum {} {{\n", cmd.cmd_enum_ident()); 77 | for sub in &cmd.subcommands { 78 | let name = sub.ident(); 79 | w!(buf, " {name}({name}),\n"); 80 | } 81 | w!(buf, "}}\n"); 82 | 83 | for sub in &cmd.subcommands { 84 | blank_line(buf); 85 | emit_cmd(buf, sub); 86 | } 87 | } 88 | } 89 | 90 | fn gen_flag_ty(arity: ast::Arity, ty: Option<&ast::Ty>) -> String { 91 | match ty { 92 | None => match arity { 93 | ast::Arity::Optional => "bool".to_string(), 94 | ast::Arity::Required => "()".to_string(), 95 | ast::Arity::Repeated => "u32".to_string(), 96 | }, 97 | Some(ty) => gen_arg_ty(arity, ty), 98 | } 99 | } 100 | 101 | fn gen_arg_ty(arity: ast::Arity, ty: &ast::Ty) -> String { 102 | let ty = match ty { 103 | ast::Ty::PathBuf => "PathBuf".into(), 104 | ast::Ty::OsString => "OsString".into(), 105 | ast::Ty::FromStr(it) => it.clone(), 106 | }; 107 | match arity { 108 | ast::Arity::Optional => format!("Option<{}>", ty), 109 | ast::Arity::Required => ty, 110 | ast::Arity::Repeated => format!("Vec<{}>", ty), 111 | } 112 | } 113 | 114 | fn emit_api(buf: &mut String, xflags: &ast::XFlags) { 115 | w!(buf, "impl {} {{\n", xflags.cmd.ident()); 116 | 117 | w!(buf, " #[allow(dead_code)]\n"); 118 | w!(buf, " pub fn from_env_or_exit() -> Self {{\n"); 119 | w!(buf, " Self::from_env_or_exit_()\n"); 120 | w!(buf, " }}\n"); 121 | blank_line(buf); 122 | 123 | w!(buf, " #[allow(dead_code)]\n"); 124 | w!(buf, " pub fn from_env() -> xflags::Result {{\n"); 125 | w!(buf, " Self::from_env_()\n"); 126 | w!(buf, " }}\n"); 127 | blank_line(buf); 128 | 129 | w!(buf, " #[allow(dead_code)]\n"); 130 | w!(buf, " pub fn from_vec(args: Vec) -> xflags::Result {{\n"); 131 | w!(buf, " Self::from_vec_(args)\n"); 132 | w!(buf, " }}\n"); 133 | w!(buf, "}}\n"); 134 | } 135 | 136 | fn emit_impls(buf: &mut String, xflags: &ast::XFlags) { 137 | w!(buf, "impl {} {{\n", xflags.cmd.ident()); 138 | w!(buf, " fn from_env_or_exit_() -> Self {{\n"); 139 | w!(buf, " Self::from_env_().unwrap_or_else(|err| err.exit())\n"); 140 | w!(buf, " }}\n"); 141 | w!(buf, " fn from_env_() -> xflags::Result {{\n"); 142 | w!(buf, " let mut p = xflags::rt::Parser::new_from_env();\n"); 143 | w!(buf, " Self::parse_(&mut p)\n"); 144 | w!(buf, " }}\n"); 145 | w!(buf, " fn from_vec_(args: Vec) -> xflags::Result {{\n"); 146 | w!(buf, " let mut p = xflags::rt::Parser::new(args);\n"); 147 | w!(buf, " Self::parse_(&mut p)\n"); 148 | w!(buf, " }}\n"); 149 | w!(buf, "}}\n"); 150 | blank_line(buf); 151 | emit_parse(buf, &xflags.cmd) 152 | } 153 | 154 | fn emit_parse(buf: &mut String, cmd: &ast::Cmd) { 155 | w!(buf, "impl {} {{\n", cmd.ident()); 156 | w!(buf, "fn parse_(p_: &mut xflags::rt::Parser) -> xflags::Result {{\n"); 157 | w!(buf, "#![allow(non_snake_case, unused_mut)]\n"); 158 | 159 | let mut prefix = String::new(); 160 | emit_locals_rec(buf, &mut prefix, cmd); 161 | blank_line(buf); 162 | w!(buf, "let mut state_ = 0u8;\n"); 163 | 164 | // No while loop needed for command with no items (clippy::never_loop) 165 | if cmd.args.len() + cmd.flags.len() + cmd.subcommands.len() <= 1 { 166 | w!(buf, "if let Some(arg_) = p_.pop_flag() {{\n"); 167 | } else { 168 | w!(buf, "while let Some(arg_) = p_.pop_flag() {{\n"); 169 | } 170 | 171 | w!(buf, "match arg_ {{\n"); 172 | { 173 | w!(buf, "Ok(flag_) => match (state_, flag_.as_str()) {{\n"); 174 | emit_match_flag_rec(buf, &mut prefix, cmd); 175 | w!(buf, "_ => return Err(p_.unexpected_flag(&flag_)),\n"); 176 | w!(buf, "}}\n"); 177 | 178 | w!(buf, "Err(arg_) => match (state_, arg_.to_str().unwrap_or(\"\")) {{\n"); 179 | emit_match_arg_rec(buf, &mut prefix, cmd); 180 | w!(buf, "_ => return Err(p_.unexpected_arg(arg_)),\n"); 181 | w!(buf, "}}\n"); 182 | } 183 | w!(buf, "}}\n"); 184 | w!(buf, "}}\n"); 185 | emit_default_transitions(buf, cmd); 186 | 187 | w!(buf, "Ok("); 188 | emit_record_rec(buf, &mut prefix, cmd); 189 | w!(buf, ")"); 190 | 191 | w!(buf, "}}\n"); 192 | w!(buf, "}}\n"); 193 | } 194 | 195 | fn emit_locals_rec(buf: &mut String, prefix: &mut String, cmd: &ast::Cmd) { 196 | for flag in &cmd.flags { 197 | if !flag.is_help() { 198 | w!(buf, "let mut {prefix}{} = Vec::new();\n", flag.ident()); 199 | } 200 | } 201 | for arg in &cmd.args { 202 | w!(buf, "let mut {prefix}{} = (false, Vec::new());\n", arg.val.ident()); 203 | } 204 | for sub in &cmd.subcommands { 205 | let l = sub.push_prefix(prefix); 206 | emit_locals_rec(buf, prefix, sub); 207 | prefix.truncate(l); 208 | } 209 | } 210 | 211 | fn emit_match_flag_rec(buf: &mut String, prefix: &mut String, cmd: &ast::Cmd) { 212 | w!( 213 | buf, 214 | "({}, \"--help\" | \"-h\") => return Err(p_.help(Self::HELP_{})),\n", 215 | cmd.idx, 216 | snake(prefix).to_uppercase() 217 | ); 218 | for flag in cmd.flags.iter().filter(|f| !f.is_help()) { 219 | w!(buf, "("); 220 | emit_all_ids_rec(buf, cmd); 221 | w!(buf, ", \"--{}\"", flag.name); 222 | if let Some(short) = &flag.short { 223 | w!(buf, "| \"-{short}\""); 224 | } 225 | w!(buf, ") => "); 226 | w!(buf, "{prefix}{}.push(", flag.ident()); 227 | match &flag.val { 228 | Some(val) => match &val.ty { 229 | ast::Ty::OsString | ast::Ty::PathBuf => { 230 | w!(buf, "p_.next_value(&flag_)?.into()") 231 | } 232 | ast::Ty::FromStr(ty) => { 233 | w!(buf, "p_.next_value_from_str::<{ty}>(&flag_)?") 234 | } 235 | }, 236 | None => w!(buf, "()"), 237 | } 238 | w!(buf, "),\n"); 239 | } 240 | if let Some(sub) = cmd.default_subcommand() { 241 | w!(buf, "({}, _) => {{ p_.push_back(Ok(flag_)); state_ = {}; }}", cmd.idx, sub.idx); 242 | } 243 | for sub in &cmd.subcommands { 244 | let l = sub.push_prefix(prefix); 245 | emit_match_flag_rec(buf, prefix, sub); 246 | prefix.truncate(l); 247 | } 248 | } 249 | 250 | fn emit_match_arg_rec(buf: &mut String, prefix: &mut String, cmd: &ast::Cmd) { 251 | for sub in cmd.named_subcommands() { 252 | let sub_match = 253 | sub.all_identifiers().map(|s| format!("\"{s}\"")).collect::>().join(" | "); 254 | w!(buf, "({}, {}) => state_ = {},\n", cmd.idx, sub_match, sub.idx); 255 | } 256 | 257 | if cmd.args.is_empty() { 258 | // add `help` subcommand only if command takes no args to make sure it doesn't take precedence 259 | w!( 260 | buf, 261 | "({}, \"help\") => return Err(p_.help(Self::HELP_{})),\n", 262 | cmd.idx, 263 | snake(prefix).to_uppercase() 264 | ); 265 | } 266 | 267 | if !cmd.args.is_empty() || cmd.has_subcommands() { 268 | w!(buf, "({}, _) => {{\n", cmd.idx); 269 | for arg in &cmd.args { 270 | let done = match arg.arity { 271 | ast::Arity::Optional | ast::Arity::Required => "done_ @ ", 272 | ast::Arity::Repeated => "", 273 | }; 274 | w!(buf, "if let ({done}false, buf_) = &mut {prefix}{} {{\n", arg.val.ident()); 275 | w!(buf, "buf_.push("); 276 | match &arg.val.ty { 277 | ast::Ty::OsString | ast::Ty::PathBuf => { 278 | w!(buf, "arg_.into()") 279 | } 280 | ast::Ty::FromStr(ty) => { 281 | w!(buf, "p_.value_from_str::<{ty}>(\"{}\", arg_)?", arg.val.name); 282 | } 283 | } 284 | w!(buf, ");\n"); 285 | match arg.arity { 286 | ast::Arity::Optional | ast::Arity::Required => { 287 | w!(buf, "*done_ = true;\n"); 288 | } 289 | ast::Arity::Repeated => (), 290 | } 291 | w!(buf, "continue;\n"); 292 | w!(buf, "}}\n"); 293 | } 294 | 295 | if let Some(sub) = cmd.default_subcommand() { 296 | w!(buf, "p_.push_back(Err(arg_)); state_ = {};", sub.idx); 297 | } else { 298 | w!(buf, "return Err(p_.unexpected_arg(arg_));"); 299 | } 300 | 301 | w!(buf, "}}\n"); 302 | } 303 | 304 | for sub in &cmd.subcommands { 305 | let l = sub.push_prefix(prefix); 306 | emit_match_arg_rec(buf, prefix, sub); 307 | prefix.truncate(l); 308 | } 309 | } 310 | 311 | fn emit_record_rec(buf: &mut String, prefix: &mut String, cmd: &ast::Cmd) { 312 | w!(buf, "{} {{\n", cmd.ident()); 313 | 314 | for flag in &cmd.flags { 315 | if flag.is_help() { 316 | continue; 317 | } 318 | w!(buf, "{}: ", flag.ident()); 319 | match &flag.val { 320 | Some(_val) => match flag.arity { 321 | ast::Arity::Optional => { 322 | w!(buf, "p_.optional(\"--{}\", {prefix}{})?", flag.name, flag.ident()) 323 | } 324 | ast::Arity::Required => { 325 | w!(buf, "p_.required(\"--{}\", {prefix}{})?", flag.name, flag.ident()) 326 | } 327 | ast::Arity::Repeated => w!(buf, "{prefix}{}", flag.ident()), 328 | }, 329 | None => match flag.arity { 330 | ast::Arity::Optional => { 331 | w!(buf, "p_.optional(\"--{}\", {prefix}{})?.is_some()", flag.name, flag.ident()) 332 | } 333 | ast::Arity::Required => { 334 | w!(buf, "p_.required(\"--{}\", {prefix}{})?", flag.name, flag.ident()) 335 | } 336 | ast::Arity::Repeated => w!(buf, "{prefix}{}.len() as u32", flag.ident()), 337 | }, 338 | } 339 | w!(buf, ",\n"); 340 | } 341 | for arg in &cmd.args { 342 | let val = &arg.val; 343 | w!(buf, "{}: ", val.ident()); 344 | match arg.arity { 345 | ast::Arity::Optional => { 346 | w!(buf, "p_.optional(\"{}\", {prefix}{}.1)?", val.name, val.ident()) 347 | } 348 | ast::Arity::Required => { 349 | w!(buf, "p_.required(\"{}\", {prefix}{}.1)?", val.name, val.ident()) 350 | } 351 | ast::Arity::Repeated => w!(buf, "{prefix}{}.1", val.ident()), 352 | } 353 | w!(buf, ",\n"); 354 | } 355 | if cmd.has_subcommands() { 356 | w!(buf, "subcommand: match state_ {{\n"); 357 | for sub in &cmd.subcommands { 358 | emit_leaf_ids_rec(buf, sub); 359 | w!(buf, " => {}::{}(", cmd.cmd_enum_ident(), sub.ident()); 360 | let l = prefix.len(); 361 | prefix.push_str(&snake(&sub.name)); 362 | prefix.push_str("__"); 363 | emit_record_rec(buf, prefix, sub); 364 | prefix.truncate(l); 365 | w!(buf, "),\n"); 366 | } 367 | w!(buf, "_ => return Err(p_.subcommand_required())"); 368 | w!(buf, "}}\n"); 369 | } 370 | 371 | w!(buf, "}}"); 372 | } 373 | 374 | fn emit_leaf_ids_rec(buf: &mut String, cmd: &ast::Cmd) { 375 | if cmd.has_subcommands() { 376 | for sub in &cmd.subcommands { 377 | emit_leaf_ids_rec(buf, sub) 378 | } 379 | } else { 380 | w!(buf, "| {}", cmd.idx) 381 | } 382 | } 383 | 384 | fn emit_all_ids_rec(buf: &mut String, cmd: &ast::Cmd) { 385 | w!(buf, "| {}", cmd.idx); 386 | for sub in &cmd.subcommands { 387 | emit_all_ids_rec(buf, sub) 388 | } 389 | } 390 | 391 | fn emit_default_transitions(buf: &mut String, cmd: &ast::Cmd) { 392 | if let Some(sub) = cmd.default_subcommand() { 393 | w!(buf, "state_ = if state_ == {} {{ {} }} else {{ state_ }};", cmd.idx, sub.idx); 394 | } 395 | for sub in &cmd.subcommands { 396 | emit_default_transitions(buf, sub); 397 | } 398 | } 399 | 400 | fn emit_help(buf: &mut String, xflags: &ast::XFlags) { 401 | w!(buf, "impl {} {{\n", xflags.cmd.ident()); 402 | 403 | cmd_help_rec(buf, &xflags.cmd, ""); 404 | 405 | w!(buf, "}}\n"); 406 | } 407 | 408 | fn cmd_help_rec(buf: &mut String, cmd: &ast::Cmd, prefix: &str) { 409 | let mut help_buf = String::new(); 410 | w!(help_buf, "Usage: {}", cmd.name); 411 | for arg in cmd.args_with_default() { 412 | let (l, r) = arg.arity.brackets(); 413 | w!(help_buf, " {l}{}{r}", arg.val.name); 414 | } 415 | for flag in cmd.flags_with_default() { 416 | // <-f> doesn't make sense, if it has to be included it should just be -f 417 | let (l, r) = match flag.arity { 418 | ast::Arity::Required => ("", ""), 419 | _ => flag.arity.brackets(), 420 | }; 421 | let f = flag.short.clone().unwrap_or_else(|| format!("-{}", flag.name)); 422 | 423 | match &flag.val { 424 | Some(v) => w!(help_buf, " {l}-{f} <{}>{r}", v.name), 425 | None => w!(help_buf, " {l}-{f}{r}"), 426 | } 427 | } 428 | if cmd.has_subcommands() { 429 | w!(help_buf, " ") 430 | } 431 | if let Some(doc) = &cmd.doc { 432 | w!(help_buf, "\n\n{}\n", doc); 433 | } 434 | let args_with_default = cmd.args_with_default(); 435 | if !args_with_default.is_empty() { 436 | w!(help_buf, "\nArguments:\n"); 437 | for arg in args_with_default { 438 | let (l, r) = arg.arity.brackets(); 439 | let pre_doc = format!("{l}{}{r}", arg.val.name); 440 | w!(help_buf, " {:<20} {}\n", pre_doc, arg.doc.as_deref().unwrap_or("")); 441 | } 442 | } 443 | let flags_with_default = cmd.flags_with_default(); 444 | if !flags_with_default.is_empty() { 445 | w!(help_buf, "\nOptions:\n"); 446 | for flag in flags_with_default { 447 | let short = flag.short.as_ref().map(|it| format!("-{it}, ")).unwrap_or_default(); 448 | let value = flag.val.as_ref().map(|it| format!(" <{}>", it.name)).unwrap_or_default(); 449 | let pre_doc = format!("{short}--{}{value}", flag.name); 450 | w!(help_buf, " {:<20} {}\n", pre_doc, flag.doc.as_deref().unwrap_or("")); 451 | } 452 | } 453 | w!(help_buf, "\nCommands:"); 454 | for subcommand in cmd.named_subcommands() { 455 | w!(help_buf, "\n {:<20} {}", subcommand.name, subcommand.doc.as_deref().unwrap_or("")); 456 | } 457 | for subcommand in &cmd.subcommands { 458 | let prefix = format!("{}{}__", prefix, subcommand.name); 459 | cmd_help_rec(buf, subcommand, &prefix); 460 | } 461 | w!(help_buf, "\n {:<20} ", "help"); 462 | w!(help_buf, "Print this message or the help of the given subcommand(s)"); 463 | w!(buf, "const HELP_{}: &'static str = \"{help_buf}\";\n", snake(prefix).to_uppercase()); 464 | } 465 | 466 | impl ast::Cmd { 467 | fn ident(&self) -> String { 468 | if self.name.is_empty() { 469 | return "Flags".to_string(); 470 | } 471 | camel(&self.name) 472 | } 473 | pub(crate) fn all_identifiers(&self) -> impl Iterator { 474 | [&self.name].into_iter().chain(self.aliases.iter()) 475 | } 476 | fn cmd_enum_ident(&self) -> String { 477 | format!("{}Cmd", self.ident()) 478 | } 479 | fn push_prefix(&self, buf: &mut String) -> usize { 480 | let l = buf.len(); 481 | buf.push_str(&snake(&self.name)); 482 | buf.push_str("__"); 483 | l 484 | } 485 | fn has_subcommands(&self) -> bool { 486 | !self.subcommands.is_empty() 487 | } 488 | fn named_subcommands(&self) -> &[ast::Cmd] { 489 | let start = if self.default { 1 } else { 0 }; 490 | &self.subcommands[start..] 491 | } 492 | fn default_subcommand(&self) -> Option<&ast::Cmd> { 493 | if self.default { 494 | self.subcommands.first() 495 | } else { 496 | None 497 | } 498 | } 499 | fn args_with_default(&self) -> Vec<&ast::Arg> { 500 | let mut res = self.args.iter().collect::>(); 501 | if let Some(sub) = self.default_subcommand() { 502 | res.extend(sub.args_with_default()); 503 | } 504 | res 505 | } 506 | fn flags_with_default(&self) -> Vec<&ast::Flag> { 507 | let mut res = self.flags.iter().collect::>(); 508 | if let Some(sub) = self.default_subcommand() { 509 | res.extend(sub.flags_with_default()) 510 | } 511 | res 512 | } 513 | } 514 | 515 | impl ast::Flag { 516 | fn ident(&self) -> String { 517 | snake(&self.name) 518 | } 519 | } 520 | 521 | impl ast::Arity { 522 | fn brackets(&self) -> (&str, &str) { 523 | match self { 524 | ast::Arity::Optional => ("[", "]"), 525 | ast::Arity::Required => ("<", ">"), 526 | ast::Arity::Repeated => ("[", "]..."), 527 | } 528 | } 529 | } 530 | 531 | impl ast::Val { 532 | fn ident(&self) -> String { 533 | snake(&self.name) 534 | } 535 | } 536 | 537 | fn blank_line(buf: &mut String) { 538 | w!(buf, "\n"); 539 | } 540 | 541 | fn camel(s: &str) -> String { 542 | s.split('-').map(first_upper).collect() 543 | } 544 | 545 | fn first_upper(s: &str) -> String { 546 | s.chars() 547 | .next() 548 | .map(|it| it.to_ascii_uppercase()) 549 | .into_iter() 550 | .chain(s.chars().skip(1)) 551 | .collect() 552 | } 553 | 554 | fn snake(s: &str) -> String { 555 | s.replace('-', "_") 556 | } 557 | 558 | #[cfg(test)] 559 | mod tests { 560 | use std::{ 561 | fs, 562 | io::Write, 563 | path::Path, 564 | process::{Command, Stdio}, 565 | }; 566 | 567 | fn reformat(text: String) -> Option { 568 | let mut rustfmt = 569 | Command::new("rustfmt").stdin(Stdio::piped()).stdout(Stdio::piped()).spawn().unwrap(); 570 | let mut stdin = rustfmt.stdin.take().unwrap(); 571 | stdin.write_all(text.as_bytes()).unwrap(); 572 | drop(stdin); 573 | let out = rustfmt.wait_with_output().unwrap(); 574 | let res = String::from_utf8(out.stdout).unwrap(); 575 | if res.is_empty() { 576 | None 577 | } else { 578 | Some(res) 579 | } 580 | } 581 | 582 | fn update_on_disk_if_different(file: &Path, new_contents: String) -> bool { 583 | let old_contents = fs::read_to_string(file).unwrap_or_default(); 584 | if old_contents.trim() == new_contents.trim() { 585 | return false; 586 | } 587 | fs::write(file, new_contents).unwrap(); 588 | true 589 | } 590 | 591 | #[test] 592 | fn gen_it() { 593 | let test_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests"); 594 | 595 | let mut did_update = false; 596 | for entry in fs::read_dir(test_dir.join("data")).unwrap() { 597 | let entry = entry.unwrap(); 598 | 599 | let text = fs::read_to_string(entry.path()).unwrap(); 600 | let mut lines = text.lines().collect::>(); 601 | lines.pop(); 602 | lines.remove(0); 603 | let text = lines.join("\n"); 604 | 605 | let res = crate::compile(&text); 606 | let fmt = reformat(res.clone()); 607 | 608 | let code = format!( 609 | "#![allow(dead_code)] // unused fields 610 | #[allow(unused)] 611 | use std::{{ffi::OsString, path::PathBuf}}; 612 | 613 | {}", 614 | fmt.as_deref().unwrap_or(&res) 615 | ); 616 | 617 | let name = entry.file_name(); 618 | did_update |= update_on_disk_if_different(&test_dir.join("it").join(name), code); 619 | 620 | if fmt.is_none() { 621 | panic!("syntax error"); 622 | } 623 | } 624 | if did_update { 625 | panic!("generated output changed") 626 | } 627 | } 628 | } 629 | -------------------------------------------------------------------------------- /crates/xflags-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod ast; 2 | mod parse; 3 | mod emit; 4 | mod update; 5 | 6 | #[proc_macro] 7 | pub fn xflags(_ts: proc_macro::TokenStream) -> proc_macro::TokenStream { 8 | // Stub out the code, but let rust-analyzer resolve the invocation 9 | #[cfg(not(test))] 10 | { 11 | let text = match parse::xflags(_ts) { 12 | Ok(cmd) => emit::emit(&cmd), 13 | Err(err) => format!("compile_error!(\"invalid flags syntax, {err}\");"), 14 | }; 15 | text.parse().unwrap() 16 | } 17 | #[cfg(test)] 18 | unimplemented!() 19 | } 20 | 21 | #[proc_macro] 22 | pub fn parse_or_exit(_ts: proc_macro::TokenStream) -> proc_macro::TokenStream { 23 | // Stub out the code, but let rust-analyzer resolve the invocation 24 | #[cfg(not(test))] 25 | { 26 | let text = match parse::parse_or_exit(_ts) { 27 | Ok(cmd) => emit::emit(&cmd), 28 | Err(err) => format!("compile_error!(\"invalid flags syntax, {err}\")"), 29 | }; 30 | text.parse().unwrap() 31 | } 32 | #[cfg(test)] 33 | { 34 | let _ = parse::parse_or_exit; 35 | unimplemented!(); 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | pub fn compile(src: &str) -> String { 41 | use proc_macro2::TokenStream; 42 | 43 | let ts = src.parse::().unwrap(); 44 | let cmd = parse::xflags(ts).unwrap(); 45 | emit::emit(&cmd) 46 | } 47 | -------------------------------------------------------------------------------- /crates/xflags-macros/src/parse.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, mem}; 2 | 3 | #[cfg(not(test))] 4 | use proc_macro::{Delimiter, TokenStream, TokenTree}; 5 | #[cfg(test)] 6 | use proc_macro2::{Delimiter, TokenStream, TokenTree}; 7 | 8 | use crate::ast; 9 | 10 | type Result = std::result::Result; 11 | 12 | #[derive(Debug)] 13 | pub(crate) struct Error { 14 | msg: String, 15 | } 16 | 17 | impl std::error::Error for Error {} 18 | 19 | impl fmt::Display for Error { 20 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 21 | fmt::Display::fmt(&self.msg, f) 22 | } 23 | } 24 | 25 | pub(crate) fn xflags(ts: TokenStream) -> Result { 26 | let p = &mut Parser::new(ts); 27 | let src = if p.eat_keyword("src") { Some(p.expect_string()?) } else { None }; 28 | let doc = opt_doc(p)?; 29 | let mut cmd = cmd(p)?; 30 | cmd.doc = doc; 31 | add_help(&mut cmd); 32 | let res = ast::XFlags { src, cmd }; 33 | Ok(res) 34 | } 35 | 36 | pub(crate) fn parse_or_exit(ts: TokenStream) -> Result { 37 | let p = &mut Parser::new(ts); 38 | let mut cmd = anon_cmd(p)?; 39 | assert!(cmd.subcommands.is_empty()); 40 | add_help(&mut cmd); 41 | let res = ast::XFlags { src: None, cmd }; 42 | Ok(res) 43 | } 44 | 45 | fn add_help(cmd: &mut ast::Cmd) { 46 | let help = ast::Flag { 47 | arity: ast::Arity::Optional, 48 | name: "help".to_string(), 49 | short: Some("h".to_string()), 50 | doc: Some("Prints help".to_string()), 51 | val: None, 52 | }; 53 | cmd.flags.push(help); 54 | } 55 | 56 | macro_rules! format_err { 57 | ($($tt:tt)*) => { 58 | Error { msg: format!($($tt)*) } 59 | // panic!($($tt)*) 60 | }; 61 | } 62 | 63 | macro_rules! bail { 64 | ($($tt:tt)*) => { 65 | return Err(format_err!($($tt)*)) 66 | }; 67 | } 68 | 69 | fn anon_cmd(p: &mut Parser) -> Result { 70 | cmd_impl(p, true) 71 | } 72 | 73 | fn cmd(p: &mut Parser) -> Result { 74 | cmd_impl(p, false) 75 | } 76 | 77 | fn cmd_impl(p: &mut Parser, anon: bool) -> Result { 78 | let name = if anon { 79 | String::new() 80 | } else { 81 | p.expect_keyword("cmd")?; 82 | cmd_name(p)? 83 | }; 84 | 85 | let aliases = alias_names(p); 86 | 87 | let idx = p.idx; 88 | p.idx += 1; 89 | 90 | let mut res = ast::Cmd { 91 | name, 92 | aliases, 93 | doc: None, 94 | args: Vec::new(), 95 | flags: Vec::new(), 96 | subcommands: Vec::new(), 97 | default: false, 98 | idx, 99 | }; 100 | 101 | if !anon { 102 | p.enter_delim(Delimiter::Brace)?; 103 | } 104 | while !p.end() { 105 | let doc = opt_doc(p)?; 106 | let default = !anon && p.eat_keyword("default"); 107 | if !anon && (default || p.at_keyword("cmd")) { 108 | let mut cmd = cmd(p)?; 109 | cmd.doc = doc; 110 | res.subcommands.push(cmd); 111 | if default { 112 | if res.default { 113 | bail!("only one subcommand can be default") 114 | } 115 | res.default = true; 116 | res.subcommands.rotate_right(1); 117 | } 118 | } else { 119 | let arity = arity(p)?; 120 | let is_val = p.lookahead_punct(':', 1); 121 | let name = p.expect_name()?; 122 | if name.starts_with('-') { 123 | let mut flag = flag(p, name)?; 124 | flag.doc = doc; 125 | flag.arity = arity; 126 | res.flags.push(flag) 127 | } else if is_val { 128 | p.expect_punct(':')?; 129 | let ty = ty(p)?; 130 | let val = ast::Val { name, ty }; 131 | let arg = ast::Arg { arity, doc, val }; 132 | res.args.push(arg); 133 | } else { 134 | bail!("expected `--flag` or `arg: Type`") 135 | } 136 | } 137 | } 138 | if !anon { 139 | p.exit_delim()?; 140 | } 141 | 142 | let mut unique_identifiers = std::collections::HashSet::new(); 143 | 144 | for ident in res.subcommands.iter().flat_map(|cmd| cmd.all_identifiers()) { 145 | if !unique_identifiers.insert(ident) { 146 | bail!("`{}` is defined multiple times", ident) 147 | } 148 | } 149 | 150 | Ok(res) 151 | } 152 | 153 | fn flag(p: &mut Parser, name: String) -> Result { 154 | let short; 155 | let long; 156 | if name.starts_with("--") { 157 | short = None; 158 | long = name; 159 | } else { 160 | short = Some(name); 161 | if !p.eat_punct(',') { 162 | bail!("long option is required for `{}`", short.unwrap()); 163 | } 164 | long = flag_name(p)?; 165 | if !long.starts_with("--") { 166 | bail!("long name must begin with `--`: `{long}`"); 167 | } 168 | } 169 | 170 | if long == "--help" { 171 | bail!("`--help` flag is generated automatically") 172 | } 173 | 174 | let val = opt_val(p)?; 175 | Ok(ast::Flag { 176 | arity: ast::Arity::Required, 177 | name: long[2..].to_string(), 178 | short: short.map(|it| it[1..].to_string()), 179 | doc: None, 180 | val, 181 | }) 182 | } 183 | 184 | fn opt_val(p: &mut Parser) -> Result, Error> { 185 | if !p.lookahead_punct(':', 1) { 186 | return Ok(None); 187 | } 188 | 189 | let name = p.expect_name()?; 190 | p.expect_punct(':')?; 191 | let ty = ty(p)?; 192 | let res = ast::Val { name, ty }; 193 | Ok(Some(res)) 194 | } 195 | 196 | fn arity(p: &mut Parser) -> Result { 197 | if p.eat_keyword("optional") { 198 | return Ok(ast::Arity::Optional); 199 | } 200 | if p.eat_keyword("required") { 201 | return Ok(ast::Arity::Required); 202 | } 203 | if p.eat_keyword("repeated") { 204 | return Ok(ast::Arity::Repeated); 205 | } 206 | if let Some(name) = p.eat_name() { 207 | bail!("expected one of `optional`, `required`, `repeated`, got `{name}`") 208 | } 209 | bail!("expected one of `optional`, `required`, `repeated`, got {:?}", p.ts.pop()) 210 | } 211 | 212 | fn ty(p: &mut Parser) -> Result { 213 | let name = p.expect_name()?; 214 | let res = match name.as_str() { 215 | "PathBuf" => ast::Ty::PathBuf, 216 | "OsString" => ast::Ty::OsString, 217 | _ => ast::Ty::FromStr(name), 218 | }; 219 | Ok(res) 220 | } 221 | 222 | fn opt_single_doc(p: &mut Parser) -> Result> { 223 | if !p.eat_punct('#') { 224 | return Ok(None); 225 | } 226 | p.enter_delim(Delimiter::Bracket)?; 227 | p.expect_keyword("doc")?; 228 | p.expect_punct('=')?; 229 | let mut res = p.expect_string()?; 230 | if let Some(suf) = res.strip_prefix(' ') { 231 | res = suf.to_string(); 232 | } 233 | p.exit_delim()?; 234 | Ok(Some(res)) 235 | } 236 | 237 | fn opt_doc(p: &mut Parser) -> Result> { 238 | let lines = 239 | core::iter::from_fn(|| opt_single_doc(p).transpose()).collect::>>()?; 240 | let lines = lines.join("\n"); 241 | 242 | if lines.is_empty() { 243 | Ok(None) 244 | } else { 245 | Ok(Some(lines)) 246 | } 247 | } 248 | 249 | fn cmd_name(p: &mut Parser) -> Result { 250 | let name = p.expect_name()?; 251 | if name.starts_with('-') { 252 | bail!("command name can't begin with `-`: `{name}`"); 253 | } 254 | Ok(name) 255 | } 256 | 257 | fn alias_names(p: &mut Parser) -> Vec { 258 | let mut aliases = vec![]; 259 | 260 | while let Some(alias) = p.eat_name() { 261 | aliases.push(alias); 262 | } 263 | 264 | aliases 265 | } 266 | 267 | fn flag_name(p: &mut Parser) -> Result { 268 | let name = p.expect_name()?; 269 | if !name.starts_with('-') { 270 | bail!("flag name should begin with `-`: `{name}`"); 271 | } 272 | Ok(name) 273 | } 274 | 275 | struct Parser { 276 | stack: Vec>, 277 | ts: Vec, 278 | idx: u8, 279 | } 280 | 281 | impl Parser { 282 | fn new(ts: TokenStream) -> Self { 283 | let mut ts = ts.into_iter().collect::>(); 284 | ts.reverse(); 285 | Self { stack: Vec::new(), ts, idx: 0 } 286 | } 287 | 288 | fn enter_delim(&mut self, delimiter: Delimiter) -> Result<()> { 289 | match self.ts.pop() { 290 | Some(TokenTree::Group(g)) if g.delimiter() == delimiter => { 291 | let mut ts = g.stream().into_iter().collect::>(); 292 | ts.reverse(); 293 | let ts = mem::replace(&mut self.ts, ts); 294 | self.stack.push(ts); 295 | } 296 | _ => bail!("expected `{{`"), 297 | } 298 | Ok(()) 299 | } 300 | fn exit_delim(&mut self) -> Result<()> { 301 | if !self.end() { 302 | bail!("expected `}}`") 303 | } 304 | self.ts = self.stack.pop().unwrap(); 305 | Ok(()) 306 | } 307 | fn end(&mut self) -> bool { 308 | self.ts.last().is_none() 309 | } 310 | 311 | fn expect_keyword(&mut self, kw: &str) -> Result<()> { 312 | if !self.eat_keyword(kw) { 313 | bail!("expected `{kw}`") 314 | } 315 | Ok(()) 316 | } 317 | fn eat_keyword(&mut self, kw: &str) -> bool { 318 | if self.at_keyword(kw) { 319 | self.ts.pop().unwrap(); 320 | true 321 | } else { 322 | false 323 | } 324 | } 325 | fn at_keyword(&mut self, kw: &str) -> bool { 326 | match self.ts.last() { 327 | #[allow(clippy::cmp_owned)] 328 | Some(TokenTree::Ident(ident)) => ident.to_string() == kw, 329 | _ => false, 330 | } 331 | } 332 | 333 | fn expect_name(&mut self) -> Result { 334 | self.eat_name().ok_or_else(|| { 335 | let next = self.ts.pop().map(|it| it.to_string()).unwrap_or_default(); 336 | format_err!("expected a name, got: `{next}`") 337 | }) 338 | } 339 | fn eat_name(&mut self) -> Option { 340 | let mut buf = String::new(); 341 | let mut prev_ident = false; 342 | loop { 343 | match self.ts.last() { 344 | Some(TokenTree::Punct(p)) if p.as_char() == '-' => { 345 | prev_ident = false; 346 | buf.push('-'); 347 | } 348 | Some(TokenTree::Ident(ident)) if !prev_ident => { 349 | prev_ident = true; 350 | buf.push_str(&ident.to_string()); 351 | } 352 | _ => break, 353 | } 354 | self.ts.pop(); 355 | } 356 | if buf.is_empty() { 357 | None 358 | } else { 359 | Some(buf) 360 | } 361 | } 362 | 363 | fn _expect_ident(&mut self) -> Result { 364 | match self.ts.pop() { 365 | Some(TokenTree::Ident(ident)) => Ok(ident.to_string()), 366 | _ => bail!("expected ident"), 367 | } 368 | } 369 | 370 | fn expect_punct(&mut self, punct: char) -> Result<()> { 371 | if !self.eat_punct(punct) { 372 | bail!("expected `{punct}`") 373 | } 374 | Ok(()) 375 | } 376 | fn eat_punct(&mut self, punct: char) -> bool { 377 | match self.ts.last() { 378 | Some(TokenTree::Punct(p)) if p.as_char() == punct => { 379 | self.ts.pop(); 380 | true 381 | } 382 | _ => false, 383 | } 384 | } 385 | fn lookahead_punct(&mut self, punct: char, n: usize) -> bool { 386 | match self.ts.iter().rev().nth(n) { 387 | Some(TokenTree::Punct(p)) => p.as_char() == punct, 388 | _ => false, 389 | } 390 | } 391 | 392 | fn expect_string(&mut self) -> Result { 393 | match self.ts.pop() { 394 | Some(TokenTree::Literal(lit)) if lit.to_string().starts_with('"') => { 395 | let res = str_lit_value(lit.to_string()); 396 | Ok(res) 397 | } 398 | _ => bail!("expected a string"), 399 | } 400 | } 401 | } 402 | 403 | /// "Parser" a string literal into the corresponding value. 404 | /// 405 | /// Really needs support in the proc_macro library: 406 | /// 407 | fn str_lit_value(lit: String) -> String { 408 | lit.trim_matches('"').replace("\\'", "'") 409 | } 410 | -------------------------------------------------------------------------------- /crates/xflags-macros/src/update.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, ops::Range, path::Path}; 2 | 3 | pub(crate) fn in_place(api: &str, path: &Path) { 4 | let path = { 5 | let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); 6 | Path::new(&dir).join(path) 7 | }; 8 | 9 | let mut text = fs::read_to_string(&path).unwrap_or_else(|_| panic!("failed to read {path:?}")); 10 | 11 | let (insert_to, indent) = locate(&text); 12 | 13 | let api: String = 14 | with_preamble(api) 15 | .lines() 16 | .map(|it| { 17 | if it.trim().is_empty() { 18 | "\n".to_string() 19 | } else { 20 | format!("{}{}\n", indent, it) 21 | } 22 | }) 23 | .collect(); 24 | text.replace_range(insert_to, &api); 25 | 26 | fs::write(&path, text.as_bytes()).unwrap(); 27 | } 28 | 29 | pub(crate) fn stdout(api: &str) { 30 | print!("{}", with_preamble(api)) 31 | } 32 | 33 | fn with_preamble(api: &str) -> String { 34 | format!( 35 | "\ 36 | // generated start 37 | // The following code is generated by `xflags` macro. 38 | // Run `env UPDATE_XFLAGS=1 cargo build` to regenerate. 39 | {} 40 | // generated end 41 | ", 42 | api.trim() 43 | ) 44 | } 45 | 46 | fn locate(text: &str) -> (Range, String) { 47 | if let Some(it) = locate_existing(text) { 48 | return it; 49 | } 50 | if let Some(it) = locate_new(text) { 51 | return it; 52 | } 53 | panic!("failed to update xflags in place") 54 | } 55 | 56 | fn locate_existing(text: &str) -> Option<(Range, String)> { 57 | let start_idx = text.find("// generated start")?; 58 | let start_idx = newline_before(text, start_idx); 59 | 60 | let end_idx = text.find("// generated end")?; 61 | let end_idx = newline_after(text, end_idx); 62 | 63 | let indent = indent_at(text, start_idx); 64 | 65 | Some((start_idx..end_idx, indent)) 66 | } 67 | 68 | fn newline_before(text: &str, start_idx: usize) -> usize { 69 | text[..start_idx].rfind('\n').map_or(start_idx, |it| it + 1) 70 | } 71 | 72 | fn newline_after(text: &str, start_idx: usize) -> usize { 73 | start_idx + text[start_idx..].find('\n').map_or(text[start_idx..].len(), |it| it + 1) 74 | } 75 | 76 | fn indent_at(text: &str, start_idx: usize) -> String { 77 | text[start_idx..].chars().take_while(|&it| it == ' ').collect() 78 | } 79 | 80 | fn locate_new(text: &str) -> Option<(Range, String)> { 81 | let mut idx = text.find("xflags!")?; 82 | let mut lvl = 0i32; 83 | for c in text[idx..].chars() { 84 | idx += c.len_utf8(); 85 | match c { 86 | '{' => lvl += 1, 87 | '}' if lvl == 1 => break, 88 | '}' => lvl -= 1, 89 | _ => (), 90 | } 91 | } 92 | let indent = indent_at(text, newline_before(text, idx)); 93 | if text[idx..].starts_with('\n') { 94 | idx += 1; 95 | } 96 | Some((idx..idx, indent)) 97 | } 98 | -------------------------------------------------------------------------------- /crates/xflags-macros/tests/data/aliases.rs: -------------------------------------------------------------------------------- 1 | xflags! { 2 | /// commands with different aliases 3 | cmd alias-cmd { 4 | /// And even an aliased subcommand! 5 | cmd sub s { 6 | /// Little sanity check to see if this still works as intended 7 | optional -c, --count count: usize 8 | } 9 | cmd this one has a lot of aliases {} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /crates/xflags-macros/tests/data/empty.rs: -------------------------------------------------------------------------------- 1 | xflags! { 2 | cmd empty { 3 | } 4 | } 5 | -------------------------------------------------------------------------------- /crates/xflags-macros/tests/data/help.rs: -------------------------------------------------------------------------------- 1 | xflags! { 2 | /// Does stuff 3 | /// 4 | /// Helpful stuff. 5 | cmd helpful { 6 | /// With an arg. 7 | optional src: PathBuf 8 | 9 | /// Another arg. 10 | /// 11 | /// This time, we provide some extra info about the 12 | /// arg. Maybe some caveats, or what kinds of 13 | /// values are accepted. 14 | optional extra: String 15 | 16 | /// And a switch. 17 | required -s, --switch 18 | 19 | /// And even a subcommand! 20 | cmd sub { 21 | /// With an optional flag. This has a really long 22 | /// description which spans multiple lines. 23 | optional -f, --flag 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/xflags-macros/tests/data/repeated_pos.rs: -------------------------------------------------------------------------------- 1 | xflags! { 2 | cmd RepeatedPos { 3 | required a: PathBuf 4 | optional b: u32 5 | optional c: OsString 6 | repeated rest: OsString 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /crates/xflags-macros/tests/data/smoke.rs: -------------------------------------------------------------------------------- 1 | xflags! { 2 | /// LSP server for rust. 3 | cmd rust-analyzer { 4 | required workspace: PathBuf 5 | /// Number of concurrent jobs. 6 | optional jobs: u32 7 | /// Path to log file. By default, logs go to stderr. 8 | optional --log-file path: PathBuf 9 | repeated -v, --verbose 10 | required -n, --number n: u32 11 | repeated --data value: OsString 12 | optional --emoji 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/xflags-macros/tests/data/subcommands.rs: -------------------------------------------------------------------------------- 1 | xflags! { 2 | cmd rust-analyzer { 3 | repeated -v, --verbose 4 | 5 | cmd server { 6 | optional --dir path:PathBuf 7 | default cmd launch { 8 | optional --log 9 | } 10 | cmd watch { 11 | } 12 | } 13 | 14 | cmd analysis-stats { 15 | required path: PathBuf 16 | optional --parallel 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/xflags-macros/tests/it/aliases.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] // unused fields 2 | #[allow(unused)] 3 | use std::{ffi::OsString, path::PathBuf}; 4 | 5 | #[derive(Debug)] 6 | pub struct AliasCmd { 7 | pub subcommand: AliasCmdCmd, 8 | } 9 | 10 | #[derive(Debug)] 11 | pub enum AliasCmdCmd { 12 | Sub(Sub), 13 | This(This), 14 | } 15 | 16 | #[derive(Debug)] 17 | pub struct Sub { 18 | pub count: Option, 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct This; 23 | 24 | impl AliasCmd { 25 | #[allow(dead_code)] 26 | pub fn from_env_or_exit() -> Self { 27 | Self::from_env_or_exit_() 28 | } 29 | 30 | #[allow(dead_code)] 31 | pub fn from_env() -> xflags::Result { 32 | Self::from_env_() 33 | } 34 | 35 | #[allow(dead_code)] 36 | pub fn from_vec(args: Vec) -> xflags::Result { 37 | Self::from_vec_(args) 38 | } 39 | } 40 | 41 | impl AliasCmd { 42 | fn from_env_or_exit_() -> Self { 43 | Self::from_env_().unwrap_or_else(|err| err.exit()) 44 | } 45 | fn from_env_() -> xflags::Result { 46 | let mut p = xflags::rt::Parser::new_from_env(); 47 | Self::parse_(&mut p) 48 | } 49 | fn from_vec_(args: Vec) -> xflags::Result { 50 | let mut p = xflags::rt::Parser::new(args); 51 | Self::parse_(&mut p) 52 | } 53 | } 54 | 55 | impl AliasCmd { 56 | fn parse_(p_: &mut xflags::rt::Parser) -> xflags::Result { 57 | #![allow(non_snake_case, unused_mut)] 58 | let mut sub__count = Vec::new(); 59 | 60 | let mut state_ = 0u8; 61 | while let Some(arg_) = p_.pop_flag() { 62 | match arg_ { 63 | Ok(flag_) => match (state_, flag_.as_str()) { 64 | (0, "--help" | "-h") => return Err(p_.help(Self::HELP_)), 65 | (1, "--help" | "-h") => return Err(p_.help(Self::HELP_SUB__)), 66 | (1, "--count" | "-c") => { 67 | sub__count.push(p_.next_value_from_str::(&flag_)?) 68 | } 69 | (2, "--help" | "-h") => return Err(p_.help(Self::HELP_THIS__)), 70 | _ => return Err(p_.unexpected_flag(&flag_)), 71 | }, 72 | Err(arg_) => match (state_, arg_.to_str().unwrap_or("")) { 73 | (0, "sub" | "s") => state_ = 1, 74 | (0, "this" | "one" | "has" | "a" | "lot" | "of" | "aliases") => state_ = 2, 75 | (0, "help") => return Err(p_.help(Self::HELP_)), 76 | (0, _) => { 77 | return Err(p_.unexpected_arg(arg_)); 78 | } 79 | (1, "help") => return Err(p_.help(Self::HELP_SUB__)), 80 | (2, "help") => return Err(p_.help(Self::HELP_THIS__)), 81 | _ => return Err(p_.unexpected_arg(arg_)), 82 | }, 83 | } 84 | } 85 | Ok(AliasCmd { 86 | subcommand: match state_ { 87 | 1 => AliasCmdCmd::Sub(Sub { count: p_.optional("--count", sub__count)? }), 88 | 2 => AliasCmdCmd::This(This {}), 89 | _ => return Err(p_.subcommand_required()), 90 | }, 91 | }) 92 | } 93 | } 94 | impl AliasCmd { 95 | const HELP_SUB__: &'static str = "Usage: sub [-c ] 96 | 97 | And even an aliased subcommand! 98 | 99 | Options: 100 | -c, --count Little sanity check to see if this still works as intended 101 | 102 | Commands: 103 | help Print this message or the help of the given subcommand(s)"; 104 | const HELP_THIS__: &'static str = "Usage: this 105 | Commands: 106 | help Print this message or the help of the given subcommand(s)"; 107 | const HELP_: &'static str = "Usage: alias-cmd [-h] 108 | 109 | commands with different aliases 110 | 111 | Options: 112 | -h, --help Prints help 113 | 114 | Commands: 115 | sub And even an aliased subcommand! 116 | this 117 | help Print this message or the help of the given subcommand(s)"; 118 | } 119 | -------------------------------------------------------------------------------- /crates/xflags-macros/tests/it/empty.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] // unused fields 2 | #[allow(unused)] 3 | use std::{ffi::OsString, path::PathBuf}; 4 | 5 | #[derive(Debug)] 6 | pub struct Empty; 7 | 8 | impl Empty { 9 | #[allow(dead_code)] 10 | pub fn from_env_or_exit() -> Self { 11 | Self::from_env_or_exit_() 12 | } 13 | 14 | #[allow(dead_code)] 15 | pub fn from_env() -> xflags::Result { 16 | Self::from_env_() 17 | } 18 | 19 | #[allow(dead_code)] 20 | pub fn from_vec(args: Vec) -> xflags::Result { 21 | Self::from_vec_(args) 22 | } 23 | } 24 | 25 | impl Empty { 26 | fn from_env_or_exit_() -> Self { 27 | Self::from_env_().unwrap_or_else(|err| err.exit()) 28 | } 29 | fn from_env_() -> xflags::Result { 30 | let mut p = xflags::rt::Parser::new_from_env(); 31 | Self::parse_(&mut p) 32 | } 33 | fn from_vec_(args: Vec) -> xflags::Result { 34 | let mut p = xflags::rt::Parser::new(args); 35 | Self::parse_(&mut p) 36 | } 37 | } 38 | 39 | impl Empty { 40 | fn parse_(p_: &mut xflags::rt::Parser) -> xflags::Result { 41 | #![allow(non_snake_case, unused_mut)] 42 | 43 | let mut state_ = 0u8; 44 | if let Some(arg_) = p_.pop_flag() { 45 | match arg_ { 46 | Ok(flag_) => match (state_, flag_.as_str()) { 47 | (0, "--help" | "-h") => return Err(p_.help(Self::HELP_)), 48 | _ => return Err(p_.unexpected_flag(&flag_)), 49 | }, 50 | Err(arg_) => match (state_, arg_.to_str().unwrap_or("")) { 51 | (0, "help") => return Err(p_.help(Self::HELP_)), 52 | _ => return Err(p_.unexpected_arg(arg_)), 53 | }, 54 | } 55 | } 56 | Ok(Empty {}) 57 | } 58 | } 59 | impl Empty { 60 | const HELP_: &'static str = "Usage: empty [-h] 61 | Options: 62 | -h, --help Prints help 63 | 64 | Commands: 65 | help Print this message or the help of the given subcommand(s)"; 66 | } 67 | -------------------------------------------------------------------------------- /crates/xflags-macros/tests/it/help.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] // unused fields 2 | #[allow(unused)] 3 | use std::{ffi::OsString, path::PathBuf}; 4 | 5 | #[derive(Debug)] 6 | pub struct Helpful { 7 | pub src: Option, 8 | pub extra: Option, 9 | 10 | pub switch: (), 11 | pub subcommand: HelpfulCmd, 12 | } 13 | 14 | #[derive(Debug)] 15 | pub enum HelpfulCmd { 16 | Sub(Sub), 17 | } 18 | 19 | #[derive(Debug)] 20 | pub struct Sub { 21 | pub flag: bool, 22 | } 23 | 24 | impl Helpful { 25 | #[allow(dead_code)] 26 | pub fn from_env_or_exit() -> Self { 27 | Self::from_env_or_exit_() 28 | } 29 | 30 | #[allow(dead_code)] 31 | pub fn from_env() -> xflags::Result { 32 | Self::from_env_() 33 | } 34 | 35 | #[allow(dead_code)] 36 | pub fn from_vec(args: Vec) -> xflags::Result { 37 | Self::from_vec_(args) 38 | } 39 | } 40 | 41 | impl Helpful { 42 | fn from_env_or_exit_() -> Self { 43 | Self::from_env_().unwrap_or_else(|err| err.exit()) 44 | } 45 | fn from_env_() -> xflags::Result { 46 | let mut p = xflags::rt::Parser::new_from_env(); 47 | Self::parse_(&mut p) 48 | } 49 | fn from_vec_(args: Vec) -> xflags::Result { 50 | let mut p = xflags::rt::Parser::new(args); 51 | Self::parse_(&mut p) 52 | } 53 | } 54 | 55 | impl Helpful { 56 | fn parse_(p_: &mut xflags::rt::Parser) -> xflags::Result { 57 | #![allow(non_snake_case, unused_mut)] 58 | let mut switch = Vec::new(); 59 | let mut src = (false, Vec::new()); 60 | let mut extra = (false, Vec::new()); 61 | let mut sub__flag = Vec::new(); 62 | 63 | let mut state_ = 0u8; 64 | while let Some(arg_) = p_.pop_flag() { 65 | match arg_ { 66 | Ok(flag_) => match (state_, flag_.as_str()) { 67 | (0, "--help" | "-h") => return Err(p_.help(Self::HELP_)), 68 | (0 | 1, "--switch" | "-s") => switch.push(()), 69 | (1, "--help" | "-h") => return Err(p_.help(Self::HELP_SUB__)), 70 | (1, "--flag" | "-f") => sub__flag.push(()), 71 | _ => return Err(p_.unexpected_flag(&flag_)), 72 | }, 73 | Err(arg_) => match (state_, arg_.to_str().unwrap_or("")) { 74 | (0, "sub") => state_ = 1, 75 | (0, _) => { 76 | if let (done_ @ false, buf_) = &mut src { 77 | buf_.push(arg_.into()); 78 | *done_ = true; 79 | continue; 80 | } 81 | if let (done_ @ false, buf_) = &mut extra { 82 | buf_.push(p_.value_from_str::("extra", arg_)?); 83 | *done_ = true; 84 | continue; 85 | } 86 | return Err(p_.unexpected_arg(arg_)); 87 | } 88 | (1, "help") => return Err(p_.help(Self::HELP_SUB__)), 89 | _ => return Err(p_.unexpected_arg(arg_)), 90 | }, 91 | } 92 | } 93 | Ok(Helpful { 94 | switch: p_.required("--switch", switch)?, 95 | src: p_.optional("src", src.1)?, 96 | extra: p_.optional("extra", extra.1)?, 97 | subcommand: match state_ { 98 | 1 => HelpfulCmd::Sub(Sub { flag: p_.optional("--flag", sub__flag)?.is_some() }), 99 | _ => return Err(p_.subcommand_required()), 100 | }, 101 | }) 102 | } 103 | } 104 | impl Helpful { 105 | const HELP_SUB__: &'static str = "Usage: sub [-f] 106 | 107 | And even a subcommand! 108 | 109 | Options: 110 | -f, --flag With an optional flag. This has a really long 111 | description which spans multiple lines. 112 | 113 | Commands: 114 | help Print this message or the help of the given subcommand(s)"; 115 | const HELP_: &'static str = "Usage: helpful [src] [extra] -s [-h] 116 | 117 | Does stuff 118 | 119 | Helpful stuff. 120 | 121 | Arguments: 122 | [src] With an arg. 123 | [extra] Another arg. 124 | 125 | This time, we provide some extra info about the 126 | arg. Maybe some caveats, or what kinds of 127 | values are accepted. 128 | 129 | Options: 130 | -s, --switch And a switch. 131 | -h, --help Prints help 132 | 133 | Commands: 134 | sub And even a subcommand! 135 | help Print this message or the help of the given subcommand(s)"; 136 | } 137 | -------------------------------------------------------------------------------- /crates/xflags-macros/tests/it/main.rs: -------------------------------------------------------------------------------- 1 | mod empty; 2 | mod smoke; 3 | mod repeated_pos; 4 | mod subcommands; 5 | mod help; 6 | 7 | use std::{ffi::OsString, fmt}; 8 | 9 | use expect_test::{expect, Expect}; 10 | 11 | fn check(f: F, args: &str, expect: Expect) 12 | where 13 | F: FnOnce(Vec) -> xflags::Result, 14 | A: fmt::Debug, 15 | { 16 | let args = args.split_ascii_whitespace().map(OsString::from).collect::>(); 17 | let res = f(args); 18 | match res { 19 | Ok(args) => { 20 | expect.assert_debug_eq(&args); 21 | } 22 | Err(err) => { 23 | expect.assert_eq(&err.to_string()); 24 | } 25 | } 26 | } 27 | 28 | #[test] 29 | fn empty() { 30 | check( 31 | empty::Empty::from_vec, 32 | "", 33 | expect![[r#" 34 | Empty 35 | "#]], 36 | ) 37 | } 38 | 39 | #[test] 40 | fn smoke() { 41 | check( 42 | smoke::RustAnalyzer::from_vec, 43 | "-n 92 .", 44 | expect![[r#" 45 | RustAnalyzer { 46 | workspace: ".", 47 | jobs: None, 48 | log_file: None, 49 | verbose: 0, 50 | number: 92, 51 | data: [], 52 | emoji: false, 53 | } 54 | "#]], 55 | ); 56 | check( 57 | smoke::RustAnalyzer::from_vec, 58 | "-n 92 -v --verbose -v --data 0xDEAD --log-file /tmp/log.txt --data 0xBEEF .", 59 | expect![[r#" 60 | RustAnalyzer { 61 | workspace: ".", 62 | jobs: None, 63 | log_file: Some( 64 | "/tmp/log.txt", 65 | ), 66 | verbose: 3, 67 | number: 92, 68 | data: [ 69 | "0xDEAD", 70 | "0xBEEF", 71 | ], 72 | emoji: false, 73 | } 74 | "#]], 75 | ); 76 | 77 | check( 78 | smoke::RustAnalyzer::from_vec, 79 | "-n 92 --werbose", 80 | expect!["Unknown flag: `--werbose`. Use `help` for more information"], 81 | ); 82 | check( 83 | smoke::RustAnalyzer::from_vec, 84 | "", 85 | expect!["Flag is required: `--number`. Use `help` for more information"], 86 | ); 87 | check( 88 | smoke::RustAnalyzer::from_vec, 89 | ".", 90 | expect!["Flag is required: `--number`. Use `help` for more information"], 91 | ); 92 | check(smoke::RustAnalyzer::from_vec, "-n", expect![[r#"expected a value for `-n`"#]]); 93 | check( 94 | smoke::RustAnalyzer::from_vec, 95 | "-n 92", 96 | expect!["Flag is required: `workspace`. Use `help` for more information"], 97 | ); 98 | check( 99 | smoke::RustAnalyzer::from_vec, 100 | "-n lol", 101 | expect!["Can't parse `-n`, invalid digit found in string"], 102 | ); 103 | check( 104 | smoke::RustAnalyzer::from_vec, 105 | "-n 1 -n 2 .", 106 | expect!["Flag specified more than once: `--number`"], 107 | ); 108 | check( 109 | smoke::RustAnalyzer::from_vec, 110 | "-n 1 . 92 lol", 111 | expect!["Unknown command: `lol`. Use `help` for more information"], 112 | ); 113 | check( 114 | smoke::RustAnalyzer::from_vec, 115 | "-n 1 . --emoji --emoji", 116 | expect!["Flag specified more than once: `--emoji`"], 117 | ); 118 | } 119 | 120 | #[test] 121 | fn repeated_argument() { 122 | check( 123 | repeated_pos::RepeatedPos::from_vec, 124 | "a 11 c d e f", 125 | expect![[r#" 126 | RepeatedPos { 127 | a: "a", 128 | b: Some( 129 | 11, 130 | ), 131 | c: Some( 132 | "c", 133 | ), 134 | rest: [ 135 | "d", 136 | "e", 137 | "f", 138 | ], 139 | } 140 | "#]], 141 | ); 142 | } 143 | 144 | #[test] 145 | fn subcommands() { 146 | check( 147 | subcommands::RustAnalyzer::from_vec, 148 | "server", 149 | expect![[r#" 150 | RustAnalyzer { 151 | verbose: 0, 152 | subcommand: Server( 153 | Server { 154 | dir: None, 155 | subcommand: Launch( 156 | Launch { 157 | log: false, 158 | }, 159 | ), 160 | }, 161 | ), 162 | } 163 | "#]], 164 | ); 165 | 166 | check( 167 | subcommands::RustAnalyzer::from_vec, 168 | "server --dir . --log", 169 | expect![[r#" 170 | RustAnalyzer { 171 | verbose: 0, 172 | subcommand: Server( 173 | Server { 174 | dir: Some( 175 | ".", 176 | ), 177 | subcommand: Launch( 178 | Launch { 179 | log: true, 180 | }, 181 | ), 182 | }, 183 | ), 184 | } 185 | "#]], 186 | ); 187 | 188 | check( 189 | subcommands::RustAnalyzer::from_vec, 190 | "server watch", 191 | expect![[r#" 192 | RustAnalyzer { 193 | verbose: 0, 194 | subcommand: Server( 195 | Server { 196 | dir: None, 197 | subcommand: Watch( 198 | Watch, 199 | ), 200 | }, 201 | ), 202 | } 203 | "#]], 204 | ); 205 | 206 | check( 207 | subcommands::RustAnalyzer::from_vec, 208 | "-v analysis-stats . --parallel", 209 | expect![[r#" 210 | RustAnalyzer { 211 | verbose: 1, 212 | subcommand: AnalysisStats( 213 | AnalysisStats { 214 | path: ".", 215 | parallel: true, 216 | }, 217 | ), 218 | } 219 | "#]], 220 | ); 221 | 222 | check( 223 | subcommands::RustAnalyzer::from_vec, 224 | "", 225 | expect!["A subcommand is required. Use `help` for more information"], 226 | ); 227 | } 228 | 229 | #[test] 230 | fn subcommand_flag_inheritance() { 231 | check( 232 | subcommands::RustAnalyzer::from_vec, 233 | "server watch --verbose --dir .", 234 | expect![[r#" 235 | RustAnalyzer { 236 | verbose: 1, 237 | subcommand: Server( 238 | Server { 239 | dir: Some( 240 | ".", 241 | ), 242 | subcommand: Watch( 243 | Watch, 244 | ), 245 | }, 246 | ), 247 | } 248 | "#]], 249 | ); 250 | check( 251 | subcommands::RustAnalyzer::from_vec, 252 | "analysis-stats --verbose --dir .", 253 | expect!["Unknown flag: `--dir`. Use `help` for more information"], 254 | ); 255 | check( 256 | subcommands::RustAnalyzer::from_vec, 257 | "--dir . server", 258 | expect!["Unknown flag: `--dir`. Use `help` for more information"], 259 | ); 260 | } 261 | 262 | #[test] 263 | fn edge_cases() { 264 | check( 265 | subcommands::RustAnalyzer::from_vec, 266 | "server --dir --log", 267 | expect![[r#" 268 | RustAnalyzer { 269 | verbose: 0, 270 | subcommand: Server( 271 | Server { 272 | dir: Some( 273 | "--log", 274 | ), 275 | subcommand: Launch( 276 | Launch { 277 | log: false, 278 | }, 279 | ), 280 | }, 281 | ), 282 | } 283 | "#]], 284 | ); 285 | check( 286 | subcommands::RustAnalyzer::from_vec, 287 | "server --dir -- --log", 288 | expect![[r#" 289 | RustAnalyzer { 290 | verbose: 0, 291 | subcommand: Server( 292 | Server { 293 | dir: Some( 294 | "--", 295 | ), 296 | subcommand: Launch( 297 | Launch { 298 | log: true, 299 | }, 300 | ), 301 | }, 302 | ), 303 | } 304 | "#]], 305 | ); 306 | check( 307 | subcommands::RustAnalyzer::from_vec, 308 | "-- -v server", 309 | expect!["Unknown command: `-v`. Use `help` for more information"], 310 | ); 311 | check( 312 | repeated_pos::RepeatedPos::from_vec, 313 | "pos 1 prog -j", 314 | expect!["Unknown flag: `-j`. Use `help` for more information"], 315 | ); 316 | check( 317 | repeated_pos::RepeatedPos::from_vec, 318 | "pos 1 -- prog -j", 319 | expect![[r#" 320 | RepeatedPos { 321 | a: "pos", 322 | b: Some( 323 | 1, 324 | ), 325 | c: Some( 326 | "prog", 327 | ), 328 | rest: [ 329 | "-j", 330 | ], 331 | } 332 | "#]], 333 | ); 334 | } 335 | -------------------------------------------------------------------------------- /crates/xflags-macros/tests/it/repeated_pos.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] // unused fields 2 | #[allow(unused)] 3 | use std::{ffi::OsString, path::PathBuf}; 4 | 5 | #[derive(Debug)] 6 | pub struct RepeatedPos { 7 | pub a: PathBuf, 8 | pub b: Option, 9 | pub c: Option, 10 | pub rest: Vec, 11 | } 12 | 13 | impl RepeatedPos { 14 | #[allow(dead_code)] 15 | pub fn from_env_or_exit() -> Self { 16 | Self::from_env_or_exit_() 17 | } 18 | 19 | #[allow(dead_code)] 20 | pub fn from_env() -> xflags::Result { 21 | Self::from_env_() 22 | } 23 | 24 | #[allow(dead_code)] 25 | pub fn from_vec(args: Vec) -> xflags::Result { 26 | Self::from_vec_(args) 27 | } 28 | } 29 | 30 | impl RepeatedPos { 31 | fn from_env_or_exit_() -> Self { 32 | Self::from_env_().unwrap_or_else(|err| err.exit()) 33 | } 34 | fn from_env_() -> xflags::Result { 35 | let mut p = xflags::rt::Parser::new_from_env(); 36 | Self::parse_(&mut p) 37 | } 38 | fn from_vec_(args: Vec) -> xflags::Result { 39 | let mut p = xflags::rt::Parser::new(args); 40 | Self::parse_(&mut p) 41 | } 42 | } 43 | 44 | impl RepeatedPos { 45 | fn parse_(p_: &mut xflags::rt::Parser) -> xflags::Result { 46 | #![allow(non_snake_case, unused_mut)] 47 | let mut a = (false, Vec::new()); 48 | let mut b = (false, Vec::new()); 49 | let mut c = (false, Vec::new()); 50 | let mut rest = (false, Vec::new()); 51 | 52 | let mut state_ = 0u8; 53 | while let Some(arg_) = p_.pop_flag() { 54 | match arg_ { 55 | Ok(flag_) => match (state_, flag_.as_str()) { 56 | (0, "--help" | "-h") => return Err(p_.help(Self::HELP_)), 57 | _ => return Err(p_.unexpected_flag(&flag_)), 58 | }, 59 | Err(arg_) => match (state_, arg_.to_str().unwrap_or("")) { 60 | (0, _) => { 61 | if let (done_ @ false, buf_) = &mut a { 62 | buf_.push(arg_.into()); 63 | *done_ = true; 64 | continue; 65 | } 66 | if let (done_ @ false, buf_) = &mut b { 67 | buf_.push(p_.value_from_str::("b", arg_)?); 68 | *done_ = true; 69 | continue; 70 | } 71 | if let (done_ @ false, buf_) = &mut c { 72 | buf_.push(arg_.into()); 73 | *done_ = true; 74 | continue; 75 | } 76 | if let (false, buf_) = &mut rest { 77 | buf_.push(arg_.into()); 78 | continue; 79 | } 80 | return Err(p_.unexpected_arg(arg_)); 81 | } 82 | _ => return Err(p_.unexpected_arg(arg_)), 83 | }, 84 | } 85 | } 86 | Ok(RepeatedPos { 87 | a: p_.required("a", a.1)?, 88 | b: p_.optional("b", b.1)?, 89 | c: p_.optional("c", c.1)?, 90 | rest: rest.1, 91 | }) 92 | } 93 | } 94 | impl RepeatedPos { 95 | const HELP_: &'static str = "Usage: RepeatedPos [b] [c] [rest]... [-h] 96 | Arguments: 97 | 98 | [b] 99 | [c] 100 | [rest]... 101 | 102 | Options: 103 | -h, --help Prints help 104 | 105 | Commands: 106 | help Print this message or the help of the given subcommand(s)"; 107 | } 108 | -------------------------------------------------------------------------------- /crates/xflags-macros/tests/it/smoke.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] // unused fields 2 | #[allow(unused)] 3 | use std::{ffi::OsString, path::PathBuf}; 4 | 5 | #[derive(Debug)] 6 | pub struct RustAnalyzer { 7 | pub workspace: PathBuf, 8 | pub jobs: Option, 9 | 10 | pub log_file: Option, 11 | pub verbose: u32, 12 | pub number: u32, 13 | pub data: Vec, 14 | pub emoji: bool, 15 | } 16 | 17 | impl RustAnalyzer { 18 | #[allow(dead_code)] 19 | pub fn from_env_or_exit() -> Self { 20 | Self::from_env_or_exit_() 21 | } 22 | 23 | #[allow(dead_code)] 24 | pub fn from_env() -> xflags::Result { 25 | Self::from_env_() 26 | } 27 | 28 | #[allow(dead_code)] 29 | pub fn from_vec(args: Vec) -> xflags::Result { 30 | Self::from_vec_(args) 31 | } 32 | } 33 | 34 | impl RustAnalyzer { 35 | fn from_env_or_exit_() -> Self { 36 | Self::from_env_().unwrap_or_else(|err| err.exit()) 37 | } 38 | fn from_env_() -> xflags::Result { 39 | let mut p = xflags::rt::Parser::new_from_env(); 40 | Self::parse_(&mut p) 41 | } 42 | fn from_vec_(args: Vec) -> xflags::Result { 43 | let mut p = xflags::rt::Parser::new(args); 44 | Self::parse_(&mut p) 45 | } 46 | } 47 | 48 | impl RustAnalyzer { 49 | fn parse_(p_: &mut xflags::rt::Parser) -> xflags::Result { 50 | #![allow(non_snake_case, unused_mut)] 51 | let mut log_file = Vec::new(); 52 | let mut verbose = Vec::new(); 53 | let mut number = Vec::new(); 54 | let mut data = Vec::new(); 55 | let mut emoji = Vec::new(); 56 | let mut workspace = (false, Vec::new()); 57 | let mut jobs = (false, Vec::new()); 58 | 59 | let mut state_ = 0u8; 60 | while let Some(arg_) = p_.pop_flag() { 61 | match arg_ { 62 | Ok(flag_) => match (state_, flag_.as_str()) { 63 | (0, "--help" | "-h") => return Err(p_.help(Self::HELP_)), 64 | (0, "--log-file") => log_file.push(p_.next_value(&flag_)?.into()), 65 | (0, "--verbose" | "-v") => verbose.push(()), 66 | (0, "--number" | "-n") => number.push(p_.next_value_from_str::(&flag_)?), 67 | (0, "--data") => data.push(p_.next_value(&flag_)?.into()), 68 | (0, "--emoji") => emoji.push(()), 69 | _ => return Err(p_.unexpected_flag(&flag_)), 70 | }, 71 | Err(arg_) => match (state_, arg_.to_str().unwrap_or("")) { 72 | (0, _) => { 73 | if let (done_ @ false, buf_) = &mut workspace { 74 | buf_.push(arg_.into()); 75 | *done_ = true; 76 | continue; 77 | } 78 | if let (done_ @ false, buf_) = &mut jobs { 79 | buf_.push(p_.value_from_str::("jobs", arg_)?); 80 | *done_ = true; 81 | continue; 82 | } 83 | return Err(p_.unexpected_arg(arg_)); 84 | } 85 | _ => return Err(p_.unexpected_arg(arg_)), 86 | }, 87 | } 88 | } 89 | Ok(RustAnalyzer { 90 | log_file: p_.optional("--log-file", log_file)?, 91 | verbose: verbose.len() as u32, 92 | number: p_.required("--number", number)?, 93 | data: data, 94 | emoji: p_.optional("--emoji", emoji)?.is_some(), 95 | workspace: p_.required("workspace", workspace.1)?, 96 | jobs: p_.optional("jobs", jobs.1)?, 97 | }) 98 | } 99 | } 100 | impl RustAnalyzer { 101 | const HELP_: &'static str = "Usage: rust-analyzer [jobs] [--log-file ] [-v]... -n [--data ]... [--emoji] [-h] 102 | 103 | LSP server for rust. 104 | 105 | Arguments: 106 | 107 | [jobs] Number of concurrent jobs. 108 | 109 | Options: 110 | --log-file Path to log file. By default, logs go to stderr. 111 | -v, --verbose 112 | -n, --number 113 | --data 114 | --emoji 115 | -h, --help Prints help 116 | 117 | Commands: 118 | help Print this message or the help of the given subcommand(s)"; 119 | } 120 | -------------------------------------------------------------------------------- /crates/xflags-macros/tests/it/subcommands.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] // unused fields 2 | #[allow(unused)] 3 | use std::{ffi::OsString, path::PathBuf}; 4 | 5 | #[derive(Debug)] 6 | pub struct RustAnalyzer { 7 | pub verbose: u32, 8 | pub subcommand: RustAnalyzerCmd, 9 | } 10 | 11 | #[derive(Debug)] 12 | pub enum RustAnalyzerCmd { 13 | Server(Server), 14 | AnalysisStats(AnalysisStats), 15 | } 16 | 17 | #[derive(Debug)] 18 | pub struct Server { 19 | pub dir: Option, 20 | pub subcommand: ServerCmd, 21 | } 22 | 23 | #[derive(Debug)] 24 | pub enum ServerCmd { 25 | Launch(Launch), 26 | Watch(Watch), 27 | } 28 | 29 | #[derive(Debug)] 30 | pub struct Launch { 31 | pub log: bool, 32 | } 33 | 34 | #[derive(Debug)] 35 | pub struct Watch; 36 | 37 | #[derive(Debug)] 38 | pub struct AnalysisStats { 39 | pub path: PathBuf, 40 | 41 | pub parallel: bool, 42 | } 43 | 44 | impl RustAnalyzer { 45 | #[allow(dead_code)] 46 | pub fn from_env_or_exit() -> Self { 47 | Self::from_env_or_exit_() 48 | } 49 | 50 | #[allow(dead_code)] 51 | pub fn from_env() -> xflags::Result { 52 | Self::from_env_() 53 | } 54 | 55 | #[allow(dead_code)] 56 | pub fn from_vec(args: Vec) -> xflags::Result { 57 | Self::from_vec_(args) 58 | } 59 | } 60 | 61 | impl RustAnalyzer { 62 | fn from_env_or_exit_() -> Self { 63 | Self::from_env_().unwrap_or_else(|err| err.exit()) 64 | } 65 | fn from_env_() -> xflags::Result { 66 | let mut p = xflags::rt::Parser::new_from_env(); 67 | Self::parse_(&mut p) 68 | } 69 | fn from_vec_(args: Vec) -> xflags::Result { 70 | let mut p = xflags::rt::Parser::new(args); 71 | Self::parse_(&mut p) 72 | } 73 | } 74 | 75 | impl RustAnalyzer { 76 | fn parse_(p_: &mut xflags::rt::Parser) -> xflags::Result { 77 | #![allow(non_snake_case, unused_mut)] 78 | let mut verbose = Vec::new(); 79 | let mut server__dir = Vec::new(); 80 | let mut server__launch__log = Vec::new(); 81 | let mut analysis_stats__parallel = Vec::new(); 82 | let mut analysis_stats__path = (false, Vec::new()); 83 | 84 | let mut state_ = 0u8; 85 | while let Some(arg_) = p_.pop_flag() { 86 | match arg_ { 87 | Ok(flag_) => match (state_, flag_.as_str()) { 88 | (0, "--help" | "-h") => return Err(p_.help(Self::HELP_)), 89 | (0 | 1 | 2 | 3 | 4, "--verbose" | "-v") => verbose.push(()), 90 | (1, "--help" | "-h") => return Err(p_.help(Self::HELP_SERVER__)), 91 | (1 | 2 | 3, "--dir") => server__dir.push(p_.next_value(&flag_)?.into()), 92 | (1, _) => { 93 | p_.push_back(Ok(flag_)); 94 | state_ = 2; 95 | } 96 | (2, "--help" | "-h") => return Err(p_.help(Self::HELP_SERVER__LAUNCH__)), 97 | (2, "--log") => server__launch__log.push(()), 98 | (3, "--help" | "-h") => return Err(p_.help(Self::HELP_SERVER__WATCH__)), 99 | (4, "--help" | "-h") => return Err(p_.help(Self::HELP_ANALYSIS_STATS__)), 100 | (4, "--parallel") => analysis_stats__parallel.push(()), 101 | _ => return Err(p_.unexpected_flag(&flag_)), 102 | }, 103 | Err(arg_) => match (state_, arg_.to_str().unwrap_or("")) { 104 | (0, "server") => state_ = 1, 105 | (0, "analysis-stats") => state_ = 4, 106 | (0, "help") => return Err(p_.help(Self::HELP_)), 107 | (0, _) => { 108 | return Err(p_.unexpected_arg(arg_)); 109 | } 110 | (1, "watch") => state_ = 3, 111 | (1, "help") => return Err(p_.help(Self::HELP_SERVER__)), 112 | (1, _) => { 113 | p_.push_back(Err(arg_)); 114 | state_ = 2; 115 | } 116 | (2, "help") => return Err(p_.help(Self::HELP_SERVER__LAUNCH__)), 117 | (3, "help") => return Err(p_.help(Self::HELP_SERVER__WATCH__)), 118 | (4, _) => { 119 | if let (done_ @ false, buf_) = &mut analysis_stats__path { 120 | buf_.push(arg_.into()); 121 | *done_ = true; 122 | continue; 123 | } 124 | return Err(p_.unexpected_arg(arg_)); 125 | } 126 | _ => return Err(p_.unexpected_arg(arg_)), 127 | }, 128 | } 129 | } 130 | state_ = if state_ == 1 { 2 } else { state_ }; 131 | Ok(RustAnalyzer { 132 | verbose: verbose.len() as u32, 133 | subcommand: match state_ { 134 | 2 | 3 => RustAnalyzerCmd::Server(Server { 135 | dir: p_.optional("--dir", server__dir)?, 136 | subcommand: match state_ { 137 | 2 => ServerCmd::Launch(Launch { 138 | log: p_.optional("--log", server__launch__log)?.is_some(), 139 | }), 140 | 3 => ServerCmd::Watch(Watch {}), 141 | _ => return Err(p_.subcommand_required()), 142 | }, 143 | }), 144 | 4 => RustAnalyzerCmd::AnalysisStats(AnalysisStats { 145 | parallel: p_.optional("--parallel", analysis_stats__parallel)?.is_some(), 146 | path: p_.required("path", analysis_stats__path.1)?, 147 | }), 148 | _ => return Err(p_.subcommand_required()), 149 | }, 150 | }) 151 | } 152 | } 153 | impl RustAnalyzer { 154 | const HELP_SERVER__LAUNCH__: &'static str = "Usage: launch [--log] 155 | Options: 156 | --log 157 | 158 | Commands: 159 | help Print this message or the help of the given subcommand(s)"; 160 | const HELP_SERVER__WATCH__: &'static str = "Usage: watch 161 | Commands: 162 | help Print this message or the help of the given subcommand(s)"; 163 | const HELP_SERVER__: &'static str = "Usage: server [--dir ] [--log] 164 | Options: 165 | --dir 166 | --log 167 | 168 | Commands: 169 | watch 170 | help Print this message or the help of the given subcommand(s)"; 171 | const HELP_ANALYSIS_STATS__: &'static str = "Usage: analysis-stats [--parallel] 172 | Arguments: 173 | 174 | 175 | Options: 176 | --parallel 177 | 178 | Commands: 179 | help Print this message or the help of the given subcommand(s)"; 180 | const HELP_: &'static str = "Usage: rust-analyzer [-v]... [-h] 181 | Options: 182 | -v, --verbose 183 | -h, --help Prints help 184 | 185 | Commands: 186 | server 187 | analysis-stats 188 | help Print this message or the help of the given subcommand(s)"; 189 | } 190 | -------------------------------------------------------------------------------- /crates/xflags/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xflags" 3 | description = "Moderately simple command line arguments parser." 4 | categories = ["command-line-interface"] 5 | version.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | authors.workspace = true 9 | edition.workspace = true 10 | 11 | [dependencies] 12 | xflags-macros = { path = "../xflags-macros", version = "=0.4.0-pre.2" } 13 | -------------------------------------------------------------------------------- /crates/xflags/examples/hello-generated.rs: -------------------------------------------------------------------------------- 1 | mod flags { 2 | #![allow(unused)] 3 | 4 | xflags::xflags! { 5 | src "./examples/hello-generated.rs" 6 | 7 | /// Prints a greeting. 8 | cmd hello { 9 | /// Whom to greet. 10 | required name: String 11 | 12 | /// Use non-ascii symbols in the output. 13 | optional -e, --emoji 14 | } 15 | } 16 | 17 | // generated start 18 | // The following code is generated by `xflags` macro. 19 | // Run `env UPDATE_XFLAGS=1 cargo build` to regenerate. 20 | #[derive(Debug)] 21 | pub struct Hello { 22 | pub name: String, 23 | 24 | pub emoji: bool, 25 | } 26 | 27 | impl Hello { 28 | #[allow(dead_code)] 29 | pub fn from_env() -> xflags::Result { 30 | Self::from_env_() 31 | } 32 | 33 | #[allow(dead_code)] 34 | pub fn from_vec(args: Vec) -> xflags::Result { 35 | Self::from_vec_(args) 36 | } 37 | } 38 | // generated end 39 | } 40 | 41 | fn main() { 42 | match flags::Hello::from_env() { 43 | Ok(flags) => { 44 | let bang = if flags.emoji { "❣️" } else { "!" }; 45 | println!("Hello {}{}", flags.name, bang); 46 | } 47 | Err(err) => err.exit(), 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/xflags/examples/hello.rs: -------------------------------------------------------------------------------- 1 | mod flags { 2 | xflags::xflags! { 3 | cmd hello { 4 | required name: String 5 | optional -e, --emoji 6 | } 7 | } 8 | } 9 | 10 | fn main() { 11 | match flags::Hello::from_env() { 12 | Ok(flags) => { 13 | let bang = if flags.emoji { "❣️" } else { "!" }; 14 | println!("Hello {}{}", flags.name, bang); 15 | } 16 | Err(err) => { 17 | eprintln!("{}", err); 18 | std::process::exit(1) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/xflags/examples/immediate-mode.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | fn main() { 4 | let flags = xflags::parse_or_exit! { 5 | /// Remove directories and their contents recursively. 6 | optional -r,--recursive 7 | /// File or directory to remove 8 | required path: PathBuf 9 | }; 10 | 11 | println!( 12 | "removing {}{}", 13 | flags.path.display(), 14 | if flags.recursive { "recursively" } else { "" }, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /crates/xflags/examples/longer.rs: -------------------------------------------------------------------------------- 1 | mod flags { 2 | #![allow(unused)] 3 | 4 | use std::path::PathBuf; 5 | 6 | xflags::xflags! { 7 | src "./examples/longer.rs" 8 | 9 | cmd rust-analyzer { 10 | /// Set verbosity level 11 | repeated -v, --verbose 12 | /// Log to the specified file instead of stderr. 13 | optional --log-file path: PathBuf 14 | 15 | default cmd run-server { 16 | /// Print version 17 | optional --version 18 | } 19 | 20 | /// Parse tree 21 | cmd parse { 22 | /// Suppress printing 23 | optional --no-dump 24 | } 25 | 26 | /// Benchmark specific analysis operation 27 | cmd analysis-bench { 28 | /// Directory with Cargo.toml 29 | optional path: PathBuf 30 | /// Compute syntax highlighting for this file 31 | required --highlight path: PathBuf 32 | /// Compute highlighting for this line 33 | optional --line num: u32 34 | } 35 | } 36 | } 37 | 38 | // generated start 39 | // The following code is generated by `xflags` macro. 40 | // Run `env UPDATE_XFLAGS=1 cargo build` to regenerate. 41 | #[derive(Debug)] 42 | pub struct RustAnalyzer { 43 | pub verbose: u32, 44 | pub log_file: Option, 45 | pub subcommand: RustAnalyzerCmd, 46 | } 47 | 48 | #[derive(Debug)] 49 | pub enum RustAnalyzerCmd { 50 | RunServer(RunServer), 51 | Parse(Parse), 52 | AnalysisBench(AnalysisBench), 53 | } 54 | 55 | #[derive(Debug)] 56 | pub struct RunServer { 57 | pub version: bool, 58 | } 59 | 60 | #[derive(Debug)] 61 | pub struct Parse { 62 | pub no_dump: bool, 63 | } 64 | 65 | #[derive(Debug)] 66 | pub struct AnalysisBench { 67 | pub path: Option, 68 | 69 | pub highlight: PathBuf, 70 | pub line: Option, 71 | } 72 | 73 | impl RustAnalyzer { 74 | #[allow(dead_code)] 75 | pub fn from_env() -> xflags::Result { 76 | Self::from_env_() 77 | } 78 | 79 | #[allow(dead_code)] 80 | pub fn from_vec(args: Vec) -> xflags::Result { 81 | Self::from_vec_(args) 82 | } 83 | } 84 | // generated end 85 | } 86 | 87 | fn main() { 88 | match flags::RustAnalyzer::from_env() { 89 | Ok(flags) => eprintln!("{:#?}", flags), 90 | Err(err) => eprintln!("{}", err), 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /crates/xflags/examples/non-utf8.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | 3 | mod flags { 4 | use std::{ffi::OsString, path::PathBuf}; 5 | 6 | xflags::xflags! { 7 | cmd Cmd { 8 | required a: OsString 9 | required b: PathBuf 10 | required c: String 11 | } 12 | } 13 | } 14 | 15 | #[cfg(unix)] 16 | fn main() { 17 | use std::os::unix::ffi::OsStringExt; 18 | 19 | let flags = flags::Cmd::from_vec(vec![ 20 | OsString::from_vec(vec![254]), 21 | OsString::from_vec(vec![255]), 22 | "utf8".into(), 23 | ]); 24 | 25 | eprintln!("flags = {:?}", flags); 26 | } 27 | 28 | #[cfg(windows)] 29 | fn main() { 30 | use std::os::windows::ffi::OsStringExt; 31 | 32 | let flags = flags::Cmd::from_vec(vec![ 33 | OsString::from_wide(&[0xD800]), 34 | OsString::from_wide(&[0xDC00]), 35 | "utf8".into(), 36 | ]); 37 | 38 | eprintln!("flags = {:?}", flags); 39 | } 40 | 41 | #[cfg(not(any(unix, windows)))] 42 | fn main() {} 43 | -------------------------------------------------------------------------------- /crates/xflags/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `xflags` provides a procedural macro for parsing command line arguments. 2 | //! 3 | //! It is intended for use in development tools, so it emphasizes fast compile 4 | //! times and convenience at the expense of features. 5 | //! 6 | //! Rough decision tree for picking an argument parsing library: 7 | //! 8 | //! * if you need all of the features and don't care about minimalism, use 9 | //! [clap](https://github.com/clap-rs/clap) 10 | //! * if you want to be maximally minimal, need only basic features (eg, no help 11 | //! generation), and want to be pedantically correct, use 12 | //! [lexopt](https://github.com/blyxxyz/lexopt) 13 | //! * if you want to get things done fast (eg, you want auto help, but not at 14 | //! the cost of waiting for syn to compile), consider this crate. 15 | //! 16 | //! The secret sauce of xflags is that it is the opposite of a derive macro. 17 | //! Rather than generating a command line grammar from a Rust struct, `xflags` 18 | //! generates Rust structs based on input grammar. The grammar definition is 19 | //! both shorter and simpler to write, and is lighter on compile times. 20 | //! 21 | //! Here's a complete example of `parse_or_exit!` macro which parses arguments 22 | //! into an "anonymous" struct: 23 | //! 24 | //! ```no_run 25 | //! use std::path::PathBuf; 26 | //! 27 | //! fn main() { 28 | //! let flags = xflags::parse_or_exit! { 29 | //! /// Remove directories and their contents recursively. 30 | //! optional -r,--recursive 31 | //! /// File or directory to remove 32 | //! required path: PathBuf 33 | //! }; 34 | //! 35 | //! println!( 36 | //! "removing {}{}", 37 | //! flags.path.display(), 38 | //! if flags.recursive { "recursively" } else { "" }, 39 | //! ) 40 | //! } 41 | //! ``` 42 | //! 43 | //! The above program, when run with `--help` argument, generates the following 44 | //! help: 45 | //! 46 | //! ```text 47 | //! Usage: [-r] [-h] 48 | //! Arguments: 49 | //! File or directory to remove 50 | //! 51 | //! Options: 52 | //! -r, --recursive Remove directories and their contents recursively. 53 | //! -h, --help Prints help 54 | //! 55 | //! Commands: 56 | //! help Print this message or the help of the given subcommand(s) 57 | //! ``` 58 | //! 59 | //! For larger programs, you'd typically want to use `xflags!` macro, which 60 | //! generates _named_ structs for you. Unlike a typical macro, `xflags` writes 61 | //! generated code into the source file, to make it easy to understand the rust 62 | //! types at a glance. 63 | //! 64 | //! ``` 65 | //! mod flags { 66 | //! use std::path::PathBuf; 67 | //! 68 | //! xflags::xflags! { 69 | //! src "./examples/basic.rs" 70 | //! 71 | //! cmd my-command { 72 | //! required path: PathBuf 73 | //! optional -v, --verbose 74 | //! } 75 | //! } 76 | //! 77 | //! // generated start 78 | //! // The following code is generated by `xflags` macro. 79 | //! // Run `env UPDATE_XFLAGS=1 cargo build` to regenerate. 80 | //! #[derive(Debug)] 81 | //! pub struct MyCommand { 82 | //! pub path: PathBuf, 83 | //! pub verbose: bool, 84 | //! } 85 | //! 86 | //! impl MyCommand { 87 | //! pub fn from_env_or_exit() -> Self { 88 | //! Self::from_env_or_exit_() 89 | //! } 90 | //! pub fn from_env() -> xflags::Result { 91 | //! Self::from_env_() 92 | //! } 93 | //! pub fn from_vec(args: Vec) -> xflags::Result { 94 | //! Self::from_vec_(args) 95 | //! } 96 | //! } 97 | //! // generated end 98 | //! } 99 | //! 100 | //! fn main() { 101 | //! let flags = flags::MyCommand::from_env(); 102 | //! println!("{:#?}", flags); 103 | //! } 104 | //! ``` 105 | //! 106 | //! If you'd rather use a typical proc-macro which generates hidden code, just 107 | //! omit the src attribute. 108 | //! 109 | //! xflags correctly handles non-utf8 arguments. 110 | //! 111 | //! ## Syntax Reference 112 | //! 113 | //! The `xflags!` macro uses **cmd** keyword to introduce a command or 114 | //! subcommand that accepts positional arguments and switches. 115 | //! 116 | //! ``` 117 | //! xflags::xflags! { 118 | //! cmd command-name { } 119 | //! } 120 | //! ``` 121 | //! 122 | //! Switches are specified inside the curly braces. Long names (`--switch`) are 123 | //! mandatory, short names (`-s`) are optional. Each switch can be **optional**, 124 | //! **required**, or **repeated**. Dashes are allowed in switch names. 125 | //! 126 | //! ``` 127 | //! xflags::xflags! { 128 | //! cmd switches { 129 | //! optional -q,--quiet 130 | //! required --pass-me 131 | //! repeated --verbose 132 | //! } 133 | //! } 134 | //! ``` 135 | //! 136 | //! Switches can also take values. If the value type is `OsString` or `PathBuf`, 137 | //! it is created directly from the underlying argument. Otherwise, `FromStr` is 138 | //! used for parsing 139 | //! 140 | //! ``` 141 | //! use std::{path::PathBuf, ffi::OsString}; 142 | //! 143 | //! xflags::xflags! { 144 | //! cmd switches-with-values { 145 | //! optional --config path: PathBuf 146 | //! repeated --data val: OsString 147 | //! optional -j, --jobs n: u32 148 | //! } 149 | //! } 150 | //! ``` 151 | //! 152 | //! Arguments without `--` in then are are positional. 153 | //! 154 | //! ``` 155 | //! use std::{path::PathBuf, ffi::OsString}; 156 | //! 157 | //! xflags::xflags! { 158 | //! cmd positional-arguments { 159 | //! required program: PathBuf 160 | //! repeated args: OsString 161 | //! } 162 | //! } 163 | //! ``` 164 | //! 165 | //! You can create aliases if desired, which is as simple as adding extra names to the `cmd` definition. 166 | //! In this case, `run` can be called as `run`, `r` and `exec`: 167 | //! 168 | //! ```rust 169 | //! xflags::xflags! { 170 | //! cmd run r exec {} 171 | //! } 172 | //! ``` 173 | //! 174 | //! Nesting **cmd** is allowed. `xflag` automatically generates boilerplate 175 | //! enums for subcommands: 176 | //! 177 | //! ```ignore 178 | //! xflags::xflags! { 179 | //! src "./examples/subcommands.rs" 180 | //! cmd app { 181 | //! repeated -v, --verbose 182 | //! cmd foo { optional -s, --switch } 183 | //! cmd bar {} 184 | //! } 185 | //! } 186 | //! 187 | //! // generated start 188 | //! // The following code is generated by `xflags` macro. 189 | //! // Run `env UPDATE_XFLAGS=1 cargo build` to regenerate. 190 | //! #[derive(Debug)] 191 | //! pub struct App { 192 | //! pub verbose: u32, 193 | //! pub subcommand: AppCmd, 194 | //! } 195 | //! 196 | //! #[derive(Debug)] 197 | //! pub enum AppCmd { 198 | //! Foo(Foo), 199 | //! Bar(Bar), 200 | //! } 201 | //! 202 | //! #[derive(Debug)] 203 | //! pub struct Foo { 204 | //! pub switch: bool, 205 | //! } 206 | //! 207 | //! #[derive(Debug)] 208 | //! pub struct Bar { 209 | //! } 210 | //! 211 | //! impl App { 212 | //! pub fn from_env_or_exit() -> Self { 213 | //! Self::from_env_or_exit_() 214 | //! } 215 | //! pub fn from_env() -> xflags::Result { 216 | //! Self::from_env_() 217 | //! } 218 | //! pub fn from_vec(args: Vec) -> xflags::Result { 219 | //! Self::from_vec_(args) 220 | //! } 221 | //! } 222 | //! // generated end 223 | //! ``` 224 | //! 225 | //! Switches are always "inherited". Both `app -v foo` and `app foo -v` produce 226 | //! the same result. 227 | //! 228 | //! To make subcommand name optional use the **default** keyword to mark a 229 | //! subcommand to select if no subcommand name is passed. The name of the 230 | //! default subcommand affects only the name of the generated Rust struct, it 231 | //! can't be specified explicitly on the command line. 232 | //! 233 | //! ``` 234 | //! xflags::xflags! { 235 | //! cmd app { 236 | //! repeated -v, --verbose 237 | //! default cmd foo { optional -s, --switch } 238 | //! cmd bar {} 239 | //! } 240 | //! } 241 | //! ``` 242 | //! 243 | //! Commands, arguments, and switches can be documented. Doc comments become a 244 | //! part of generated help: 245 | //! 246 | //! ``` 247 | //! mod flags { 248 | //! use std::path::PathBuf; 249 | //! 250 | //! xflags::xflags! { 251 | //! /// Run basic system diagnostics. 252 | //! cmd healthck { 253 | //! /// Optional configuration file. 254 | //! optional config: PathBuf 255 | //! /// Verbosity level, can be repeated multiple times. 256 | //! repeated -v, --verbose 257 | //! } 258 | //! } 259 | //! } 260 | //! 261 | //! fn main() { 262 | //! match flags::Healthck::from_env() { 263 | //! Ok(flags) => { 264 | //! run_checks(flags.config, flags.verbose); 265 | //! } 266 | //! Err(err) => err.exit() 267 | //! } 268 | //! } 269 | //! 270 | //! # fn run_checks(_config: Option, _verbosity: u32) {} 271 | //! ``` 272 | //! 273 | //! The **src** keyword controls how the code generation works. If it is absent, 274 | //! `xflags` acts as a typical procedure macro, which generates a bunch of 275 | //! structs and impls. 276 | //! 277 | //! If the **src** keyword is present, it should specify the path to the file 278 | //! with `xflags!` invocation. The path should be relative to the directory with 279 | //! Cargo.toml. The macro then will avoid generating the structs. Instead, if 280 | //! the `UPDATE_XFLAGS` environmental variable is set, the macro will write them 281 | //! directly to the specified file. 282 | //! 283 | //! By convention, `xflag!` macro should be invoked from the `flags` submodule. 284 | //! The `flags::` prefix should be used to refer to command names. Additional 285 | //! validation logic can go to the `flags` module: 286 | //! 287 | //! ``` 288 | //! mod flags { 289 | //! xflags::xflags! { 290 | //! cmd my-command { 291 | //! repeated -v, --verbose 292 | //! optional -q, --quiet 293 | //! } 294 | //! } 295 | //! 296 | //! impl MyCommand { 297 | //! fn validate(&self) -> xflags::Result<()> { 298 | //! if self.quiet && self.verbose > 0 { 299 | //! return Err(xflags::Error::new( 300 | //! "`-q` and `-v` can't be specified at the same time" 301 | //! )); 302 | //! } 303 | //! Ok(()) 304 | //! } 305 | //! } 306 | //! } 307 | //! ``` 308 | //! 309 | //! The `parse_or_exit!` macro is a syntactic sure for `xflags!`, which 310 | //! immediately parses the argument, exiting the process if needed. 311 | //! `parse_or_exit` only supports single top-level command and doesn't need the 312 | //! `cmd` keyword. 313 | //! 314 | //! ## Limitations 315 | //! 316 | //! `xflags` follows 317 | //! [Fuchsia](https://fuchsia.dev/fuchsia-src/development/api/cli#command_line_arguments) 318 | //! conventions for command line arguments. GNU conventions such as grouping 319 | //! short-flags (`-xyz`) or gluing short flag and a value `(-fVAL)` are not 320 | //! supported. 321 | //! 322 | //! `xflags` requires the command line interface to be fully static. It's 323 | //! impossible to include additional flags at runtime. 324 | //! 325 | //! Implementation is not fully robust, there might be some residual bugs in 326 | //! edge cases. 327 | 328 | use std::fmt; 329 | 330 | /// Generates a parser for command line arguments from a DSL. 331 | /// 332 | /// See the module-level for detailed syntax specification. 333 | pub use xflags_macros::{parse_or_exit, xflags}; 334 | 335 | pub type Result = std::result::Result; 336 | 337 | /// An error occurred when parssing command line arguments. 338 | /// 339 | /// Either the command line was syntactically invalid, or `--help` was 340 | /// explicitly requested. 341 | #[derive(Debug)] 342 | pub struct Error { 343 | msg: String, 344 | help: bool, 345 | } 346 | 347 | impl fmt::Display for Error { 348 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 349 | fmt::Display::fmt(&self.msg, f) 350 | } 351 | } 352 | 353 | impl std::error::Error for Error {} 354 | 355 | impl Error { 356 | /// Creates a new `Error` from a given message. 357 | /// 358 | /// Use this to report custom validation errors. 359 | pub fn new(message: impl Into) -> Error { 360 | Error { msg: message.into(), help: false } 361 | } 362 | 363 | /// Error that carries `--help` message. 364 | pub fn is_help(&self) -> bool { 365 | self.help 366 | } 367 | 368 | /// Prints the error and exists the process. 369 | pub fn exit(self) -> ! { 370 | if self.is_help() { 371 | println!("{self}"); 372 | std::process::exit(0) 373 | } else { 374 | eprintln!("{self}"); 375 | std::process::exit(2) 376 | } 377 | } 378 | 379 | /// Appends to the contained message 380 | pub fn chain(mut self, msg: &str) -> Self { 381 | self.msg.push_str(msg); 382 | self 383 | } 384 | } 385 | 386 | /// Private impl details for macros. 387 | #[doc(hidden)] 388 | pub mod rt; 389 | -------------------------------------------------------------------------------- /crates/xflags/src/rt.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsString, fmt, str::FromStr}; 2 | 3 | use crate::{Error, Result}; 4 | 5 | macro_rules! format_err { 6 | ($($tt:tt)*) => { 7 | Error { msg: format!($($tt)*), help: false } 8 | }; 9 | } 10 | 11 | macro_rules! bail { 12 | ($($tt:tt)*) => { 13 | return Err(format_err!($($tt)*)) 14 | }; 15 | } 16 | 17 | pub struct Parser { 18 | after_double_dash: bool, 19 | rargs: Vec, 20 | } 21 | 22 | impl Parser { 23 | pub fn new(mut args: Vec) -> Self { 24 | // parse `help` command last when encountered somewhere along the way to be able to do 25 | // `help ` or `cmd help sub` without creating a bunch of leafs in the parse tree for it 26 | if let Some((i, _)) = args.iter().enumerate().find(|(_, arg)| *arg == "help") { 27 | args.remove(i); 28 | args.push("--help".into()) 29 | } 30 | 31 | args.reverse(); 32 | 33 | Self { after_double_dash: false, rargs: args } 34 | } 35 | 36 | pub fn new_from_env() -> Self { 37 | let args = std::env::args_os().collect::>(); 38 | let mut res = Parser::new(args); 39 | let _progn = res.next(); 40 | res 41 | } 42 | 43 | pub fn pop_flag(&mut self) -> Option> { 44 | if self.after_double_dash { 45 | self.next().map(Err) 46 | } else { 47 | let arg = self.next()?; 48 | let arg_str = arg.to_str().unwrap_or_default(); 49 | if arg_str.starts_with('-') { 50 | if arg_str == "--" { 51 | self.after_double_dash = true; 52 | return self.next().map(Err); 53 | } 54 | Some(arg.into_string()) 55 | } else { 56 | Some(Err(arg)) 57 | } 58 | } 59 | } 60 | 61 | pub fn push_back(&mut self, arg: Result) { 62 | let arg = match arg { 63 | Ok(it) => it.into(), 64 | Err(it) => it, 65 | }; 66 | self.rargs.push(arg) 67 | } 68 | 69 | fn next(&mut self) -> Option { 70 | self.rargs.pop() 71 | } 72 | 73 | pub fn next_value(&mut self, flag: &str) -> Result { 74 | self.next().ok_or_else(|| format_err!("expected a value for `{flag}`")) 75 | } 76 | 77 | pub fn next_value_from_str(&mut self, flag: &str) -> Result 78 | where 79 | T::Err: fmt::Display, 80 | { 81 | let value = self.next_value(flag)?; 82 | self.value_from_str(flag, value) 83 | } 84 | 85 | pub fn value_from_str(&mut self, flag: &str, value: OsString) -> Result 86 | where 87 | T::Err: fmt::Display, 88 | { 89 | match value.into_string() { 90 | Ok(str) => str.parse::().map_err(|err| format_err!("Can't parse `{flag}`, {err}")), 91 | Err(it) => { 92 | bail!("Can't parse `{flag}`, invalid utf8: {it:?}") 93 | } 94 | } 95 | } 96 | 97 | pub fn unexpected_flag(&self, flag: &str) -> Error { 98 | format_err!("Unknown flag: `{flag}`. Use `help` for more information") 99 | } 100 | 101 | pub fn unexpected_arg(&self, arg: OsString) -> Error { 102 | // `to_string_lossy()` seems appropriate here but OsString's debug implementation actually 103 | // escapes codes that are not valid utf-8, rather than replace them with `FFFD` 104 | let dbg = format!("{arg:?}"); 105 | let arg = dbg.trim_matches('"'); 106 | 107 | format_err!("Unknown command: `{arg}`. Use `help` for more information") 108 | } 109 | 110 | pub fn subcommand_required(&self) -> Error { 111 | format_err!("A subcommand is required. Use `help` for more information") 112 | } 113 | 114 | pub fn help(&self, help: &'static str) -> Error { 115 | Error { msg: help.to_string(), help: true } 116 | } 117 | 118 | pub fn optional(&self, flag: &str, mut vals: Vec) -> Result> { 119 | if vals.len() > 1 { 120 | bail!("Flag specified more than once: `{flag}`") 121 | } 122 | Ok(vals.pop()) 123 | } 124 | 125 | pub fn required(&self, flag: &str, mut vals: Vec) -> Result { 126 | if vals.len() > 1 { 127 | bail!("Flag specified more than once: `{flag}`") 128 | } 129 | vals.pop().ok_or_else(|| { 130 | format_err!("Flag is required: `{flag}`. Use `help` for more information") 131 | }) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_modules = false 2 | use_small_heuristics = "Max" 3 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.0.0" 4 | publish = false 5 | authors = ["Aleksey Kladov "] 6 | edition = "2021" 7 | 8 | [dependencies] 9 | xshell = "0.2.2" 10 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tidy; 3 | 4 | use std::{ 5 | thread, 6 | time::{Duration, Instant}, 7 | }; 8 | 9 | use xshell::{cmd, Shell}; 10 | 11 | fn main() -> xshell::Result<()> { 12 | let sh = Shell::new()?; 13 | 14 | cmd!(sh, "rustup toolchain install stable --no-self-update").run()?; 15 | let _e = sh.push_env("RUSTUP_TOOLCHAIN", "stable"); 16 | cmd!(sh, "rustc --version").run()?; 17 | 18 | { 19 | let _s = section("BUILD"); 20 | cmd!(sh, "cargo test --workspace --no-run").run()?; 21 | } 22 | 23 | { 24 | let _s = section("TEST"); 25 | cmd!(sh, "cargo test --workspace -- --nocapture").run()?; 26 | } 27 | 28 | { 29 | let _s = section("PUBLISH"); 30 | 31 | let version = 32 | cmd!(sh, "cargo pkgid -p xflags").read()?.rsplit_once('#').unwrap().1.to_string(); 33 | let tag = format!("v{version}"); 34 | 35 | let current_branch = cmd!(sh, "git branch --show-current").read()?; 36 | let tag_exists = 37 | cmd!(sh, "git tag --list").read()?.split_ascii_whitespace().any(|it| it == tag); 38 | 39 | if current_branch == "master" && !tag_exists { 40 | cmd!(sh, "git tag v{version}").run()?; 41 | 42 | { 43 | cmd!(sh, "cargo publish -p xflags-macros").run()?; 44 | for _ in 0..100 { 45 | thread::sleep(Duration::from_secs(3)); 46 | let err_msg = cmd!( 47 | sh, 48 | "cargo install xflags-macros --version {version} --bin non-existing" 49 | ) 50 | .ignore_status() 51 | .read_stderr()?; 52 | 53 | let not_found = err_msg.contains("could not find "); 54 | let tried_installing = err_msg.contains("Installing"); 55 | assert!(not_found ^ tried_installing); 56 | if tried_installing { 57 | break; 58 | } 59 | } 60 | } 61 | cmd!(sh, "cargo publish -p xflags").run()?; 62 | cmd!(sh, "git push --tags").run()?; 63 | } 64 | } 65 | 66 | Ok(()) 67 | } 68 | 69 | fn section(name: &'static str) -> impl Drop { 70 | println!("::group::{name}"); 71 | let start = Instant::now(); 72 | defer(move || { 73 | let elapsed = start.elapsed(); 74 | eprintln!("{name}: {elapsed:.2?}"); 75 | println!("::endgroup::"); 76 | }) 77 | } 78 | 79 | fn defer(f: F) -> impl Drop { 80 | struct D(Option); 81 | impl Drop for D { 82 | fn drop(&mut self) { 83 | if let Some(f) = self.0.take() { 84 | f() 85 | } 86 | } 87 | } 88 | D(Some(f)) 89 | } 90 | -------------------------------------------------------------------------------- /xtask/src/tidy.rs: -------------------------------------------------------------------------------- 1 | use xshell::{cmd, Shell}; 2 | 3 | #[test] 4 | fn test_formatting() { 5 | let sh = Shell::new().unwrap(); 6 | cmd!(sh, "cargo fmt --all -- --check").run().unwrap() 7 | } 8 | --------------------------------------------------------------------------------