diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index ffe1a73f30b..c10ad18a21c 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -417,14 +417,14 @@ jobs: --arg multisize "$SIZE_MULTI" \ '{($date): { sha: $sha, size: $size, multisize: $multisize, }}' > size-result.json - name: Download the previous individual size result - uses: dawidd6/action-download-artifact@v7 + uses: dawidd6/action-download-artifact@v8 with: workflow: CICD.yml name: individual-size-result repo: uutils/coreutils path: dl - name: Download the previous size result - uses: dawidd6/action-download-artifact@v7 + uses: dawidd6/action-download-artifact@v8 with: workflow: CICD.yml name: size-result diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 35eaf8cb35b..e4a2558fb99 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -91,7 +91,7 @@ jobs: working-directory: ${{ steps.vars.outputs.path_GNU }} - name: Retrieve reference artifacts - uses: dawidd6/action-download-artifact@v7 + uses: dawidd6/action-download-artifact@v8 # ref: continue-on-error: true ## don't break the build for missing reference artifacts (may be expired or just not generated yet) with: @@ -105,7 +105,7 @@ jobs: run: | ## Install dependencies sudo apt-get update - sudo apt-get install -y autoconf autopoint bison texinfo gperf gcc g++ gdb python3-pyinotify jq valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev libselinux1-dev attr + sudo apt-get install -y autoconf autopoint bison texinfo gperf gcc g++ gdb python3-pyinotify jq valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev libselinux1-dev attr quilt - name: Add various locales shell: bash run: | diff --git a/.github/workflows/ignore-intermittent.txt b/.github/workflows/ignore-intermittent.txt index eb3d8b54bf5..ed6e3b6ce80 100644 --- a/.github/workflows/ignore-intermittent.txt +++ b/.github/workflows/ignore-intermittent.txt @@ -1,3 +1,5 @@ tests/tail/inotify-dir-recreate tests/timeout/timeout tests/rm/rm1 +tests/misc/stdbuf +tests/misc/usage_vs_getopt diff --git a/Cargo.lock b/Cargo.lock index 16d6039c021..b02f7eaa905 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -337,18 +337,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.26" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.26" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -861,7 +861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1276,7 +1276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -1996,7 +1996,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2241,7 +2241,7 @@ dependencies = [ "getrandom", "once_cell", "rustix 0.38.43", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2617,9 +2617,7 @@ name = "uu_date" version = "0.0.29" dependencies = [ "chrono", - "chrono-tz", "clap", - "iana-time-zone", "libc", "parse_datetime", "uucore", @@ -2879,11 +2877,9 @@ version = "0.0.29" dependencies = [ "ansi-width", "chrono", - "chrono-tz", "clap", "glob", "hostname", - "iana-time-zone", "lscolors", "number_prefix", "once_cell", @@ -3472,6 +3468,8 @@ version = "0.0.29" dependencies = [ "blake2b_simd", "blake3", + "chrono", + "chrono-tz", "clap", "crc32fast", "data-encoding", @@ -3481,6 +3479,7 @@ dependencies = [ "dunce", "glob", "hex", + "iana-time-zone", "itertools 0.14.0", "lazy_static", "libc", @@ -3656,7 +3655,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7178771ae21..b4c369eb3cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -576,6 +576,7 @@ semicolon_if_nothing_returned = "warn" single_char_pattern = "warn" explicit_iter_loop = "warn" if_not_else = "warn" +manual_if_else = "warn" all = { level = "deny", priority = -1 } cargo = { level = "warn", priority = -1 } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 6f1de3b5476..0f3a3691d9d 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -241,6 +241,8 @@ DEBUG=1 bash util/run-gnu-test.sh tests/misc/sm3sum.pl Note that GNU test suite relies on individual utilities (not the multicall binary). +You also need to install [quilt](https://savannah.nongnu.org/projects/quilt), a tool used to manage a stack of patches for modifying GNU tests. + On FreeBSD, you need to install packages for GNU coreutils and sed (used in shell scripts instead of system commands): ```shell diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index f502fb23466..3df9c98a3b3 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -125,16 +125,13 @@ fn basename(fullname: &str, suffix: &str) -> String { // Convert to path buffer and get last path component let pb = PathBuf::from(path); - match pb.components().last() { - Some(c) => { - let name = c.as_os_str().to_str().unwrap(); - if name == suffix { - name.to_string() - } else { - name.strip_suffix(suffix).unwrap_or(name).to_string() - } - } - None => String::new(), - } + pb.components().next_back().map_or_else(String::new, |c| { + let name = c.as_os_str().to_str().unwrap(); + if name == suffix { + name.to_string() + } else { + name.strip_suffix(suffix).unwrap_or(name).to_string() + } + }) } diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 84be146ec08..cf95d1bd24a 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -13,8 +13,9 @@ use std::iter; use std::path::Path; use uucore::checksum::{ calculate_blake2b_length, detect_algo, digest_reader, perform_checksum_validation, - ChecksumError, ChecksumOptions, ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, - ALGORITHM_OPTIONS_CRC, ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SYSV, SUPPORTED_ALGORITHMS, + ChecksumError, ChecksumOptions, ChecksumVerbose, ALGORITHM_OPTIONS_BLAKE2B, + ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC, ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SYSV, + SUPPORTED_ALGORITHMS, }; use uucore::{ encoding, @@ -322,13 +323,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { || iter::once(OsStr::new("-")).collect::>(), |files| files.map(OsStr::new).collect::>(), ); + + let verbose = ChecksumVerbose::new(status, quiet, warn); + let opts = ChecksumOptions { binary: binary_flag, ignore_missing, - quiet, - status, strict, - warn, + verbose, }; return perform_checksum_validation(files.iter().copied(), algo_option, length, opts); @@ -462,19 +464,22 @@ pub fn uu_app() -> Command { .short('w') .long("warn") .help("warn about improperly formatted checksum lines") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::STATUS, options::QUIET]), ) .arg( Arg::new(options::STATUS) .long("status") .help("don't output anything, status code shows success") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::WARN, options::QUIET]), ) .arg( Arg::new(options::QUIET) .long(options::QUIET) .help("don't print OK for each successfully verified file") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::WARN, options::STATUS]), ) .arg( Arg::new(options::IGNORE_MISSING) diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 87e8d383a75..71f52250742 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -20,10 +20,8 @@ path = "src/date.rs" [dependencies] chrono = { workspace = true } clap = { workspace = true } -uucore = { workspace = true } +uucore = { workspace = true, features = ["custom-tz-fmt"] } parse_datetime = { workspace = true } -chrono-tz = { workspace = true } -iana-time-zone = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index f4d420c3fd2..a3f2ad0426c 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -6,17 +6,16 @@ // spell-checker:ignore (chrono) Datelike Timelike ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes use chrono::format::{Item, StrftimeItems}; -use chrono::{DateTime, FixedOffset, Local, Offset, TimeDelta, TimeZone, Utc}; +use chrono::{DateTime, FixedOffset, Local, Offset, TimeDelta, Utc}; #[cfg(windows)] use chrono::{Datelike, Timelike}; -use chrono_tz::{OffsetName, Tz}; use clap::{crate_version, Arg, ArgAction, Command}; -use iana_time_zone::get_timezone; #[cfg(all(unix, not(target_os = "macos"), not(target_os = "redox")))] use libc::{clock_settime, timespec, CLOCK_REALTIME}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; +use uucore::custom_tz_fmt::custom_time_format; use uucore::display::Quotable; use uucore::error::FromIo; use uucore::error::{UResult, USimpleError}; @@ -274,21 +273,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { for date in dates { match date { Ok(date) => { - // TODO - Revisit when chrono 0.5 is released. https://github.com/chronotope/chrono/issues/970 - let tz = match std::env::var("TZ") { - // TODO Support other time zones... - Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC, - _ => match get_timezone() { - Ok(tz_str) => tz_str.parse().unwrap(), - Err(_) => Tz::Etc__UTC, - }, - }; - let offset = tz.offset_from_utc_date(&Utc::now().date_naive()); - let tz_abbreviation = offset.abbreviation(); - // GNU `date` uses `%N` for nano seconds, however crate::chrono uses `%f` - let format_string = &format_string - .replace("%N", "%f") - .replace("%Z", tz_abbreviation.unwrap_or("UTC")); + let format_string = custom_time_format(format_string); // Refuse to pass this string to chrono as it is crashing in this crate if format_string.contains("%#z") { return Err(USimpleError::new( @@ -298,7 +283,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } // Hack to work around panic in chrono, // TODO - remove when a fix for https://github.com/chronotope/chrono/issues/623 is released - let format_items = StrftimeItems::new(format_string); + let format_items = StrftimeItems::new(format_string.as_str()); if format_items.clone().any(|i| i == Item::Error) { return Err(USimpleError::new( 1, diff --git a/src/uu/df/src/filesystem.rs b/src/uu/df/src/filesystem.rs index 6f59e2c1027..401a3bec7b5 100644 --- a/src/uu/df/src/filesystem.rs +++ b/src/uu/df/src/filesystem.rs @@ -54,7 +54,7 @@ fn is_over_mounted(mounts: &[MountInfo], mount: &MountInfo) -> bool { let last_mount_for_dir = mounts .iter() .filter(|m| m.mount_dir == mount.mount_dir) - .last(); + .next_back(); if let Some(lmi) = last_mount_for_dir { lmi.dev_name != mount.dev_name diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index b000857a882..c4d9ef70ee6 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -130,14 +130,11 @@ fn parse_signal_opt<'a>(opts: &mut Options<'a>, opt: &'a OsStr) -> UResult<()> { } }); for sig in sig_vec { - let sig_str = match sig.to_str() { - Some(s) => s, - None => { - return Err(USimpleError::new( - 1, - format!("{}: invalid signal", sig.quote()), - )) - } + let Some(sig_str) = sig.to_str() else { + return Err(USimpleError::new( + 1, + format!("{}: invalid signal", sig.quote()), + )); }; let sig_val = parse_signal_value(sig_str)?; if !opts.ignore_signal.contains(&sig_val) { diff --git a/src/uu/fmt/src/parasplit.rs b/src/uu/fmt/src/parasplit.rs index 8aa18c4c987..f9da4ad58fd 100644 --- a/src/uu/fmt/src/parasplit.rs +++ b/src/uu/fmt/src/parasplit.rs @@ -255,9 +255,8 @@ impl ParagraphStream<'_> { if l_slice.starts_with("From ") { true } else { - let colon_posn = match l_slice.find(':') { - Some(n) => n, - None => return false, + let Some(colon_posn) = l_slice.find(':') else { + return false; }; // header field must be nonzero length @@ -560,12 +559,11 @@ impl<'a> Iterator for WordSplit<'a> { // find the start of the next word, and record if we find a tab character let (before_tab, after_tab, word_start) = - match self.analyze_tabs(&self.string[old_position..]) { - (b, a, Some(s)) => (b, a, s + old_position), - (_, _, None) => { - self.position = self.length; - return None; - } + if let (b, a, Some(s)) = self.analyze_tabs(&self.string[old_position..]) { + (b, a, s + old_position) + } else { + self.position = self.length; + return None; }; // find the beginning of the next whitespace diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 1d3a758f5ea..b8dc63c323d 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -24,6 +24,7 @@ use uucore::checksum::escape_filename; use uucore::checksum::perform_checksum_validation; use uucore::checksum::ChecksumError; use uucore::checksum::ChecksumOptions; +use uucore::checksum::ChecksumVerbose; use uucore::checksum::HashAlgorithm; use uucore::error::{FromIo, UResult}; use uucore::sum::{Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256}; @@ -240,13 +241,14 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { || iter::once(OsStr::new("-")).collect::>(), |files| files.map(OsStr::new).collect::>(), ); + + let verbose = ChecksumVerbose::new(status, quiet, warn); + let opts = ChecksumOptions { binary, ignore_missing, - quiet, - status, strict, - warn, + verbose, }; // Execute the checksum validation @@ -356,14 +358,16 @@ pub fn uu_app_common() -> Command { .short('q') .long(options::QUIET) .help("don't print OK for each successfully verified file") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::STATUS, options::WARN]), ) .arg( Arg::new(options::STATUS) .short('s') .long("status") .help("don't output anything, status code shows success") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::QUIET, options::WARN]), ) .arg( Arg::new(options::STRICT) @@ -382,7 +386,8 @@ pub fn uu_app_common() -> Command { .short('w') .long("warn") .help("warn about improperly formatted checksum lines") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::QUIET, options::STATUS]), ) .arg( Arg::new("zero") diff --git a/src/uu/head/src/parse.rs b/src/uu/head/src/parse.rs index dce60bae012..619b48e05e9 100644 --- a/src/uu/head/src/parse.rs +++ b/src/uu/head/src/parse.rs @@ -91,9 +91,8 @@ fn process_num_block( } if let Some(n) = multiplier { options.push(OsString::from("-c")); - let num = match num.checked_mul(n) { - Some(n) => n, - None => return Some(Err(ParseError::Overflow)), + let Some(num) = num.checked_mul(n) else { + return Some(Err(ParseError::Overflow)); }; options.push(OsString::from(format!("{num}"))); } else { diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index cf810937794..89522f15d1f 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -652,7 +652,7 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR } let mut targetpath = target_dir.to_path_buf(); - let filename = sourcepath.components().last().unwrap(); + let filename = sourcepath.components().next_back().unwrap(); targetpath.push(filename); show_if_err!(copy(sourcepath, &targetpath, b)); diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index 1dc3526538d..632bdc7d65a 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -154,12 +154,7 @@ fn handle_obsolete(args: &mut Vec) -> Option { } fn table() { - // GNU kill doesn't list the EXIT signal with --table, so we ignore it, too - for (idx, signal) in ALL_SIGNALS - .iter() - .enumerate() - .filter(|(_, s)| **s != "EXIT") - { + for (idx, signal) in ALL_SIGNALS.iter().enumerate() { println!("{idx: >#2} {signal}"); } } @@ -183,8 +178,7 @@ fn print_signal(signal_name_or_value: &str) -> UResult<()> { } fn print_signals() { - // GNU kill doesn't list the EXIT signal with --list, so we ignore it, too - for signal in ALL_SIGNALS.iter().filter(|x| **x != "EXIT") { + for signal in ALL_SIGNALS { println!("{signal}"); } } diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index 0b60009e65b..a21f178542a 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -19,11 +19,9 @@ path = "src/ls.rs" [dependencies] ansi-width = { workspace = true } chrono = { workspace = true } -chrono-tz = { workspace = true } clap = { workspace = true, features = ["env"] } glob = { workspace = true } hostname = { workspace = true } -iana-time-zone = { workspace = true } lscolors = { workspace = true } number_prefix = { workspace = true } once_cell = { workspace = true } @@ -31,6 +29,7 @@ selinux = { workspace = true, optional = true } terminal_size = { workspace = true } uucore = { workspace = true, features = [ "colors", + "custom-tz-fmt", "entries", "format", "fs", diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 9aaa0d0a4e3..680fe94ab4c 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -27,14 +27,12 @@ use std::{ use std::{collections::HashSet, io::IsTerminal}; use ansi_width::ansi_width; -use chrono::{DateTime, Local, TimeDelta, TimeZone, Utc}; -use chrono_tz::{OffsetName, Tz}; +use chrono::{DateTime, Local, TimeDelta}; use clap::{ builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, crate_version, Arg, ArgAction, Command, }; use glob::{MatchOptions, Pattern}; -use iana_time_zone::get_timezone; use lscolors::LsColors; use term_grid::{Direction, Filling, Grid, GridOptions}; @@ -60,6 +58,7 @@ use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; use uucore::line_ending::LineEnding; use uucore::quoting_style::{self, escape_name, QuotingStyle}; use uucore::{ + custom_tz_fmt, display::Quotable, error::{set_exit_code, UError, UResult}, format_usage, @@ -345,31 +344,6 @@ fn is_recent(time: DateTime) -> bool { time + TimeDelta::try_seconds(31_556_952 / 2).unwrap() > Local::now() } -/// Get the alphabetic abbreviation of the current timezone. -/// -/// For example, "UTC" or "CET" or "PDT". -fn timezone_abbrev() -> String { - let tz = match std::env::var("TZ") { - // TODO Support other time zones... - Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC, - _ => match get_timezone() { - Ok(tz_str) => tz_str.parse().unwrap(), - Err(_) => Tz::Etc__UTC, - }, - }; - let offset = tz.offset_from_utc_date(&Utc::now().date_naive()); - offset.abbreviation().unwrap_or("UTC").to_string() -} - -/// Format the given time according to a custom format string. -fn custom_time_format(fmt: &str, time: DateTime) -> String { - // TODO Refactor the common code from `ls` and `date` for rendering dates. - // TODO - Revisit when chrono 0.5 is released. https://github.com/chronotope/chrono/issues/970 - // GNU `date` uses `%N` for nano seconds, however the `chrono` crate uses `%f`. - let fmt = fmt.replace("%N", "%f").replace("%Z", &timezone_abbrev()); - time.format(&fmt).to_string() -} - impl TimeStyle { /// Format the given time according to this time format style. fn format(&self, time: DateTime) -> String { @@ -386,7 +360,9 @@ impl TimeStyle { //So it's not yet implemented (Self::Locale, true) => time.format("%b %e %H:%M").to_string(), (Self::Locale, false) => time.format("%b %e %Y").to_string(), - (Self::Format(e), _) => custom_time_format(e, time), + (Self::Format(fmt), _) => time + .format(custom_tz_fmt::custom_time_format(fmt).as_str()) + .to_string(), } } } @@ -403,8 +379,8 @@ fn parse_time_style(options: &clap::ArgMatches) -> Result { //If both FULL_TIME and TIME_STYLE are present //The one added last is dominant if options.get_flag(options::FULL_TIME) - && options.indices_of(options::FULL_TIME).unwrap().last() - > options.indices_of(options::TIME_STYLE).unwrap().last() + && options.indices_of(options::FULL_TIME).unwrap().next_back() + > options.indices_of(options::TIME_STYLE).unwrap().next_back() { Ok(TimeStyle::FullIso) } else { diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 0ee5101d7ef..323bf18300f 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -101,8 +101,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let options = SeqOptions { separator: matches .get_one::(OPT_SEPARATOR) - .map(|s| s.as_str()) - .unwrap_or("\n") + .map_or("\n", |s| s.as_str()) .to_string(), terminator: matches .get_one::(OPT_TERMINATOR) @@ -150,13 +149,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let precision = select_precision(first_precision, increment_precision, last_precision); - let format = match options.format { - Some(f) => { - let f = Format::::parse(f)?; - Some(f) - } - None => None, - }; + let format = options + .format + .map(Format::::parse) + .transpose()?; + let result = print_seq( (first.number, increment.number, last.number), precision, @@ -164,12 +161,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { &options.terminator, options.equal_width, padding, - &format, + format.as_ref(), ); match result { - Ok(_) => Ok(()), + Ok(()) => Ok(()), Err(err) if err.kind() == ErrorKind::BrokenPipe => Ok(()), - Err(e) => Err(e.map_err_context(|| "write error".into())), + Err(err) => Err(err.map_err_context(|| "write error".into())), } } @@ -263,7 +260,7 @@ fn print_seq( terminator: &str, pad: bool, padding: usize, - format: &Option>, + format: Option<&Format>, ) -> std::io::Result<()> { let stdout = stdout(); let mut stdout = stdout.lock(); diff --git a/src/uu/sort/BENCHMARKING.md b/src/uu/sort/BENCHMARKING.md index 0cc344c3118..355245b077b 100644 --- a/src/uu/sort/BENCHMARKING.md +++ b/src/uu/sort/BENCHMARKING.md @@ -48,7 +48,7 @@ rand = "0.8.3" ```rust use rand::prelude::*; fn main() { - let suffixes = ['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + let suffixes = ['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q']; let mut rng = thread_rng(); for _ in 0..100000 { println!( diff --git a/src/uu/sort/src/ext_sort.rs b/src/uu/sort/src/ext_sort.rs index 57e434e99b2..f984760bd2a 100644 --- a/src/uu/sort/src/ext_sort.rs +++ b/src/uu/sort/src/ext_sort.rs @@ -224,11 +224,8 @@ fn read_write_loop( let mut sender_option = Some(sender); let mut tmp_files = vec![]; loop { - let chunk = match receiver.recv() { - Ok(it) => it, - _ => { - return Ok(ReadResult::WroteChunksToFile { tmp_files }); - } + let Ok(chunk) = receiver.recv() else { + return Ok(ReadResult::WroteChunksToFile { tmp_files }); }; let tmp_file = write::( diff --git a/src/uu/sort/src/numeric_str_cmp.rs b/src/uu/sort/src/numeric_str_cmp.rs index 54950f2dbfe..86cbddc6424 100644 --- a/src/uu/sort/src/numeric_str_cmp.rs +++ b/src/uu/sort/src/numeric_str_cmp.rs @@ -82,7 +82,10 @@ impl NumInfo { if Self::is_invalid_char(char, &mut had_decimal_pt, parse_settings) { return if let Some(start) = start { let has_si_unit = parse_settings.accept_si_units - && matches!(char, 'K' | 'k' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y'); + && matches!( + char, + 'K' | 'k' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y' | 'R' | 'Q' + ); ( Self { exponent, sign }, start..if has_si_unit { idx + 1 } else { idx }, @@ -176,6 +179,8 @@ fn get_unit(unit: Option) -> u8 { 'E' => 6, 'Z' => 7, 'Y' => 8, + 'R' => 9, + 'Q' => 10, _ => 0, } } else { diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index fed0d6d2062..edff2baae85 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -34,6 +34,7 @@ use std::ffi::{OsStr, OsString}; use std::fs::{File, OpenOptions}; use std::hash::{Hash, Hasher}; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; +use std::num::IntErrorKind; use std::ops::Range; use std::path::Path; use std::path::PathBuf; @@ -288,7 +289,7 @@ impl GlobalSettings { // GNU sort (8.32) invalid: b, B, 1B, p, e, z, y let size = Parser::default() .with_allow_list(&[ - "b", "k", "K", "m", "M", "g", "G", "t", "T", "P", "E", "Z", "Y", "%", + "b", "k", "K", "m", "M", "g", "G", "t", "T", "P", "E", "Z", "Y", "R", "Q", "%", ]) .with_default_unit("K") .with_b_byte_count(true) @@ -534,8 +535,9 @@ impl<'a> Line<'a> { } else { // include a trailing si unit if selector.settings.mode == SortMode::HumanNumeric - && self.line[selection.end..initial_selection.end] - .starts_with(&['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'][..]) + && self.line[selection.end..initial_selection.end].starts_with( + &['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q'][..], + ) { selection.end += 1; } @@ -696,9 +698,17 @@ impl KeyPosition { .ok_or_else(|| format!("invalid key {}", key.quote()))?; let char = field_and_char.next(); - let field = field - .parse() - .map_err(|e| format!("failed to parse field index {}: {}", field.quote(), e))?; + let field = match field.parse::() { + Ok(f) => f, + Err(e) if *e.kind() == IntErrorKind::PosOverflow => usize::MAX, + Err(e) => { + return Err(format!( + "failed to parse field index {} {}", + field.quote(), + e + )) + } + }; if field == 0 { return Err("field index can not be 0".to_string()); } @@ -1361,14 +1371,14 @@ pub fn uu_app() -> Command { options::check::QUIET, options::check::DIAGNOSE_FIRST, ])) - .conflicts_with(options::OUTPUT) + .conflicts_with_all([options::OUTPUT, options::check::CHECK_SILENT]) .help("check for sorted input; do not sort"), ) .arg( Arg::new(options::check::CHECK_SILENT) .short('C') .long(options::check::CHECK_SILENT) - .conflicts_with(options::OUTPUT) + .conflicts_with_all([options::OUTPUT, options::check::CHECK]) .help( "exit successfully if the given file is already sorted, \ and exit with status 1 otherwise.", diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 053d86e8c28..9c5f1f4d1ee 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -1408,18 +1408,15 @@ where }; let bytes = line.as_slice(); - match kth_chunk { - Some(chunk_number) => { - if (i % num_chunks) == (chunk_number - 1) as usize { - stdout_writer.write_all(bytes)?; - } + if let Some(chunk_number) = kth_chunk { + if (i % num_chunks) == (chunk_number - 1) as usize { + stdout_writer.write_all(bytes)?; } - None => { - let writer = out_files.get_writer(i % num_chunks, settings)?; - let writer_stdin_open = custom_write_all(bytes, writer, settings)?; - if !writer_stdin_open { - closed_writers += 1; - } + } else { + let writer = out_files.get_writer(i % num_chunks, settings)?; + let writer_stdin_open = custom_write_all(bytes, writer, settings)?; + if !writer_stdin_open { + closed_writers += 1; } } i += 1; diff --git a/src/uu/tail/src/parse.rs b/src/uu/tail/src/parse.rs index 6d6826077f8..2e768d1c913 100644 --- a/src/uu/tail/src/parse.rs +++ b/src/uu/tail/src/parse.rs @@ -34,9 +34,8 @@ pub enum ParseError { /// Parses obsolete syntax /// tail -\[NUM\]\[bcl\]\[f\] and tail +\[NUM\]\[bcl\]\[f\] pub fn parse_obsolete(src: &OsString) -> Option> { - let mut rest = match src.to_str() { - Some(src) => src, - None => return Some(Err(ParseError::InvalidEncoding)), + let Some(mut rest) = src.to_str() else { + return Some(Err(ParseError::InvalidEncoding)); }; let sign = if let Some(r) = rest.strip_prefix('-') { rest = r; @@ -86,9 +85,8 @@ pub fn parse_obsolete(src: &OsString) -> Option } let multiplier = if mode == 'b' { 512 } else { 1 }; - let num = match num.checked_mul(multiplier) { - Some(n) => n, - None => return Some(Err(ParseError::Overflow)), + let Some(num) = num.checked_mul(multiplier) else { + return Some(Err(ParseError::Overflow)); }; Some(Ok(ObsoleteArgs { diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index ec8bc91d911..0ef70b1c3f4 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -210,13 +210,8 @@ fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult { fn files(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult { // Don't manage the error. GNU doesn't show error when doing // test foo -nt bar - let f_a = match fs::metadata(a) { - Ok(f) => f, - Err(_) => return Ok(false), - }; - let f_b = match fs::metadata(b) { - Ok(f) => f, - Err(_) => return Ok(false), + let (Ok(f_a), Ok(f_b)) = (fs::metadata(a), fs::metadata(b)) else { + return Ok(false); }; Ok(match op.to_str() { @@ -290,11 +285,8 @@ fn path(path: &OsStr, condition: &PathCondition) -> bool { fs::metadata(path) }; - let metadata = match metadata { - Ok(metadata) => metadata, - Err(_) => { - return false; - } + let Ok(metadata) = metadata else { + return false; }; let file_type = metadata.file_type(); diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 2ba93769aa1..3194d273714 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -129,6 +129,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::FOREGROUND) .long(options::FOREGROUND) + .short('f') .help( "when not running timeout directly from a shell prompt, allow \ COMMAND to read from the TTY and get TTY signals; in this mode, \ @@ -148,6 +149,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::PRESERVE_STATUS) .long(options::PRESERVE_STATUS) + .short('p') .help("exit with the same status as COMMAND, even when the command times out") .action(ArgAction::SetTrue), ) diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index f2c78ea61c3..de66e52ee28 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -599,14 +599,11 @@ fn parse_timestamp(s: &str) -> UResult { let local = NaiveDateTime::parse_from_str(&ts, format) .map_err(|_| USimpleError::new(1, format!("invalid date ts format {}", ts.quote())))?; - let mut local = match chrono::Local.from_local_datetime(&local) { - LocalResult::Single(dt) => dt, - _ => { - return Err(USimpleError::new( - 1, - format!("invalid date ts format {}", ts.quote()), - )) - } + let LocalResult::Single(mut local) = chrono::Local.from_local_datetime(&local) else { + return Err(USimpleError::new( + 1, + format!("invalid date ts format {}", ts.quote()), + )); }; // Chrono caps seconds at 59, but 60 is valid. It might be a leap second diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 4995f8c198e..1f0b28253e8 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -171,12 +171,9 @@ impl Uniq { // Convert the leftover bytes to UTF-8 for character-based -w // If invalid UTF-8, just compare them as individual bytes (fallback). - let string_after_skip = match std::str::from_utf8(fields_to_check) { - Ok(s) => s, - Err(_) => { - // Fallback: if invalid UTF-8, treat them as single-byte “chars” - return closure(&mut fields_to_check.iter().map(|&b| b as char)); - } + let Ok(string_after_skip) = std::str::from_utf8(fields_to_check) else { + // Fallback: if invalid UTF-8, treat them as single-byte “chars” + return closure(&mut fields_to_check.iter().map(|&b| b as char)); }; let total_chars = string_after_skip.chars().count(); diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index ce097d410a1..bc10328fbb7 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -18,6 +18,8 @@ edition = "2021" path = "src/lib/lib.rs" [dependencies] +chrono = { workspace = true } +chrono-tz = { workspace = true } clap = { workspace = true } uucore_procs = { workspace = true } number_prefix = { workspace = true } @@ -25,6 +27,7 @@ dns-lookup = { workspace = true, optional = true } dunce = { version = "1.0.4", optional = true } wild = "2.2.1" glob = { workspace = true } +iana-time-zone = { workspace = true } lazy_static = "1.4.0" # * optional itertools = { workspace = true, optional = true } @@ -114,4 +117,5 @@ utf8 = [] utmpx = ["time", "time/macros", "libc", "dns-lookup"] version-cmp = [] wide = [] +custom-tz-fmt = [] tty = [] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index ef5be724d9f..00079eed886 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -12,6 +12,8 @@ pub mod buf_copy; pub mod checksum; #[cfg(feature = "colors")] pub mod colors; +#[cfg(feature = "custom-tz-fmt")] +pub mod custom_tz_fmt; #[cfg(feature = "encoding")] pub mod encoding; #[cfg(feature = "format")] diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index 8b136922fb8..9912bea7454 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum.rs @@ -19,7 +19,7 @@ use std::{ }; use crate::{ - error::{set_exit_code, FromIo, UError, UResult, USimpleError}, + error::{FromIo, UError, UResult, USimpleError}, os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_error, show_warning_caps, sum::{ Blake2b, Blake3, Digest, DigestWriter, Md5, Sha1, Sha224, Sha256, Sha384, Sha3_224, @@ -130,10 +130,12 @@ impl From for LineCheckError { enum FileCheckError { /// a generic UError was encountered in sub-functions UError(Box), - /// the checksum file is improperly formatted. - ImproperlyFormatted, /// reading of the checksum file failed CantOpenChecksumFile, + /// processing of the file is considered as a failure regarding the + /// provided flags. This however does not stop the processing of + /// further files. + Failed, } impl From> for FileCheckError { @@ -148,15 +150,57 @@ impl From for FileCheckError { } } +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)] +pub enum ChecksumVerbose { + Status, + Quiet, + Normal, + Warning, +} + +impl ChecksumVerbose { + pub fn new(status: bool, quiet: bool, warn: bool) -> Self { + use ChecksumVerbose::*; + + // Assume only one of the three booleans will be enabled at once. + // This is ensured by clap's overriding arguments. + match (status, quiet, warn) { + (true, _, _) => Status, + (_, true, _) => Quiet, + (_, _, true) => Warning, + _ => Normal, + } + } + + #[inline] + pub fn over_status(self) -> bool { + self > Self::Status + } + + #[inline] + pub fn over_quiet(self) -> bool { + self > Self::Quiet + } + + #[inline] + pub fn at_least_warning(self) -> bool { + self >= Self::Warning + } +} + +impl Default for ChecksumVerbose { + fn default() -> Self { + Self::Normal + } +} + /// This struct regroups CLI flags. #[derive(Debug, Default, Clone, Copy)] pub struct ChecksumOptions { pub binary: bool, pub ignore_missing: bool, - pub quiet: bool, - pub status: bool, pub strict: bool, - pub warn: bool, + pub verbose: ChecksumVerbose, } #[derive(Debug, Error)] @@ -235,20 +279,19 @@ pub fn create_sha3(bits: Option) -> UResult { } #[allow(clippy::comparison_chain)] -fn cksum_output(res: &ChecksumResult, status: bool) { +fn print_cksum_report(res: &ChecksumResult) { if res.bad_format == 1 { show_warning_caps!("{} line is improperly formatted", res.bad_format); } else if res.bad_format > 1 { show_warning_caps!("{} lines are improperly formatted", res.bad_format); } - if !status { - if res.failed_cksum == 1 { - show_warning_caps!("{} computed checksum did NOT match", res.failed_cksum); - } else if res.failed_cksum > 1 { - show_warning_caps!("{} computed checksums did NOT match", res.failed_cksum); - } + if res.failed_cksum == 1 { + show_warning_caps!("{} computed checksum did NOT match", res.failed_cksum); + } else if res.failed_cksum > 1 { + show_warning_caps!("{} computed checksums did NOT match", res.failed_cksum); } + if res.failed_open_file == 1 { show_warning_caps!("{} listed file could not be read", res.failed_open_file); } else if res.failed_open_file > 1 { @@ -284,10 +327,10 @@ impl FileChecksumResult { /// The cli options might prevent to display on the outcome of the /// comparison on STDOUT. - fn can_display(&self, opts: ChecksumOptions) -> bool { + fn can_display(&self, verbose: ChecksumVerbose) -> bool { match self { - FileChecksumResult::Ok => !opts.status && !opts.quiet, - FileChecksumResult::Failed => !opts.status, + FileChecksumResult::Ok => verbose.over_quiet(), + FileChecksumResult::Failed => verbose.over_status(), FileChecksumResult::CantOpen => true, } } @@ -310,9 +353,9 @@ fn print_file_report( filename: &[u8], result: FileChecksumResult, prefix: &str, - opts: ChecksumOptions, + verbose: ChecksumVerbose, ) { - if result.can_display(opts) { + if result.can_display(verbose) { let _ = write!(w, "{prefix}"); let _ = w.write_all(filename); let _ = writeln!(w, ": {result}"); @@ -589,7 +632,7 @@ fn get_file_to_check( filename_bytes, FileChecksumResult::CantOpen, "", - opts, + opts.verbose, ); }; match File::open(filename) { @@ -648,12 +691,11 @@ fn get_input_file(filename: &OsStr) -> UResult> { fn identify_algo_name_and_length( line_info: &LineInfo, algo_name_input: Option<&str>, + last_algo: &mut Option, ) -> Option<(String, Option)> { - let algorithm = line_info - .algo_name - .clone() - .unwrap_or_default() - .to_lowercase(); + let algo_from_line = line_info.algo_name.clone().unwrap_or_default(); + let algorithm = algo_from_line.to_lowercase(); + *last_algo = Some(algo_from_line); // check if we are called with XXXsum (example: md5sum) but we detected a different algo parsing the file // (for example SHA1 (f) = d...) @@ -711,7 +753,7 @@ fn compute_and_check_digest_from_file( filename, FileChecksumResult::from_bool(checksum_correct), prefix, - opts, + opts.verbose, ); if checksum_correct { @@ -726,11 +768,13 @@ fn process_algo_based_line( line_info: &LineInfo, cli_algo_name: Option<&str>, opts: ChecksumOptions, + last_algo: &mut Option, ) -> Result<(), LineCheckError> { let filename_to_check = line_info.filename.as_slice(); - let (algo_name, algo_byte_len) = identify_algo_name_and_length(line_info, cli_algo_name) - .ok_or(LineCheckError::ImproperlyFormatted)?; + let (algo_name, algo_byte_len) = + identify_algo_name_and_length(line_info, cli_algo_name, last_algo) + .ok_or(LineCheckError::ImproperlyFormatted)?; // If the digest bitlen is known, we can check the format of the expected // checksum with it. @@ -789,13 +833,13 @@ fn process_non_algo_based_line( /// matched the expected. /// If the comparison didn't happen, return a `LineChecksumError`. fn process_checksum_line( - filename_input: &OsStr, line: &OsStr, i: usize, cli_algo_name: Option<&str>, cli_algo_length: Option, opts: ChecksumOptions, cached_regex: &mut Option, + last_algo: &mut Option, ) -> Result<(), LineCheckError> { let line_bytes = os_str_as_bytes(line)?; @@ -806,34 +850,19 @@ fn process_checksum_line( // Use `LineInfo` to extract the data of a line. // Then, depending on its format, apply a different pre-treatment. - if let Some(line_info) = LineInfo::parse(line, cached_regex) { - if line_info.format == LineFormat::AlgoBased { - process_algo_based_line(&line_info, cli_algo_name, opts) - } else if let Some(cli_algo) = cli_algo_name { - // If we match a non-algo based regex, we expect a cli argument - // to give us the algorithm to use - process_non_algo_based_line(i, &line_info, cli_algo, cli_algo_length, opts) - } else { - // We have no clue of what algorithm to use - return Err(LineCheckError::ImproperlyFormatted); - } - } else { - if opts.warn { - let algo = if let Some(algo_name_input) = cli_algo_name { - algo_name_input.to_uppercase() - } else { - "Unknown algorithm".to_string() - }; - eprintln!( - "{}: {}: {}: improperly formatted {} checksum line", - util_name(), - &filename_input.maybe_quote(), - i + 1, - algo - ); - } + let Some(line_info) = LineInfo::parse(line, cached_regex) else { + return Err(LineCheckError::ImproperlyFormatted); + }; - Err(LineCheckError::ImproperlyFormatted) + if line_info.format == LineFormat::AlgoBased { + process_algo_based_line(&line_info, cli_algo_name, opts, last_algo) + } else if let Some(cli_algo) = cli_algo_name { + // If we match a non-algo based regex, we expect a cli argument + // to give us the algorithm to use + process_non_algo_based_line(i, &line_info, cli_algo, cli_algo_length, opts) + } else { + // We have no clue of what algorithm to use + return Err(LineCheckError::ImproperlyFormatted); } } @@ -856,7 +885,6 @@ fn process_checksum_file( Err(e) => { // Could not read the file, show the error and continue to the next file show_error!("{e}"); - set_exit_code(1); return Err(FileCheckError::CantOpenChecksumFile); } } @@ -868,16 +896,20 @@ fn process_checksum_file( // cached_regex is used to ensure that several non algo-based checksum line // will use the same regex. let mut cached_regex = None; + // last_algo caches the algorithm used in the last line to print a warning + // message for the current line if improperly formatted. + // Behavior tested in gnu_cksum_c::test_warn + let mut last_algo = None; for (i, line) in lines.iter().enumerate() { let line_result = process_checksum_line( - filename_input, line, i, cli_algo_name, cli_algo_length, opts, &mut cached_regex, + &mut last_algo, ); // Match a first time to elude critical UErrors, and increment the total @@ -893,7 +925,26 @@ fn process_checksum_file( match line_result { Ok(()) => res.correct += 1, Err(DigestMismatch) => res.failed_cksum += 1, - Err(ImproperlyFormatted) => res.bad_format += 1, + Err(ImproperlyFormatted) => { + res.bad_format += 1; + + if opts.verbose.at_least_warning() { + let algo = if let Some(algo_name_input) = cli_algo_name { + Cow::Owned(algo_name_input.to_uppercase()) + } else if let Some(algo) = &last_algo { + Cow::Borrowed(algo.as_str()) + } else { + Cow::Borrowed("Unknown algorithm") + }; + eprintln!( + "{}: {}: {}: improperly formatted {} checksum line", + util_name(), + &filename_input.maybe_quote(), + i + 1, + algo + ); + } + } Err(CantOpenFile | FileIsDirectory) => res.failed_open_file += 1, Err(FileNotFound) if !opts.ignore_missing => res.failed_open_file += 1, _ => continue, @@ -903,36 +954,43 @@ fn process_checksum_file( // not a single line correctly formatted found // return an error if res.total_properly_formatted() == 0 { - if !opts.status { + if opts.verbose.over_status() { log_no_properly_formatted(get_filename_for_output(filename_input, input_is_stdin)); } - set_exit_code(1); - return Err(FileCheckError::ImproperlyFormatted); + return Err(FileCheckError::Failed); } // if any incorrectly formatted line, show it - cksum_output(&res, opts.status); + if opts.verbose.over_status() { + print_cksum_report(&res); + } if opts.ignore_missing && res.correct == 0 { // we have only bad format // and we had ignore-missing - eprintln!( - "{}: {}: no file was verified", - util_name(), - filename_input.maybe_quote(), - ); - set_exit_code(1); + if opts.verbose.over_status() { + eprintln!( + "{}: {}: no file was verified", + util_name(), + filename_input.maybe_quote(), + ); + } + return Err(FileCheckError::Failed); } // strict means that we should have an exit code. if opts.strict && res.bad_format > 0 { - set_exit_code(1); + return Err(FileCheckError::Failed); + } + + // If a file was missing, return an error unless we explicitly ignore it. + if res.failed_open_file > 0 && !opts.ignore_missing { + return Err(FileCheckError::Failed); } - // if we have any failed checksum verification, we set an exit code - // except if we have ignore_missing - if (res.failed_cksum > 0 || res.failed_open_file > 0) && !opts.ignore_missing { - set_exit_code(1); + // Obviously, if a checksum failed at some point, report the error. + if res.failed_cksum > 0 { + return Err(FileCheckError::Failed); } Ok(()) @@ -950,16 +1008,23 @@ pub fn perform_checksum_validation<'a, I>( where I: Iterator, { + let mut failed = false; + // if cksum has several input files, it will print the result for each file for filename_input in files { use FileCheckError::*; match process_checksum_file(filename_input, algo_name_input, length_input, opts) { Err(UError(e)) => return Err(e), - Err(CantOpenChecksumFile | ImproperlyFormatted) | Ok(_) => continue, + Err(Failed | CantOpenChecksumFile) => failed = true, + Ok(_) => continue, } } - Ok(()) + if failed { + Err(USimpleError::new(1, "")) + } else { + Ok(()) + } } pub fn digest_reader( @@ -1416,7 +1481,7 @@ mod tests { for (filename, result, prefix, expected) in cases { let mut buffer: Vec = vec![]; - print_file_report(&mut buffer, filename, *result, prefix, opts); + print_file_report(&mut buffer, filename, *result, prefix, opts.verbose); assert_eq!(&buffer, expected) } } diff --git a/src/uucore/src/lib/features/custom_tz_fmt.rs b/src/uucore/src/lib/features/custom_tz_fmt.rs new file mode 100644 index 00000000000..132155f540a --- /dev/null +++ b/src/uucore/src/lib/features/custom_tz_fmt.rs @@ -0,0 +1,58 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use chrono::{TimeZone, Utc}; +use chrono_tz::{OffsetName, Tz}; +use iana_time_zone::get_timezone; + +/// Get the alphabetic abbreviation of the current timezone. +/// +/// For example, "UTC" or "CET" or "PDT" +fn timezone_abbreviation() -> String { + let tz = match std::env::var("TZ") { + // TODO Support other time zones... + Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC, + _ => match get_timezone() { + Ok(tz_str) => tz_str.parse().unwrap(), + Err(_) => Tz::Etc__UTC, + }, + }; + + let offset = tz.offset_from_utc_date(&Utc::now().date_naive()); + offset.abbreviation().unwrap_or("UTC").to_string() +} + +/// Adapt the given string to be accepted by the chrono library crate. +/// +/// # Arguments +/// +/// fmt: the format of the string +/// +/// # Return +/// +/// A string that can be used as parameter of the chrono functions that use formats +pub fn custom_time_format(fmt: &str) -> String { + // TODO - Revisit when chrono 0.5 is released. https://github.com/chronotope/chrono/issues/970 + // GNU `date` uses `%N` for nano seconds, however the `chrono` crate uses `%f`. + fmt.replace("%N", "%f") + .replace("%Z", timezone_abbreviation().as_ref()) +} + +#[cfg(test)] +mod tests { + use super::{custom_time_format, timezone_abbreviation}; + + #[test] + fn test_custom_time_format() { + assert_eq!(custom_time_format("%Y-%m-%d %H-%M-%S"), "%Y-%m-%d %H-%M-%S"); + assert_eq!(custom_time_format("%d-%m-%Y %H-%M-%S"), "%d-%m-%Y %H-%M-%S"); + assert_eq!(custom_time_format("%Y-%m-%d %H-%M-%S"), "%Y-%m-%d %H-%M-%S"); + assert_eq!( + custom_time_format("%Y-%m-%d %H-%M-%S.%N"), + "%Y-%m-%d %H-%M-%S.%f" + ); + assert_eq!(custom_time_format("%Z"), timezone_abbreviation()); + } +} diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index d0875f78a91..8ef645cfbf9 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -652,14 +652,10 @@ pub fn are_hardlinks_to_same_file(_source: &Path, _target: &Path) -> bool { /// * `bool` - Returns `true` if the paths are hard links to the same file, and `false` otherwise. #[cfg(unix)] pub fn are_hardlinks_to_same_file(source: &Path, target: &Path) -> bool { - let source_metadata = match fs::symlink_metadata(source) { - Ok(metadata) => metadata, - Err(_) => return false, - }; - - let target_metadata = match fs::symlink_metadata(target) { - Ok(metadata) => metadata, - Err(_) => return false, + let (Ok(source_metadata), Ok(target_metadata)) = + (fs::symlink_metadata(source), fs::symlink_metadata(target)) + else { + return false; }; source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev() @@ -682,14 +678,10 @@ pub fn are_hardlinks_or_one_way_symlink_to_same_file(_source: &Path, _target: &P /// * `bool` - Returns `true` if either of above conditions are true, and `false` otherwise. #[cfg(unix)] pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Path) -> bool { - let source_metadata = match fs::metadata(source) { - Ok(metadata) => metadata, - Err(_) => return false, - }; - - let target_metadata = match fs::symlink_metadata(target) { - Ok(metadata) => metadata, - Err(_) => return false, + let (Ok(source_metadata), Ok(target_metadata)) = + (fs::metadata(source), fs::symlink_metadata(target)) + else { + return false; }; source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev() diff --git a/src/uucore/src/lib/features/fsxattr.rs b/src/uucore/src/lib/features/fsxattr.rs index 3fb626a3039..1913b0669fc 100644 --- a/src/uucore/src/lib/features/fsxattr.rs +++ b/src/uucore/src/lib/features/fsxattr.rs @@ -79,13 +79,10 @@ pub fn apply_xattrs>( /// `true` if the file has extended attributes (indicating an ACL), `false` otherwise. pub fn has_acl>(file: P) -> bool { // don't use exacl here, it is doing more getxattr call then needed - match xattr::list(file) { - Ok(acl) => { - // if we have extra attributes, we have an acl - acl.count() > 0 - } - Err(_) => false, - } + xattr::list(file).is_ok_and(|acl| { + // if we have extra attributes, we have an acl + acl.count() > 0 + }) } /// Returns the permissions bits of a file or directory which has Access Control List (ACL) entries based on its @@ -132,7 +129,7 @@ pub fn get_acl_perm_bits_from_xattr>(source: P) -> u32 { for entry in acl_entries.chunks_exact(4) { // Third byte and fourth byte will be the perm bits - perm = (perm << 3) | entry[2] as u32 | entry[3] as u32; + perm = (perm << 3) | u32::from(entry[2]) | u32::from(entry[3]); } return perm; } diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index 62e7d56ed2f..3d10dfd07bc 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -273,18 +273,15 @@ impl ChownExecutor { #[allow(clippy::cognitive_complexity)] fn traverse>(&self, root: P) -> i32 { let path = root.as_ref(); - let meta = match self.obtain_meta(path, self.dereference) { - Some(m) => m, - _ => { - if self.verbosity.level == VerbosityLevel::Verbose { - println!( - "failed to change ownership of {} to {}", - path.quote(), - self.raw_owner - ); - } - return 1; + let Some(meta) = self.obtain_meta(path, self.dereference) else { + if self.verbosity.level == VerbosityLevel::Verbose { + println!( + "failed to change ownership of {} to {}", + path.quote(), + self.raw_owner + ); } + return 1; }; if self.recursive @@ -370,17 +367,15 @@ impl ChownExecutor { Ok(entry) => entry, }; let path = entry.path(); - let meta = match self.obtain_meta(path, self.dereference) { - Some(m) => m, - _ => { - ret = 1; - if entry.file_type().is_dir() { - // Instruct walkdir to skip this directory to avoid getting another error - // when walkdir tries to query the children of this directory. - iterator.skip_current_dir(); - } - continue; + + let Some(meta) = self.obtain_meta(path, self.dereference) else { + ret = 1; + if entry.file_type().is_dir() { + // Instruct walkdir to skip this directory to avoid getting another error + // when walkdir tries to query the children of this directory. + iterator.skip_current_dir(); } + continue; }; if self.preserve_root && is_root(path, self.traverse_symlinks == TraverseSymlinks::All) @@ -425,24 +420,18 @@ impl ChownExecutor { fn obtain_meta>(&self, path: P, follow: bool) -> Option { let path = path.as_ref(); - - let meta = get_metadata(path, follow); - - match meta { - Err(e) => { - match self.verbosity.level { - VerbosityLevel::Silent => (), - _ => show_error!( + get_metadata(path, follow) + .inspect_err(|e| { + if self.verbosity.level != VerbosityLevel::Silent { + show_error!( "cannot {} {}: {}", if follow { "dereference" } else { "access" }, path.quote(), - strip_errno(&e) - ), + strip_errno(e) + ); } - None - } - Ok(meta) => Some(meta), - } + }) + .ok() } #[inline] diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 9516b5e1bf6..da29baf0c70 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -46,6 +46,8 @@ pub use crate::features::buf_copy; pub use crate::features::checksum; #[cfg(feature = "colors")] pub use crate::features::colors; +#[cfg(feature = "custom-tz-fmt")] +pub use crate::features::custom_tz_fmt; #[cfg(feature = "encoding")] pub use crate::features::encoding; #[cfg(feature = "format")] diff --git a/src/uucore/src/lib/parser/parse_size.rs b/src/uucore/src/lib/parser/parse_size.rs index 4071a3870b8..b18e1695d70 100644 --- a/src/uucore/src/lib/parser/parse_size.rs +++ b/src/uucore/src/lib/parser/parse_size.rs @@ -269,16 +269,9 @@ impl<'parser> Parser<'parser> { /// Same as `parse()` but tries to return u64 pub fn parse_u64(&self, size: &str) -> Result { - match self.parse(size) { - Ok(num_u128) => { - let num_u64 = match u64::try_from(num_u128) { - Ok(n) => n, - Err(_) => return Err(ParseSizeError::size_too_big(size)), - }; - Ok(num_u64) - } - Err(e) => Err(e), - } + self.parse(size).and_then(|num_u128| { + u64::try_from(num_u128).map_err(|_| ParseSizeError::size_too_big(size)) + }) } /// Same as `parse_u64()`, except returns `u64::MAX` on overflow diff --git a/src/uucore/src/lib/parser/parse_time.rs b/src/uucore/src/lib/parser/parse_time.rs index 727ee28b1bf..d22d4d372c1 100644 --- a/src/uucore/src/lib/parser/parse_time.rs +++ b/src/uucore/src/lib/parser/parse_time.rs @@ -49,9 +49,8 @@ pub fn from_str(string: &str) -> Result { if len == 0 { return Err("empty string".to_owned()); } - let slice = match string.get(..len - 1) { - Some(s) => s, - None => return Err(format!("invalid time interval {}", string.quote())), + let Some(slice) = string.get(..len - 1) else { + return Err(format!("invalid time interval {}", string.quote())); }; let (numstr, times) = match string.chars().next_back().unwrap() { 's' => (slice, 1), diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index b73ee1425d5..d2e8ac4c67d 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb +// spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb dont use crate::common::util::TestScenario; @@ -1284,6 +1284,18 @@ fn test_several_files_error_mgmt() { .stderr_contains("incorrect: no properly "); } +#[test] +fn test_check_unknown_checksum_file() { + let scene = TestScenario::new(util_name!()); + + scene + .ucmd() + .arg("--check") + .arg("missing") + .fails() + .stderr_only("cksum: missing: No such file or directory\n"); +} + #[test] fn test_check_comment_line() { // A comment in a checksum file shall be discarded unnoticed. @@ -1811,6 +1823,373 @@ mod gnu_cksum_base64 { } } +/// This module reimplements the cksum-c.sh GNU test. +mod gnu_cksum_c { + use super::*; + + const INVALID_SUM: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaafdb57c725157cb40b5aee8d937b8351477e"; + + fn make_scene() -> TestScenario { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("input", "9\n7\n1\n4\n2\n6\n3\n5\n8\n10\n"); + + let algos: &[&[&str]] = &[ + &["-a", "sha384"], + &["-a", "blake2b"], + &["-a", "blake2b", "-l", "384"], + &["-a", "sm3"], + ]; + + for args in algos { + let result = scene.ucmd().args(args).succeeds(); + let stdout = result.stdout(); + at.append_bytes("CHECKSUMS", stdout); + } + + scene + } + + #[test] + #[ignore] + fn test_signed_checksums() { + todo!() + } + + #[test] + fn test_check_individual_digests_in_mixed_file() { + let scene = make_scene(); + + scene + .ucmd() + .arg("--check") + .arg("-a") + .arg("sm3") + .arg("CHECKSUMS") + .succeeds(); + } + + #[test] + fn test_check_against_older_non_hex_formats() { + let scene = make_scene(); + + scene + .ucmd() + .arg("-c") + .arg("-a") + .arg("crc") + .arg("CHECKSUMS") + .fails(); + + let crc_cmd = scene.ucmd().arg("-a").arg("crc").arg("input").succeeds(); + let crc_cmd_out = crc_cmd.stdout(); + scene.fixtures.write_bytes("CHECKSUMS.crc", crc_cmd_out); + + scene.ucmd().arg("-c").arg("CHECKSUMS.crc").fails(); + } + + #[test] + fn test_status() { + let scene = make_scene(); + + scene + .ucmd() + .arg("--status") + .arg("--check") + .arg("CHECKSUMS") + .succeeds() + .no_output(); + } + + fn make_scene_with_comment() -> TestScenario { + let scene = make_scene(); + + scene + .fixtures + .append("CHECKSUMS", "# Very important comment\n"); + + scene + } + + #[test] + fn test_status_with_comment() { + let scene = make_scene_with_comment(); + + scene + .ucmd() + .arg("--status") + .arg("--check") + .arg("CHECKSUMS") + .succeeds() + .no_output(); + } + + fn make_scene_with_invalid_line() -> TestScenario { + let scene = make_scene_with_comment(); + + scene.fixtures.append("CHECKSUMS", "invalid_line\n"); + + scene + } + + #[test] + fn test_check_strict() { + let scene = make_scene_with_invalid_line(); + + // without strict, succeeds + scene + .ucmd() + .arg("--check") + .arg("CHECKSUMS") + .succeeds() + .stderr_contains("1 line is improperly formatted"); + + // with strict, fails + scene + .ucmd() + .arg("--strict") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stderr_contains("1 line is improperly formatted"); + } + + fn make_scene_with_two_invalid_lines() -> TestScenario { + let scene = make_scene_with_comment(); + + scene + .fixtures + .append("CHECKSUMS", "invalid_line\ninvalid_line\n"); + + scene + } + + #[test] + fn test_check_strict_plural_checks() { + let scene = make_scene_with_two_invalid_lines(); + + scene + .ucmd() + .arg("--strict") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stderr_contains("2 lines are improperly formatted"); + } + + fn make_scene_with_incorrect_checksum() -> TestScenario { + let scene = make_scene_with_two_invalid_lines(); + + scene + .fixtures + .append("CHECKSUMS", &format!("SM3 (input) = {INVALID_SUM}\n")); + + scene + } + + #[test] + fn test_check_with_incorrect_checksum() { + let scene = make_scene_with_incorrect_checksum(); + + scene + .ucmd() + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stdout_contains("input: FAILED") + .stderr_contains("1 computed checksum did NOT match"); + + // also fails with strict + scene + .ucmd() + .arg("--strict") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stdout_contains("input: FAILED") + .stderr_contains("1 computed checksum did NOT match"); + } + + #[test] + fn test_status_with_errors() { + let scene = make_scene_with_incorrect_checksum(); + + scene + .ucmd() + .arg("--status") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .no_output(); + } + + #[test] + fn test_check_with_non_existing_file() { + let scene = make_scene(); + scene + .fixtures + .write("CHECKSUMS2", &format!("SM3 (input2) = {INVALID_SUM}\n")); + + scene + .ucmd() + .arg("--check") + .arg("CHECKSUMS2") + .fails() + .stdout_contains("input2: FAILED open or read") + .stderr_contains("1 listed file could not be read"); + + // also fails with strict + scene + .ucmd() + .arg("--strict") + .arg("--check") + .arg("CHECKSUMS2") + .fails() + .stdout_contains("input2: FAILED open or read") + .stderr_contains("1 listed file could not be read"); + } + + fn make_scene_with_another_improperly_formatted() -> TestScenario { + let scene = make_scene_with_incorrect_checksum(); + + scene.fixtures.append( + "CHECKSUMS", + &format!("BLAKE2b (missing-file) = {INVALID_SUM}\n"), + ); + + scene + } + + #[test] + fn test_warn() { + let scene = make_scene_with_another_improperly_formatted(); + + scene + .ucmd() + .arg("--warn") + .arg("--check") + .arg("CHECKSUMS") + .run() + .stderr_contains("CHECKSUMS: 6: improperly formatted SM3 checksum line") + .stderr_contains("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line"); + } + + fn make_scene_with_checksum_missing() -> TestScenario { + let scene = make_scene_with_another_improperly_formatted(); + + scene.fixtures.write( + "CHECKSUMS-missing", + &format!("SM3 (nonexistent) = {INVALID_SUM}\n"), + ); + + scene + } + + #[test] + fn test_ignore_missing() { + let scene = make_scene_with_checksum_missing(); + + scene + .ucmd() + .arg("--ignore-missing") + .arg("--check") + .arg("CHECKSUMS-missing") + .fails() + .stdout_does_not_contain("nonexistent: No such file or directory") + .stdout_does_not_contain("nonexistent: FAILED open or read") + .stderr_contains("CHECKSUMS-missing: no file was verified"); + } + + #[test] + fn test_status_and_warn() { + let scene = make_scene_with_checksum_missing(); + + // --status before --warn + scene + .ucmd() + .arg("--status") + .arg("--warn") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stderr_contains("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line") + .stderr_contains("WARNING: 3 lines are improperly formatted") + .stderr_contains("WARNING: 1 computed checksum did NOT match"); + + // --warn before --status (status hides the results) + scene + .ucmd() + .arg("--warn") + .arg("--status") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stderr_does_not_contain("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line") + .stderr_does_not_contain("WARNING: 3 lines are improperly formatted") + .stderr_does_not_contain("WARNING: 1 computed checksum did NOT match"); + } + + #[test] + fn test_status_and_ignore_missing() { + let scene = make_scene_with_checksum_missing(); + + scene + .ucmd() + .arg("--status") + .arg("--ignore-missing") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .no_output(); + } + + #[test] + fn test_status_warn_and_ignore_missing() { + let scene = make_scene_with_checksum_missing(); + + scene + .ucmd() + .arg("--status") + .arg("--warn") + .arg("--ignore-missing") + .arg("--check") + .arg("CHECKSUMS-missing") + .fails() + .stderr_contains("CHECKSUMS-missing: no file was verified") + .stdout_does_not_contain("nonexistent: No such file or directory"); + } + + #[test] + fn test_check_several_files_dont_exist() { + let scene = make_scene(); + + scene + .ucmd() + .arg("--check") + .arg("non-existing-1") + .arg("non-existing-2") + .fails() + .stderr_contains("non-existing-1: No such file or directory") + .stderr_contains("non-existing-2: No such file or directory"); + } + + #[test] + fn test_check_several_files_empty() { + let scene = make_scene(); + scene.fixtures.touch("empty-1"); + scene.fixtures.touch("empty-2"); + + scene + .ucmd() + .arg("--check") + .arg("empty-1") + .arg("empty-2") + .fails() + .stderr_contains("empty-1: no properly formatted checksum lines found") + .stderr_contains("empty-2: no properly formatted checksum lines found"); + } +} + /// The tests in this module check the behavior of cksum when given different /// checksum formats and algorithms in the same file, while specifying an /// algorithm on CLI or not. diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 79ca0d2f45c..2a532029576 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -950,7 +950,7 @@ mod tests_split_iterator { | '*' | '?' | '[' | '#' | '˜' | '=' | '%' => { special = true; } - _ => continue, + _ => (), } } diff --git a/tests/by-util/test_kill.rs b/tests/by-util/test_kill.rs index ba2b963518d..a130aa1bc98 100644 --- a/tests/by-util/test_kill.rs +++ b/tests/by-util/test_kill.rs @@ -63,7 +63,7 @@ fn test_kill_list_all_signals() { .stdout_contains("KILL") .stdout_contains("TERM") .stdout_contains("HUP") - .stdout_does_not_contain("EXIT"); + .stdout_contains("EXIT"); } #[test] @@ -80,15 +80,16 @@ fn test_kill_list_all_signals_as_table() { .succeeds() .stdout_contains("KILL") .stdout_contains("TERM") - .stdout_contains("HUP"); + .stdout_contains("HUP") + .stdout_contains("EXIT"); } #[test] -fn test_kill_table_starts_at_1() { +fn test_kill_table_starts_at_0() { new_ucmd!() .arg("-t") .succeeds() - .stdout_matches(&Regex::new("^\\s?1\\sHUP").unwrap()); + .stdout_matches(&Regex::new("^\\s?0\\sEXIT").unwrap()); } #[test] @@ -104,6 +105,7 @@ fn test_kill_table_lists_all_vertically() { assert!(signals.contains(&"KILL")); assert!(signals.contains(&"TERM")); assert!(signals.contains(&"HUP")); + assert!(signals.contains(&"EXIT")); } #[test] @@ -143,6 +145,7 @@ fn test_kill_list_all_vertically() { assert!(signals.contains(&"KILL")); assert!(signals.contains(&"TERM")); assert!(signals.contains(&"HUP")); + assert!(signals.contains(&"EXIT")); } #[test] diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 845b8581aaa..a134cbc2078 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1315,3 +1315,30 @@ fn test_same_sort_mode_twice() { fn test_args_override() { new_ucmd!().args(&["-f", "-f"]).pipe_in("foo").succeeds(); } + +#[test] +fn test_k_overflow() { + let input = "2\n1\n"; + let output = "1\n2\n"; + new_ucmd!() + .args(&["-k", "18446744073709551616"]) + .pipe_in(input) + .succeeds() + .stdout_is(output); +} + +#[test] +fn test_human_blocks_r_and_q() { + let input = "1Q\n1R\n"; + let output = "1R\n1Q\n"; + new_ucmd!() + .args(&["-h"]) + .pipe_in(input) + .succeeds() + .stdout_is(output); +} + +#[test] +fn test_args_check_conflict() { + new_ucmd!().arg("-c").arg("-C").fails(); +} diff --git a/tests/by-util/test_timeout.rs b/tests/by-util/test_timeout.rs index 1ba6445c81b..cc7c04565d4 100644 --- a/tests/by-util/test_timeout.rs +++ b/tests/by-util/test_timeout.rs @@ -65,13 +65,11 @@ fn test_zero_timeout() { new_ucmd!() .args(&["-v", "0", "sleep", ".1"]) .succeeds() - .no_stderr() - .no_stdout(); + .no_output(); new_ucmd!() .args(&["-v", "0", "-s0", "-k0", "sleep", ".1"]) .succeeds() - .no_stderr() - .no_stdout(); + .no_output(); } #[test] @@ -82,15 +80,27 @@ fn test_command_empty_args() { .stderr_contains("timeout: empty string"); } +#[test] +fn test_foreground() { + for arg in ["-f", "--foreground"] { + new_ucmd!() + .args(&[arg, ".1", "sleep", "10"]) + .fails() + .code_is(124) + .no_output(); + } +} + #[test] fn test_preserve_status() { - new_ucmd!() - .args(&["--preserve-status", ".1", "sleep", "10"]) - .fails() - // 128 + SIGTERM = 128 + 15 - .code_is(128 + 15) - .no_stderr() - .no_stdout(); + for arg in ["-p", "--preserve-status"] { + new_ucmd!() + .args(&[arg, ".1", "sleep", "10"]) + .fails() + // 128 + SIGTERM = 128 + 15 + .code_is(128 + 15) + .no_output(); + } } #[test] @@ -102,8 +112,7 @@ fn test_preserve_status_even_when_send_signal() { .args(&["-s", cont_spelling, "--preserve-status", ".1", "sleep", "2"]) .succeeds() .code_is(0) - .no_stderr() - .no_stdout(); + .no_output(); } } @@ -113,14 +122,12 @@ fn test_dont_overflow() { .args(&["9223372036854775808d", "sleep", "0"]) .succeeds() .code_is(0) - .no_stderr() - .no_stdout(); + .no_output(); new_ucmd!() .args(&["-k", "9223372036854775808d", "10", "sleep", "0"]) .succeeds() .code_is(0) - .no_stderr() - .no_stdout(); + .no_output(); } #[test] @@ -153,8 +160,7 @@ fn test_kill_after_long() { new_ucmd!() .args(&["--kill-after=1", "1", "sleep", "0"]) .succeeds() - .no_stdout() - .no_stderr(); + .no_output(); } #[test] diff --git a/util/build-gnu.sh b/util/build-gnu.sh index e05341b2218..fda57ec7800 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -94,6 +94,12 @@ if [ "$(uname)" == "Linux" ]; then export SELINUX_ENABLED=1 fi +# Set up quilt for patch management +export QUILT_PATCHES="${ME_dir}/gnu-patches/" +cd "$path_GNU" +quilt push -a +cd - + "${MAKE}" PROFILE="${UU_MAKE_PROFILE}" cp "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target @@ -206,8 +212,6 @@ grep -rlE '/usr/local/bin/\s?/usr/local/bin' init.cfg tests/* | xargs -r sed -Ei # we should not regress our project just to match what GNU is going. # So, do some changes on the fly -eval cat "$path_UUTILS/util/gnu-patches/*.patch" | patch -N -r - -d "$path_GNU" -p 1 -i - || true - sed -i -e "s|rm: cannot remove 'e/slink'|rm: cannot remove 'e'|g" tests/rm/fail-eacces.sh sed -i -e "s|rm: cannot remove 'a/b/file'|rm: cannot remove 'a'|g" tests/rm/cycle.sh diff --git a/util/gnu-patches/series b/util/gnu-patches/series new file mode 100644 index 00000000000..f303e89c42b --- /dev/null +++ b/util/gnu-patches/series @@ -0,0 +1,10 @@ +tests_factor_factor.pl.patch +tests_cksum_base64.patch +tests_comm.pl.patch +tests_cut_error_msg.patch +tests_dup_source.patch +tests_env_env-S.pl.patch +tests_invalid_opt.patch +tests_ls_no_cap.patch +tests_sort_merge.pl.patch +tests_tsort.patch diff --git a/util/gnu-patches/tests_cksum_base64.patch b/util/gnu-patches/tests_cksum_base64.patch index 2a8ed0af40e..ea6bf92e164 100644 --- a/util/gnu-patches/tests_cksum_base64.patch +++ b/util/gnu-patches/tests_cksum_base64.patch @@ -1,8 +1,8 @@ -diff --git a/tests/cksum/cksum-base64.pl b/tests/cksum/cksum-base64.pl -index a037a1628..c6d87d447 100755 ---- a/tests/cksum/cksum-base64.pl -+++ b/tests/cksum/cksum-base64.pl -@@ -91,8 +91,8 @@ my $prog = 'cksum'; +Index: gnu/tests/cksum/cksum-base64.pl +=================================================================== +--- gnu.orig/tests/cksum/cksum-base64.pl ++++ gnu/tests/cksum/cksum-base64.pl +@@ -92,8 +92,8 @@ my $prog = 'cksum'; my $fail = run_tests ($program_name, $prog, \@Tests, $save_temps, $verbose); # Ensure hash names from cksum --help match those in @pairs above. diff --git a/util/gnu-patches/tests_comm.pl.patch b/util/gnu-patches/tests_comm.pl.patch index d3d5595a2c5..602071f483a 100644 --- a/util/gnu-patches/tests_comm.pl.patch +++ b/util/gnu-patches/tests_comm.pl.patch @@ -1,7 +1,7 @@ -diff --git a/tests/misc/comm.pl b/tests/misc/comm.pl -index 5bd5f56d7..8322d92ba 100755 ---- a/tests/misc/comm.pl -+++ b/tests/misc/comm.pl +Index: gnu/tests/misc/comm.pl +=================================================================== +--- gnu.orig/tests/misc/comm.pl ++++ gnu/tests/misc/comm.pl @@ -73,18 +73,24 @@ my @Tests = # invalid missing command line argument (1) diff --git a/util/gnu-patches/tests_cut_error_msg.patch b/util/gnu-patches/tests_cut_error_msg.patch index 3f57d204813..1b1673fef72 100644 --- a/util/gnu-patches/tests_cut_error_msg.patch +++ b/util/gnu-patches/tests_cut_error_msg.patch @@ -1,7 +1,7 @@ -diff --git a/tests/cut/cut.pl b/tests/cut/cut.pl -index 1670db02e..ed633792a 100755 ---- a/tests/cut/cut.pl -+++ b/tests/cut/cut.pl +Index: gnu/tests/cut/cut.pl +=================================================================== +--- gnu.orig/tests/cut/cut.pl ++++ gnu/tests/cut/cut.pl @@ -29,13 +29,15 @@ my $mb_locale = $ENV{LOCALE_FR_UTF8}; my $prog = 'cut'; diff --git a/util/gnu-patches/tests_dup_source.patch b/util/gnu-patches/tests_dup_source.patch index 44e33723bc1..4c24498253d 100644 --- a/util/gnu-patches/tests_dup_source.patch +++ b/util/gnu-patches/tests_dup_source.patch @@ -1,8 +1,8 @@ -diff --git a/tests/mv/dup-source.sh b/tests/mv/dup-source.sh -index 7bcd82fc3..0f9005296 100755 ---- a/tests/mv/dup-source.sh -+++ b/tests/mv/dup-source.sh -@@ -83,7 +83,7 @@ $i: cannot stat 'a': No such file or directory +Index: gnu/tests/mv/dup-source.sh +=================================================================== +--- gnu.orig/tests/mv/dup-source.sh ++++ gnu/tests/mv/dup-source.sh +@@ -83,7 +83,7 @@ $i: cannot stat 'a': No such file or dir $i: cannot stat 'a': No such file or directory $i: cannot stat 'b': No such file or directory $i: cannot move './b' to a subdirectory of itself, 'b/b' diff --git a/util/gnu-patches/tests_env_env-S.pl.patch b/util/gnu-patches/tests_env_env-S.pl.patch index 404a00ca60e..4a1ae939a6b 100644 --- a/util/gnu-patches/tests_env_env-S.pl.patch +++ b/util/gnu-patches/tests_env_env-S.pl.patch @@ -1,7 +1,7 @@ -diff --git a/tests/env/env-S.pl b/tests/env/env-S.pl -index 710ca82cf..af7cf6efa 100755 ---- a/tests/env/env-S.pl -+++ b/tests/env/env-S.pl +Index: gnu/tests/env/env-S.pl +=================================================================== +--- gnu.orig/tests/env/env-S.pl ++++ gnu/tests/env/env-S.pl @@ -209,27 +209,28 @@ my @Tests = {ERR=>"$prog: no terminating quote in -S string\n"}], ['err5', q[-S'A=B\\q'], {EXIT=>125}, diff --git a/util/gnu-patches/tests_factor_factor.pl.patch b/util/gnu-patches/tests_factor_factor.pl.patch index 731abcc9117..892d526964e 100644 --- a/util/gnu-patches/tests_factor_factor.pl.patch +++ b/util/gnu-patches/tests_factor_factor.pl.patch @@ -1,7 +1,7 @@ -diff --git a/tests/factor/factor.pl b/tests/factor/factor.pl -index b1406c266..3d97cd6a5 100755 ---- a/tests/factor/factor.pl -+++ b/tests/factor/factor.pl +Index: gnu/tests/factor/factor.pl +=================================================================== +--- gnu.orig/tests/factor/factor.pl ++++ gnu/tests/factor/factor.pl @@ -61,12 +61,14 @@ my @Tests = # Map newer glibc diagnostic to expected. # Also map OpenBSD 5.1's "unknown option" to expected "invalid option". diff --git a/util/gnu-patches/tests_invalid_opt.patch b/util/gnu-patches/tests_invalid_opt.patch index 1c70bc8c92a..c23476674e7 100644 --- a/util/gnu-patches/tests_invalid_opt.patch +++ b/util/gnu-patches/tests_invalid_opt.patch @@ -1,7 +1,7 @@ -diff --git a/tests/misc/invalid-opt.pl b/tests/misc/invalid-opt.pl -index 4b9c4c184..4ccd89482 100755 ---- a/tests/misc/invalid-opt.pl -+++ b/tests/misc/invalid-opt.pl +Index: gnu/tests/misc/invalid-opt.pl +=================================================================== +--- gnu.orig/tests/misc/invalid-opt.pl ++++ gnu/tests/misc/invalid-opt.pl @@ -74,23 +74,13 @@ foreach my $prog (@built_programs) defined $out or $out = ''; diff --git a/util/gnu-patches/tests_sort_merge.pl.patch b/util/gnu-patches/tests_sort_merge.pl.patch index a19677a6d0a..d6db2e09c60 100644 --- a/util/gnu-patches/tests_sort_merge.pl.patch +++ b/util/gnu-patches/tests_sort_merge.pl.patch @@ -1,7 +1,7 @@ -diff --git a/tests/sort/sort-merge.pl b/tests/sort/sort-merge.pl -index 89eed0c64..c2f5aa7e5 100755 ---- a/tests/sort/sort-merge.pl -+++ b/tests/sort/sort-merge.pl +Index: gnu/tests/sort/sort-merge.pl +=================================================================== +--- gnu.orig/tests/sort/sort-merge.pl ++++ gnu/tests/sort/sort-merge.pl @@ -43,22 +43,22 @@ my @Tests = # check validation of --batch-size option ['nmerge-0', "-m --batch-size=0", @inputs, diff --git a/util/gnu-patches/tests_tsort.patch b/util/gnu-patches/tests_tsort.patch index 40c612c288f..1084f97043a 100644 --- a/util/gnu-patches/tests_tsort.patch +++ b/util/gnu-patches/tests_tsort.patch @@ -1,7 +1,7 @@ -diff --git a/tests/misc/tsort.pl b/tests/misc/tsort.pl -index 70bdc474c..4fd420a4e 100755 ---- a/tests/misc/tsort.pl -+++ b/tests/misc/tsort.pl +Index: gnu/tests/misc/tsort.pl +=================================================================== +--- gnu.orig/tests/misc/tsort.pl ++++ gnu/tests/misc/tsort.pl @@ -54,8 +54,10 @@ my @Tests = ['only-one', {IN => {f => ""}}, {IN => {g => ""}},