Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow subcommand flattening #126

Open
theli-ua opened this issue May 24, 2022 · 3 comments
Open

Allow subcommand flattening #126

theli-ua opened this issue May 24, 2022 · 3 comments

Comments

@theli-ua
Copy link
Contributor

theli-ua commented May 24, 2022

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
Copy link
Contributor Author

theli-ua commented May 25, 2022

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

@qhua948
Copy link
Contributor

qhua948 commented Jun 10, 2022

Duplicate of #15

@theli-ua
Copy link
Contributor Author

Duplicate of #15

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants