pacak / bpaf

Command line parser with applicative interface
Apache License 2.0
357 stars 17 forks source link

`external` + `positional` #206

Closed aDogCalledSpot closed 1 year ago

aDogCalledSpot commented 1 year ago

I'm currently building a project manager. I have multiple operations that are defined for the same set of projects. For example, build and clean (amongst many others) which all take a project as a positional argument.

I would like to save the possible projects as an enum in a top-level file so I can access it in both my build.rs and clean.rs.

// projects.rs
use bpaf::*;

#[derive(Debug, Clone, Bpaf)]
pub enum Project {
    ProjectA,
    ProjectB,
    ProjectC,
}
// build.rs
use bpaf::*;
use crate::projects::Project;

#[derive(Debug, Clone, Bpaf)]
pub struct Args {
     #[bpaf(external(crate::projects::project), positional("PROJECT")]
     project: Project,
}

This currently does not work because external always has to be first but positional, being part of "consumer" needs to come before external.

It can be worked around using the combinatoric API, but it would be great to see this limitation removed in the future.

pacak commented 1 year ago

When using bpaf with combinatoric API you start by defining a consumer - let's say a positional and apply extra transformations, fallbacks, help, etc on top of that:

fn jobs() -> impl Parser<usize> {
    positional("JOBS")
        .help("number of jobs to use to compile the project")
        .fallback(10)
}

In derive API those parsers are defined inline based on field type, if name is present, etc, so this derives a combination from 3 different flags: --project-a .. --project-c:

#[derive(Debug, Clone, Bpaf)]
pub enum Project {
    ProjectA,
    ProjectB,
    ProjectC,
}

Generated code is something like

fn project() -> impl Parser<Project> {
   let proj_a = long("project_a").req_flag(Project::ProjectA);
   let proj_b = long("project_b").req_flag(Project::ProjectB);
   let proj_c = long("project_c").req_flag(Project::ProjectC);
   construct!([proj_a, proj_b, proj_c]))
}

external lets you to to start a chain by calling a function instead of using derived or specified consumer, it's more obvious when you have a struct:

struct Foo {
    field_1: usize,
    #[bpaf(external(get_field_2))]
    field_2: usize,
}

generates something like this:

fn foo() -> impl Parser<Foo> {
    let field_1 = long("field-1").argument("ARG");
    let field_2 = get_field_2(); // <- comes from external attribute
    constuct!(Foo { field_1, field_2 }))
}

And you can't really mix several consumers or consumers with external since they need to start this chain and either external or one of the consumers (positional, argument, etc) needs to be first.

Now back to your problem. If you want to consume project name positionally all you need to do is to define a FromStr instance for it. You can do it either by hand or using EnumString macro in strum crate and the magic of positional will take care of the rest.

// projects.rs
use strum::EnumString
#[derive(Debug, Clone, EnumString)]
pub enum Project {
    ProjectA,
    ProjectB,
    ProjectC,
}
// build.rs
use bpaf::*;
use crate::projects::Project;

#[derive(Debug, Clone, Bpaf)]
pub struct Args {
     #[bpaf(positional("PROJECT")]
     project: Project,
}
aDogCalledSpot commented 1 year ago

Wow! Thanks for the detailed answer and quick reply! I already had the FromStr since I needed it for the combinatoric approach anyway. This cleans everything up a lot.