From ccf999473cdb2483e881ef9d3b1ae14799a0b917 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Tue, 31 Jan 2023 10:08:11 +0100 Subject: [PATCH 01/15] comm: use delimiter on "total" line --- src/uu/comm/src/comm.rs | 38 +++++++++---------- tests/by-util/test_comm.rs | 8 ++++ .../comm/ab_total_delimiter_word.expected | 4 ++ 3 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 tests/fixtures/comm/ab_total_delimiter_word.expected diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index b91f9ddbc..49da51fce 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -32,21 +32,12 @@ mod options { pub const TOTAL: &str = "total"; } -fn mkdelim(col: usize, opts: &ArgMatches) -> String { - let mut s = String::new(); - let delim = match opts.get_one::(options::DELIMITER).unwrap().as_str() { - "" => "\0", - delim => delim, - }; - - if col > 1 && !opts.get_flag(options::COLUMN_1) { - s.push_str(delim.as_ref()); +fn column_width(col: &str, opts: &ArgMatches) -> usize { + if opts.get_flag(col) { + 0 + } else { + 1 } - if col > 2 && !opts.get_flag(options::COLUMN_2) { - s.push_str(delim.as_ref()); - } - - s } fn ensure_nl(line: &mut String) { @@ -70,7 +61,16 @@ impl LineReader { } fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) { - let delim: Vec = (0..4).map(|col| mkdelim(col, opts)).collect(); + let delim = match opts.get_one::(options::DELIMITER).unwrap().as_str() { + "" => "\0", + delim => delim, + }; + + let width_col_1 = column_width(options::COLUMN_1, opts); + let width_col_2 = column_width(options::COLUMN_2, opts); + + let delim_col_2 = delim.repeat(width_col_1); + let delim_col_3 = delim.repeat(width_col_1 + width_col_2); let ra = &mut String::new(); let mut na = a.read_line(ra); @@ -98,7 +98,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) { Ordering::Less => { if !opts.get_flag(options::COLUMN_1) { ensure_nl(ra); - print!("{}{}", delim[1], ra); + print!("{ra}"); } ra.clear(); na = a.read_line(ra); @@ -107,7 +107,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) { Ordering::Greater => { if !opts.get_flag(options::COLUMN_2) { ensure_nl(rb); - print!("{}{}", delim[2], rb); + print!("{delim_col_2}{rb}"); } rb.clear(); nb = b.read_line(rb); @@ -116,7 +116,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) { Ordering::Equal => { if !opts.get_flag(options::COLUMN_3) { ensure_nl(ra); - print!("{}{}", delim[3], ra); + print!("{delim_col_3}{ra}"); } ra.clear(); rb.clear(); @@ -128,7 +128,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) { } if opts.get_flag(options::TOTAL) { - println!("{total_col_1}\t{total_col_2}\t{total_col_3}\ttotal"); + println!("{total_col_1}{delim}{total_col_2}{delim}{total_col_3}{delim}total"); } } diff --git a/tests/by-util/test_comm.rs b/tests/by-util/test_comm.rs index cea4d500c..727d9fee4 100644 --- a/tests/by-util/test_comm.rs +++ b/tests/by-util/test_comm.rs @@ -71,6 +71,14 @@ fn total_with_suppressed_regular_output() { .stdout_is_fixture("ab_total_suppressed_regular_output.expected"); } +#[test] +fn total_with_output_delimiter() { + new_ucmd!() + .args(&["--total", "--output-delimiter=word", "a", "b"]) + .succeeds() + .stdout_is_fixture("ab_total_delimiter_word.expected"); +} + #[test] fn output_delimiter() { new_ucmd!() diff --git a/tests/fixtures/comm/ab_total_delimiter_word.expected b/tests/fixtures/comm/ab_total_delimiter_word.expected new file mode 100644 index 000000000..f7215ea28 --- /dev/null +++ b/tests/fixtures/comm/ab_total_delimiter_word.expected @@ -0,0 +1,4 @@ +a +wordb +wordwordz +1word1word1wordtotal From 0ed6a9f882e1909a0ae81bd2014add0657fa450d Mon Sep 17 00:00:00 2001 From: Joining7943 <111500881+Joining7943@users.noreply.github.com> Date: Mon, 6 Feb 2023 10:13:31 +0100 Subject: [PATCH 02/15] tail: Fix parsing of sleep interval. Use duration parser from fundu crate. Activate tests for parsing sleep interval --- Cargo.lock | 7 ++++++ Cargo.toml | 3 ++- src/uu/tail/Cargo.toml | 2 ++ src/uu/tail/src/args.rs | 27 +++++++++++++--------- tests/by-util/test_tail.rs | 46 +++++++++++++++++--------------------- 5 files changed, 48 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3df301eac..c9a6094a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -878,6 +878,12 @@ dependencies = [ "libc", ] +[[package]] +name = "fundu" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925250bc259498d4008ee072bf16586083ab2c491aa4b06b3c4d0a6556cebd74" + [[package]] name = "futures" version = "0.3.25" @@ -3094,6 +3100,7 @@ version = "0.0.17" dependencies = [ "atty", "clap", + "fundu", "libc", "memchr", "nix", diff --git a/Cargo.toml b/Cargo.toml index 679f96d25..ebd8685ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # coreutils (uutils) # * see the repository LICENSE, README, and CONTRIBUTING files for more information -# spell-checker:ignore (libs) libselinux gethostid procfs bigdecimal kqueue +# spell-checker:ignore (libs) libselinux gethostid procfs bigdecimal kqueue fundu [package] name = "coreutils" @@ -282,6 +282,7 @@ filetime = "0.2" fnv = "1.0.7" fs_extra = "1.1.0" fts-sys = "0.2" +fundu = "0.3.0" gcd = "2.2" glob = "0.3.0" half = "2.1" diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index d6dbf2fb7..12736f36a 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -1,3 +1,4 @@ +# spell-checker:ignore (libs) kqueue fundu [package] name = "uu_tail" version = "0.0.17" @@ -22,6 +23,7 @@ notify = { workspace=true } uucore = { workspace=true, features=["ringbuffer", "lines"] } same-file = { workspace=true } atty = { workspace=true } +fundu = { workspace=true } [target.'cfg(windows)'.dependencies] windows-sys = { workspace=true, features = ["Win32_System_Threading", "Win32_Foundation"] } diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index 5f7ea1028..bfc314185 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -3,13 +3,14 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// spell-checker:ignore (ToDO) kqueue Signum +// spell-checker:ignore (ToDO) kqueue Signum fundu use crate::paths::Input; use crate::{parse, platform, Quotable}; use atty::Stream; use clap::crate_version; use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command}; +use fundu::DurationParser; use same_file::Handle; use std::collections::VecDeque; use std::ffi::OsString; @@ -148,16 +149,20 @@ impl Settings { settings.retry = matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_RETRY); - if let Some(s) = matches.get_one::(options::SLEEP_INT) { - settings.sleep_sec = match s.parse::() { - Ok(s) => Duration::from_secs_f32(s), - Err(_) => { - return Err(UUsageError::new( - 1, - format!("invalid number of seconds: {}", s.quote()), - )) - } - } + if let Some(source) = matches.get_one::(options::SLEEP_INT) { + // Advantage of `fundu` over `Duration::(try_)from_secs_f64(source.parse().unwrap())`: + // * doesn't panic on errors like `Duration::from_secs_f64` would. + // * no precision loss, rounding errors or other floating point problems. + // * evaluates to `Duration::MAX` if the parsed number would have exceeded + // `DURATION::MAX` or `infinity` was given + // * not applied here but it supports customizable time units and provides better error + // messages + settings.sleep_sec = + DurationParser::without_time_units() + .parse(source) + .map_err(|_| { + UUsageError::new(1, format!("invalid number of seconds: '{source}'")) + })?; } settings.use_polling = matches.get_flag(options::USE_POLLING); diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 5670e23c9..b2ec0e7bd 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -13,6 +13,7 @@ use crate::common::random::*; use crate::common::util::*; use pretty_assertions::assert_eq; use rand::distributions::Alphanumeric; +use rstest::rstest; use std::char::from_digit; use std::fs::File; use std::io::Write; @@ -4453,29 +4454,24 @@ fn test_follow_when_files_are_pointing_to_same_relative_file_and_file_stays_same .stdout_only(expected_stdout); } -#[test] -#[cfg(disable_until_fixed)] -fn test_args_sleep_interval_when_illegal_argument_then_usage_error() { - let scene = TestScenario::new(util_name!()); - for interval in [ - &format!("{}0", f64::MAX), - &format!("{}0.0", f64::MAX), - "1_000", - ".", - "' '", - "", - " ", - "0,0", - "one.zero", - ".zero", - "one.", - "0..0", - ] { - scene - .ucmd() - .args(&["--sleep-interval", interval]) - .run() - .usage_error(format!("invalid number of seconds: '{}'", interval)) - .code_is(1); - } +#[rstest] +#[case::exponent_exceed_float_max("1.0e2048")] +#[case::underscore_delimiter("1_000")] +#[case::only_point(".")] +#[case::space_in_primes("' '")] +#[case::space(" ")] +#[case::empty("")] +#[case::comma_separator("0,0")] +#[case::words_nominator_fract("one.zero")] +#[case::words_fract(".zero")] +#[case::words_nominator("one.")] +#[case::two_points("0..0")] +#[case::seconds_unit("1.0s")] +#[case::circumflex_exponent("1.0e^1000")] +fn test_args_sleep_interval_when_illegal_argument_then_usage_error(#[case] sleep_interval: &str) { + new_ucmd!() + .args(&["--sleep-interval", sleep_interval]) + .run() + .usage_error(format!("invalid number of seconds: '{sleep_interval}'")) + .code_is(1); } From 99f3fc835658b79e768b5e6e4d727e6c382dc8ac Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Tue, 14 Feb 2023 09:48:34 +0100 Subject: [PATCH 03/15] Replace get_long_usage fns with const --- src/uu/chmod/src/chmod.rs | 23 +++++------ src/uu/dirname/src/dirname.rs | 14 +++---- src/uu/mkdir/src/mkdir.rs | 12 ++---- src/uu/rm/src/rm.rs | 37 ++++++++---------- src/uu/stat/src/stat.rs | 67 +++------------------------------ src/uu/stat/stat.md | 64 +++++++++++++++++++++++++++++++ src/uu/tr/src/tr.rs | 15 +++----- src/uu/truncate/src/truncate.rs | 43 ++++++++++----------- src/uu/uniq/src/uniq.rs | 21 ++++------- 9 files changed, 135 insertions(+), 161 deletions(-) create mode 100644 src/uu/stat/stat.md diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index 4d578eeba..a2dc8e06b 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -19,8 +19,14 @@ use uucore::libc::mode_t; use uucore::mode; use uucore::{format_usage, show_error}; -static ABOUT: &str = "Change the mode of each FILE to MODE. - With --reference, change the mode of each FILE to that of RFILE."; +const ABOUT: &str = "Change the mode of each FILE to MODE.\n\ + With --reference, change the mode of each FILE to that of RFILE."; +const USAGE: &str = "\ + {} [OPTION]... MODE[,MODE]... FILE... + {} [OPTION]... OCTAL-MODE FILE... + {} [OPTION]... --reference=RFILE FILE..."; +const LONG_USAGE: &str = + "Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'."; mod options { pub const CHANGES: &str = "changes"; @@ -34,15 +40,6 @@ mod options { pub const FILE: &str = "FILE"; } -const USAGE: &str = "\ - {} [OPTION]... MODE[,MODE]... FILE... - {} [OPTION]... OCTAL-MODE FILE... - {} [OPTION]... --reference=RFILE FILE..."; - -fn get_long_usage() -> &'static str { - "Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'." -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut args = args.collect_lossy(); @@ -51,9 +48,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // a possible MODE prefix '-' needs to be removed (e.g. "chmod -x FILE"). let mode_had_minus_prefix = mode::strip_minus_from_mode(&mut args); - let after_help = get_long_usage(); - - let matches = uu_app().after_help(after_help).try_get_matches_from(args)?; + let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?; let changes = matches.get_flag(options::CHANGES); let quiet = matches.get_flag(options::QUIET); diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index b53f6135a..bebdd4d36 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -11,26 +11,22 @@ use uucore::display::print_verbatim; use uucore::error::{UResult, UUsageError}; use uucore::format_usage; -static ABOUT: &str = "Strip last component from file name"; +const ABOUT: &str = "Strip last component from file name"; const USAGE: &str = "{} [OPTION] NAME..."; +const LONG_USAGE: &str = "\ + Output each NAME with its last non-slash component and trailing slashes \n\ + removed; if NAME contains no /'s, output '.' (meaning the current directory)."; mod options { pub const ZERO: &str = "zero"; pub const DIR: &str = "dir"; } -fn get_long_usage() -> &'static str { - "Output each NAME with its last non-slash component and trailing slashes \n\ - removed; if NAME contains no /'s, output '.' (meaning the current directory)." -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = args.collect_lossy(); - let matches = uu_app() - .after_help(get_long_usage()) - .try_get_matches_from(args)?; + let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?; let separator = if matches.get_flag(options::ZERO) { "\0" diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index 046091b34..9aced8ba5 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -22,8 +22,10 @@ use uucore::{format_usage, show, show_if_err}; static DEFAULT_PERM: u32 = 0o755; -static ABOUT: &str = "Create the given DIRECTORY(ies) if they do not exist"; +const ABOUT: &str = "Create the given DIRECTORY(ies) if they do not exist"; const USAGE: &str = "{} [OPTION]... [USER]"; +const LONG_USAGE: &str = + "Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'."; mod options { pub const MODE: &str = "mode"; @@ -32,10 +34,6 @@ mod options { pub const DIRS: &str = "dirs"; } -fn get_long_usage() -> &'static str { - "Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'." -} - #[cfg(windows)] fn get_mode(_matches: &ArgMatches, _mode_had_minus_prefix: bool) -> Result { Ok(DEFAULT_PERM) @@ -92,9 +90,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Linux-specific options, not implemented // opts.optflag("Z", "context", "set SELinux security context" + // " of each created directory to CTX"), - let matches = uu_app() - .after_help(get_long_usage()) - .try_get_matches_from(args)?; + let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?; let dirs = matches .get_many::(options::DIRS) diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 850e18408..0d621355d 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -37,8 +37,22 @@ struct Options { verbose: bool, } -static ABOUT: &str = "Remove (unlink) the FILE(s)"; +const ABOUT: &str = "Remove (unlink) the FILE(s)"; const USAGE: &str = "{} [OPTION]... FILE..."; +const LONG_USAGE: &str = "\ +By default, rm does not remove directories. Use the --recursive (-r or -R) +option to remove each listed directory, too, along with all of its contents + +To remove a file whose name starts with a '-', for example '-foo', +use one of these commands: +rm -- -foo + +rm ./-foo + +Note that if you use rm to remove a file, it might be possible to recover +some of its contents, given sufficient expertise and/or time. For greater +assurance that the contents are truly unrecoverable, consider using shred."; + static OPT_DIR: &str = "dir"; static OPT_INTERACTIVE: &str = "interactive"; static OPT_FORCE: &str = "force"; @@ -53,28 +67,9 @@ static PRESUME_INPUT_TTY: &str = "-presume-input-tty"; static ARG_FILES: &str = "files"; -fn get_long_usage() -> String { - String::from( - "By default, rm does not remove directories. Use the --recursive (-r or -R) - option to remove each listed directory, too, along with all of its contents - - To remove a file whose name starts with a '-', for example '-foo', - use one of these commands: - rm -- -foo - - rm ./-foo - - Note that if you use rm to remove a file, it might be possible to recover - some of its contents, given sufficient expertise and/or time. For greater - assurance that the contents are truly unrecoverable, consider using shred.", - ) -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app() - .after_help(get_long_usage()) - .try_get_matches_from(args)?; + let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?; let files: Vec = matches .get_many::(ARG_FILES) diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 119c9c7fb..2d0949223 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -13,7 +13,7 @@ use uucore::fsext::{ pretty_filetype, pretty_fstype, pretty_time, read_fs_list, statfs, BirthTime, FsMeta, }; use uucore::libc::mode_t; -use uucore::{entries, format_usage, show_error, show_warning}; +use uucore::{entries, format_usage, help_section, help_usage, show_error, show_warning}; use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use std::borrow::Cow; @@ -24,8 +24,9 @@ use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::os::unix::prelude::OsStrExt; use std::path::Path; -const ABOUT: &str = "Display file or file system status."; -const USAGE: &str = "{} [OPTION]... FILE..."; +const ABOUT: &str = help_section!("about", "stat.md"); +const USAGE: &str = help_usage!("stat.md"); +const LONG_USAGE: &str = help_section!("long usage", "stat.md"); mod options { pub const DEREFERENCE: &str = "dereference"; @@ -751,67 +752,9 @@ impl Stater { } } -fn get_long_usage() -> &'static str { - " -The valid format sequences for files (without --file-system): - - %a access rights in octal (note '#' and '0' printf flags) - %A access rights in human readable form - %b number of blocks allocated (see %B) - %B the size in bytes of each block reported by %b - %C SELinux security context string - %d device number in decimal - %D device number in hex - %f raw mode in hex - %F file type - %g group ID of owner - %G group name of owner - %h number of hard links - %i inode number - %m mount point - %n file name - %N quoted file name with dereference if symbolic link - %o optimal I/O transfer size hint - %s total size, in bytes - %t major device type in hex, for character/block device special files - %T minor device type in hex, for character/block device special files - %u user ID of owner - %U user name of owner - %w time of file birth, human-readable; - if unknown - %W time of file birth, seconds since Epoch; 0 if unknown - %x time of last access, human-readable - %X time of last access, seconds since Epoch - %y time of last data modification, human-readable - %Y time of last data modification, seconds since Epoch - %z time of last status change, human-readable - %Z time of last status change, seconds since Epoch - -Valid format sequences for file systems: - - %a free blocks available to non-superuser - %b total data blocks in file system - %c total file nodes in file system - %d free file nodes in file system - %f free blocks in file system - %i file system ID in hex - %l maximum length of filenames - %n file name - %s block size (for faster transfers) - %S fundamental block size (for block counts) - %t file system type in hex - %T file system type in human readable form - -NOTE: your shell may have its own version of stat, which usually supersedes -the version described here. Please refer to your shell's documentation -for details about the options it supports. -" -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app() - .after_help(get_long_usage()) - .try_get_matches_from(args)?; + let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?; let stater = Stater::new(&matches)?; let exit_status = stater.exec(); diff --git a/src/uu/stat/stat.md b/src/uu/stat/stat.md new file mode 100644 index 000000000..5c8f86bc6 --- /dev/null +++ b/src/uu/stat/stat.md @@ -0,0 +1,64 @@ +# stat + +## About + +Display file or file system status. + +## Usage +``` +stat [OPTION]... FILE... +``` + +## Long Usage + +The valid format sequences for files (without --file-system): + + %a access rights in octal (note '#' and '0' printf flags) + %A access rights in human readable form + %b number of blocks allocated (see %B) + %B the size in bytes of each block reported by %b + %C SELinux security context string + %d device number in decimal + %D device number in hex + %f raw mode in hex + %F file type + %g group ID of owner + %G group name of owner + %h number of hard links + %i inode number + %m mount point + %n file name + %N quoted file name with dereference if symbolic link + %o optimal I/O transfer size hint + %s total size, in bytes + %t major device type in hex, for character/block device special files + %T minor device type in hex, for character/block device special files + %u user ID of owner + %U user name of owner + %w time of file birth, human-readable; - if unknown + %W time of file birth, seconds since Epoch; 0 if unknown + %x time of last access, human-readable + %X time of last access, seconds since Epoch + %y time of last data modification, human-readable + %Y time of last data modification, seconds since Epoch + %z time of last status change, human-readable + %Z time of last status change, seconds since Epoch + +Valid format sequences for file systems: + + %a free blocks available to non-superuser + %b total data blocks in file system + %c total file nodes in file system + %d free file nodes in file system + %f free blocks in file system + %i file system ID in hex + %l maximum length of filenames + %n file name + %s block size (for faster transfers) + %S fundamental block size (for block counts) + %t file system type in hex + %T file system type in human readable form + +NOTE: your shell may have its own version of stat, which usually supersedes +the version described here. Please refer to your shell's documentation +for details about the options it supports. diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index d63f8bced..8ccd08366 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -19,8 +19,11 @@ use crate::operation::DeleteOperation; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError}; -static ABOUT: &str = "Translate or delete characters"; +const ABOUT: &str = "Translate or delete characters"; const USAGE: &str = "{} [OPTION]... SET1 [SET2]"; +const LONG_USAGE: &str = "\ + Translate, squeeze, and/or delete characters from standard input, \ + writing to standard output."; mod options { pub const COMPLEMENT: &str = "complement"; @@ -30,19 +33,11 @@ mod options { pub const SETS: &str = "sets"; } -fn get_long_usage() -> String { - "Translate, squeeze, and/or delete characters from standard input, \ - writing to standard output." - .to_string() -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = args.collect_lossy(); - let matches = uu_app() - .after_help(get_long_usage()) - .try_get_matches_from(args)?; + let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?; let delete_flag = matches.get_flag(options::DELETE); let complement_flag = matches.get_flag(options::COMPLEMENT); diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 5a5ef0a97..090865313 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -73,8 +73,25 @@ impl TruncateMode { } } -static ABOUT: &str = "Shrink or extend the size of each file to the specified size."; +const ABOUT: &str = "Shrink or extend the size of each file to the specified size."; const USAGE: &str = "{} [OPTION]... [FILE]..."; +const LONG_USAGE: &str = "\ +SIZE is an integer with an optional prefix and optional unit. +The available units (K, M, G, T, P, E, Z, and Y) use the following format: + 'KB' => 1000 (kilobytes) + 'K' => 1024 (kibibytes) + 'MB' => 1000*1000 (megabytes) + 'M' => 1024*1024 (mebibytes) + 'GB' => 1000*1000*1000 (gigabytes) + 'G' => 1024*1024*1024 (gibibytes) +SIZE may also be prefixed by one of the following to adjust the size of each +file based on its current size: + '+' => extend by + '-' => reduce by + '<' => at most + '>' => at least + '/' => round down to multiple of + '%' => round up to multiple of"; pub mod options { pub static IO_BLOCKS: &str = "io-blocks"; @@ -84,32 +101,10 @@ pub mod options { pub static ARG_FILES: &str = "files"; } -fn get_long_usage() -> String { - String::from( - " - SIZE is an integer with an optional prefix and optional unit. - The available units (K, M, G, T, P, E, Z, and Y) use the following format: - 'KB' => 1000 (kilobytes) - 'K' => 1024 (kibibytes) - 'MB' => 1000*1000 (megabytes) - 'M' => 1024*1024 (mebibytes) - 'GB' => 1000*1000*1000 (gigabytes) - 'G' => 1024*1024*1024 (gibibytes) - SIZE may also be prefixed by one of the following to adjust the size of each - file based on its current size: - '+' => extend by - '-' => reduce by - '<' => at most - '>' => at least - '/' => round down to multiple of - '%' => round up to multiple of", - ) -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app() - .after_help(get_long_usage()) + .after_help(LONG_USAGE) .try_get_matches_from(args) .map_err(|e| { e.print().expect("Error writing clap::Error"); diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 7275e8877..79ed743bb 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -15,8 +15,14 @@ use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::format_usage; -static ABOUT: &str = "Report or omit repeated lines."; +const ABOUT: &str = "Report or omit repeated lines."; const USAGE: &str = "{} [OPTION]... [INPUT [OUTPUT]]..."; +const LONG_USAGE: &str = "\ + Filter adjacent matching lines from INPUT (or standard input),\n\ + writing to OUTPUT (or standard output).\n\n\ + Note: 'uniq' does not detect repeated lines unless they are adjacent.\n\ + You may want to sort the input first, or use 'sort -u' without 'uniq'."; + pub mod options { pub static ALL_REPEATED: &str = "all-repeated"; pub static CHECK_CHARS: &str = "check-chars"; @@ -241,20 +247,9 @@ fn opt_parsed(opt_name: &str, matches: &ArgMatches) -> UResult String { - String::from( - "Filter adjacent matching lines from INPUT (or standard input),\n\ - writing to OUTPUT (or standard output). - Note: 'uniq' does not detect repeated lines unless they are adjacent.\n\ - You may want to sort the input first, or use 'sort -u' without 'uniq'.\n", - ) -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app() - .after_help(get_long_usage()) - .try_get_matches_from(args)?; + let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?; let files: Vec = matches .get_many::(ARG_FILES) From 3a613cbc7fe12cfb9d281925728fd2403a158aad Mon Sep 17 00:00:00 2001 From: Joining7943 <111500881+Joining7943@users.noreply.github.com> Date: Sun, 18 Dec 2022 00:30:32 +0100 Subject: [PATCH 04/15] tests/util: Fix path resolution of test executable --- tests/common/util.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/common/util.rs b/tests/common/util.rs index 7229deb5a..f7b62a695 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -36,11 +36,6 @@ use std::{env, hint, thread}; use tempfile::{Builder, TempDir}; use uucore::Args; -#[cfg(windows)] -static PROGNAME: &str = concat!(env!("CARGO_PKG_NAME"), ".exe"); -#[cfg(not(windows))] -static PROGNAME: &str = env!("CARGO_PKG_NAME"); - static TESTS_DIR: &str = "tests"; static FIXTURES_DIR: &str = "fixtures"; @@ -1101,13 +1096,7 @@ impl TestScenario { pub fn new(util_name: &str) -> Self { let tmpd = Rc::new(TempDir::new().unwrap()); let ts = Self { - bin_path: { - // Instead of hard coding the path relative to the current - // directory, use Cargo's OUT_DIR to find path to executable. - // This allows tests to be run using profiles other than debug. - let target_dir = path_concat!(env!("OUT_DIR"), "..", "..", "..", PROGNAME); - PathBuf::from(AtPath::new(Path::new(&target_dir)).root_dir_resolved()) - }, + bin_path: PathBuf::from(env!("CARGO_BIN_EXE_coreutils")), util_name: String::from(util_name), fixtures: AtPath::new(tmpd.as_ref().path()), tmpd, From ec81a23afcc4c6fba0673c6141f328817fa661bd Mon Sep 17 00:00:00 2001 From: Yang Hau Date: Fri, 10 Feb 2023 22:16:07 +0800 Subject: [PATCH 05/15] tr: Remove the extra newline in stderr closes #4301 --- src/uu/tr/src/operation.rs | 12 ++++++------ tests/by-util/test_tr.rs | 32 ++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/uu/tr/src/operation.rs b/src/uu/tr/src/operation.rs index 03cea92e7..f27ccc0b9 100644 --- a/src/uu/tr/src/operation.rs +++ b/src/uu/tr/src/operation.rs @@ -37,21 +37,21 @@ pub enum BadSequence { impl Display for BadSequence { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::MissingCharClassName => writeln!(f, "missing character class name '[::]'"), + Self::MissingCharClassName => write!(f, "missing character class name '[::]'"), Self::MissingEquivalentClassChar => { - writeln!(f, "missing equivalence class character '[==]'") + write!(f, "missing equivalence class character '[==]'") } Self::MultipleCharRepeatInSet2 => { - writeln!(f, "only one [c*] repeat construct may appear in string2") + write!(f, "only one [c*] repeat construct may appear in string2") } Self::CharRepeatInSet1 => { - writeln!(f, "the [c*] repeat construct may not appear in string1") + write!(f, "the [c*] repeat construct may not appear in string1") } Self::InvalidRepeatCount(count) => { - writeln!(f, "invalid repeat count '{count}' in [c*n] construct") + write!(f, "invalid repeat count '{count}' in [c*n] construct") } Self::EmptySet2WhenNotTruncatingSet1 => { - writeln!(f, "when not truncating set1, string2 must be non-empty") + write!(f, "when not truncating set1, string2 must be non-empty") } } } diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index 0a098801b..e6daa9c48 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -777,10 +777,7 @@ fn check_against_gnu_tr_tests_range_a_a() { .stdout_is("zbc"); } -// FIXME: Since pr https://github.com/uutils/coreutils/pull/4261: -// stderr ends with 2 newlines but expected is only 1. #[test] -#[cfg(disabled_until_fixed)] fn check_against_gnu_tr_tests_null() { // ['null', qw(a ''), {IN=>''}, {OUT=>''}, {EXIT=>1}, // {ERR=>"$prog: when not truncating set1, string2 must be non-empty\n"}], @@ -855,10 +852,7 @@ fn check_against_gnu_tr_tests_rep_3() { .stdout_is("1x2"); } -// FIXME: Since pr https://github.com/uutils/coreutils/pull/4261: -// stderr ends with 2 newlines but expected is only 1. #[test] -#[cfg(disabled_until_fixed)] fn check_against_gnu_tr_tests_o_rep_1() { // # Another couple octal repeat count tests. // ['o-rep-1', qw('[b*08]' '[x*]'), {IN=>''}, {OUT=>''}, {EXIT=>1}, @@ -1032,10 +1026,6 @@ fn check_against_gnu_tr_tests_ross_6() { .stdout_is(""); } -// FIXME: Since pr https://github.com/uutils/coreutils/pull/4261: -// stderr ends with 2 newlines but expected is only 1. -#[test] -#[cfg(disabled_until_fixed)] #[test] fn check_against_gnu_tr_tests_empty_eq() { // # Ensure that these fail. @@ -1049,10 +1039,6 @@ fn check_against_gnu_tr_tests_empty_eq() { .stderr_is("tr: missing equivalence class character '[==]'\n"); } -// FIXME: Since pr https://github.com/uutils/coreutils/pull/4261: -// stderr ends with 2 newlines but expected is only 1. -#[test] -#[cfg(disabled_until_fixed)] #[test] fn check_against_gnu_tr_tests_empty_cc() { // ['empty-cc', qw('[::]' x), {IN=>''}, {OUT=>''}, {EXIT=>1}, @@ -1064,6 +1050,24 @@ fn check_against_gnu_tr_tests_empty_cc() { .stderr_is("tr: missing character class name '[::]'\n"); } +#[test] +fn check_against_gnu_tr_tests_repeat_set1() { + new_ucmd!() + .args(&["[a*]", "a"]) + .pipe_in("") + .fails() + .stderr_is("tr: the [c*] repeat construct may not appear in string1\n"); +} + +#[test] +fn check_against_gnu_tr_tests_repeat_set2() { + new_ucmd!() + .args(&["a", "[a*][a*]"]) + .pipe_in("") + .fails() + .stderr_is("tr: only one [c*] repeat construct may appear in string2\n"); +} + #[test] fn check_against_gnu_tr_tests_repeat_bs_9() { // # Weird repeat counts. From 04b6d806a21cfed9473e0071ec0b862c1053bad4 Mon Sep 17 00:00:00 2001 From: "Guilherme A. de Souza" <125218612+rgasnix@users.noreply.github.com> Date: Tue, 14 Feb 2023 18:43:09 -0300 Subject: [PATCH 06/15] nproc: replace num_cpus crate with thread::available_parallelism (#4352) * nproc: replace num_cpus crate with std::thread::available_parallelism * nproc: unwrap Result for Windows * nproc: if thread::available_parallelism results in err return 1 * nproc: wrap the call to available_parallelism into a function * nproc: remove comment in the wrong place * nproc: fix style violation * nproc: fix comment, refers to the new function --- Cargo.lock | 1 - src/uu/nproc/Cargo.toml | 1 - src/uu/nproc/src/nproc.rs | 23 ++++++++++++++++------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3df301eac..5668a918e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2807,7 +2807,6 @@ version = "0.0.17" dependencies = [ "clap", "libc", - "num_cpus", "uucore", ] diff --git a/src/uu/nproc/Cargo.toml b/src/uu/nproc/Cargo.toml index dc51d8a55..c2c224a40 100644 --- a/src/uu/nproc/Cargo.toml +++ b/src/uu/nproc/Cargo.toml @@ -16,7 +16,6 @@ path = "src/nproc.rs" [dependencies] libc = { workspace=true } -num_cpus = { workspace=true } clap = { workspace=true } uucore = { workspace=true, features=["fs"] } diff --git a/src/uu/nproc/src/nproc.rs b/src/uu/nproc/src/nproc.rs index 960957df6..fbfeebb23 100644 --- a/src/uu/nproc/src/nproc.rs +++ b/src/uu/nproc/src/nproc.rs @@ -8,7 +8,7 @@ // spell-checker:ignore (ToDO) NPROCESSORS nprocs numstr threadstr sysconf use clap::{crate_version, Arg, ArgAction, Command}; -use std::env; +use std::{env, thread}; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; use uucore::format_usage; @@ -73,16 +73,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // If OMP_NUM_THREADS=0, rejects the value let thread: Vec<&str> = threadstr.split_terminator(',').collect(); match &thread[..] { - [] => num_cpus::get(), + [] => available_parallelism(), [s, ..] => match s.parse() { - Ok(0) | Err(_) => num_cpus::get(), + Ok(0) | Err(_) => available_parallelism(), Ok(n) => n, }, } } // the variable 'OMP_NUM_THREADS' doesn't exist // fallback to the regular CPU detection - Err(_) => num_cpus::get(), + Err(_) => available_parallelism(), } }; @@ -127,7 +127,7 @@ fn num_cpus_all() -> usize { if nprocs == 1 { // In some situation, /proc and /sys are not mounted, and sysconf returns 1. // However, we want to guarantee that `nproc --all` >= `nproc`. - num_cpus::get() + available_parallelism() } else if nprocs > 0 { nprocs as usize } else { @@ -135,7 +135,7 @@ fn num_cpus_all() -> usize { } } -// Other platforms (e.g., windows), num_cpus::get() directly. +// Other platforms (e.g., windows), available_parallelism() directly. #[cfg(not(any( target_os = "linux", target_vendor = "apple", @@ -143,5 +143,14 @@ fn num_cpus_all() -> usize { target_os = "netbsd" )))] fn num_cpus_all() -> usize { - num_cpus::get() + available_parallelism() +} + +// In some cases, thread::available_parallelism() may return an Err +// In this case, we will return 1 (like GNU) +fn available_parallelism() -> usize { + match thread::available_parallelism() { + Ok(n) => n.get(), + Err(_) => 1, + } } From 675c55ba0181dd5cd351a3b88773eb4ba15cc06f Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Wed, 15 Feb 2023 16:16:10 +0100 Subject: [PATCH 07/15] stat: fix markdown formatting --- src/uu/stat/stat.md | 86 ++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/uu/stat/stat.md b/src/uu/stat/stat.md index 5c8f86bc6..4304e4b0c 100644 --- a/src/uu/stat/stat.md +++ b/src/uu/stat/stat.md @@ -11,53 +11,53 @@ stat [OPTION]... FILE... ## Long Usage -The valid format sequences for files (without --file-system): +The valid format sequences for files (without `--file-system`): - %a access rights in octal (note '#' and '0' printf flags) - %A access rights in human readable form - %b number of blocks allocated (see %B) - %B the size in bytes of each block reported by %b - %C SELinux security context string - %d device number in decimal - %D device number in hex - %f raw mode in hex - %F file type - %g group ID of owner - %G group name of owner - %h number of hard links - %i inode number - %m mount point - %n file name - %N quoted file name with dereference if symbolic link - %o optimal I/O transfer size hint - %s total size, in bytes - %t major device type in hex, for character/block device special files - %T minor device type in hex, for character/block device special files - %u user ID of owner - %U user name of owner - %w time of file birth, human-readable; - if unknown - %W time of file birth, seconds since Epoch; 0 if unknown - %x time of last access, human-readable - %X time of last access, seconds since Epoch - %y time of last data modification, human-readable - %Y time of last data modification, seconds since Epoch - %z time of last status change, human-readable - %Z time of last status change, seconds since Epoch + %a access rights in octal (note '#' and '0' printf flags) + %A access rights in human readable form + %b number of blocks allocated (see %B) + %B the size in bytes of each block reported by %b + %C SELinux security context string + %d device number in decimal + %D device number in hex + %f raw mode in hex + %F file type + %g group ID of owner + %G group name of owner + %h number of hard links + %i inode number + %m mount point + %n file name + %N quoted file name with dereference if symbolic link + %o optimal I/O transfer size hint + %s total size, in bytes + %t major device type in hex, for character/block device special files + %T minor device type in hex, for character/block device special files + %u user ID of owner + %U user name of owner + %w time of file birth, human-readable; - if unknown + %W time of file birth, seconds since Epoch; 0 if unknown + %x time of last access, human-readable + %X time of last access, seconds since Epoch + %y time of last data modification, human-readable + %Y time of last data modification, seconds since Epoch + %z time of last status change, human-readable + %Z time of last status change, seconds since Epoch Valid format sequences for file systems: - %a free blocks available to non-superuser - %b total data blocks in file system - %c total file nodes in file system - %d free file nodes in file system - %f free blocks in file system - %i file system ID in hex - %l maximum length of filenames - %n file name - %s block size (for faster transfers) - %S fundamental block size (for block counts) - %t file system type in hex - %T file system type in human readable form + %a free blocks available to non-superuser + %b total data blocks in file system + %c total file nodes in file system + %d free file nodes in file system + %f free blocks in file system + %i file system ID in hex + %l maximum length of filenames + %n file name + %s block size (for faster transfers) + %S fundamental block size (for block counts) + %t file system type in hex + %T file system type in human readable form NOTE: your shell may have its own version of stat, which usually supersedes the version described here. Please refer to your shell's documentation From 5e98227714559ebaca4c7ae23f682716646dcbc4 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 15 Feb 2023 08:35:19 +0100 Subject: [PATCH 08/15] refresh the installation doc with new distros --- docs/src/installation.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/src/installation.md b/docs/src/installation.md index cb5c89163..e2e72699b 100644 --- a/docs/src/installation.md +++ b/docs/src/installation.md @@ -43,13 +43,23 @@ pacman -S uutils-coreutils ### Debian -[![Debian Unstable package](https://repology.org/badge/version-for-repo/debian_unstable/uutils-coreutils.svg)](https://packages.debian.org/sid/source/rust-coreutils) +[![Debian package](https://repology.org/badge/version-for-repo/debian_unstable/uutils-coreutils.svg)](https://packages.debian.org/sid/source/rust-coreutils) ```bash apt install rust-coreutils +# To use it: +export PATH=/usr/lib/cargo/bin/coreutils:$PATH ``` -> **Note**: Requires the `unstable` repository. +> **Note**: Only available from Bookworm (Debian 12) + +### Gentoo + +[![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/uutils-coreutils.svg)](https://packages.gentoo.org/packages/sys-apps/uutils) + +```bash +emerge -pv sys-apps/uutils +``` ### Manjaro ![Manjaro Stable package](https://repology.org/badge/version-for-repo/manjaro_stable/uutils-coreutils.svg) @@ -69,6 +79,18 @@ pamac install uutils-coreutils nix-env -iA nixos.uutils-coreutils ``` +### Ubuntu + +[![Ubuntu package](https://repology.org/badge/version-for-repo/ubuntu_23_04/uutils-coreutils.svg)](https://packages.ubuntu.com/source/lunar/rust-coreutils) + +```bash +apt install rust-coreutils +# To use it: +export PATH=/usr/lib/cargo/bin/coreutils:$PATH +``` + +> **Note**: Only available from Kinetic (Ubuntu 22.10) + ## MacOS ### Homebrew From 34e31f20e74846d6984b985e16e75996d190b5e0 Mon Sep 17 00:00:00 2001 From: papparapa <37232476+papparapa@users.noreply.github.com> Date: Thu, 16 Feb 2023 19:23:32 +0900 Subject: [PATCH 09/15] cp: move help strings to markdown file (#4372) * cp: move help strings to markdown file * cp: change markdown section order --- src/uu/cp/cp.md | 12 ++++++++++++ src/uu/cp/src/cp.rs | 9 +++------ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 src/uu/cp/cp.md diff --git a/src/uu/cp/cp.md b/src/uu/cp/cp.md new file mode 100644 index 000000000..3659423ff --- /dev/null +++ b/src/uu/cp/cp.md @@ -0,0 +1,12 @@ +# cp + +## Usage +``` +cp [OPTION]... [-T] SOURCE DEST +cp [OPTION]... SOURCE... DIRECTORY +cp [OPTION]... -t DIRECTORY SOURCE... +``` + +## About + +Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY. diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 1b7d58d06..a41ef836b 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -40,7 +40,7 @@ use uucore::error::{set_exit_code, UClapError, UError, UResult, UUsageError}; use uucore::fs::{ canonicalize, paths_refer_to_same_file, FileInformation, MissingHandling, ResolveMode, }; -use uucore::{crash, format_usage, prompt_yes, show_error, show_warning}; +use uucore::{crash, format_usage, help_section, help_usage, prompt_yes, show_error, show_warning}; use crate::copydir::copy_directory; @@ -228,13 +228,10 @@ pub struct Options { progress_bar: bool, } -static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."; +const ABOUT: &str = help_section!("about", "cp.md"); static EXIT_ERR: i32 = 1; -const USAGE: &str = "\ - {} [OPTION]... [-T] SOURCE DEST - {} [OPTION]... SOURCE... DIRECTORY - {} [OPTION]... -t DIRECTORY SOURCE..."; +const USAGE: &str = help_usage!("cp.md"); // Argument constants mod options { From 5cc91304900204865b29a0c46a00390161e0df17 Mon Sep 17 00:00:00 2001 From: Koki Ueha Date: Thu, 16 Feb 2023 10:47:34 +0000 Subject: [PATCH 10/15] cat: move help strings to markdown file --- src/uu/cat/cat.md | 11 +++++++++++ src/uu/cat/src/cat.rs | 7 +++---- 2 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 src/uu/cat/cat.md diff --git a/src/uu/cat/cat.md b/src/uu/cat/cat.md new file mode 100644 index 000000000..0188be123 --- /dev/null +++ b/src/uu/cat/cat.md @@ -0,0 +1,11 @@ +# cat + +## Usage +``` +cat [OPTION]... [FILE]... +``` + +## About + +Concatenate FILE(s), or standard input, to standard output +With no FILE, or when FILE is -, read standard input. diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 35ef5abc1..d8cc46035 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -33,11 +33,10 @@ use std::net::Shutdown; use std::os::unix::fs::FileTypeExt; #[cfg(unix)] use std::os::unix::net::UnixStream; -use uucore::format_usage; +use uucore::{format_usage, help_section, help_usage}; -static USAGE: &str = "{} [OPTION]... [FILE]..."; -static ABOUT: &str = "Concatenate FILE(s), or standard input, to standard output -With no FILE, or when FILE is -, read standard input."; +const USAGE: &str = help_usage!("cat.md"); +const ABOUT: &str = help_section!("about", "cat.md"); #[derive(Error, Debug)] enum CatError { From f610f33aa73b501a8a9da3ac45320d1e54f1f6dd Mon Sep 17 00:00:00 2001 From: Joining7943 <111500881+Joining7943@users.noreply.github.com> Date: Thu, 16 Feb 2023 15:33:33 +0100 Subject: [PATCH 11/15] `tests/util`: Don't trim output in `CmdResult::stdout_matches` and `stdout_does_not_match` (#4304) * tests/util: Fix documentation of UCommand::stderr_only and usage_error * tests/util: Remove trimming from CmdResult::stdout_matches and stdout_does_not_match. Fix tests. The tests are fixed to match the trailing newline instead of ignoring it. --- tests/by-util/test_date.rs | 10 +++++----- tests/by-util/test_dir.rs | 2 +- tests/by-util/test_ls.rs | 2 +- tests/by-util/test_realpath.rs | 6 +++--- tests/by-util/test_vdir.rs | 2 +- tests/common/util.rs | 8 ++++---- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 4442f2df4..a1064a8fa 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -68,10 +68,10 @@ fn test_date_utc() { fn test_date_format_y() { let scene = TestScenario::new(util_name!()); - let mut re = Regex::new(r"^\d{4}$").unwrap(); + let mut re = Regex::new(r"^\d{4}\n$").unwrap(); scene.ucmd().arg("+%Y").succeeds().stdout_matches(&re); - re = Regex::new(r"^\d{2}$").unwrap(); + re = Regex::new(r"^\d{2}\n$").unwrap(); scene.ucmd().arg("+%y").succeeds().stdout_matches(&re); } @@ -82,7 +82,7 @@ fn test_date_format_m() { let mut re = Regex::new(r"\S+").unwrap(); scene.ucmd().arg("+%b").succeeds().stdout_matches(&re); - re = Regex::new(r"^\d{2}$").unwrap(); + re = Regex::new(r"^\d{2}\n$").unwrap(); scene.ucmd().arg("+%m").succeeds().stdout_matches(&re); } @@ -96,7 +96,7 @@ fn test_date_format_day() { re = Regex::new(r"\S+").unwrap(); scene.ucmd().arg("+%A").succeeds().stdout_matches(&re); - re = Regex::new(r"^\d{1}$").unwrap(); + re = Regex::new(r"^\d{1}\n$").unwrap(); scene.ucmd().arg("+%u").succeeds().stdout_matches(&re); } @@ -117,7 +117,7 @@ fn test_date_issue_3780() { #[test] fn test_date_nano_seconds() { // %N nanoseconds (000000000..999999999) - let re = Regex::new(r"^\d{1,9}$").unwrap(); + let re = Regex::new(r"^\d{1,9}\n$").unwrap(); new_ucmd!().arg("+%N").succeeds().stdout_matches(&re); } diff --git a/tests/by-util/test_dir.rs b/tests/by-util/test_dir.rs index 4d20b8e8e..068ffbbcc 100644 --- a/tests/by-util/test_dir.rs +++ b/tests/by-util/test_dir.rs @@ -49,5 +49,5 @@ fn test_long_output() { .ucmd() .arg("-l") .succeeds() - .stdout_matches(&Regex::new("[rwx-]{10}.*some-file1$").unwrap()); + .stdout_matches(&Regex::new("[rwx-]{10}.*some-file1\n$").unwrap()); } diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index f1ab3eea4..339c78c12 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -950,7 +950,7 @@ fn test_ls_commas_trailing() { .arg("./test-commas-trailing-1") .arg("./test-commas-trailing-2") .succeeds() - .stdout_matches(&Regex::new(r"\S$").unwrap()); // matches if there is no whitespace at the end of stdout. + .stdout_matches(&Regex::new(r"\S\n$").unwrap()); } #[test] diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index 5ff98c78d..2993d8dbb 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -300,7 +300,7 @@ fn test_relative_base_not_prefix_of_relative_to() { .succeeds(); #[cfg(windows)] - result.stdout_matches(&Regex::new(r"^.*:\\usr\n.*:\\usr\\local$").unwrap()); + result.stdout_matches(&Regex::new(r"^.*:\\usr\n.*:\\usr\\local\n$").unwrap()); #[cfg(not(windows))] result.stdout_is("/usr\n/usr/local\n"); @@ -344,7 +344,7 @@ fn test_relative() { #[cfg(not(windows))] result.stdout_is("/tmp\n.\n"); #[cfg(windows)] - result.stdout_matches(&Regex::new(r"^.*:\\tmp\n\.$").unwrap()); + result.stdout_matches(&Regex::new(r"^.*:\\tmp\n\.\n$").unwrap()); new_ucmd!() .args(&["-sm", "--relative-base=/", "--relative-to=/", "/", "/usr"]) @@ -357,7 +357,7 @@ fn test_relative() { #[cfg(not(windows))] result.stdout_is("/tmp\n.\n"); #[cfg(windows)] - result.stdout_matches(&Regex::new(r"^.*:\\tmp\n\.$").unwrap()); + result.stdout_matches(&Regex::new(r"^.*:\\tmp\n\.\n$").unwrap()); new_ucmd!() .args(&["-sm", "--relative-base=/", "/", "/usr"]) diff --git a/tests/by-util/test_vdir.rs b/tests/by-util/test_vdir.rs index dc5e9f5ec..0781256b9 100644 --- a/tests/by-util/test_vdir.rs +++ b/tests/by-util/test_vdir.rs @@ -29,7 +29,7 @@ fn test_default_output() { scene .ucmd() .succeeds() - .stdout_matches(&Regex::new("[rwx-]{10}.*some-file1$").unwrap()); + .stdout_matches(&Regex::new("[rwx-]{10}.*some-file1\n$").unwrap()); } #[test] diff --git a/tests/common/util.rs b/tests/common/util.rs index f7b62a695..ed4ecf8e9 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -598,7 +598,7 @@ impl CmdResult { /// asserts that /// 1. the command resulted in stderr stream output that equals the - /// passed in value, when both are trimmed of trailing whitespace + /// passed in value /// 2. the command resulted in empty (zero-length) stdout stream output #[track_caller] pub fn stderr_only>(&self, msg: T) -> &Self { @@ -623,7 +623,7 @@ impl CmdResult { /// asserts that /// 1. the command resulted in stderr stream output that equals the - /// the following format when both are trimmed of trailing whitespace + /// the following format /// `"{util_name}: {msg}\nTry '{bin_path} {util_name} --help' for more information."` /// This the expected format when a `UUsageError` is returned or when `show_error!` is called /// `msg` should be the same as the one provided to `UUsageError::new` or `show_error!` @@ -681,7 +681,7 @@ impl CmdResult { #[track_caller] pub fn stdout_matches(&self, regex: ®ex::Regex) -> &Self { assert!( - regex.is_match(self.stdout_str().trim()), + regex.is_match(self.stdout_str()), "Stdout does not match regex:\n{}", self.stdout_str() ); @@ -691,7 +691,7 @@ impl CmdResult { #[track_caller] pub fn stdout_does_not_match(&self, regex: ®ex::Regex) -> &Self { assert!( - !regex.is_match(self.stdout_str().trim()), + !regex.is_match(self.stdout_str()), "Stdout matches regex:\n{}", self.stdout_str() ); From cca54089fbc8c3629025d7841d789bd38f55c079 Mon Sep 17 00:00:00 2001 From: Joining7943 <111500881+Joining7943@users.noreply.github.com> Date: Thu, 16 Feb 2023 19:43:00 +0100 Subject: [PATCH 12/15] tail: Cargo.toml: Remove unneeded features of uucore and the nix dependency --- Cargo.lock | 1 - src/uu/tail/Cargo.toml | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f01c623f9..6d210d303 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3102,7 +3102,6 @@ dependencies = [ "fundu", "libc", "memchr", - "nix", "notify", "same-file", "uucore", diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index 12736f36a..bd16132ea 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -20,7 +20,7 @@ clap = { workspace=true } libc = { workspace=true } memchr = { workspace=true } notify = { workspace=true } -uucore = { workspace=true, features=["ringbuffer", "lines"] } +uucore = { workspace=true } same-file = { workspace=true } atty = { workspace=true } fundu = { workspace=true } @@ -29,9 +29,6 @@ fundu = { workspace=true } windows-sys = { workspace=true, features = ["Win32_System_Threading", "Win32_Foundation"] } winapi-util = { workspace=true } -[target.'cfg(unix)'.dependencies] -nix = { workspace=true, features = ["fs"] } - [[bin]] name = "tail" path = "src/main.rs" From 946c8d2d4a57d0d825a1e7bbee0f9139ff7d5707 Mon Sep 17 00:00:00 2001 From: David Matos Date: Tue, 14 Feb 2023 00:18:17 +0100 Subject: [PATCH 13/15] chmod: change permissions for files present even when there is a missing file --- src/uu/chmod/src/chmod.rs | 15 ++++---- tests/by-util/test_chmod.rs | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index a2dc8e06b..374f22874 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -12,12 +12,12 @@ use std::fs; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::Path; use uucore::display::Quotable; -use uucore::error::{ExitCode, UResult, USimpleError, UUsageError}; +use uucore::error::{set_exit_code, ExitCode, UResult, USimpleError, UUsageError}; use uucore::fs::display_permissions_unix; use uucore::libc::mode_t; #[cfg(not(windows))] use uucore::mode; -use uucore::{format_usage, show_error}; +use uucore::{format_usage, show, show_error}; const ABOUT: &str = "Change the mode of each FILE to MODE.\n\ With --reference, change the mode of each FILE to that of RFILE."; @@ -195,21 +195,24 @@ impl Chmoder { filename.quote() ); if !self.quiet { - return Err(USimpleError::new( + show!(USimpleError::new( 1, format!("cannot operate on dangling symlink {}", filename.quote()), )); } } else if !self.quiet { - return Err(USimpleError::new( + show!(USimpleError::new( 1, format!( "cannot access {}: No such file or directory", filename.quote() - ), + ) )); } - return Err(ExitCode::new(1)); + // GNU exits with exit code 1 even if -q or --quiet are passed + // So we set the exit code, because it hasn't been set yet if `self.quiet` is true. + set_exit_code(1); + continue; } if self.recursive && self.preserve_root && filename == "/" { return Err(USimpleError::new( diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 5dfd1a714..feb756632 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -574,3 +574,71 @@ fn test_mode_after_dash_dash() { ucmd, ); } + +#[test] +fn test_chmod_file_after_non_existing_file() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(TEST_FILE); + at.touch("file2"); + set_permissions(at.plus(TEST_FILE), Permissions::from_mode(0o664)).unwrap(); + set_permissions(at.plus("file2"), Permissions::from_mode(0o664)).unwrap(); + scene + .ucmd() + .arg("u+x") + .arg("does-not-exist") + .arg(TEST_FILE) + .fails() + .stderr_contains("chmod: cannot access 'does-not-exist': No such file or directory") + .code_is(1); + + assert_eq!(at.metadata(TEST_FILE).permissions().mode(), 0o100764); + + scene + .ucmd() + .arg("u+x") + .arg("--q") + .arg("does-not-exist") + .arg("file2") + .fails() + .no_stderr() + .code_is(1); + assert_eq!(at.metadata("file2").permissions().mode(), 0o100764); +} + +#[test] +fn test_chmod_file_symlink_after_non_existing_file() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let existing = "file"; + let test_existing_symlink = "file_symlink"; + + let non_existing = "test_chmod_symlink_non_existing_file"; + let test_dangling_symlink = "test_chmod_symlink_non_existing_file_symlink"; + let expected_stdout = &format!( + "failed to change mode of '{test_dangling_symlink}' from 0000 (---------) to 0000 (---------)" + ); + let expected_stderr = &format!("cannot operate on dangling symlink '{test_dangling_symlink}'"); + + at.touch(existing); + set_permissions(at.plus(existing), Permissions::from_mode(0o664)).unwrap(); + at.symlink_file(non_existing, test_dangling_symlink); + at.symlink_file(existing, test_existing_symlink); + + // this cannot succeed since the symbolic link dangles + // but the metadata for the existing target should change + scene + .ucmd() + .arg("u+x") + .arg("-v") + .arg(test_dangling_symlink) + .arg(test_existing_symlink) + .fails() + .code_is(1) + .stdout_contains(expected_stdout) + .stderr_contains(expected_stderr); + assert_eq!( + at.metadata(test_existing_symlink).permissions().mode(), + 0o100764 + ); +} From b763143db0d5ff2741ed41710d4dad0859267309 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 17 Feb 2023 17:55:33 +0100 Subject: [PATCH 14/15] End the current execution if there is a new changeset in the PR --- .github/workflows/CICD.yml | 5 +++++ .github/workflows/GnuTests.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index b1b8dca54..cf12ee2d7 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -20,6 +20,11 @@ on: [push, pull_request] permissions: contents: read # to fetch code (actions/checkout) +# End the current execution if there is a new changeset in the PR. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + jobs: cargo-deny: name: Style/cargo-deny diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index a462b946a..61da457ed 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -14,6 +14,11 @@ on: [push, pull_request] permissions: contents: read +# End the current execution if there is a new changeset in the PR. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + jobs: gnu: permissions: From 2f64dc9d0353b4899e73cb0ad76f76167d5612b3 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 22 Jan 2023 22:05:53 +0100 Subject: [PATCH 15/15] GNU: Ignore some intermittent We have a list, no need to show them over and over. They are adding noise: https://github.com/orgs/uutils/projects/2 --- .github/workflows/GnuTests.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index a462b946a..342c24af6 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -196,6 +196,9 @@ jobs: REF_LOG_FILE='${{ steps.vars.outputs.path_reference }}/test-logs/test-suite.log' REF_SUMMARY_FILE='${{ steps.vars.outputs.path_reference }}/test-summary/gnu-result.json' REPO_DEFAULT_BRANCH='${{ steps.vars.outputs.repo_default_branch }}' + # https://github.com/uutils/coreutils/issues/4294 + # https://github.com/uutils/coreutils/issues/4295 + IGNORE_INTERMITTENT='tests/tail-2/inotify-dir-recreate tests/misc/timeout tests/rm/rm1' mkdir -p ${{ steps.vars.outputs.path_reference }} @@ -227,10 +230,18 @@ jobs: do if ! grep -Fxq ${LINE}<<<"${REF_FAILING}" then - MSG="GNU test failed: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?" - echo "::error ::$MSG" - echo $MSG >> ${COMMENT_LOG} - have_new_failures="true" + if ! grep ${LINE} ${IGNORE_INTERMITTENT} + then + MSG="GNU test failed: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?" + echo "::error ::$MSG" + echo $MSG >> ${COMMENT_LOG} + have_new_failures="true" + else + MSG="Skip an intermittent issue ${LINE}" + echo "::warning ::$MSG" + echo $MSG >> ${COMMENT_LOG} + echo "" + fi fi done for LINE in ${REF_ERROR}