Skip to content

Instantly share code, notes, and snippets.

@Dzejkop
Last active September 12, 2021 12:16
Show Gist options
  • Save Dzejkop/39235d70be38857276540de1039cef35 to your computer and use it in GitHub Desktop.
Save Dzejkop/39235d70be38857276540de1039cef35 to your computer and use it in GitHub Desktop.
Converting clap apps to StructOpt style
# In your dependencies add the following
syn = "1.0"
quote = "1.0"
proc_macro2 = "1.0"
heck = "0.3"
///! 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