Reporting errors in a command-line program without necessarily quitting

I am attempting to write some small Unix-style command line programs and I am having trouble with error reporting, particularly when the program might suffer both fatal errors (cannot continue after the error is encountered) and non-fatal errors (it is useful to keep going after the first error is encountered, if only to report more errors, but the ultimate process exit status should be unsuccessful).

When all errors are fatal, I can use an outermost skeleton like this:

#[derive(Debug, StructOpt)] struct CommandArgs { ... }
fn main() {
    if let Err(e) = inner_main(CommandArgs::from_args()) {
        eprintln!("program-name: {}", e);
        std::process::exit(1);
    }
}
fn inner_main(args: CommandArgs) -> Result<(), Error> {
    // in here we can use the ? operator to report fatal errors
}

But when all errors are not fatal, inner_main needs to do some of the error reporting itself, and then somehow tell main to exit unsuccessfully without printing further errors, perhaps like this:

#[derive(Debug, StructOpt)] struct CommandArgs { ... }
fn main() {
    use std::process::exit;
    match inner_main(CommandArgs::from_args()) {
        Ok(true) => exit(0),
        Ok(false) => exit(1),
        Err(e) => {
            eprintln!("program-name: {}", e);
            exit(1);
        }
    }
}
fn inner_main(args: CommandArgs) -> Result<bool, Error> {
    let mut failed = false;

    // use ? to report fatal errors as before
    // report non-fatal errors ourselves, then set failed to true

    Ok(failed)
}

So far so good. The problem I'm having is with actually reporting the non-fatal errors, because I can't use ? to do it. Most of the stock Option and Result combinators are also not helpful. I find myself writing things like this:

    if let Some(db) = extradata_db {
        for (addr, result) in results {
            match result {
                Err(e) => {
                    failed = true;
                    eprintln!("program-name: {}: {}", addr, e);
                },
                Ok(addr2) => {
                    out.write_record(&[addr.to_string(),
                                       addr2.to_string()])
                        .unwrap_or_else(|e| {
                            failed = true;
                            eprintln!("program-name: {}: {}",
                                      out_name, e);
                        });
                }
            }
        }
    } else {
        for (addr, result) in results {
            match result {
                Err(e) => {
                    failed = true;
                    eprintln!("program-name: {}: {}", addr, e);
                },
                Ok(addr2) => {
                    match lookup_extra(db, addr, addr2) {
                        Err(e) => {
                            failed = true;
                            eprintln!("program-name: {}: {}", addr2, e);
                        },
                        Ok(record) => {
                            out.write_record(&record)
                                .unwrap_or_else(|e| {
                                    failed = true;
                                    eprintln!("program-name: {}: {}",
                                              out_name, e);
                                });
                        }
                    }
                }
            }
        }
    }

and I can't help thinking there must be a better way...

Hm, I think this is roughly how it should be done, but the repetedness can be abstracted:

struct Emitter { has_errors: bool }

impl Emitter {
    fn emit(&mut self, result: Result<E, T>) -> Option<T> { ... }
    fn has_errors(&self) -> bool;
}

for addr in results.into_iter().filter_map(|r| emitter.emit(r)) {
    ...
}

That way, you can deal with non-fatal errors that bubble up. And passing-in emitter as a &mut argument should avoid bubbling.

More abstractly, fatal errors -- either monad (?), warnings -- writer monad (passing sink as an argument), and you need both.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.