1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-29 12:07:46 +00:00

Merge pull request #2355 from tertsdiepraam/improve-error-handling

Improve error handling with `UResult`
This commit is contained in:
Sylvestre Ledru 2021-06-28 15:46:14 +02:00 committed by GitHub
commit 2428a1ccfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 797 additions and 297 deletions

View file

@ -26,10 +26,11 @@ use quoting_style::{escape_name, QuotingStyle};
use std::os::windows::fs::MetadataExt;
use std::{
cmp::Reverse,
error::Error,
fmt::Display,
fs::{self, DirEntry, FileType, Metadata},
io::{stdout, BufWriter, Stdout, Write},
path::{Path, PathBuf},
process::exit,
time::{SystemTime, UNIX_EPOCH},
};
#[cfg(unix)]
@ -38,8 +39,8 @@ use std::{
os::unix::fs::{FileTypeExt, MetadataExt},
time::Duration,
};
use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
use uucore::error::{set_exit_code, FromIo, UCustomError, UResult};
use unicode_width::UnicodeWidthStr;
#[cfg(unix)]
@ -125,6 +126,32 @@ pub mod options {
pub static IGNORE: &str = "ignore";
}
#[derive(Debug)]
enum LsError {
InvalidLineWidth(String),
NoMetadata(PathBuf),
}
impl UCustomError for LsError {
fn code(&self) -> i32 {
match self {
LsError::InvalidLineWidth(_) => 2,
LsError::NoMetadata(_) => 1,
}
}
}
impl Error for LsError {}
impl Display for LsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LsError::InvalidLineWidth(s) => write!(f, "invalid line width: '{}'", s),
LsError::NoMetadata(p) => write!(f, "could not open file: '{}'", p.display()),
}
}
}
#[derive(PartialEq, Eq)]
enum Format {
Columns,
@ -218,7 +245,7 @@ struct LongFormat {
impl Config {
#[allow(clippy::cognitive_complexity)]
fn from(options: clap::ArgMatches) -> Config {
fn from(options: clap::ArgMatches) -> UResult<Config> {
let (mut format, opt) = if let Some(format_) = options.value_of(options::FORMAT) {
(
match format_ {
@ -369,15 +396,13 @@ impl Config {
}
};
let width = options
.value_of(options::WIDTH)
.map(|x| {
x.parse::<u16>().unwrap_or_else(|_e| {
show_error!("invalid line width: '{}'", x);
exit(2);
})
})
.or_else(|| termsize::get().map(|s| s.cols));
let width = match options.value_of(options::WIDTH) {
Some(x) => match x.parse::<u16>() {
Ok(u) => Some(u),
Err(_) => return Err(LsError::InvalidLineWidth(x.into()).into()),
},
None => termsize::get().map(|s| s.cols),
};
#[allow(clippy::needless_bool)]
let show_control = if options.is_present(options::HIDE_CONTROL_CHARS) {
@ -528,7 +553,7 @@ impl Config {
Dereference::DirArgs
};
Config {
Ok(Config {
format,
files,
sort,
@ -547,11 +572,12 @@ impl Config {
quoting_style,
indicator_style,
time_style,
}
})
}
}
pub fn uumain(args: impl uucore::Args) -> i32 {
#[uucore_procs::gen_uumain]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = args
.collect_str(InvalidEncodingHandling::Ignore)
.accept_any();
@ -567,7 +593,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_else(|| vec![String::from(".")]);
list(locs, Config::from(matches))
list(locs, Config::from(matches)?)
}
pub fn uu_app() -> App<'static, 'static> {
@ -1190,10 +1216,9 @@ impl PathData {
}
}
fn list(locs: Vec<String>, config: Config) -> i32 {
fn list(locs: Vec<String>, config: Config) -> UResult<()> {
let mut files = Vec::<PathData>::new();
let mut dirs = Vec::<PathData>::new();
let mut has_failed = false;
let mut out = BufWriter::new(stdout());
@ -1202,19 +1227,16 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
let path_data = PathData::new(p, None, None, &config, true);
if path_data.md().is_none() {
show_error!("'{}': {}", &loc, "No such file or directory");
/*
We found an error, the return code of ls should not be 0
And no need to continue the execution
*/
has_failed = true;
show!(std::io::ErrorKind::NotFound
.map_err_context(|| format!("cannot access '{}'", path_data.p_buf.display())));
// We found an error, no need to continue the execution
continue;
}
let show_dir_contents = match path_data.file_type() {
Some(ft) => !config.directory && ft.is_dir(),
None => {
has_failed = true;
set_exit_code(1);
false
}
};
@ -1235,11 +1257,8 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
}
enter_directory(&dir, &config, &mut out);
}
if has_failed {
1
} else {
0
}
Ok(())
}
fn sort_entries(entries: &mut Vec<PathData>, config: &Config) {
@ -1478,7 +1497,7 @@ fn display_item_long(
) {
let md = match item.md() {
None => {
show_error!("could not show file: {}", &item.p_buf.display());
show!(LsError::NoMetadata(item.p_buf.clone()));
return;
}
Some(md) => md,

View file

@ -8,25 +8,26 @@
#[macro_use]
extern crate uucore;
use clap::OsValues;
use clap::{crate_version, App, Arg};
use std::fs;
use std::path::Path;
use uucore::error::{FromIo, UResult, USimpleError};
static ABOUT: &str = "Create the given DIRECTORY(ies) if they do not exist";
static OPT_MODE: &str = "mode";
static OPT_PARENTS: &str = "parents";
static OPT_VERBOSE: &str = "verbose";
static ARG_DIRS: &str = "dirs";
mod options {
pub const MODE: &str = "mode";
pub const PARENTS: &str = "parents";
pub const VERBOSE: &str = "verbose";
pub const DIRS: &str = "dirs";
}
fn get_usage() -> String {
format!("{0} [OPTION]... [USER]", executable!())
}
/**
* Handles option parsing
*/
pub fn uumain(args: impl uucore::Args) -> i32 {
#[uucore_procs::gen_uumain]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let usage = get_usage();
// Linux-specific options, not implemented
@ -34,26 +35,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
// " of each created directory to CTX"),
let matches = uu_app().usage(&usage[..]).get_matches_from(args);
let dirs: Vec<String> = matches
.values_of(ARG_DIRS)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
let verbose = matches.is_present(OPT_VERBOSE);
let recursive = matches.is_present(OPT_PARENTS);
let dirs = matches.values_of_os(options::DIRS).unwrap_or_default();
let verbose = matches.is_present(options::VERBOSE);
let recursive = matches.is_present(options::PARENTS);
// Translate a ~str in octal form to u16, default to 755
// Not tested on Windows
let mode_match = matches.value_of(OPT_MODE);
let mode: u16 = match mode_match {
Some(m) => {
let res: Option<u16> = u16::from_str_radix(m, 8).ok();
match res {
Some(r) => r,
_ => crash!(1, "no mode given"),
}
}
_ => 0o755_u16,
let mode: u16 = match matches.value_of(options::MODE) {
Some(m) => u16::from_str_radix(m, 8)
.map_err(|_| USimpleError::new(1, format!("invalid mode '{}'", m)))?,
None => 0o755_u16,
};
exec(dirs, recursive, mode, verbose)
@ -64,27 +55,27 @@ pub fn uu_app() -> App<'static, 'static> {
.version(crate_version!())
.about(ABOUT)
.arg(
Arg::with_name(OPT_MODE)
Arg::with_name(options::MODE)
.short("m")
.long(OPT_MODE)
.long(options::MODE)
.help("set file mode (not implemented on windows)")
.default_value("755"),
)
.arg(
Arg::with_name(OPT_PARENTS)
Arg::with_name(options::PARENTS)
.short("p")
.long(OPT_PARENTS)
.long(options::PARENTS)
.alias("parent")
.help("make parent directories as needed"),
)
.arg(
Arg::with_name(OPT_VERBOSE)
Arg::with_name(options::VERBOSE)
.short("v")
.long(OPT_VERBOSE)
.long(options::VERBOSE)
.help("print a message for each printed directory"),
)
.arg(
Arg::with_name(ARG_DIRS)
Arg::with_name(options::DIRS)
.multiple(true)
.takes_value(true)
.min_values(1),
@ -94,64 +85,43 @@ pub fn uu_app() -> App<'static, 'static> {
/**
* Create the list of new directories
*/
fn exec(dirs: Vec<String>, recursive: bool, mode: u16, verbose: bool) -> i32 {
let mut status = 0;
let empty = Path::new("");
for dir in &dirs {
fn exec(dirs: OsValues, recursive: bool, mode: u16, verbose: bool) -> UResult<()> {
for dir in dirs {
let path = Path::new(dir);
if !recursive {
if let Some(parent) = path.parent() {
if parent != empty && !parent.exists() {
show_error!(
"cannot create directory '{}': No such file or directory",
path.display()
);
status = 1;
continue;
}
}
}
status |= mkdir(path, recursive, mode, verbose);
show_if_err!(mkdir(path, recursive, mode, verbose));
}
status
Ok(())
}
/**
* Wrapper to catch errors, return 1 if failed
*/
fn mkdir(path: &Path, recursive: bool, mode: u16, verbose: bool) -> i32 {
fn mkdir(path: &Path, recursive: bool, mode: u16, verbose: bool) -> UResult<()> {
let create_dir = if recursive {
fs::create_dir_all
} else {
fs::create_dir
};
if let Err(e) = create_dir(path) {
show_error!("{}: {}", path.display(), e.to_string());
return 1;
}
create_dir(path).map_err_context(|| format!("cannot create directory '{}'", path.display()))?;
if verbose {
println!("{}: created directory '{}'", executable!(), path.display());
}
#[cfg(any(unix, target_os = "redox"))]
fn chmod(path: &Path, mode: u16) -> i32 {
use std::fs::{set_permissions, Permissions};
use std::os::unix::fs::PermissionsExt;
let mode = Permissions::from_mode(u32::from(mode));
if let Err(err) = set_permissions(path, mode) {
show_error!("{}: {}", path.display(), err);
return 1;
}
0
}
#[cfg(windows)]
#[allow(unused_variables)]
fn chmod(path: &Path, mode: u16) -> i32 {
// chmod on Windows only sets the readonly flag, which isn't even honored on directories
0
}
chmod(path, mode)
}
#[cfg(any(unix, target_os = "redox"))]
fn chmod(path: &Path, mode: u16) -> UResult<()> {
use std::fs::{set_permissions, Permissions};
use std::os::unix::fs::PermissionsExt;
let mode = Permissions::from_mode(u32::from(mode));
set_permissions(path, mode)
.map_err_context(|| format!("cannot set permissions '{}'", path.display()))
}
#[cfg(windows)]
fn chmod(_path: &Path, _mode: u16) -> UResult<()> {
// chmod on Windows only sets the readonly flag, which isn't even honored on directories
Ok(())
}

View file

@ -12,8 +12,11 @@
extern crate uucore;
use clap::{crate_version, App, Arg};
use uucore::error::{FromIo, UCustomError, UResult};
use std::env;
use std::error::Error;
use std::fmt::Display;
use std::iter;
use std::path::{is_separator, PathBuf};
@ -37,7 +40,40 @@ fn get_usage() -> String {
format!("{0} [OPTION]... [TEMPLATE]", executable!())
}
pub fn uumain(args: impl uucore::Args) -> i32 {
#[derive(Debug)]
enum MkTempError {
PersistError(PathBuf),
MustEndInX(String),
TooFewXs(String),
ContainsDirSeparator(String),
InvalidTemplate(String),
}
impl UCustomError for MkTempError {}
impl Error for MkTempError {}
impl Display for MkTempError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use MkTempError::*;
match self {
PersistError(p) => write!(f, "could not persist file '{}'", p.display()),
MustEndInX(s) => write!(f, "with --suffix, template '{}' must end in X", s),
TooFewXs(s) => write!(f, "too few X's in template '{}'", s),
ContainsDirSeparator(s) => {
write!(f, "invalid suffix '{}', contains directory separator", s)
}
InvalidTemplate(s) => write!(
f,
"invalid template, '{}'; with --tmpdir, it may not be absolute",
s
),
}
}
}
#[uucore_procs::gen_uumain]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let usage = get_usage();
let matches = uu_app().usage(&usage[..]).get_matches_from(args);
@ -73,47 +109,27 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let dry_run = matches.is_present(OPT_DRY_RUN);
let suppress_file_err = matches.is_present(OPT_QUIET);
let (prefix, rand, suffix) = match parse_template(template) {
Some((p, r, s)) => match matches.value_of(OPT_SUFFIX) {
Some(suf) => {
if s.is_empty() {
(p, r, suf)
} else {
crash!(
1,
"Template should end with 'X' when you specify suffix option."
)
}
}
None => (p, r, s),
},
None => ("", 0, ""),
};
if rand < 3 {
crash!(1, "Too few 'X's in template")
}
if suffix.chars().any(is_separator) {
crash!(1, "suffix cannot contain any path separators");
}
let (prefix, rand, suffix) = parse_template(template, matches.value_of(OPT_SUFFIX))?;
if matches.is_present(OPT_TMPDIR) && PathBuf::from(prefix).is_absolute() {
show_error!(
"invalid template, '{}'; with --tmpdir, it may not be absolute",
template
);
return 1;
};
return Err(MkTempError::InvalidTemplate(template.into()).into());
}
if matches.is_present(OPT_T) {
tmpdir = env::temp_dir()
};
}
if dry_run {
let res = if dry_run {
dry_exec(tmpdir, prefix, rand, suffix)
} else {
exec(tmpdir, prefix, rand, suffix, make_dir, suppress_file_err)
exec(tmpdir, prefix, rand, suffix, make_dir)
};
if suppress_file_err {
// Mapping all UErrors to ExitCodes prevents the errors from being printed
res.map_err(|e| e.code().into())
} else {
res
}
}
@ -173,19 +189,40 @@ pub fn uu_app() -> App<'static, 'static> {
)
}
fn parse_template(temp: &str) -> Option<(&str, usize, &str)> {
fn parse_template<'a>(
temp: &'a str,
suffix: Option<&'a str>,
) -> UResult<(&'a str, usize, &'a str)> {
let right = match temp.rfind('X') {
Some(r) => r + 1,
None => return None,
None => return Err(MkTempError::TooFewXs(temp.into()).into()),
};
let left = temp[..right].rfind(|c| c != 'X').map_or(0, |i| i + 1);
let prefix = &temp[..left];
let rand = right - left;
let suffix = &temp[right..];
Some((prefix, rand, suffix))
if rand < 3 {
return Err(MkTempError::TooFewXs(temp.into()).into());
}
let mut suf = &temp[right..];
if let Some(s) = suffix {
if suf.is_empty() {
suf = s;
} else {
return Err(MkTempError::MustEndInX(temp.into()).into());
}
};
if suf.chars().any(is_separator) {
return Err(MkTempError::ContainsDirSeparator(suf.into()).into());
}
Ok((prefix, rand, suf))
}
pub fn dry_exec(mut tmpdir: PathBuf, prefix: &str, rand: usize, suffix: &str) -> i32 {
pub fn dry_exec(mut tmpdir: PathBuf, prefix: &str, rand: usize, suffix: &str) -> UResult<()> {
let len = prefix.len() + suffix.len() + rand;
let mut buf = String::with_capacity(len);
buf.push_str(prefix);
@ -208,51 +245,35 @@ pub fn dry_exec(mut tmpdir: PathBuf, prefix: &str, rand: usize, suffix: &str) ->
}
tmpdir.push(buf);
println!("{}", tmpdir.display());
0
Ok(())
}
fn exec(dir: PathBuf, prefix: &str, rand: usize, suffix: &str, make_dir: bool, quiet: bool) -> i32 {
let res = if make_dir {
let tmpdir = Builder::new()
.prefix(prefix)
.rand_bytes(rand)
.suffix(suffix)
.tempdir_in(&dir);
// `into_path` consumes the TempDir without removing it
tmpdir.map(|d| d.into_path().to_string_lossy().to_string())
} else {
let tmpfile = Builder::new()
.prefix(prefix)
.rand_bytes(rand)
.suffix(suffix)
.tempfile_in(&dir);
match tmpfile {
Ok(f) => {
// `keep` ensures that the file is not deleted
match f.keep() {
Ok((_, p)) => Ok(p.to_string_lossy().to_string()),
Err(e) => {
show_error!("'{}': {}", dir.display(), e);
return 1;
}
}
}
Err(x) => Err(x),
}
fn exec(dir: PathBuf, prefix: &str, rand: usize, suffix: &str, make_dir: bool) -> UResult<()> {
let context = || {
format!(
"failed to create file via template '{}{}{}'",
prefix,
"X".repeat(rand),
suffix
)
};
match res {
Ok(ref f) => {
println!("{}", f);
0
}
Err(e) => {
if !quiet {
show_error!("{}: {}", e, dir.display());
}
1
}
}
let mut builder = Builder::new();
builder.prefix(prefix).rand_bytes(rand).suffix(suffix);
let path = if make_dir {
builder
.tempdir_in(&dir)
.map_err_context(context)?
.into_path() // `into_path` consumes the TempDir without removing it
} else {
builder
.tempfile_in(&dir)
.map_err_context(context)?
.keep() // `keep` ensures that the file is not deleted
.map_err(|e| MkTempError::PersistError(e.file.path().to_path_buf()))?
.1
};
println!("{}", path.display());
Ok(())
}

View file

@ -16,9 +16,8 @@ extern crate uucore;
use clap::{crate_version, App, Arg, ArgGroup};
use filetime::*;
use std::fs::{self, File};
use std::io::Error;
use std::path::Path;
use std::process;
use uucore::error::{FromIo, UResult, USimpleError};
static ABOUT: &str = "Update the access and modification times of each FILE to the current time.";
pub mod options {
@ -52,57 +51,38 @@ fn get_usage() -> String {
format!("{0} [OPTION]... [USER]", executable!())
}
pub fn uumain(args: impl uucore::Args) -> i32 {
#[uucore_procs::gen_uumain]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let usage = get_usage();
let matches = uu_app().usage(&usage[..]).get_matches_from(args);
let files: Vec<String> = matches
.values_of(ARG_FILES)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
let files = matches.values_of_os(ARG_FILES).unwrap();
let (mut atime, mut mtime) = if matches.is_present(options::sources::REFERENCE) {
stat(
matches.value_of(options::sources::REFERENCE).unwrap(),
!matches.is_present(options::NO_DEREF),
)
} else if matches.is_present(options::sources::DATE)
|| matches.is_present(options::sources::CURRENT)
{
let timestamp = if matches.is_present(options::sources::DATE) {
parse_date(matches.value_of(options::sources::DATE).unwrap())
let (mut atime, mut mtime) =
if let Some(reference) = matches.value_of_os(options::sources::REFERENCE) {
stat(Path::new(reference), !matches.is_present(options::NO_DEREF))?
} else {
parse_timestamp(matches.value_of(options::sources::CURRENT).unwrap())
let timestamp = if let Some(date) = matches.value_of(options::sources::DATE) {
parse_date(date)?
} else if let Some(current) = matches.value_of(options::sources::CURRENT) {
parse_timestamp(current)?
} else {
local_tm_to_filetime(time::now())
};
(timestamp, timestamp)
};
(timestamp, timestamp)
} else {
let now = local_tm_to_filetime(time::now());
(now, now)
};
let mut error_code = 0;
for filename in &files {
let path = &filename[..];
if !Path::new(path).exists() {
for filename in files {
let path = Path::new(filename);
if !path.exists() {
// no-dereference included here for compatibility
if matches.is_present(options::NO_CREATE) || matches.is_present(options::NO_DEREF) {
continue;
}
if let Err(e) = File::create(path) {
match e.kind() {
std::io::ErrorKind::NotFound => {
show_error!("cannot touch '{}': {}", path, "No such file or directory")
}
std::io::ErrorKind::PermissionDenied => {
show_error!("cannot touch '{}': {}", path, "Permission denied")
}
_ => show_error!("cannot touch '{}': {}", path, e),
}
error_code = 1;
show!(e.map_err_context(|| format!("cannot touch '{}'", path.display())));
continue;
};
@ -118,7 +98,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|| matches.is_present(options::MODIFICATION)
|| matches.is_present(options::TIME)
{
let st = stat(path, !matches.is_present(options::NO_DEREF));
let st = stat(path, !matches.is_present(options::NO_DEREF))?;
let time = matches.value_of(options::TIME).unwrap_or("");
if !(matches.is_present(options::ACCESS)
@ -138,29 +118,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
}
if matches.is_present(options::NO_DEREF) {
if let Err(e) = set_symlink_file_times(path, atime, mtime) {
// we found an error, it should fail in any case
error_code = 1;
if e.kind() == std::io::ErrorKind::PermissionDenied {
// GNU compatibility (not-owner.sh)
show_error!("setting times of '{}': {}", path, "Permission denied");
} else {
show_error!("setting times of '{}': {}", path, e);
}
}
} else if let Err(e) = filetime::set_file_times(path, atime, mtime) {
// we found an error, it should fail in any case
error_code = 1;
if e.kind() == std::io::ErrorKind::PermissionDenied {
// GNU compatibility (not-owner.sh)
show_error!("setting times of '{}': {}", path, "Permission denied");
} else {
show_error!("setting times of '{}': {}", path, e);
}
set_symlink_file_times(path, atime, mtime)
} else {
filetime::set_file_times(path, atime, mtime)
}
.map_err_context(|| format!("setting times of '{}'", path.display()))?;
}
error_code
Ok(())
}
pub fn uu_app() -> App<'static, 'static> {
@ -238,28 +203,21 @@ pub fn uu_app() -> App<'static, 'static> {
]))
}
fn stat(path: &str, follow: bool) -> (FileTime, FileTime) {
fn stat(path: &Path, follow: bool) -> UResult<(FileTime, FileTime)> {
let metadata = if follow {
fs::symlink_metadata(path)
} else {
fs::metadata(path)
};
match metadata {
Ok(m) => (
FileTime::from_last_access_time(&m),
FileTime::from_last_modification_time(&m),
),
Err(_) => crash!(
1,
"failed to get attributes of '{}': {}",
path,
Error::last_os_error()
),
}
.map_err_context(|| format!("failed to get attributes of '{}'", path.display()))?;
Ok((
FileTime::from_last_access_time(&metadata),
FileTime::from_last_modification_time(&metadata),
))
}
fn parse_date(str: &str) -> FileTime {
fn parse_date(str: &str) -> UResult<FileTime> {
// This isn't actually compatible with GNU touch, but there doesn't seem to
// be any simple specification for what format this parameter allows and I'm
// not about to implement GNU parse_datetime.
@ -267,18 +225,22 @@ fn parse_date(str: &str) -> FileTime {
let formats = vec!["%c", "%F"];
for f in formats {
if let Ok(tm) = time::strptime(str, f) {
return local_tm_to_filetime(to_local(tm));
return Ok(local_tm_to_filetime(to_local(tm)));
}
}
if let Ok(tm) = time::strptime(str, "@%s") {
// Don't convert to local time in this case - seconds since epoch are not time-zone dependent
return local_tm_to_filetime(tm);
return Ok(local_tm_to_filetime(tm));
}
show_error!("Unable to parse date: {}\n", str);
process::exit(1);
Err(USimpleError::new(
1,
format!("Unable to parse date: {}", str),
))
}
fn parse_timestamp(s: &str) -> FileTime {
fn parse_timestamp(s: &str) -> UResult<FileTime> {
let now = time::now();
let (format, ts) = match s.chars().count() {
15 => ("%Y%m%d%H%M.%S", s.to_owned()),
@ -287,31 +249,28 @@ fn parse_timestamp(s: &str) -> FileTime {
10 => ("%y%m%d%H%M", s.to_owned()),
11 => ("%Y%m%d%H%M.%S", format!("{}{}", now.tm_year + 1900, s)),
8 => ("%Y%m%d%H%M", format!("{}{}", now.tm_year + 1900, s)),
_ => panic!("Unknown timestamp format"),
_ => return Err(USimpleError::new(1, format!("invalid date format '{}'", s))),
};
match time::strptime(&ts, format) {
Ok(tm) => {
let mut local = to_local(tm);
local.tm_isdst = -1;
let ft = local_tm_to_filetime(local);
let tm = time::strptime(&ts, format)
.map_err(|_| USimpleError::new(1, format!("invalid date format '{}'", s)))?;
// We have to check that ft is valid time. Due to daylight saving
// time switch, local time can jump from 1:59 AM to 3:00 AM,
// in which case any time between 2:00 AM and 2:59 AM is not valid.
// Convert back to local time and see if we got the same value back.
let ts = time::Timespec {
sec: ft.unix_seconds(),
nsec: 0,
};
let tm2 = time::at(ts);
if tm.tm_hour != tm2.tm_hour {
show_error!("invalid date format {}", s);
process::exit(1);
}
let mut local = to_local(tm);
local.tm_isdst = -1;
let ft = local_tm_to_filetime(local);
ft
}
Err(e) => panic!("Unable to parse timestamp\n{}", e),
// We have to check that ft is valid time. Due to daylight saving
// time switch, local time can jump from 1:59 AM to 3:00 AM,
// in which case any time between 2:00 AM and 2:59 AM is not valid.
// Convert back to local time and see if we got the same value back.
let ts = time::Timespec {
sec: ft.unix_seconds(),
nsec: 0,
};
let tm2 = time::at(ts);
if tm.tm_hour != tm2.tm_hour {
return Err(USimpleError::new(1, format!("invalid date format '{}'", s)));
}
Ok(ft)
}

View file

@ -27,6 +27,7 @@ mod parser; // string parsing modules
// * cross-platform modules
pub use crate::mods::backup_control;
pub use crate::mods::coreopts;
pub use crate::mods::error;
pub use crate::mods::os;
pub use crate::mods::panic;
pub use crate::mods::ranges;

View file

@ -21,6 +21,24 @@ macro_rules! executable(
})
);
#[macro_export]
macro_rules! show(
($err:expr) => ({
let e = $err;
uucore::error::set_exit_code(e.code());
eprintln!("{}: {}", executable!(), e);
})
);
#[macro_export]
macro_rules! show_if_err(
($res:expr) => ({
if let Err(e) = $res {
show!(e);
}
})
);
/// Show an error to stderr in a similar style to GNU coreutils.
#[macro_export]
macro_rules! show_error(

View file

@ -2,6 +2,7 @@
pub mod backup_control;
pub mod coreopts;
pub mod error;
pub mod os;
pub mod panic;
pub mod ranges;

View file

@ -0,0 +1,479 @@
//! All utils return exit with an exit code. Usually, the following scheme is used:
//! * `0`: succeeded
//! * `1`: minor problems
//! * `2`: major problems
//!
//! This module provides types to reconcile these exit codes with idiomatic Rust error
//! handling. This has a couple advantages over manually using [`std::process::exit`]:
//! 1. It enables the use of `?`, `map_err`, `unwrap_or`, etc. in `uumain`.
//! 1. It encourages the use of `UResult`/`Result` in functions in the utils.
//! 1. The error messages are largely standardized across utils.
//! 1. Standardized error messages can be created from external result types
//! (i.e. [`std::io::Result`] & `clap::ClapResult`).
//! 1. `set_exit_code` takes away the burden of manually tracking exit codes for non-fatal errors.
//!
//! # Usage
//! The signature of a typical util should be:
//! ```ignore
//! fn uumain(args: impl uucore::Args) -> UResult<()> {
//! ...
//! }
//! ```
//! [`UResult`] is a simple wrapper around [`Result`] with a custom error type: [`UError`]. The
//! most important difference with types implementing [`std::error::Error`] is that [`UError`]s
//! can specify the exit code of the program when they are returned from `uumain`:
//! * When `Ok` is returned, the code set with [`set_exit_code`] is used as exit code. If
//! [`set_exit_code`] was not used, then `0` is used.
//! * When `Err` is returned, the code corresponding with the error is used as exit code and the
//! error message is displayed.
//!
//! Additionally, the errors can be displayed manually with the [`show`] and [`show_if_err`] macros:
//! ```ignore
//! let res = Err(USimpleError::new(1, "Error!!"));
//! show_if_err!(res);
//! // or
//! if let Err(e) = res {
//! show!(e);
//! }
//! ```
//!
//! **Note**: The [`show`] and [`show_if_err`] macros set the exit code of the program using
//! [`set_exit_code`]. See the documentation on that function for more information.
//!
//! # Guidelines
//! * Use common errors where possible.
//! * Add variants to [`UCommonError`] if an error appears in multiple utils.
//! * Prefer proper custom error types over [`ExitCode`] and [`USimpleError`].
//! * [`USimpleError`] may be used in small utils with simple error handling.
//! * Using [`ExitCode`] is not recommended but can be useful for converting utils to use
//! [`UResult`].
use std::{
error::Error,
fmt::{Display, Formatter},
sync::atomic::{AtomicI32, Ordering},
};
static EXIT_CODE: AtomicI32 = AtomicI32::new(0);
/// Get the last exit code set with [`set_exit_code`].
/// The default value is `0`.
pub fn get_exit_code() -> i32 {
EXIT_CODE.load(Ordering::SeqCst)
}
/// Set the exit code for the program if `uumain` returns `Ok(())`.
///
/// This function is most useful for non-fatal errors, for example when applying an operation to
/// multiple files:
/// ```ignore
/// use uucore::error::{UResult, set_exit_code};
///
/// fn uumain(args: impl uucore::Args) -> UResult<()> {
/// ...
/// for file in files {
/// let res = some_operation_that_might_fail(file);
/// match res {
/// Ok() => {},
/// Err(_) => set_exit_code(1),
/// }
/// }
/// Ok(()) // If any of the operations failed, 1 is returned.
/// }
/// ```
pub fn set_exit_code(code: i32) {
EXIT_CODE.store(code, Ordering::SeqCst);
}
/// Should be returned by all utils.
///
/// Two additional methods are implemented on [`UResult`] on top of the normal [`Result`] methods:
/// `map_err_code` & `map_err_code_message`.
///
/// These methods are used to convert [`UCommonError`]s into errors with a custom error code and
/// message.
pub type UResult<T> = Result<T, UError>;
trait UResultTrait<T> {
fn map_err_code(self, mapper: fn(&UCommonError) -> Option<i32>) -> Self;
fn map_err_code_and_message(self, mapper: fn(&UCommonError) -> Option<(i32, String)>) -> Self;
}
impl<T> UResultTrait<T> for UResult<T> {
fn map_err_code(self, mapper: fn(&UCommonError) -> Option<i32>) -> Self {
if let Err(UError::Common(error)) = self {
if let Some(code) = mapper(&error) {
Err(UCommonErrorWithCode { code, error }.into())
} else {
Err(error.into())
}
} else {
self
}
}
fn map_err_code_and_message(self, mapper: fn(&UCommonError) -> Option<(i32, String)>) -> Self {
if let Err(UError::Common(ref error)) = self {
if let Some((code, message)) = mapper(error) {
return Err(USimpleError { code, message }.into());
}
}
self
}
}
/// The error type of [`UResult`].
///
/// `UError::Common` errors are defined in [`uucore`](crate) while `UError::Custom` errors are
/// defined by the utils.
/// ```
/// use uucore::error::USimpleError;
/// let err = USimpleError::new(1, "Error!!".into());
/// assert_eq!(1, err.code());
/// assert_eq!(String::from("Error!!"), format!("{}", err));
/// ```
pub enum UError {
Common(UCommonError),
Custom(Box<dyn UCustomError>),
}
impl UError {
pub fn code(&self) -> i32 {
match self {
UError::Common(e) => e.code(),
UError::Custom(e) => e.code(),
}
}
}
impl From<UCommonError> for UError {
fn from(v: UCommonError) -> Self {
UError::Common(v)
}
}
impl From<i32> for UError {
fn from(v: i32) -> Self {
UError::Custom(Box::new(ExitCode(v)))
}
}
impl<E: UCustomError + 'static> From<E> for UError {
fn from(v: E) -> Self {
UError::Custom(Box::new(v) as Box<dyn UCustomError>)
}
}
impl Display for UError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
UError::Common(e) => e.fmt(f),
UError::Custom(e) => e.fmt(f),
}
}
}
/// Custom errors defined by the utils.
///
/// All errors should implement [`std::error::Error`], [`std::fmt::Display`] and
/// [`std::fmt::Debug`] and have an additional `code` method that specifies the exit code of the
/// program if the error is returned from `uumain`.
///
/// An example of a custom error from `ls`:
/// ```
/// use uucore::error::{UCustomError};
/// use std::{
/// error::Error,
/// fmt::{Display, Debug},
/// path::PathBuf
/// };
///
/// #[derive(Debug)]
/// enum LsError {
/// InvalidLineWidth(String),
/// NoMetadata(PathBuf),
/// }
///
/// impl UCustomError for LsError {
/// fn code(&self) -> i32 {
/// match self {
/// LsError::InvalidLineWidth(_) => 2,
/// LsError::NoMetadata(_) => 1,
/// }
/// }
/// }
///
/// impl Error for LsError {}
///
/// impl Display for LsError {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// match self {
/// LsError::InvalidLineWidth(s) => write!(f, "invalid line width: '{}'", s),
/// LsError::NoMetadata(p) => write!(f, "could not open file: '{}'", p.display()),
/// }
/// }
/// }
/// ```
/// A crate like [`quick_error`](https://crates.io/crates/quick-error) might also be used, but will
/// still require an `impl` for the `code` method.
pub trait UCustomError: Error {
fn code(&self) -> i32 {
1
}
}
impl From<Box<dyn UCustomError>> for i32 {
fn from(e: Box<dyn UCustomError>) -> i32 {
e.code()
}
}
/// A [`UCommonError`] with an overridden exit code.
///
/// This exit code is returned instead of the default exit code for the [`UCommonError`]. This is
/// typically created with the either the `UResult::map_err_code` or `UCommonError::with_code`
/// method.
#[derive(Debug)]
pub struct UCommonErrorWithCode {
code: i32,
error: UCommonError,
}
impl Error for UCommonErrorWithCode {}
impl Display for UCommonErrorWithCode {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
self.error.fmt(f)
}
}
impl UCustomError for UCommonErrorWithCode {
fn code(&self) -> i32 {
self.code
}
}
/// A simple error type with an exit code and a message that implements [`UCustomError`].
///
/// It is typically created with the `UResult::map_err_code_and_message` method. Alternatively, it
/// can be constructed by manually:
/// ```
/// use uucore::error::{UResult, USimpleError};
/// let err = USimpleError { code: 1, message: "error!".into()};
/// let res: UResult<()> = Err(err.into());
/// // or using the `new` method:
/// let res: UResult<()> = Err(USimpleError::new(1, "error!".into()));
/// ```
#[derive(Debug)]
pub struct USimpleError {
pub code: i32,
pub message: String,
}
impl USimpleError {
#[allow(clippy::new_ret_no_self)]
pub fn new(code: i32, message: String) -> UError {
UError::Custom(Box::new(Self { code, message }))
}
}
impl Error for USimpleError {}
impl Display for USimpleError {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
self.message.fmt(f)
}
}
impl UCustomError for USimpleError {
fn code(&self) -> i32 {
self.code
}
}
/// Wrapper type around [`std::io::Error`].
///
/// The messages displayed by [`UIoError`] should match the error messages displayed by GNU
/// coreutils.
///
/// There are two ways to construct this type: with [`UIoError::new`] or by calling the
/// [`FromIo::map_err_context`] method on a [`std::io::Result`] or [`std::io::Error`].
/// ```
/// use uucore::error::{FromIo, UResult, UIoError, UCommonError};
/// use std::fs::File;
/// use std::path::Path;
/// let path = Path::new("test.txt");
///
/// // Manual construction
/// let e: UIoError = UIoError::new(
/// std::io::ErrorKind::NotFound,
/// format!("cannot access '{}'", path.display())
/// );
/// let res: UResult<()> = Err(e.into());
///
/// // Converting from an `std::io::Error`.
/// let res: UResult<File> = File::open(path).map_err_context(|| format!("cannot access '{}'", path.display()));
/// ```
#[derive(Debug)]
pub struct UIoError {
context: String,
inner: std::io::Error,
}
impl UIoError {
pub fn new(kind: std::io::ErrorKind, context: String) -> Self {
Self {
context,
inner: std::io::Error::new(kind, ""),
}
}
pub fn code(&self) -> i32 {
1
}
}
impl Error for UIoError {}
impl Display for UIoError {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
use std::io::ErrorKind::*;
write!(
f,
"{}: {}",
self.context,
match self.inner.kind() {
NotFound => "No such file or directory",
PermissionDenied => "Permission denied",
ConnectionRefused => "Connection refused",
ConnectionReset => "Connection reset",
ConnectionAborted => "Connection aborted",
NotConnected => "Not connected",
AddrInUse => "Address in use",
AddrNotAvailable => "Address not available",
BrokenPipe => "Broken pipe",
AlreadyExists => "Already exists",
WouldBlock => "Would block",
InvalidInput => "Invalid input",
InvalidData => "Invalid data",
TimedOut => "Timed out",
WriteZero => "Write zero",
Interrupted => "Interrupted",
Other => "Other",
UnexpectedEof => "Unexpected end of file",
_ => panic!("Unexpected io error: {}", self.inner),
},
)
}
}
/// Enables the conversion from `std::io::Error` to `UError` and from `std::io::Result` to
/// `UResult`.
pub trait FromIo<T> {
fn map_err_context(self, context: impl FnOnce() -> String) -> T;
}
impl FromIo<UIoError> for std::io::Error {
fn map_err_context(self, context: impl FnOnce() -> String) -> UIoError {
UIoError {
context: (context)(),
inner: self,
}
}
}
impl<T> FromIo<UResult<T>> for std::io::Result<T> {
fn map_err_context(self, context: impl FnOnce() -> String) -> UResult<T> {
self.map_err(|e| UError::Common(UCommonError::Io(e.map_err_context(context))))
}
}
impl FromIo<UIoError> for std::io::ErrorKind {
fn map_err_context(self, context: impl FnOnce() -> String) -> UIoError {
UIoError {
context: (context)(),
inner: std::io::Error::new(self, ""),
}
}
}
impl From<UIoError> for UCommonError {
fn from(e: UIoError) -> UCommonError {
UCommonError::Io(e)
}
}
impl From<UIoError> for UError {
fn from(e: UIoError) -> UError {
let common: UCommonError = e.into();
common.into()
}
}
/// Common errors for utilities.
///
/// If identical errors appear across multiple utilities, they should be added here.
#[derive(Debug)]
pub enum UCommonError {
Io(UIoError),
// Clap(UClapError),
}
impl UCommonError {
pub fn with_code(self, code: i32) -> UCommonErrorWithCode {
UCommonErrorWithCode { code, error: self }
}
pub fn code(&self) -> i32 {
1
}
}
impl From<UCommonError> for i32 {
fn from(common: UCommonError) -> i32 {
match common {
UCommonError::Io(e) => e.code(),
}
}
}
impl Error for UCommonError {}
impl Display for UCommonError {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
UCommonError::Io(e) => e.fmt(f),
}
}
}
/// A special error type that does not print any message when returned from
/// `uumain`. Especially useful for porting utilities to using [`UResult`].
///
/// There are two ways to construct an [`ExitCode`]:
/// ```
/// use uucore::error::{ExitCode, UResult};
/// // Explicit
/// let res: UResult<()> = Err(ExitCode(1).into());
///
/// // Using into on `i32`:
/// let res: UResult<()> = Err(1.into());
/// ```
/// This type is especially useful for a trivial conversion from utils returning [`i32`] to
/// returning [`UResult`].
#[derive(Debug)]
pub struct ExitCode(pub i32);
impl Error for ExitCode {}
impl Display for ExitCode {
fn fmt(&self, _: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
Ok(())
}
}
impl UCustomError for ExitCode {
fn code(&self) -> i32 {
self.0
}
}

View file

@ -1,6 +1,10 @@
// Copyright (C) ~ Roy Ivy III <rivy.dev@gmail.com>; MIT license
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{self, parse_macro_input, ItemFn};
//## rust proc-macro background info
//* ref: <https://dev.to/naufraghi/procedural-macro-in-rust-101-k3f> @@ <http://archive.is/Vbr5e>
@ -41,7 +45,7 @@ impl syn::parse::Parse for Tokens {
}
#[proc_macro]
pub fn main(stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
pub fn main(stream: TokenStream) -> TokenStream {
let Tokens { expr } = syn::parse_macro_input!(stream as Tokens);
proc_dbg!(&expr);
@ -78,5 +82,32 @@ pub fn main(stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
std::process::exit(code);
}
};
proc_macro::TokenStream::from(result)
TokenStream::from(result)
}
#[proc_macro_attribute]
pub fn gen_uumain(_args: TokenStream, stream: TokenStream) -> TokenStream {
let mut ast = parse_macro_input!(stream as ItemFn);
// Change the name of the function to "uumain_result" to prevent name-conflicts
ast.sig.ident = Ident::new("uumain_result", Span::call_site());
let new = quote!(
pub fn uumain(args: impl uucore::Args) -> i32 {
#ast
let result = uumain_result(args);
match result {
Ok(()) => uucore::error::get_exit_code(),
Err(e) => {
let s = format!("{}", e);
if s != "" {
show_error!("{}", s);
}
e.code()
}
}
}
);
TokenStream::from(new)
}

View file

@ -125,7 +125,8 @@ fn test_mktemp_mktemp_t() {
.arg(TEST_TEMPLATE8)
.fails()
.no_stdout()
.stderr_contains("suffix cannot contain any path separators");
.stderr_contains("invalid suffix")
.stderr_contains("contains directory separator");
}
#[test]