Search code examples
rustclap

Return an error and exit gracefully in clap cli


I'd like to implement a simple clap cli that operates on git repositories, but that isn't critical to the question; it will help clarify I believe. I'm trying to identify the most idiomatic way to exit with an error if not run from the root of a repository. Here are three options; I'm not sure any are good.

What is the best way to do these steps:

  1. check that I've run from repo root
  2. if so continue, if not exit
  3. if no command is given, generate help
  4. if command is given, run the command

Ideally, I'd be able to output the error and usage. Also, there will be other errors that happen in the subcommands, and I'm not sure the best way to exit gracefully in those cases.

Consider the following cli definition:

use clap::ErrorKind::Io;
use clap::{Parser, Subcommand};
use git2::Repository;
use std::process;

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

#[derive(Debug, Subcommand)]
enum Commands {
    /// Do a thing.
    Do,
}

The three main options I see currently are:

Option 1

fn main() -> Result<(), String> {
    let repo = match Repository::open(".") {
        Ok(repo) => repo,
        Err(_) => return Err("must be run from root of repository".to_owned()),
    };
    let args = Cli::parse();
    match args.command {
        Commands::Do => {
            println!("{:?}: Doing a thing with the repository.", repo.workdir());
        }
    }
    Ok(())
}

Option 2

fn main() {
    let repo = match Repository::open(".") {
        Ok(repo) => repo,
        Err(_) => {
            eprintln!("{}", "must be run from root of repository".to_owned());
            process::exit(1);
        }
    };
    let args = Cli::parse();
    match args.command {
        Commands::Do => {
            println!("{:?}: Doing a thing with the repository.", repo.workdir());
        }
    }
}

Option 3

fn main() -> clap::Result<(), clap::Error> {
    let repo = match Repository::open(".") {
        Ok(repo) => repo,
        Err(_) => return Err(clap::Error::raw(Io, "not in repo")),
    };
    let args = Cli::parse();
    match args.command {
        Commands::Do => {
            println!("{:?}: Doing a thing with the repository.", repo.workdir());
        }
    }
    Ok(())
}

Are any or all of these horrific, serviceable, or improvable?


I see a closure vote for seeking subjective information, but what I'm after is maybe more binary than it seems. I will of course respect the will of the community, but I am wondering if any or all of these are grossly out of the norm of are problematic for some reason.


Solution

  • I'm personally a big fan of error wrapper libraries like anyhow, eyre or miette.

    Some remarks:

    • consider using .map_err() instead of match for transposing an error into a different one, it's a lot more compact and easier to read
    • consider using the ? operator instead of return to propagate errors upwards. The ? operator is a short version of "unwrap if Ok and return error if Err" and makes code a lot easier to read.

    Here's an example using anyhow:

    fn main() -> anyhow::Result<()> {
        let repo = Repository::open(".").map_err(|_| anyhow!("must be run from root of repository"))?;
        let args = Cli::parse();
        match args.command {
            Commands::Do => {
                println!("{:?}: Doing a thing with the repository.", repo.workdir());
            }
        }
        Ok(())
    }