Search code examples
rusterror-handlingcommand-line-interfaceclap

What is the best way of handling error in rust? (CLI with clap)


I'm trying to develop a simple cli bill manager in rust using clap. I want to let the user add new bills by entering its name and value, remove some bills, and I also want to add undo and redo features. As it is my first project in rust, I called it p1. In the terminal, the user must do like this:

p1 -n bill1 -v 50

And then, the bill will be added or

p1 undo 5

Then, the last 5 actions will be undone. But, because of the usual functioning of clap, at least as far as I understood, this behavior is also accepted:

p1 -n bill2 -v 30 redo 30

And I don't want to allow it. I don't want to let the user use flags and subcommands at the same time. So I made some validation. To make it easier for you to help me I will show the pertinent part of the code.

use clap::{Parser, Subcommand};
use std::{collections::HashMap, path::PathBuf};
use home::home_dir;
use std::fs;


#[derive(Parser, Debug)]
struct Args {
    /// The name of the bill
    #[clap(short, long, value_parser)]
    name: Option<String>,
    /// The value of the bill
    #[clap(short, long, value_parser)]
    value: Option<u32>,
    #[clap(subcommand)]
    command: Option<Commands>,
}


#[derive(Subcommand, Debug)]
enum Commands {
    /// Undo
    Undo { undo: Option<u32> },
    /// Redo
    Redo { redo: Option<u32> },
    /// Remove
    Remove { remove: Option<String> },
}

fn validate_args(args: Args) -> Result<Args, String> {
    match (&args.name, &args.value, &args.command) {
        (Some(_), Some(_), None) => Ok(args),
        (None, None, Some(_)) => Ok(args),
        (None, None, None) => Ok(args),
        _ => Err("You can't use options and subcommands at the same time".to_string())
    }
}


fn exit_on_error(error: &Result<Args, String>) {
    println!("{:?}", error);
    panic!("aaaaaaaaaaaaaaaaaa");
}


fn main() {
    let args: Result<Args, String> = validate_args(Args::parse());
    match args {
        Ok(_) => (),
        Err(_) => exit_on_error(&args)
    };
    ...
}

Another thing I need help is. When the user do not insert neither flags nor subcommands, just typing "p1" in the terminal, I want to redirect him to the help subcommand as if he had typed

p1 help

How can I do it?

And also, I'm still not used to the rust style of handle with variable possession. The "exit_on_error" function can only receive borrowed results because, apparently, strings cannot implement Copy. This prevents me to unwrap the Err before printing it, which makes it appears with quotation marks in the terminal. What should I do to workaround It?

Please help me and let me know if something isn't clear in my question.


Solution

  • I'd agree with @SirDarius that you probably shouldn't do this, but eh, doesn't mean it hurts to know how you could do it:

    When the user do not insert neither flags nor subcommands, just typing "p1" in the terminal, I want to redirect him to the help subcommand as if he had typed p1 help

    If knowing whether any arguments were passed at all from the parsed Args is difficult, you can for example sidestep clap and check the argument count from std::env::args()

    if std::env::args().count() <= 1 {
        Args::parse_from(&[
            // Argument 0 is always the program name.
            // Could just use "p1" here, but this is more generic:
            std::env::args()
                .next()
                .as_ref()
                .map(String::as_str)
                .unwrap_or(env!("CARGO_CRATE_NAME")),
            // as if help was typed:
            "help",
        ]);
        unreachable!("Should print help and exit");
    }
    

    I think you can also use clap's ArgGroups to achieve this kind of behaviour, but I find them to be clunky.

    And also, I'm still not used to the rust style of handle with variable possession.

    This is a classic Rust problem, and there's so many things you could do:

    1. Just exit from validate_args directly. It's what Args::parse does, too.
    2. Use let args = match args { Ok(args) => args, Err(e) => exit_on_error(e) }; (Must change the type of exit_on_error for this.)
    3. Make exit_on_error have type fn(Result<Args, String>) -> Args and move the match into it.
    4. Make main return Result<(), Box<dyn std::error::Error>> so you can use ? to unwrap Results (though you would need to choose a different error type with a nice Display implementation type instead of String. Might as well use the anyhow crate.)
    5. Use error.as_ref().err().unwrap() (You could even add Option::cloned to have an owned copy of the error string.)

    Lastly: One question per question on StackOverflow, please.