diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 6ca3f4bbe..059981e28 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -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 { 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::().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::() { + 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, config: Config) -> i32 { +fn list(locs: Vec, config: Config) -> UResult<()> { let mut files = Vec::::new(); let mut dirs = Vec::::new(); - let mut has_failed = false; let mut out = BufWriter::new(stdout()); @@ -1202,19 +1227,16 @@ fn list(locs: Vec, 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, config: Config) -> i32 { } enter_directory(&dir, &config, &mut out); } - if has_failed { - 1 - } else { - 0 - } + + Ok(()) } fn sort_entries(entries: &mut Vec, 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, diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index 82d561213..a99867570 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -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 = 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::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, 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(()) +} diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index bbccf6628..8a4b472aa 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -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(()) } diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 3e9ff5624..dd2b05d0e 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -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 = 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 { // 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 { 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) } diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index bf2e5b1bb..00477cd7e 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -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; diff --git a/src/uucore/src/lib/macros.rs b/src/uucore/src/lib/macros.rs index 07d47eed8..e4d83e746 100644 --- a/src/uucore/src/lib/macros.rs +++ b/src/uucore/src/lib/macros.rs @@ -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( diff --git a/src/uucore/src/lib/mods.rs b/src/uucore/src/lib/mods.rs index 2689361a0..040da2f02 100644 --- a/src/uucore/src/lib/mods.rs +++ b/src/uucore/src/lib/mods.rs @@ -2,6 +2,7 @@ pub mod backup_control; pub mod coreopts; +pub mod error; pub mod os; pub mod panic; pub mod ranges; diff --git a/src/uucore/src/lib/mods/error.rs b/src/uucore/src/lib/mods/error.rs new file mode 100644 index 000000000..f13c777b6 --- /dev/null +++ b/src/uucore/src/lib/mods/error.rs @@ -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 = Result; + +trait UResultTrait { + fn map_err_code(self, mapper: fn(&UCommonError) -> Option) -> Self; + fn map_err_code_and_message(self, mapper: fn(&UCommonError) -> Option<(i32, String)>) -> Self; +} + +impl UResultTrait for UResult { + fn map_err_code(self, mapper: fn(&UCommonError) -> Option) -> 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), +} + +impl UError { + pub fn code(&self) -> i32 { + match self { + UError::Common(e) => e.code(), + UError::Custom(e) => e.code(), + } + } +} + +impl From for UError { + fn from(v: UCommonError) -> Self { + UError::Common(v) + } +} + +impl From for UError { + fn from(v: i32) -> Self { + UError::Custom(Box::new(ExitCode(v))) + } +} + +impl From for UError { + fn from(v: E) -> Self { + UError::Custom(Box::new(v) as Box) + } +} + +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> for i32 { + fn from(e: Box) -> 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::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 { + fn map_err_context(self, context: impl FnOnce() -> String) -> T; +} + +impl FromIo for std::io::Error { + fn map_err_context(self, context: impl FnOnce() -> String) -> UIoError { + UIoError { + context: (context)(), + inner: self, + } + } +} + +impl FromIo> for std::io::Result { + fn map_err_context(self, context: impl FnOnce() -> String) -> UResult { + self.map_err(|e| UError::Common(UCommonError::Io(e.map_err_context(context)))) + } +} + +impl FromIo 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 for UCommonError { + fn from(e: UIoError) -> UCommonError { + UCommonError::Io(e) + } +} + +impl From 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 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 + } +} diff --git a/src/uucore_procs/src/lib.rs b/src/uucore_procs/src/lib.rs index e0d247c3f..93567a12d 100644 --- a/src/uucore_procs/src/lib.rs +++ b/src/uucore_procs/src/lib.rs @@ -1,6 +1,10 @@ // Copyright (C) ~ Roy Ivy III ; 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: @@ @@ -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) } diff --git a/tests/by-util/test_mktemp.rs b/tests/by-util/test_mktemp.rs index bcf75ee20..e824df061 100644 --- a/tests/by-util/test_mktemp.rs +++ b/tests/by-util/test_mktemp.rs @@ -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]