Last active
September 12, 2021 12:16
-
-
Save Dzejkop/39235d70be38857276540de1039cef35 to your computer and use it in GitHub Desktop.
Converting clap apps to StructOpt style
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# In your dependencies add the following | |
syn = "1.0" | |
quote = "1.0" | |
proc_macro2 = "1.0" | |
heck = "0.3" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
///! Requires a custom version of clap (works with 2.33) | |
///! with OptBuilder and FlagsBuilder exposed to public interface | |
use clap::{ | |
args::{FlagBuilder, OptBuilder}, | |
App, ArgSettings, | |
}; | |
use proc_macro2::{Span, TokenStream}; | |
use syn::Ident; | |
pub fn clap_app_to_structopt(app: &App) -> TokenStream { | |
let span = Span::call_site(); | |
let mut arg_struct_idents = vec![]; | |
let mut arg_structs = quote::quote!(); | |
let app_name = heck::CamelCase::to_camel_case(app.p.meta.name.as_str()); | |
let app_subcommands_ident = Ident::new(&format!("{}Subcommands", app_name), span); | |
let app_struct_ident = Ident::new(&app_name, span); | |
for subcommand in &app.p.subcommands { | |
let subname = heck::CamelCase::to_camel_case(subcommand.p.meta.name.as_str()); | |
let subcommand_args_variant_name = Ident::new(&subname, span); | |
let subcommand_args_struct_name = Ident::new(&format!("{}", subname), span); | |
arg_struct_idents.push(( | |
subcommand_args_variant_name, | |
subcommand_args_struct_name.clone(), | |
)); | |
let inner = clap_app_to_structopt(subcommand); | |
arg_structs = quote::quote! { | |
#arg_structs | |
#inner | |
}; | |
} | |
let variants = arg_struct_idents.iter().map(|(variant_name, struct_name)| { | |
quote::quote! { | |
#variant_name(#struct_name) | |
} | |
}); | |
let flags = app.p.flags.iter().map(flag_to_structopt); | |
let opts = app.p.opts.iter().map(opt_to_structopt); | |
let has_subcommands = app.p.has_subcommands(); | |
let has_opts = app.p.has_opts(); | |
let has_flags = app.p.has_flags(); | |
if has_subcommands && !(has_opts || has_flags) { | |
quote::quote! { | |
#[derive(Debug, Clone, StructOpt)] | |
#[structopt(rename_all = "kebab-case")] | |
pub enum #app_struct_ident { | |
#(#variants),* | |
} | |
#arg_structs | |
} | |
} else if arg_struct_idents.is_empty() { | |
quote::quote! { | |
#[derive(Debug, Clone, StructOpt)] | |
#[structopt(rename_all = "kebab-case")] | |
pub struct #app_struct_ident { | |
#(#opts)* | |
#(#flags)* | |
} | |
} | |
} else { | |
quote::quote! { | |
#[derive(Debug, Clone, StructOpt)] | |
#[structopt(rename_all = "kebab-case")] | |
pub struct #app_struct_ident { | |
#(#opts)* | |
#(#flags)* | |
#[structopt(subcommand)] | |
pub subcommand: #app_subcommands_ident | |
} | |
#[derive(Debug, Clone, StructOpt)] | |
#[structopt(rename_all = "kebab-case")] | |
pub enum #app_subcommands_ident { | |
#(#variants),* | |
} | |
#arg_structs | |
} | |
} | |
} | |
fn flag_to_structopt(flag: &FlagBuilder) -> TokenStream { | |
let flag_name = heck::SnakeCase::to_snake_case(flag.b.name); | |
let ident = Ident::new(&flag_name, Span::call_site()); | |
let mut docs = vec![]; | |
if let Some(help) = &flag.b.help { | |
for line in help.lines() { | |
docs.push(quote::quote! { | |
#[doc = #line] | |
}); | |
} | |
} | |
quote::quote! { | |
#(#docs)* | |
#[structopt(long)] | |
pub #ident: bool, | |
} | |
} | |
fn opt_to_structopt(opt: &OptBuilder) -> TokenStream { | |
let opt_name = heck::SnakeCase::to_snake_case(opt.b.name); | |
let ident = Ident::new(&opt_name, Span::call_site()); | |
let is_required = opt.b.settings.is_set(ArgSettings::Required); | |
let has_default_value = opt.v.default_val.is_some(); | |
let ty = if is_required || has_default_value { | |
quote::quote! { String } | |
} else { | |
quote::quote! { Option<String> } | |
}; | |
let mut docs = vec![]; | |
if let Some(help) = &opt.b.help { | |
for line in help.lines() { | |
docs.push(quote::quote! { | |
#[doc = #line] | |
}); | |
} | |
} | |
let def_val = if let Some(v) = &opt.v.default_val { | |
let v = v.to_string_lossy(); | |
quote::quote! { | |
#[structopt(default_value = #v)] | |
} | |
} else { | |
quote::quote!() | |
}; | |
let value_name = if let Some(v) = &opt.v.val_names { | |
let vname = &v[0]; | |
quote::quote! { | |
#[structopt(value_name = #vname)] | |
} | |
} else { | |
quote::quote!() | |
}; | |
quote::quote! { | |
#(#docs)* | |
#[structopt(long)] | |
#def_val | |
#value_name | |
pub #ident: #ty, | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment