1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-27 11:07:44 +00:00

Merge branch 'main' into sort-mem-percent

This commit is contained in:
Sylvestre Ledru 2025-01-23 22:52:00 +01:00 committed by GitHub
commit 4f83924092
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 890 additions and 411 deletions

View file

@ -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

View file

@ -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: <https://github.com/dawidd6/action-download-artifact>
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: |

View file

@ -1,3 +1,5 @@
tests/tail/inotify-dir-recreate
tests/timeout/timeout
tests/rm/rm1
tests/misc/stdbuf
tests/misc/usage_vs_getopt

25
Cargo.lock generated
View file

@ -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]]

View file

@ -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 }

View file

@ -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

View file

@ -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) => {
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()
}
}
None => String::new(),
}
})
}

View file

@ -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::<Vec<_>>(),
|files| files.map(OsStr::new).collect::<Vec<_>>(),
);
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)

View file

@ -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 }

View file

@ -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,

View file

@ -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

View file

@ -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 => {
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) {

View file

@ -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) => {
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

View file

@ -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::<Vec<_>>(),
|files| files.map(OsStr::new).collect::<Vec<_>>(),
);
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")

View file

@ -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 {

View file

@ -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));

View file

@ -154,12 +154,7 @@ fn handle_obsolete(args: &mut Vec<String>) -> Option<usize> {
}
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}");
}
}

View file

@ -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",

View file

