argh icon indicating copy to clipboard operation
argh copied to clipboard

Allow subcommand flattening

Open theli-ua opened this issue 2 years ago • 3 comments

There is already an issue open for struct flattening. This one is specifically for subcommand flattenin. Here is use-case example (using proc-macro from second comment):

#[derive(FromArgs, PartialEq, Debug)]
/// Top-level command.
struct TopLevel {
    #[argh(subcommand)]
    nested: MySubCommandEnum,
}

#[derive(PartialEq, Debug, FlattenSubcommand)]
enum MySubCommandEnum {
    Global(MySubCommandEnumGlobal),
    Sys(MySubCommandEnumSys),
}

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand)]
enum MySubCommandEnumGlobal {
    One(SubCommandOne),
    Two(SubCommandTwo),
}

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand)]
enum MySubCommandEnumSys {
    Three(SubCommandThree),
    Four(SubCommandFour),
}

#[derive(FromArgs, PartialEq, Debug)]
/// First subcommand.
#[argh(subcommand, name = "one")]
struct SubCommandOne {}

#[derive(FromArgs, PartialEq, Debug)]
/// Second subcommand.
#[argh(subcommand, name = "two")]
struct SubCommandTwo {
    #[argh(option)]
    /// x
    x: usize,
}

#[derive(FromArgs, PartialEq, Debug)]
/// Third subcommand.
#[argh(subcommand, name = "three")]
struct SubCommandThree {}

#[derive(FromArgs, PartialEq, Debug)]
/// Fourth subcommand.
#[argh(subcommand, name = "four")]
struct SubCommandFour {}

fn main() {
    let up: TopLevel = argh::from_env();
    println!("{:?}", up);
}

theli-ua avatar May 24 '22 23:05 theli-ua

For my usecase I ended up writing following derive proc macro:

#[proc_macro_derive(FlattenSubcommand)]
pub fn flatten_subcommand(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let ast = syn::parse_macro_input!(input as syn::DeriveInput);
    let de = match ast.data {
        syn::Data::Enum(v) => v,
        _ => unreachable!(),
    };
    let name = &ast.ident;

    // An enum variant like `<name>(<ty>)`
    struct SubCommandVariant<'a> {
        name: &'a syn::Ident,
        ty: &'a syn::Type,
    }

    let variants: Vec<SubCommandVariant<'_>> = de
        .variants
        .iter()
        .map(|variant| {
            let name = &variant.ident;
            let ty = match &variant.fields {
                syn::Fields::Unnamed(field) => {
                    if field.unnamed.len() != 1 {
                        unreachable!()
                    }

                    &field.unnamed.first().unwrap().ty
                }
                _ => unreachable!(),
            };
            SubCommandVariant { name, ty }
        })
        .collect();

    let variant_ty = variants.iter().map(|x| x.ty).collect::<Vec<_>>();
    let variant_names = variants.iter().map(|x| x.name).collect::<Vec<_>>();

    (quote! {
        impl argh::FromArgs for #name {
            fn from_args(command_name: &[&str], args: &[&str])
                -> std::result::Result<Self, argh::EarlyExit>
            {
                let subcommand_name = if let Some(subcommand_name) = command_name.last() {
                    *subcommand_name
                } else {
                    return Err(argh::EarlyExit::from("no subcommand name".to_owned()));
                };

                #(
                    if <#variant_ty as argh::SubCommands>::COMMANDS
                    .iter()
                    .find(|ci| ci.name.eq(subcommand_name))
                    .is_some()
                    {
                        return <#variant_ty as argh::FromArgs>::from_args(command_name, args)
                            .map(|v| Self::#variant_names(v));
                    }
                )*

                Err(argh::EarlyExit::from("no subcommand matched".to_owned()))
            }

            fn redact_arg_values(command_name: &[&str], args: &[&str]) -> std::result::Result<Vec<String>, argh::EarlyExit> {
                let subcommand_name = if let Some(subcommand_name) = command_name.last() {
                    *subcommand_name
                } else {
                    return Err(argh::EarlyExit::from("no subcommand name".to_owned()));
                };

                #(
                    if <#variant_ty as argh::SubCommands>::COMMANDS
                    .iter()
                    .find(|ci| ci.name.eq(subcommand_name))
                    .is_some()
                    {
                        return <#variant_ty as argh::FromArgs>::redact_arg_values(
                            command_name,
                            args,
                        );
                    }

                )*

                Err(argh::EarlyExit::from("no subcommand matched".to_owned()))
            }
        }

        impl argh::SubCommands for #name {
            const COMMANDS: &'static [&'static argh::CommandInfo] = {
                const TOTAL_LEN: usize = #(<#variant_ty as argh::SubCommands>::COMMANDS.len())+*;
                const COMMANDS: [&'static argh::CommandInfo; TOTAL_LEN] = {
                    let slices = &[#(<#variant_ty as argh::SubCommands>::COMMANDS,)*];
                    // Its not possible for slices[0][0] to be invalid
                    let mut output = [slices[0][0]; TOTAL_LEN];

                    let mut output_index = 0;
                    let mut which_slice = 0;
                    while which_slice < slices.len() {
                        let slice = &slices[which_slice];
                        let mut index_in_slice = 0;
                        while index_in_slice < slice.len() {
                            output[output_index] = slice[index_in_slice];
                            output_index += 1;
                            index_in_slice += 1;
                        }
                        which_slice += 1;
                    } 
                    output
                };
                &COMMANDS
            };
        }
    })
    .into()
}

However I'm not sure if argh is interested in this feature and if adding another derive macro is the way they would like to do that

theli-ua avatar May 25 '22 21:05 theli-ua

Duplicate of https://github.com/google/argh/issues/15

qhua948 avatar Jun 10 '22 09:06 qhua948

Duplicate of #15

#15 specifically talks about structs and i'm talking about flattening command enums here

theli-ua avatar Jun 10 '22 16:06 theli-ua