As for just numeric constants I would say, enums.
As for Error Termination Codes buckle up.
TLDR: The enum would, in my opinion be the more rustic, way of doing it. And would be adequate for your use case.
Errors are somewhat verbose in rust, and while the principle isn't changing, a lot of language level features are still being worked on. Now generally when you create an Error type, you would want to implement the Error trait, but it can be a lot. A raw rustic way to do it would be like the following (this won't work if you try to compile it):
// application error type
enum CmdErr {
ArgParsing(String), // Store underlying information
File(std::io::Error), // Or the source error
}
// Display is required to implement Error
impl Display for CmdErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CmdErr::ArgParsing(arg) => write!(f, "Failed to parse arg: {}", arg),
CmdErr::File(source) => write!(f, "Failed to process file: {}", source),
}
}
}
// The Error trait is used to specify errors
impl Error for CmdErr {}
// Your Error could have an underlying error, should probably
// implement the From trait for convience
impl From<std::io::Error> for CmdErr {
fn from(error: std::io::Error) -> Self {
CmdErr::File(error)
}
}
// The nightly termination trait returns the exit code
// main will use on err exit
impl Termination for CmdError {
fn report(self) -> i32 {
match self {
CmdErr::ArgParsing(arg) => 0,
CmdErr::File(source) => 1,
}
}
}
// Main can return a Result
fn main() -> Result<(), CmdErr> {
run().or_else(|err| log_error!(err))
}
Note: this won't compile on stable rust because the Termination trait is currently only in nightly builds.
Now as you can see, this is just absolutely awful. But most people tend to use a crate such as thiserror to generate the boilerplate for them through macros. The above would essential become
use thiserror::Error;
#[derive(Debug, Error)]
enum CmdErr {
#[error("Failed to parse the argument {0}")]
ArgParsing(String),
#[error("Failed to process file: {0}")]
File(#[from] std::io::Error),
}
Now this still leaves the termination values, you could impl the enum to return them
impl CmdErr {
fn exit_code(self) -> i32 {
match self {
CmdErr::ArgParsing(_) => 1,
CmdErr::File(_) => 2,
}
}
}
Then in main if a function returned a CmdError you could call the method.
println!("{}", cmd_err); // tell the user what errored
std::process::exit(cmd_err.exit_code()) // exit
Now for your case, do you need to go through and derive the Error trait, impl methods to get the Error codes, propagate all the Errors upto main for handling, impl From for all underlying Errors? Probably not, if you were writing a crate to be used as a library, you may want to have a nice Error type so users could get the detailed error information in a packaged and usable form. But if you just want exit codes and outputing the error messages at the failure site, then C style enum is fine for that.
One day you may be able to derive Termination
#[derive(Debug, Error, Termination)]
enum CmdErr {
#[error("Failed to parse the argument {0}")]
ArgParsing(String),
#[error("Failed to process file: {0}")]
File(#[from] std::io::Error),
}
But we aren't quite there yet 