From 596a1b6818483a1fe795e3cfee31a33c82988bfd Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Wed, 12 Apr 2023 11:40:18 -0700 Subject: [PATCH] Fix use in library crates (#4) - The `CARGO_BIN_NAME` env var is not available when building library crates, so we have to infer the binary name from the environment at runtime. - Consequently, this allows the macro to be used in tests, so we now have a doc-test! - Also uses explicit type paths to avoid certain kinds of name collisions with e.g. top-level `use anyhow::Result`. --- onlyargs_derive/src/lib.rs | 63 +++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/onlyargs_derive/src/lib.rs b/onlyargs_derive/src/lib.rs index 3ea5c8b..0511ece 100644 --- a/onlyargs_derive/src/lib.rs +++ b/onlyargs_derive/src/lib.rs @@ -3,6 +3,36 @@ //! The parser generated by this macro is very opinionated. The implementation attempts to be as //! light as possible while also being usable for most applications. //! +//! # Example +//! +//! ``` +//! use onlyargs_derive::OnlyArgs; +//! +//! /// Doc comments will appear in your application's help text. +//! /// Multi-line comments are also supported. +//! #[derive(Debug, OnlyArgs)] +//! struct Args { +//! /// Optional output path. +//! output: Option, +//! +//! /// Enable verbose output. +//! verbose: bool, +//! } +//! +//! let args: Args = onlyargs::parse()?; +//! +//! if let Some(output) = args.output { +//! if args.verbose { +//! eprintln!("Creating file: `{path}`", path = output.display()); +//! } +//! +//! // Do something with `output`... +//! } +//! # Ok::<_, onlyargs::CliError>(()) +//! ``` +//! +//! # DSL reference +//! //! Only structs with named fields are supported. Doc comments are used for the generated help text. //! Argument names are generated automatically from field names with only a few rules: //! @@ -276,13 +306,32 @@ pub fn derive_parser(input: TokenStream) -> TokenStream { .unwrap_or_default(); let name = ast.name; - let doc_comment = format!("\n{}\n", ast.doc.join("\n")); + let doc_comment = if ast.doc.is_empty() { + String::new() + } else { + format!("\n{}\n", ast.doc.join("\n")) + }; + let bin_name = std::env::var_os("CARGO_BIN_NAME").and_then(|name| name.into_string().ok()); + let help_impl = if bin_name.is_none() { + r#"fn help() -> ! { + let bin_name = ::std::env::args_os() + .next() + .unwrap_or_default() + .to_string_lossy() + .into_owned(); + ::std::eprintln!("{}", Self::HELP.replace("{bin_name}", &bin_name)); + ::std::process::exit(0); + }"# + } else { + "" + }; + let bin_name = bin_name.unwrap_or_else(|| "{bin_name}".to_string()); // Produce final code. let code = TokenStream::from_str(&format!( r#" impl ::onlyargs::OnlyArgs for {name} {{ - const HELP: &'static str = concat!( + const HELP: &'static str = ::std::concat!( env!("CARGO_PKG_NAME"), " v", env!("CARGO_PKG_VERSION"), @@ -291,7 +340,7 @@ pub fn derive_parser(input: TokenStream) -> TokenStream { "\n", {doc_comment:?}, "\nUsage:\n ", - env!("CARGO_BIN_NAME"), + {bin_name:?}, " [flags] [options]", {positional_header:?}, "\n\nFlags:\n", @@ -302,8 +351,14 @@ pub fn derive_parser(input: TokenStream) -> TokenStream { "\n", ); - fn parse(args: Vec) -> Result {{ + {help_impl} + + fn parse(args: Vec<::std::ffi::OsString>) -> + ::std::result::Result + {{ use ::onlyargs::traits::*; + use ::std::option::Option::{{None, Some}}; + use ::std::result::Result::{{Err, Ok}}; {flags_vars} {options_vars}