@ -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<Local>) -> 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<Local>) -> 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<Local>) -> 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<TimeStyle, LsError> {
//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 {

View file

@ -101,8 +101,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let options = SeqOptions {
separator: matches
.get_one::<String>(OPT_SEPARATOR)
.map(|s| s.as_str())
.unwrap_or("\n")
.map_or("\n", |s| s.as_str())
.to_string(),
terminator: matches
.get_one::<String>(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::<num_format::Float>::parse(f)?;
Some(f)
}
None => None,
};
let format = options
.format
.map(Format::<num_format::Float>::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<num_format::Float>>,
format: Option<&Format<num_format::Float>>,
) -> std::io::Result<()> {
let stdout = stdout();
let mut stdout = stdout.lock();

View file

@ -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!(

View file

@ -224,11 +224,8 @@ fn read_write_loop<I: WriteableTmpFile>(
let mut sender_option = Some(sender);
let mut tmp_files = vec![];
loop {
let chunk = match receiver.recv() {
Ok(it) => it,
_ => {
let Ok(chunk) = receiver.recv() else {
return Ok(ReadResult::WroteChunksToFile { tmp_files });
}
};
let tmp_file = write::<I>(

View file

@ -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<char>) -> u8 {
'E' => 6,
'Z' => 7,
'Y' => 8,
'R' => 9,
'Q' => 10,
_ => 0,
}
} else {

View file

@ -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::<usize>() {
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.",

View file

@ -1408,20 +1408,17 @@ where
};
let bytes = line.as_slice();
match kth_chunk {
Some(chunk_number) => {
if let Some(chunk_number) = kth_chunk {
if (i % num_chunks) == (chunk_number - 1) as usize {
stdout_writer.write_all(bytes)?;
}
}
None => {
} 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;
if closed_writers == num_chunks {
// all writers are closed - stop reading

View file

@ -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<Result<ObsoleteArgs, ParseError>> {
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<Result<ObsoleteArgs, ParseError>
}
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 {

View file

@ -210,13 +210,8 @@ fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult<bool> {
fn files(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult<bool> {
// 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(_) => {
let Ok(metadata) = metadata else {
return false;
}
};
let file_type = metadata.file_type();

View file

@ -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),
)

View file

@ -599,14 +599,11 @@ fn parse_timestamp(s: &str) -> UResult<FileTime> {
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,
_ => {
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

View file

@ -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(_) => {
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();

View file

@ -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 = []

View file

@ -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")]

View file

@ -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<ChecksumError> for LineCheckError {
enum FileCheckError {
/// a generic UError was encountered in sub-functions
UError(Box<dyn UError>),
/// 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<Box<dyn UError>> for FileCheckError {
@ -148,15 +150,57 @@ impl From<ChecksumError> 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<usize>) -> UResult<HashAlgorithm> {
}
#[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_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<W: Write>(
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<Box<dyn Read>> {
fn identify_algo_name_and_length(
line_info: &LineInfo,
algo_name_input: Option<&str>,
last_algo: &mut Option<String>,
) -> Option<(String, Option<usize>)> {
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,10 +768,12 @@ fn process_algo_based_line(
line_info: &LineInfo,
cli_algo_name: Option<&str>,
opts: ChecksumOptions,
last_algo: &mut Option<String>,
) -> 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)
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
@ -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<usize>,
opts: ChecksumOptions,
cached_regex: &mut Option<LineFormat>,
last_algo: &mut Option<String>,
) -> Result<(), LineCheckError> {
let line_bytes = os_str_as_bytes(line)?;
@ -806,9 +850,12 @@ 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) {
let Some(line_info) = LineInfo::parse(line, cached_regex) else {
return Err(LineCheckError::ImproperlyFormatted);
};
if line_info.format == LineFormat::AlgoBased {
process_algo_based_line(&line_info, cli_algo_name, opts)
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
@ -817,24 +864,6 @@ fn process_checksum_line(
// 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
);
}
Err(LineCheckError::ImproperlyFormatted)
}
}
fn process_checksum_file(
@ -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
if opts.verbose.over_status() {
eprintln!(
"{}: {}: no file was verified",
util_name(),
filename_input.maybe_quote(),
);
set_exit_code(1);
}
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 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);
// 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);
}
// Obviously, if a checksum failed at some point, report the error.
if res.failed_cksum > 0 {
return Err(FileCheckError::Failed);
}
Ok(())
@ -950,17 +1008,24 @@ pub fn perform_checksum_validation<'a, I>(
where
I: Iterator<Item = &'a OsStr>,
{
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,
}
}
if failed {
Err(USimpleError::new(1, ""))
} else {
Ok(())
}
}
pub fn digest_reader<T: Read>(
digest: &mut Box<dyn Digest>,
@ -1416,7 +1481,7 @@ mod tests {
for (filename, result, prefix, expected) in cases {
let mut buffer: Vec<u8> = vec![];
print_file_report(&mut buffer, filename, *result, prefix, opts);
print_file_report(&mut buffer, filename, *result, prefix, opts.verbose);
assert_eq!(&buffer, expected)
}
}

View file

@ -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());
}
}

View file

@ -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()

View file

@ -79,13 +79,10 @@ pub fn apply_xattrs<P: AsRef<Path>>(
/// `true` if the file has extended attributes (indicating an ACL), `false` otherwise.
pub fn has_acl<P: AsRef<Path>>(file: P) -> bool {
// don't use exacl here, it is doing more getxattr call then needed
match xattr::list(file) {
Ok(acl) => {
xattr::list(file).is_ok_and(|acl| {
// if we have extra attributes, we have an acl
acl.count() > 0
}
Err(_) => false,
}
})
}
/// 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<P: AsRef<Path>>(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;
}

View file

@ -273,9 +273,7 @@ impl ChownExecutor {
#[allow(clippy::cognitive_complexity)]
fn traverse<P: AsRef<Path>>(&self, root: P) -> i32 {
let path = root.as_ref();
let meta = match self.obtain_meta(path, self.dereference) {
Some(m) => m,
_ => {
let Some(meta) = self.obtain_meta(path, self.dereference) else {
if self.verbosity.level == VerbosityLevel::Verbose {
println!(
"failed to change ownership of {} to {}",
@ -284,7 +282,6 @@ impl ChownExecutor {
);
}
return 1;
}
};
if self.recursive
@ -370,9 +367,8 @@ impl ChownExecutor {
Ok(entry) => entry,
};
let path = entry.path();
let meta = match self.obtain_meta(path, self.dereference) {
Some(m) => m,
_ => {
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
@ -380,7 +376,6 @@ impl ChownExecutor {
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<P: AsRef<Path>>(&self, path: P, follow: bool) -> Option<Metadata> {
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)
),
}
None
}
Ok(meta) => Some(meta),
strip_errno(e)
);
}
})
.ok()
}
#[inline]

View file

@ -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")]

View file

@ -269,16 +269,9 @@ impl<'parser> Parser<'parser> {
/// Same as `parse()` but tries to return u64
pub fn parse_u64(&self, size: &str) -> Result<u64, ParseSizeError> {
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

View file

@ -49,9 +49,8 @@ pub fn from_str(string: &str) -> Result<Duration, String> {
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),

View file

@ -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.

View file

@ -950,7 +950,7 @@ mod tests_split_iterator {
| '*' | '?' | '[' | '#' | '˜' | '=' | '%' => {
special = true;
}
_ => continue,
_ => (),
}
}

View file

@ -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]

View file

@ -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();
}

View file

@ -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]
@ -83,14 +81,26 @@ fn test_command_empty_args() {
}
#[test]
fn test_preserve_status() {
fn test_foreground() {
for arg in ["-f", "--foreground"] {
new_ucmd!()
.args(&["--preserve-status", ".1", "sleep", "10"])
.args(&[arg, ".1", "sleep", "10"])
.fails()
.code_is(124)
.no_output();
}
}
#[test]
fn test_preserve_status() {
for arg in ["-p", "--preserve-status"] {
new_ucmd!()
.args(&[arg, ".1", "sleep", "10"])
.fails()
// 128 + SIGTERM = 128 + 15
.code_is(128 + 15)
.no_stderr()
.no_stdout();
.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]

View file

@ -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

10
util/gnu-patches/series Normal file
View file

@ -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

View file

@ -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.

View file

@ -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)

View file

@ -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';

View file

@ -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'

View file

@ -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},

View file

@ -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".

View file

@ -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 = '';

View file

@ -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,

View file

@ -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 => ""}},