xflags-macros-0.3.1/.cargo_vcs_info.json0000644000000001620000000000100136020ustar { "git": { "sha1": "5b33711a3813e15564f7b7855d3c8406669f599f" }, "path_in_vcs": "crates/xflags-macros" }xflags-macros-0.3.1/Cargo.toml0000644000000015340000000000100116040ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "xflags-macros" version = "0.3.1" authors = ["Aleksey Kladov "] description = "Private implementation details of xflags." license = "MIT OR Apache-2.0" repository = "https://github.com/matklad/xflags" resolver = "1" [lib] proc-macro = true [dev-dependencies.expect-test] version = "1" [dev-dependencies.proc-macro2] version = "1" xflags-macros-0.3.1/Cargo.toml.orig000064400000000000000000000005141046102023000152620ustar 00000000000000[package] name = "xflags-macros" description = "Private implementation details of xflags." version.workspace = true license.workspace = true repository.workspace = true authors.workspace = true edition.workspace = true [lib] proc-macro = true [dev-dependencies] proc-macro2 = "1" expect-test = "1" xflags = { path = "../xflags" } xflags-macros-0.3.1/src/ast.rs000064400000000000000000000022571046102023000143250ustar 00000000000000#[derive(Debug)] pub(crate) struct XFlags { pub(crate) src: Option, pub(crate) cmd: Cmd, } impl XFlags { pub fn is_anon(&self) -> bool { self.cmd.name.is_empty() } } #[derive(Debug)] pub(crate) struct Cmd { pub(crate) name: String, pub(crate) doc: Option, pub(crate) args: Vec, pub(crate) flags: Vec, pub(crate) subcommands: Vec, pub(crate) default: bool, pub(crate) idx: u8, } #[derive(Debug)] pub(crate) struct Arg { pub(crate) arity: Arity, pub(crate) doc: Option, pub(crate) val: Val, } #[derive(Debug)] pub(crate) struct Flag { pub(crate) arity: Arity, pub(crate) name: String, pub(crate) short: Option, pub(crate) doc: Option, pub(crate) val: Option, } impl Flag { pub(crate) fn is_help(&self) -> bool { self.name == "help" } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Arity { Optional, Required, Repeated, } #[derive(Debug)] pub(crate) struct Val { pub(crate) name: String, pub(crate) ty: Ty, } #[derive(Debug)] pub(crate) enum Ty { PathBuf, OsString, FromStr(String), } xflags-macros-0.3.1/src/emit.rs000064400000000000000000000444611046102023000144770ustar 00000000000000use crate::{ast, update}; use std::{env, fmt::Write, path::Path}; macro_rules! w { ($($tt:tt)*) => { drop(write!($($tt)*)) }; } pub(crate) fn emit(xflags: &ast::XFlags) -> String { let mut buf = String::new(); if xflags.is_anon() { w!(buf, "{{\n"); } emit_cmd(&mut buf, &xflags.cmd); blank_line(&mut buf); emit_api(&mut buf, xflags); if !xflags.is_anon() && env::var("UPDATE_XFLAGS").is_ok() { if let Some(src) = &xflags.src { update::in_place(&buf, Path::new(src.as_str())) } else { update::stdout(&buf); } } if xflags.src.is_some() { buf.clear() } blank_line(&mut buf); emit_impls(&mut buf, xflags); emit_help(&mut buf, xflags); if xflags.is_anon() { w!(buf, "Flags::from_env_or_exit()"); w!(buf, "}}\n"); } buf } fn emit_cmd(buf: &mut String, cmd: &ast::Cmd) { w!(buf, "#[derive(Debug)]\n"); w!(buf, "pub struct {}", cmd.ident()); let flags = cmd.flags.iter().filter(|it| !it.is_help()).collect::>(); if cmd.args.is_empty() && flags.is_empty() && cmd.subcommands.is_empty() { w!(buf, ";\n"); return; } w!(buf, " {{\n"); for arg in &cmd.args { let ty = gen_arg_ty(arg.arity, &arg.val.ty); w!(buf, " pub {}: {ty},\n", arg.val.ident()); } if !cmd.args.is_empty() && !flags.is_empty() { blank_line(buf); } for flag in &flags { let ty = gen_flag_ty(flag.arity, flag.val.as_ref().map(|it| &it.ty)); w!(buf, " pub {}: {ty},\n", flag.ident()); } if cmd.has_subcommands() { w!(buf, " pub subcommand: {},\n", cmd.cmd_enum_ident()); } w!(buf, "}}\n"); if cmd.has_subcommands() { blank_line(buf); w!(buf, "#[derive(Debug)]\n"); w!(buf, "pub enum {} {{\n", cmd.cmd_enum_ident()); for sub in &cmd.subcommands { let name = sub.ident(); w!(buf, " {name}({name}),\n"); } w!(buf, "}}\n"); for sub in &cmd.subcommands { blank_line(buf); emit_cmd(buf, sub); } } } fn gen_flag_ty(arity: ast::Arity, ty: Option<&ast::Ty>) -> String { match ty { None => match arity { ast::Arity::Optional => "bool".to_string(), ast::Arity::Required => "()".to_string(), ast::Arity::Repeated => "u32".to_string(), }, Some(ty) => gen_arg_ty(arity, ty), } } fn gen_arg_ty(arity: ast::Arity, ty: &ast::Ty) -> String { let ty = match ty { ast::Ty::PathBuf => "PathBuf".into(), ast::Ty::OsString => "OsString".into(), ast::Ty::FromStr(it) => it.clone(), }; match arity { ast::Arity::Optional => format!("Option<{}>", ty), ast::Arity::Required => ty, ast::Arity::Repeated => format!("Vec<{}>", ty), } } fn emit_api(buf: &mut String, xflags: &ast::XFlags) { w!(buf, "impl {} {{\n", xflags.cmd.ident()); w!(buf, " #[allow(dead_code)]\n"); w!(buf, " pub fn from_env_or_exit() -> Self {{\n"); w!(buf, " Self::from_env_or_exit_()\n"); w!(buf, " }}\n"); blank_line(buf); w!(buf, " #[allow(dead_code)]\n"); w!(buf, " pub fn from_env() -> xflags::Result {{\n"); w!(buf, " Self::from_env_()\n"); w!(buf, " }}\n"); blank_line(buf); w!(buf, " #[allow(dead_code)]\n"); w!(buf, " pub fn from_vec(args: Vec) -> xflags::Result {{\n"); w!(buf, " Self::from_vec_(args)\n"); w!(buf, " }}\n"); w!(buf, "}}\n"); } fn emit_impls(buf: &mut String, xflags: &ast::XFlags) -> () { w!(buf, "impl {} {{\n", xflags.cmd.ident()); w!(buf, " fn from_env_or_exit_() -> Self {{\n"); w!(buf, " Self::from_env_().unwrap_or_else(|err| err.exit())\n"); w!(buf, " }}\n"); w!(buf, " fn from_env_() -> xflags::Result {{\n"); w!(buf, " let mut p = xflags::rt::Parser::new_from_env();\n"); w!(buf, " Self::parse_(&mut p)\n"); w!(buf, " }}\n"); w!(buf, " fn from_vec_(args: Vec) -> xflags::Result {{\n"); w!(buf, " let mut p = xflags::rt::Parser::new(args);\n"); w!(buf, " Self::parse_(&mut p)\n"); w!(buf, " }}\n"); w!(buf, "}}\n"); blank_line(buf); emit_parse(buf, &xflags.cmd) } fn emit_parse(buf: &mut String, cmd: &ast::Cmd) { w!(buf, "impl {} {{\n", cmd.ident()); w!(buf, "fn parse_(p_: &mut xflags::rt::Parser) -> xflags::Result {{\n"); w!(buf, "#![allow(non_snake_case)]\n"); let mut prefix = String::new(); emit_locals_rec(buf, &mut prefix, cmd); blank_line(buf); w!(buf, "let mut state_ = 0u8;\n"); w!(buf, "while let Some(arg_) = p_.pop_flag() {{\n"); w!(buf, "match arg_ {{\n"); { w!(buf, "Ok(flag_) => match (state_, flag_.as_str()) {{\n"); emit_match_flag_rec(buf, &mut prefix, cmd); w!(buf, "_ => return Err(p_.unexpected_flag(&flag_)),\n"); w!(buf, "}}\n"); w!(buf, "Err(arg_) => match (state_, arg_.to_str().unwrap_or(\"\")) {{\n"); emit_match_arg_rec(buf, &mut prefix, cmd); w!(buf, "_ => return Err(p_.unexpected_arg(arg_)),\n"); w!(buf, "}}\n"); } w!(buf, "}}\n"); w!(buf, "}}\n"); emit_default_transitions(buf, cmd); w!(buf, "Ok("); emit_record_rec(buf, &mut prefix, cmd); w!(buf, ")"); w!(buf, "}}\n"); w!(buf, "}}\n"); } fn emit_locals_rec(buf: &mut String, prefix: &mut String, cmd: &ast::Cmd) { for flag in &cmd.flags { if !flag.is_help() { w!(buf, "let mut {prefix}{} = Vec::new();\n", flag.ident()); } } for arg in &cmd.args { w!(buf, "let mut {prefix}{} = (false, Vec::new());\n", arg.val.ident()); } for sub in &cmd.subcommands { let l = sub.push_prefix(prefix); emit_locals_rec(buf, prefix, sub); prefix.truncate(l); } } fn emit_match_flag_rec(buf: &mut String, prefix: &mut String, cmd: &ast::Cmd) { for flag in &cmd.flags { w!(buf, "("); emit_all_ids_rec(buf, cmd); w!(buf, ", \"--{}\"", flag.name); if let Some(short) = &flag.short { w!(buf, "| \"-{short}\""); } w!(buf, ") => "); if flag.is_help() { w!(buf, "return Err(p_.help(Self::HELP_)),"); } else { w!(buf, "{prefix}{}.push(", flag.ident()); match &flag.val { Some(val) => match &val.ty { ast::Ty::OsString | ast::Ty::PathBuf => { w!(buf, "p_.next_value(&flag_)?.into()") } ast::Ty::FromStr(ty) => { w!(buf, "p_.next_value_from_str::<{ty}>(&flag_)?") } }, None => w!(buf, "()"), } w!(buf, "),"); } } if let Some(sub) = cmd.default_subcommand() { w!(buf, "({}, _) => {{ p_.push_back(Ok(flag_)); state_ = {}; }}", cmd.idx, sub.idx); } for sub in &cmd.subcommands { let l = sub.push_prefix(prefix); emit_match_flag_rec(buf, prefix, sub); prefix.truncate(l); } } fn emit_match_arg_rec(buf: &mut String, prefix: &mut String, cmd: &ast::Cmd) { for sub in cmd.named_subcommands() { w!(buf, "({}, \"{}\") => state_ = {},\n", cmd.idx, sub.name, sub.idx); } if !cmd.args.is_empty() || cmd.has_subcommands() { w!(buf, "({}, _) => {{\n", cmd.idx); for arg in &cmd.args { let done = match arg.arity { ast::Arity::Optional | ast::Arity::Required => "done_ @ ", ast::Arity::Repeated => "", }; w!(buf, "if let ({done}false, buf_) = &mut {prefix}{} {{\n", arg.val.ident()); w!(buf, "buf_.push("); match &arg.val.ty { ast::Ty::OsString | ast::Ty::PathBuf => { w!(buf, "arg_.into()") } ast::Ty::FromStr(ty) => { w!(buf, "p_.value_from_str::<{ty}>(\"{}\", arg_)?", arg.val.name); } } w!(buf, ");\n"); match arg.arity { ast::Arity::Optional | ast::Arity::Required => { w!(buf, "*done_ = true;\n"); } ast::Arity::Repeated => (), } w!(buf, "continue;\n"); w!(buf, "}}\n"); } if let Some(sub) = cmd.default_subcommand() { w!(buf, "p_.push_back(Err(arg_)); state_ = {};", sub.idx); } else { w!(buf, "return Err(p_.unexpected_arg(arg_));"); } w!(buf, "}}\n"); } for sub in &cmd.subcommands { let l = sub.push_prefix(prefix); emit_match_arg_rec(buf, prefix, sub); prefix.truncate(l); } } fn emit_record_rec(buf: &mut String, prefix: &mut String, cmd: &ast::Cmd) { w!(buf, "{} {{\n", cmd.ident()); for flag in &cmd.flags { if flag.is_help() { continue; } w!(buf, "{}: ", flag.ident()); match &flag.val { Some(_val) => match flag.arity { ast::Arity::Optional => { w!(buf, "p_.optional(\"--{}\", {prefix}{})?", flag.name, flag.ident()) } ast::Arity::Required => { w!(buf, "p_.required(\"--{}\", {prefix}{})?", flag.name, flag.ident()) } ast::Arity::Repeated => w!(buf, "{prefix}{}", flag.ident()), }, None => match flag.arity { ast::Arity::Optional => { w!(buf, "p_.optional(\"--{}\", {prefix}{})?.is_some()", flag.name, flag.ident()) } ast::Arity::Required => { w!(buf, "p_.required(\"--{}\", {prefix}{})?", flag.name, flag.ident()) } ast::Arity::Repeated => w!(buf, "{prefix}{}.len() as u32", flag.ident()), }, } w!(buf, ",\n"); } for arg in &cmd.args { let val = &arg.val; w!(buf, "{}: ", val.ident()); match arg.arity { ast::Arity::Optional => { w!(buf, "p_.optional(\"{}\", {prefix}{}.1)?", val.name, val.ident()) } ast::Arity::Required => { w!(buf, "p_.required(\"{}\", {prefix}{}.1)?", val.name, val.ident()) } ast::Arity::Repeated => w!(buf, "{prefix}{}.1", val.ident()), } w!(buf, ",\n"); } if cmd.has_subcommands() { w!(buf, "subcommand: match state_ {{\n"); for sub in &cmd.subcommands { emit_leaf_ids_rec(buf, sub); w!(buf, " => {}::{}(", cmd.cmd_enum_ident(), sub.ident()); let l = prefix.len(); prefix.push_str(&snake(&sub.name)); prefix.push_str("__"); emit_record_rec(buf, prefix, sub); prefix.truncate(l); w!(buf, "),\n"); } w!(buf, "_ => return Err(p_.subcommand_required()),"); w!(buf, "}}\n"); } w!(buf, "}}"); } fn emit_leaf_ids_rec(buf: &mut String, cmd: &ast::Cmd) { if cmd.has_subcommands() { for sub in &cmd.subcommands { emit_leaf_ids_rec(buf, sub) } } else { w!(buf, "| {}", cmd.idx) } } fn emit_all_ids_rec(buf: &mut String, cmd: &ast::Cmd) { w!(buf, "| {}", cmd.idx); for sub in &cmd.subcommands { emit_all_ids_rec(buf, sub) } } fn emit_default_transitions(buf: &mut String, cmd: &ast::Cmd) { if let Some(sub) = cmd.default_subcommand() { w!(buf, "state_ = if state_ == {} {{ {} }} else {{ state_ }};", cmd.idx, sub.idx); } for sub in &cmd.subcommands { emit_default_transitions(buf, sub); } } fn emit_help(buf: &mut String, xflags: &ast::XFlags) { w!(buf, "impl {} {{\n", xflags.cmd.ident()); let help = { let mut buf = String::new(); help_rec(&mut buf, "", &xflags.cmd); buf }; let help = format!("{:?}", help); let help = help.replace("\\n", "\n").replacen("\"", "\"\\\n", 1); w!(buf, "const HELP_: &'static str = {help};"); w!(buf, "}}\n"); } fn write_lines_indented(buf: &mut String, multiline_str: &str, indent: usize) { for line in multiline_str.split('\n').map(str::trim_end) { if line.is_empty() { w!(buf, "\n") } else { w!(buf, "{blank:indent$}{line}\n", blank = ""); } } } fn help_rec(buf: &mut String, prefix: &str, cmd: &ast::Cmd) { let mut empty_help = true; if !cmd.name.is_empty() { empty_help = false; w!(buf, "{}{}\n", prefix, cmd.name); } if let Some(doc) = &cmd.doc { empty_help = false; write_lines_indented(buf, doc, 2); } let indent = if prefix.is_empty() { "" } else { " " }; let args = cmd.args_with_default(); if !args.is_empty() { if !empty_help { blank_line(buf); } empty_help = false; w!(buf, "{}ARGS:\n", indent); let mut blank = ""; for arg in &args { w!(buf, "{blank}"); blank = "\n"; let (l, r) = match arg.arity { ast::Arity::Optional => ("[", "]"), ast::Arity::Required => ("<", ">"), ast::Arity::Repeated => ("<", ">..."), }; w!(buf, " {l}{}{r}\n", arg.val.name); if let Some(doc) = &arg.doc { write_lines_indented(buf, doc, 6) } } } let flags = cmd.flags_with_default(); if !flags.is_empty() { if !empty_help { blank_line(buf); } w!(buf, "{indent}OPTIONS:\n"); let mut blank = ""; for flag in &flags { w!(buf, "{blank}",); blank = "\n"; let short = flag.short.as_ref().map(|it| format!("-{it}, ")).unwrap_or_default(); let value = flag.val.as_ref().map(|it| format!(" <{}>", it.name)).unwrap_or_default(); w!(buf, " {short}--{}{value}\n", flag.name); if let Some(doc) = &flag.doc { write_lines_indented(buf, doc, 6); } } } let subcommands = cmd.named_subcommands(); if !subcommands.is_empty() { if prefix.is_empty() { blank_line(buf); w!(buf, "SUBCOMMANDS:"); } let prefix = format!("{}{} ", prefix, cmd.name); for sub in subcommands { blank_line(buf); blank_line(buf); help_rec(buf, &prefix, sub); } } } impl ast::Cmd { fn ident(&self) -> String { if self.name.is_empty() { return "Flags".to_string(); } camel(&self.name) } fn cmd_enum_ident(&self) -> String { format!("{}Cmd", self.ident()) } fn push_prefix(&self, buf: &mut String) -> usize { let l = buf.len(); buf.push_str(&snake(&self.name)); buf.push_str("__"); l } fn has_subcommands(&self) -> bool { !self.subcommands.is_empty() } fn named_subcommands(&self) -> &[ast::Cmd] { let start = if self.default { 1 } else { 0 }; &self.subcommands[start..] } fn default_subcommand(&self) -> Option<&ast::Cmd> { if self.default { self.subcommands.first() } else { None } } fn args_with_default(&self) -> Vec<&ast::Arg> { let mut res = self.args.iter().collect::>(); if let Some(sub) = self.default_subcommand() { res.extend(sub.args_with_default()); } res } fn flags_with_default(&self) -> Vec<&ast::Flag> { let mut res = self.flags.iter().collect::>(); if let Some(sub) = self.default_subcommand() { res.extend(sub.flags_with_default()) } res } } impl ast::Flag { fn ident(&self) -> String { snake(&self.name) } } impl ast::Val { fn ident(&self) -> String { snake(&self.name) } } fn blank_line(buf: &mut String) { w!(buf, "\n"); } fn camel(s: &str) -> String { s.split('-').map(first_upper).collect() } fn first_upper(s: &str) -> String { s.chars() .next() .map(|it| it.to_ascii_uppercase()) .into_iter() .chain(s.chars().skip(1)) .collect() } fn snake(s: &str) -> String { s.replace('-', "_") } #[cfg(test)] mod tests { use std::{ fs, io::Write, path::Path, process::{Command, Stdio}, }; fn reformat(text: String) -> Option { let mut rustfmt = Command::new("rustfmt").stdin(Stdio::piped()).stdout(Stdio::piped()).spawn().unwrap(); let mut stdin = rustfmt.stdin.take().unwrap(); stdin.write_all(text.as_bytes()).unwrap(); drop(stdin); let out = rustfmt.wait_with_output().unwrap(); let res = String::from_utf8(out.stdout).unwrap(); if res.is_empty() { None } else { Some(res) } } fn update_on_disk_if_different(file: &Path, new_contents: String) -> bool { let old_contents = fs::read_to_string(file).unwrap_or_default(); if old_contents.trim() == new_contents.trim() { return false; } fs::write(file, new_contents).unwrap(); true } #[test] fn gen_it() { let test_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests"); let mut did_update = false; for entry in fs::read_dir(test_dir.join("data")).unwrap() { let entry = entry.unwrap(); let text = fs::read_to_string(entry.path()).unwrap(); let mut lines = text.lines().collect::>(); lines.pop(); lines.remove(0); let text = lines.join("\n"); let res = crate::compile(&text); let fmt = reformat(res.clone()); let code = format!( "#![allow(unused)]\nuse std::{{ffi::OsString, path::PathBuf}};\n\n{}", fmt.as_deref().unwrap_or(&res) ); let name = entry.file_name(); did_update |= update_on_disk_if_different(&test_dir.join("it").join(name), code); if fmt.is_none() { panic!("syntax error"); } } if did_update { panic!("generated output changed") } } } xflags-macros-0.3.1/src/lib.rs000064400000000000000000000022641046102023000143020ustar 00000000000000mod ast; mod parse; mod emit; mod update; #[proc_macro] pub fn xflags(_ts: proc_macro::TokenStream) -> proc_macro::TokenStream { // Stub out the code, but let rust-analyzer resolve the invocation #[cfg(not(test))] { let text = match parse::xflags(_ts) { Ok(cmd) => emit::emit(&cmd), Err(err) => format!("compile_error!(\"invalid flags syntax, {err}\");"), }; text.parse().unwrap() } #[cfg(test)] unimplemented!() } #[proc_macro] pub fn parse_or_exit(_ts: proc_macro::TokenStream) -> proc_macro::TokenStream { // Stub out the code, but let rust-analyzer resolve the invocation #[cfg(not(test))] { let text = match parse::parse_or_exit(_ts) { Ok(cmd) => emit::emit(&cmd), Err(err) => format!("compile_error!(\"invalid flags syntax, {err}\")"), }; text.parse().unwrap() } #[cfg(test)] { let _ = parse::parse_or_exit; unimplemented!(); } } #[cfg(test)] pub fn compile(src: &str) -> String { use proc_macro2::TokenStream; let ts = src.parse::().unwrap(); let cmd = parse::xflags(ts).unwrap(); emit::emit(&cmd) } xflags-macros-0.3.1/src/parse.rs000064400000000000000000000237741046102023000146570ustar 00000000000000use std::{fmt, mem}; #[cfg(not(test))] use proc_macro::{Delimiter, TokenStream, TokenTree}; #[cfg(test)] use proc_macro2::{Delimiter, TokenStream, TokenTree}; use crate::ast; type Result = std::result::Result; #[derive(Debug)] pub(crate) struct Error { msg: String, } impl std::error::Error for Error {} impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.msg, f) } } pub(crate) fn xflags(ts: TokenStream) -> Result { let p = &mut Parser::new(ts); let src = if p.eat_keyword("src") { Some(p.expect_string()?) } else { None }; let doc = opt_doc(p)?; let mut cmd = cmd(p)?; cmd.doc = doc; add_help(&mut cmd); let res = ast::XFlags { src, cmd }; Ok(res) } pub(crate) fn parse_or_exit(ts: TokenStream) -> Result { let p = &mut Parser::new(ts); let mut cmd = anon_cmd(p)?; assert!(cmd.subcommands.is_empty()); add_help(&mut cmd); let res = ast::XFlags { src: None, cmd }; Ok(res) } fn add_help(cmd: &mut ast::Cmd) { let help = ast::Flag { arity: ast::Arity::Optional, name: "help".to_string(), short: Some("h".to_string()), doc: Some("Prints help information.".to_string()), val: None, }; cmd.flags.push(help); } macro_rules! format_err { ($($tt:tt)*) => { Error { msg: format!($($tt)*) } // panic!($($tt)*) }; } macro_rules! bail { ($($tt:tt)*) => { return Err(format_err!($($tt)*)) }; } fn anon_cmd(p: &mut Parser) -> Result { cmd_impl(p, true) } fn cmd(p: &mut Parser) -> Result { cmd_impl(p, false) } fn cmd_impl(p: &mut Parser, anon: bool) -> Result { let name = if anon { String::new() } else { p.expect_keyword("cmd")?; cmd_name(p)? }; let idx = p.idx; p.idx += 1; let mut res = ast::Cmd { name, doc: None, args: Vec::new(), flags: Vec::new(), subcommands: Vec::new(), default: false, idx, }; if !anon { p.enter_delim(Delimiter::Brace)?; } while !p.end() { let doc = opt_doc(p)?; let default = !anon && p.eat_keyword("default"); if !anon && (default || p.at_keyword("cmd")) { let mut cmd = cmd(p)?; cmd.doc = doc; res.subcommands.push(cmd); if default { if res.default { bail!("only one subcommand can be default") } res.default = true; res.subcommands.rotate_right(1); } } else { let arity = arity(p)?; let is_val = p.lookahead_punct(':', 1); let name = p.expect_name()?; if name.starts_with('-') { let mut flag = flag(p, name)?; flag.doc = doc; flag.arity = arity; res.flags.push(flag) } else if is_val { p.expect_punct(':')?; let ty = ty(p)?; let val = ast::Val { name, ty }; let arg = ast::Arg { arity, doc, val }; res.args.push(arg); } else { bail!("expected `--flag` or `arg: Type`") } } } if !anon { p.exit_delim()?; } Ok(res) } fn flag(p: &mut Parser, name: String) -> Result { let short; let long; if name.starts_with("--") { short = None; long = name; } else { short = Some(name); if !p.eat_punct(',') { bail!("long option is required for `{}`", short.unwrap()); } long = flag_name(p)?; if !long.starts_with("--") { bail!("long name must begin with `--`: `{long}`"); } } if long == "--help" { bail!("`--help` flag is generated automatically") } let val = opt_val(p)?; Ok(ast::Flag { arity: ast::Arity::Required, name: long[2..].to_string(), short: short.map(|it| it[1..].to_string()), doc: None, val, }) } fn opt_val(p: &mut Parser) -> Result, Error> { if !p.lookahead_punct(':', 1) { return Ok(None); } let name = p.expect_name()?; p.expect_punct(':')?; let ty = ty(p)?; let res = ast::Val { name, ty }; Ok(Some(res)) } fn arity(p: &mut Parser) -> Result { if p.eat_keyword("optional") { return Ok(ast::Arity::Optional); } if p.eat_keyword("required") { return Ok(ast::Arity::Required); } if p.eat_keyword("repeated") { return Ok(ast::Arity::Repeated); } if let Some(name) = p.eat_name() { bail!("expected one of `optional`, `required`, `repeated`, got `{name}`") } bail!("expected one of `optional`, `required`, `repeated`, got {:?}", p.ts.pop()) } fn ty(p: &mut Parser) -> Result { let name = p.expect_name()?; let res = match name.as_str() { "PathBuf" => ast::Ty::PathBuf, "OsString" => ast::Ty::OsString, _ => ast::Ty::FromStr(name), }; Ok(res) } fn opt_single_doc(p: &mut Parser) -> Result> { if !p.eat_punct('#') { return Ok(None); } p.enter_delim(Delimiter::Bracket)?; p.expect_keyword("doc")?; p.expect_punct('=')?; let mut res = p.expect_string()?; if let Some(suf) = res.strip_prefix(' ') { res = suf.to_string(); } p.exit_delim()?; Ok(Some(res)) } fn opt_doc(p: &mut Parser) -> Result> { let lines = core::iter::from_fn(|| opt_single_doc(p).transpose()).collect::>>()?; let lines = lines.join("\n"); if lines.is_empty() { Ok(None) } else { Ok(Some(lines)) } } fn cmd_name(p: &mut Parser) -> Result { let name = p.expect_name()?; if name.starts_with('-') { bail!("command name can't begin with `-`: `{name}`"); } Ok(name) } fn flag_name(p: &mut Parser) -> Result { let name = p.expect_name()?; if !name.starts_with('-') { bail!("flag name should begin with `-`: `{name}`"); } Ok(name) } struct Parser { stack: Vec>, ts: Vec, idx: u8, } impl Parser { fn new(ts: TokenStream) -> Self { let mut ts = ts.into_iter().collect::>(); ts.reverse(); Self { stack: Vec::new(), ts, idx: 0 } } fn enter_delim(&mut self, delimiter: Delimiter) -> Result<()> { match self.ts.pop() { Some(TokenTree::Group(g)) if g.delimiter() == delimiter => { let mut ts = g.stream().into_iter().collect::>(); ts.reverse(); let ts = mem::replace(&mut self.ts, ts); self.stack.push(ts); } _ => bail!("expected `{{`"), } Ok(()) } fn exit_delim(&mut self) -> Result<()> { if !self.end() { bail!("expected `}}`") } self.ts = self.stack.pop().unwrap(); Ok(()) } fn end(&mut self) -> bool { self.ts.last().is_none() } fn expect_keyword(&mut self, kw: &str) -> Result<()> { if !self.eat_keyword(kw) { bail!("expected `{kw}`") } Ok(()) } fn eat_keyword(&mut self, kw: &str) -> bool { if self.at_keyword(kw) { self.ts.pop().unwrap(); true } else { false } } fn at_keyword(&mut self, kw: &str) -> bool { match self.ts.last() { Some(TokenTree::Ident(ident)) => &ident.to_string() == kw, _ => false, } } fn expect_name(&mut self) -> Result { self.eat_name().ok_or_else(|| { let next = self.ts.pop().map(|it| it.to_string()).unwrap_or_default(); format_err!("expected a name, got: `{next}`") }) } fn eat_name(&mut self) -> Option { let mut buf = String::new(); let mut prev_ident = false; loop { match self.ts.last() { Some(TokenTree::Punct(p)) if p.as_char() == '-' => { prev_ident = false; buf.push('-'); } Some(TokenTree::Ident(ident)) if !prev_ident => { prev_ident = true; buf.push_str(&ident.to_string()); } _ => break, } self.ts.pop(); } if buf.is_empty() { None } else { Some(buf) } } fn _expect_ident(&mut self) -> Result { match self.ts.pop() { Some(TokenTree::Ident(ident)) => Ok(ident.to_string()), _ => bail!("expected ident"), } } fn expect_punct(&mut self, punct: char) -> Result<()> { if !self.eat_punct(punct) { bail!("expected `{punct}`") } Ok(()) } fn eat_punct(&mut self, punct: char) -> bool { match self.ts.last() { Some(TokenTree::Punct(p)) if p.as_char() == punct => { self.ts.pop(); true } _ => false, } } fn lookahead_punct(&mut self, punct: char, n: usize) -> bool { match self.ts.iter().rev().nth(n) { Some(TokenTree::Punct(p)) => p.as_char() == punct, _ => false, } } fn expect_string(&mut self) -> Result { match self.ts.pop() { Some(TokenTree::Literal(lit)) if lit.to_string().starts_with('"') => { let res = str_lit_value(lit.to_string()); Ok(res) } _ => bail!("expected a string"), } } } /// "Parser" a string literal into the corresponding value. /// /// Really needs support in the proc_macro library: /// fn str_lit_value(lit: String) -> String { lit.trim_matches('"').replace("\\'", "'") } xflags-macros-0.3.1/src/update.rs000064400000000000000000000050201046102023000150070ustar 00000000000000use std::{fs, ops::Range, path::Path}; pub(crate) fn in_place(api: &str, path: &Path) { let path = { let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); Path::new(&dir).join(path) }; let mut text = fs::read_to_string(&path).unwrap_or_else(|_| panic!("failed to read {path:?}")); let (insert_to, indent) = locate(&text); let api: String = with_preamble(api) .lines() .map(|it| { if it.trim().is_empty() { "\n".to_string() } else { format!("{}{}\n", indent, it) } }) .collect(); text.replace_range(insert_to, &api); fs::write(&path, text.as_bytes()).unwrap(); } pub(crate) fn stdout(api: &str) { print!("{}", with_preamble(api)) } fn with_preamble(api: &str) -> String { format!( "\ // generated start // The following code is generated by `xflags` macro. // Run `env UPDATE_XFLAGS=1 cargo build` to regenerate. {} // generated end ", api.trim() ) } fn locate(text: &str) -> (Range, String) { if let Some(it) = locate_existing(text) { return it; } if let Some(it) = locate_new(text) { return it; } panic!("failed to update xflags in place") } fn locate_existing(text: &str) -> Option<(Range, String)> { let start_idx = text.find("// generated start")?; let start_idx = newline_before(text, start_idx); let end_idx = text.find("// generated end")?; let end_idx = newline_after(text, end_idx); let indent = indent_at(text, start_idx); Some((start_idx..end_idx, indent)) } fn newline_before(text: &str, start_idx: usize) -> usize { text[..start_idx].rfind('\n').map_or(start_idx, |it| it + 1) } fn newline_after(text: &str, start_idx: usize) -> usize { start_idx + text[start_idx..].find('\n').map_or(text[start_idx..].len(), |it| it + 1) } fn indent_at(text: &str, start_idx: usize) -> String { text[start_idx..].chars().take_while(|&it| it == ' ').collect() } fn locate_new(text: &str) -> Option<(Range, String)> { let mut idx = text.find("xflags!")?; let mut lvl = 0i32; for c in text[idx..].chars() { idx += c.len_utf8(); match c { '{' => lvl += 1, '}' if lvl == 1 => break, '}' => lvl -= 1, _ => (), } } let indent = indent_at(text, newline_before(text, idx)); if text[idx..].starts_with('\n') { idx += 1; } Some((idx..idx, indent)) } xflags-macros-0.3.1/tests/data/help.rs000064400000000000000000000011641046102023000157460ustar 00000000000000xflags! { /// Does stuff /// /// Helpful stuff. cmd helpful { /// With an arg. optional src: PathBuf /// Another arg. /// /// This time, we provide some extra info about the /// arg. Maybe some caveats, or what kinds of /// values are accepted. optional extra: String /// And a switch. required -s, --switch /// And even a subcommand! cmd sub { /// With an optional flag. This has a really long /// description which spans multiple lines. optional -f, --flag } } } xflags-macros-0.3.1/tests/data/repeated_pos.rs000064400000000000000000000002311046102023000174620ustar 00000000000000xflags! { cmd RepeatedPos { required a: PathBuf optional b: u32 optional c: OsString repeated rest: OsString } } xflags-macros-0.3.1/tests/data/smoke.rs000064400000000000000000000006311046102023000161320ustar 00000000000000xflags! { /// LSP server for rust. cmd rust-analyzer { required workspace: PathBuf /// Number of concurrent jobs. optional jobs: u32 /// Path to log file. By default, logs go to stderr. optional --log-file path: PathBuf repeated -v, --verbose required -n, --number n: u32 repeated --data value: OsString optional --emoji } } xflags-macros-0.3.1/tests/data/subcommands.rs000064400000000000000000000005601046102023000173300ustar 00000000000000xflags! { cmd rust-analyzer { repeated -v, --verbose cmd server { optional --dir path:PathBuf default cmd launch { optional --log } cmd watch { } } cmd analysis-stats { required path: PathBuf optional --parallel } } } xflags-macros-0.3.1/tests/it/help.rs000064400000000000000000000070721046102023000154550ustar 00000000000000#![allow(unused)] use std::{ffi::OsString, path::PathBuf}; #[derive(Debug)] pub struct Helpful { pub src: Option, pub extra: Option, pub switch: (), pub subcommand: HelpfulCmd, } #[derive(Debug)] pub enum HelpfulCmd { Sub(Sub), } #[derive(Debug)] pub struct Sub { pub flag: bool, } impl Helpful { #[allow(dead_code)] pub fn from_env_or_exit() -> Self { Self::from_env_or_exit_() } #[allow(dead_code)] pub fn from_env() -> xflags::Result { Self::from_env_() } #[allow(dead_code)] pub fn from_vec(args: Vec) -> xflags::Result { Self::from_vec_(args) } } impl Helpful { fn from_env_or_exit_() -> Self { Self::from_env_().unwrap_or_else(|err| err.exit()) } fn from_env_() -> xflags::Result { let mut p = xflags::rt::Parser::new_from_env(); Self::parse_(&mut p) } fn from_vec_(args: Vec) -> xflags::Result { let mut p = xflags::rt::Parser::new(args); Self::parse_(&mut p) } } impl Helpful { fn parse_(p_: &mut xflags::rt::Parser) -> xflags::Result { #![allow(non_snake_case)] let mut switch = Vec::new(); let mut src = (false, Vec::new()); let mut extra = (false, Vec::new()); let mut sub__flag = Vec::new(); let mut state_ = 0u8; while let Some(arg_) = p_.pop_flag() { match arg_ { Ok(flag_) => match (state_, flag_.as_str()) { (0 | 1, "--switch" | "-s") => switch.push(()), (0 | 1, "--help" | "-h") => return Err(p_.help(Self::HELP_)), (1, "--flag" | "-f") => sub__flag.push(()), _ => return Err(p_.unexpected_flag(&flag_)), }, Err(arg_) => match (state_, arg_.to_str().unwrap_or("")) { (0, "sub") => state_ = 1, (0, _) => { if let (done_ @ false, buf_) = &mut src { buf_.push(arg_.into()); *done_ = true; continue; } if let (done_ @ false, buf_) = &mut extra { buf_.push(p_.value_from_str::("extra", arg_)?); *done_ = true; continue; } return Err(p_.unexpected_arg(arg_)); } _ => return Err(p_.unexpected_arg(arg_)), }, } } Ok(Helpful { switch: p_.required("--switch", switch)?, src: p_.optional("src", src.1)?, extra: p_.optional("extra", extra.1)?, subcommand: match state_ { 1 => HelpfulCmd::Sub(Sub { flag: p_.optional("--flag", sub__flag)?.is_some() }), _ => return Err(p_.subcommand_required()), }, }) } } impl Helpful { const HELP_: &'static str = "\ helpful Does stuff Helpful stuff. ARGS: [src] With an arg. [extra] Another arg. This time, we provide some extra info about the arg. Maybe some caveats, or what kinds of values are accepted. OPTIONS: -s, --switch And a switch. -h, --help Prints help information. SUBCOMMANDS: helpful sub And even a subcommand! OPTIONS: -f, --flag With an optional flag. This has a really long description which spans multiple lines. "; } xflags-macros-0.3.1/tests/it/main.rs000064400000000000000000000171551046102023000154540ustar 00000000000000mod repeated_pos; mod smoke; mod subcommands; mod help; use std::{ffi::OsString, fmt}; use expect_test::{expect, Expect}; fn check(f: F, args: &str, expect: Expect) where F: FnOnce(Vec) -> xflags::Result, A: fmt::Debug, { let args = args.split_ascii_whitespace().map(OsString::from).collect::>(); let res = f(args); match res { Ok(args) => { expect.assert_debug_eq(&args); } Err(err) => { expect.assert_eq(&err.to_string()); } } } #[test] fn smoke() { check( smoke::RustAnalyzer::from_vec, "-n 92 .", expect![[r#" RustAnalyzer { workspace: ".", jobs: None, log_file: None, verbose: 0, number: 92, data: [], emoji: false, } "#]], ); check( smoke::RustAnalyzer::from_vec, "-n 92 -v --verbose -v --data 0xDEAD --log-file /tmp/log.txt --data 0xBEEF .", expect![[r#" RustAnalyzer { workspace: ".", jobs: None, log_file: Some( "/tmp/log.txt", ), verbose: 3, number: 92, data: [ "0xDEAD", "0xBEEF", ], emoji: false, } "#]], ); check( smoke::RustAnalyzer::from_vec, "-n 92 --werbose", expect![[r#"unexpected flag: `--werbose`"#]], ); check(smoke::RustAnalyzer::from_vec, "", expect!["flag is required: `--number`"]); check(smoke::RustAnalyzer::from_vec, ".", expect![[r#"flag is required: `--number`"#]]); check(smoke::RustAnalyzer::from_vec, "-n", expect![[r#"expected a value for `-n`"#]]); check(smoke::RustAnalyzer::from_vec, "-n 92", expect!["flag is required: `workspace`"]); check( smoke::RustAnalyzer::from_vec, "-n lol", expect![[r#"can't parse `-n`, invalid digit found in string"#]], ); check( smoke::RustAnalyzer::from_vec, "-n 1 -n 2 .", expect![[r#"flag specified more than once: `--number`"#]], ); check( smoke::RustAnalyzer::from_vec, "-n 1 . 92 lol", expect![[r#"unexpected argument: "lol""#]], ); check( smoke::RustAnalyzer::from_vec, "-n 1 . --emoji --emoji", expect![[r#"flag specified more than once: `--emoji`"#]], ); } #[test] fn repeated_argument() { check( repeated_pos::RepeatedPos::from_vec, "a 11 c d e f", expect![[r#" RepeatedPos { a: "a", b: Some( 11, ), c: Some( "c", ), rest: [ "d", "e", "f", ], } "#]], ); } #[test] fn subcommands() { check( subcommands::RustAnalyzer::from_vec, "server", expect![[r#" RustAnalyzer { verbose: 0, subcommand: Server( Server { dir: None, subcommand: Launch( Launch { log: false, }, ), }, ), } "#]], ); check( subcommands::RustAnalyzer::from_vec, "server --dir . --log", expect![[r#" RustAnalyzer { verbose: 0, subcommand: Server( Server { dir: Some( ".", ), subcommand: Launch( Launch { log: true, }, ), }, ), } "#]], ); check( subcommands::RustAnalyzer::from_vec, "server watch", expect![[r#" RustAnalyzer { verbose: 0, subcommand: Server( Server { dir: None, subcommand: Watch( Watch, ), }, ), } "#]], ); check( subcommands::RustAnalyzer::from_vec, "-v analysis-stats . --parallel", expect![[r#" RustAnalyzer { verbose: 1, subcommand: AnalysisStats( AnalysisStats { path: ".", parallel: true, }, ), } "#]], ); check(subcommands::RustAnalyzer::from_vec, "", expect![[r#"subcommand is required"#]]); } #[test] fn subcommand_flag_inheritance() { check( subcommands::RustAnalyzer::from_vec, "server watch --verbose --dir .", expect![[r#" RustAnalyzer { verbose: 1, subcommand: Server( Server { dir: Some( ".", ), subcommand: Watch( Watch, ), }, ), } "#]], ); check( subcommands::RustAnalyzer::from_vec, "analysis-stats --verbose --dir .", expect!["unexpected flag: `--dir`"], ); check( subcommands::RustAnalyzer::from_vec, "--dir . server", expect!["unexpected flag: `--dir`"], ); } #[test] fn edge_cases() { check( subcommands::RustAnalyzer::from_vec, "server --dir --log", expect![[r#" RustAnalyzer { verbose: 0, subcommand: Server( Server { dir: Some( "--log", ), subcommand: Launch( Launch { log: false, }, ), }, ), } "#]], ); check( subcommands::RustAnalyzer::from_vec, "server --dir -- --log", expect![[r#" RustAnalyzer { verbose: 0, subcommand: Server( Server { dir: Some( "--", ), subcommand: Launch( Launch { log: true, }, ), }, ), } "#]], ); check( subcommands::RustAnalyzer::from_vec, "-- -v server", expect![[r#"unexpected argument: "-v""#]], ); check(repeated_pos::RepeatedPos::from_vec, "pos 1 prog -j", expect!["unexpected flag: `-j`"]); check( repeated_pos::RepeatedPos::from_vec, "pos 1 -- prog -j", expect![[r#" RepeatedPos { a: "pos", b: Some( 1, ), c: Some( "prog", ), rest: [ "-j", ], } "#]], ); } xflags-macros-0.3.1/tests/it/repeated_pos.rs000064400000000000000000000061021046102023000171700ustar 00000000000000#![allow(unused)] use std::{ffi::OsString, path::PathBuf}; #[derive(Debug)] pub struct RepeatedPos { pub a: PathBuf, pub b: Option, pub c: Option, pub rest: Vec, } impl RepeatedPos { #[allow(dead_code)] pub fn from_env_or_exit() -> Self { Self::from_env_or_exit_() } #[allow(dead_code)] pub fn from_env() -> xflags::Result { Self::from_env_() } #[allow(dead_code)] pub fn from_vec(args: Vec) -> xflags::Result { Self::from_vec_(args) } } impl RepeatedPos { fn from_env_or_exit_() -> Self { Self::from_env_().unwrap_or_else(|err| err.exit()) } fn from_env_() -> xflags::Result { let mut p = xflags::rt::Parser::new_from_env(); Self::parse_(&mut p) } fn from_vec_(args: Vec) -> xflags::Result { let mut p = xflags::rt::Parser::new(args); Self::parse_(&mut p) } } impl RepeatedPos { fn parse_(p_: &mut xflags::rt::Parser) -> xflags::Result { #![allow(non_snake_case)] let mut a = (false, Vec::new()); let mut b = (false, Vec::new()); let mut c = (false, Vec::new()); let mut rest = (false, Vec::new()); let mut state_ = 0u8; while let Some(arg_) = p_.pop_flag() { match arg_ { Ok(flag_) => match (state_, flag_.as_str()) { (0, "--help" | "-h") => return Err(p_.help(Self::HELP_)), _ => return Err(p_.unexpected_flag(&flag_)), }, Err(arg_) => match (state_, arg_.to_str().unwrap_or("")) { (0, _) => { if let (done_ @ false, buf_) = &mut a { buf_.push(arg_.into()); *done_ = true; continue; } if let (done_ @ false, buf_) = &mut b { buf_.push(p_.value_from_str::("b", arg_)?); *done_ = true; continue; } if let (done_ @ false, buf_) = &mut c { buf_.push(arg_.into()); *done_ = true; continue; } if let (false, buf_) = &mut rest { buf_.push(arg_.into()); continue; } return Err(p_.unexpected_arg(arg_)); } _ => return Err(p_.unexpected_arg(arg_)), }, } } Ok(RepeatedPos { a: p_.required("a", a.1)?, b: p_.optional("b", b.1)?, c: p_.optional("c", c.1)?, rest: rest.1, }) } } impl RepeatedPos { const HELP_: &'static str = "\ RepeatedPos ARGS: [b] [c] ... OPTIONS: -h, --help Prints help information. "; } xflags-macros-0.3.1/tests/it/smoke.rs000064400000000000000000000072401046102023000156400ustar 00000000000000#![allow(unused)] use std::{ffi::OsString, path::PathBuf}; #[derive(Debug)] pub struct RustAnalyzer { pub workspace: PathBuf, pub jobs: Option, pub log_file: Option, pub verbose: u32, pub number: u32, pub data: Vec, pub emoji: bool, } impl RustAnalyzer { #[allow(dead_code)] pub fn from_env_or_exit() -> Self { Self::from_env_or_exit_() } #[allow(dead_code)] pub fn from_env() -> xflags::Result { Self::from_env_() } #[allow(dead_code)] pub fn from_vec(args: Vec) -> xflags::Result { Self::from_vec_(args) } } impl RustAnalyzer { fn from_env_or_exit_() -> Self { Self::from_env_().unwrap_or_else(|err| err.exit()) } fn from_env_() -> xflags::Result { let mut p = xflags::rt::Parser::new_from_env(); Self::parse_(&mut p) } fn from_vec_(args: Vec) -> xflags::Result { let mut p = xflags::rt::Parser::new(args); Self::parse_(&mut p) } } impl RustAnalyzer { fn parse_(p_: &mut xflags::rt::Parser) -> xflags::Result { #![allow(non_snake_case)] let mut log_file = Vec::new(); let mut verbose = Vec::new(); let mut number = Vec::new(); let mut data = Vec::new(); let mut emoji = Vec::new(); let mut workspace = (false, Vec::new()); let mut jobs = (false, Vec::new()); let mut state_ = 0u8; while let Some(arg_) = p_.pop_flag() { match arg_ { Ok(flag_) => match (state_, flag_.as_str()) { (0, "--log-file") => log_file.push(p_.next_value(&flag_)?.into()), (0, "--verbose" | "-v") => verbose.push(()), (0, "--number" | "-n") => number.push(p_.next_value_from_str::(&flag_)?), (0, "--data") => data.push(p_.next_value(&flag_)?.into()), (0, "--emoji") => emoji.push(()), (0, "--help" | "-h") => return Err(p_.help(Self::HELP_)), _ => return Err(p_.unexpected_flag(&flag_)), }, Err(arg_) => match (state_, arg_.to_str().unwrap_or("")) { (0, _) => { if let (done_ @ false, buf_) = &mut workspace { buf_.push(arg_.into()); *done_ = true; continue; } if let (done_ @ false, buf_) = &mut jobs { buf_.push(p_.value_from_str::("jobs", arg_)?); *done_ = true; continue; } return Err(p_.unexpected_arg(arg_)); } _ => return Err(p_.unexpected_arg(arg_)), }, } } Ok(RustAnalyzer { log_file: p_.optional("--log-file", log_file)?, verbose: verbose.len() as u32, number: p_.required("--number", number)?, data: data, emoji: p_.optional("--emoji", emoji)?.is_some(), workspace: p_.required("workspace", workspace.1)?, jobs: p_.optional("jobs", jobs.1)?, }) } } impl RustAnalyzer { const HELP_: &'static str = "\ rust-analyzer LSP server for rust. ARGS: [jobs] Number of concurrent jobs. OPTIONS: --log-file Path to log file. By default, logs go to stderr. -v, --verbose -n, --number --data --emoji -h, --help Prints help information. "; } xflags-macros-0.3.1/tests/it/subcommands.rs000064400000000000000000000114241046102023000170340ustar 00000000000000#![allow(unused)] use std::{ffi::OsString, path::PathBuf}; #[derive(Debug)] pub struct RustAnalyzer { pub verbose: u32, pub subcommand: RustAnalyzerCmd, } #[derive(Debug)] pub enum RustAnalyzerCmd { Server(Server), AnalysisStats(AnalysisStats), } #[derive(Debug)] pub struct Server { pub dir: Option, pub subcommand: ServerCmd, } #[derive(Debug)] pub enum ServerCmd { Launch(Launch), Watch(Watch), } #[derive(Debug)] pub struct Launch { pub log: bool, } #[derive(Debug)] pub struct Watch; #[derive(Debug)] pub struct AnalysisStats { pub path: PathBuf, pub parallel: bool, } impl RustAnalyzer { #[allow(dead_code)] pub fn from_env_or_exit() -> Self { Self::from_env_or_exit_() } #[allow(dead_code)] pub fn from_env() -> xflags::Result { Self::from_env_() } #[allow(dead_code)] pub fn from_vec(args: Vec) -> xflags::Result { Self::from_vec_(args) } } impl RustAnalyzer { fn from_env_or_exit_() -> Self { Self::from_env_().unwrap_or_else(|err| err.exit()) } fn from_env_() -> xflags::Result { let mut p = xflags::rt::Parser::new_from_env(); Self::parse_(&mut p) } fn from_vec_(args: Vec) -> xflags::Result { let mut p = xflags::rt::Parser::new(args); Self::parse_(&mut p) } } impl RustAnalyzer { fn parse_(p_: &mut xflags::rt::Parser) -> xflags::Result { #![allow(non_snake_case)] let mut verbose = Vec::new(); let mut server__dir = Vec::new(); let mut server__launch__log = Vec::new(); let mut analysis_stats__parallel = Vec::new(); let mut analysis_stats__path = (false, Vec::new()); let mut state_ = 0u8; while let Some(arg_) = p_.pop_flag() { match arg_ { Ok(flag_) => match (state_, flag_.as_str()) { (0 | 1 | 2 | 3 | 4, "--verbose" | "-v") => verbose.push(()), (0 | 1 | 2 | 3 | 4, "--help" | "-h") => return Err(p_.help(Self::HELP_)), (1 | 2 | 3, "--dir") => server__dir.push(p_.next_value(&flag_)?.into()), (1, _) => { p_.push_back(Ok(flag_)); state_ = 2; } (2, "--log") => server__launch__log.push(()), (4, "--parallel") => analysis_stats__parallel.push(()), _ => return Err(p_.unexpected_flag(&flag_)), }, Err(arg_) => match (state_, arg_.to_str().unwrap_or("")) { (0, "server") => state_ = 1, (0, "analysis-stats") => state_ = 4, (0, _) => { return Err(p_.unexpected_arg(arg_)); } (1, "watch") => state_ = 3, (1, _) => { p_.push_back(Err(arg_)); state_ = 2; } (4, _) => { if let (done_ @ false, buf_) = &mut analysis_stats__path { buf_.push(arg_.into()); *done_ = true; continue; } return Err(p_.unexpected_arg(arg_)); } _ => return Err(p_.unexpected_arg(arg_)), }, } } state_ = if state_ == 1 { 2 } else { state_ }; Ok(RustAnalyzer { verbose: verbose.len() as u32, subcommand: match state_ { 2 | 3 => RustAnalyzerCmd::Server(Server { dir: p_.optional("--dir", server__dir)?, subcommand: match state_ { 2 => ServerCmd::Launch(Launch { log: p_.optional("--log", server__launch__log)?.is_some(), }), 3 => ServerCmd::Watch(Watch {}), _ => return Err(p_.subcommand_required()), }, }), 4 => RustAnalyzerCmd::AnalysisStats(AnalysisStats { parallel: p_.optional("--parallel", analysis_stats__parallel)?.is_some(), path: p_.required("path", analysis_stats__path.1)?, }), _ => return Err(p_.subcommand_required()), }, }) } } impl RustAnalyzer { const HELP_: &'static str = "\ rust-analyzer OPTIONS: -v, --verbose -h, --help Prints help information. SUBCOMMANDS: rust-analyzer server OPTIONS: --dir --log rust-analyzer server watch rust-analyzer analysis-stats ARGS: OPTIONS: --parallel "; }