Search code examples
rustcommand-line-interfaceclap

How to parse custom string with Clap derive


I found a link for what i want here: Parse user input String with clap for command line programming But it's not completely clear. I see lots of post using App::new() but i can't find any trace of it in clap documentation.

I want to parse a string inside my rust application (it doesn't come from the cmdline) Currently i'm doing it like this:

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Optional name to operate on
    test: String,
    name: Option<String>,
}

pub fn test(mut to_parse: &String) {
    let Ok(r) = shellwords::split(to_parse) else {
        info!("error during process");
        return;
    };
    let test = Cli::parse_from(r);
    if let Some(name) = test.name.as_deref() {
        println!("Value for name: {}", name);
    }
}

And it kind of work, but maybe i don't understand enough how clap work. It only take positional argument like --test or --name. there must be a way to have a command before argument? And the other question i have is : if i'm using a struct to define a command and use Cli::parse_from() to parse my string, how do i parse my string with multiple command (maybe unclear, but how do i use multiple commands?)

------EDIT I've played with clap a little, and now i've something like this:

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Com,
}

/// Doc comment
#[derive(Subcommand)]
enum Com {
    /// First command
    Test(TestArg),
    ///Second command
    SecondTest(OtherArg),
}
#[derive(Args)]
struct TestArg {
    name: String,
}

#[derive(Args)]
struct OtherArg {
    name: Option<String>,
    and: Option<String>,
}

Nevertheless, when i enter a command in my in-game console (still parse with parse_from), nothing is recognize, i always have the error message.


Solution

  • Your question does not show what you pass in and what error you get, so it's rather hard to point to an issue.

    I think you might be missing the first argument which clap (apparently) requires to work when parsing from an array of string slices, which is the binary name/path.

    Using your code from above (2nd snippet), the snippet below does not work.

    let cli = Cli::parse_from(["test", "asd"]);
    dbg!(cli); // You need to add #[derive(Debug)] to your structs for it to work
    

    as it ends with an error like

    error: The subcommand 'asd' wasn't recognized
    
    Usage: test <COMMAND>
    
    For more information try '--help'
    

    If you look closely, you'll see that the test argument, which we are trying to pass as a subcommand to our Cli struct, is recognized as the binary name.

    If we add an additional argument at the beginning (the content does not matter, it can be even an empty string slice), it does work.

    fn main() {
        let cli = Cli::parse_from(["", "test", "asd"]);
        dbg!(cli);
    }
    

    The above snippet prints successfully parsed arguments

    [src/main.rs:32] cli = Cli {
        command: Test(
            TestArg {
                name: "asd",
            },
        ),
    }
    

    It does work also with the second sub-command SecondTest

    let cli = Cli::parse_from(["", "second-test", "some name"]);
    dbg!(cli);
    

    Prints

    [src/main.rs:35] cli = Cli {
        command: SecondTest(
            OtherArg {
                name: Some(
                    "some name",
                ),
                and: None,
            },
        ),
    }
    

    Nevertheless, it seems like your code is missing some attribute macros in order to work as (I think) you expect it to work.

    Assuming you want to parse it from a string like

    mycmd test --name someName
    

    your TestArg struct should look like this

    #[derive(Args)]
    struct TestArg {
        #[clap(short, long)]
        name: String,
    }
    

    now the name is taken not as a positional argument but as a flag(?) argument (required in this case), where the argument needs to be provided as --name <NAME> or -n <NAME>

    Same for the second subcommand, you could declare it like this

    #[derive(Args, Debug)]
    struct OtherArg {
        #[clap(long)]
        name: Option<String>,
        #[clap(long)]
        and: Option<String>,
    }
    

    And then you need to pass the arguments with flags with long names, like so:

    let cli = Cli::parse_from([
        "",
        "second-test",
        "--name",
        "some name",
        "--and",
        "other name",
    ]);
    dbg!(cli);
    

    which results in

    [src/main.rs:41] cli = Cli {
        command: SecondTest(
            OtherArg {
                name: Some(
                    "some name",
                ),
                and: Some(
                    "other name",
                ),
            },
        ),
    }