From 83eac9c0a86bf1e53378e284488553df2875b2ee Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Fri, 21 Jan 2022 21:38:54 -0500 Subject: [PATCH 001/161] head: incorporate "all but last" option into Mode Refactor the `Mode` enum in the `head.rs` module so that it includes not only the mode type---lines or bytes---but also whether to read the first NUM items of that type or all but the last NUM. Before this commit, these two pieces of information were stored separately. This made it difficult to read the code through several function calls and understand at a glance which strategy was being employed. --- src/uu/head/src/head.rs | 144 +++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 84 deletions(-) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index eded419df..9fcdb3faa 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -5,7 +5,7 @@ // spell-checker:ignore (vars) zlines BUFWRITER seekable -use clap::{crate_version, App, AppSettings, Arg}; +use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; use std::convert::{TryFrom, TryInto}; use std::ffi::OsString; use std::io::{self, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write}; @@ -104,25 +104,42 @@ pub fn uu_app<'a>() -> App<'a> { ) .arg(Arg::new(options::FILES_NAME).multiple_occurrences(true)) } -#[derive(PartialEq, Debug, Clone, Copy)] -enum Modes { - Lines(usize), - Bytes(usize), + +#[derive(Debug, PartialEq)] +enum Mode { + FirstLines(usize), + AllButLastLines(usize), + FirstBytes(usize), + AllButLastBytes(usize), } -impl Default for Modes { +impl Default for Mode { fn default() -> Self { - Self::Lines(10) + Self::FirstLines(10) } } -fn parse_mode(src: &str, closure: F) -> Result<(Modes, bool), String> -where - F: FnOnce(usize) -> Modes, -{ - match parse::parse_num(src) { - Ok((n, last)) => Ok((closure(n), last)), - Err(e) => Err(e.to_string()), +impl Mode { + fn from(matches: &ArgMatches) -> Result { + if let Some(v) = matches.value_of(options::BYTES_NAME) { + let (n, all_but_last) = + parse::parse_num(v).map_err(|err| format!("invalid number of bytes: {}", err))?; + if all_but_last { + Ok(Mode::AllButLastBytes(n)) + } else { + Ok(Mode::FirstBytes(n)) + } + } else if let Some(v) = matches.value_of(options::LINES_NAME) { + let (n, all_but_last) = + parse::parse_num(v).map_err(|err| format!("invalid number of lines: {}", err))?; + if all_but_last { + Ok(Mode::AllButLastLines(n)) + } else { + Ok(Mode::FirstLines(n)) + } + } else { + Ok(Default::default()) + } } } @@ -157,8 +174,7 @@ struct HeadOptions { pub quiet: bool, pub verbose: bool, pub zeroed: bool, - pub all_but_last: bool, - pub mode: Modes, + pub mode: Mode, pub files: Vec, } @@ -173,18 +189,7 @@ impl HeadOptions { options.verbose = matches.is_present(options::VERBOSE_NAME); options.zeroed = matches.is_present(options::ZERO_NAME); - let mode_and_from_end = if let Some(v) = matches.value_of(options::BYTES_NAME) { - parse_mode(v, Modes::Bytes) - .map_err(|err| format!("invalid number of bytes: {}", err))? - } else if let Some(v) = matches.value_of(options::LINES_NAME) { - parse_mode(v, Modes::Lines) - .map_err(|err| format!("invalid number of lines: {}", err))? - } else { - (Modes::Lines(10), false) - }; - - options.mode = mode_and_from_end.0; - options.all_but_last = mode_and_from_end.1; + options.mode = Mode::from(&matches)?; options.files = match matches.values_of(options::FILES_NAME) { Some(v) => v.map(|s| s.to_owned()).collect(), @@ -374,9 +379,8 @@ where } fn head_backwards_file(input: &mut std::fs::File, options: &HeadOptions) -> std::io::Result<()> { - assert!(options.all_but_last); match options.mode { - Modes::Bytes(n) => { + Mode::AllButLastBytes(n) => { let size = input.metadata()?.len().try_into().unwrap(); if n >= size { return Ok(()); @@ -387,31 +391,29 @@ fn head_backwards_file(input: &mut std::fs::File, options: &HeadOptions) -> std: )?; } } - Modes::Lines(n) => { + Mode::AllButLastLines(n) => { let found = find_nth_line_from_end(input, n, options.zeroed)?; read_n_bytes( &mut std::io::BufReader::with_capacity(BUF_SIZE, input), found, )?; } + _ => unreachable!(), } Ok(()) } fn head_file(input: &mut std::fs::File, options: &HeadOptions) -> std::io::Result<()> { - if options.all_but_last { - head_backwards_file(input, options) - } else { - match options.mode { - Modes::Bytes(n) => { - read_n_bytes(&mut std::io::BufReader::with_capacity(BUF_SIZE, input), n) - } - Modes::Lines(n) => read_n_lines( - &mut std::io::BufReader::with_capacity(BUF_SIZE, input), - n, - options.zeroed, - ), + match options.mode { + Mode::FirstBytes(n) => { + read_n_bytes(&mut std::io::BufReader::with_capacity(BUF_SIZE, input), n) } + Mode::FirstLines(n) => read_n_lines( + &mut std::io::BufReader::with_capacity(BUF_SIZE, input), + n, + options.zeroed, + ), + Mode::AllButLastBytes(_) | Mode::AllButLastLines(_) => head_backwards_file(input, options), } } @@ -429,19 +431,11 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { let stdin = std::io::stdin(); let mut stdin = stdin.lock(); match options.mode { - Modes::Bytes(n) => { - if options.all_but_last { - read_but_last_n_bytes(&mut stdin, n) - } else { - read_n_bytes(&mut stdin, n) - } - } - Modes::Lines(n) => { - if options.all_but_last { - read_but_last_n_lines(&mut stdin, n, options.zeroed) - } else { - read_n_lines(&mut stdin, n, options.zeroed) - } + Mode::FirstBytes(n) => read_n_bytes(&mut stdin, n), + Mode::AllButLastBytes(n) => read_but_last_n_bytes(&mut stdin, n), + Mode::FirstLines(n) => read_n_lines(&mut stdin, n, options.zeroed), + Mode::AllButLastLines(n) => { + read_but_last_n_lines(&mut stdin, n, options.zeroed) } } } @@ -512,17 +506,16 @@ mod tests { let args = options("-n -10M -vz").unwrap(); assert!(args.zeroed); assert!(args.verbose); - assert!(args.all_but_last); - assert_eq!(args.mode, Modes::Lines(10 * 1024 * 1024)); + assert_eq!(args.mode, Mode::AllButLastLines(10 * 1024 * 1024)); } #[test] fn test_gnu_compatibility() { let args = options("-n 1 -c 1 -n 5 -c kiB -vqvqv").unwrap(); // spell-checker:disable-line - assert!(args.mode == Modes::Bytes(1024)); + assert!(args.mode == Mode::FirstBytes(1024)); assert!(args.verbose); - assert_eq!(options("-5").unwrap().mode, Modes::Lines(5)); - assert_eq!(options("-2b").unwrap().mode, Modes::Bytes(1024)); - assert_eq!(options("-5 -c 1").unwrap().mode, Modes::Bytes(1)); + assert_eq!(options("-5").unwrap().mode, Mode::FirstLines(5)); + assert_eq!(options("-2b").unwrap().mode, Mode::FirstBytes(1024)); + assert_eq!(options("-5 -c 1").unwrap().mode, Mode::FirstBytes(1)); } #[test] fn all_args_test() { @@ -533,10 +526,10 @@ mod tests { assert!(options("-v").unwrap().verbose); assert!(options("--zero-terminated").unwrap().zeroed); assert!(options("-z").unwrap().zeroed); - assert_eq!(options("--lines 15").unwrap().mode, Modes::Lines(15)); - assert_eq!(options("-n 15").unwrap().mode, Modes::Lines(15)); - assert_eq!(options("--bytes 15").unwrap().mode, Modes::Bytes(15)); - assert_eq!(options("-c 15").unwrap().mode, Modes::Bytes(15)); + assert_eq!(options("--lines 15").unwrap().mode, Mode::FirstLines(15)); + assert_eq!(options("-n 15").unwrap().mode, Mode::FirstLines(15)); + assert_eq!(options("--bytes 15").unwrap().mode, Mode::FirstBytes(15)); + assert_eq!(options("-c 15").unwrap().mode, Mode::FirstBytes(15)); } #[test] fn test_options_errors() { @@ -550,26 +543,9 @@ mod tests { assert!(!opts.verbose); assert!(!opts.quiet); assert!(!opts.zeroed); - assert!(!opts.all_but_last); - assert_eq!(opts.mode, Modes::Lines(10)); + assert_eq!(opts.mode, Mode::FirstLines(10)); assert!(opts.files.is_empty()); } - #[test] - fn test_parse_mode() { - assert_eq!( - parse_mode("123", Modes::Lines), - Ok((Modes::Lines(123), false)) - ); - assert_eq!( - parse_mode("-456", Modes::Bytes), - Ok((Modes::Bytes(456), true)) - ); - assert!(parse_mode("Nonsensical Nonsense", Modes::Bytes).is_err()); - #[cfg(target_pointer_width = "64")] - assert!(parse_mode("1Y", Modes::Lines).is_err()); - #[cfg(target_pointer_width = "32")] - assert!(parse_mode("1T", Modes::Bytes).is_err()); - } fn arg_outputs(src: &str) -> Result { let split = src.split_whitespace().map(OsString::from); match arg_iterate(split) { From fe5b537f567774f1cb43d2ca51bb141090233faf Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Fri, 28 Jan 2022 22:24:07 -0500 Subject: [PATCH 002/161] truncate: error when trying to truncate a fifo Terminate the `truncate` program with an error message when trying to truncate a named pipe (also known as a fifo). --- src/uu/truncate/src/truncate.rs | 43 ++++++++++++++++++++++++++++++++- tests/by-util/test_truncate.rs | 35 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 685363f8f..5072502db 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -10,6 +10,8 @@ use clap::{crate_version, App, AppSettings, Arg}; use std::convert::TryFrom; use std::fs::{metadata, OpenOptions}; use std::io::ErrorKind; +#[cfg(unix)] +use std::os::unix::fs::FileTypeExt; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; @@ -207,6 +209,8 @@ fn file_truncate(filename: &str, create: bool, size: usize) -> std::io::Result<( /// /// If the any file could not be opened, or there was a problem setting /// the size of at least one file. +/// +/// If at least one file is a named pipe (also known as a fifo). fn truncate_reference_and_size( rfilename: &str, size_string: &str, @@ -239,6 +243,17 @@ fn truncate_reference_and_size( let fsize = metadata.len() as usize; let tsize = mode.to_size(fsize); for filename in filenames { + #[cfg(unix)] + if std::fs::metadata(filename)?.file_type().is_fifo() { + return Err(USimpleError::new( + 1, + format!( + "cannot open {} for writing: No such device or address", + filename.quote() + ), + )); + } + file_truncate(filename, create, tsize) .map_err_context(|| format!("cannot open {} for writing", filename.quote()))?; } @@ -256,6 +271,8 @@ fn truncate_reference_and_size( /// /// If the any file could not be opened, or there was a problem setting /// the size of at least one file. +/// +/// If at least one file is a named pipe (also known as a fifo). fn truncate_reference_file_only( rfilename: &str, filenames: &[String], @@ -273,6 +290,16 @@ fn truncate_reference_file_only( })?; let tsize = metadata.len() as usize; for filename in filenames { + #[cfg(unix)] + if std::fs::metadata(filename)?.file_type().is_fifo() { + return Err(USimpleError::new( + 1, + format!( + "cannot open {} for writing: No such device or address", + filename.quote() + ), + )); + } file_truncate(filename, create, tsize) .map_err_context(|| format!("cannot open {} for writing", filename.quote()))?; } @@ -294,6 +321,8 @@ fn truncate_reference_file_only( /// /// If the any file could not be opened, or there was a problem setting /// the size of at least one file. +/// +/// If at least one file is a named pipe (also known as a fifo). fn truncate_size_only(size_string: &str, filenames: &[String], create: bool) -> UResult<()> { let mode = parse_mode_and_size(size_string) .map_err(|e| USimpleError::new(1, format!("Invalid number: {}", e)))?; @@ -302,7 +331,19 @@ fn truncate_size_only(size_string: &str, filenames: &[String], create: bool) -> } for filename in filenames { let fsize = match metadata(filename) { - Ok(m) => m.len(), + Ok(m) => { + #[cfg(unix)] + if m.file_type().is_fifo() { + return Err(USimpleError::new( + 1, + format!( + "cannot open {} for writing: No such device or address", + filename.quote() + ), + )); + } + m.len() + } Err(_) => 0, }; let tsize = mode.to_size(fsize as usize); diff --git a/tests/by-util/test_truncate.rs b/tests/by-util/test_truncate.rs index 0ef65ec16..716c77ab0 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -398,3 +398,38 @@ fn test_underflow_relative_size() { assert!(at.file_exists(FILE1)); assert!(at.read_bytes(FILE1).is_empty()); } + +#[cfg(not(windows))] +#[test] +fn test_fifo_error_size_only() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkfifo("fifo"); + ucmd.args(&["-s", "0", "fifo"]) + .fails() + .no_stdout() + .stderr_contains("cannot open 'fifo' for writing: No such device or address"); +} + +#[cfg(not(windows))] +#[test] +fn test_fifo_error_reference_file_only() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkfifo("fifo"); + at.make_file("reference_file"); + ucmd.args(&["-r", "reference_file", "fifo"]) + .fails() + .no_stdout() + .stderr_contains("cannot open 'fifo' for writing: No such device or address"); +} + +#[cfg(not(windows))] +#[test] +fn test_fifo_error_reference_and_size() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkfifo("fifo"); + at.make_file("reference_file"); + ucmd.args(&["-r", "reference_file", "-s", "+0", "fifo"]) + .fails() + .no_stdout() + .stderr_contains("cannot open 'fifo' for writing: No such device or address"); +} From 371278e043389d80afa48ac5dd7c6bc0d6cb8aeb Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sat, 29 Jan 2022 12:20:11 -0500 Subject: [PATCH 003/161] truncate: fix typo in docs: "the any" -> "any" --- src/uu/truncate/src/truncate.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 5072502db..fdcef0706 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -207,7 +207,7 @@ fn file_truncate(filename: &str, create: bool, size: usize) -> std::io::Result<( /// /// # Errors /// -/// If the any file could not be opened, or there was a problem setting +/// If any file could not be opened, or there was a problem setting /// the size of at least one file. /// /// If at least one file is a named pipe (also known as a fifo). @@ -269,7 +269,7 @@ fn truncate_reference_and_size( /// /// # Errors /// -/// If the any file could not be opened, or there was a problem setting +/// If any file could not be opened, or there was a problem setting /// the size of at least one file. /// /// If at least one file is a named pipe (also known as a fifo). @@ -319,7 +319,7 @@ fn truncate_reference_file_only( /// /// # Errors /// -/// If the any file could not be opened, or there was a problem setting +/// If any file could not be opened, or there was a problem setting /// the size of at least one file. /// /// If at least one file is a named pipe (also known as a fifo). From cba0696b90091d9efc115ca2cf1c6b67db94befd Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sat, 29 Jan 2022 22:15:23 -0500 Subject: [PATCH 004/161] head: don't add trailing newline to end of file Prevent `head` from adding a trailing newline to the end of a file that did not originally have one when using `head --lines=-0`. --- src/uu/head/src/head.rs | 5 ++- src/uu/head/src/lines.rs | 80 +++++++++++++++++++++++++++++++++++++- tests/by-util/test_head.rs | 8 +++- 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index eded419df..959d87604 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -38,6 +38,7 @@ mod options { mod lines; mod parse; mod take; +use lines::lines; use lines::zlines; use take::take_all_but; use take::take_lines; @@ -285,8 +286,8 @@ fn read_but_last_n_lines( stdout.write_all(&bytes?)?; } } else { - for line in take_all_but(input.lines(), n) { - println!("{}", line?); + for line in take_all_but(lines(input), n) { + print!("{}", line?); } } Ok(()) diff --git a/src/uu/head/src/lines.rs b/src/uu/head/src/lines.rs index 474f5717d..5c1b23b27 100644 --- a/src/uu/head/src/lines.rs +++ b/src/uu/head/src/lines.rs @@ -1,11 +1,75 @@ +// * 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. // spell-checker:ignore (vars) zline zlines - -//! Iterate over zero-terminated lines. +//! Iterate over lines, including the line ending character(s). +//! +//! This module provides the [`lines`] and [`zlines`] functions, +//! similar to the [`BufRead::lines`] method. While the +//! [`BufRead::lines`] method yields [`String`] instances that do not +//! include the line ending characters (`"\n"` or `"\r\n"`), our +//! functions yield [`String`] instances that include the line ending +//! characters. This is useful if the input data does not end with a +//! newline character and you want to preserve the exact form of the +//! input data. use std::io::BufRead; /// The zero byte, representing the null character. const ZERO: u8 = 0; +/// Returns an iterator over the lines, including line ending characters. +/// +/// This function is just like [`BufRead::lines`], but it includes the +/// line ending characters in each yielded [`String`] if the input +/// data has them. +/// +/// # Examples +/// +/// If the input data does not end with a newline character (`'\n'`), +/// then the last [`String`] yielded by this iterator also does not +/// end with a newline: +/// +/// ```rust,ignore +/// use std::io::BufRead; +/// use std::io::Cursor; +/// +/// let cursor = Cursor::new(b"x\ny\nz"); +/// let mut it = cursor.lines(); +/// +/// assert_eq!(it.next(), Some(String::from("x\n"))); +/// assert_eq!(it.next(), Some(String::from("y\n"))); +/// assert_eq!(it.next(), Some(String::from("z"))); +/// assert_eq!(it.next(), None); +/// ``` +pub(crate) fn lines(reader: B) -> Lines +where + B: BufRead, +{ + Lines { buf: reader } +} + +/// An iterator over the lines of an instance of `BufRead`. +/// +/// This struct is generally created by calling [`lines`] on a `BufRead`. +/// Please see the documentation of [`lines`] for more details. +pub(crate) struct Lines { + buf: B, +} + +impl Iterator for Lines { + type Item = std::io::Result; + + fn next(&mut self) -> Option> { + let mut buf = String::new(); + match self.buf.read_line(&mut buf) { + Ok(0) => None, + Ok(_n) => Some(Ok(buf)), + Err(e) => Some(Err(e)), + } + } +} + /// Returns an iterator over the lines of the given reader. /// /// The iterator returned from this function will yield instances of @@ -50,6 +114,7 @@ impl Iterator for ZLines { #[cfg(test)] mod tests { + use crate::lines::lines; use crate::lines::zlines; use std::io::Cursor; @@ -72,4 +137,15 @@ mod tests { assert_eq!(iter.next(), Some(b"z".to_vec())); assert_eq!(iter.next(), None); } + + #[test] + fn test_lines() { + let cursor = Cursor::new(b"x\ny\nz"); + let mut it = lines(cursor).map(|l| l.unwrap()); + + assert_eq!(it.next(), Some(String::from("x\n"))); + assert_eq!(it.next(), Some(String::from("y\n"))); + assert_eq!(it.next(), Some(String::from("z"))); + assert_eq!(it.next(), None); + } } diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 246f5b62a..8f4932edf 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -157,11 +157,17 @@ fn test_negative_byte_syntax() { #[test] fn test_negative_zero_lines() { new_ucmd!() - .args(&["--lines=-0"]) + .arg("--lines=-0") .pipe_in("a\nb\n") .succeeds() .stdout_is("a\nb\n"); + new_ucmd!() + .arg("--lines=-0") + .pipe_in("a\nb") + .succeeds() + .stdout_is("a\nb"); } + #[test] fn test_negative_zero_bytes() { new_ucmd!() From b9c2066ee9a6c517aceb7a089c5a4e83a84f8dc5 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sat, 29 Jan 2022 22:24:27 -0500 Subject: [PATCH 005/161] uucore: move lines.rs to be a uucore feature Refactor the `lines.rs` module to be a feature in `uucore`. It was common to both `head` and `tail`. --- src/uu/head/Cargo.toml | 2 +- src/uu/head/src/head.rs | 12 +- src/uu/head/src/lines.rs | 151 ------------------ src/uu/tail/Cargo.toml | 2 +- src/uu/tail/src/tail.rs | 3 +- src/uucore/Cargo.toml | 1 + src/uucore/src/lib/features.rs | 2 + .../src => uucore/src/lib/features}/lines.rs | 14 +- src/uucore/src/lib/lib.rs | 2 + 9 files changed, 22 insertions(+), 167 deletions(-) delete mode 100644 src/uu/head/src/lines.rs rename src/{uu/tail/src => uucore/src/lib/features}/lines.rs (89%) diff --git a/src/uu/head/Cargo.toml b/src/uu/head/Cargo.toml index 5d05f1921..04a512492 100644 --- a/src/uu/head/Cargo.toml +++ b/src/uu/head/Cargo.toml @@ -17,7 +17,7 @@ path = "src/head.rs" [dependencies] clap = { version = "3.0", features = ["wrap_help", "cargo"] } memchr = "2" -uucore = { version=">=0.0.11", package="uucore", path="../../uucore", features=["ringbuffer"] } +uucore = { version=">=0.0.11", package="uucore", path="../../uucore", features=["ringbuffer", "lines"] } [[bin]] name = "head" diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 959d87604..e78dec78b 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -11,6 +11,7 @@ use std::ffi::OsString; use std::io::{self, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write}; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; +use uucore::lines::lines; use uucore::show; const BUF_SIZE: usize = 65536; @@ -35,11 +36,8 @@ mod options { pub const ZERO_NAME: &str = "ZERO"; pub const FILES_NAME: &str = "FILE"; } -mod lines; mod parse; mod take; -use lines::lines; -use lines::zlines; use take::take_all_but; use take::take_lines; @@ -282,12 +280,14 @@ fn read_but_last_n_lines( if zero { let stdout = std::io::stdout(); let mut stdout = stdout.lock(); - for bytes in take_all_but(zlines(input), n) { + for bytes in take_all_but(lines(input, b'\0'), n) { stdout.write_all(&bytes?)?; } } else { - for line in take_all_but(lines(input), n) { - print!("{}", line?); + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + for bytes in take_all_but(lines(input, b'\n'), n) { + stdout.write_all(&bytes?)?; } } Ok(()) diff --git a/src/uu/head/src/lines.rs b/src/uu/head/src/lines.rs deleted file mode 100644 index 5c1b23b27..000000000 --- a/src/uu/head/src/lines.rs +++ /dev/null @@ -1,151 +0,0 @@ -// * 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. -// spell-checker:ignore (vars) zline zlines -//! Iterate over lines, including the line ending character(s). -//! -//! This module provides the [`lines`] and [`zlines`] functions, -//! similar to the [`BufRead::lines`] method. While the -//! [`BufRead::lines`] method yields [`String`] instances that do not -//! include the line ending characters (`"\n"` or `"\r\n"`), our -//! functions yield [`String`] instances that include the line ending -//! characters. This is useful if the input data does not end with a -//! newline character and you want to preserve the exact form of the -//! input data. -use std::io::BufRead; - -/// The zero byte, representing the null character. -const ZERO: u8 = 0; - -/// Returns an iterator over the lines, including line ending characters. -/// -/// This function is just like [`BufRead::lines`], but it includes the -/// line ending characters in each yielded [`String`] if the input -/// data has them. -/// -/// # Examples -/// -/// If the input data does not end with a newline character (`'\n'`), -/// then the last [`String`] yielded by this iterator also does not -/// end with a newline: -/// -/// ```rust,ignore -/// use std::io::BufRead; -/// use std::io::Cursor; -/// -/// let cursor = Cursor::new(b"x\ny\nz"); -/// let mut it = cursor.lines(); -/// -/// assert_eq!(it.next(), Some(String::from("x\n"))); -/// assert_eq!(it.next(), Some(String::from("y\n"))); -/// assert_eq!(it.next(), Some(String::from("z"))); -/// assert_eq!(it.next(), None); -/// ``` -pub(crate) fn lines(reader: B) -> Lines -where - B: BufRead, -{ - Lines { buf: reader } -} - -/// An iterator over the lines of an instance of `BufRead`. -/// -/// This struct is generally created by calling [`lines`] on a `BufRead`. -/// Please see the documentation of [`lines`] for more details. -pub(crate) struct Lines { - buf: B, -} - -impl Iterator for Lines { - type Item = std::io::Result; - - fn next(&mut self) -> Option> { - let mut buf = String::new(); - match self.buf.read_line(&mut buf) { - Ok(0) => None, - Ok(_n) => Some(Ok(buf)), - Err(e) => Some(Err(e)), - } - } -} - -/// Returns an iterator over the lines of the given reader. -/// -/// The iterator returned from this function will yield instances of -/// [`std::io::Result`]<[`Vec`]<[`u8`]>>, representing the bytes of the line -/// *including* the null character (with the possible exception of the -/// last line, which may not have one). -/// -/// # Examples -/// -/// ```rust,ignore -/// use std::io::Cursor; -/// -/// let cursor = Cursor::new(b"x\0y\0z\0"); -/// let mut iter = zlines(cursor).map(|l| l.unwrap()); -/// assert_eq!(iter.next(), Some(b"x\0".to_vec())); -/// assert_eq!(iter.next(), Some(b"y\0".to_vec())); -/// assert_eq!(iter.next(), Some(b"z\0".to_vec())); -/// assert_eq!(iter.next(), None); -/// ``` -pub fn zlines(buf: B) -> ZLines { - ZLines { buf } -} - -/// An iterator over the zero-terminated lines of an instance of `BufRead`. -pub struct ZLines { - buf: B, -} - -impl Iterator for ZLines { - type Item = std::io::Result>; - - fn next(&mut self) -> Option>> { - let mut buf = Vec::new(); - match self.buf.read_until(ZERO, &mut buf) { - Ok(0) => None, - Ok(_) => Some(Ok(buf)), - Err(e) => Some(Err(e)), - } - } -} - -#[cfg(test)] -mod tests { - - use crate::lines::lines; - use crate::lines::zlines; - use std::io::Cursor; - - #[test] - fn test_null_terminated() { - let cursor = Cursor::new(b"x\0y\0z\0"); - let mut iter = zlines(cursor).map(|l| l.unwrap()); - assert_eq!(iter.next(), Some(b"x\0".to_vec())); - assert_eq!(iter.next(), Some(b"y\0".to_vec())); - assert_eq!(iter.next(), Some(b"z\0".to_vec())); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_not_null_terminated() { - let cursor = Cursor::new(b"x\0y\0z"); - let mut iter = zlines(cursor).map(|l| l.unwrap()); - assert_eq!(iter.next(), Some(b"x\0".to_vec())); - assert_eq!(iter.next(), Some(b"y\0".to_vec())); - assert_eq!(iter.next(), Some(b"z".to_vec())); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_lines() { - let cursor = Cursor::new(b"x\ny\nz"); - let mut it = lines(cursor).map(|l| l.unwrap()); - - assert_eq!(it.next(), Some(String::from("x\n"))); - assert_eq!(it.next(), Some(String::from("y\n"))); - assert_eq!(it.next(), Some(String::from("z"))); - assert_eq!(it.next(), None); - } -} diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index d70502dab..4f40431b1 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -17,7 +17,7 @@ path = "src/tail.rs" [dependencies] clap = { version = "3.0", features = ["wrap_help", "cargo"] } libc = "0.2.42" -uucore = { version=">=0.0.11", package="uucore", path="../../uucore", features=["ringbuffer"] } +uucore = { version=">=0.0.11", package="uucore", path="../../uucore", features=["ringbuffer", "lines"] } [target.'cfg(windows)'.dependencies] winapi = { version="0.3", features=["fileapi", "handleapi", "processthreadsapi", "synchapi", "winbase"] } diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 951399866..2c9a248f0 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -16,11 +16,9 @@ extern crate clap; extern crate uucore; mod chunks; -mod lines; mod parse; mod platform; use chunks::ReverseChunks; -use lines::lines; use clap::{App, AppSettings, Arg}; use std::collections::VecDeque; @@ -33,6 +31,7 @@ use std::thread::sleep; use std::time::Duration; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::lines::lines; use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::ringbuffer::RingBuffer; diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 3a6bf25c1..5bd5994cc 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -55,6 +55,7 @@ encoding = ["data-encoding", "data-encoding-macro", "z85", "thiserror"] entries = ["libc"] fs = ["libc", "nix", "winapi-util"] fsext = ["libc", "time"] +lines = [] memo = ["itertools"] mode = ["libc"] perms = ["libc", "walkdir"] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index 999d8af6c..b1b87a613 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -6,6 +6,8 @@ pub mod encoding; pub mod fs; #[cfg(feature = "fsext")] pub mod fsext; +#[cfg(feature = "lines")] +pub mod lines; #[cfg(feature = "memo")] pub mod memo; #[cfg(feature = "ringbuffer")] diff --git a/src/uu/tail/src/lines.rs b/src/uucore/src/lib/features/lines.rs similarity index 89% rename from src/uu/tail/src/lines.rs rename to src/uucore/src/lib/features/lines.rs index ee8b36662..a7f4df76d 100644 --- a/src/uu/tail/src/lines.rs +++ b/src/uucore/src/lib/features/lines.rs @@ -2,15 +2,17 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. +// spell-checker:ignore (vars) //! Iterate over lines, including the line ending character(s). //! //! This module provides the [`lines`] function, similar to the //! [`BufRead::lines`] method. While the [`BufRead::lines`] method //! yields [`String`] instances that do not include the line ending -//! characters (`"\n"` or `"\r\n"`), our function yields [`String`] -//! instances that include the line ending characters. This is useful -//! if the input data does not end with a newline character and you -//! want to preserve the exact form of the input data. +//! characters (`"\n"` or `"\r\n"`), our functions yield +//! [`Vec`]<['u8']> instances that include the line ending +//! characters. This is useful if the input data does not end with a +//! newline character and you want to preserve the exact form of the +//! input data. use std::io::BufRead; /// Returns an iterator over the lines, including line ending characters. @@ -51,7 +53,7 @@ use std::io::BufRead; /// assert_eq!(it.next(), Some(Vec::from("z"))); /// assert_eq!(it.next(), None); /// ``` -pub(crate) fn lines(reader: B, sep: u8) -> Lines +pub fn lines(reader: B, sep: u8) -> Lines where B: BufRead, { @@ -62,7 +64,7 @@ where /// /// This struct is generally created by calling [`lines`] on a `BufRead`. /// Please see the documentation of [`lines`] for more details. -pub(crate) struct Lines { +pub struct Lines { buf: B, sep: u8, } diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index ae7788e05..4dc5e6987 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -38,6 +38,8 @@ pub use crate::features::encoding; pub use crate::features::fs; #[cfg(feature = "fsext")] pub use crate::features::fsext; +#[cfg(feature = "lines")] +pub use crate::features::lines; #[cfg(feature = "memo")] pub use crate::features::memo; #[cfg(feature = "ringbuffer")] From fa44957a63e367271ef5455d025b08036a34b3b6 Mon Sep 17 00:00:00 2001 From: Zachary Dremann Date: Sun, 30 Jan 2022 22:03:17 -0500 Subject: [PATCH 006/161] paste: Handle unicode delimiters --- src/uu/paste/src/paste.rs | 10 ++++++---- tests/by-util/test_paste.rs | 12 ++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 0792da458..5208f1b5a 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -10,7 +10,6 @@ use clap::{crate_version, App, AppSettings, Arg}; use std::fs::File; use std::io::{stdin, BufRead, BufReader, Read}; -use std::iter::repeat; use std::path::Path; use uucore::error::{FromIo, UResult}; @@ -110,10 +109,11 @@ fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> } delim_count += 1; } - println!("{}", &output[..output.len() - 1]); + output.pop(); + println!("{}", output); } } else { - let mut eof: Vec = repeat(false).take(files.len()).collect(); + let mut eof = vec![false; files.len()]; loop { let mut output = String::new(); let mut eof_count = 0; @@ -137,7 +137,9 @@ fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> if files.len() == eof_count { break; } - println!("{}", &output[..output.len() - 1]); + // Remove final delimiter + output.pop(); + println!("{}", output); delim_count = 0; } } diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index 1afe84be8..5363e6962 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -60,6 +60,18 @@ static EXAMPLE_DATA: &[TestData] = &[ ins: &["1\na\n", "2\nb\n"], out: "1 2\na b\n", }, + TestData { + name: "multibyte-delim", + args: &["-d", "πŸ’£"], + ins: &["1\na\n", "2\nb\n"], + out: "1πŸ’£2\naπŸ’£b\n", + }, + TestData { + name: "multibyte-delim-serial", + args: &["-d", "πŸ’£", "-s"], + ins: &["1\na\n", "2\nb\n"], + out: "1πŸ’£a\n2πŸ’£b\n", + }, ]; #[test] From c6ec4f8f170e1d29ae0580db9111c10e23edd999 Mon Sep 17 00:00:00 2001 From: Zachary Dremann Date: Sun, 30 Jan 2022 22:06:33 -0500 Subject: [PATCH 007/161] paste: Store delimiters as chars, rather than strings --- src/uu/paste/src/paste.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 5208f1b5a..c43a86b75 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -88,10 +88,7 @@ fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> files.push(file); } - let delimiters: Vec = unescape(delimiters) - .chars() - .map(|x| x.to_string()) - .collect(); + let delimiters: Vec = unescape(delimiters).chars().collect(); let mut delim_count = 0; if serial { @@ -103,7 +100,7 @@ fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> Ok(0) => break, Ok(_) => { output.push_str(line.trim_end()); - output.push_str(&delimiters[delim_count % delimiters.len()]); + output.push(delimiters[delim_count % delimiters.len()]); } Err(e) => return Err(e.map_err_context(String::new)), } @@ -131,7 +128,7 @@ fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> Err(e) => return Err(e.map_err_context(String::new)), } } - output.push_str(&delimiters[delim_count % delimiters.len()]); + output.push(delimiters[delim_count % delimiters.len()]); delim_count += 1; } if files.len() == eof_count { From 8905d52279c4ac1a737767a815d09c179e0798cd Mon Sep 17 00:00:00 2001 From: Zachary Dremann Date: Sun, 30 Jan 2022 22:26:13 -0500 Subject: [PATCH 008/161] paste: Write to a locked stdout --- src/uu/paste/src/paste.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index c43a86b75..dbc0bde76 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -9,7 +9,7 @@ use clap::{crate_version, App, AppSettings, Arg}; use std::fs::File; -use std::io::{stdin, BufRead, BufReader, Read}; +use std::io::{stdin, BufRead, BufReader, Read, stdout, Write}; use std::path::Path; use uucore::error::{FromIo, UResult}; @@ -90,12 +90,15 @@ fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> let delimiters: Vec = unescape(delimiters).chars().collect(); let mut delim_count = 0; + let stdout = stdout(); + let mut stdout = stdout.lock(); + let mut line = String::new(); if serial { for file in &mut files { let mut output = String::new(); loop { - let mut line = String::new(); + line.clear(); match read_line(file.as_mut(), &mut line) { Ok(0) => break, Ok(_) => { @@ -107,7 +110,7 @@ fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> delim_count += 1; } output.pop(); - println!("{}", output); + writeln!(stdout, "{}", output)?; } } else { let mut eof = vec![false; files.len()]; @@ -118,7 +121,7 @@ fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> if eof[i] { eof_count += 1; } else { - let mut line = String::new(); + line.clear(); match read_line(file.as_mut(), &mut line) { Ok(0) => { eof[i] = true; @@ -136,7 +139,7 @@ fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> } // Remove final delimiter output.pop(); - println!("{}", output); + writeln!(stdout, "{}", output)?; delim_count = 0; } } From ff14f25c34d4d22f3f6d031ab88b94a6a9115a0f Mon Sep 17 00:00:00 2001 From: Zachary Dremann Date: Sun, 30 Jan 2022 22:27:22 -0500 Subject: [PATCH 009/161] paste: Reuse `output` allocation --- src/uu/paste/src/paste.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index dbc0bde76..db5fa5b8a 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -94,9 +94,10 @@ fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> let mut stdout = stdout.lock(); let mut line = String::new(); + let mut output = String::new(); if serial { for file in &mut files { - let mut output = String::new(); + output.clear(); loop { line.clear(); match read_line(file.as_mut(), &mut line) { @@ -115,7 +116,7 @@ fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> } else { let mut eof = vec![false; files.len()]; loop { - let mut output = String::new(); + output.clear(); let mut eof_count = 0; for (i, file) in files.iter_mut().enumerate() { if eof[i] { From 85a81d328a9e2c40c0c2682a11071c1634a1a204 Mon Sep 17 00:00:00 2001 From: Zachary Dremann Date: Sun, 30 Jan 2022 22:28:05 -0500 Subject: [PATCH 010/161] paste: Create vec with capacity --- src/uu/paste/src/paste.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index db5fa5b8a..6051fbeed 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -9,7 +9,7 @@ use clap::{crate_version, App, AppSettings, Arg}; use std::fs::File; -use std::io::{stdin, BufRead, BufReader, Read, stdout, Write}; +use std::io::{stdin, stdout, BufRead, BufReader, Read, Write}; use std::path::Path; use uucore::error::{FromIo, UResult}; @@ -76,7 +76,7 @@ pub fn uu_app<'a>() -> App<'a> { } fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> { - let mut files = vec![]; + let mut files = Vec::with_capacity(filenames.len()); for name in filenames { let file = if name == "-" { None From ad4c5d3357b93c810321b1216454cf061c2c3adc Mon Sep 17 00:00:00 2001 From: Zachary Dremann Date: Sun, 30 Jan 2022 22:45:50 -0500 Subject: [PATCH 011/161] paste: Use a single buffer --- src/uu/paste/src/paste.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 6051fbeed..dc93ae625 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -93,17 +93,17 @@ fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> let stdout = stdout(); let mut stdout = stdout.lock(); - let mut line = String::new(); let mut output = String::new(); if serial { for file in &mut files { output.clear(); loop { - line.clear(); - match read_line(file.as_mut(), &mut line) { + match read_line(file.as_mut(), &mut output) { Ok(0) => break, Ok(_) => { - output.push_str(line.trim_end()); + if output.ends_with('\n') { + output.pop(); + } output.push(delimiters[delim_count % delimiters.len()]); } Err(e) => return Err(e.map_err_context(String::new)), @@ -122,13 +122,16 @@ fn paste(filenames: Vec, serial: bool, delimiters: &str) -> UResult<()> if eof[i] { eof_count += 1; } else { - line.clear(); - match read_line(file.as_mut(), &mut line) { + match read_line(file.as_mut(), &mut output) { Ok(0) => { eof[i] = true; eof_count += 1; } - Ok(_) => output.push_str(line.trim_end()), + Ok(_) => { + if output.ends_with('\n') { + output.pop(); + } + } Err(e) => return Err(e.map_err_context(String::new)), } } From f75466bb31f9ac7932582ee821100bf9debc4666 Mon Sep 17 00:00:00 2001 From: Zachary Dremann Date: Sun, 30 Jan 2022 22:49:18 -0500 Subject: [PATCH 012/161] tests/paste: Add test to avoid trimming extra whitespace --- tests/by-util/test_paste.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index 5363e6962..09401ec59 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -72,6 +72,12 @@ static EXAMPLE_DATA: &[TestData] = &[ ins: &["1\na\n", "2\nb\n"], out: "1πŸ’£a\n2πŸ’£b\n", }, + TestData { + name: "trailing whitespace", + args: &["-d", "|"], + ins: &["1 \na \n", "2\t\nb\t\n"], + out: "1 |2\t\na |b\t\n", + }, ]; #[test] From 184b65df206efce4a2f0289814b4a0f34ac83c92 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Mon, 31 Jan 2022 12:10:57 +0100 Subject: [PATCH 013/161] uucore: allow backup suffix with hyphen value --- src/uucore/src/lib/mods/backup_control.rs | 10 +++++++++ tests/by-util/test_cp.rs | 18 +++++++++++++++ tests/by-util/test_install.rs | 25 +++++++++++++++++++++ tests/by-util/test_ln.rs | 27 +++++++++++++++++++++++ tests/by-util/test_mv.rs | 21 ++++++++++++++++++ 5 files changed, 101 insertions(+) diff --git a/src/uucore/src/lib/mods/backup_control.rs b/src/uucore/src/lib/mods/backup_control.rs index e14716591..a2753b964 100644 --- a/src/uucore/src/lib/mods/backup_control.rs +++ b/src/uucore/src/lib/mods/backup_control.rs @@ -231,6 +231,7 @@ pub mod arguments { .help("override the usual backup suffix") .takes_value(true) .value_name("SUFFIX") + .allow_hyphen_values(true) } } @@ -618,4 +619,13 @@ mod tests { assert_eq!(result, BackupMode::SimpleBackup); env::remove_var(ENV_VERSION_CONTROL); } + + #[test] + fn test_suffix_takes_hyphen_value() { + let _dummy = TEST_MUTEX.lock().unwrap(); + let matches = make_app().get_matches_from(vec!["app", "-b", "--suffix", "-v"]); + + let result = determine_backup_suffix(&matches); + assert_eq!(result, "-v"); + } } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 92637dfbe..e9b149ede 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -385,6 +385,24 @@ fn test_cp_arg_suffix() { ); } +#[test] +fn test_cp_arg_suffix_hyphen_value() { + let (at, mut ucmd) = at_and_ucmd!(); + + ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg("-b") + .arg("--suffix") + .arg("-v") + .arg(TEST_HOW_ARE_YOU_SOURCE) + .succeeds(); + + assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); + assert_eq!( + at.read(&*format!("{}-v", TEST_HOW_ARE_YOU_SOURCE)), + "How are you?\n" + ); +} + #[test] fn test_cp_custom_backup_suffix_via_env() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index 97169f934..23bebf224 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -815,6 +815,31 @@ fn test_install_backup_short_custom_suffix() { assert!(at.file_exists(&format!("{}{}", file_b, suffix))); } +#[test] +fn test_install_backup_short_custom_suffix_hyphen_value() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_backup_custom_suffix_file_a"; + let file_b = "test_install_backup_custom_suffix_file_b"; + let suffix = "-v"; + + at.touch(file_a); + at.touch(file_b); + scene + .ucmd() + .arg("-b") + .arg(format!("--suffix={}", suffix)) + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}{}", file_b, suffix))); +} + #[test] fn test_install_backup_custom_suffix_via_env() { let scene = TestScenario::new(util_name!()); diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index 9fa73c0bc..a2a31464f 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -180,6 +180,33 @@ fn test_symlink_custom_backup_suffix() { assert_eq!(at.resolve_link(backup), file); } +#[test] +fn test_symlink_custom_backup_suffix_hyphen_value() { + let (at, mut ucmd) = at_and_ucmd!(); + let file = "test_symlink_custom_backup_suffix"; + let link = "test_symlink_custom_backup_suffix_link"; + let suffix = "-v"; + + at.touch(file); + at.symlink_file(file, link); + assert!(at.file_exists(file)); + assert!(at.is_symlink(link)); + assert_eq!(at.resolve_link(link), file); + + let arg = &format!("--suffix={}", suffix); + ucmd.args(&["-b", arg, "-s", file, link]) + .succeeds() + .no_stderr(); + assert!(at.file_exists(file)); + + assert!(at.is_symlink(link)); + assert_eq!(at.resolve_link(link), file); + + let backup = &format!("{}{}", link, suffix); + assert!(at.is_symlink(backup)); + assert_eq!(at.resolve_link(backup), file); +} + #[test] fn test_symlink_backup_numbering() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 89f4043f8..a0bd0209d 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -340,6 +340,27 @@ fn test_mv_custom_backup_suffix() { assert!(at.file_exists(&format!("{}{}", file_b, suffix))); } +#[test] +fn test_mv_custom_backup_suffix_hyphen_value() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_a = "test_mv_custom_backup_suffix_file_a"; + let file_b = "test_mv_custom_backup_suffix_file_b"; + let suffix = "-v"; + + at.touch(file_a); + at.touch(file_b); + ucmd.arg("-b") + .arg(format!("--suffix={}", suffix)) + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(!at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}{}", file_b, suffix))); +} + #[test] fn test_mv_custom_backup_suffix_via_env() { let (at, mut ucmd) = at_and_ucmd!(); From 4f8d1c5fcfe1bff7971047e05895e7f896c675eb Mon Sep 17 00:00:00 2001 From: Daniel Eades Date: Sun, 30 Jan 2022 21:25:09 +0100 Subject: [PATCH 014/161] add additional lints --- .cargo/config | 9 +++++++++ src/bin/coreutils.rs | 8 ++++---- src/uu/chroot/src/chroot.rs | 6 +++--- src/uu/du/src/du.rs | 2 +- src/uu/env/src/env.rs | 1 + src/uu/kill/src/kill.rs | 6 +++--- src/uu/split/src/number.rs | 24 ++++++++++++------------ src/uu/split/src/split.rs | 6 +++--- src/uu/tail/src/platform/unix.rs | 1 + src/uu/tail/src/platform/windows.rs | 4 ++-- src/uucore/src/lib/features/fsext.rs | 8 ++++---- src/uucore/src/lib/features/wide.rs | 4 ++-- tests/by-util/test_du.rs | 6 +++--- tests/by-util/test_env.rs | 2 +- 14 files changed, 49 insertions(+), 38 deletions(-) diff --git a/.cargo/config b/.cargo/config index 58e1381b1..0a8fd3d00 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,2 +1,11 @@ [target.x86_64-unknown-redox] linker = "x86_64-unknown-redox-gcc" + +[target.'cfg(feature = "cargo-clippy")'] +rustflags = [ + "-Wclippy::use_self", + "-Wclippy::needless_pass_by_value", + "-Wclippy::semicolon_if_nothing_returned", + "-Wclippy::single_char_pattern", + "-Wclippy::explicit_iter_loop", +] diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 901139edc..fcd86c93f 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -87,7 +87,7 @@ fn main() { }; if util == "completion" { - gen_completions(args, utils); + gen_completions(args, &utils); } match utils.get(util) { @@ -132,7 +132,7 @@ fn main() { /// Prints completions for the utility in the first parameter for the shell in the second parameter to stdout fn gen_completions( args: impl Iterator, - util_map: UtilityMap, + util_map: &UtilityMap, ) -> ! { let all_utilities: Vec<_> = std::iter::once("coreutils") .chain(util_map.keys().copied()) @@ -168,9 +168,9 @@ fn gen_completions( process::exit(0); } -fn gen_coreutils_app(util_map: UtilityMap) -> App<'static> { +fn gen_coreutils_app(util_map: &UtilityMap) -> App<'static> { let mut app = App::new("coreutils"); - for (_, (_, sub_app)) in &util_map { + for (_, (_, sub_app)) in util_map { app = app.subcommand(sub_app()); } app diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index 179880b14..f656ed77d 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -201,12 +201,12 @@ fn set_main_group(group: &str) -> UResult<()> { } #[cfg(any(target_vendor = "apple", target_os = "freebsd"))] -fn set_groups(groups: Vec) -> libc::c_int { +fn set_groups(groups: &[libc::gid_t]) -> libc::c_int { unsafe { setgroups(groups.len() as libc::c_int, groups.as_ptr()) } } #[cfg(target_os = "linux")] -fn set_groups(groups: Vec) -> libc::c_int { +fn set_groups(groups: &[libc::gid_t]) -> libc::c_int { unsafe { setgroups(groups.len() as libc::size_t, groups.as_ptr()) } } @@ -220,7 +220,7 @@ fn set_groups_from_str(groups: &str) -> UResult<()> { }; groups_vec.push(gid); } - let err = set_groups(groups_vec); + let err = set_groups(&groups_vec); if err != 0 { return Err(ChrootError::SetGroupsFailed(Error::last_os_error()).into()); } diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 7629aba69..e75210ef5 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -144,7 +144,7 @@ impl Stat { #[cfg(windows)] let file_info = get_file_info(&path); #[cfg(windows)] - Ok(Stat { + Ok(Self { path, is_dir: metadata.is_dir(), size: metadata.len(), diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 677ffe1bf..c0e94a578 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -104,6 +104,7 @@ fn load_config_file(opts: &mut Options) -> UResult<()> { } #[cfg(not(windows))] +#[allow(clippy::ptr_arg)] fn build_command<'a, 'b>(args: &'a mut Vec<&'b str>) -> (Cow<'b, str>, &'a [&'b str]) { let progname = Cow::from(args[0]); (progname, &args[1..]) diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index 02dc91dbf..413a183a8 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -74,7 +74,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { table(); Ok(()) } - Mode::List => list(pids_or_signals.get(0).cloned()), + Mode::List => list(pids_or_signals.get(0)), } } @@ -168,9 +168,9 @@ fn print_signals() { println!(); } -fn list(arg: Option) -> UResult<()> { +fn list(arg: Option<&String>) -> UResult<()> { match arg { - Some(ref x) => print_signal(x), + Some(x) => print_signal(x), None => { print_signals(); Ok(()) diff --git a/src/uu/split/src/number.rs b/src/uu/split/src/number.rs index b2c402716..ef3ccbc4b 100644 --- a/src/uu/split/src/number.rs +++ b/src/uu/split/src/number.rs @@ -96,8 +96,8 @@ impl Number { #[allow(dead_code)] fn digits(&self) -> &Vec { match self { - Number::FixedWidth(number) => &number.digits, - Number::DynamicWidth(number) => &number.digits, + Self::FixedWidth(number) => &number.digits, + Self::DynamicWidth(number) => &number.digits, } } @@ -136,8 +136,8 @@ impl Number { /// ``` pub fn increment(&mut self) -> Result<(), Overflow> { match self { - Number::FixedWidth(number) => number.increment(), - Number::DynamicWidth(number) => number.increment(), + Self::FixedWidth(number) => number.increment(), + Self::DynamicWidth(number) => number.increment(), } } } @@ -145,8 +145,8 @@ impl Number { impl Display for Number { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - Number::FixedWidth(number) => number.fmt(f), - Number::DynamicWidth(number) => number.fmt(f), + Self::FixedWidth(number) => number.fmt(f), + Self::DynamicWidth(number) => number.fmt(f), } } } @@ -183,8 +183,8 @@ pub struct FixedWidthNumber { impl FixedWidthNumber { /// Instantiate a number of the given radix and width. - pub fn new(radix: u8, width: usize) -> FixedWidthNumber { - FixedWidthNumber { + pub fn new(radix: u8, width: usize) -> Self { + Self { radix, digits: vec![0; width], } @@ -286,8 +286,8 @@ impl DynamicWidthNumber { /// /// This associated function returns a new instance of the struct /// with the given radix and a width of two digits, both 0. - pub fn new(radix: u8) -> DynamicWidthNumber { - DynamicWidthNumber { + pub fn new(radix: u8) -> Self { + Self { radix, digits: vec![0, 0], } @@ -404,7 +404,7 @@ mod tests { fn num(n: usize) -> Number { let mut number = Number::DynamicWidth(DynamicWidthNumber::new(26)); for _ in 0..n { - number.increment().unwrap() + number.increment().unwrap(); } number } @@ -428,7 +428,7 @@ mod tests { fn num(n: usize) -> Number { let mut number = Number::DynamicWidth(DynamicWidthNumber::new(10)); for _ in 0..n { - number.increment().unwrap() + number.increment().unwrap(); } number } diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 23eb24768..a05959810 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -62,7 +62,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .override_usage(&usage[..]) .after_help(&long_usage[..]) .get_matches_from(args); - let settings = Settings::from(matches)?; + let settings = Settings::from(&matches)?; split(&settings) } @@ -232,7 +232,7 @@ struct Settings { impl Settings { /// Parse a strategy from the command-line arguments. - fn from(matches: ArgMatches) -> UResult { + fn from(matches: &ArgMatches) -> UResult { let result = Self { suffix_length: matches .value_of(OPT_SUFFIX_LENGTH) @@ -242,7 +242,7 @@ impl Settings { numeric_suffix: matches.occurrences_of(OPT_NUMERIC_SUFFIXES) > 0, additional_suffix: matches.value_of(OPT_ADDITIONAL_SUFFIX).unwrap().to_owned(), verbose: matches.occurrences_of("verbose") > 0, - strategy: Strategy::from(&matches)?, + strategy: Strategy::from(matches)?, input: matches.value_of(ARG_INPUT).unwrap().to_owned(), prefix: matches.value_of(ARG_PREFIX).unwrap().to_owned(), filter: matches.value_of(OPT_FILTER).map(|s| s.to_owned()), diff --git a/src/uu/tail/src/platform/unix.rs b/src/uu/tail/src/platform/unix.rs index e7f75c31e..e01d5e444 100644 --- a/src/uu/tail/src/platform/unix.rs +++ b/src/uu/tail/src/platform/unix.rs @@ -30,6 +30,7 @@ impl ProcessChecker { } // Borrowing mutably to be aligned with Windows implementation + #[allow(clippy::wrong_self_convention)] pub fn is_dead(&mut self) -> bool { unsafe { libc::kill(self.pid, 0) != 0 && get_errno() != libc::EPERM } } diff --git a/src/uu/tail/src/platform/windows.rs b/src/uu/tail/src/platform/windows.rs index 7faa872e6..c63040a2a 100644 --- a/src/uu/tail/src/platform/windows.rs +++ b/src/uu/tail/src/platform/windows.rs @@ -24,11 +24,11 @@ pub struct ProcessChecker { } impl ProcessChecker { - pub fn new(process_id: self::Pid) -> ProcessChecker { + pub fn new(process_id: self::Pid) -> Self { #[allow(non_snake_case)] let FALSE = 0i32; let h = unsafe { OpenProcess(SYNCHRONIZE, FALSE, process_id as DWORD) }; - ProcessChecker { + Self { dead: h.is_null(), handle: h, } diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index d1e623757..a3b05dff8 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -238,7 +238,7 @@ impl MountInfo { } } #[cfg(windows)] - fn new(mut volume_name: String) -> Option { + fn new(mut volume_name: String) -> Option { let mut dev_name_buf = [0u16; MAX_PATH]; volume_name.pop(); unsafe { @@ -289,7 +289,7 @@ impl MountInfo { } else { None }; - let mut mn_info = MountInfo { + let mut mn_info = Self { dev_id: volume_name, dev_name, fs_type: fs_type.unwrap_or_else(|| "".to_string()), @@ -319,7 +319,7 @@ use std::ffi::CStr; ))] impl From for MountInfo { fn from(statfs: StatFs) -> Self { - let mut info = MountInfo { + let mut info = Self { dev_id: "".to_string(), dev_name: unsafe { // spell-checker:disable-next-line @@ -553,7 +553,7 @@ impl FsUsage { } let bytes_per_cluster = sectors_per_cluster as u64 * bytes_per_sector as u64; - FsUsage { + Self { // f_bsize File system block size. blocksize: bytes_per_cluster as u64, // f_blocks - Total number of blocks on the file system, in units of f_frsize. diff --git a/src/uucore/src/lib/features/wide.rs b/src/uucore/src/lib/features/wide.rs index 49ce575a7..6b9368f50 100644 --- a/src/uucore/src/lib/features/wide.rs +++ b/src/uucore/src/lib/features/wide.rs @@ -27,10 +27,10 @@ pub trait FromWide { fn from_wide_null(wide: &[u16]) -> Self; } impl FromWide for String { - fn from_wide(wide: &[u16]) -> String { + fn from_wide(wide: &[u16]) -> Self { OsString::from_wide(wide).to_string_lossy().into_owned() } - fn from_wide_null(wide: &[u16]) -> String { + fn from_wide_null(wide: &[u16]) -> Self { let len = wide.iter().take_while(|&&c| c != 0).count(); OsString::from_wide(&wide[..len]) .to_string_lossy() diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index efc097773..b0506d071 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -179,15 +179,15 @@ fn test_du_hard_link() { #[cfg(target_vendor = "apple")] fn _du_hard_link(s: &str) { - assert_eq!(s, "12\tsubdir/links\n") + assert_eq!(s, "12\tsubdir/links\n"); } #[cfg(target_os = "windows")] fn _du_hard_link(s: &str) { - assert_eq!(s, "8\tsubdir/links\n") + assert_eq!(s, "8\tsubdir/links\n"); } #[cfg(target_os = "freebsd")] fn _du_hard_link(s: &str) { - assert_eq!(s, "16\tsubdir/links\n") + assert_eq!(s, "16\tsubdir/links\n"); } #[cfg(all( not(target_vendor = "apple"), diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index e1950f0df..3559e74cd 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -219,7 +219,7 @@ fn test_change_directory() { .args(&pwd) .succeeds() .stdout_move_str(); - assert_eq!(out.trim(), temporary_path.as_os_str()) + assert_eq!(out.trim(), temporary_path.as_os_str()); } #[test] From cd1b5c5748ab2613faa5c63c59ed415a9739c27d Mon Sep 17 00:00:00 2001 From: Sam Caldwell Date: Mon, 31 Jan 2022 22:08:59 -0700 Subject: [PATCH 015/161] [truncate] change cli error return code Exit with status code 1 for argument parsing errors in `truncate`. When `clap` encounters an error during argument parsing, it exits with status code 2. This causes some GNU tests to fail since they expect status code 1. --- src/uu/truncate/src/truncate.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 685363f8f..142524aad 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -6,6 +6,7 @@ // * file that was distributed with this source code. // spell-checker:ignore (ToDO) RFILE refsize rfilename fsize tsize +use clap; use clap::{crate_version, App, AppSettings, Arg}; use std::convert::TryFrom; use std::fs::{metadata, OpenOptions}; @@ -115,7 +116,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app() .override_usage(&usage[..]) .after_help(&long_usage[..]) - .get_matches_from(args); + .try_get_matches_from(args) + .unwrap_or_else(|e| { + e.print(); + match e.kind { + clap::ErrorKind::DisplayHelp | clap::ErrorKind::DisplayVersion => { + std::process::exit(0) + } + _ => std::process::exit(1), + } + }); let files: Vec = matches .values_of(options::ARG_FILES) From 6f24166c63a3aa001f9c06df8a93fc2f0ab211b7 Mon Sep 17 00:00:00 2001 From: Sam Caldwell Date: Mon, 31 Jan 2022 22:25:59 -0700 Subject: [PATCH 016/161] [truncate] handle unused_must_use warning --- src/uu/truncate/src/truncate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 142524aad..4850e592a 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -118,7 +118,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .after_help(&long_usage[..]) .try_get_matches_from(args) .unwrap_or_else(|e| { - e.print(); + e.print().expect("Error writing clap::Error"); match e.kind { clap::ErrorKind::DisplayHelp | clap::ErrorKind::DisplayVersion => { std::process::exit(0) From e1f7c774d811c4c3da6cd548bdc25c47a3183580 Mon Sep 17 00:00:00 2001 From: Sam Caldwell Date: Mon, 31 Jan 2022 22:59:10 -0700 Subject: [PATCH 017/161] Remove redundant import --- src/uu/truncate/src/truncate.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 4850e592a..12f413247 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -6,7 +6,6 @@ // * file that was distributed with this source code. // spell-checker:ignore (ToDO) RFILE refsize rfilename fsize tsize -use clap; use clap::{crate_version, App, AppSettings, Arg}; use std::convert::TryFrom; use std::fs::{metadata, OpenOptions}; From d762bebc1c1a16c3fbb0d8fef024d4d995f9db51 Mon Sep 17 00:00:00 2001 From: Linux User Date: Tue, 1 Feb 2022 06:55:11 +0000 Subject: [PATCH 018/161] update shell scripts according to shellcheck recommendations and minor cleanup --- .travis/redox-toolchain.sh | 2 +- util/GHA-delete-GNU-workflow-logs.sh | 20 +++++++++--------- util/build-code_coverage.sh | 18 +++++++++------- util/build-gnu.sh | 4 ++-- util/publish.sh | 31 ++++++++++++---------------- util/run-gnu-test.sh | 3 ++- util/show-code_coverage.sh | 7 +++---- util/show-utils.sh | 16 ++++++-------- util/update-version.sh | 7 +++++-- 9 files changed, 52 insertions(+), 56 deletions(-) diff --git a/.travis/redox-toolchain.sh b/.travis/redox-toolchain.sh index 83bc8fc45..d8b43b489 100755 --- a/.travis/redox-toolchain.sh +++ b/.travis/redox-toolchain.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh rustup target add x86_64-unknown-redox sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys AA12E97F0881517F diff --git a/util/GHA-delete-GNU-workflow-logs.sh b/util/GHA-delete-GNU-workflow-logs.sh index 19e3311d4..f7406b831 100755 --- a/util/GHA-delete-GNU-workflow-logs.sh +++ b/util/GHA-delete-GNU-workflow-logs.sh @@ -15,21 +15,21 @@ ME_parent_dir_abs="$(realpath -mP -- "${ME_parent_dir}")" # * `gh` available? unset GH -gh --version 1>/dev/null 2>&1 -if [ $? -eq 0 ]; then export GH="gh"; fi +if gh --version 1>/dev/null 2>&1; then + export GH="gh" +else + echo "ERR!: missing \`gh\` (see install instructions at )" 1>&2 +fi # * `jq` available? unset JQ -jq --version 1>/dev/null 2>&1 -if [ $? -eq 0 ]; then export JQ="jq"; fi +if jq --version 1>/dev/null 2>&1; then + export JQ="jq" +else + echo "ERR!: missing \`jq\` (install with \`sudo apt install jq\`)" 1>&2 +fi if [ -z "${GH}" ] || [ -z "${JQ}" ]; then - if [ -z "${GH}" ]; then - echo 'ERR!: missing `gh` (see install instructions at )' 1>&2 - fi - if [ -z "${JQ}" ]; then - echo 'ERR!: missing `jq` (install with `sudo apt install jq`)' 1>&2 - fi exit 1 fi diff --git a/util/build-code_coverage.sh b/util/build-code_coverage.sh index 7ad3165fe..b92b7eb48 100755 --- a/util/build-code_coverage.sh +++ b/util/build-code_coverage.sh @@ -8,12 +8,13 @@ FEATURES_OPTION="--features feat_os_unix" -ME_dir="$(dirname -- $(readlink -fm -- "$0"))" +ME_dir="$(dirname -- "$(readlink -fm -- "$0")")" REPO_main_dir="$(dirname -- "${ME_dir}")" -cd "${REPO_main_dir}" +cd "${REPO_main_dir}" && echo "[ \"$PWD\" ]" +#shellcheck disable=SC2086 UTIL_LIST=$("${ME_dir}"/show-utils.sh ${FEATURES_OPTION}) CARGO_INDIVIDUAL_PACKAGE_OPTIONS="" for UTIL in ${UTIL_LIST}; do @@ -30,10 +31,12 @@ export RUSTC_WRAPPER="" ## NOTE: RUSTC_WRAPPER=='sccache' breaks code covera export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" export RUSTDOCFLAGS="-Cpanic=abort" export RUSTUP_TOOLCHAIN="nightly-gnu" -cargo build ${FEATURES_OPTION} -cargo test --no-run ${FEATURES_OPTION} -cargo test --quiet ${FEATURES_OPTION} -cargo test --quiet ${FEATURES_OPTION} ${CARGO_INDIVIDUAL_PACKAGE_OPTIONS} +#shellcheck disable=SC2086 +{ cargo build ${FEATURES_OPTION} + cargo test --no-run ${FEATURES_OPTION} + cargo test --quiet ${FEATURES_OPTION} + cargo test --quiet ${FEATURES_OPTION} ${CARGO_INDIVIDUAL_PACKAGE_OPTIONS} +} export COVERAGE_REPORT_DIR if [ -z "${COVERAGE_REPORT_DIR}" ]; then COVERAGE_REPORT_DIR="${REPO_main_dir}/target/debug/coverage-nix"; fi @@ -47,8 +50,7 @@ mkdir -p "${COVERAGE_REPORT_DIR}" grcov . --output-type lcov --output-path "${COVERAGE_REPORT_DIR}/../lcov.info" --branch --ignore build.rs --ignore '/*' --ignore '[A-Za-z]:/*' --ignore 'C:/Users/*' --excl-br-line '^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()' # * build HTML # -- use `genhtml` if available for display of additional branch coverage information -genhtml --version 2>/dev/null 1>&2 -if [ $? -eq 0 ]; then +if genhtml --version 2>/dev/null 1>&2; then genhtml "${COVERAGE_REPORT_DIR}/../lcov.info" --output-directory "${COVERAGE_REPORT_DIR}" --branch-coverage --function-coverage | grep ": [0-9]" else grcov . --output-type html --output-path "${COVERAGE_REPORT_DIR}" --branch --ignore build.rs --ignore '/*' --ignore '[A-Za-z]:/*' --ignore 'C:/Users/*' --excl-br-line '^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()' diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 8b1e4925b..a52d42107 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -15,7 +15,7 @@ if test ! -d ../gnulib; then fi -pushd $(pwd) +pushd "$PWD" make PROFILE=release BUILDDIR="$PWD/target/release/" cp "${BUILDDIR}/install" "${BUILDDIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target @@ -49,7 +49,7 @@ make -j "$(nproc)" # Used to be 36. Reduced to 20 to decrease the log size for i in {00..20} do - make tests/factor/t${i}.sh + make "tests/factor/t${i}.sh" done # strip the long stuff diff --git a/util/publish.sh b/util/publish.sh index ae171e39c..6f4d9f237 100755 --- a/util/publish.sh +++ b/util/publish.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh set -e ARG="" @@ -6,26 +6,21 @@ if test "$1" != "--do-it"; then ARG="--dry-run --allow-dirty" fi -cd src/uucore/ -cargo publish $ARG -cd - -sleep 2s - -cd src/uucore_procs/ -cargo publish $ARG -cd - -sleep 2s - -cd src/uu/stdbuf/src/libstdbuf/ -cargo publish $ARG -cd - -sleep 2s +for dir in src/uucore/ src/uucore_procs/ src/uu/stdbuf/src/libstdbuf/ ; do + ( cd "$dir" + #shellcheck disable=SC2086 + cargo publish $ARG + ) + sleep 2s +done PROGS=$(ls -1d src/uu/*/) for p in $PROGS; do - cd $p - cargo publish $ARG - cd - + ( cd "$p" + #shellcheck disable=SC2086 + cargo publish $ARG + ) done +#shellcheck disable=SC2086 cargo publish $ARG diff --git a/util/run-gnu-test.sh b/util/run-gnu-test.sh index 483fc1be9..ff61e636e 100755 --- a/util/run-gnu-test.sh +++ b/util/run-gnu-test.sh @@ -1,6 +1,6 @@ #!/bin/bash # spell-checker:ignore (env/vars) BUILDDIR GNULIB SUBDIRS -cd "$(dirname "${BASH_SOURCE[0]}")/../.." +cd "$(dirname -- "$(readlink -fm -- "$0")")/../.." set -e BUILDDIR="${PWD}/uutils/target/release" GNULIB_DIR="${PWD}/gnulib" @@ -13,4 +13,5 @@ if test -n "$1"; then export RUN_TEST="TESTS=$1" fi +#shellcheck disable=SC2086 timeout -sKILL 2h make -j "$(nproc)" check $RUN_TEST SUBDIRS=. RUN_EXPENSIVE_TESTS=yes RUN_VERY_EXPENSIVE_TESTS=yes VERBOSE=no || : # Kill after 4 hours in case something gets stuck in make diff --git a/util/show-code_coverage.sh b/util/show-code_coverage.sh index 365041434..6226d856b 100755 --- a/util/show-code_coverage.sh +++ b/util/show-code_coverage.sh @@ -2,15 +2,14 @@ # spell-checker:ignore (vars) OSID -ME_dir="$(dirname -- $(readlink -fm -- "$0"))" +ME_dir="$(dirname -- "$(readlink -fm -- "$0")")" REPO_main_dir="$(dirname -- "${ME_dir}")" export COVERAGE_REPORT_DIR="${REPO_main_dir}/target/debug/coverage-nix" -"${ME_dir}/build-code_coverage.sh" -if [ $? -ne 0 ]; then exit 1 ; fi +if ! "${ME_dir}/build-code_coverage.sh"; then exit 1 ; fi case ";$OSID_tags;" in - *";wsl;"* ) powershell.exe -c $(wslpath -w "${COVERAGE_REPORT_DIR}"/index.html) ;; + *";wsl;"* ) powershell.exe -c "$(wslpath -w "${COVERAGE_REPORT_DIR}"/index.html)" ;; * ) xdg-open --version >/dev/null 2>&1 && xdg-open "${COVERAGE_REPORT_DIR}"/index.html || echo "report available at '\"${COVERAGE_REPORT_DIR}\"/index.html'" ;; esac ; diff --git a/util/show-utils.sh b/util/show-utils.sh index b4a613d9b..f69b42678 100755 --- a/util/show-utils.sh +++ b/util/show-utils.sh @@ -15,17 +15,13 @@ default_utils="base32 base64 basename cat cksum comm cp cut date dircolors dirna project_main_dir="${ME_parent_dir_abs}" # printf 'project_main_dir="%s"\n' "${project_main_dir}" -cd "${project_main_dir}" +cd "${project_main_dir}" && # `jq` available? -unset JQ -jq --version 1>/dev/null 2>&1 -if [ $? -eq 0 ]; then export JQ="jq"; fi - -if [ -z "${JQ}" ]; then - echo 'WARN: missing `jq` (install with `sudo apt install jq`); falling back to default (only fully cross-platform) utility list' 1>&2 - echo $default_utils +if ! jq --version 1>/dev/null 2>&1; then + echo "WARN: missing \`jq\` (install with \`sudo apt install jq\`); falling back to default (only fully cross-platform) utility list" 1>&2 + echo "$default_utils" else - cargo metadata $* --format-version 1 | jq -r "[.resolve.nodes[] | { id: .id, deps: [.deps[] | { name:.name, pkg:.pkg }] }] | .[] | select(.id|startswith(\"coreutils\")) | [.deps[] | select((.name|startswith(\"uu_\")) or (.pkg|startswith(\"uu_\")))] | [.[].pkg | match(\"^\\\w+\";\"g\")] | [.[].string | sub(\"^uu_\"; \"\")] | sort | join(\" \")" - # cargo metadata $* --format-version 1 | jq -r "[.resolve.nodes[] | { id: .id, deps: [.deps[] | { name:.name, pkg:.pkg }] }] | .[] | select(.id|startswith(\"coreutils\")) | [.deps[] | select((.name|startswith(\"uu_\")) or (.pkg|startswith(\"uu_\")))] | [.[].pkg | match(\"^\\\w+\";\"g\")] | [.[].string] | sort | join(\" \")" + cargo metadata "$*" --format-version 1 | jq -r "[.resolve.nodes[] | { id: .id, deps: [.deps[] | { name:.name, pkg:.pkg }] }] | .[] | select(.id|startswith(\"coreutils\")) | [.deps[] | select((.name|startswith(\"uu_\")) or (.pkg|startswith(\"uu_\")))] | [.[].pkg | match(\"^\\\w+\";\"g\")] | [.[].string | sub(\"^uu_\"; \"\")] | sort | join(\" \")" + # cargo metadata "$*" --format-version 1 | jq -r "[.resolve.nodes[] | { id: .id, deps: [.deps[] | { name:.name, pkg:.pkg }] }] | .[] | select(.id|startswith(\"coreutils\")) | [.deps[] | select((.name|startswith(\"uu_\")) or (.pkg|startswith(\"uu_\")))] | [.[].pkg | match(\"^\\\w+\";\"g\")] | [.[].string] | sort | join(\" \")" fi diff --git a/util/update-version.sh b/util/update-version.sh index 503f65e52..62b130bda 100755 --- a/util/update-version.sh +++ b/util/update-version.sh @@ -1,10 +1,10 @@ -#!/bin/bash +#!/bin/sh # This is a stupid helper. I will mass replace all versions (including other crates) # So, it should be triple-checked # How to ship a new release: # 1) update this script -# 2) run it: bash util/update-version.sh +# 2) run it: sh util/update-version.sh # 3) Do a spot check with "git diff" # 4) cargo test --release --features unix # 5) Run util/publish.sh in dry mode (it will fail as packages needs more recent version of uucore) @@ -23,6 +23,7 @@ UUCORE_TO="0.0.11" PROGS=$(ls -1d src/uu/*/Cargo.toml src/uu/stdbuf/src/libstdbuf/Cargo.toml Cargo.toml src/uu/base64/Cargo.toml) # update the version of all programs +#shellcheck disable=SC2086 sed -i -e "s|version = \"$FROM\"|version = \"$TO\"|" $PROGS # Update uucore_procs @@ -35,6 +36,8 @@ sed -i -e "s|= { optional=true, version=\"$FROM\", package=\"uu_|= { optional=tr # Update uucore itself sed -i -e "s|version = \"$UUCORE_FROM\"|version = \"$UUCORE_TO\"|" src/uucore/Cargo.toml # Update crates using uucore +#shellcheck disable=SC2086 sed -i -e "s|uucore = { version=\">=$UUCORE_FROM\",|uucore = { version=\">=$UUCORE_TO\",|" $PROGS # Update crates using uucore_procs +#shellcheck disable=SC2086 sed -i -e "s|uucore_procs = { version=\">=$UUCORE_PROCS_FROM\",|uucore_procs = { version=\">=$UUCORE_PROCS_TO\",|" $PROGS From b29e219e4dc7fe162c83d0e0dec301da4fd9526c Mon Sep 17 00:00:00 2001 From: Andreas Molzer Date: Tue, 1 Feb 2022 00:35:26 +0100 Subject: [PATCH 019/161] true: Rework to return true more often Now treats recognized command line options and ignores unrecognized command line options instead of returning a special exit status for them. There is one point of interest, which is related to an implementation detail in GNU `true`. It may return a non-true exit status (in particular EXIT_FAIL) if writing the diagnostics of a GNU specific option fails. For example `true --version > /dev/full` would fail and have exit status 1. This behavior was acknowledged in gnu in commit <9a6a486e6503520fd2581f2d3356b7149f1b225d>. No further justification provided for keeping this quirk. POSIX knows no such options, and requires an exit status of 0 in all cases. We replicate GNU here which is a consistency improvement over the prior implementation. Adds documentation to clarify the intended behavior more properly. --- src/uu/true/src/true.rs | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/uu/true/src/true.rs b/src/uu/true/src/true.rs index ff5b08e85..21b064e73 100644 --- a/src/uu/true/src/true.rs +++ b/src/uu/true/src/true.rs @@ -4,16 +4,41 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. +use clap::{App, AppSettings, ErrorKind}; +use std::io::Write; +use uucore::error::{set_exit_code, UResult}; -use clap::{App, AppSettings}; -use uucore::error::UResult; +static ABOUT: &str = " + Returns true, a successful exit status. + + Immediately returns with the exit status `0`, except when invoked with one of the recognized + options. In those cases it will try to write the help or version text. Any IO error during this + operation causes the program to return `1` instead. +"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - uu_app().get_matches_from(args); + let app = uu_app(); + + if let Err(err) = app.try_get_matches_from(args) { + if let ErrorKind::DisplayHelp | ErrorKind::DisplayVersion = err.kind { + if let Err(print_fail) = err.print() { + // Try to display this error. + let _ = writeln!(std::io::stderr(), "{}: {}", uucore::util_name(), print_fail); + // Mirror GNU options. When failing to print warnings or version flags, then we exit + // with FAIL. This avoids allocation some error information which may result in yet + // other types of failure. + set_exit_code(1); + } + } + } + Ok(()) } pub fn uu_app<'a>() -> App<'a> { - App::new(uucore::util_name()).setting(AppSettings::InferLongArgs) + App::new(uucore::util_name()) + .version(clap::crate_version!()) + .about(ABOUT) + .setting(AppSettings::InferLongArgs) } From 08f1199b080549b55d1667611434cd022dc41ccf Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Mon, 31 Jan 2022 22:28:40 +0100 Subject: [PATCH 020/161] ls: fix flaky test test_ls_io_errors --- tests/by-util/test_ls.rs | 117 +++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 61 deletions(-) diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index e3fd99e00..cad49084e 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -1,28 +1,27 @@ // spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile -#[cfg(unix)] -extern crate unix_socket; -use crate::common::util::*; - -extern crate regex; -use self::regex::Regex; - -#[cfg(all(unix, feature = "chmod"))] -use nix::unistd::{close, dup2}; -use std::collections::HashMap; -#[cfg(all(unix, feature = "chmod"))] -use std::os::unix::io::AsRawFd; -use std::path::Path; -use std::thread::sleep; -use std::time::Duration; #[cfg(not(windows))] extern crate libc; +extern crate regex; +#[cfg(not(windows))] +extern crate tempfile; +#[cfg(unix)] +extern crate unix_socket; + +use self::regex::Regex; +use crate::common::util::*; +#[cfg(all(unix, feature = "chmod"))] +use nix::unistd::{close, dup}; +use std::collections::HashMap; +#[cfg(all(unix, feature = "chmod"))] +use std::os::unix::io::IntoRawFd; +use std::path::Path; #[cfg(not(windows))] use std::path::PathBuf; #[cfg(not(windows))] use std::sync::Mutex; -#[cfg(not(windows))] -extern crate tempfile; +use std::thread::sleep; +use std::time::Duration; #[cfg(not(windows))] lazy_static! { @@ -142,7 +141,7 @@ fn test_ls_devices() { } } -#[cfg(all(feature = "chmod"))] +#[cfg(feature = "chmod")] #[test] fn test_ls_io_errors() { let scene = TestScenario::new(util_name!()); @@ -202,56 +201,52 @@ fn test_ls_io_errors() { #[cfg(unix)] { at.touch("some-dir4/bad-fd.txt"); - let fd1 = at.open("some-dir4/bad-fd.txt").as_raw_fd(); - let fd2 = 25000; - let rv1 = dup2(fd1, fd2); - let rv2 = close(fd1); + let fd1 = at.open("some-dir4/bad-fd.txt").into_raw_fd(); + let fd2 = dup(dbg!(fd1)).unwrap(); + close(fd1).unwrap(); - // dup and close work on the mac, but doesn't work in some linux containers - // so check to see that return values are non-error before proceeding - if rv1.is_ok() && rv2.is_ok() { - // on the mac and in certain Linux containers bad fds are typed as dirs, - // however sometimes bad fds are typed as links and directory entry on links won't fail - if PathBuf::from(format!("/dev/fd/{fd}", fd = fd2)).is_dir() { - scene - .ucmd() - .arg("-alR") - .arg(format!("/dev/fd/{fd}", fd = fd2)) - .fails() - .stderr_contains(format!( - "cannot open directory '/dev/fd/{fd}': Bad file descriptor", - fd = fd2 - )) - .stdout_does_not_contain(format!("{fd}:\n", fd = fd2)); - - scene - .ucmd() - .arg("-RiL") - .arg(format!("/dev/fd/{fd}", fd = fd2)) - .fails() - .stderr_contains(format!("cannot open directory '/dev/fd/{fd}': Bad file descriptor", fd = fd2)) - // don't double print bad fd errors - .stderr_does_not_contain(format!("ls: cannot open directory '/dev/fd/{fd}': Bad file descriptor\nls: cannot open directory '/dev/fd/{fd}': Bad file descriptor", fd = fd2)); - } else { - scene - .ucmd() - .arg("-alR") - .arg(format!("/dev/fd/{fd}", fd = fd2)) - .succeeds(); - - scene - .ucmd() - .arg("-RiL") - .arg(format!("/dev/fd/{fd}", fd = fd2)) - .succeeds(); - } + // on the mac and in certain Linux containers bad fds are typed as dirs, + // however sometimes bad fds are typed as links and directory entry on links won't fail + if PathBuf::from(format!("/dev/fd/{fd}", fd = fd2)).is_dir() { + scene + .ucmd() + .arg("-alR") + .arg(format!("/dev/fd/{fd}", fd = fd2)) + .fails() + .stderr_contains(format!( + "cannot open directory '/dev/fd/{fd}': Bad file descriptor", + fd = fd2 + )) + .stdout_does_not_contain(format!("{fd}:\n", fd = fd2)); scene .ucmd() - .arg("-alL") + .arg("-RiL") + .arg(format!("/dev/fd/{fd}", fd = fd2)) + .fails() + .stderr_contains(format!("cannot open directory '/dev/fd/{fd}': Bad file descriptor", fd = fd2)) + // don't double print bad fd errors + .stderr_does_not_contain(format!("ls: cannot open directory '/dev/fd/{fd}': Bad file descriptor\nls: cannot open directory '/dev/fd/{fd}': Bad file descriptor", fd = fd2)); + } else { + scene + .ucmd() + .arg("-alR") + .arg(format!("/dev/fd/{fd}", fd = fd2)) + .succeeds(); + + scene + .ucmd() + .arg("-RiL") .arg(format!("/dev/fd/{fd}", fd = fd2)) .succeeds(); } + + scene + .ucmd() + .arg("-alL") + .arg(format!("/dev/fd/{fd}", fd = fd2)) + .succeeds(); + let _ = close(fd2); } } From dcf177f9083b2f676b85049846f78a1ce642d602 Mon Sep 17 00:00:00 2001 From: Andreas Molzer Date: Tue, 1 Feb 2022 01:43:59 +0100 Subject: [PATCH 021/161] false: Align behavior to true and GNU --- src/uu/false/src/false.rs | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/uu/false/src/false.rs b/src/uu/false/src/false.rs index 324f90579..b304b4af6 100644 --- a/src/uu/false/src/false.rs +++ b/src/uu/false/src/false.rs @@ -4,16 +4,43 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. +use clap::{App, AppSettings, ErrorKind}; +use std::io::Write; +use uucore::error::{set_exit_code, UResult}; -use clap::App; -use uucore::error::UResult; +static ABOUT: &str = " + Returns false, an unsuccessful exit status. + + Immediately returns with the exit status `1`. When invoked with one of the recognized options it + will try to write the help or version text. Any IO error during this operation is diagnosed, yet + the program will also return `1`. +"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - uu_app().get_matches_from(args); - Err(1.into()) + let app = uu_app(); + + // Mirror GNU options, always return `1`. In particular even the 'successful' cases of no-op, + // and the interrupted display of help and version should return `1`. Also, we return Ok in all + // paths to avoid the allocation of an error object, an operation that could, in theory, fail + // and unwind through the standard library allocation handling machinery. + set_exit_code(1); + + if let Err(err) = app.try_get_matches_from(args) { + if let ErrorKind::DisplayHelp | ErrorKind::DisplayVersion = err.kind { + if let Err(print_fail) = err.print() { + // Try to display this error. + let _ = writeln!(std::io::stderr(), "{}: {}", uucore::util_name(), print_fail); + } + } + } + + Ok(()) } pub fn uu_app<'a>() -> App<'a> { App::new(uucore::util_name()) + .version(clap::crate_version!()) + .about(ABOUT) + .setting(AppSettings::InferLongArgs) } From 149399c1bf54f7c0df5558578e28bd82ad219581 Mon Sep 17 00:00:00 2001 From: Andreas Molzer Date: Tue, 1 Feb 2022 12:19:00 +0100 Subject: [PATCH 022/161] false,true: Add by-util tests for options --- tests/by-util/test_false.rs | 39 +++++++++++++++++++++++++++++++++++++ tests/by-util/test_true.rs | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/tests/by-util/test_false.rs b/tests/by-util/test_false.rs index bbabc7a52..366dd277b 100644 --- a/tests/by-util/test_false.rs +++ b/tests/by-util/test_false.rs @@ -1,6 +1,45 @@ use crate::common::util::*; +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +use std::fs::OpenOptions; #[test] fn test_exit_code() { new_ucmd!().fails(); } + +#[test] +fn test_version() { + new_ucmd!() + .args(&["--version"]) + .fails() + .stdout_contains("false"); +} + +#[test] +fn test_help() { + new_ucmd!() + .args(&["--help"]) + .fails() + .stdout_contains("false"); +} + +#[test] +fn test_short_options() { + for option in ["-h", "-v"] { + new_ucmd!().arg(option).fails().stdout_is(""); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +fn test_full() { + for option in ["--version", "--help"] { + let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap(); + + new_ucmd!() + .arg(option) + .set_stdout(dev_full) + .fails() + .stderr_contains("No space left on device"); + } +} diff --git a/tests/by-util/test_true.rs b/tests/by-util/test_true.rs index 1d8622c96..d8ac2003b 100644 --- a/tests/by-util/test_true.rs +++ b/tests/by-util/test_true.rs @@ -1,6 +1,45 @@ use crate::common::util::*; +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +use std::fs::OpenOptions; #[test] fn test_exit_code() { new_ucmd!().succeeds(); } + +#[test] +fn test_version() { + new_ucmd!() + .args(&["--version"]) + .succeeds() + .stdout_contains("true"); +} + +#[test] +fn test_help() { + new_ucmd!() + .args(&["--help"]) + .succeeds() + .stdout_contains("true"); +} + +#[test] +fn test_short_options() { + for option in ["-h", "-v"] { + new_ucmd!().arg(option).succeeds().stdout_is(""); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +fn test_full() { + for option in ["--version", "--help"] { + let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap(); + + new_ucmd!() + .arg(option) + .set_stdout(dev_full) + .fails() + .stderr_contains("No space left on device"); + } +} From c1e108933fe66853e0d081f13b657c3acee75cd6 Mon Sep 17 00:00:00 2001 From: Andreas Molzer Date: Tue, 1 Feb 2022 12:32:30 +0100 Subject: [PATCH 023/161] false,true: Align behavior of short flags to GNU --- src/uu/false/src/false.rs | 15 +++++++++++++-- src/uu/true/src/true.rs | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/uu/false/src/false.rs b/src/uu/false/src/false.rs index b304b4af6..0ebbb5be2 100644 --- a/src/uu/false/src/false.rs +++ b/src/uu/false/src/false.rs @@ -4,7 +4,7 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -use clap::{App, AppSettings, ErrorKind}; +use clap::{App, Arg, ArgSettings, ErrorKind}; use std::io::Write; use uucore::error::{set_exit_code, UResult}; @@ -42,5 +42,16 @@ pub fn uu_app<'a>() -> App<'a> { App::new(uucore::util_name()) .version(clap::crate_version!()) .about(ABOUT) - .setting(AppSettings::InferLongArgs) + // Hide the default -V and -h for version and help. + // This requires us to overwrite short, not short_aliases. + .arg( + Arg::new("dummy-help") + .short('h') + .setting(ArgSettings::Hidden), + ) + .arg( + Arg::new("dummy-version") + .short('V') + .setting(ArgSettings::Hidden), + ) } diff --git a/src/uu/true/src/true.rs b/src/uu/true/src/true.rs index 21b064e73..20ee100b7 100644 --- a/src/uu/true/src/true.rs +++ b/src/uu/true/src/true.rs @@ -4,7 +4,7 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -use clap::{App, AppSettings, ErrorKind}; +use clap::{App, Arg, ArgSettings, ErrorKind}; use std::io::Write; use uucore::error::{set_exit_code, UResult}; @@ -40,5 +40,16 @@ pub fn uu_app<'a>() -> App<'a> { App::new(uucore::util_name()) .version(clap::crate_version!()) .about(ABOUT) - .setting(AppSettings::InferLongArgs) + // Hide the default -V and -h for version and help. + // This requires us to overwrite short, not short_aliases. + .arg( + Arg::new("dummy-help") + .short('h') + .setting(ArgSettings::Hidden), + ) + .arg( + Arg::new("dummy-version") + .short('V') + .setting(ArgSettings::Hidden), + ) } From 23a544c485672743b9fa119e0c6614ea12706da3 Mon Sep 17 00:00:00 2001 From: Andreas Molzer Date: Tue, 1 Feb 2022 14:29:26 +0100 Subject: [PATCH 024/161] false,true: Implement custom help, version This avoids hacking around the short options of these command line arguments that have been introduced by clap. Additionally, we test and correctly handle the combination of both version and help. The GNU binary will ignore both arguments in this case while clap would perform the first one. A test for this edge case was added. --- src/uu/false/src/false.rs | 40 ++++++++++++++++++------------- src/uu/true/src/true.rs | 47 +++++++++++++++++++++---------------- tests/by-util/test_false.rs | 10 +++++++- tests/by-util/test_true.rs | 10 +++++++- 4 files changed, 69 insertions(+), 38 deletions(-) diff --git a/src/uu/false/src/false.rs b/src/uu/false/src/false.rs index 0ebbb5be2..8b487847d 100644 --- a/src/uu/false/src/false.rs +++ b/src/uu/false/src/false.rs @@ -4,7 +4,7 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -use clap::{App, Arg, ArgSettings, ErrorKind}; +use clap::{App, AppSettings, Arg}; use std::io::Write; use uucore::error::{set_exit_code, UResult}; @@ -18,7 +18,7 @@ static ABOUT: &str = " #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let app = uu_app(); + let mut app = uu_app(); // Mirror GNU options, always return `1`. In particular even the 'successful' cases of no-op, // and the interrupted display of help and version should return `1`. Also, we return Ok in all @@ -26,12 +26,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // and unwind through the standard library allocation handling machinery. set_exit_code(1); - if let Err(err) = app.try_get_matches_from(args) { - if let ErrorKind::DisplayHelp | ErrorKind::DisplayVersion = err.kind { - if let Err(print_fail) = err.print() { - // Try to display this error. - let _ = writeln!(std::io::stderr(), "{}: {}", uucore::util_name(), print_fail); - } + if let Ok(matches) = app.try_get_matches_from_mut(args) { + let error = if matches.index_of("help").is_some() { + app.print_long_help() + } else if matches.index_of("version").is_some() { + writeln!(std::io::stdout(), "{}", app.render_version()) + } else { + Ok(()) + }; + + // Try to display this error. + if let Err(print_fail) = error { + // Completely ignore any error here, no more failover and we will fail in any case. + let _ = writeln!(std::io::stderr(), "{}: {}", uucore::util_name(), print_fail); } } @@ -42,16 +49,17 @@ pub fn uu_app<'a>() -> App<'a> { App::new(uucore::util_name()) .version(clap::crate_version!()) .about(ABOUT) - // Hide the default -V and -h for version and help. - // This requires us to overwrite short, not short_aliases. + // We provide our own help and version options, to ensure maximum compatibility with GNU. + .setting(AppSettings::DisableHelpFlag | AppSettings::DisableVersionFlag) .arg( - Arg::new("dummy-help") - .short('h') - .setting(ArgSettings::Hidden), + Arg::new("help") + .long("help") + .help("Print help information") + .exclusive(true), ) .arg( - Arg::new("dummy-version") - .short('V') - .setting(ArgSettings::Hidden), + Arg::new("version") + .long("version") + .help("Print version information"), ) } diff --git a/src/uu/true/src/true.rs b/src/uu/true/src/true.rs index 20ee100b7..c3026e684 100644 --- a/src/uu/true/src/true.rs +++ b/src/uu/true/src/true.rs @@ -4,7 +4,7 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -use clap::{App, Arg, ArgSettings, ErrorKind}; +use clap::{App, AppSettings, Arg}; use std::io::Write; use uucore::error::{set_exit_code, UResult}; @@ -18,18 +18,24 @@ static ABOUT: &str = " #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let app = uu_app(); + let mut app = uu_app(); - if let Err(err) = app.try_get_matches_from(args) { - if let ErrorKind::DisplayHelp | ErrorKind::DisplayVersion = err.kind { - if let Err(print_fail) = err.print() { - // Try to display this error. - let _ = writeln!(std::io::stderr(), "{}: {}", uucore::util_name(), print_fail); - // Mirror GNU options. When failing to print warnings or version flags, then we exit - // with FAIL. This avoids allocation some error information which may result in yet - // other types of failure. - set_exit_code(1); - } + if let Ok(matches) = app.try_get_matches_from_mut(args) { + let error = if matches.index_of("help").is_some() { + app.print_long_help() + } else if matches.index_of("version").is_some() { + writeln!(std::io::stdout(), "{}", app.render_version()) + } else { + Ok(()) + }; + + if let Err(print_fail) = error { + // Try to display this error. + let _ = writeln!(std::io::stderr(), "{}: {}", uucore::util_name(), print_fail); + // Mirror GNU options. When failing to print warnings or version flags, then we exit + // with FAIL. This avoids allocation some error information which may result in yet + // other types of failure. + set_exit_code(1); } } @@ -40,16 +46,17 @@ pub fn uu_app<'a>() -> App<'a> { App::new(uucore::util_name()) .version(clap::crate_version!()) .about(ABOUT) - // Hide the default -V and -h for version and help. - // This requires us to overwrite short, not short_aliases. + // We provide our own help and version options, to ensure maximum compatibility with GNU. + .setting(AppSettings::DisableHelpFlag | AppSettings::DisableVersionFlag) .arg( - Arg::new("dummy-help") - .short('h') - .setting(ArgSettings::Hidden), + Arg::new("help") + .long("help") + .help("Print help information") + .exclusive(true), ) .arg( - Arg::new("dummy-version") - .short('V') - .setting(ArgSettings::Hidden), + Arg::new("version") + .long("version") + .help("Print version information"), ) } diff --git a/tests/by-util/test_false.rs b/tests/by-util/test_false.rs index 366dd277b..5ce64e7a8 100644 --- a/tests/by-util/test_false.rs +++ b/tests/by-util/test_false.rs @@ -25,11 +25,19 @@ fn test_help() { #[test] fn test_short_options() { - for option in ["-h", "-v"] { + for option in ["-h", "-V"] { new_ucmd!().arg(option).fails().stdout_is(""); } } +#[test] +fn test_conflict() { + new_ucmd!() + .args(&["--help", "--version"]) + .fails() + .stdout_is(""); +} + #[test] #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] fn test_full() { diff --git a/tests/by-util/test_true.rs b/tests/by-util/test_true.rs index d8ac2003b..aba32578b 100644 --- a/tests/by-util/test_true.rs +++ b/tests/by-util/test_true.rs @@ -25,11 +25,19 @@ fn test_help() { #[test] fn test_short_options() { - for option in ["-h", "-v"] { + for option in ["-h", "-V"] { new_ucmd!().arg(option).succeeds().stdout_is(""); } } +#[test] +fn test_conflict() { + new_ucmd!() + .args(&["--help", "--version"]) + .succeeds() + .stdout_is(""); +} + #[test] #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] fn test_full() { From dbbabedec6decfffe9aba62e58320cff530cbfb4 Mon Sep 17 00:00:00 2001 From: Tillmann Rendel Date: Tue, 1 Feb 2022 14:55:29 +0100 Subject: [PATCH 025/161] Allow comments in VS Code configuration Since JSON formally cannot contain comments, GitHub highlights comments in JSON files as errors, but the configuration files for VS Code in this repository contain comments. This commit configures GitHub to use a different syntax highlighter for the affected files. --- .vscode/.gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .vscode/.gitattributes diff --git a/.vscode/.gitattributes b/.vscode/.gitattributes new file mode 100644 index 000000000..026e40131 --- /dev/null +++ b/.vscode/.gitattributes @@ -0,0 +1,2 @@ +# Configure GitHub to not mark comments in configuration files as errors +*.json linguist-language=JSON-with-Comments From 420045210c8d7ea316ebf8de236a81388f0e3698 Mon Sep 17 00:00:00 2001 From: Tillmann Rendel Date: Tue, 1 Feb 2022 15:27:59 +0100 Subject: [PATCH 026/161] Try moving .gitattributes to root --- .vscode/.gitattributes => .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .vscode/.gitattributes => .gitattributes (58%) diff --git a/.vscode/.gitattributes b/.gitattributes similarity index 58% rename from .vscode/.gitattributes rename to .gitattributes index 026e40131..73153b0d3 100644 --- a/.vscode/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ # Configure GitHub to not mark comments in configuration files as errors -*.json linguist-language=JSON-with-Comments +.vscode/*.json linguist-language=JSON-with-Comments From 19cc63df9ae94791856ab6e6853ced040d711fdc Mon Sep 17 00:00:00 2001 From: Ivan Majeru Date: Tue, 1 Feb 2022 20:32:56 +0200 Subject: [PATCH 027/161] dd: allow multiple instances of arguments Correct the behavior of `dd` when multiple arguments are provided. Before this commit, if the multiple arguments was provided then the validation error are returned. For example ``` $ printf '' | ./target/debug/dd status=none status=noxfer error: The argument '--status=' was provided more than once, but cannot be used multiple times USAGE: dd [OPTIONS] For more information try --help ``` The unittest was added for this case. --- src/uu/dd/src/dd.rs | 13 +++ src/uu/dd/src/parseargs/unit_tests.rs | 112 ++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 54e3190ce..296c6c6b1 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -954,6 +954,7 @@ pub fn uu_app<'a>() -> App<'a> { .arg( Arg::new(options::INFILE) .long(options::INFILE) + .overrides_with(options::INFILE) .takes_value(true) .require_equals(true) .value_name("FILE") @@ -962,6 +963,7 @@ pub fn uu_app<'a>() -> App<'a> { .arg( Arg::new(options::OUTFILE) .long(options::OUTFILE) + .overrides_with(options::OUTFILE) .takes_value(true) .require_equals(true) .value_name("FILE") @@ -970,6 +972,7 @@ pub fn uu_app<'a>() -> App<'a> { .arg( Arg::new(options::IBS) .long(options::IBS) + .overrides_with(options::IBS) .takes_value(true) .require_equals(true) .value_name("N") @@ -978,6 +981,7 @@ pub fn uu_app<'a>() -> App<'a> { .arg( Arg::new(options::OBS) .long(options::OBS) + .overrides_with(options::OBS) .takes_value(true) .require_equals(true) .value_name("N") @@ -986,6 +990,7 @@ pub fn uu_app<'a>() -> App<'a> { .arg( Arg::new(options::BS) .long(options::BS) + .overrides_with(options::BS) .takes_value(true) .require_equals(true) .value_name("N") @@ -994,6 +999,7 @@ pub fn uu_app<'a>() -> App<'a> { .arg( Arg::new(options::CBS) .long(options::CBS) + .overrides_with(options::CBS) .takes_value(true) .require_equals(true) .value_name("N") @@ -1002,6 +1008,7 @@ pub fn uu_app<'a>() -> App<'a> { .arg( Arg::new(options::SKIP) .long(options::SKIP) + .overrides_with(options::SKIP) .takes_value(true) .require_equals(true) .value_name("N") @@ -1010,6 +1017,7 @@ pub fn uu_app<'a>() -> App<'a> { .arg( Arg::new(options::SEEK) .long(options::SEEK) + .overrides_with(options::SEEK) .takes_value(true) .require_equals(true) .value_name("N") @@ -1018,6 +1026,7 @@ pub fn uu_app<'a>() -> App<'a> { .arg( Arg::new(options::COUNT) .long(options::COUNT) + .overrides_with(options::COUNT) .takes_value(true) .require_equals(true) .value_name("N") @@ -1026,6 +1035,7 @@ pub fn uu_app<'a>() -> App<'a> { .arg( Arg::new(options::STATUS) .long(options::STATUS) + .overrides_with(options::STATUS) .takes_value(true) .require_equals(true) .value_name("LEVEL") @@ -1050,6 +1060,7 @@ Printing performance stats is also triggered by the INFO signal (where supported .arg( Arg::new(options::CONV) .long(options::CONV) + .overrides_with(options::CONV) .takes_value(true) .require_equals(true) .value_name("CONV") @@ -1087,6 +1098,7 @@ Conversion Flags: .arg( Arg::new(options::IFLAG) .long(options::IFLAG) + .overrides_with(options::IFLAG) .takes_value(true) .require_equals(true) .value_name("FLAG") @@ -1113,6 +1125,7 @@ General-Flags .arg( Arg::new(options::OFLAG) .long(options::OFLAG) + .overrides_with(options::OFLAG) .takes_value(true) .require_equals(true) .value_name("FLAG") diff --git a/src/uu/dd/src/parseargs/unit_tests.rs b/src/uu/dd/src/parseargs/unit_tests.rs index a72944309..64da4640f 100644 --- a/src/uu/dd/src/parseargs/unit_tests.rs +++ b/src/uu/dd/src/parseargs/unit_tests.rs @@ -299,6 +299,118 @@ fn test_status_level_noxfer() { assert_eq!(st, StatusLevel::Noxfer); } +#[test] +fn test_override_multiple_levels() { + let args = vec![ + String::from("dd"), + String::from("--if=foo.file"), + String::from("--if=correct.file"), + String::from("--of=bar.file"), + String::from("--of=correct.file"), + String::from("--ibs=256"), + String::from("--ibs=1024"), + String::from("--obs=256"), + String::from("--obs=1024"), + String::from("--cbs=1"), + String::from("--cbs=2"), + String::from("--skip=0"), + String::from("--skip=2"), + String::from("--seek=0"), + String::from("--seek=2"), + String::from("--status=none"), + String::from("--status=noxfer"), + String::from("--count=512"), + String::from("--count=1024"), + String::from("--conv=ascii,ucase"), + String::from("--conv=ebcdic,lcase,unblock"), + String::from("--iflag=direct,nocache"), + String::from("--iflag=count_bytes,skip_bytes"), + String::from("--oflag=append,direct"), + String::from("--oflag=append,seek_bytes"), + ]; + + let matches = uu_app().try_get_matches_from(args).unwrap(); + + // if + assert_eq!("correct.file", matches.value_of(options::INFILE).unwrap()); + + // of + assert_eq!("correct.file", matches.value_of(options::OUTFILE).unwrap()); + + // ibs + assert_eq!(1024, parse_ibs(&matches).unwrap()); + + // obs + assert_eq!(1024, parse_obs(&matches).unwrap()); + + // cbs + assert_eq!(2, parse_cbs(&matches).unwrap().unwrap()); + + // status + assert_eq!( + StatusLevel::Noxfer, + parse_status_level(&matches).unwrap().unwrap() + ); + + // skip + assert_eq!( + 200, + parse_skip_amt(&100, &IFlags::default(), &matches) + .unwrap() + .unwrap() + ); + + // seek + assert_eq!( + 200, + parse_seek_amt(&100, &OFlags::default(), &matches) + .unwrap() + .unwrap() + ); + + // conv + let exp = vec![ConvFlag::FmtEtoA, ConvFlag::LCase, ConvFlag::Unblock]; + let act = parse_flag_list::("conv", &matches).unwrap(); + assert_eq!(exp.len(), act.len()); + for cf in &exp { + assert!(exp.contains(cf)); + } + + // count + assert_eq!( + CountType::Bytes(1024), + parse_count( + &IFlags { + count_bytes: true, + ..IFlags::default() + }, + &matches + ) + .unwrap() + .unwrap() + ); + + // iflag + assert_eq!( + IFlags { + count_bytes: true, + skip_bytes: true, + ..IFlags::default() + }, + parse_iflags(&matches).unwrap() + ); + + // oflag + assert_eq!( + OFlags { + seek_bytes: true, + append: true, + ..OFlags::default() + }, + parse_oflags(&matches).unwrap() + ); +} + // ----- IConvFlags/Output ----- #[test] From c6d5eccf6c85420dd99fa2bca7501b292f0416f0 Mon Sep 17 00:00:00 2001 From: Andreas Molzer Date: Tue, 1 Feb 2022 19:53:25 +0100 Subject: [PATCH 028/161] false,true: Resolve formatting nit in About --- src/uu/false/src/false.rs | 10 +++++----- src/uu/true/src/true.rs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/uu/false/src/false.rs b/src/uu/false/src/false.rs index 8b487847d..c6661dc35 100644 --- a/src/uu/false/src/false.rs +++ b/src/uu/false/src/false.rs @@ -8,12 +8,12 @@ use clap::{App, AppSettings, Arg}; use std::io::Write; use uucore::error::{set_exit_code, UResult}; -static ABOUT: &str = " - Returns false, an unsuccessful exit status. +static ABOUT: &str = "\ +Returns false, an unsuccessful exit status. - Immediately returns with the exit status `1`. When invoked with one of the recognized options it - will try to write the help or version text. Any IO error during this operation is diagnosed, yet - the program will also return `1`. +Immediately returns with the exit status `1`. When invoked with one of the recognized options it +will try to write the help or version text. Any IO error during this operation is diagnosed, yet +the program will also return `1`. "; #[uucore::main] diff --git a/src/uu/true/src/true.rs b/src/uu/true/src/true.rs index c3026e684..4a8452db6 100644 --- a/src/uu/true/src/true.rs +++ b/src/uu/true/src/true.rs @@ -8,12 +8,12 @@ use clap::{App, AppSettings, Arg}; use std::io::Write; use uucore::error::{set_exit_code, UResult}; -static ABOUT: &str = " - Returns true, a successful exit status. +static ABOUT: &str = "\ +Returns true, a successful exit status. - Immediately returns with the exit status `0`, except when invoked with one of the recognized - options. In those cases it will try to write the help or version text. Any IO error during this - operation causes the program to return `1` instead. +Immediately returns with the exit status `0`, except when invoked with one of the recognized +options. In those cases it will try to write the help or version text. Any IO error during this +operation causes the program to return `1` instead. "; #[uucore::main] From ea5541db5692022df684084bf6b4be2c58da6352 Mon Sep 17 00:00:00 2001 From: Sam Caldwell Date: Tue, 1 Feb 2022 08:29:22 -0700 Subject: [PATCH 029/161] truncate: add test_invalid_option --- tests/by-util/test_truncate.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/by-util/test_truncate.rs b/tests/by-util/test_truncate.rs index 0ef65ec16..c1e44f605 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -250,11 +250,24 @@ fn test_size_and_reference() { #[test] fn test_error_filename_only() { // truncate: you must specify either '--size' or '--reference' - new_ucmd!().args(&["file"]).fails().stderr_contains( - "error: The following required arguments were not provided: + new_ucmd!() + .args(&["file"]) + .fails() + .code_is(1) + .stderr_contains( + "error: The following required arguments were not provided: --reference --size ", - ); + ); +} + +#[test] +fn test_invalid_option() { + // truncate: cli parsing error returns 1 + new_ucmd!() + .args(&["--this-arg-does-not-exist"]) + .fails() + .code_is(1); } #[test] From f117fd8dd69cff0b821f68e41fb74bb5b72f8fa4 Mon Sep 17 00:00:00 2001 From: Rishi Kumar Ray Date: Wed, 2 Feb 2022 02:40:59 +0530 Subject: [PATCH 030/161] added correct format to ABOUT --- src/uu/base32/src/base32.rs | 14 +++++++------- src/uu/base64/src/base64.rs | 14 +++++++------- src/uu/basenc/src/basenc.rs | 10 +++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/uu/base32/src/base32.rs b/src/uu/base32/src/base32.rs index 6d9759fa4..006a796f0 100644 --- a/src/uu/base32/src/base32.rs +++ b/src/uu/base32/src/base32.rs @@ -12,14 +12,14 @@ use uucore::{encoding::Format, error::UResult}; pub mod base_common; -static ABOUT: &str = " - With no FILE, or when FILE is -, read standard input. +static ABOUT: &str = "\ +With no FILE, or when FILE is -, read standard input. - The data are encoded as described for the base32 alphabet in RFC - 4648. When decoding, the input may contain newlines in addition - to the bytes of the formal base32 alphabet. Use --ignore-garbage - to attempt to recover from any other non-alphabet bytes in the - encoded stream. +The data are encoded as described for the base32 alphabet in RFC +4648. When decoding, the input may contain newlines in addition +to the bytes of the formal base32 alphabet. Use --ignore-garbage +to attempt to recover from any other non-alphabet bytes in the +encoded stream. "; fn usage() -> String { diff --git a/src/uu/base64/src/base64.rs b/src/uu/base64/src/base64.rs index 6d0192df9..20a9f55a5 100644 --- a/src/uu/base64/src/base64.rs +++ b/src/uu/base64/src/base64.rs @@ -13,14 +13,14 @@ use uucore::{encoding::Format, error::UResult}; use std::io::{stdin, Read}; -static ABOUT: &str = " - With no FILE, or when FILE is -, read standard input. +static ABOUT: &str = "\ +With no FILE, or when FILE is -, read standard input. - The data are encoded as described for the base64 alphabet in RFC - 3548. When decoding, the input may contain newlines in addition - to the bytes of the formal base64 alphabet. Use --ignore-garbage - to attempt to recover from any other non-alphabet bytes in the - encoded stream. +The data are encoded as described for the base64 alphabet in RFC +3548. When decoding, the input may contain newlines in addition +to the bytes of the formal base64 alphabet. Use --ignore-garbage +to attempt to recover from any other non-alphabet bytes in the +encoded stream. "; fn usage() -> String { diff --git a/src/uu/basenc/src/basenc.rs b/src/uu/basenc/src/basenc.rs index c21e224da..ef133b151 100644 --- a/src/uu/basenc/src/basenc.rs +++ b/src/uu/basenc/src/basenc.rs @@ -19,12 +19,12 @@ use uucore::{ use std::io::{stdin, Read}; -static ABOUT: &str = " - With no FILE, or when FILE is -, read standard input. +static ABOUT: &str = "\ +With no FILE, or when FILE is -, read standard input. - When decoding, the input may contain newlines in addition to the bytes of - the formal alphabet. Use --ignore-garbage to attempt to recover - from any other non-alphabet bytes in the encoded stream. +When decoding, the input may contain newlines in addition to the bytes of +the formal alphabet. Use --ignore-garbage to attempt to recover +from any other non-alphabet bytes in the encoded stream. "; const ENCODINGS: &[(&str, Format)] = &[ From 39f8329222acf64ff9d5a3193cd8608b46c1f4f4 Mon Sep 17 00:00:00 2001 From: Sam Caldwell Date: Tue, 1 Feb 2022 14:13:52 -0700 Subject: [PATCH 031/161] truncate: use `map_err` instead of `unwrap_or_else` --- src/uu/truncate/src/truncate.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 12f413247..242dd416a 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -116,15 +116,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .override_usage(&usage[..]) .after_help(&long_usage[..]) .try_get_matches_from(args) - .unwrap_or_else(|e| { + .map_err(|e| { e.print().expect("Error writing clap::Error"); match e.kind { - clap::ErrorKind::DisplayHelp | clap::ErrorKind::DisplayVersion => { - std::process::exit(0) - } - _ => std::process::exit(1), + clap::ErrorKind::DisplayHelp | clap::ErrorKind::DisplayVersion => 0, + _ => 1, } - }); + })?; let files: Vec = matches .values_of(options::ARG_FILES) From 587e6d2dede880fda71fcb84de53e54e6eb897dc Mon Sep 17 00:00:00 2001 From: Tillmann Rendel Date: Tue, 1 Feb 2022 23:33:24 +0100 Subject: [PATCH 032/161] Move back to .vscode folder --- .gitattributes => .vscode/.gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .gitattributes => .vscode/.gitattributes (58%) diff --git a/.gitattributes b/.vscode/.gitattributes similarity index 58% rename from .gitattributes rename to .vscode/.gitattributes index 73153b0d3..c050fbbf3 100644 --- a/.gitattributes +++ b/.vscode/.gitattributes @@ -1,2 +1,2 @@ # Configure GitHub to not mark comments in configuration files as errors -.vscode/*.json linguist-language=JSON-with-Comments +*.json linguist-language=jsonc From d26f6f0f2f7bdac8468778d2de938767de699b59 Mon Sep 17 00:00:00 2001 From: Tillmann Rendel Date: Tue, 1 Feb 2022 23:36:50 +0100 Subject: [PATCH 033/161] Format cspell config and tweak comments Mostly to flush the highlighting cache on GitHub, but hopefully it also improves the file. --- .vscode/cSpell.json | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json index 95b6f0485..2ff4d4b7e 100644 --- a/.vscode/cSpell.json +++ b/.vscode/cSpell.json @@ -1,7 +1,12 @@ // `cspell` settings { - "version": "0.1", // Version of the setting file. Always 0.1 - "language": "en", // language - current active spelling language + // version of the setting file (always 0.1) + "version": "0.1", + + // spelling language + "language": "en", + + // custom dictionaries "dictionaries": ["acronyms+names", "jargon", "people", "shell", "workspace"], "dictionaryDefinitions": [ { "name": "acronyms+names", "path": "./cspell.dictionaries/acronyms+names.wordlist.txt" }, @@ -10,10 +15,19 @@ { "name": "shell", "path": "./cspell.dictionaries/shell.wordlist.txt" }, { "name": "workspace", "path": "./cspell.dictionaries/workspace.wordlist.txt" } ], - // ignorePaths - a list of globs to specify which files are to be ignored - "ignorePaths": ["Cargo.lock", "target/**", "tests/**/fixtures/**", "src/uu/dd/test-resources/**", "vendor/**"], - // ignoreWords - a list of words to be ignored (even if they are in the flagWords) + + // files to ignore (globs supported) + "ignorePaths": [ + "Cargo.lock", + "target/**", + "tests/**/fixtures/**", + "src/uu/dd/test-resources/**", + "vendor/**" + ], + + // words to ignore (even if they are in the flagWords) "ignoreWords": [], - // words - list of words to be always considered correct + + // words to always consider correct "words": [] } From 3bfcf78c03fd30d6e5ddac900da9d044b949cebf Mon Sep 17 00:00:00 2001 From: Tillmann Rendel Date: Tue, 1 Feb 2022 23:38:18 +0100 Subject: [PATCH 034/161] Tweak comment in extensions.json To flush the highlighting cache. --- .vscode/extensions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 30b38bfa9..a02baee69 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,7 +2,7 @@ // see for the documentation about the extensions.json format { "recommendations": [ - // Rust language support. + // Rust language support "matklad.rust-analyzer", // `cspell` spell-checker support "streetsidesoftware.code-spell-checker" From f6174dd946bb51338cb45d33aca6320a1c81686c Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 1 Feb 2022 17:34:06 -0500 Subject: [PATCH 035/161] printf: add description and version Adds a version number and brief description to the printf utility in the user documentation --- src/uu/printf/src/printf.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 1e6c5fbd3..1bd53740c 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -9,6 +9,8 @@ use uucore::InvalidEncodingHandling; const VERSION: &str = "version"; const HELP: &str = "help"; +const USAGE: &str = "printf FORMATSTRING [ARGUMENT]..."; +const ABOUT: &str = "Print output based off of the format string and proceeding arguments."; static LONGHELP_LEAD: &str = "printf USAGE: printf FORMATSTRING [ARGUMENT]... @@ -295,7 +297,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app<'a>() -> App<'a> { App::new(uucore::util_name()) - .arg(Arg::new(VERSION).long(VERSION)) - .arg(Arg::new(HELP).long(HELP)) + .version(crate_version!()) + .about(ABOUT) + .override_usage(USAGE) + .arg(Arg::new(HELP).long(HELP).help("Print help information")) + .arg( + Arg::new(VERSION) + .long(VERSION) + .help("Print version information"), + ) .setting(AppSettings::InferLongArgs) } From 29b613a8523c26c6841edc43f411705c5db78358 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 1 Feb 2022 22:22:12 -0500 Subject: [PATCH 036/161] printf: resolve formatting nit in LONGHELP strings Removed 1 preceeding space for LONGHELP_LEAD and 2 preceeding spaces for LONGHELP_BODY --- src/uu/printf/src/printf.rs | 334 ++++++++++++++++++------------------ 1 file changed, 167 insertions(+), 167 deletions(-) diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 1bd53740c..f282dc923 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -13,13 +13,13 @@ const USAGE: &str = "printf FORMATSTRING [ARGUMENT]..."; const ABOUT: &str = "Print output based off of the format string and proceeding arguments."; static LONGHELP_LEAD: &str = "printf - USAGE: printf FORMATSTRING [ARGUMENT]... +USAGE: printf FORMATSTRING [ARGUMENT]... - basic anonymous string templating: +basic anonymous string templating: - prints format string at least once, repeating as long as there are remaining arguments - output prints escaped literals in the format string as character literals - output replaces anonymous fields with the next unused argument, formatted according to the field. +prints format string at least once, repeating as long as there are remaining arguments +output prints escaped literals in the format string as character literals +output replaces anonymous fields with the next unused argument, formatted according to the field. Options: --help display this help and exit @@ -27,239 +27,239 @@ Options: "; static LONGHELP_BODY: &str = " - Prints the , replacing escaped character sequences with character literals - and substitution field sequences with passed arguments +Prints the , replacing escaped character sequences with character literals + and substitution field sequences with passed arguments - literally, with the exception of the below - escaped character sequences, and the substitution sequences described further down. +literally, with the exception of the below + escaped character sequences, and the substitution sequences described further down. - ESCAPE SEQUENCES +ESCAPE SEQUENCES - The following escape sequences, organized here in alphabetical order, - will print the corresponding character literal: +The following escape sequences, organized here in alphabetical order, +will print the corresponding character literal: - \" double quote +\" double quote - \\\\ backslash +\\\\ backslash - \\a alert (BEL) +\\a alert (BEL) - \\b backspace +\\b backspace - \\c End-of-Input +\\c End-of-Input - \\e escape +\\e escape - \\f form feed +\\f form feed - \\n new line +\\n new line - \\r carriage return +\\r carriage return - \\t horizontal tab +\\t horizontal tab - \\v vertical tab +\\v vertical tab - \\NNN byte with value expressed in octal value NNN (1 to 3 digits) - values greater than 256 will be treated +\\NNN byte with value expressed in octal value NNN (1 to 3 digits) + values greater than 256 will be treated - \\xHH byte with value expressed in hexadecimal value NN (1 to 2 digits) +\\xHH byte with value expressed in hexadecimal value NN (1 to 2 digits) - \\uHHHH Unicode (IEC 10646) character with value expressed in hexadecimal value HHHH (4 digits) +\\uHHHH Unicode (IEC 10646) character with value expressed in hexadecimal value HHHH (4 digits) - \\uHHHH Unicode character with value expressed in hexadecimal value HHHH (8 digits) +\\uHHHH Unicode character with value expressed in hexadecimal value HHHH (8 digits) - %% a single % +%% a single % - SUBSTITUTIONS +SUBSTITUTIONS - SUBSTITUTION QUICK REFERENCE +SUBSTITUTION QUICK REFERENCE - Fields +Fields - %s - string - %b - string parsed for literals - second parameter is max length +%s - string +%b - string parsed for literals + second parameter is max length - %c - char - no second parameter +%c - char + no second parameter - %i or %d - 64-bit integer - %u - 64 bit unsigned integer - %x or %X - 64-bit unsigned integer as hex - %o - 64-bit unsigned integer as octal - second parameter is min-width, integer - output below that width is padded with leading zeroes +%i or %d - 64-bit integer +%u - 64 bit unsigned integer +%x or %X - 64-bit unsigned integer as hex +%o - 64-bit unsigned integer as octal + second parameter is min-width, integer + output below that width is padded with leading zeroes - %f or %F - decimal floating point value - %e or %E - scientific notation floating point value - %g or %G - shorter of specially interpreted decimal or SciNote floating point value. - second parameter is - -max places after decimal point for floating point output - -max number of significant digits for scientific notation output +%f or %F - decimal floating point value +%e or %E - scientific notation floating point value +%g or %G - shorter of specially interpreted decimal or SciNote floating point value. + second parameter is + -max places after decimal point for floating point output + -max number of significant digits for scientific notation output - parameterizing fields +parameterizing fields - examples: +examples: - printf '%4.3i' 7 - has a first parameter of 4 - and a second parameter of 3 - will result in ' 007' +printf '%4.3i' 7 +has a first parameter of 4 + and a second parameter of 3 +will result in ' 007' - printf '%.1s' abcde - has no first parameter - and a second parameter of 1 - will result in 'a' +printf '%.1s' abcde +has no first parameter + and a second parameter of 1 +will result in 'a' - printf '%4c' q - has a first parameter of 4 - and no second parameter - will result in ' q' +printf '%4c' q +has a first parameter of 4 + and no second parameter +will result in ' q' - The first parameter of a field is the minimum width to pad the output to - if the output is less than this absolute value of this width, - it will be padded with leading spaces, or, if the argument is negative, - with trailing spaces. the default is zero. +The first parameter of a field is the minimum width to pad the output to + if the output is less than this absolute value of this width, + it will be padded with leading spaces, or, if the argument is negative, + with trailing spaces. the default is zero. - The second parameter of a field is particular to the output field type. - defaults can be found in the full substitution help below +The second parameter of a field is particular to the output field type. + defaults can be found in the full substitution help below - special prefixes to numeric arguments - 0 (e.g. 010) - interpret argument as octal (integer output fields only) - 0x (e.g. 0xABC) - interpret argument as hex (numeric output fields only) - \' (e.g. \'a) - interpret argument as a character constant +special prefixes to numeric arguments + 0 (e.g. 010) - interpret argument as octal (integer output fields only) + 0x (e.g. 0xABC) - interpret argument as hex (numeric output fields only) + \' (e.g. \'a) - interpret argument as a character constant - HOW TO USE SUBSTITUTIONS +HOW TO USE SUBSTITUTIONS - Substitutions are used to pass additional argument(s) into the FORMAT string, to be formatted a - particular way. E.g. +Substitutions are used to pass additional argument(s) into the FORMAT string, to be formatted a +particular way. E.g. - printf 'the letter %X comes before the letter %X' 10 11 + printf 'the letter %X comes before the letter %X' 10 11 - will print +will print - 'the letter A comes before the letter B' + 'the letter A comes before the letter B' - because the substitution field %X means - 'take an integer argument and write it as a hexadecimal number' +because the substitution field %X means +'take an integer argument and write it as a hexadecimal number' - Passing more arguments than are in the format string will cause the format string to be - repeated for the remaining substitutions +Passing more arguments than are in the format string will cause the format string to be + repeated for the remaining substitutions - printf 'it is %i F in %s \n' 22 Portland 25 Boston 27 New York + printf 'it is %i F in %s \n' 22 Portland 25 Boston 27 New York - will print +will print - 'it is 22 F in Portland - it is 25 F in Boston - it is 27 F in Boston - ' - If a format string is printed but there are less arguments remaining - than there are substitution fields, substitution fields without - an argument will default to empty strings, or for numeric fields - the value 0 + 'it is 22 F in Portland + it is 25 F in Boston + it is 27 F in Boston + ' +If a format string is printed but there are less arguments remaining + than there are substitution fields, substitution fields without + an argument will default to empty strings, or for numeric fields + the value 0 - AVAILABLE SUBSTITUTIONS +AVAILABLE SUBSTITUTIONS - This program, like GNU coreutils printf, - interprets a modified subset of the POSIX C printf spec, - a quick reference to substitutions is below. +This program, like GNU coreutils printf, +interprets a modified subset of the POSIX C printf spec, +a quick reference to substitutions is below. - STRING SUBSTITUTIONS - All string fields have a 'max width' parameter - %.3s means 'print no more than three characters of the original input' + STRING SUBSTITUTIONS + All string fields have a 'max width' parameter + %.3s means 'print no more than three characters of the original input' - %s - string + %s - string - %b - escaped string - the string will be checked for any escaped literals from - the escaped literal list above, and translate them to literal characters. - e.g. \\n will be transformed into a newline character. + %b - escaped string - the string will be checked for any escaped literals from + the escaped literal list above, and translate them to literal characters. + e.g. \\n will be transformed into a newline character. - One special rule about %b mode is that octal literals are interpreted differently - In arguments passed by %b, pass octal-interpreted literals must be in the form of \\0NNN - instead of \\NNN. (Although, for legacy reasons, octal literals in the form of \\NNN will - still be interpreted and not throw a warning, you will have problems if you use this for a - literal whose code begins with zero, as it will be viewed as in \\0NNN form.) + One special rule about %b mode is that octal literals are interpreted differently + In arguments passed by %b, pass octal-interpreted literals must be in the form of \\0NNN + instead of \\NNN. (Although, for legacy reasons, octal literals in the form of \\NNN will + still be interpreted and not throw a warning, you will have problems if you use this for a + literal whose code begins with zero, as it will be viewed as in \\0NNN form.) - CHAR SUBSTITUTIONS - The character field does not have a secondary parameter. + CHAR SUBSTITUTIONS + The character field does not have a secondary parameter. - %c - a single character + %c - a single character - INTEGER SUBSTITUTIONS - All integer fields have a 'pad with zero' parameter - %.4i means an integer which if it is less than 4 digits in length, - is padded with leading zeros until it is 4 digits in length. + INTEGER SUBSTITUTIONS + All integer fields have a 'pad with zero' parameter + %.4i means an integer which if it is less than 4 digits in length, + is padded with leading zeros until it is 4 digits in length. - %d or %i - 64-bit integer + %d or %i - 64-bit integer - %u - 64 bit unsigned integer + %u - 64 bit unsigned integer - %x or %X - 64 bit unsigned integer printed in Hexadecimal (base 16) - %X instead of %x means to use uppercase letters for 'a' through 'f' + %x or %X - 64 bit unsigned integer printed in Hexadecimal (base 16) + %X instead of %x means to use uppercase letters for 'a' through 'f' - %o - 64 bit unsigned integer printed in octal (base 8) + %o - 64 bit unsigned integer printed in octal (base 8) - FLOATING POINT SUBSTITUTIONS + FLOATING POINT SUBSTITUTIONS - All floating point fields have a 'max decimal places / max significant digits' parameter - %.10f means a decimal floating point with 7 decimal places past 0 - %.10e means a scientific notation number with 10 significant digits - %.10g means the same behavior for decimal and Sci. Note, respectively, and provides the shorter - of each's output. + All floating point fields have a 'max decimal places / max significant digits' parameter + %.10f means a decimal floating point with 7 decimal places past 0 + %.10e means a scientific notation number with 10 significant digits + %.10g means the same behavior for decimal and Sci. Note, respectively, and provides the shorter + of each's output. - Like with GNU coreutils, the value after the decimal point is these outputs is parsed as a - double first before being rendered to text. For both implementations do not expect meaningful - precision past the 18th decimal place. When using a number of decimal places that is 18 or - higher, you can expect variation in output between GNU coreutils printf and this printf at the - 18th decimal place of +/- 1 + Like with GNU coreutils, the value after the decimal point is these outputs is parsed as a + double first before being rendered to text. For both implementations do not expect meaningful + precision past the 18th decimal place. When using a number of decimal places that is 18 or + higher, you can expect variation in output between GNU coreutils printf and this printf at the + 18th decimal place of +/- 1 - %f - floating point value presented in decimal, truncated and displayed to 6 decimal places by - default. There is not past-double behavior parity with Coreutils printf, values are not - estimated or adjusted beyond input values. + %f - floating point value presented in decimal, truncated and displayed to 6 decimal places by + default. There is not past-double behavior parity with Coreutils printf, values are not + estimated or adjusted beyond input values. - %e or %E - floating point value presented in scientific notation - 7 significant digits by default - %E means use to use uppercase E for the mantissa. + %e or %E - floating point value presented in scientific notation + 7 significant digits by default + %E means use to use uppercase E for the mantissa. - %g or %G - floating point value presented in the shorter of decimal and scientific notation - behaves differently from %f and %E, please see posix printf spec for full details, - some examples of different behavior: + %g or %G - floating point value presented in the shorter of decimal and scientific notation + behaves differently from %f and %E, please see posix printf spec for full details, + some examples of different behavior: - Sci Note has 6 significant digits by default - Trailing zeroes are removed - Instead of being truncated, digit after last is rounded + Sci Note has 6 significant digits by default + Trailing zeroes are removed + Instead of being truncated, digit after last is rounded - Like other behavior in this utility, the design choices of floating point - behavior in this utility is selected to reproduce in exact - the behavior of GNU coreutils' printf from an inputs and outputs standpoint. + Like other behavior in this utility, the design choices of floating point + behavior in this utility is selected to reproduce in exact + the behavior of GNU coreutils' printf from an inputs and outputs standpoint. - USING PARAMETERS - Most substitution fields can be parameterized using up to 2 numbers that can - be passed to the field, between the % sign and the field letter. +USING PARAMETERS + Most substitution fields can be parameterized using up to 2 numbers that can + be passed to the field, between the % sign and the field letter. - The 1st parameter always indicates the minimum width of output, it is useful for creating - columnar output. Any output that would be less than this minimum width is padded with - leading spaces - The 2nd parameter is proceeded by a dot. - You do not have to use parameters + The 1st parameter always indicates the minimum width of output, it is useful for creating + columnar output. Any output that would be less than this minimum width is padded with + leading spaces + The 2nd parameter is proceeded by a dot. + You do not have to use parameters - SPECIAL FORMS OF INPUT - For numeric input, the following additional forms of input are accepted besides decimal: +SPECIAL FORMS OF INPUT + For numeric input, the following additional forms of input are accepted besides decimal: - Octal (only with integer): if the argument begins with a 0 the proceeding characters - will be interpreted as octal (base 8) for integer fields + Octal (only with integer): if the argument begins with a 0 the proceeding characters + will be interpreted as octal (base 8) for integer fields - Hexadecimal: if the argument begins with 0x the proceeding characters will be interpreted - will be interpreted as hex (base 16) for any numeric fields - for float fields, hexadecimal input results in a precision - limit (in converting input past the decimal point) of 10^-15 + Hexadecimal: if the argument begins with 0x the proceeding characters will be interpreted + will be interpreted as hex (base 16) for any numeric fields + for float fields, hexadecimal input results in a precision + limit (in converting input past the decimal point) of 10^-15 - Character Constant: if the argument begins with a single quote character, the first byte - of the next character will be interpreted as an 8-bit unsigned integer. If there are - additional bytes, they will throw an error (unless the environment variable POSIXLY_CORRECT - is set) + Character Constant: if the argument begins with a single quote character, the first byte + of the next character will be interpreted as an 8-bit unsigned integer. If there are + additional bytes, they will throw an error (unless the environment variable POSIXLY_CORRECT + is set) WRITTEN BY : Nathan E. Ross, et al. for the uutils project From 560cd74a639157e85b256542ce9a867d30daf377 Mon Sep 17 00:00:00 2001 From: Hanif Bin Ariffin Date: Wed, 2 Feb 2022 12:49:40 +0800 Subject: [PATCH 037/161] test_ls: Do not rely on the system time of metadata'access time Signed-off-by: Hanif Bin Ariffin --- tests/by-util/test_ls.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index e3fd99e00..f61611390 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -1327,17 +1327,14 @@ fn test_ls_order_time() { // So the order should be 2 3 4 1 for arg in &["-u", "--time=atime", "--time=access", "--time=use"] { let result = scene.ucmd().arg("-t").arg(arg).succeeds(); - let file3_access = at.open("test-3").metadata().unwrap().accessed().unwrap(); - let file4_access = at.open("test-4").metadata().unwrap().accessed().unwrap(); + at.open("test-3").metadata().unwrap().accessed().unwrap(); + at.open("test-4").metadata().unwrap().accessed().unwrap(); // It seems to be dependent on the platform whether the access time is actually set - if file3_access > file4_access { - result.stdout_only("test-3\ntest-4\ntest-2\ntest-1\n"); - } else { - // Access time does not seem to be set on Windows and some other - // systems so the order is 4 3 2 1 - result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n"); - } + #[cfg(unix)] + result.stdout_only("test-3\ntest-4\ntest-2\ntest-1\n"); + #[cfg(windows)] + result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n"); } // test-2 had the last ctime change when the permissions were set From 7e32b6ba17a482d970e78966abffed7a4a2d4ee2 Mon Sep 17 00:00:00 2001 From: Rahul Kadukar Date: Tue, 1 Feb 2022 23:51:48 -0500 Subject: [PATCH 038/161] Added description for hostid --- src/uu/hostid/src/hostid.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/uu/hostid/src/hostid.rs b/src/uu/hostid/src/hostid.rs index 764b7d279..99b800f14 100644 --- a/src/uu/hostid/src/hostid.rs +++ b/src/uu/hostid/src/hostid.rs @@ -12,6 +12,7 @@ use libc::c_long; use uucore::error::UResult; static SYNTAX: &str = "[options]"; +const SUMMARY: &str = "Print the numeric identifier (in hexadecimal) for the current host"; // currently rust libc interface doesn't include gethostid extern "C" { @@ -28,6 +29,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app<'a>() -> App<'a> { App::new(uucore::util_name()) .version(crate_version!()) + .about(SUMMARY) .override_usage(SYNTAX) .setting(AppSettings::InferLongArgs) } From 773ceb5534a0c85ff2e7ca86222b4b037767bad9 Mon Sep 17 00:00:00 2001 From: DevSaab Date: Wed, 2 Feb 2022 10:08:48 -0500 Subject: [PATCH 039/161] Include ABOUT for shuf --- src/uu/shuf/src/shuf.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index a7dcd48e9..da2dfff1b 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -31,6 +31,7 @@ Write a random permutation of the input lines to standard output. With no FILE, or when FILE is -, read standard input. "#; +static ABOUT: &str = "Shuffle the input by outputting a random permutation of input lines. Each output permutation is equally likely."; static TEMPLATE: &str = "Usage: {usage}\nMandatory arguments to long options are mandatory for short options too.\n{options}"; struct Options { @@ -121,6 +122,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app<'a>() -> App<'a> { App::new(uucore::util_name()) .name(NAME) + .about(ABOUT) .version(crate_version!()) .help_template(TEMPLATE) .override_usage(USAGE) From d50c9c3e776b273569b9bc18bce8271ce789a95f Mon Sep 17 00:00:00 2001 From: Eli Youngs Date: Wed, 2 Feb 2022 23:40:26 -0800 Subject: [PATCH 040/161] Fail when copying a directory to a file --- src/uu/cp/src/cp.rs | 5 ++++- tests/by-util/test_cp.rs | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index f8ce6f241..2ffa1e3ca 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1025,7 +1025,10 @@ fn copy_directory( if is_symlink && !options.dereference { copy_link(&path, &local_to_target, symlinked_files)?; } else if path.is_dir() && !local_to_target.exists() { - or_continue!(fs::create_dir_all(local_to_target)); + if target.is_file() { + return Err("cannot overwrite non-directory with directory".into()); + } + fs::create_dir_all(local_to_target)?; } else if !path.is_dir() { if preserve_hard_links { let mut found_hard_link = false; diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 92637dfbe..e194b59ff 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -1444,3 +1444,12 @@ fn test_cp_archive_on_nonexistent_file() { "cp: cannot stat 'nonexistent_file.txt': No such file or directory (os error 2)", ); } +#[test] +fn test_cp_dir_vs_file() { + new_ucmd!() + .arg("-R") + .arg(TEST_COPY_FROM_FOLDER) + .arg(TEST_EXISTING_FILE) + .fails() + .stderr_only("cp: cannot overwrite non-directory with directory"); +} From ff8a83b256b1983c56d85933fa0d869046a503d9 Mon Sep 17 00:00:00 2001 From: Hanif Ariffin Date: Thu, 3 Feb 2022 21:10:39 +0800 Subject: [PATCH 041/161] touch: Better error message when no args is given Matches the behavior of GNU touch ```shell hbina@akarin ~/g/uutils (hbina-realpath-absolute-symlinks)> touch > /dev/null touch: missing file operand Try 'touch --help' for more information. hbina@akarin ~/g/uutils (hbina-realpath-absolute-symlinks) [1]> cargo run --quiet -- touch > /dev/null touch: missing file operand Try 'touch --help' for more information. hbina@akarin ~/g/uutils (hbina-realpath-absolute-symlinks) [1]> cargo run --quiet -- touch 2> /dev/null hbina@akarin ~/g/uutils (hbina-realpath-absolute-symlinks) [1]> touch 2> /dev/null ``` Signed-off-by: Hanif Ariffin --- src/uu/touch/src/touch.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index b1df1aca4..32dd4817d 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -58,7 +58,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().override_usage(&usage[..]).get_matches_from(args); - let files = matches.values_of_os(ARG_FILES).unwrap(); + let files = matches.values_of_os(ARG_FILES).ok_or(USimpleError::new( + 1, + r##"missing file operand +Try 'touch --help' for more information."##, + ))?; let (mut atime, mut mtime) = if let Some(reference) = matches.value_of_os(options::sources::REFERENCE) { From 9cd65c766af5d9996926b8a429d2e442b6a5b2e9 Mon Sep 17 00:00:00 2001 From: Hanif Ariffin Date: Thu, 3 Feb 2022 21:14:56 +0800 Subject: [PATCH 042/161] Add tests Signed-off-by: Hanif Ariffin --- tests/by-util/test_touch.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index e661907cc..dd4a0b6cc 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -530,3 +530,12 @@ fn test_touch_permission_denied_error_msg() { &full_path )); } + +#[test] +fn test_touch_no_args() { + let mut ucmd = new_ucmd!(); + ucmd.fails().stderr_only( + r##"touch: missing file operand +Try 'touch --help' for more information."##, + ); +} From ee721ebf4e4288fc83a3fab3c26bd5a8d2bc6d0e Mon Sep 17 00:00:00 2001 From: snobee Date: Wed, 2 Feb 2022 21:22:28 -0800 Subject: [PATCH 043/161] head: handle multibyte numeric utf-8 chars --- src/uu/head/src/parse.rs | 2 +- tests/by-util/test_head.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/uu/head/src/parse.rs b/src/uu/head/src/parse.rs index 3f1d8ef42..b44a8b69d 100644 --- a/src/uu/head/src/parse.rs +++ b/src/uu/head/src/parse.rs @@ -20,7 +20,7 @@ pub fn parse_obsolete(src: &str) -> Option let mut has_num = false; let mut last_char = 0 as char; for (n, c) in &mut chars { - if c.is_numeric() { + if c.is_digit(10) { has_num = true; num_end = n; } else { diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 246f5b62a..25410d76f 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -306,6 +306,10 @@ fn test_head_invalid_num() { )); } } + new_ucmd!() + .args(&["-c", "-Β³"]) + .fails() + .stderr_is("head: invalid number of bytes: 'Β³'"); } #[test] From 3586465917372aa3a95f677a9c387749cd5a4a85 Mon Sep 17 00:00:00 2001 From: Hanif Bin Ariffin Date: Thu, 3 Feb 2022 20:17:53 +0800 Subject: [PATCH 044/161] dont use is_numeric to check for digits Signed-off-by: Hanif Bin Ariffin --- src/uu/tail/src/parse.rs | 2 +- tests/by-util/test_stat.rs | 6 ++++++ tests/by-util/test_tail.rs | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/uu/tail/src/parse.rs b/src/uu/tail/src/parse.rs index 1c4f36bbd..ea9df9a02 100644 --- a/src/uu/tail/src/parse.rs +++ b/src/uu/tail/src/parse.rs @@ -19,7 +19,7 @@ pub fn parse_obsolete(src: &str) -> Option let mut has_num = false; let mut last_char = 0 as char; for (n, c) in &mut chars { - if c.is_numeric() { + if c.is_digit(10) { has_num = true; num_end = n; } else { diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index 9bbb1c1ca..8c1255a88 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -36,6 +36,12 @@ fn test_group_num() { assert_eq!("", group_num("")); } +#[test] +#[should_panic] +fn test_group_num_panic_if_invalid_numeric_characters() { + group_num("Β³Β³Β³Β³Β³"); +} + #[cfg(test)] mod test_generate_tokens { use super::*; diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index dcdb2e9dc..ebcd29cf5 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -491,6 +491,10 @@ fn test_tail_invalid_num() { )); } } + new_ucmd!() + .args(&["-c", "-Β³"]) + .fails() + .stderr_is("tail: invalid number of bytes: 'Β³'"); } #[test] From 861437addf4dee8cb3ab932cf0b23eb62e548296 Mon Sep 17 00:00:00 2001 From: Hanif Ariffin Date: Thu, 3 Feb 2022 21:45:02 +0800 Subject: [PATCH 045/161] Fix small clippy issue Signed-off-by: Hanif Ariffin --- src/uu/touch/src/touch.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 32dd4817d..e27dbfc18 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -58,11 +58,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().override_usage(&usage[..]).get_matches_from(args); - let files = matches.values_of_os(ARG_FILES).ok_or(USimpleError::new( - 1, - r##"missing file operand + let files = matches.values_of_os(ARG_FILES).ok_or_else(|| { + USimpleError::new( + 1, + r##"missing file operand Try 'touch --help' for more information."##, - ))?; + ) + })?; let (mut atime, mut mtime) = if let Some(reference) = matches.value_of_os(options::sources::REFERENCE) { From e60b581c38eaf195fa3264b84693427fe9645cb7 Mon Sep 17 00:00:00 2001 From: Hanif Ariffin Date: Thu, 3 Feb 2022 23:02:50 +0800 Subject: [PATCH 046/161] test_sort: Preserve the environment variable when executing tests (#3031) * test_sort: Output sorted files to a file with different name Signed-off-by: Hanif Bin Ariffin * Fix the test by saving the environment variable Signed-off-by: Hanif Bin Ariffin Co-authored-by: Hanif Bin Ariffin --- tests/by-util/test_sort.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 5cf622fb8..5ae56b29e 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1066,10 +1066,13 @@ fn test_separator_null() { #[test] fn test_output_is_input() { let input = "a\nb\nc\n"; - let (at, mut cmd) = at_and_ucmd!(); + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; at.touch("file"); at.append("file", input); - cmd.args(&["-m", "-u", "-o", "file", "file", "file", "file"]) + scene + .ucmd_keepenv() + .args(&["-m", "-u", "-o", "file", "file", "file", "file"]) .succeeds(); assert_eq!(at.read("file"), input); } From caad4db712193804f04179bd921140994989c6b1 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Tue, 1 Feb 2022 23:01:34 -0600 Subject: [PATCH 047/161] maint/CICD ~ add MSRV check for '.clippy.toml' --- .github/workflows/CICD.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index aec424312..5fd51f852 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -340,6 +340,13 @@ jobs: ## Confirm MinSRV compatible 'Cargo.lock' # * 'Cargo.lock' is required to be in a format that `cargo` of MinSRV can interpret (eg, v1-format for MinSRV < v1.38) cargo fetch --locked --quiet || { echo "::error file=Cargo.lock::Incompatible (or out-of-date) 'Cargo.lock' file; update using \`cargo +${{ env.RUST_MIN_SRV }} update\`" ; exit 1 ; } + - name: Confirm MinSRV equivalence for '.clippy.toml' + shell: bash + run: | + ## Confirm MinSRV equivalence for '.clippy.toml' + # * ensure '.clippy.toml' MSRV configuration setting is equal to ${{ env.RUST_MIN_SRV }} + CLIPPY_MSRV=$(grep -P "(?i)^\s*msrv\s*=\s*" .clippy.toml | grep -oP "\d+([.]\d+)+") + if [ "${CLIPPY_MSRV}" != "${{ env.RUST_MIN_SRV }}" ]; then { echo "::error file=.clippy.toml::Incorrect MSRV configuration for clippy (found '${CLIPPY_MSRV}'; should be '${{ env.RUST_MIN_SRV }}'); update '.clippy.toml' with 'msrv = \"${{ env.RUST_MIN_SRV }}\"'" ; exit 1 ; } ; fi - name: Info shell: bash run: | From f01c3ef46a55a2a3a12d728a06344ebcabb0b59f Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Tue, 1 Feb 2022 23:01:56 -0600 Subject: [PATCH 048/161] maint/polish ~ whitespace normalization --- .github/workflows/GnuTests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index b36bbcb33..8303ee403 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -63,8 +63,8 @@ jobs: XPASS=$(sed -n "s/.*# XPASS: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1) ERROR=$(sed -n "s/.*# ERROR: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1) if [[ "$TOTAL" -eq 0 || "$TOTAL" -eq 1 ]]; then - echo "Error in the execution, failing early" - exit 1 + echo "Error in the execution, failing early" + exit 1 fi output="GNU tests summary = TOTAL: $TOTAL / PASS: $PASS / FAIL: $FAIL / ERROR: $ERROR" echo "${output}" From f7e31f600873161fe2774a3f93c0989922f33e7c Mon Sep 17 00:00:00 2001 From: snobee Date: Thu, 3 Feb 2022 15:55:37 -0800 Subject: [PATCH 049/161] stat: allow formatting of negative numbers --- src/uu/stat/src/stat.rs | 10 +++++++++- tests/by-util/test_stat.rs | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index e2a0f57ef..38fbc0fec 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -193,11 +193,19 @@ impl ScanUtil for str { } pub fn group_num(s: &str) -> Cow { - assert!(s.chars().all(char::is_numeric)); + let is_negative = s.starts_with('-'); + assert!(is_negative || s.chars().take(1).all(|c| c.is_digit(10))); + assert!(s.chars().skip(1).all(|c| c.is_digit(10))); if s.len() < 4 { return s.into(); } let mut res = String::with_capacity((s.len() - 1) / 3); + let s = if is_negative { + res.push('-'); + &s[1..] + } else { + s + }; let mut alone = (s.len() - 1) % 3 + 1; res.push_str(&s[..alone]); while alone != s.len() { diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index 9bbb1c1ca..dfaaf8add 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -34,6 +34,8 @@ fn test_group_num() { assert_eq!("24", group_num("24")); assert_eq!("4", group_num("4")); assert_eq!("", group_num("")); + assert_eq!("-5", group_num("-5")); + assert_eq!("-1,234", group_num("-1234")); } #[cfg(test)] From 3fbaa79359f1712489f13f5d4273caed36bce40e Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Tue, 1 Feb 2022 22:53:19 -0500 Subject: [PATCH 050/161] dd: add support for 'b' and 'x' multipliers Support the suffix 'b' (multiply by 512) and 'x' (multiply by an arbitrary amount) when specifying numeric arguments to dd. --- src/uu/dd/src/parseargs.rs | 124 ++++++++++++++++++++++++++++++++----- tests/by-util/test_dd.rs | 22 ++++++- 2 files changed, 128 insertions(+), 18 deletions(-) diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index 492ab70cb..915a99344 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -4,7 +4,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore ctty, ctable, iconvflags, oconvflags +// spell-checker:ignore ctty, ctable, iconvflags, oconvflags parseargs #[cfg(test)] mod unit_tests; @@ -12,6 +12,7 @@ mod unit_tests; use super::*; use std::error::Error; use uucore::error::UError; +use uucore::parse_size::ParseSizeError; pub type Matches = ArgMatches; @@ -31,6 +32,25 @@ pub enum ParseError { Unimplemented(String), } +impl ParseError { + /// Replace the argument, if any, with the given string, consuming self. + fn with_arg(self, s: String) -> Self { + match self { + Self::MultipleFmtTable => Self::MultipleFmtTable, + Self::MultipleUCaseLCase => Self::MultipleUCaseLCase, + Self::MultipleBlockUnblock => Self::MultipleBlockUnblock, + Self::MultipleExclNoCreate => Self::MultipleExclNoCreate, + Self::FlagNoMatch(_) => Self::FlagNoMatch(s), + Self::ConvFlagNoMatch(_) => Self::ConvFlagNoMatch(s), + Self::MultiplierStringParseFailure(_) => Self::MultiplierStringParseFailure(s), + Self::MultiplierStringOverflow(_) => Self::MultiplierStringOverflow(s), + Self::BlockUnblockWithoutCBS => Self::BlockUnblockWithoutCBS, + Self::StatusLevelNotRecognized(_) => Self::StatusLevelNotRecognized(s), + Self::Unimplemented(_) => Self::Unimplemented(s), + } + } +} + impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -310,28 +330,76 @@ fn parse_bytes_only(s: &str) -> Result { .map_err(|_| ParseError::MultiplierStringParseFailure(s.to_string())) } +/// Parse a number of bytes from the given string, assuming no `'x'` characters. +/// +/// The `'x'` character means "multiply the number before the `'x'` by +/// the number after the `'x'`". In order to compute the numbers +/// before and after the `'x'`, use this function, which assumes there +/// are no `'x'` characters in the string. +/// +/// A suffix `'c'` means multiply by 1, `'w'` by 2, and `'b'` by +/// 512. You can also use standard block size suffixes like `'k'` for +/// 1024. +/// +/// # Errors +/// +/// If a number cannot be parsed or if the multiplication would cause +/// an overflow. +/// +/// # Examples +/// +/// ```rust,ignore +/// assert_eq!(parse_bytes_no_x("123").unwrap(), 123); +/// assert_eq!(parse_bytes_no_x("2c").unwrap(), 2 * 1); +/// assert_eq!(parse_bytes_no_x("3w").unwrap(), 3 * 2); +/// assert_eq!(parse_bytes_no_x("2b").unwrap(), 2 * 512); +/// assert_eq!(parse_bytes_no_x("2k").unwrap(), 2 * 1024); +/// ``` +fn parse_bytes_no_x(s: &str) -> Result { + let (num, multiplier) = match (s.find('c'), s.rfind('w'), s.rfind('b')) { + (None, None, None) => match uucore::parse_size::parse_size(s) { + Ok(n) => (n, 1), + Err(ParseSizeError::ParseFailure(s)) => { + return Err(ParseError::MultiplierStringParseFailure(s)) + } + Err(ParseSizeError::SizeTooBig(s)) => { + return Err(ParseError::MultiplierStringOverflow(s)) + } + }, + (Some(i), None, None) => (parse_bytes_only(&s[..i])?, 1), + (None, Some(i), None) => (parse_bytes_only(&s[..i])?, 2), + (None, None, Some(i)) => (parse_bytes_only(&s[..i])?, 512), + _ => return Err(ParseError::MultiplierStringParseFailure(s.to_string())), + }; + num.checked_mul(multiplier) + .ok_or_else(|| ParseError::MultiplierStringOverflow(s.to_string())) +} + /// Parse byte and multiplier like 512, 5KiB, or 1G. /// Uses uucore::parse_size, and adds the 'w' and 'c' suffixes which are mentioned /// in dd's info page. fn parse_bytes_with_opt_multiplier(s: &str) -> Result { - if let Some(idx) = s.rfind('c') { - parse_bytes_only(&s[..idx]) - } else if let Some(idx) = s.rfind('w') { - let partial = parse_bytes_only(&s[..idx])?; + // TODO On my Linux system, there seems to be a maximum block size of 4096 bytes: + // + // $ printf "%0.sa" {1..10000} | dd bs=4095 count=1 status=none | wc -c + // 4095 + // $ printf "%0.sa" {1..10000} | dd bs=4k count=1 status=none | wc -c + // 4096 + // $ printf "%0.sa" {1..10000} | dd bs=4097 count=1 status=none | wc -c + // 4096 + // $ printf "%0.sa" {1..10000} | dd bs=5k count=1 status=none | wc -c + // 4096 + // - partial - .checked_mul(2) - .ok_or_else(|| ParseError::MultiplierStringOverflow(s.to_string())) - } else { - uucore::parse_size::parse_size(s).map_err(|e| match e { - uucore::parse_size::ParseSizeError::ParseFailure(s) => { - ParseError::MultiplierStringParseFailure(s) - } - uucore::parse_size::ParseSizeError::SizeTooBig(s) => { - ParseError::MultiplierStringOverflow(s) - } - }) + // Split on the 'x' characters. Each component will be parsed + // individually, then multiplied together. + let mut total = 1; + for part in s.split('x') { + let num = parse_bytes_no_x(part).map_err(|e| e.with_arg(s.to_string()))?; + total *= num; } + + Ok(total) } pub fn parse_ibs(matches: &Matches) -> Result { @@ -689,3 +757,25 @@ pub fn parse_input_non_ascii(matches: &Matches) -> Result { Ok(false) } } + +#[cfg(test)] +mod tests { + + use crate::parseargs::parse_bytes_with_opt_multiplier; + + #[test] + fn test_parse_bytes_with_opt_multiplier() { + assert_eq!(parse_bytes_with_opt_multiplier("123").unwrap(), 123); + assert_eq!(parse_bytes_with_opt_multiplier("123c").unwrap(), 123 * 1); + assert_eq!(parse_bytes_with_opt_multiplier("123w").unwrap(), 123 * 2); + assert_eq!(parse_bytes_with_opt_multiplier("123b").unwrap(), 123 * 512); + assert_eq!(parse_bytes_with_opt_multiplier("123x3").unwrap(), 123 * 3); + assert_eq!(parse_bytes_with_opt_multiplier("123k").unwrap(), 123 * 1024); + assert_eq!(parse_bytes_with_opt_multiplier("1x2x3").unwrap(), 1 * 2 * 3); + assert_eq!( + parse_bytes_with_opt_multiplier("1wx2cx3w").unwrap(), + (1 * 2) * (2 * 1) * (3 * 2) + ); + assert!(parse_bytes_with_opt_multiplier("123asdf").is_err()); + } +} diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index e73fe0673..e9a1f9468 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -1,4 +1,4 @@ -// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, availible, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat abcdefghijklm +// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, availible, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat abcdefghijklm abcdefghi use crate::common::util::*; @@ -178,6 +178,26 @@ fn test_stdin_stdout_count_w_multiplier() { .success(); } +#[test] +fn test_b_multiplier() { + // "2b" means 2 * 512, which is 1024. + new_ucmd!() + .args(&["bs=2b", "count=1"]) + .pipe_in("a".repeat(1025)) + .succeeds() + .stdout_is("a".repeat(1024)); +} + +#[test] +fn test_x_multiplier() { + // "2x3" means 2 * 3, which is 6. + new_ucmd!() + .args(&["bs=2x3", "count=1"]) + .pipe_in("abcdefghi") + .succeeds() + .stdout_is("abcdef"); +} + #[test] fn test_final_stats_noxfer() { new_ucmd!() From 639971e5200ea213c6f9f8dacec10b19b08a61e3 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Thu, 3 Feb 2022 23:19:14 -0500 Subject: [PATCH 051/161] df: refactor function for parsing CLI args Add a `Options::from()` function to collect the code for parsing an `Options` object from the `clap::ArgMatches` object. --- src/uu/df/src/df.rs | 97 +++++++++++++++++---------------------------- 1 file changed, 37 insertions(+), 60 deletions(-) diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 77deeb6df..07aa82dc1 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -12,17 +12,17 @@ use uucore::error::UResult; use uucore::fsext::statfs_fn; use uucore::fsext::{read_fs_list, FsUsage, MountInfo}; -use clap::{crate_version, App, AppSettings, Arg}; +use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; use number_prefix::NumberPrefix; use std::cell::Cell; use std::collections::HashMap; use std::collections::HashSet; - use std::error::Error; #[cfg(unix)] use std::ffi::CString; use std::fmt::Display; +use std::iter::FromIterator; #[cfg(unix)] use std::mem; @@ -69,6 +69,27 @@ struct Options { fs_selector: FsSelector, } +impl Options { + /// Convert command-line arguments into [`Options`]. + fn from(matches: &ArgMatches) -> Self { + Self { + show_local_fs: matches.is_present(OPT_LOCAL), + show_all_fs: matches.is_present(OPT_ALL), + show_listed_fs: false, + show_fs_type: matches.is_present(OPT_PRINT_TYPE), + show_inode_instead: matches.is_present(OPT_INODES), + human_readable_base: if matches.is_present(OPT_HUMAN_READABLE) { + 1024 + } else if matches.is_present(OPT_HUMAN_READABLE_2) { + 1000 + } else { + -1 + }, + fs_selector: FsSelector::from(matches), + } + } +} + #[derive(Debug, Clone)] struct Filesystem { mount_info: MountInfo, @@ -80,18 +101,19 @@ fn usage() -> String { } impl FsSelector { - fn new() -> Self { - Self::default() - } - - #[inline(always)] - fn include(&mut self, fs_type: String) { - self.include.insert(fs_type); - } - - #[inline(always)] - fn exclude(&mut self, fs_type: String) { - self.exclude.insert(fs_type); + /// Convert command-line arguments into a [`FsSelector`]. + /// + /// This function reads the include and exclude sets from + /// [`ArgMatches`] and returns the corresponding [`FsSelector`] + /// instance. + fn from(matches: &ArgMatches) -> Self { + let include = HashSet::from_iter(matches.values_of_lossy(OPT_TYPE).unwrap_or_default()); + let exclude = HashSet::from_iter( + matches + .values_of_lossy(OPT_EXCLUDE_TYPE) + .unwrap_or_default(), + ); + Self { include, exclude } } fn should_select(&self, fs_type: &str) -> bool { @@ -102,24 +124,6 @@ impl FsSelector { } } -impl Options { - fn new() -> Self { - Self { - show_local_fs: false, - show_all_fs: false, - show_listed_fs: false, - show_fs_type: false, - show_inode_instead: false, - // block_size: match env::var("BLOCKSIZE") { - // Ok(size) => size.parse().unwrap(), - // Err(_) => 512, - // }, - human_readable_base: -1, - fs_selector: FsSelector::new(), - } - } -} - impl Filesystem { // TODO: resolve uuid in `mount_info.dev_name` if exists fn new(mount_info: MountInfo) -> Option { @@ -293,34 +297,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } - let mut opt = Options::new(); - if matches.is_present(OPT_LOCAL) { - opt.show_local_fs = true; - } - if matches.is_present(OPT_ALL) { - opt.show_all_fs = true; - } - if matches.is_present(OPT_INODES) { - opt.show_inode_instead = true; - } - if matches.is_present(OPT_PRINT_TYPE) { - opt.show_fs_type = true; - } - if matches.is_present(OPT_HUMAN_READABLE) { - opt.human_readable_base = 1024; - } - if matches.is_present(OPT_HUMAN_READABLE_2) { - opt.human_readable_base = 1000; - } - for fs_type in matches.values_of_lossy(OPT_TYPE).unwrap_or_default() { - opt.fs_selector.include(fs_type.to_owned()); - } - for fs_type in matches - .values_of_lossy(OPT_EXCLUDE_TYPE) - .unwrap_or_default() - { - opt.fs_selector.exclude(fs_type.to_owned()); - } + let opt = Options::from(&matches); let fs_list = filter_mount_list(read_fs_list(), &paths, &opt) .into_iter() From ae755bb9bdcf63709a74ff0b66aa1aa30881cca0 Mon Sep 17 00:00:00 2001 From: Guilherme Augusto de Souza <98732503+lguist@users.noreply.github.com> Date: Fri, 4 Feb 2022 06:28:15 -0300 Subject: [PATCH 052/161] test: add version and about (#3011) --- src/uu/test/src/test.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index 1a3f4139e..cc7437bff 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -10,7 +10,7 @@ mod parser; -use clap::{crate_version, App, AppSettings}; +use clap::{crate_version, App}; use parser::{parse, Operator, Symbol, UnaryOperator}; use std::ffi::{OsStr, OsString}; use uucore::display::Quotable; @@ -86,10 +86,14 @@ NOTE: your shell may have its own version of test and/or [, which usually supers the version described here. Please refer to your shell's documentation for details about the options it supports."; +const ABOUT: &str = "Check file types and compare values."; + pub fn uu_app<'a>() -> App<'a> { App::new(uucore::util_name()) - .setting(AppSettings::DisableHelpFlag) - .setting(AppSettings::DisableVersionFlag) + .version(crate_version!()) + .about(ABOUT) + .override_usage(USAGE) + .after_help(AFTER_HELP) } #[uucore::main] @@ -104,6 +108,7 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { // Let clap pretty-print help and version App::new(binary_name) .version(crate_version!()) + .about(ABOUT) .override_usage(USAGE) .after_help(AFTER_HELP) // Disable printing of -h and -v as valid alternatives for --help and --version, From 793d3dd97c37c5394b152fdace5fdf66d972247c Mon Sep 17 00:00:00 2001 From: Hanif Ariffin Date: Fri, 4 Feb 2022 18:47:19 +0800 Subject: [PATCH 053/161] test_printf: add test for additional escape (\c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using this escaped character will cause `printf` to stop generating characters. For instance, ```rust hbina@akarin ~/g/uutils (hbina-add-test-for-additional-escape)> cargo run --quiet -- printf "%s\c%s" a b a⏎ ``` Signed-off-by: Hanif Ariffin --- tests/by-util/test_printf.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/by-util/test_printf.rs b/tests/by-util/test_printf.rs index b5c9dc3ed..f8f941ad8 100644 --- a/tests/by-util/test_printf.rs +++ b/tests/by-util/test_printf.rs @@ -429,3 +429,11 @@ fn sub_any_specifiers_after_second_param() { .succeeds() .stdout_only("3"); } + +#[test] +fn stop_after_additional_escape() { + new_ucmd!() + .args(&["A%sC\\cD%sF", "B", "E"]) //spell-checker:disable-line + .succeeds() + .stdout_only("ABC"); +} From 6a6875012eed330e922e2d5566016a9427f6b1ef Mon Sep 17 00:00:00 2001 From: Allan Silva Date: Sun, 30 Jan 2022 01:18:32 -0300 Subject: [PATCH 054/161] wc: implement files0-from option When this option is present, the files argument is not processed. This option processes the file list from provided file, splitting them by the ascii NUL (\0) character. When files0-from is '-', the file list is processed from stdin. --- src/uu/wc/src/wc.rs | 121 ++++++++++++++++--- tests/by-util/test_wc.rs | 54 +++++++++ tests/fixtures/wc/files0_list.txt | Bin 0 -> 53 bytes tests/fixtures/wc/files0_list_with_stdin.txt | Bin 0 -> 31 bytes 4 files changed, 155 insertions(+), 20 deletions(-) create mode 100644 tests/fixtures/wc/files0_list.txt create mode 100644 tests/fixtures/wc/files0_list_with_stdin.txt diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 59aea1542..2afbe4e21 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -20,12 +20,15 @@ use word_count::{TitledWordCount, WordCount}; use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; use std::cmp::max; +use std::error::Error; +use std::ffi::OsStr; +use std::fmt::Display; use std::fs::{self, File}; -use std::io::{self, Write}; +use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; use uucore::display::{Quotable, Quoted}; -use uucore::error::{UResult, USimpleError}; +use uucore::error::{UError, UResult, USimpleError}; /// The minimum character width for formatting counts when reading from stdin. const MINIMUM_WIDTH: usize = 7; @@ -83,12 +86,14 @@ more than one FILE is specified."; pub mod options { pub static BYTES: &str = "bytes"; pub static CHAR: &str = "chars"; + pub static FILES0_FROM: &str = "files0-from"; pub static LINES: &str = "lines"; pub static MAX_LINE_LENGTH: &str = "max-line-length"; pub static WORDS: &str = "words"; } static ARG_FILES: &str = "files"; +static STDIN_REPR: &str = "-"; fn usage() -> String { format!( @@ -115,12 +120,22 @@ enum Input { Stdin(StdinKind), } +impl From<&OsStr> for Input { + fn from(input: &OsStr) -> Self { + if input == STDIN_REPR { + Input::Stdin(StdinKind::Explicit) + } else { + Input::Path(input.into()) + } + } +} + impl Input { /// Converts input to title that appears in stats. fn to_title(&self) -> Option<&Path> { match self { Input::Path(path) => Some(path), - Input::Stdin(StdinKind::Explicit) => Some("-".as_ref()), + Input::Stdin(StdinKind::Explicit) => Some(STDIN_REPR.as_ref()), Input::Stdin(StdinKind::Implicit) => None, } } @@ -133,29 +148,43 @@ impl Input { } } +#[derive(Debug)] +enum WcError { + FilesDisabled(String), + StdinReprNotAllowed(String), +} + +impl UError for WcError { + fn code(&self) -> i32 { + match self { + WcError::FilesDisabled(_) | WcError::StdinReprNotAllowed(_) => 1, + } + } + + fn usage(&self) -> bool { + matches!(self, WcError::FilesDisabled(_)) + } +} + +impl Error for WcError {} + +impl Display for WcError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WcError::FilesDisabled(message) | WcError::StdinReprNotAllowed(message) => { + write!(f, "{}", message) + } + } + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let usage = usage(); let matches = uu_app().override_usage(&usage[..]).get_matches_from(args); - let mut inputs: Vec = matches - .values_of_os(ARG_FILES) - .map(|v| { - v.map(|i| { - if i == "-" { - Input::Stdin(StdinKind::Explicit) - } else { - Input::Path(i.into()) - } - }) - .collect() - }) - .unwrap_or_default(); - - if inputs.is_empty() { - inputs.push(Input::Stdin(StdinKind::Implicit)); - } + let inputs = inputs(&matches)?; let settings = Settings::new(&matches); @@ -179,6 +208,17 @@ pub fn uu_app<'a>() -> App<'a> { .long(options::CHAR) .help("print the character counts"), ) + .arg( + Arg::new(options::FILES0_FROM) + .long(options::FILES0_FROM) + .takes_value(true) + .value_name("F") + .help( + "read input from the files specified by + NUL-terminated names in file F; + If F is - then read names from standard input", + ), + ) .arg( Arg::new(options::LINES) .short('l') @@ -205,6 +245,47 @@ pub fn uu_app<'a>() -> App<'a> { ) } +fn inputs(matches: &ArgMatches) -> UResult> { + match matches.values_of_os(ARG_FILES) { + Some(os_values) => { + if matches.is_present(options::FILES0_FROM) { + return Err(WcError::FilesDisabled( + "file operands cannot be combined with --files0-from".into(), + ) + .into()); + } + + Ok(os_values.map(Input::from).collect()) + } + None => match matches.value_of(options::FILES0_FROM) { + Some(files_0_from) => create_paths_from_files0(files_0_from), + None => Ok(vec![Input::Stdin(StdinKind::Implicit)]), + }, + } +} + +fn create_paths_from_files0(files_0_from: &str) -> UResult> { + let mut paths = String::new(); + let read_from_stdin = files_0_from == STDIN_REPR; + + if read_from_stdin { + io::stdin().lock().read_to_string(&mut paths)?; + } else { + File::open(files_0_from)?.read_to_string(&mut paths)?; + } + + let paths: Vec<&str> = paths.split_terminator('\0').collect(); + + if read_from_stdin && paths.contains(&STDIN_REPR) { + return Err(WcError::StdinReprNotAllowed( + "when reading file names from stdin, no file name of '-' allowed".into(), + ) + .into()); + } + + Ok(paths.iter().map(OsStr::new).map(Input::from).collect()) +} + fn word_count_from_reader( mut reader: T, settings: &Settings, diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index 5c4763f99..39689afc9 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -245,3 +245,57 @@ fn test_files_from_pseudo_filesystem() { let result = new_ucmd!().arg("-c").arg("/proc/version").succeeds(); assert_ne!(result.stdout_str(), "0 /proc/version\n"); } + +#[test] +fn test_files0_disabled_files_argument() { + const MSG: &str = "file operands cannot be combined with --files0-from"; + new_ucmd!() + .args(&["--files0-from=files0_list.txt"]) + .arg("lorem_ipsum.txt") + .fails() + .stderr_contains(MSG) + .stdout_is(""); +} + +#[test] +fn test_files0_from() { + new_ucmd!() + .args(&["--files0-from=files0_list.txt"]) + .run() + .stdout_is( + " 13 109 772 lorem_ipsum.txt\n 18 204 1115 moby_dick.txt\n 5 57 302 \ + alice_in_wonderland.txt\n 36 370 2189 total\n", + ); +} + +#[test] +fn test_files0_from_with_stdin() { + new_ucmd!() + .args(&["--files0-from=-"]) + .pipe_in("lorem_ipsum.txt") + .run() + .stdout_is(" 13 109 772 lorem_ipsum.txt\n"); +} + +#[test] +fn test_files0_from_with_stdin_in_file() { + new_ucmd!() + .args(&["--files0-from=files0_list_with_stdin.txt"]) + .pipe_in_fixture("alice_in_wonderland.txt") + .run() + .stdout_is( + " 13 109 772 lorem_ipsum.txt\n 18 204 1115 moby_dick.txt\n 5 57 302 \ + -\n 36 370 2189 total\n", + ); +} + +#[test] +fn test_files0_from_with_stdin_try_read_from_stdin() { + const MSG: &str = "when reading file names from stdin, no file name of '-' allowed"; + new_ucmd!() + .args(&["--files0-from=-"]) + .pipe_in("-") + .fails() + .stderr_contains(MSG) + .stdout_is(""); +} diff --git a/tests/fixtures/wc/files0_list.txt b/tests/fixtures/wc/files0_list.txt new file mode 100644 index 0000000000000000000000000000000000000000..5c7af28f0d7f4d299f800105b5031cc0bc1f2fe2 GIT binary patch literal 53 zcmd1FFG|gg&nze|&DATZC}GIWPpXVh$xO}$^AdA1lT+g}^Ww|%^HNfaauV}WK;i%~ CJQT?Q literal 0 HcmV?d00001 diff --git a/tests/fixtures/wc/files0_list_with_stdin.txt b/tests/fixtures/wc/files0_list_with_stdin.txt new file mode 100644 index 0000000000000000000000000000000000000000..b938a78677a223a828e066f8d3e565e9c8488e0f GIT binary patch literal 31 icmd1FFG|gg&nze|&DATZC}GIWPpXVh$xO}$^K=2lObe#~ literal 0 HcmV?d00001 From 5e790918ef556d5afcdef324e2416294d3a29ce2 Mon Sep 17 00:00:00 2001 From: ndd7xv Date: Fri, 4 Feb 2022 21:43:21 -0500 Subject: [PATCH 055/161] printf: use clap default help and version --- src/uu/printf/src/printf.rs | 42 +++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index f282dc923..a30d18c53 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -11,22 +11,13 @@ const VERSION: &str = "version"; const HELP: &str = "help"; const USAGE: &str = "printf FORMATSTRING [ARGUMENT]..."; const ABOUT: &str = "Print output based off of the format string and proceeding arguments."; -static LONGHELP_LEAD: &str = "printf - -USAGE: printf FORMATSTRING [ARGUMENT]... - +const AFTER_HELP: &str = " basic anonymous string templating: prints format string at least once, repeating as long as there are remaining arguments output prints escaped literals in the format string as character literals output replaces anonymous fields with the next unused argument, formatted according to the field. -Options: - --help display this help and exit - --version output version information and exit - -"; -static LONGHELP_BODY: &str = " Prints the , replacing escaped character sequences with character literals and substitution field sequences with passed arguments @@ -273,32 +264,36 @@ COPYRIGHT : "; +mod options { + pub const FORMATSTRING: &str = "FORMATSTRING"; + pub const ARGUMENT: &str = "ARGUMENT"; +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = args .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); + let matches = uu_app().get_matches_from(args); - if args.len() <= 1 { - return Err(UUsageError::new(1, "missing operand")); - } - let formatstr = &args[1]; + let format_string = matches + .value_of(options::FORMATSTRING) + .ok_or_else(|| UUsageError::new(1, "missing operand"))?; + let values: Vec = match matches.values_of(options::ARGUMENT) { + Some(s) => s.map(|s| s.to_string()).collect(), + None => vec![], + }; - if formatstr == "--help" { - print!("{} {}", LONGHELP_LEAD, LONGHELP_BODY); - } else if formatstr == "--version" { - println!("{} {}", uucore::util_name(), crate_version!()); - } else { - let printf_args = &args[2..]; - memo::Memo::run_all(formatstr, printf_args); - } + memo::Memo::run_all(format_string, &values[..]); Ok(()) } pub fn uu_app<'a>() -> App<'a> { App::new(uucore::util_name()) + .setting(AppSettings::AllowHyphenValues) .version(crate_version!()) .about(ABOUT) + .after_help(AFTER_HELP) .override_usage(USAGE) .arg(Arg::new(HELP).long(HELP).help("Print help information")) .arg( @@ -306,5 +301,6 @@ pub fn uu_app<'a>() -> App<'a> { .long(VERSION) .help("Print version information"), ) - .setting(AppSettings::InferLongArgs) + .arg(Arg::new(options::FORMATSTRING)) + .arg(Arg::new(options::ARGUMENT).multiple_occurrences(true)) } From 162f85773e11b0b0b016d3fdf9379c8fdd6f8530 Mon Sep 17 00:00:00 2001 From: Eli Youngs Date: Sat, 5 Feb 2022 00:43:09 -0800 Subject: [PATCH 056/161] printf: Support leading zeroes with %0n formatting --- src/uucore/src/lib/features/tokenize/sub.rs | 11 ++++++++++- tests/by-util/test_printf.rs | 8 ++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/uucore/src/lib/features/tokenize/sub.rs b/src/uucore/src/lib/features/tokenize/sub.rs index 6f9196d93..ac471ae3e 100644 --- a/src/uucore/src/lib/features/tokenize/sub.rs +++ b/src/uucore/src/lib/features/tokenize/sub.rs @@ -60,6 +60,7 @@ pub struct Sub { field_char: char, field_type: FieldType, orig: String, + prefix_char: char, } impl Sub { pub fn new( @@ -67,6 +68,7 @@ impl Sub { second_field: CanAsterisk>, field_char: char, orig: String, + prefix_char: char, ) -> Self { // for more dry printing, field characters are grouped // in initialization of token. @@ -90,6 +92,7 @@ impl Sub { field_char, field_type, orig, + prefix_char, } } } @@ -126,6 +129,11 @@ impl SubParser { fn build_token(parser: Self) -> Box { // not a self method so as to allow move of sub-parser vals. // return new Sub struct as token + let prefix_char = match &parser.min_width_tmp { + Some(width) if width.starts_with('0') => '0', + _ => ' ', + }; + let t: Box = Box::new(Sub::new( if parser.min_width_is_asterisk { CanAsterisk::Asterisk @@ -139,6 +147,7 @@ impl SubParser { }, parser.field_char.unwrap(), parser.text_so_far, + prefix_char, )); t } @@ -394,7 +403,7 @@ impl token::Token for Sub { final_str.push_str(&pre_min_width); } for _ in 0..diff { - final_str.push(' '); + final_str.push(self.prefix_char); } if pad_before { final_str.push_str(&pre_min_width); diff --git a/tests/by-util/test_printf.rs b/tests/by-util/test_printf.rs index f8f941ad8..b3e608dc9 100644 --- a/tests/by-util/test_printf.rs +++ b/tests/by-util/test_printf.rs @@ -437,3 +437,11 @@ fn stop_after_additional_escape() { .succeeds() .stdout_only("ABC"); } + +#[test] +fn sub_float_leading_zeroes() { + new_ucmd!() + .args(&["%010f", "1"]) + .succeeds() + .stdout_only("001.000000"); +} From f39b861469c8d7598741864f2602c15a5496287b Mon Sep 17 00:00:00 2001 From: Hanif Ariffin Date: Sat, 5 Feb 2022 19:12:05 +0800 Subject: [PATCH 057/161] Refactor padding calculations into a function Signed-off-by: Hanif Ariffin --- src/uu/ls/src/ls.rs | 184 +++++++++++++++++++++++--------------------- 1 file changed, 97 insertions(+), 87 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 7fdec53f0..e86744955 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1275,9 +1275,9 @@ only ignore '.' and '..'.", ) } -/// Represents a Path along with it's associated data -/// Any data that will be reused several times makes sense to be added to this structure -/// Caching data here helps eliminate redundant syscalls to fetch same information +/// Represents a Path along with it's associated data. +/// Any data that will be reused several times makes sense to be added to this structure. +/// Caching data here helps eliminate redundant syscalls to fetch same information. #[derive(Debug)] struct PathData { // Result got from symlink_metadata() or metadata() based on config @@ -1678,92 +1678,10 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter, +) -> PaddingCollection { + let ( + mut longest_inode_len, + mut longest_link_count_len, + mut longest_uname_len, + mut longest_group_len, + mut longest_context_len, + mut longest_size_len, + mut longest_major_len, + mut longest_minor_len, + ) = (1, 1, 1, 1, 1, 1, 1, 1); + + for item in items { + let context_len = item.security_context.len(); + let (link_count_len, uname_len, group_len, size_len, major_len, minor_len, inode_len) = + display_dir_entry_size(item, config, out); + longest_inode_len = inode_len.max(longest_inode_len); + longest_link_count_len = link_count_len.max(longest_link_count_len); + longest_uname_len = uname_len.max(longest_uname_len); + longest_group_len = group_len.max(longest_group_len); + if config.context { + longest_context_len = context_len.max(longest_context_len); + } + if items.len() == 1usize { + longest_size_len = 0usize; + longest_major_len = 0usize; + longest_minor_len = 0usize; + } else { + longest_major_len = major_len.max(longest_major_len); + longest_minor_len = minor_len.max(longest_minor_len); + longest_size_len = size_len + .max(longest_size_len) + .max(longest_major_len + longest_minor_len + 2usize); + } + } + + PaddingCollection { + longest_inode_len, + longest_link_count_len, + longest_uname_len, + longest_group_len, + longest_context_len, + longest_size_len, + longest_major_len, + longest_minor_len, + } +} + +#[cfg(not(unix))] +fn calculate_paddings( + items: &[PathData], + config: &Config, + out: &mut BufWriter, +) -> PaddingCollection { + let ( + mut longest_link_count_len, + mut longest_uname_len, + mut longest_group_len, + mut longest_context_len, + mut longest_size_len, + ) = (1, 1, 1, 1, 1); + + for item in items { + let context_len = item.security_context.len(); + let (link_count_len, uname_len, group_len, size_len, _major_len, _minor_len, _inode_len) = + display_dir_entry_size(item, config, out); + longest_link_count_len = link_count_len.max(longest_link_count_len); + longest_uname_len = uname_len.max(longest_uname_len); + longest_group_len = group_len.max(longest_group_len); + if config.context { + longest_context_len = context_len.max(longest_context_len); + } + longest_size_len = size_len.max(longest_size_len); + } + + PaddingCollection { + longest_inode_len, + longest_link_count_len, + longest_uname_len, + longest_group_len, + longest_context_len, + longest_size_len, + longest_major_len, + longest_minor_len, + } +} From e35b93156ab31cb7e563ddbb342df4aa52787ef4 Mon Sep 17 00:00:00 2001 From: Hanif Ariffin Date: Sat, 5 Feb 2022 19:30:39 +0800 Subject: [PATCH 058/161] Propagate all write and (most) flush errors Signed-off-by: Hanif Ariffin --- src/uu/ls/src/ls.rs | 127 +++++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 60 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index e86744955..f118a7594 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1379,7 +1379,8 @@ impl PathData { // if not, check if we can use Path metadata match get_metadata(self.p_buf.as_path(), self.must_dereference) { Err(err) => { - let _ = out.flush(); + // FIXME: A bit tricky to propagate the result here + out.flush().unwrap(); let errno = err.raw_os_error().unwrap_or(1i32); // a bad fd will throw an error when dereferenced, // but GNU will not throw an error until a bad fd "dir" @@ -1443,7 +1444,7 @@ fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { sort_entries(&mut files, config, &mut out); sort_entries(&mut dirs, config, &mut out); - display_items(&files, config, &mut out); + display_items(&files, config, &mut out)?; for (pos, path_data) in dirs.iter().enumerate() { // Do read_dir call here to match GNU semantics by printing @@ -1451,7 +1452,7 @@ fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { let read_dir = match fs::read_dir(&path_data.p_buf) { Err(err) => { // flush stdout buffer before the error to preserve formatting and order - let _ = out.flush(); + out.flush()?; show!(LsError::IOErrorContext(err, path_data.p_buf.clone())); continue; } @@ -1461,12 +1462,12 @@ fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { // Print dir heading - name... 'total' comes after error display if initial_locs_len > 1 || config.recursive { if pos.eq(&0usize) && files.is_empty() { - let _ = writeln!(out, "{}:", path_data.p_buf.display()); + writeln!(out, "{}:", path_data.p_buf.display())?; } else { - let _ = writeln!(out, "\n{}:", path_data.p_buf.display()); + writeln!(out, "\n{}:", path_data.p_buf.display())?; } } - enter_directory(path_data, read_dir, config, &mut out); + enter_directory(path_data, read_dir, config, &mut out)?; } Ok(()) @@ -1540,7 +1541,7 @@ fn enter_directory( read_dir: ReadDir, config: &Config, out: &mut BufWriter, -) { +) -> UResult<()> { // Create vec of entries with initial dot files let mut entries: Vec = if config.files == Files::All { vec![ @@ -1570,7 +1571,7 @@ fn enter_directory( let dir_entry = match raw_entry { Ok(path) => path, Err(err) => { - let _ = out.flush(); + out.flush()?; show!(LsError::IOError(err)); continue; } @@ -1588,10 +1589,10 @@ fn enter_directory( // Print total after any error display if config.format == Format::Long { - display_total(&entries, config, out); + display_total(&entries, config, out)?; } - display_items(&entries, config, out); + display_items(&entries, config, out)?; if config.recursive { for e in entries @@ -1603,17 +1604,19 @@ fn enter_directory( { match fs::read_dir(&e.p_buf) { Err(err) => { - let _ = out.flush(); + out.flush()?; show!(LsError::IOErrorContext(err, e.p_buf.clone())); continue; } Ok(rd) => { - let _ = writeln!(out, "\n{}:", e.p_buf.display()); - enter_directory(e, rd, config, out); + writeln!(out, "\n{}:", e.p_buf.display())?; + enter_directory(e, rd, config, out)?; } } } } + + Ok(()) } fn get_metadata(p_buf: &Path, dereference: bool) -> std::io::Result { @@ -1661,7 +1664,7 @@ fn pad_right(string: &str, count: usize) -> String { format!("{:) { +fn display_total(items: &[PathData], config: &Config, out: &mut BufWriter) -> UResult<()> { let mut total_size = 0; for item in items { total_size += item @@ -1669,19 +1672,19 @@ fn display_total(items: &[PathData], config: &Config, out: &mut BufWriter) { +fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter) -> UResult<()> { // `-Z`, `--context`: // Display the SELinux security context or '?' if none is found. When used with the `-l` // option, print the security context to the left of the size column. if config.format == Format::Long { let padding_collection = calculate_padding_collection(items, config, out); - for item in items { - display_item_long(item, &padding_collection, config, out); + display_item_long(item, &padding_collection, config, out)?; } } else { let mut longest_context_len = 1; @@ -1718,13 +1721,13 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter display_grid(names, config.width, Direction::TopToBottom, out), - Format::Across => display_grid(names, config.width, Direction::LeftToRight, out), + Format::Columns => display_grid(names, config.width, Direction::TopToBottom, out)?, + Format::Across => display_grid(names, config.width, Direction::LeftToRight, out)?, Format::Commas => { let mut current_col = 0; let mut names = names; if let Some(name) = names.next() { - let _ = write!(out, "{}", name.contents); + write!(out, "{}", name.contents)?; current_col = name.width as u16 + 2; } for name in names { @@ -1732,25 +1735,27 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter config.width { current_col = name_width + 2; - let _ = write!(out, ",\n{}", name.contents); + write!(out, ",\n{}", name.contents)?; } else { current_col += name_width + 2; - let _ = write!(out, ", {}", name.contents); + write!(out, ", {}", name.contents)?; } } // Current col is never zero again if names have been printed. // So we print a newline. if current_col > 0 { - let _ = writeln!(out,); + writeln!(out,)?; } } _ => { for name in names { - let _ = writeln!(out, "{}", name.contents); + writeln!(out, "{}", name.contents)?; } } - } + }; } + + Ok(()) } fn get_block_size(md: &Metadata, config: &Config) -> u64 { @@ -1769,7 +1774,6 @@ fn get_block_size(md: &Metadata, config: &Config) -> u64 { #[cfg(not(unix))] { - let _ = config; // no way to get block size for windows, fall-back to file size md.len() } @@ -1780,19 +1784,19 @@ fn display_grid( width: u16, direction: Direction, out: &mut BufWriter, -) { +) -> UResult<()> { if width == 0 { // If the width is 0 we print one single line let mut printed_something = false; for name in names { if printed_something { - let _ = write!(out, " "); + write!(out, " ")?; } printed_something = true; - let _ = write!(out, "{}", name.contents); + write!(out, "{}", name.contents)?; } if printed_something { - let _ = writeln!(out); + writeln!(out)?; } } else { let mut grid = Grid::new(GridOptions { @@ -1806,14 +1810,15 @@ fn display_grid( match grid.fit_into_width(width as usize) { Some(output) => { - let _ = write!(out, "{}", output); + write!(out, "{}", output)?; } // Width is too small for the grid, so we fit it in one column None => { - let _ = write!(out, "{}", grid.fit_into_columns(1)); + write!(out, "{}", grid.fit_into_columns(1))?; } } } + Ok(()) } /// This writes to the BufWriter out a single string of the output of `ls -l`. @@ -1849,20 +1854,20 @@ fn display_item_long( padding: &PaddingCollection, config: &Config, out: &mut BufWriter, -) { +) -> UResult<()> { if let Some(md) = item.md(out) { #[cfg(unix)] { if config.inode { - let _ = write!( + write!( out, "{} ", pad_left(&get_inode(md), padding.longest_inode_len), - ); + )?; } } - let _ = write!( + write!( out, "{}{} {}", display_permissions(md, true), @@ -1874,48 +1879,48 @@ fn display_item_long( "" }, pad_left(&display_symlink_count(md), padding.longest_link_count_len), - ); + )?; if config.long.owner { - let _ = write!( + write!( out, " {}", pad_right(&display_uname(md, config), padding.longest_uname_len), - ); + )?; } if config.long.group { - let _ = write!( + write!( out, " {}", pad_right(&display_group(md, config), padding.longest_group_len), - ); + )?; } if config.context { - let _ = write!( + write!( out, " {}", pad_right(&item.security_context, padding.longest_context_len), - ); + )?; } // Author is only different from owner on GNU/Hurd, so we reuse // the owner, since GNU/Hurd is not currently supported by Rust. if config.long.author { - let _ = write!( + write!( out, " {}", pad_right(&display_uname(md, config), padding.longest_uname_len), - ); + )?; } match display_size_or_rdev(md, config) { SizeOrDeviceId::Size(size) => { - let _ = write!(out, " {}", pad_left(&size, padding.longest_size_len),); + write!(out, " {}", pad_left(&size, padding.longest_size_len),)?; } SizeOrDeviceId::Device(major, minor) => { - let _ = write!( + write!( out, " {}, {}", pad_left( @@ -1936,19 +1941,19 @@ fn display_item_long( #[cfg(unix)] padding.longest_minor_len, ), - ); + )?; } }; let dfn = display_file_name(item, config, None, 0, out).contents; - let _ = writeln!(out, " {} {}", display_date(md, config), dfn); + writeln!(out, " {} {}", display_date(md, config), dfn)?; } else { // this 'else' is expressly for the case of a dangling symlink/restricted file #[cfg(unix)] { if config.inode { - let _ = write!(out, "{} ", pad_left("?", padding.longest_inode_len),); + write!(out, "{} ", pad_left("?", padding.longest_inode_len),)?; } } @@ -1985,7 +1990,7 @@ fn display_item_long( } }; - let _ = write!( + write!( out, "{}{} {}", format_args!("{}?????????", leading_char), @@ -1997,41 +2002,43 @@ fn display_item_long( "" }, pad_left("?", padding.longest_link_count_len), - ); + )?; if config.long.owner { - let _ = write!(out, " {}", pad_right("?", padding.longest_uname_len)); + write!(out, " {}", pad_right("?", padding.longest_uname_len))?; } if config.long.group { - let _ = write!(out, " {}", pad_right("?", padding.longest_group_len)); + write!(out, " {}", pad_right("?", padding.longest_group_len))?; } if config.context { - let _ = write!( + write!( out, " {}", pad_right(&item.security_context, padding.longest_context_len) - ); + )?; } // Author is only different from owner on GNU/Hurd, so we reuse // the owner, since GNU/Hurd is not currently supported by Rust. if config.long.author { - let _ = write!(out, " {}", pad_right("?", padding.longest_uname_len)); + write!(out, " {}", pad_right("?", padding.longest_uname_len))?; } let dfn = display_file_name(item, config, None, 0, out).contents; let date_len = 12; - let _ = writeln!( + writeln!( out, " {} {} {}", pad_left("?", padding.longest_size_len), pad_left("?", date_len), dfn, - ); + )?; } + + Ok(()) } #[cfg(unix)] From 519e82240a1bb7367bc76e69cd32b484913edcc3 Mon Sep 17 00:00:00 2001 From: Hanif Ariffin Date: Sat, 5 Feb 2022 23:32:44 +0800 Subject: [PATCH 059/161] Revert "Refactor padding calculations into a function" This reverts commit f39b861469c8d7598741864f2602c15a5496287b. Signed-off-by: Hanif Ariffin --- src/uu/ls/src/ls.rs | 185 +++++++++++++++++++++----------------------- 1 file changed, 88 insertions(+), 97 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index f118a7594..aba59fe7e 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1275,9 +1275,9 @@ only ignore '.' and '..'.", ) } -/// Represents a Path along with it's associated data. -/// Any data that will be reused several times makes sense to be added to this structure. -/// Caching data here helps eliminate redundant syscalls to fetch same information. +/// Represents a Path along with it's associated data +/// Any data that will be reused several times makes sense to be added to this structure +/// Caching data here helps eliminate redundant syscalls to fetch same information #[derive(Debug)] struct PathData { // Result got from symlink_metadata() or metadata() based on config @@ -1682,9 +1682,92 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter, -) -> PaddingCollection { - let ( - mut longest_inode_len, - mut longest_link_count_len, - mut longest_uname_len, - mut longest_group_len, - mut longest_context_len, - mut longest_size_len, - mut longest_major_len, - mut longest_minor_len, - ) = (1, 1, 1, 1, 1, 1, 1, 1); - - for item in items { - let context_len = item.security_context.len(); - let (link_count_len, uname_len, group_len, size_len, major_len, minor_len, inode_len) = - display_dir_entry_size(item, config, out); - longest_inode_len = inode_len.max(longest_inode_len); - longest_link_count_len = link_count_len.max(longest_link_count_len); - longest_uname_len = uname_len.max(longest_uname_len); - longest_group_len = group_len.max(longest_group_len); - if config.context { - longest_context_len = context_len.max(longest_context_len); - } - if items.len() == 1usize { - longest_size_len = 0usize; - longest_major_len = 0usize; - longest_minor_len = 0usize; - } else { - longest_major_len = major_len.max(longest_major_len); - longest_minor_len = minor_len.max(longest_minor_len); - longest_size_len = size_len - .max(longest_size_len) - .max(longest_major_len + longest_minor_len + 2usize); - } - } - - PaddingCollection { - longest_inode_len, - longest_link_count_len, - longest_uname_len, - longest_group_len, - longest_context_len, - longest_size_len, - longest_major_len, - longest_minor_len, - } -} - -#[cfg(not(unix))] -fn calculate_paddings( - items: &[PathData], - config: &Config, - out: &mut BufWriter, -) -> PaddingCollection { - let ( - mut longest_link_count_len, - mut longest_uname_len, - mut longest_group_len, - mut longest_context_len, - mut longest_size_len, - ) = (1, 1, 1, 1, 1); - - for item in items { - let context_len = item.security_context.len(); - let (link_count_len, uname_len, group_len, size_len, _major_len, _minor_len, _inode_len) = - display_dir_entry_size(item, config, out); - longest_link_count_len = link_count_len.max(longest_link_count_len); - longest_uname_len = uname_len.max(longest_uname_len); - longest_group_len = group_len.max(longest_group_len); - if config.context { - longest_context_len = context_len.max(longest_context_len); - } - longest_size_len = size_len.max(longest_size_len); - } - - PaddingCollection { - longest_inode_len, - longest_link_count_len, - longest_uname_len, - longest_group_len, - longest_context_len, - longest_size_len, - longest_major_len, - longest_minor_len, - } -} From 78847e2ad098196b028f553641806a9c765daf36 Mon Sep 17 00:00:00 2001 From: Hanif Ariffin Date: Sat, 5 Feb 2022 23:40:23 +0800 Subject: [PATCH 060/161] Undo a small change that was meant to silence clippy Signed-off-by: Hanif Ariffin --- src/uu/ls/src/ls.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index aba59fe7e..19acf36b5 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1857,6 +1857,8 @@ fn get_block_size(md: &Metadata, config: &Config) -> u64 { #[cfg(not(unix))] { + // Silence linter warning about `config` being unused for windows. + let _ = config; // no way to get block size for windows, fall-back to file size md.len() } From cc61ea807ed0fe1750fa7b2f08b1e83521de3557 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Fri, 4 Feb 2022 17:39:57 -0600 Subject: [PATCH 061/161] docs/CICD ~ add spell-checker exceptions --- .github/workflows/CICD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 5fd51f852..81147c8dc 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -1,7 +1,7 @@ name: CICD # spell-checker:ignore (acronyms) CICD MSVC musl -# spell-checker:ignore (env/flags) Awarnings Ccodegen Coverflow Cpanic RUSTDOCFLAGS RUSTFLAGS Zpanic +# spell-checker:ignore (env/flags) Awarnings Ccodegen Coverflow Cpanic Dwarnings RUSTDOCFLAGS RUSTFLAGS Zpanic # spell-checker:ignore (jargon) SHAs deps dequote softprops subshell toolchain # spell-checker:ignore (names) CodeCOV MacOS MinGW Peltoche rivy # spell-checker:ignore (shell/tools) choco clippy dmake dpkg esac fakeroot gmake grcov halium lcov libssl mkdir popd printf pushd rustc rustfmt rustup shopt xargs From 578e5c8aba68f43a996a4b9330060ed0560fa1d4 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Thu, 3 Feb 2022 13:42:13 -0600 Subject: [PATCH 062/161] maint/CICD ~ implement 'GnuTests' workflow fixes/refactor - consolidate configuration - DRY improvements - improve flexibility/robustness in the face of missing reference test info - add reference test info IDs and additional logging to help diagnose testing failures - includes parallel refactor of 'util/run-gnu-test.sh' --- .github/workflows/GnuTests.yml | 169 +++++++++++++++++++++------------ util/run-gnu-test.sh | 26 ++++- 2 files changed, 129 insertions(+), 66 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 8303ee403..69a26608c 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -1,6 +1,6 @@ name: GnuTests -# spell-checker:ignore (names) gnulib ; (utils) autopoint gperf pyinotify texinfo ; (vars) XPASS +# spell-checker:ignore (names) gnulib ; (people) Dawid Dziurla * dawidd6 ; (utils) autopoint chksum gperf pyinotify shopt texinfo ; (vars) FILESET XPASS on: [push, pull_request] @@ -9,23 +9,55 @@ jobs: name: Run GNU tests runs-on: ubuntu-latest steps: + - name: Initialize workflow variables + id: vars + shell: bash + run: | + ## VARs setup + outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } + # * config + path_GNU="gnu" + path_GNULIB="gnulib" + path_GNU_tests="gnu/tests" + path_UUTILS="uutils" + path_reference="reference" + outputs path_GNU path_GNU_tests path_GNULIB path_reference path_UUTILS + # + repo_GNU_ref="v9.0" + repo_GNULIB_ref="8e99f24c0931a38880c6ee9b8287c7da80b0036b" + repo_reference_branch="${{ github.event.repository.default_branch }}" + outputs repo_GNU_ref repo_GNULIB_ref repo_reference_branch + # + SUITE_LOG_FILE="${path_GNU_tests}/test-suite.log" + TEST_LOGS_GLOB="${path_GNU_tests}/**/*.log" ## note: not usable at bash CLI; [why] double globstar not enabled by default b/c MacOS includes only bash v3 which doesn't have double globstar support + TEST_FILESET_PREFIX='test-fileset-IDs.sha1#' + TEST_FILESET_SUFFIX='.txt' + TEST_SUMMARY_FILE='gnu-result.json' + outputs SUITE_LOG_FILE TEST_FILESET_PREFIX TEST_FILESET_SUFFIX TEST_LOGS_GLOB TEST_SUMMARY_FILE - name: Checkout code uutil uses: actions/checkout@v2 with: - path: 'uutils' + path: '${{ steps.vars.outputs.path_UUTILS }}' - name: Checkout GNU coreutils uses: actions/checkout@v2 with: repository: 'coreutils/coreutils' - path: 'gnu' - ref: v9.0 + path: '${{ steps.vars.outputs.path_GNU }}' + ref: ${{ steps.vars.outputs.repo_GNU_ref }} - name: Checkout GNU coreutils library (gnulib) uses: actions/checkout@v2 with: repository: 'coreutils/gnulib' - path: 'gnulib' - ref: 8e99f24c0931a38880c6ee9b8287c7da80b0036b - fetch-depth: 0 # gnu gets upset if gnulib is a shallow checkout + path: '${{ steps.vars.outputs.path_GNULIB }}' + ref: ${{ steps.vars.outputs.repo_GNULIB_ref }} + fetch-depth: 0 # full depth checkout (o/w gnu gets upset if gnulib is a shallow checkout) + - name: Retrieve reference artifacts + uses: dawidd6/action-download-artifact@v2 + continue-on-error: true ## don't break the build for missing reference artifacts (may be expired or just not generated yet) + with: + workflow: GnuTests.yml + branch: "${{ steps.vars.outputs.repo_reference_branch }}" + path: "${{ steps.vars.outputs.path_reference }}" - name: Install `rust` toolchain uses: actions-rs/toolchain@v1 with: @@ -43,27 +75,33 @@ jobs: shell: bash run: | ## Build binaries - cd uutils + cd '${{ steps.vars.outputs.path_UUTILS }}' bash util/build-gnu.sh - name: Run GNU tests shell: bash run: | - bash uutils/util/run-gnu-test.sh - - name: Extract testing info + path_GNU='${{ steps.vars.outputs.path_GNU }}' + path_GNULIB='${{ steps.vars.outputs.path_GNULIB }}' + path_UUTILS='${{ steps.vars.outputs.path_UUTILS }}' + bash "${path_UUTILS}/util/run-gnu-test.sh" + - name: Extract/summarize testing info + id: summary shell: bash run: | - ## Extract testing info - LOG_FILE=gnu/tests/test-suite.log - if test -f "$LOG_FILE" + ## Extract/summarize testing info + outputs() { step_id="summary"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } + # + SUITE_LOG_FILE='${{ steps.vars.outputs.SUITE_LOG_FILE }}' + if test -f "${SUITE_LOG_FILE}" then - TOTAL=$(sed -n "s/.*# TOTAL: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1) - PASS=$(sed -n "s/.*# PASS: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1) - SKIP=$(sed -n "s/.*# SKIP: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1) - FAIL=$(sed -n "s/.*# FAIL: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1) - XPASS=$(sed -n "s/.*# XPASS: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1) - ERROR=$(sed -n "s/.*# ERROR: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1) + TOTAL=$(sed -n "s/.*# TOTAL: \(.*\)/\1/p" "${SUITE_LOG_FILE}" | tr -d '\r' | head -n1) + PASS=$(sed -n "s/.*# PASS: \(.*\)/\1/p" "${SUITE_LOG_FILE}" | tr -d '\r' | head -n1) + SKIP=$(sed -n "s/.*# SKIP: \(.*\)/\1/p" "${SUITE_LOG_FILE}" | tr -d '\r' | head -n1) + FAIL=$(sed -n "s/.*# FAIL: \(.*\)/\1/p" "${SUITE_LOG_FILE}" | tr -d '\r' | head -n1) + XPASS=$(sed -n "s/.*# XPASS: \(.*\)/\1/p" "${SUITE_LOG_FILE}" | tr -d '\r' | head -n1) + ERROR=$(sed -n "s/.*# ERROR: \(.*\)/\1/p" "${SUITE_LOG_FILE}" | tr -d '\r' | head -n1) if [[ "$TOTAL" -eq 0 || "$TOTAL" -eq 1 ]]; then - echo "Error in the execution, failing early" + echo "::error ::Failed to parse test results from '${SUITE_LOG_FILE}'; failing early" exit 1 fi output="GNU tests summary = TOTAL: $TOTAL / PASS: $PASS / FAIL: $FAIL / ERROR: $ERROR" @@ -78,54 +116,61 @@ jobs: --arg fail "$FAIL" \ --arg xpass "$XPASS" \ --arg error "$ERROR" \ - '{($date): { sha: $sha, total: $total, pass: $pass, skip: $skip, fail: $fail, xpass: $xpass, error: $error, }}' > gnu-result.json + '{($date): { sha: $sha, total: $total, pass: $pass, skip: $skip, fail: $fail, xpass: $xpass, error: $error, }}' > '${{ steps.vars.outputs.TEST_SUMMARY_FILE }}' + HASH=$(sha1sum '${{ steps.vars.outputs.TEST_SUMMARY_FILE }}' | cut --delim=" " -f 1) + outputs HASH else - echo "::error ::Failed to get summary of test results" + echo "::error ::Failed to find summary of test results (missing '${SUITE_LOG_FILE}'); failing early" + exit 1 fi - - uses: actions/upload-artifact@v2 + - name: Reserve SHA1/ID of 'test-summary' + uses: actions/upload-artifact@v2 with: - name: test-report - path: gnu/tests/**/*.log - - uses: actions/upload-artifact@v2 + name: "${{ steps.summary.outputs.HASH }}" + path: "${{ steps.vars.outputs.TEST_SUMMARY_FILE }}" + - name: Reserve test results summary + uses: actions/upload-artifact@v2 with: - name: gnu-result - path: gnu-result.json - - name: Download the result - uses: dawidd6/action-download-artifact@v2 + name: test-summary + path: "${{ steps.vars.outputs.TEST_SUMMARY_FILE }}" + - name: Reserve test logs + uses: actions/upload-artifact@v2 with: - workflow: GnuTests.yml - name: gnu-result - repo: uutils/coreutils - branch: main - path: dl - - name: Download the log - uses: dawidd6/action-download-artifact@v2 - with: - workflow: GnuTests.yml - name: test-report - repo: uutils/coreutils - branch: main - path: dl - - name: Compare failing tests against main + name: test-logs + path: "${{ steps.vars.outputs.TEST_LOGS_GLOB }}" + - name: Compare test failures VS reference shell: bash run: | - OLD_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" dl/test-suite.log | sort) - NEW_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" gnu/tests/test-suite.log | sort) - for LINE in $OLD_FAILING - do - if ! grep -Fxq $LINE<<<"$NEW_FAILING"; then - echo "::warning ::Congrats! The gnu test $LINE is now passing!" - fi - done - for LINE in $NEW_FAILING - do - if ! grep -Fxq $LINE<<<"$OLD_FAILING" - then - echo "::error ::GNU test failed: $LINE. $LINE is passing on 'main'. Maybe you have to rebase?" - fi - done - - name: Compare against main results + 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' + if test -f "${REF_LOG_FILE}"; then + echo "Reference SHA1/ID (of '${REF_SUMMARY_FILE}'): $(sha1sum -- "${REF_SUMMARY_FILE}")" + REF_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" "${REF_LOG_FILE}" | sort) + NEW_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" '${{ steps.vars.outputs.path_GNU_tests }}/test-suite.log' | sort) + for LINE in $REF_FAILING + do + if ! grep -Fxq $LINE<<<"$NEW_FAILING"; then + echo "::warning ::Congrats! The gnu test $LINE is now passing!" + fi + done + for LINE in $NEW_FAILING + do + if ! grep -Fxq $LINE<<<"$REF_FAILING" + then + echo "::error ::GNU test failed: $LINE. $LINE is passing on 'main'. Maybe you have to rebase?" + fi + done + else + echo "::warning ::Skipping test failure comparison; no prior reference test logs are available." + fi + - name: Compare test summary VS reference shell: bash run: | - mv dl/gnu-result.json main-gnu-result.json - python uutils/util/compare_gnu_result.py + REF_SUMMARY_FILE='${{ steps.vars.outputs.path_reference }}/test-summary/gnu-result.json' + if test -f "${REF_SUMMARY_FILE}"; then + echo "Reference SHA1/ID (of '${REF_SUMMARY_FILE}'): $(sha1sum -- "${REF_SUMMARY_FILE}")" + mv "${REF_SUMMARY_FILE}" main-gnu-result.json + python uutils/util/compare_gnu_result.py + else + echo "::warning ::Skipping test summary comparison; no prior reference summary is available." + fi diff --git a/util/run-gnu-test.sh b/util/run-gnu-test.sh index ff61e636e..123c4dab2 100755 --- a/util/run-gnu-test.sh +++ b/util/run-gnu-test.sh @@ -1,10 +1,28 @@ #!/bin/bash +# `$0 [TEST]` +# run GNU test (or all tests if TEST is missing/null) # spell-checker:ignore (env/vars) BUILDDIR GNULIB SUBDIRS -cd "$(dirname -- "$(readlink -fm -- "$0")")/../.." + +ME_dir="$(dirname -- "$(readlink -fm -- "$0")")" +REPO_main_dir="$(dirname -- "${ME_dir}")" + set -e -BUILDDIR="${PWD}/uutils/target/release" -GNULIB_DIR="${PWD}/gnulib" -pushd gnu + +### * config (from environment with fallback defaults) + +path_UUTILS=${path_UUTILS:-${REPO_main_dir}} +path_GNU=${path_GNU:-${path_UUTILS}/../gnu} +path_GNULIB=${path_GNULIB:-${path_UUTILS}/../gnulib} + +### + +BUILD_DIR="$(realpath -- "${path_UUTILS}/target/release")" +GNULIB_DIR="$(realpath -- "${path_GNULIB}")" + +export BUILD_DIR +export GNULIB_DIR + +pushd "$(realpath -- "${path_GNU}")" export RUST_BACKTRACE=1 From 1af709f642c8406625e5d9006fa43d342fb9be33 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sat, 5 Feb 2022 13:58:05 -0500 Subject: [PATCH 063/161] dd: truncate to specified seek length When specifying `seek=N` and *not* specifying `conv=notrunc`, truncate the output file to `N` blocks instead of truncating it to zero before starting to write output. For example $ printf "abc" > outfile $ printf "123" | dd bs=1 skip=1 seek=1 count=1 status=noxfer of=outfile 1+0 records in 1+0 records out $ cat outfile a2 Fixes #3068. --- src/uu/dd/src/dd.rs | 13 ++++++------- tests/by-util/test_dd.rs | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 54e3190ce..448eaf937 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -469,7 +469,6 @@ impl OutputTrait for Output { let mut opts = OpenOptions::new(); opts.write(true) .create(!cflags.nocreat) - .truncate(!cflags.notrunc) .create_new(cflags.excl) .append(oflags.append); @@ -489,13 +488,13 @@ impl OutputTrait for Output { let mut dst = open_dst(Path::new(&fname), &cflags, &oflags) .map_err_context(|| format!("failed to open {}", fname.quote()))?; - if let Some(amt) = seek { - let amt: u64 = amt - .try_into() - .map_err(|_| USimpleError::new(1, "failed to parse seek amount"))?; - dst.seek(io::SeekFrom::Start(amt)) - .map_err_context(|| "failed to seek in output file".to_string())?; + let i = seek.unwrap_or(0).try_into().unwrap(); + if !cflags.notrunc { + dst.set_len(i) + .map_err_context(|| "failed to truncate output file".to_string())?; } + dst.seek(io::SeekFrom::Start(i)) + .map_err_context(|| "failed to seek in output file".to_string())?; Ok(Self { dst, obs, cflags }) } else { diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index e73fe0673..688c629ef 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -604,5 +604,27 @@ fn test_seek_bytes() { .stdout_is("\0\0\0\0\0\0\0\0abcdefghijklm\n"); } +#[test] +fn test_seek_do_not_overwrite() { + let (at, mut ucmd) = at_and_ucmd!(); + let mut outfile = at.make_file("outfile"); + outfile.write_all(b"abc").unwrap(); + // Skip the first byte of the input, seek past the first byte of + // the output, and write only one byte to the output. + ucmd.args(&[ + "bs=1", + "skip=1", + "seek=1", + "count=1", + "status=noxfer", + "of=outfile", + ]) + .pipe_in("123") + .succeeds() + .stderr_is("1+0 records in\n1+0 records out\n") + .no_stdout(); + assert_eq!(at.read("outfile"), "a2"); +} + // conv=[ascii,ebcdic,ibm], conv=[ucase,lcase], conv=[block,unblock], conv=sync // TODO: Move conv tests from unit test module From 9ce9a4405280cb9272900c9c113aa93b9c6cad47 Mon Sep 17 00:00:00 2001 From: Hanif Bin Ariffin Date: Sun, 6 Feb 2022 10:26:01 +0800 Subject: [PATCH 064/161] Silencing clippy about unused variables Signed-off-by: Hanif Bin Ariffin --- src/uu/ls/src/ls.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index aba59fe7e..d78813569 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1857,6 +1857,7 @@ fn get_block_size(md: &Metadata, config: &Config) -> u64 { #[cfg(not(unix))] { + let _ = config; // no way to get block size for windows, fall-back to file size md.len() } From fec662a6237c243a5d54d2ab3c3b63729d8aefff Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Tue, 1 Feb 2022 23:22:01 -0500 Subject: [PATCH 065/161] dd: show warning when using 0x size multiplier Show a warning when a block size includes "0x" since this is ambiguous: the user may have meant "multiply the next number by zero" or they may have meant "the following characters should be interpreted as a hexadecimal number". --- src/uu/dd/src/parseargs.rs | 8 ++++++++ tests/by-util/test_dd.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index 915a99344..6bc7bcfd9 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -13,6 +13,7 @@ use super::*; use std::error::Error; use uucore::error::UError; use uucore::parse_size::ParseSizeError; +use uucore::show_warning; pub type Matches = ArgMatches; @@ -356,6 +357,13 @@ fn parse_bytes_only(s: &str) -> Result { /// assert_eq!(parse_bytes_no_x("2k").unwrap(), 2 * 1024); /// ``` fn parse_bytes_no_x(s: &str) -> Result { + if s == "0" { + show_warning!( + "{} is a zero multiplier; use {} if that is intended", + "0x".quote(), + "00x".quote() + ); + } let (num, multiplier) = match (s.find('c'), s.rfind('w'), s.rfind('b')) { (None, None, None) => match uucore::parse_size::parse_size(s) { Ok(n) => (n, 1), diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index e9a1f9468..d27122a75 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -198,6 +198,39 @@ fn test_x_multiplier() { .stdout_is("abcdef"); } +#[test] +fn test_zero_multiplier_warning() { + for arg in ["count", "seek", "skip"] { + new_ucmd!() + .args(&[format!("{}=00x1", arg).as_str(), "status=none"]) + .pipe_in("") + .succeeds() + .no_stdout() + .no_stderr(); + + new_ucmd!() + .args(&[format!("{}=0x1", arg).as_str(), "status=none"]) + .pipe_in("") + .succeeds() + .no_stdout() + .stderr_contains("warning: '0x' is a zero multiplier; use '00x' if that is intended"); + + new_ucmd!() + .args(&[format!("{}=0x0x1", arg).as_str(), "status=none"]) + .pipe_in("") + .succeeds() + .no_stdout() + .stderr_is("dd: warning: '0x' is a zero multiplier; use '00x' if that is intended\ndd: warning: '0x' is a zero multiplier; use '00x' if that is intended\n"); + + new_ucmd!() + .args(&[format!("{}=1x0x1", arg).as_str(), "status=none"]) + .pipe_in("") + .succeeds() + .no_stdout() + .stderr_contains("warning: '0x' is a zero multiplier; use '00x' if that is intended"); + } +} + #[test] fn test_final_stats_noxfer() { new_ucmd!() From 3842ecb1b4b7c83e6188d97dba7fe2650199768a Mon Sep 17 00:00:00 2001 From: ndd7xv Date: Sun, 6 Feb 2022 00:19:43 -0500 Subject: [PATCH 066/161] dd: status=progress rewrites once/sec --- src/uu/dd/src/dd.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 54e3190ce..8308ef429 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -832,10 +832,14 @@ fn gen_prog_updater(rx: mpsc::Receiver, print_level: Option= progress_as_secs + { reprint_prog_line(&update); + progress_as_secs = update.duration.as_secs() + 1; } // Handle signals #[cfg(target_os = "linux")] From e6a63a78f6ed84b102c136865f7f604f53e15597 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sun, 6 Feb 2022 18:03:13 -0500 Subject: [PATCH 067/161] tests: fix no_stderr check in stderr_only_bytes() Fix a bug in `stderr_only_bytes()` where it was unintentionally checking `no_stderr()` when it should have been checking `no_stdout()`. --- tests/common/util.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/common/util.rs b/tests/common/util.rs index d21aea968..4db4e2561 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -356,7 +356,7 @@ impl CmdResult { /// of the passed value /// 2. the command resulted in an empty stdout stream pub fn stderr_only_bytes>(&self, msg: T) -> &Self { - self.no_stderr().stderr_is_bytes(msg) + self.no_stdout().stderr_is_bytes(msg) } pub fn fails_silently(&self) -> &Self { From 572b2e032c509e0deabddbefe2aa7bf29ca86479 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sun, 6 Feb 2022 11:07:19 -0500 Subject: [PATCH 068/161] df: refactor filter_mount_list() to be more flat Use a `for` loop in the `filter_mount_list()` function to make the filtering logic easier to read. --- src/uu/df/src/df.rs | 129 ++++++++++++++++++++++++-------------------- 1 file changed, 72 insertions(+), 57 deletions(-) diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 07aa82dc1..e856a6b1e 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -161,64 +161,79 @@ impl Filesystem { } } +/// Keep only the specified subset of [`MountInfo`] instances. +/// +/// If `paths` is non-empty, this function excludes any [`MountInfo`] +/// that is not mounted at the specified path. +/// +/// The `opt` argument specifies a variety of ways of excluding +/// [`MountInfo`] instances; see [`Options`] for more information. +/// +/// Finally, if there are duplicate entries, the one with the shorter +/// path is kept. fn filter_mount_list(vmi: Vec, paths: &[String], opt: &Options) -> Vec { - vmi.into_iter() - .filter_map(|mi| { - if (mi.remote && opt.show_local_fs) - || (mi.dummy && !opt.show_all_fs && !opt.show_listed_fs) - || !opt.fs_selector.should_select(&mi.fs_type) - { - None - } else { - if paths.is_empty() { - // No path specified - return Some((mi.dev_id.clone(), mi)); - } - if paths.contains(&mi.mount_dir) { - // One or more paths have been provided - Some((mi.dev_id.clone(), mi)) - } else { - // Not a path we want to see - None - } - } - }) - .fold( - HashMap::>::new(), - |mut acc, (id, mi)| { - #[allow(clippy::map_entry)] - { - if acc.contains_key(&id) { - let seen = acc[&id].replace(mi.clone()); - let target_nearer_root = seen.mount_dir.len() > mi.mount_dir.len(); - // With bind mounts, prefer items nearer the root of the source - let source_below_root = !seen.mount_root.is_empty() - && !mi.mount_root.is_empty() - && seen.mount_root.len() < mi.mount_root.len(); - // let "real" devices with '/' in the name win. - if (!mi.dev_name.starts_with('/') || seen.dev_name.starts_with('/')) - // let points towards the root of the device win. - && (!target_nearer_root || source_below_root) - // let an entry over-mounted on a new device win... - && (seen.dev_name == mi.dev_name - /* ... but only when matching an existing mnt point, - to avoid problematic replacement when given - inaccurate mount lists, seen with some chroot - environments for example. */ - || seen.mount_dir != mi.mount_dir) - { - acc[&id].replace(seen); - } - } else { - acc.insert(id, Cell::new(mi)); - } - acc - } - }, - ) - .into_iter() - .map(|ent| ent.1.into_inner()) - .collect::>() + let mut mount_info_by_id = HashMap::>::new(); + for mi in vmi { + // Don't show remote filesystems if `--local` has been given. + if mi.remote && opt.show_local_fs { + continue; + } + + // Don't show pseudo filesystems unless `--all` has been given. + if mi.dummy && !opt.show_all_fs && !opt.show_listed_fs { + continue; + } + + // Don't show filesystems if they have been explicitly excluded. + if !opt.fs_selector.should_select(&mi.fs_type) { + continue; + } + + // Don't show filesystems other than the ones specified on the + // command line, if any. + if !paths.is_empty() && !paths.contains(&mi.mount_dir) { + continue; + } + + // If the device ID has not been encountered yet, just store it. + let id = mi.dev_id.clone(); + #[allow(clippy::map_entry)] + if !mount_info_by_id.contains_key(&id) { + mount_info_by_id.insert(id, Cell::new(mi)); + continue; + } + + // Otherwise, if we have seen the current device ID before, + // then check if we need to update it or keep the previously + // seen one. + let seen = mount_info_by_id[&id].replace(mi.clone()); + let target_nearer_root = seen.mount_dir.len() > mi.mount_dir.len(); + // With bind mounts, prefer items nearer the root of the source + let source_below_root = !seen.mount_root.is_empty() + && !mi.mount_root.is_empty() + && seen.mount_root.len() < mi.mount_root.len(); + // let "real" devices with '/' in the name win. + if (!mi.dev_name.starts_with('/') || seen.dev_name.starts_with('/')) + // let points towards the root of the device win. + && (!target_nearer_root || source_below_root) + // let an entry over-mounted on a new device win... + && (seen.dev_name == mi.dev_name + /* ... but only when matching an existing mnt point, + to avoid problematic replacement when given + inaccurate mount lists, seen with some chroot + environments for example. */ + || seen.mount_dir != mi.mount_dir) + { + mount_info_by_id[&id].replace(seen); + } + } + + // Take ownership of the `MountInfo` instances and collect them + // into a `Vec`. + mount_info_by_id + .into_values() + .map(|m| m.into_inner()) + .collect() } /// Convert `value` to a human readable string based on `base`. From e5361a8c113cc02dda1d1feb8d6628427fb94c69 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Mon, 31 Jan 2022 19:04:32 -0500 Subject: [PATCH 069/161] split: correct error message on invalid arg. to -a Correct the error message displayed on an invalid parameter to the `--suffix-length` or `-a` command-line option. --- src/uu/split/src/split.rs | 12 +++++++----- tests/by-util/test_split.rs | 9 +++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index a05959810..9df7bd42c 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -233,12 +233,14 @@ struct Settings { impl Settings { /// Parse a strategy from the command-line arguments. fn from(matches: &ArgMatches) -> UResult { + let suffix_length_str = matches.value_of(OPT_SUFFIX_LENGTH).unwrap(); let result = Self { - suffix_length: matches - .value_of(OPT_SUFFIX_LENGTH) - .unwrap() - .parse() - .unwrap_or_else(|_| panic!("Invalid number for {}", OPT_SUFFIX_LENGTH)), + suffix_length: suffix_length_str.parse().map_err(|_| { + USimpleError::new( + 1, + format!("invalid suffix length: {}", suffix_length_str.quote()), + ) + })?, numeric_suffix: matches.occurrences_of(OPT_NUMERIC_SUFFIXES) > 0, additional_suffix: matches.value_of(OPT_ADDITIONAL_SUFFIX).unwrap().to_owned(), verbose: matches.occurrences_of("verbose") > 0, diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 8e61c5153..911a7bf30 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -440,3 +440,12 @@ fn test_number() { assert_eq!(file_read("xad"), "pqrst"); assert_eq!(file_read("xae"), "uvwxyz"); } + +#[test] +fn test_invalid_suffix_length() { + new_ucmd!() + .args(&["-a", "xyz"]) + .fails() + .no_stdout() + .stderr_contains("invalid suffix length: 'xyz'"); +} From 8fa679725558ec1845b5058c8838259aabea7b5a Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Mon, 31 Jan 2022 19:09:27 -0500 Subject: [PATCH 070/161] split: add structure to errors that can be created Add some structure to errors that can be created during parsing of settings from command-line options. This commit creates `StrategyError` and `SettingsError` enumerations to represent the various parsing and other errors that can arise when transforming `ArgMatches` into `Settings`. --- src/uu/split/src/split.rs | 107 +++++++++++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 26 deletions(-) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 9df7bd42c..1b6680142 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -15,12 +15,14 @@ use crate::filenames::FilenameIterator; use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; use std::convert::TryFrom; use std::env; +use std::fmt; use std::fs::{metadata, remove_file, File}; use std::io::{stdin, BufRead, BufReader, BufWriter, Read, Write}; +use std::num::ParseIntError; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; -use uucore::parse_size::parse_size; +use uucore::parse_size::{parse_size, ParseSizeError}; static OPT_BYTES: &str = "bytes"; static OPT_LINE_BYTES: &str = "line-bytes"; @@ -62,8 +64,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .override_usage(&usage[..]) .after_help(&long_usage[..]) .get_matches_from(args); - let settings = Settings::from(&matches)?; - split(&settings) + match Settings::from(&matches) { + Ok(settings) => split(&settings), + Err(e) if e.requires_usage() => Err(UUsageError::new(1, format!("{}", e))), + Err(e) => Err(USimpleError::new(1, format!("{}", e))), + } } pub fn uu_app<'a>() -> App<'a> { @@ -169,9 +174,35 @@ enum Strategy { Number(usize), } +/// An error when parsing a chunking strategy from command-line arguments. +enum StrategyError { + /// Invalid number of lines. + Lines(ParseSizeError), + + /// Invalid number of bytes. + Bytes(ParseSizeError), + + /// Invalid number of chunks. + NumberOfChunks(ParseIntError), + + /// Multiple chunking strategies were specified (but only one should be). + MultipleWays, +} + +impl fmt::Display for StrategyError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Lines(e) => write!(f, "invalid number of lines: {}", e), + Self::Bytes(e) => write!(f, "invalid number of bytes: {}", e), + Self::NumberOfChunks(e) => write!(f, "invalid number of chunks: {}", e), + Self::MultipleWays => write!(f, "cannot split in more than one way"), + } + } +} + impl Strategy { /// Parse a strategy from the command-line arguments. - fn from(matches: &ArgMatches) -> UResult { + fn from(matches: &ArgMatches) -> Result { // Check that the user is not specifying more than one strategy. // // Note: right now, this exact behavior cannot be handled by @@ -186,30 +217,25 @@ impl Strategy { (0, 0, 0, 0) => Ok(Self::Lines(1000)), (1, 0, 0, 0) => { let s = matches.value_of(OPT_LINES).unwrap(); - let n = parse_size(s) - .map_err(|e| USimpleError::new(1, format!("invalid number of lines: {}", e)))?; + let n = parse_size(s).map_err(StrategyError::Lines)?; Ok(Self::Lines(n)) } (0, 1, 0, 0) => { let s = matches.value_of(OPT_BYTES).unwrap(); - let n = parse_size(s) - .map_err(|e| USimpleError::new(1, format!("invalid number of bytes: {}", e)))?; + let n = parse_size(s).map_err(StrategyError::Bytes)?; Ok(Self::Bytes(n)) } (0, 0, 1, 0) => { let s = matches.value_of(OPT_LINE_BYTES).unwrap(); - let n = parse_size(s) - .map_err(|e| USimpleError::new(1, format!("invalid number of bytes: {}", e)))?; + let n = parse_size(s).map_err(StrategyError::Bytes)?; Ok(Self::LineBytes(n)) } (0, 0, 0, 1) => { let s = matches.value_of(OPT_NUMBER).unwrap(); - let n = s.parse::().map_err(|e| { - USimpleError::new(1, format!("invalid number of chunks: {}", e)) - })?; + let n = s.parse::().map_err(StrategyError::NumberOfChunks)?; Ok(Self::Number(n)) } - _ => Err(UUsageError::new(1, "cannot split in more than one way")), + _ => Err(StrategyError::MultipleWays), } } } @@ -230,21 +256,53 @@ struct Settings { verbose: bool, } +/// An error when parsing settings from command-line arguments. +enum SettingsError { + /// Invalid chunking strategy. + Strategy(StrategyError), + + /// Invalid suffix length parameter. + SuffixLength(String), + + /// The `--filter` option is not supported on Windows. + #[cfg(windows)] + NotSupported, +} + +impl SettingsError { + /// Whether the error demands a usage message. + fn requires_usage(&self) -> bool { + matches!(self, Self::Strategy(StrategyError::MultipleWays)) + } +} + +impl fmt::Display for SettingsError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Strategy(e) => e.fmt(f), + Self::SuffixLength(s) => write!(f, "invalid suffix length: {}", s.quote()), + #[cfg(windows)] + Self::NotSupported => write!( + f, + "{} is currently not supported in this platform", + OPT_FILTER + ), + } + } +} + impl Settings { /// Parse a strategy from the command-line arguments. - fn from(matches: &ArgMatches) -> UResult { + fn from(matches: &ArgMatches) -> Result { let suffix_length_str = matches.value_of(OPT_SUFFIX_LENGTH).unwrap(); let result = Self { - suffix_length: suffix_length_str.parse().map_err(|_| { - USimpleError::new( - 1, - format!("invalid suffix length: {}", suffix_length_str.quote()), - ) - })?, + suffix_length: suffix_length_str + .parse() + .map_err(|_| SettingsError::SuffixLength(suffix_length_str.to_string()))?, numeric_suffix: matches.occurrences_of(OPT_NUMERIC_SUFFIXES) > 0, additional_suffix: matches.value_of(OPT_ADDITIONAL_SUFFIX).unwrap().to_owned(), verbose: matches.occurrences_of("verbose") > 0, - strategy: Strategy::from(matches)?, + strategy: Strategy::from(matches).map_err(SettingsError::Strategy)?, input: matches.value_of(ARG_INPUT).unwrap().to_owned(), prefix: matches.value_of(ARG_PREFIX).unwrap().to_owned(), filter: matches.value_of(OPT_FILTER).map(|s| s.to_owned()), @@ -252,10 +310,7 @@ impl Settings { #[cfg(windows)] if result.filter.is_some() { // see https://github.com/rust-lang/rust/issues/29494 - return Err(USimpleError::new( - -1, - format!("{} is currently not supported in this platform", OPT_FILTER), - )); + return Err(SettingsError::NotSupported); } Ok(result) From 84d4f24b8c0e2d1dd24b6319ba37ea0fcb90a9e8 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Fri, 4 Feb 2022 21:42:21 -0500 Subject: [PATCH 071/161] dd: avoid infinite loop in Input::force_fill() Avoid an infinite loop in `Input::force_fill()` when the input has fewer bytes than are being requested to be read from the input. --- src/uu/dd/src/dd.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 54e3190ce..ac7b50d1b 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -80,8 +80,7 @@ impl Input { }; if let Some(amt) = skip { - let mut buf = vec![BUF_INIT_BYTE; amt]; - i.force_fill(&mut buf, amt) + i.force_fill(amt.try_into().unwrap()) .map_err_context(|| "failed to read input".to_string())?; } @@ -264,17 +263,19 @@ impl Input { }) } - /// Force-fills a buffer, ignoring zero-length reads which would otherwise be - /// interpreted as EOF. - /// Note: This will not return unless the source (eventually) produces - /// enough bytes to meet target_len. - fn force_fill(&mut self, buf: &mut [u8], target_len: usize) -> std::io::Result { - let mut base_idx = 0; - while base_idx < target_len { - base_idx += self.read(&mut buf[base_idx..target_len])?; - } - - Ok(base_idx) + /// Read the specified number of bytes from this reader. + /// + /// On success, this method returns the number of bytes read. If + /// this reader has fewer than `n` bytes available, then it reads + /// as many as possible. In that case, this method returns a + /// number less than `n`. + /// + /// # Errors + /// + /// If there is a problem reading. + fn force_fill(&mut self, n: u64) -> std::io::Result { + let mut buf = vec![]; + self.take(n).read_to_end(&mut buf) } } From 9f8ec676c53e1f4f4a01d228972601c4e283e0ad Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Fri, 4 Feb 2022 21:44:26 -0500 Subject: [PATCH 072/161] dd: show warning if skipping past end of input Show a warning if the `skip=N` command-line argument would cause `dd` to skip past the end of the input. For example: $ printf "abcd" | dd bs=1 skip=5 count=0 status=noxfer 'standard input': cannot skip to specified offset 0+0 records in 0+0 records out --- src/uu/dd/src/dd.rs | 7 ++++++- tests/by-util/test_dd.rs | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index ac7b50d1b..13bacd946 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -42,6 +42,7 @@ use gcd::Gcd; use signal_hook::consts::signal; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::show_error; use uucore::InvalidEncodingHandling; const ABOUT: &str = "copy, and optionally convert, a file system resource"; @@ -80,8 +81,12 @@ impl Input { }; if let Some(amt) = skip { - i.force_fill(amt.try_into().unwrap()) + let num_bytes_read = i + .force_fill(amt.try_into().unwrap()) .map_err_context(|| "failed to read input".to_string())?; + if num_bytes_read < amt { + show_error!("'standard input': cannot skip to specified offset"); + } } Ok(i) diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index e9a1f9468..30adb05fc 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -624,5 +624,18 @@ fn test_seek_bytes() { .stdout_is("\0\0\0\0\0\0\0\0abcdefghijklm\n"); } +/// Test for skipping beyond the number of bytes in a file. +#[test] +fn test_skip_beyond_file() { + new_ucmd!() + .args(&["bs=1", "skip=5", "count=0", "status=noxfer"]) + .pipe_in("abcd") + .succeeds() + .no_stdout() + .stderr_contains( + "'standard input': cannot skip to specified offset\n0+0 records in\n0+0 records out\n", + ); +} + // conv=[ascii,ebcdic,ibm], conv=[ucase,lcase], conv=[block,unblock], conv=sync // TODO: Move conv tests from unit test module From 44772a8dbb1897c6b67e2f981363bf6428081fb5 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sun, 6 Feb 2022 21:52:44 -0500 Subject: [PATCH 073/161] uucore: set meaningless FsUsage.ffree value to 0 Set the value of the `FsUsage.ffree` value to 0 on Windows, because even though it is meaningless, it should not exceed the `FsUsage.files` value so that client code can rely on the guarantee that `FsUsage.ffree <= FsUsage.files`. --- src/uucore/src/lib/features/fsext.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index a3b05dff8..b8207d68c 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -567,7 +567,7 @@ impl FsUsage { // Total number of file nodes (inodes) on the file system. files: 0, // Not available on windows // Total number of free file nodes (inodes). - ffree: 4096, // Meaningless on Windows + ffree: 0, // Meaningless on Windows } } } From 9528d514bf9573ade24c0008d58834d767dca0a6 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sun, 6 Feb 2022 11:41:19 -0500 Subject: [PATCH 074/161] df: refactor data table into Row, Header structs Refactor the code for representing the `df` data table into `Header` and `Row` structs. These structs live in a new module `table.rs`. When combined with the `Options` struct, these structs can be `Display`ed. Organizing the code this way makes it possible to test the display settings independently of the machinery for getting the filesystem data. New unit tests have been added to `table.rs` to demonstrate this benefit. --- src/uu/df/src/df.rs | 162 +------------ src/uu/df/src/table.rs | 501 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 512 insertions(+), 151 deletions(-) create mode 100644 src/uu/df/src/table.rs diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 07aa82dc1..f7affbbfc 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -5,8 +5,8 @@ // // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. +mod table; -use uucore::error::UError; use uucore::error::UResult; #[cfg(unix)] use uucore::fsext::statfs_fn; @@ -14,14 +14,11 @@ use uucore::fsext::{read_fs_list, FsUsage, MountInfo}; use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; -use number_prefix::NumberPrefix; use std::cell::Cell; use std::collections::HashMap; use std::collections::HashSet; -use std::error::Error; #[cfg(unix)] use std::ffi::CString; -use std::fmt::Display; use std::iter::FromIterator; #[cfg(unix)] use std::mem; @@ -29,6 +26,8 @@ use std::mem; #[cfg(windows)] use std::path::Path; +use crate::table::{DisplayRow, Header, Row}; + static ABOUT: &str = "Show information about the file system on which each FILE resides,\n\ or all file systems by default."; @@ -58,6 +57,7 @@ struct FsSelector { exclude: HashSet, } +#[derive(Default)] struct Options { show_local_fs: bool, show_all_fs: bool, @@ -221,64 +221,6 @@ fn filter_mount_list(vmi: Vec, paths: &[String], opt: &Options) -> Ve .collect::>() } -/// Convert `value` to a human readable string based on `base`. -/// e.g. It returns 1G when value is 1 * 1024 * 1024 * 1024 and base is 1024. -/// Note: It returns `value` if `base` isn't positive. -fn human_readable(value: u64, base: i64) -> UResult { - let base_str = match base { - d if d < 0 => value.to_string(), - - // ref: [Binary prefix](https://en.wikipedia.org/wiki/Binary_prefix) @@ - // ref: [SI/metric prefix](https://en.wikipedia.org/wiki/Metric_prefix) @@ - 1000 => match NumberPrefix::decimal(value as f64) { - NumberPrefix::Standalone(bytes) => bytes.to_string(), - NumberPrefix::Prefixed(prefix, bytes) => format!("{:.1}{}", bytes, prefix.symbol()), - }, - - 1024 => match NumberPrefix::binary(value as f64) { - NumberPrefix::Standalone(bytes) => bytes.to_string(), - NumberPrefix::Prefixed(prefix, bytes) => format!("{:.1}{}", bytes, prefix.symbol()), - }, - - _ => return Err(DfError::InvalidBaseValue(base.to_string()).into()), - }; - - Ok(base_str) -} - -fn use_size(free_size: u64, total_size: u64) -> String { - if total_size == 0 { - return String::from("-"); - } - return format!( - "{:.0}%", - 100f64 - 100f64 * (free_size as f64 / total_size as f64) - ); -} - -#[derive(Debug)] -enum DfError { - InvalidBaseValue(String), -} - -impl Display for DfError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DfError::InvalidBaseValue(s) => write!(f, "Internal error: Unknown base value {}", s), - } - } -} - -impl Error for DfError {} - -impl UError for DfError { - fn code(&self) -> i32 { - match self { - DfError::InvalidBaseValue(_) => 1, - } - } -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let usage = usage(); @@ -299,98 +241,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let opt = Options::from(&matches); - let fs_list = filter_mount_list(read_fs_list(), &paths, &opt) + let mounts = read_fs_list(); + let data: Vec = filter_mount_list(mounts, &paths, &opt) .into_iter() .filter_map(Filesystem::new) .filter(|fs| fs.usage.blocks != 0 || opt.show_all_fs || opt.show_listed_fs) - .collect::>(); - - // set headers - let mut header = vec!["Filesystem"]; - if opt.show_fs_type { - header.push("Type"); - } - header.extend_from_slice(&if opt.show_inode_instead { - // spell-checker:disable-next-line - ["Inodes", "Iused", "IFree", "IUses%"] - } else { - [ - if opt.human_readable_base == -1 { - "1k-blocks" - } else { - "Size" - }, - "Used", - "Available", - "Use%", - ] - }); - if cfg!(target_os = "macos") && !opt.show_inode_instead { - header.insert(header.len() - 1, "Capacity"); - } - header.push("Mounted on"); - - for (idx, title) in header.iter().enumerate() { - if idx == 0 || idx == header.len() - 1 { - print!("{0: <16} ", title); - } else if opt.show_fs_type && idx == 1 { - print!("{0: <5} ", title); - } else if idx == header.len() - 2 { - print!("{0: >5} ", title); - } else { - print!("{0: >12} ", title); - } - } - println!(); - for fs in &fs_list { - print!("{0: <16} ", fs.mount_info.dev_name); - if opt.show_fs_type { - print!("{0: <5} ", fs.mount_info.fs_type); - } - if opt.show_inode_instead { - print!( - "{0: >12} ", - human_readable(fs.usage.files, opt.human_readable_base)? - ); - print!( - "{0: >12} ", - human_readable(fs.usage.files - fs.usage.ffree, opt.human_readable_base)? - ); - print!( - "{0: >12} ", - human_readable(fs.usage.ffree, opt.human_readable_base)? - ); - print!( - "{0: >5} ", - format!( - "{0:.1}%", - 100f64 - 100f64 * (fs.usage.ffree as f64 / fs.usage.files as f64) - ) - ); - } else { - let total_size = fs.usage.blocksize * fs.usage.blocks; - let free_size = fs.usage.blocksize * fs.usage.bfree; - print!( - "{0: >12} ", - human_readable(total_size, opt.human_readable_base)? - ); - print!( - "{0: >12} ", - human_readable(total_size - free_size, opt.human_readable_base)? - ); - print!( - "{0: >12} ", - human_readable(free_size, opt.human_readable_base)? - ); - if cfg!(target_os = "macos") { - let used = fs.usage.blocks - fs.usage.bfree; - let blocks = used + fs.usage.bavail; - print!("{0: >12} ", use_size(used, blocks)); - } - print!("{0: >5} ", use_size(free_size, total_size)); - } - print!("{0: <16}", fs.mount_info.mount_dir); - println!(); + .map(Into::into) + .collect(); + println!("{}", Header::new(&opt)); + for row in data { + println!("{}", DisplayRow::new(row, &opt)); } Ok(()) diff --git a/src/uu/df/src/table.rs b/src/uu/df/src/table.rs new file mode 100644 index 000000000..5876bf7d2 --- /dev/null +++ b/src/uu/df/src/table.rs @@ -0,0 +1,501 @@ +// * 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. +// spell-checker:ignore tmpfs +//! The filesystem usage data table. +//! +//! A table comprises a header row ([`Header`]) and a collection of +//! data rows ([`Row`]), one per filesystem. To display a [`Row`], +//! combine it with [`Options`] in the [`DisplayRow`] struct; the +//! [`DisplayRow`] implements [`std::fmt::Display`]. +use number_prefix::NumberPrefix; + +use crate::{Filesystem, Options}; +use uucore::fsext::{FsUsage, MountInfo}; + +use std::fmt; + +/// A row in the filesystem usage data table. +/// +/// A row comprises several pieces of information, including the +/// filesystem device, the mountpoint, the number of bytes used, etc. +pub(crate) struct Row { + /// Name of the device on which the filesystem lives. + fs_device: String, + + /// Type of filesystem (for example, `"ext4"`, `"tmpfs"`, etc.). + fs_type: String, + + /// Path at which the filesystem is mounted. + fs_mount: String, + + /// Total number of bytes in the filesystem regardless of whether they are used. + bytes: u64, + + /// Number of used bytes. + bytes_used: u64, + + /// Number of free bytes. + bytes_free: u64, + + /// Percentage of bytes that are used, given as a float between 0 and 1. + /// + /// If the filesystem has zero bytes, then this is `None`. + bytes_usage: Option, + + /// Percentage of bytes that are available, given as a float between 0 and 1. + /// + /// These are the bytes that are available to non-privileged processes. + /// + /// If the filesystem has zero bytes, then this is `None`. + #[cfg(target_os = "macos")] + bytes_capacity: Option, + + /// Total number of inodes in the filesystem. + inodes: u64, + + /// Number of used inodes. + inodes_used: u64, + + /// Number of free inodes. + inodes_free: u64, + + /// Percentage of inodes that are used, given as a float between 0 and 1. + /// + /// If the filesystem has zero bytes, then this is `None`. + inodes_usage: Option, +} + +impl From for Row { + fn from(fs: Filesystem) -> Self { + let MountInfo { + dev_name, + fs_type, + mount_dir, + .. + } = fs.mount_info; + let FsUsage { + blocksize, + blocks, + bfree, + #[cfg(target_os = "macos")] + bavail, + files, + ffree, + .. + } = fs.usage; + Self { + fs_device: dev_name, + fs_type, + fs_mount: mount_dir, + bytes: blocksize * blocks, + bytes_used: blocksize * (blocks - bfree), + bytes_free: blocksize * bfree, + bytes_usage: if blocks == 0 { + None + } else { + Some(((blocks - bfree) as f64) / blocks as f64) + }, + #[cfg(target_os = "macos")] + bytes_capacity: if bavail == 0 { + None + } else { + Some(bavail as f64 / ((blocks - bfree + bavail) as f64)) + }, + inodes: files, + inodes_used: files - ffree, + inodes_free: ffree, + inodes_usage: if files == 0 { + None + } else { + Some(ffree as f64 / files as f64) + }, + } + } +} + +/// A displayable wrapper around a [`Row`]. +/// +/// The `options` control how the information in the row gets displayed. +pub(crate) struct DisplayRow<'a> { + /// The data in this row. + row: Row, + + /// Options that control how to display the data. + options: &'a Options, + // TODO We don't need all of the command-line options here. Some + // of the command-line options indicate which rows to include or + // exclude. Other command-line options indicate which columns to + // include or exclude. Still other options indicate how to format + // numbers. We could split the options up into those groups to + // reduce the coupling between this `table.rs` module and the main + // `df.rs` module. +} + +impl<'a> DisplayRow<'a> { + /// Instantiate this struct. + pub(crate) fn new(row: Row, options: &'a Options) -> Self { + Self { row, options } + } + + /// Get a string giving the scaled version of the input number. + /// + /// The scaling factor is defined in the `options` field. + /// + /// # Errors + /// + /// If the scaling factor is not 1000, 1024, or a negative number. + fn scaled(&self, size: u64) -> Result { + // TODO The argument-parsing code should be responsible for + // ensuring that the `human_readable_base` number is + // positive. Then we could remove the `Err` case from this + // function. + // + // TODO We should not be using a negative number to indicate + // default behavior. The default behavior for `df` is to show + // sizes in blocks of 1K bytes each, so we should just do + // that. + // + // TODO Support arbitrary positive scaling factors (from the + // `--block-size` command-line argument). + let number_prefix = match self.options.human_readable_base { + 1000 => NumberPrefix::decimal(size as f64), + 1024 => NumberPrefix::binary(size as f64), + d if d < 0 => return Ok(size.to_string()), + _ => return Err(fmt::Error {}), + }; + match number_prefix { + NumberPrefix::Standalone(bytes) => Ok(bytes.to_string()), + NumberPrefix::Prefixed(prefix, bytes) => Ok(format!("{:.1}{}", bytes, prefix.symbol())), + } + } + + /// Convert a float between 0 and 1 into a percentage string. + /// + /// If `None`, return the string `"-"` instead. + fn percentage(fraction: Option) -> String { + match fraction { + None => "-".to_string(), + Some(x) => format!("{:.0}%", 100.0 * x), + } + } + + /// Write the bytes data for this row. + /// + /// # Errors + /// + /// If there is a problem writing to `f`. + /// + /// If the scaling factor is not 1000, 1024, or a negative number. + fn fmt_bytes(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{0: >12} ", self.scaled(self.row.bytes)?)?; + write!(f, "{0: >12} ", self.scaled(self.row.bytes_used)?)?; + write!(f, "{0: >12} ", self.scaled(self.row.bytes_free)?)?; + #[cfg(target_os = "macos")] + write!( + f, + "{0: >12} ", + DisplayRow::percentage(self.row.bytes_capacity) + )?; + write!(f, "{0: >5} ", DisplayRow::percentage(self.row.bytes_usage))?; + Ok(()) + } + + /// Write the inodes data for this row. + /// + /// # Errors + /// + /// If there is a problem writing to `f`. + /// + /// If the scaling factor is not 1000, 1024, or a negative number. + fn fmt_inodes(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{0: >12} ", self.scaled(self.row.inodes)?)?; + write!(f, "{0: >12} ", self.scaled(self.row.inodes_used)?)?; + write!(f, "{0: >12} ", self.scaled(self.row.inodes_free)?)?; + write!(f, "{0: >5} ", DisplayRow::percentage(self.row.inodes_usage))?; + Ok(()) + } +} + +impl fmt::Display for DisplayRow<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{0: <16} ", self.row.fs_device)?; + if self.options.show_fs_type { + write!(f, "{0: <5} ", self.row.fs_type)?; + } + if self.options.show_inode_instead { + self.fmt_inodes(f)?; + } else { + self.fmt_bytes(f)?; + } + write!(f, "{0: <16}", self.row.fs_mount)?; + Ok(()) + } +} + +/// The header row. +/// +/// The `options` control which columns are displayed. +pub(crate) struct Header<'a> { + /// Options that control which columns are displayed. + options: &'a Options, +} + +impl<'a> Header<'a> { + /// Instantiate this struct. + pub(crate) fn new(options: &'a Options) -> Self { + Self { options } + } +} + +impl fmt::Display for Header<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{0: <16} ", "Filesystem")?; + if self.options.show_fs_type { + write!(f, "{0: <5} ", "Type")?; + } + if self.options.show_inode_instead { + write!(f, "{0: >12} ", "Inodes")?; + write!(f, "{0: >12} ", "IUsed")?; + write!(f, "{0: >12} ", "IFree")?; + write!(f, "{0: >5} ", "IUse%")?; + } else { + if self.options.human_readable_base == -1 { + write!(f, "{0: >12} ", "1k-blocks")?; + } else { + write!(f, "{0: >12} ", "Size")?; + }; + write!(f, "{0: >12} ", "Used")?; + write!(f, "{0: >12} ", "Available")?; + #[cfg(target_os = "macos")] + write!(f, "{0: >12} ", "Capacity")?; + write!(f, "{0: >5} ", "Use%")?; + } + write!(f, "{0: <16} ", "Mounted on")?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use crate::table::{DisplayRow, Header, Row}; + use crate::Options; + + #[test] + fn test_header_display() { + let options = Options { + human_readable_base: -1, + ..Default::default() + }; + assert_eq!( + Header::new(&options).to_string(), + "Filesystem 1k-blocks Used Available Use% Mounted on " + ); + } + + #[test] + fn test_header_display_fs_type() { + let options = Options { + human_readable_base: -1, + show_fs_type: true, + ..Default::default() + }; + assert_eq!( + Header::new(&options).to_string(), + "Filesystem Type 1k-blocks Used Available Use% Mounted on " + ); + } + + #[test] + fn test_header_display_inode() { + let options = Options { + human_readable_base: -1, + show_inode_instead: true, + ..Default::default() + }; + assert_eq!( + Header::new(&options).to_string(), + "Filesystem Inodes IUsed IFree IUse% Mounted on " + ); + } + + #[test] + fn test_header_display_human_readable_binary() { + let options = Options { + human_readable_base: 1024, + ..Default::default() + }; + assert_eq!( + Header::new(&options).to_string(), + "Filesystem Size Used Available Use% Mounted on " + ); + } + + #[test] + fn test_header_display_human_readable_si() { + let options = Options { + human_readable_base: 1000, + ..Default::default() + }; + assert_eq!( + Header::new(&options).to_string(), + "Filesystem Size Used Available Use% Mounted on " + ); + } + + #[test] + fn test_row_display() { + let options = Options { + human_readable_base: -1, + ..Default::default() + }; + let row = Row { + fs_device: "my_device".to_string(), + fs_type: "my_type".to_string(), + fs_mount: "my_mount".to_string(), + + bytes: 100, + bytes_used: 25, + bytes_free: 75, + bytes_usage: Some(0.25), + + #[cfg(target_os = "macos")] + bytes_capacity: Some(0.5), + + inodes: 10, + inodes_used: 2, + inodes_free: 8, + inodes_usage: Some(0.2), + }; + assert_eq!( + DisplayRow::new(row, &options).to_string(), + "my_device 100 25 75 25% my_mount " + ); + } + + #[test] + fn test_row_display_fs_type() { + let options = Options { + human_readable_base: -1, + show_fs_type: true, + ..Default::default() + }; + let row = Row { + fs_device: "my_device".to_string(), + fs_type: "my_type".to_string(), + fs_mount: "my_mount".to_string(), + + bytes: 100, + bytes_used: 25, + bytes_free: 75, + bytes_usage: Some(0.25), + + #[cfg(target_os = "macos")] + bytes_capacity: Some(0.5), + + inodes: 10, + inodes_used: 2, + inodes_free: 8, + inodes_usage: Some(0.2), + }; + assert_eq!( + DisplayRow::new(row, &options).to_string(), + "my_device my_type 100 25 75 25% my_mount " + ); + } + + #[test] + fn test_row_display_inodes() { + let options = Options { + human_readable_base: -1, + show_inode_instead: true, + ..Default::default() + }; + let row = Row { + fs_device: "my_device".to_string(), + fs_type: "my_type".to_string(), + fs_mount: "my_mount".to_string(), + + bytes: 100, + bytes_used: 25, + bytes_free: 75, + bytes_usage: Some(0.25), + + #[cfg(target_os = "macos")] + bytes_capacity: Some(0.5), + + inodes: 10, + inodes_used: 2, + inodes_free: 8, + inodes_usage: Some(0.2), + }; + assert_eq!( + DisplayRow::new(row, &options).to_string(), + "my_device 10 2 8 20% my_mount " + ); + } + + #[test] + fn test_row_display_human_readable_si() { + let options = Options { + human_readable_base: 1000, + show_fs_type: true, + ..Default::default() + }; + let row = Row { + fs_device: "my_device".to_string(), + fs_type: "my_type".to_string(), + fs_mount: "my_mount".to_string(), + + bytes: 4000, + bytes_used: 1000, + bytes_free: 3000, + bytes_usage: Some(0.25), + + #[cfg(target_os = "macos")] + bytes_capacity: Some(0.5), + + inodes: 10, + inodes_used: 2, + inodes_free: 8, + inodes_usage: Some(0.2), + }; + assert_eq!( + DisplayRow::new(row, &options).to_string(), + "my_device my_type 4.0k 1.0k 3.0k 25% my_mount " + ); + } + + #[test] + fn test_row_display_human_readable_binary() { + let options = Options { + human_readable_base: 1024, + show_fs_type: true, + ..Default::default() + }; + let row = Row { + fs_device: "my_device".to_string(), + fs_type: "my_type".to_string(), + fs_mount: "my_mount".to_string(), + + bytes: 4096, + bytes_used: 1024, + bytes_free: 3072, + bytes_usage: Some(0.25), + + #[cfg(target_os = "macos")] + bytes_capacity: Some(0.5), + + inodes: 10, + inodes_used: 2, + inodes_free: 8, + inodes_usage: Some(0.2), + }; + assert_eq!( + DisplayRow::new(row, &options).to_string(), + "my_device my_type 4.0Ki 1.0Ki 3.0Ki 25% my_mount " + ); + } +} From c12f393150c40588b4e118f5bed30d2066dc0922 Mon Sep 17 00:00:00 2001 From: Justin Tracey Date: Sun, 6 Feb 2022 00:48:04 -0500 Subject: [PATCH 075/161] join: improve error handling --- src/uu/join/src/join.rs | 142 ++++++++++++++++++++++++++-------------- 1 file changed, 92 insertions(+), 50 deletions(-) diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index ccd410e44..b8c04925d 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -12,15 +12,47 @@ extern crate uucore; use clap::{crate_version, App, AppSettings, Arg}; use std::cmp::Ordering; +use std::convert::From; +use std::error::Error; +use std::fmt::Display; use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, Split, Stdin, Write}; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; use uucore::display::Quotable; -use uucore::error::{set_exit_code, UResult, USimpleError}; +use uucore::error::{set_exit_code, UError, UResult, USimpleError}; static NAME: &str = "join"; +#[derive(Debug)] +enum JoinError { + IOError(std::io::Error), + UnorderedInput, +} + +impl UError for JoinError { + fn code(&self) -> i32 { + 1 + } +} + +impl Error for JoinError {} + +impl Display for JoinError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + JoinError::IOError(e) => write!(f, "io error: {}", e), + JoinError::UnorderedInput => Ok(()), + } + } +} + +impl From for JoinError { + fn from(error: std::io::Error) -> Self { + Self::IOError(error) + } +} + #[derive(Copy, Clone, PartialEq)] enum FileNum { File1, @@ -310,29 +342,29 @@ impl<'a> State<'a> { } /// Skip the current unpaired line. - fn skip_line(&mut self, input: &Input, repr: &Repr) -> Result<(), std::io::Error> { + fn skip_line(&mut self, input: &Input, repr: &Repr) -> Result<(), JoinError> { if self.print_unpaired { self.print_first_line(repr)?; } - self.reset_next_line(input); + self.reset_next_line(input)?; Ok(()) } /// Keep reading line sequence until the key does not change, return /// the first line whose key differs. - fn extend(&mut self, input: &Input) -> Option { - while let Some(line) = self.next_line(input) { + fn extend(&mut self, input: &Input) -> Result, JoinError> { + while let Some(line) = self.next_line(input)? { let diff = input.compare(self.get_current_key(), line.get_field(self.key)); if diff == Ordering::Equal { self.seq.push(line); } else { - return Some(line); + return Ok(Some(line)); } } - None + Ok(None) } /// Print lines in the buffers as headers. @@ -393,14 +425,16 @@ impl<'a> State<'a> { } } - fn reset_read_line(&mut self, input: &Input) { - let line = self.read_line(input.separator); + fn reset_read_line(&mut self, input: &Input) -> Result<(), std::io::Error> { + let line = self.read_line(input.separator)?; self.reset(line); + Ok(()) } - fn reset_next_line(&mut self, input: &Input) { - let line = self.next_line(input); + fn reset_next_line(&mut self, input: &Input) -> Result<(), JoinError> { + let line = self.next_line(input)?; self.reset(line); + Ok(()) } fn has_line(&self) -> bool { @@ -408,7 +442,7 @@ impl<'a> State<'a> { } fn initialize(&mut self, read_sep: Sep, autoformat: bool) -> usize { - if let Some(line) = self.read_line(read_sep) { + if let Some(line) = crash_if_err!(1, self.read_line(read_sep)) { self.seq.push(line); if autoformat { @@ -418,19 +452,19 @@ impl<'a> State<'a> { 0 } - fn finalize(&mut self, input: &Input, repr: &Repr) -> Result<(), std::io::Error> { + fn finalize(&mut self, input: &Input, repr: &Repr) -> Result<(), JoinError> { if self.has_line() { if self.print_unpaired { self.print_first_line(repr)?; } - let mut next_line = self.next_line(input); + let mut next_line = self.next_line(input)?; while let Some(line) = &next_line { if self.print_unpaired { self.print_line(line, repr)?; } self.reset(next_line); - next_line = self.next_line(input); + next_line = self.next_line(input)?; } } @@ -438,41 +472,49 @@ impl<'a> State<'a> { } /// Get the next line without the order check. - fn read_line(&mut self, sep: Sep) -> Option { - let value = self.lines.next()?; - self.line_num += 1; - Some(Line::new(crash_if_err!(1, value), sep)) + fn read_line(&mut self, sep: Sep) -> Result, std::io::Error> { + match self.lines.next() { + Some(value) => { + self.line_num += 1; + Ok(Some(Line::new(value?, sep))) + } + None => Ok(None), + } } /// Get the next line with the order check. - fn next_line(&mut self, input: &Input) -> Option { - let line = self.read_line(input.separator)?; - - if input.check_order == CheckOrder::Disabled { - return Some(line); - } - - let diff = input.compare(self.get_current_key(), line.get_field(self.key)); - - if diff == Ordering::Greater { - if input.check_order == CheckOrder::Enabled || (self.has_unpaired && !self.has_failed) { - eprintln!( - "{}: {}:{}: is not sorted: {}", - uucore::execution_phrase(), - self.file_name.maybe_quote(), - self.line_num, - String::from_utf8_lossy(&line.string) - ); - - self.has_failed = true; + fn next_line(&mut self, input: &Input) -> Result, JoinError> { + if let Some(line) = self.read_line(input.separator)? { + if input.check_order == CheckOrder::Disabled { + return Ok(Some(line)); } - // This is fatal if the check is enabled. - if input.check_order == CheckOrder::Enabled { - std::process::exit(1); - } - } - Some(line) + let diff = input.compare(self.get_current_key(), line.get_field(self.key)); + + if diff == Ordering::Greater { + if input.check_order == CheckOrder::Enabled + || (self.has_unpaired && !self.has_failed) + { + eprintln!( + "{}: {}:{}: is not sorted: {}", + uucore::execution_phrase(), + self.file_name.maybe_quote(), + self.line_num, + String::from_utf8_lossy(&line.string) + ); + + self.has_failed = true; + } + // This is fatal if the check is enabled. + if input.check_order == CheckOrder::Enabled { + return Err(JoinError::UnorderedInput); + } + } + + Ok(Some(line)) + } else { + Ok(None) + } } /// Gets the key value of the lines stored in seq. @@ -718,7 +760,7 @@ FILENUM is 1 or 2, corresponding to FILE1 or FILE2", ) } -fn exec(file1: &str, file2: &str, settings: Settings) -> Result<(), std::io::Error> { +fn exec(file1: &str, file2: &str, settings: Settings) -> Result<(), JoinError> { let stdin = stdin(); let mut state1 = State::new( @@ -776,8 +818,8 @@ fn exec(file1: &str, file2: &str, settings: Settings) -> Result<(), std::io::Err if settings.headers { state1.print_headers(&state2, &repr)?; - state1.reset_read_line(&input); - state2.reset_read_line(&input); + state1.reset_read_line(&input)?; + state2.reset_read_line(&input)?; } while state1.has_line() && state2.has_line() { @@ -795,8 +837,8 @@ fn exec(file1: &str, file2: &str, settings: Settings) -> Result<(), std::io::Err state2.has_unpaired = true; } Ordering::Equal => { - let next_line1 = state1.extend(&input); - let next_line2 = state2.extend(&input); + let next_line1 = state1.extend(&input)?; + let next_line2 = state2.extend(&input)?; if settings.print_joined { state1.combine(&state2, &repr)?; From e6f59b12f75bc1b44b86be8cbf70d1c94cc61a6e Mon Sep 17 00:00:00 2001 From: Justin Tracey Date: Sun, 6 Feb 2022 01:59:10 -0500 Subject: [PATCH 076/161] join: lock and buffer stdout By abstracting the writer we write to, we can lock stdout once at the beginning, then use buffered writes to it throughout. --- src/uu/join/src/join.rs | 145 ++++++++++++++++++++++++++++------------ 1 file changed, 104 insertions(+), 41 deletions(-) diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index b8c04925d..cb953c133 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -16,7 +16,7 @@ use std::convert::From; use std::error::Error; use std::fmt::Display; use std::fs::File; -use std::io::{stdin, stdout, BufRead, BufReader, Split, Stdin, Write}; +use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Split, Stdin, Write}; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; use uucore::display::Quotable; @@ -144,34 +144,43 @@ impl<'a> Repr<'a> { } /// Print the field or empty filler if the field is not set. - fn print_field(&self, field: Option<&Vec>) -> Result<(), std::io::Error> { + fn print_field( + &self, + writer: &mut impl Write, + field: Option<&Vec>, + ) -> Result<(), std::io::Error> { let value = match field { Some(field) => field, None => self.empty, }; - stdout().write_all(value) + writer.write_all(value) } /// Print each field except the one at the index. - fn print_fields(&self, line: &Line, index: usize) -> Result<(), std::io::Error> { + fn print_fields( + &self, + writer: &mut impl Write, + line: &Line, + index: usize, + ) -> Result<(), std::io::Error> { for i in 0..line.fields.len() { if i != index { - stdout().write_all(&[self.separator])?; - stdout().write_all(&line.fields[i])?; + writer.write_all(&[self.separator])?; + writer.write_all(&line.fields[i])?; } } Ok(()) } /// Print each field or the empty filler if the field is not set. - fn print_format(&self, f: F) -> Result<(), std::io::Error> + fn print_format(&self, writer: &mut impl Write, f: F) -> Result<(), std::io::Error> where F: Fn(&Spec) -> Option<&'a Vec>, { for i in 0..self.format.len() { if i > 0 { - stdout().write_all(&[self.separator])?; + writer.write_all(&[self.separator])?; } let field = match f(&self.format[i]) { @@ -179,13 +188,13 @@ impl<'a> Repr<'a> { None => self.empty, }; - stdout().write_all(field)?; + writer.write_all(field)?; } Ok(()) } - fn print_line_ending(&self) -> Result<(), std::io::Error> { - stdout().write_all(&[self.line_ending as u8]) + fn print_line_ending(&self, writer: &mut impl Write) -> Result<(), std::io::Error> { + writer.write_all(&[self.line_ending as u8]) } } @@ -342,9 +351,14 @@ impl<'a> State<'a> { } /// Skip the current unpaired line. - fn skip_line(&mut self, input: &Input, repr: &Repr) -> Result<(), JoinError> { + fn skip_line( + &mut self, + writer: &mut impl Write, + input: &Input, + repr: &Repr, + ) -> Result<(), JoinError> { if self.print_unpaired { - self.print_first_line(repr)?; + self.print_first_line(writer, repr)?; } self.reset_next_line(input)?; @@ -368,28 +382,38 @@ impl<'a> State<'a> { } /// Print lines in the buffers as headers. - fn print_headers(&self, other: &State, repr: &Repr) -> Result<(), std::io::Error> { + fn print_headers( + &self, + writer: &mut impl Write, + other: &State, + repr: &Repr, + ) -> Result<(), std::io::Error> { if self.has_line() { if other.has_line() { - self.combine(other, repr)?; + self.combine(writer, other, repr)?; } else { - self.print_first_line(repr)?; + self.print_first_line(writer, repr)?; } } else if other.has_line() { - other.print_first_line(repr)?; + other.print_first_line(writer, repr)?; } Ok(()) } /// Combine two line sequences. - fn combine(&self, other: &State, repr: &Repr) -> Result<(), std::io::Error> { + fn combine( + &self, + writer: &mut impl Write, + other: &State, + repr: &Repr, + ) -> Result<(), std::io::Error> { let key = self.get_current_key(); for line1 in &self.seq { for line2 in &other.seq { if repr.uses_format() { - repr.print_format(|spec| match *spec { + repr.print_format(writer, |spec| match *spec { Spec::Key => key, Spec::Field(file_num, field_num) => { if file_num == self.file_num { @@ -404,12 +428,12 @@ impl<'a> State<'a> { } })?; } else { - repr.print_field(key)?; - repr.print_fields(line1, self.key)?; - repr.print_fields(line2, other.key)?; + repr.print_field(writer, key)?; + repr.print_fields(writer, line1, self.key)?; + repr.print_fields(writer, line2, other.key)?; } - repr.print_line_ending()?; + repr.print_line_ending(writer)?; } } @@ -452,16 +476,21 @@ impl<'a> State<'a> { 0 } - fn finalize(&mut self, input: &Input, repr: &Repr) -> Result<(), JoinError> { + fn finalize( + &mut self, + writer: &mut impl Write, + input: &Input, + repr: &Repr, + ) -> Result<(), JoinError> { if self.has_line() { if self.print_unpaired { - self.print_first_line(repr)?; + self.print_first_line(writer, repr)?; } let mut next_line = self.next_line(input)?; while let Some(line) = &next_line { if self.print_unpaired { - self.print_line(line, repr)?; + self.print_line(writer, line, repr)?; } self.reset(next_line); next_line = self.next_line(input)?; @@ -522,9 +551,14 @@ impl<'a> State<'a> { self.seq[0].get_field(self.key) } - fn print_line(&self, line: &Line, repr: &Repr) -> Result<(), std::io::Error> { + fn print_line( + &self, + writer: &mut impl Write, + line: &Line, + repr: &Repr, + ) -> Result<(), std::io::Error> { if repr.uses_format() { - repr.print_format(|spec| match *spec { + repr.print_format(writer, |spec| match *spec { Spec::Key => line.get_field(self.key), Spec::Field(file_num, field_num) => { if file_num == self.file_num { @@ -535,15 +569,15 @@ impl<'a> State<'a> { } })?; } else { - repr.print_field(line.get_field(self.key))?; - repr.print_fields(line, self.key)?; + repr.print_field(writer, line.get_field(self.key))?; + repr.print_fields(writer, line, self.key)?; } - repr.print_line_ending() + repr.print_line_ending(writer) } - fn print_first_line(&self, repr: &Repr) -> Result<(), std::io::Error> { - self.print_line(&self.seq[0], repr) + fn print_first_line(&self, writer: &mut impl Write, repr: &Repr) -> Result<(), std::io::Error> { + self.print_line(writer, &self.seq[0], repr) } } @@ -816,8 +850,11 @@ fn exec(file1: &str, file2: &str, settings: Settings) -> Result<(), JoinError> { &settings.empty, ); + let stdout = stdout(); + let mut writer = BufWriter::new(stdout.lock()); + if settings.headers { - state1.print_headers(&state2, &repr)?; + state1.print_headers(&mut writer, &state2, &repr)?; state1.reset_read_line(&input)?; state2.reset_read_line(&input)?; } @@ -827,21 +864,39 @@ fn exec(file1: &str, file2: &str, settings: Settings) -> Result<(), JoinError> { match diff { Ordering::Less => { - state1.skip_line(&input, &repr)?; + if let Err(e) = state1.skip_line(&mut writer, &input, &repr) { + writer.flush()?; + return Err(e); + } state1.has_unpaired = true; state2.has_unpaired = true; } Ordering::Greater => { - state2.skip_line(&input, &repr)?; + if let Err(e) = state2.skip_line(&mut writer, &input, &repr) { + writer.flush()?; + return Err(e); + } state1.has_unpaired = true; state2.has_unpaired = true; } Ordering::Equal => { - let next_line1 = state1.extend(&input)?; - let next_line2 = state2.extend(&input)?; + let next_line1 = match state1.extend(&input) { + Ok(line) => line, + Err(e) => { + writer.flush()?; + return Err(e); + } + }; + let next_line2 = match state2.extend(&input) { + Ok(line) => line, + Err(e) => { + writer.flush()?; + return Err(e); + } + }; if settings.print_joined { - state1.combine(&state2, &repr)?; + state1.combine(&mut writer, &state2, &repr)?; } state1.reset(next_line1); @@ -850,8 +905,16 @@ fn exec(file1: &str, file2: &str, settings: Settings) -> Result<(), JoinError> { } } - state1.finalize(&input, &repr)?; - state2.finalize(&input, &repr)?; + if let Err(e) = state1.finalize(&mut writer, &input, &repr) { + writer.flush()?; + return Err(e); + }; + if let Err(e) = state2.finalize(&mut writer, &input, &repr) { + writer.flush()?; + return Err(e); + }; + + writer.flush()?; if state1.has_failed || state2.has_failed { eprintln!( From f33e058a5a3af79ee5ca7654aa2a0f372b75ae0a Mon Sep 17 00:00:00 2001 From: Justin Tracey Date: Sun, 6 Feb 2022 02:17:25 -0500 Subject: [PATCH 077/161] join: faster field parsing and representation Using indexes into the line instead of Vecs means we don't have to copy the line to store the fields (indexes instead of slices because it avoids self-referential structs). Using memchr also empirically saves a lot of intermediate allocations. --- Cargo.lock | 1 + src/uu/join/Cargo.toml | 1 + src/uu/join/src/join.rs | 58 +++++++++++++++++++++++++---------------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc7c3967b..b1c374651 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2444,6 +2444,7 @@ name = "uu_join" version = "0.0.12" dependencies = [ "clap 3.0.10", + "memchr 2.4.1", "uucore", ] diff --git a/src/uu/join/Cargo.toml b/src/uu/join/Cargo.toml index b4150b4be..2a03fe002 100644 --- a/src/uu/join/Cargo.toml +++ b/src/uu/join/Cargo.toml @@ -17,6 +17,7 @@ path = "src/join.rs" [dependencies] clap = { version = "3.0", features = ["wrap_help", "cargo"] } uucore = { version=">=0.0.11", package="uucore", path="../../uucore" } +memchr = "2" [[bin]] name = "join" diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index cb953c133..dcd699438 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -11,6 +11,7 @@ extern crate uucore; use clap::{crate_version, App, AppSettings, Arg}; +use memchr::{memchr3_iter, memchr_iter}; use std::cmp::Ordering; use std::convert::From; use std::error::Error; @@ -66,7 +67,7 @@ enum LineEnding { Newline = b'\n', } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq)] enum Sep { Char(u8), Line, @@ -147,7 +148,7 @@ impl<'a> Repr<'a> { fn print_field( &self, writer: &mut impl Write, - field: Option<&Vec>, + field: Option<&[u8]>, ) -> Result<(), std::io::Error> { let value = match field { Some(field) => field, @@ -164,10 +165,10 @@ impl<'a> Repr<'a> { line: &Line, index: usize, ) -> Result<(), std::io::Error> { - for i in 0..line.fields.len() { + for i in 0..line.field_ranges.len() { if i != index { writer.write_all(&[self.separator])?; - writer.write_all(&line.fields[i])?; + writer.write_all(line.get_field(i).unwrap())?; } } Ok(()) @@ -176,7 +177,7 @@ impl<'a> Repr<'a> { /// Print each field or the empty filler if the field is not set. fn print_format(&self, writer: &mut impl Write, f: F) -> Result<(), std::io::Error> where - F: Fn(&Spec) -> Option<&'a Vec>, + F: Fn(&Spec) -> Option<&'a [u8]>, { for i in 0..self.format.len() { if i > 0 { @@ -214,7 +215,7 @@ impl Input { } } - fn compare(&self, field1: Option<&Vec>, field2: Option<&Vec>) -> Ordering { + fn compare(&self, field1: Option<&[u8]>, field2: Option<&[u8]>) -> Ordering { if let (Some(field1), Some(field2)) = (field1, field2) { if self.ignore_case { field1 @@ -277,30 +278,41 @@ impl Spec { } struct Line { - fields: Vec>, + field_ranges: Vec<(usize, usize)>, string: Vec, } impl Line { fn new(string: Vec, separator: Sep) -> Self { - let fields = match separator { - Sep::Whitespaces => string - // GNU join uses Bourne shell field splitters by default - .split(|c| matches!(*c, b' ' | b'\t' | b'\n')) - .filter(|f| !f.is_empty()) - .map(Vec::from) - .collect(), - Sep::Char(sep) => string.split(|c| *c == sep).map(Vec::from).collect(), - Sep::Line => vec![string.clone()], - }; + let mut field_ranges = Vec::new(); + let mut last_end = 0; + if separator == Sep::Whitespaces { + // GNU join uses Bourne shell field splitters by default + for i in memchr3_iter(b' ', b'\t', b'\n', &string) { + if i > last_end { + field_ranges.push((last_end, i)); + } + last_end = i + 1; + } + } else if let Sep::Char(sep) = separator { + for i in memchr_iter(sep, &string) { + field_ranges.push((last_end, i)); + last_end = i + 1; + } + } + field_ranges.push((last_end, string.len())); - Self { fields, string } + Self { + field_ranges, + string, + } } /// Get field at index. - fn get_field(&self, index: usize) -> Option<&Vec> { - if index < self.fields.len() { - Some(&self.fields[index]) + fn get_field(&self, index: usize) -> Option<&[u8]> { + if index < self.field_ranges.len() { + let (low, high) = self.field_ranges[index]; + Some(&self.string[low..high]) } else { None } @@ -470,7 +482,7 @@ impl<'a> State<'a> { self.seq.push(line); if autoformat { - return self.seq[0].fields.len(); + return self.seq[0].field_ranges.len(); } } 0 @@ -547,7 +559,7 @@ impl<'a> State<'a> { } /// Gets the key value of the lines stored in seq. - fn get_current_key(&self) -> Option<&Vec> { + fn get_current_key(&self) -> Option<&[u8]> { self.seq[0].get_field(self.key) } From ac9d0068861e97bc892bb8fe5608b3f4a2012d5f Mon Sep 17 00:00:00 2001 From: Justin Tracey Date: Sun, 6 Feb 2022 02:22:54 -0500 Subject: [PATCH 078/161] join: guess the number of fields in each line This lets us use fewer reallocations when parsing each line. The current guess is set to the maximum fields in a line so far. This is a free performance win in the common case where each line has the same number of fields, but comes with some memory overhead in the case where there is a line with lots of fields at the beginning of the file, and fewer later, but each of these lines are typically not kept for very long anyway. --- src/uu/join/src/join.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index dcd699438..861d1684a 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -283,8 +283,8 @@ struct Line { } impl Line { - fn new(string: Vec, separator: Sep) -> Self { - let mut field_ranges = Vec::new(); + fn new(string: Vec, separator: Sep, len_guess: usize) -> Self { + let mut field_ranges = Vec::with_capacity(len_guess); let mut last_end = 0; if separator == Sep::Whitespaces { // GNU join uses Bourne shell field splitters by default @@ -325,6 +325,7 @@ struct State<'a> { file_num: FileNum, print_unpaired: bool, lines: Split>, + max_len: usize, seq: Vec, line_num: usize, has_failed: bool, @@ -355,6 +356,7 @@ impl<'a> State<'a> { file_num, print_unpaired, lines: f.split(line_ending as u8), + max_len: 1, seq: Vec::new(), line_num: 0, has_failed: false, @@ -517,7 +519,11 @@ impl<'a> State<'a> { match self.lines.next() { Some(value) => { self.line_num += 1; - Ok(Some(Line::new(value?, sep))) + let line = Line::new(value?, sep, self.max_len); + if line.field_ranges.len() > self.max_len { + self.max_len = line.field_ranges.len(); + } + Ok(Some(line)) } None => Ok(None), } From 41c90d79c44af02982dc69875ceea15977b90cdb Mon Sep 17 00:00:00 2001 From: Justin Tracey Date: Sun, 6 Feb 2022 22:35:13 -0500 Subject: [PATCH 079/161] join: add benchmarking documentation --- src/uu/join/BENCHMARKING.md | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/uu/join/BENCHMARKING.md diff --git a/src/uu/join/BENCHMARKING.md b/src/uu/join/BENCHMARKING.md new file mode 100644 index 000000000..07a006b2d --- /dev/null +++ b/src/uu/join/BENCHMARKING.md @@ -0,0 +1,55 @@ +# Benchmarking join + + + +## Performance profile + +The amount of time spent in which part of the code can vary depending on the files being joined and the flags used. +A benchmark with `-j` and `-i` shows the following time: + +| Function/Method | Fraction of Samples | Why? | +| ---------------- | ------------------- | ---- | +| `Line::new` | 27% | Linear search for field separators, plus some vector operations. | +| `read_until` | 22% | Mostly libc reading file contents, with a few vector operations to represent them. | +| `Input::compare` | 20% | ~2/3 making the keys lowercase, ~1/3 comparing them. | +| `print_fields` | 11% | Writing to and flushing the buffer. | +| Other | 20% | | +| libc | 25% | I/O and memory allocation. | + +More detailed profiles can be obtained via [flame graphs](https://github.com/flamegraph-rs/flamegraph): +``` +cargo flamegraph --bin join --package uu_join -- file1 file2 > /dev/null +``` +You may need to add the following lines to the top-level `Cargo.toml` to get full stack traces: +``` +[profile.release] +debug = true +``` + +## How to benchmark + +Benchmarking typically requires files large enough to ensure that the benchmark is not overwhelmed by background system noise; say, on the order of tens of MB. +While `join` operates on line-oriented data, and not properly formatted CSVs (e.g., `join` is not designed to accommodate escaped or quoted delimiters), +in practice many CSV datasets will function well after being sorted. + +Like most of the utils, the recommended tool for benchmarking is [hyperfine](https://github.com/sharkdp/hyperfine). +To benchmark your changes: + - checkout the main branch (without your changes), do a `--release` build, and back up the executable produced at `target/release/join` + - checkout your working branch (with your changes), do a `--release` build + - run + ``` + hyperfine -w 5 "/path/to/main/branch/build/join file1 file2" "/path/to/working/branch/build/join file1 file2" + ``` + - you'll likely need to add additional options to both commands, such as a field separator, or if you're benchmarking some particular behavior + - you can also optionally benchmark against GNU's join + +## What to benchmark + +The following options can have a non-trivial impact on performance: + - `-a`/`-v` if one of the two files has significantly more lines than the other + - `-j`/`-1`/`-2` cause work to be done to grab the appropriate field + - `-i` adds a call to `to_ascii_lowercase()` that adds some time for allocating and dropping memory for the lowercase key + - `--nocheck-order` causes some calls of `Input::compare` to be skipped + +The content of the files being joined has a very significant impact on the performance. +Things like how long each line is, how many fields there are, how long the key fields are, how many lines there are, how many lines can be joined, and how many lines each line can be joined with all change the behavior of the hotpaths. From e6c94c1cd73ec21ac520dfa14aa3d018902c2280 Mon Sep 17 00:00:00 2001 From: Allan Silva Date: Mon, 7 Feb 2022 10:20:52 -0300 Subject: [PATCH 080/161] wc: Fix clippy error --- src/uu/wc/src/wc.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 2afbe4e21..97ea26b8b 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -123,9 +123,9 @@ enum Input { impl From<&OsStr> for Input { fn from(input: &OsStr) -> Self { if input == STDIN_REPR { - Input::Stdin(StdinKind::Explicit) + Self::Stdin(StdinKind::Explicit) } else { - Input::Path(input.into()) + Self::Path(input.into()) } } } From c002b16c6715758e57870ef94ef0f4fa213bc04e Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Mon, 7 Feb 2022 10:00:49 -0500 Subject: [PATCH 081/161] dd: make main loop more concise Add some helper functions and adjust some error-handling to make the `Output::dd_out()` method, containing the main loop of the `dd` program, more concise. This commit also adds documentation and comments describing the main loop procedure in more detail. --- src/uu/dd/src/datastructures.rs | 24 +++- src/uu/dd/src/dd.rs | 191 ++++++++++++++++++-------------- 2 files changed, 133 insertions(+), 82 deletions(-) diff --git a/src/uu/dd/src/datastructures.rs b/src/uu/dd/src/datastructures.rs index 8fab1ffec..8380965a9 100644 --- a/src/uu/dd/src/datastructures.rs +++ b/src/uu/dd/src/datastructures.rs @@ -19,12 +19,34 @@ pub struct ProgUpdate { pub duration: time::Duration, } +impl ProgUpdate { + pub(crate) fn new( + read_stat: ReadStat, + write_stat: WriteStat, + duration: time::Duration, + ) -> Self { + Self { + read_stat, + write_stat, + duration, + } + } +} + #[derive(Clone, Copy, Default)] pub struct ReadStat { pub reads_complete: u64, pub reads_partial: u64, pub records_truncated: u32, } + +impl ReadStat { + /// Whether this counter has zero complete reads and zero partial reads. + pub(crate) fn is_empty(&self) -> bool { + self.reads_complete == 0 && self.reads_partial == 0 + } +} + impl std::ops::AddAssign for ReadStat { fn add_assign(&mut self, other: Self) { *self = Self { @@ -35,7 +57,7 @@ impl std::ops::AddAssign for ReadStat { } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] pub struct WriteStat { pub writes_complete: u64, pub writes_partial: u64, diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 54e3190ce..547e317c5 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -347,78 +347,111 @@ where }) } - fn dd_out(mut self, mut i: Input) -> UResult<()> { - let mut rstat = ReadStat { - reads_complete: 0, - reads_partial: 0, - records_truncated: 0, - }; - let mut wstat = WriteStat { - writes_complete: 0, - writes_partial: 0, - bytes_total: 0, - }; - let start = time::Instant::now(); - let bsize = calc_bsize(i.ibs, self.obs); - - let prog_tx = { - let (tx, rx) = mpsc::channel(); - thread::spawn(gen_prog_updater(rx, i.print_level)); - tx - }; - - while below_count_limit(&i.count, &rstat, &wstat) { - // Read/Write - let loop_bsize = calc_loop_bsize(&i.count, &rstat, &wstat, i.ibs, bsize); - match read_helper(&mut i, loop_bsize)? { - ( - ReadStat { - reads_complete: 0, - reads_partial: 0, - .. - }, - _, - ) => break, - (rstat_update, buf) => { - let wstat_update = self - .write_blocks(&buf) - .map_err_context(|| "failed to write output".to_string())?; - - rstat += rstat_update; - wstat += wstat_update; - } - }; - // Update Prog - prog_tx - .send(ProgUpdate { - read_stat: rstat, - write_stat: wstat, - duration: start.elapsed(), - }) - .map_err(|_| USimpleError::new(1, "failed to write output"))?; - } - - if self.cflags.fsync { - self.fsync() - .map_err_context(|| "failed to write output".to_string())?; - } else if self.cflags.fdatasync { - self.fdatasync() - .map_err_context(|| "failed to write output".to_string())?; - } - + /// Print the read/write statistics. + fn print_stats(&self, i: &Input, prog_update: &ProgUpdate) { match i.print_level { Some(StatusLevel::None) => {} - Some(StatusLevel::Noxfer) => print_io_lines(&ProgUpdate { - read_stat: rstat, - write_stat: wstat, - duration: start.elapsed(), - }), - Some(StatusLevel::Progress) | None => print_transfer_stats(&ProgUpdate { - read_stat: rstat, - write_stat: wstat, - duration: start.elapsed(), - }), + Some(StatusLevel::Noxfer) => print_io_lines(prog_update), + Some(StatusLevel::Progress) | None => print_transfer_stats(prog_update), } + } + + /// Flush the output to disk, if configured to do so. + fn sync(&mut self) -> std::io::Result<()> { + if self.cflags.fsync { + self.fsync() + } else if self.cflags.fdatasync { + self.fdatasync() + } else { + // Intentionally do nothing in this case. + Ok(()) + } + } + + /// Copy the given input data to this output, consuming both. + /// + /// This method contains the main loop for the `dd` program. Bytes + /// are read in blocks from `i` and written in blocks to this + /// output. Read/write statistics are reported to stderr as + /// configured by the `status` command-line argument. + /// + /// # Errors + /// + /// If there is a problem reading from the input or writing to + /// this output. + fn dd_out(mut self, mut i: Input) -> std::io::Result<()> { + // The read and write statistics. + // + // These objects are counters, initialized to zero. After each + // iteration of the main loop, each will be incremented by the + // number of blocks read and written, respectively. + let mut rstat = Default::default(); + let mut wstat = Default::default(); + + // The time at which the main loop starts executing. + // + // When `status=progress` is given on the command-line, the + // `dd` program reports its progress every second or so. Part + // of its report includes the throughput in bytes per second, + // which requires knowing how long the process has been + // running. + let start = time::Instant::now(); + + // A good buffer size for reading. + // + // This is an educated guess about a good buffer size based on + // the input and output block sizes. + let bsize = calc_bsize(i.ibs, self.obs); + + // Start a thread that reports transfer progress. + // + // When `status=progress` is given on the command-line, the + // `dd` program reports its progress every second or so. We + // perform this reporting in a new thread so as not to take + // any CPU time away from the actual reading and writing of + // data. We send a `ProgUpdate` from the transmitter `prog_tx` + // to the receives `rx`, and the receiver prints the transfer + // information. + let (prog_tx, rx) = mpsc::channel(); + thread::spawn(gen_prog_updater(rx, i.print_level)); + + // The main read/write loop. + // + // Each iteration reads blocks from the input and writes + // blocks to this output. Read/write statistics are updated on + // each iteration and cumulative statistics are reported to + // the progress reporting thread. + while below_count_limit(&i.count, &rstat, &wstat) { + // Read a block from the input then write the block to the output. + // + // As an optimization, make an educated guess about the + // best buffer size for reading based on the number of + // blocks already read and the number of blocks remaining. + let loop_bsize = calc_loop_bsize(&i.count, &rstat, &wstat, i.ibs, bsize); + let (rstat_update, buf) = read_helper(&mut i, loop_bsize)?; + if rstat_update.is_empty() { + break; + } + let wstat_update = self.write_blocks(&buf)?; + + // Update the read/write stats and inform the progress thread. + // + // If the receiver is disconnected, `send()` returns an + // error. Since it is just reporting progress and is not + // crucial to the operation of `dd`, let's just ignore the + // error. + rstat += rstat_update; + wstat += wstat_update; + let prog_update = ProgUpdate::new(rstat, wstat, start.elapsed()); + prog_tx.send(prog_update).unwrap_or(()); + } + + // Flush the output, if configured to do so. + self.sync()?; + + // Print the final read/write statistics. + let prog_update = ProgUpdate::new(rstat, wstat, start.elapsed()); + self.print_stats(&i, &prog_update); Ok(()) } } @@ -698,7 +731,7 @@ fn conv_block_unblock_helper( } /// Read helper performs read operations common to all dd reads, and dispatches the buffer to relevant helper functions as dictated by the operations requested by the user. -fn read_helper(i: &mut Input, bsize: usize) -> UResult<(ReadStat, Vec)> { +fn read_helper(i: &mut Input, bsize: usize) -> std::io::Result<(ReadStat, Vec)> { // Local Predicate Fns ----------------------------------------------- fn is_conv(i: &Input) -> bool { i.cflags.ctable.is_some() @@ -719,12 +752,8 @@ fn read_helper(i: &mut Input, bsize: usize) -> UResult<(ReadStat, Ve // Read let mut buf = vec![BUF_INIT_BYTE; bsize]; let mut rstat = match i.cflags.sync { - Some(ch) => i - .fill_blocks(&mut buf, ch) - .map_err_context(|| "failed to write output".to_string())?, - _ => i - .fill_consecutive(&mut buf) - .map_err_context(|| "failed to write output".to_string())?, + Some(ch) => i.fill_blocks(&mut buf, ch)?, + _ => i.fill_consecutive(&mut buf)?, }; // Return early if no data if rstat.reads_complete == 0 && rstat.reads_partial == 0 { @@ -736,7 +765,7 @@ fn read_helper(i: &mut Input, bsize: usize) -> UResult<(ReadStat, Ve perform_swab(&mut buf); } if is_conv(i) || is_block(i) || is_unblock(i) { - let buf = conv_block_unblock_helper(buf, i, &mut rstat)?; + let buf = conv_block_unblock_helper(buf, i, &mut rstat).unwrap(); Ok((rstat, buf)) } else { Ok((rstat, buf)) @@ -926,22 +955,22 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { (true, true) => { let i = Input::::new(&matches)?; let o = Output::::new(&matches)?; - o.dd_out(i) + o.dd_out(i).map_err_context(|| "IO error".to_string()) } (false, true) => { let i = Input::::new(&matches)?; let o = Output::::new(&matches)?; - o.dd_out(i) + o.dd_out(i).map_err_context(|| "IO error".to_string()) } (true, false) => { let i = Input::::new(&matches)?; let o = Output::::new(&matches)?; - o.dd_out(i) + o.dd_out(i).map_err_context(|| "IO error".to_string()) } (false, false) => { let i = Input::::new(&matches)?; let o = Output::::new(&matches)?; - o.dd_out(i) + o.dd_out(i).map_err_context(|| "IO error".to_string()) } } } From bf67c5d981b2bc64a3147b9ee28208b6e2f5bed7 Mon Sep 17 00:00:00 2001 From: Justin Tracey Date: Mon, 7 Feb 2022 22:07:38 -0500 Subject: [PATCH 082/161] join: add tests for --check-order and stdout error --- tests/by-util/test_join.rs | 60 ++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/tests/by-util/test_join.rs b/tests/by-util/test_join.rs index 51e3d98cc..dbc0bf411 100644 --- a/tests/by-util/test_join.rs +++ b/tests/by-util/test_join.rs @@ -1,6 +1,8 @@ -// spell-checker:ignore (words) autoformat +// spell-checker:ignore (words) autoformat nocheck use crate::common::util::*; +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +use std::fs::OpenOptions; #[cfg(unix)] use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; #[cfg(windows)] @@ -306,6 +308,16 @@ fn missing_format_fields() { .stdout_only_fixture("missing_format_fields.expected"); } +#[test] +fn nocheck_order() { + new_ucmd!() + .arg("fields_1.txt") + .arg("fields_2.txt") + .arg("--nocheck-order") + .succeeds() + .stdout_only_fixture("default.expected"); +} + #[test] fn wrong_line_order() { let ts = TestScenario::new(util_name!()); @@ -313,11 +325,24 @@ fn wrong_line_order() { .arg("fields_2.txt") .arg("fields_4.txt") .fails() + .stdout_contains("7 g f 4 fg") .stderr_is(&format!( - "{0} {1}: fields_4.txt:5: is not sorted: 11 g 5 gh\n{0} {1}: input is not in sorted order", - ts.bin_path.to_string_lossy(), - ts.util_name - )); + "{0} {1}: fields_4.txt:5: is not sorted: 11 g 5 gh\n{0} {1}: input is not in sorted order", + ts.bin_path.to_string_lossy(), + ts.util_name + )); + + new_ucmd!() + .arg("--check-order") + .arg("fields_2.txt") + .arg("fields_4.txt") + .fails() + .stdout_does_not_contain("7 g f 4 fg") + .stderr_is(&format!( + "{0} {1}: fields_4.txt:5: is not sorted: 11 g 5 gh", + ts.bin_path.to_string_lossy(), + ts.util_name + )); } #[test] @@ -327,11 +352,24 @@ fn both_files_wrong_line_order() { .arg("fields_4.txt") .arg("fields_5.txt") .fails() + .stdout_contains("5 e 3 ef") .stderr_is(&format!( "{0} {1}: fields_5.txt:4: is not sorted: 3\n{0} {1}: fields_4.txt:5: is not sorted: 11 g 5 gh\n{0} {1}: input is not in sorted order", ts.bin_path.to_string_lossy(), ts.util_name )); + + new_ucmd!() + .arg("--check-order") + .arg("fields_4.txt") + .arg("fields_5.txt") + .fails() + .stdout_does_not_contain("5 e 3 ef") + .stderr_is(&format!( + "{0} {1}: fields_5.txt:4: is not sorted: 3", + ts.bin_path.to_string_lossy(), + ts.util_name + )); } #[test] @@ -437,3 +475,15 @@ fn null_line_endings() { .succeeds() .stdout_only_fixture("z.expected"); } + +#[test] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +fn test_full() { + let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap(); + new_ucmd!() + .arg("fields_1.txt") + .arg("fields_2.txt") + .set_stdout(dev_full) + .fails() + .stderr_contains("No space left on device"); +} From b873d46ca06f3b72dc4e6f2465a64e96fc3da686 Mon Sep 17 00:00:00 2001 From: Justin Tracey Date: Mon, 7 Feb 2022 22:08:37 -0500 Subject: [PATCH 083/161] join: flush stdout before final error message --- src/uu/join/src/join.rs | 32 +++++++++++++++----------------- tests/by-util/test_join.rs | 6 ++---- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index 861d1684a..e5abefba4 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -28,7 +28,7 @@ static NAME: &str = "join"; #[derive(Debug)] enum JoinError { IOError(std::io::Error), - UnorderedInput, + UnorderedInput(String), } impl UError for JoinError { @@ -43,7 +43,7 @@ impl Display for JoinError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { JoinError::IOError(e) => write!(f, "io error: {}", e), - JoinError::UnorderedInput => Ok(()), + JoinError::UnorderedInput(e) => f.write_str(e), } } } @@ -538,24 +538,22 @@ impl<'a> State<'a> { let diff = input.compare(self.get_current_key(), line.get_field(self.key)); - if diff == Ordering::Greater { - if input.check_order == CheckOrder::Enabled - || (self.has_unpaired && !self.has_failed) - { - eprintln!( - "{}: {}:{}: is not sorted: {}", - uucore::execution_phrase(), - self.file_name.maybe_quote(), - self.line_num, - String::from_utf8_lossy(&line.string) - ); - - self.has_failed = true; - } + if diff == Ordering::Greater + && (input.check_order == CheckOrder::Enabled + || (self.has_unpaired && !self.has_failed)) + { + let err_msg = format!( + "{}:{}: is not sorted: {}", + self.file_name.maybe_quote(), + self.line_num, + String::from_utf8_lossy(&line.string) + ); // This is fatal if the check is enabled. if input.check_order == CheckOrder::Enabled { - return Err(JoinError::UnorderedInput); + return Err(JoinError::UnorderedInput(err_msg)); } + eprintln!("{}: {}", uucore::execution_phrase(), err_msg); + self.has_failed = true; } Ok(Some(line)) diff --git a/tests/by-util/test_join.rs b/tests/by-util/test_join.rs index dbc0bf411..d5da873f6 100644 --- a/tests/by-util/test_join.rs +++ b/tests/by-util/test_join.rs @@ -339,8 +339,7 @@ fn wrong_line_order() { .fails() .stdout_does_not_contain("7 g f 4 fg") .stderr_is(&format!( - "{0} {1}: fields_4.txt:5: is not sorted: 11 g 5 gh", - ts.bin_path.to_string_lossy(), + "{0}: fields_4.txt:5: is not sorted: 11 g 5 gh", ts.util_name )); } @@ -366,8 +365,7 @@ fn both_files_wrong_line_order() { .fails() .stdout_does_not_contain("5 e 3 ef") .stderr_is(&format!( - "{0} {1}: fields_5.txt:4: is not sorted: 3", - ts.bin_path.to_string_lossy(), + "{0}: fields_5.txt:4: is not sorted: 3", ts.util_name )); } From 30ae952b83e1bd5f9d3e4475fcddffa4fba4afc5 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 8 Feb 2022 14:12:31 +0100 Subject: [PATCH 084/161] shuf: remove custom randomization logic --- src/uu/shuf/src/shuf.rs | 87 ++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index da2dfff1b..058ac8637 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -8,7 +8,8 @@ // spell-checker:ignore (ToDO) cmdline evec seps rvec fdata use clap::{crate_version, App, AppSettings, Arg}; -use rand::Rng; +use rand::prelude::SliceRandom; +use rand::RngCore; use std::fs::File; use std::io::{stdin, stdout, BufReader, BufWriter, Read, Write}; use uucore::display::Quotable; @@ -254,40 +255,35 @@ fn shuf_bytes(input: &mut Vec<&[u8]>, opts: Options) -> UResult<()> { None => WrappedRng::RngDefault(rand::thread_rng()), }; - // we're generating a random usize. To keep things fair, we take this number mod ceil(log2(length+1)) - let mut len_mod = 1; - let mut len = input.len(); - while len > 0 { - len >>= 1; - len_mod <<= 1; + if input.is_empty() { + return Ok(()); } - let mut count = opts.head_count; - while count > 0 && !input.is_empty() { - let mut r = input.len(); - while r >= input.len() { - r = rng.next_usize() % len_mod; + if opts.repeat { + for _ in 0..opts.head_count { + // Returns None is the slice is empty. We checked this before, so + // this is safe. + let r = input.choose(&mut rng).unwrap(); + + output + .write_all(r) + .map_err_context(|| "write failed".to_string())?; + output + .write_all(&[opts.sep]) + .map_err_context(|| "write failed".to_string())?; } - - // write the randomly chosen value and the separator - output - .write_all(input[r]) - .map_err_context(|| "write failed".to_string())?; - output - .write_all(&[opts.sep]) - .map_err_context(|| "write failed".to_string())?; - - // if we do not allow repeats, remove the chosen value from the input vector - if !opts.repeat { - // shrink the mask if we will drop below a power of 2 - if input.len() % 2 == 0 && len_mod > 2 { - len_mod >>= 1; - } - input.swap_remove(r); + } else { + let (shuffled, _) = input.partial_shuffle(&mut rng, opts.head_count); + for r in shuffled { + output + .write_all(r) + .map_err_context(|| "write failed".to_string())?; + output + .write_all(&[opts.sep]) + .map_err_context(|| "write failed".to_string())?; } - - count -= 1; } + Ok(()) } @@ -311,11 +307,32 @@ enum WrappedRng { RngDefault(rand::rngs::ThreadRng), } -impl WrappedRng { - fn next_usize(&mut self) -> usize { - match *self { - WrappedRng::RngFile(ref mut r) => r.gen(), - WrappedRng::RngDefault(ref mut r) => r.gen(), +impl RngCore for WrappedRng { + fn next_u32(&mut self) -> u32 { + match self { + Self::RngFile(r) => r.next_u32(), + Self::RngDefault(r) => r.next_u32(), + } + } + + fn next_u64(&mut self) -> u64 { + match self { + Self::RngFile(r) => r.next_u64(), + Self::RngDefault(r) => r.next_u64(), + } + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + match self { + Self::RngFile(r) => r.fill_bytes(dest), + Self::RngDefault(r) => r.fill_bytes(dest), + } + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> { + match self { + Self::RngFile(r) => r.try_fill_bytes(dest), + Self::RngDefault(r) => r.try_fill_bytes(dest), } } } From 95388147020b2687e04959327a6962fe37654844 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 8 Feb 2022 14:20:24 +0100 Subject: [PATCH 085/161] shuf: use split_once for parsing the range --- src/uu/shuf/src/shuf.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 058ac8637..3dcd7b0e2 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -288,17 +288,16 @@ fn shuf_bytes(input: &mut Vec<&[u8]>, opts: Options) -> UResult<()> { } fn parse_range(input_range: &str) -> Result<(usize, usize), String> { - let split: Vec<&str> = input_range.split('-').collect(); - if split.len() != 2 { - Err(format!("invalid input range: {}", input_range.quote())) - } else { - let begin = split[0] + if let Some((from, to)) = input_range.split_once('-') { + let begin = from .parse::() - .map_err(|_| format!("invalid input range: {}", split[0].quote()))?; - let end = split[1] + .map_err(|_| format!("invalid input range: {}", from.quote()))?; + let end = to .parse::() - .map_err(|_| format!("invalid input range: {}", split[1].quote()))?; + .map_err(|_| format!("invalid input range: {}", to.quote()))?; Ok((begin, end + 1)) + } else { + Err(format!("invalid input range: {}", input_range.quote())) } } From 2b59b011f6a1ab9abc77bec97aaf3c9541766dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dylan=20A=C3=AFssi?= Date: Tue, 8 Feb 2022 15:50:09 +0100 Subject: [PATCH 086/161] dd - add dd in GNUmakefile --- GNUmakefile | 1 + 1 file changed, 1 insertion(+) diff --git a/GNUmakefile b/GNUmakefile index 8f9a8cae4..b3278f9ee 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -62,6 +62,7 @@ PROGS := \ csplit \ cut \ date \ + dd \ df \ dircolors \ dirname \ From dc24c9563e009f82979f30050940544527d56436 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 8 Feb 2022 21:05:39 +0100 Subject: [PATCH 087/161] shuf: BENCHMARKING.md --- src/uu/shuf/BENCHMARKING.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/uu/shuf/BENCHMARKING.md diff --git a/src/uu/shuf/BENCHMARKING.md b/src/uu/shuf/BENCHMARKING.md new file mode 100644 index 000000000..7607f04b4 --- /dev/null +++ b/src/uu/shuf/BENCHMARKING.md @@ -0,0 +1,28 @@ +# Benchmarking shuf + +`shuf` is a simple utility, but there are at least two important cases +benchmark: with and without repetition. + +When benchmarking changes, make sure to always build with the `--release` flag. +You can compare with another branch by compiling on that branch and than +renaming the executable from `shuf` to `shuf.old`. + +## Without repetition + +By default, `shuf` samples without repetition. To benchmark only the +randomization and not IO, we can pass the `-i` flag with a range of numbers to +randomly sample from. An example of a command that works well for testing: + +```shell +hyperfine --warmup 10 "target/release/shuf -i 0-10000000" +``` + +## With repetition + +When repetition is allowed, `shuf` works very differently under the hood, so it +should be benchmarked separately. In this case we have to pass the `-n` flag or +the command will run forever. An example of a hyperfine command is + +```shell +hyperfine --warmup 10 "target/release/shuf -r -n 10000000 -i 0-1000" +``` From b31d63eaa9835176cf9c9312c1addc7ae2a129ce Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Thu, 30 Dec 2021 20:01:55 -0500 Subject: [PATCH 088/161] split: add ByteChunkWriter and LineChunkWriter Add the `ByteChunkWriter` and `LineChunkWriter` structs and implementations, but don't use them yet. This structs offer an alternative approach to writing chunks of output (contrasted with `ByteSplitter` and `LineSplitter`). The main difference is that control of which underlying file is being written is inside the writer instead of outside. --- Cargo.lock | 1 + src/uu/split/Cargo.toml | 1 + src/uu/split/src/split.rs | 234 +++++++++++++++++++++++++++++++++++++- 3 files changed, 235 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index cc7c3967b..4579e8868 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2821,6 +2821,7 @@ name = "uu_split" version = "0.0.12" dependencies = [ "clap 3.0.10", + "memchr 2.4.1", "uucore", ] diff --git a/src/uu/split/Cargo.toml b/src/uu/split/Cargo.toml index cf2e76747..f6920e797 100644 --- a/src/uu/split/Cargo.toml +++ b/src/uu/split/Cargo.toml @@ -16,6 +16,7 @@ path = "src/split.rs" [dependencies] clap = { version = "3.0", features = ["wrap_help", "cargo"] } +memchr = "2" uucore = { version=">=0.0.11", package="uucore", path="../../uucore" } [[bin]] diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 1b6680142..c93065f7e 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -17,7 +17,7 @@ use std::convert::TryFrom; use std::env; use std::fmt; use std::fs::{metadata, remove_file, File}; -use std::io::{stdin, BufRead, BufReader, BufWriter, Read, Write}; +use std::io::{stdin, BufRead, BufReader, BufWriter, ErrorKind, Read, Write}; use std::num::ParseIntError; use std::path::Path; use uucore::display::Quotable; @@ -317,6 +317,238 @@ impl Settings { } } +/// Write a certain number of bytes to one file, then move on to another one. +/// +/// This struct maintains an underlying writer representing the +/// current chunk of the output. If a call to [`write`] would cause +/// the underlying writer to write more than the allowed number of +/// bytes, a new writer is created and the excess bytes are written to +/// that one instead. As many new underlying writers are created as +/// needed to write all the bytes in the input buffer. +struct ByteChunkWriter<'a> { + /// Parameters for creating the underlying writer for each new chunk. + settings: &'a Settings, + + /// The maximum number of bytes allowed for a single chunk of output. + chunk_size: usize, + + /// Running total of number of chunks that have been completed. + num_chunks_written: usize, + + /// Remaining capacity in number of bytes in the current chunk. + /// + /// This number starts at `chunk_size` and decreases as bytes are + /// written. Once it reaches zero, a writer for a new chunk is + /// initialized and this number gets reset to `chunk_size`. + num_bytes_remaining_in_current_chunk: usize, + + /// The underlying writer for the current chunk. + /// + /// Once the number of bytes written to this writer exceeds + /// `chunk_size`, a new writer is initialized and assigned to this + /// field. + inner: BufWriter>, + + /// Iterator that yields filenames for each chunk. + filename_iterator: FilenameIterator<'a>, +} + +impl<'a> ByteChunkWriter<'a> { + fn new(chunk_size: usize, settings: &'a Settings) -> Option> { + let mut filename_iterator = FilenameIterator::new( + &settings.prefix, + &settings.additional_suffix, + settings.suffix_length, + settings.numeric_suffix, + ); + let filename = filename_iterator.next()?; + if settings.verbose { + println!("creating file {}", filename.quote()); + } + let inner = platform::instantiate_current_writer(&settings.filter, &filename); + Some(ByteChunkWriter { + settings, + chunk_size, + num_bytes_remaining_in_current_chunk: chunk_size, + num_chunks_written: 0, + inner, + filename_iterator, + }) + } +} + +impl<'a> Write for ByteChunkWriter<'a> { + fn write(&mut self, mut buf: &[u8]) -> std::io::Result { + // If the length of `buf` exceeds the number of bytes remaining + // in the current chunk, we will need to write to multiple + // different underlying writers. In that case, each iteration of + // this loop writes to the underlying writer that corresponds to + // the current chunk number. + let mut carryover_bytes_written = 0; + loop { + if buf.is_empty() { + return Ok(carryover_bytes_written); + } + + // If the capacity of this chunk is greater than the number of + // bytes in `buf`, then write all the bytes in `buf`. Otherwise, + // write enough bytes to fill the current chunk, then increment + // the chunk number and repeat. + let n = buf.len(); + if n < self.num_bytes_remaining_in_current_chunk { + let num_bytes_written = self.inner.write(buf)?; + self.num_bytes_remaining_in_current_chunk -= num_bytes_written; + return Ok(carryover_bytes_written + num_bytes_written); + } else { + // Write enough bytes to fill the current chunk. + let i = self.num_bytes_remaining_in_current_chunk; + let num_bytes_written = self.inner.write(&buf[..i])?; + + // It's possible that the underlying writer did not + // write all the bytes. + if num_bytes_written < i { + self.num_bytes_remaining_in_current_chunk -= num_bytes_written; + return Ok(carryover_bytes_written + num_bytes_written); + } else { + // Move the window to look at only the remaining bytes. + buf = &buf[i..]; + + // Increment the chunk number, reset the number of + // bytes remaining, and instantiate the new + // underlying writer. + self.num_chunks_written += 1; + self.num_bytes_remaining_in_current_chunk = self.chunk_size; + + // Remember for the next iteration that we wrote these bytes. + carryover_bytes_written += num_bytes_written; + + // Only create the writer for the next chunk if + // there are any remaining bytes to write. This + // check prevents us from creating a new empty + // file. + if !buf.is_empty() { + let filename = self.filename_iterator.next().ok_or_else(|| { + std::io::Error::new(ErrorKind::Other, "output file suffixes exhausted") + })?; + if self.settings.verbose { + println!("creating file {}", filename.quote()); + } + self.inner = + platform::instantiate_current_writer(&self.settings.filter, &filename); + } + } + } + } + } + fn flush(&mut self) -> std::io::Result<()> { + self.inner.flush() + } +} + +/// Write a certain number of lines to one file, then move on to another one. +/// +/// This struct maintains an underlying writer representing the +/// current chunk of the output. If a call to [`write`] would cause +/// the underlying writer to write more than the allowed number of +/// lines, a new writer is created and the excess lines are written to +/// that one instead. As many new underlying writers are created as +/// needed to write all the lines in the input buffer. +struct LineChunkWriter<'a> { + /// Parameters for creating the underlying writer for each new chunk. + settings: &'a Settings, + + /// The maximum number of lines allowed for a single chunk of output. + chunk_size: usize, + + /// Running total of number of chunks that have been completed. + num_chunks_written: usize, + + /// Remaining capacity in number of lines in the current chunk. + /// + /// This number starts at `chunk_size` and decreases as lines are + /// written. Once it reaches zero, a writer for a new chunk is + /// initialized and this number gets reset to `chunk_size`. + num_lines_remaining_in_current_chunk: usize, + + /// The underlying writer for the current chunk. + /// + /// Once the number of lines written to this writer exceeds + /// `chunk_size`, a new writer is initialized and assigned to this + /// field. + inner: BufWriter>, + + /// Iterator that yields filenames for each chunk. + filename_iterator: FilenameIterator<'a>, +} + +impl<'a> LineChunkWriter<'a> { + fn new(chunk_size: usize, settings: &'a Settings) -> Option> { + let mut filename_iterator = FilenameIterator::new( + &settings.prefix, + &settings.additional_suffix, + settings.suffix_length, + settings.numeric_suffix, + ); + let filename = filename_iterator.next()?; + if settings.verbose { + println!("creating file {}", filename.quote()); + } + let inner = platform::instantiate_current_writer(&settings.filter, &filename); + Some(LineChunkWriter { + settings, + chunk_size, + num_lines_remaining_in_current_chunk: chunk_size, + num_chunks_written: 0, + inner, + filename_iterator, + }) + } +} + +impl<'a> Write for LineChunkWriter<'a> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + // If the number of lines in `buf` exceeds the number of lines + // remaining in the current chunk, we will need to write to + // multiple different underlying writers. In that case, each + // iteration of this loop writes to the underlying writer that + // corresponds to the current chunk number. + let mut prev = 0; + let mut total_bytes_written = 0; + for i in memchr::memchr_iter(b'\n', buf) { + // If we have exceeded the number of lines to write in the + // current chunk, then start a new chunk and its + // corresponding writer. + if self.num_lines_remaining_in_current_chunk == 0 { + self.num_chunks_written += 1; + let filename = self.filename_iterator.next().ok_or_else(|| { + std::io::Error::new(ErrorKind::Other, "output file suffixes exhausted") + })?; + if self.settings.verbose { + println!("creating file {}", filename.quote()); + } + self.inner = platform::instantiate_current_writer(&self.settings.filter, &filename); + self.num_lines_remaining_in_current_chunk = self.chunk_size; + } + + // Write the line, starting from *after* the previous + // newline character and ending *after* the current + // newline character. + let n = self.inner.write(&buf[prev..i + 1])?; + total_bytes_written += n; + prev = i + 1; + self.num_lines_remaining_in_current_chunk -= 1; + } + + let n = self.inner.write(&buf[prev..buf.len()])?; + total_bytes_written += n; + Ok(total_bytes_written) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.inner.flush() + } +} + trait Splitter { // Consume as much as possible from `reader` so as to saturate `writer`. // Equivalent to finishing one of the part files. Returns the number of From ca7af808d50fd3c4506c0c352a5e589135da985b Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sun, 30 Jan 2022 18:53:42 -0500 Subject: [PATCH 089/161] tests: correct a test case for split Correct the `test_split::test_suffixes_exhausted` test case so that it actually exercises the intended behavior of `split`. Previously, the test fixture contained 26 bytes. After this commit, the test fixture contains 27 bytes. When using a suffix width of one, only 26 filenames should be available when naming chunk files---one for each lowercase ASCII letter. This commit ensures that the filenames will be exhausted as intended by the test. --- tests/by-util/test_split.rs | 2 +- tests/fixtures/split/asciilowercase.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 911a7bf30..e9c2ec8a1 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -438,7 +438,7 @@ fn test_number() { assert_eq!(file_read("xab"), "fghij"); assert_eq!(file_read("xac"), "klmno"); assert_eq!(file_read("xad"), "pqrst"); - assert_eq!(file_read("xae"), "uvwxyz"); + assert_eq!(file_read("xae"), "uvwxyz\n"); } #[test] diff --git a/tests/fixtures/split/asciilowercase.txt b/tests/fixtures/split/asciilowercase.txt index e85d5b452..b0883f382 100644 --- a/tests/fixtures/split/asciilowercase.txt +++ b/tests/fixtures/split/asciilowercase.txt @@ -1 +1 @@ -abcdefghijklmnopqrstuvwxyz \ No newline at end of file +abcdefghijklmnopqrstuvwxyz From 1d7e1b87328d7d814e963f2c45e44d2dd152cc94 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Thu, 30 Dec 2021 20:11:03 -0500 Subject: [PATCH 090/161] split: use ByteChunkWriter and LineChunkWriter Replace `ByteSplitter` and `LineSplitter` with `ByteChunkWriter` and `LineChunkWriter` respectively. This results in a more maintainable design and an increase in the speed of splitting by lines. --- src/uu/split/src/split.rs | 99 +++++++++++++----------------- tests/by-util/test_split.rs | 20 +++++- tests/fixtures/split/fivelines.txt | 5 ++ 3 files changed, 65 insertions(+), 59 deletions(-) create mode 100644 tests/fixtures/split/fivelines.txt diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index c93065f7e..2fc475c78 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -16,13 +16,14 @@ use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; use std::convert::TryFrom; use std::env; use std::fmt; -use std::fs::{metadata, remove_file, File}; +use std::fs::{metadata, File}; use std::io::{stdin, BufRead, BufReader, BufWriter, ErrorKind, Read, Write}; use std::num::ParseIntError; use std::path::Path; use uucore::display::Quotable; -use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; +use uucore::error::{FromIo, UIoError, UResult, USimpleError, UUsageError}; use uucore::parse_size::{parse_size, ParseSizeError}; +use uucore::uio_error; static OPT_BYTES: &str = "bytes"; static OPT_LINE_BYTES: &str = "line-bytes"; @@ -739,65 +740,47 @@ fn split(settings: &Settings) -> UResult<()> { Box::new(r) as Box }); - if let Strategy::Number(num_chunks) = settings.strategy { - return split_into_n_chunks_by_byte(settings, &mut reader, num_chunks); - } - - let mut splitter: Box = match settings.strategy { - Strategy::Lines(chunk_size) => Box::new(LineSplitter::new(chunk_size)), - Strategy::Bytes(chunk_size) | Strategy::LineBytes(chunk_size) => { - Box::new(ByteSplitter::new(chunk_size)) + match settings.strategy { + Strategy::Number(num_chunks) => { + split_into_n_chunks_by_byte(settings, &mut reader, num_chunks) } - _ => unreachable!(), - }; - - // This object is responsible for creating the filename for each chunk. - let mut filename_iterator = FilenameIterator::new( - &settings.prefix, - &settings.additional_suffix, - settings.suffix_length, - settings.numeric_suffix, - ); - loop { - // Get a new part file set up, and construct `writer` for it. - let filename = filename_iterator - .next() - .ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?; - let mut writer = platform::instantiate_current_writer(&settings.filter, filename.as_str()); - - let bytes_consumed = splitter - .consume(&mut reader, &mut writer) - .map_err_context(|| "input/output error".to_string())?; - writer - .flush() - .map_err_context(|| "error flushing to output file".to_string())?; - - // If we didn't write anything we should clean up the empty file, and - // break from the loop. - if bytes_consumed == 0 { - // The output file is only ever created if --filter isn't used. - // Complicated, I know... - if settings.filter.is_none() { - remove_file(filename) - .map_err_context(|| "error removing empty file".to_string())?; + Strategy::Lines(chunk_size) => { + let mut writer = LineChunkWriter::new(chunk_size, settings) + .ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?; + match std::io::copy(&mut reader, &mut writer) { + Ok(_) => Ok(()), + Err(e) => match e.kind() { + // TODO Since the writer object controls the creation of + // new files, we need to rely on the `std::io::Result` + // returned by its `write()` method to communicate any + // errors to this calling scope. If a new file cannot be + // created because we have exceeded the number of + // allowable filenames, we use `ErrorKind::Other` to + // indicate that. A special error message needs to be + // printed in that case. + ErrorKind::Other => Err(USimpleError::new(1, "output file suffixes exhausted")), + _ => Err(uio_error!(e, "input/output error")), + }, } - break; } - - // TODO It is silly to have the "creating file" message here - // after the file has been already created. However, because - // of the way the main loop has been written, an extra file - // gets created and then deleted in the last iteration of the - // loop. So we need to make sure we are not in that case when - // printing this message. - // - // This is only here temporarily while we make some - // improvements to the architecture of the main loop in this - // function. In the future, it will move to a more appropriate - // place---at the point where the file is actually created. - if settings.verbose { - println!("creating file {}", filename.quote()); + Strategy::Bytes(chunk_size) | Strategy::LineBytes(chunk_size) => { + let mut writer = ByteChunkWriter::new(chunk_size, settings) + .ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?; + match std::io::copy(&mut reader, &mut writer) { + Ok(_) => Ok(()), + Err(e) => match e.kind() { + // TODO Since the writer object controls the creation of + // new files, we need to rely on the `std::io::Result` + // returned by its `write()` method to communicate any + // errors to this calling scope. If a new file cannot be + // created because we have exceeded the number of + // allowable filenames, we use `ErrorKind::Other` to + // indicate that. A special error message needs to be + // printed in that case. + ErrorKind::Other => Err(USimpleError::new(1, "output file suffixes exhausted")), + _ => Err(uio_error!(e, "input/output error")), + }, + } } } - Ok(()) } diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index e9c2ec8a1..7a122e04b 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -2,7 +2,7 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// spell-checker:ignore xzaaa sixhundredfiftyonebytes ninetyonebytes asciilowercase fghij klmno pqrst uvwxyz +// spell-checker:ignore xzaaa sixhundredfiftyonebytes ninetyonebytes asciilowercase fghij klmno pqrst uvwxyz fivelines extern crate rand; extern crate regex; @@ -449,3 +449,21 @@ fn test_invalid_suffix_length() { .no_stdout() .stderr_contains("invalid suffix length: 'xyz'"); } + +#[test] +fn test_include_newlines() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["-l", "2", "fivelines.txt"]).succeeds(); + + let mut s = String::new(); + at.open("xaa").read_to_string(&mut s).unwrap(); + assert_eq!(s, "1\n2\n"); + + let mut s = String::new(); + at.open("xab").read_to_string(&mut s).unwrap(); + assert_eq!(s, "3\n4\n"); + + let mut s = String::new(); + at.open("xac").read_to_string(&mut s).unwrap(); + assert_eq!(s, "5\n"); +} diff --git a/tests/fixtures/split/fivelines.txt b/tests/fixtures/split/fivelines.txt new file mode 100644 index 000000000..8a1218a10 --- /dev/null +++ b/tests/fixtures/split/fivelines.txt @@ -0,0 +1,5 @@ +1 +2 +3 +4 +5 From 70ca1f45eaa262001b8a82a1e2826aaa72ff7a92 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sat, 15 Jan 2022 22:07:48 -0500 Subject: [PATCH 091/161] split: remove unused ByteSplitter and LineSplitter --- src/uu/split/src/split.rs | 102 +------------------------------------- 1 file changed, 1 insertion(+), 101 deletions(-) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 2fc475c78..243cf2bec 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -13,11 +13,10 @@ mod platform; use crate::filenames::FilenameIterator; use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; -use std::convert::TryFrom; use std::env; use std::fmt; use std::fs::{metadata, File}; -use std::io::{stdin, BufRead, BufReader, BufWriter, ErrorKind, Read, Write}; +use std::io::{stdin, BufReader, BufWriter, ErrorKind, Read, Write}; use std::num::ParseIntError; use std::path::Path; use uucore::display::Quotable; @@ -550,105 +549,6 @@ impl<'a> Write for LineChunkWriter<'a> { } } -trait Splitter { - // Consume as much as possible from `reader` so as to saturate `writer`. - // Equivalent to finishing one of the part files. Returns the number of - // bytes that have been moved. - fn consume( - &mut self, - reader: &mut BufReader>, - writer: &mut BufWriter>, - ) -> std::io::Result; -} - -struct LineSplitter { - lines_per_split: usize, -} - -impl LineSplitter { - fn new(chunk_size: usize) -> Self { - Self { - lines_per_split: chunk_size, - } - } -} - -impl Splitter for LineSplitter { - fn consume( - &mut self, - reader: &mut BufReader>, - writer: &mut BufWriter>, - ) -> std::io::Result { - let mut bytes_consumed = 0u128; - let mut buffer = String::with_capacity(1024); - for _ in 0..self.lines_per_split { - let bytes_read = reader.read_line(&mut buffer)?; - // If we ever read 0 bytes then we know we've hit EOF. - if bytes_read == 0 { - return Ok(bytes_consumed); - } - - writer.write_all(buffer.as_bytes())?; - // Empty out the String buffer since `read_line` appends instead of - // replaces. - buffer.clear(); - - bytes_consumed += bytes_read as u128; - } - - Ok(bytes_consumed) - } -} - -struct ByteSplitter { - bytes_per_split: u128, -} - -impl ByteSplitter { - fn new(chunk_size: usize) -> Self { - Self { - bytes_per_split: u128::try_from(chunk_size).unwrap(), - } - } -} - -impl Splitter for ByteSplitter { - fn consume( - &mut self, - reader: &mut BufReader>, - writer: &mut BufWriter>, - ) -> std::io::Result { - // We buffer reads and writes. We proceed until `bytes_consumed` is - // equal to `self.bytes_per_split` or we reach EOF. - let mut bytes_consumed = 0u128; - const BUFFER_SIZE: usize = 1024; - let mut buffer = [0u8; BUFFER_SIZE]; - while bytes_consumed < self.bytes_per_split { - // Don't overshoot `self.bytes_per_split`! Note: Using std::cmp::min - // doesn't really work since we have to get types to match which - // can't be done in a way that keeps all conversions safe. - let bytes_desired = if (BUFFER_SIZE as u128) <= self.bytes_per_split - bytes_consumed { - BUFFER_SIZE - } else { - // This is a safe conversion since the difference must be less - // than BUFFER_SIZE in this branch. - (self.bytes_per_split - bytes_consumed) as usize - }; - let bytes_read = reader.read(&mut buffer[0..bytes_desired])?; - // If we ever read 0 bytes then we know we've hit EOF. - if bytes_read == 0 { - return Ok(bytes_consumed); - } - - writer.write_all(&buffer[0..bytes_read])?; - - bytes_consumed += bytes_read as u128; - } - - Ok(bytes_consumed) - } -} - /// Split a file into a specific number of chunks by byte. /// /// This function always creates one output file for each chunk, even From b37718de1027d3d9d77fbeaf117245754cbc8d0a Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Fri, 31 Dec 2021 13:16:38 -0500 Subject: [PATCH 092/161] split: add BENCHMARKING.md documentation file --- src/uu/split/BENCHMARKING.md | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/uu/split/BENCHMARKING.md diff --git a/src/uu/split/BENCHMARKING.md b/src/uu/split/BENCHMARKING.md new file mode 100644 index 000000000..9c8d5d17c --- /dev/null +++ b/src/uu/split/BENCHMARKING.md @@ -0,0 +1,47 @@ + + +# Benchmarking to measure performance + +To compare the performance of the `uutils` version of `split` with the +GNU version of `split`, you can use a benchmarking tool like +[hyperfine][0]. On Ubuntu 18.04 or later, you can install `hyperfine` by +running + + sudo apt-get install hyperfine + +Next, build the `split` binary under the release profile: + + cargo build --release -p uu_split + +Now, get a text file to test `split` on. The `split` program has three +main modes of operation: chunk by lines, chunk by bytes, and chunk by +lines with a byte limit. You may want to test the performance of `split` +with various shapes and sizes of input files and under various modes of +operation. For example, to test chunking by bytes on a large input file, +you can create a file named `testfile.txt` containing one million null +bytes like this: + + printf "%0.s\0" {1..1000000} > testfile.txt + +For another example, to test chunking by bytes on a large real-world +input file, you could download a [database dump of Wikidata][1] or some +related files that the Wikimedia project provides. For example, [this +file][2] contains about 130 million lines. + +Finally, you can compare the performance of the two versions of `split` +by running, for example, + + cd /tmp && hyperfine \ + --prepare 'rm x* || true' \ + "split -b 1000 testfile.txt" \ + "target/release/split -b 1000 testfile.txt" + +Since `split` creates a lot of files on the filesystem, I recommend +changing to the `/tmp` directory before running the benchmark. The +`--prepare` argument to `hyperfine` runs a specified command before each +timing run. We specify `rm x* || true` so that the output files from the +previous run of `split` are removed before each run begins. + +[0]: https://github.com/sharkdp/hyperfine +[1]: https://www.wikidata.org/wiki/Wikidata:Database_download +[2]: https://dumps.wikimedia.org/wikidatawiki/20211001/wikidatawiki-20211001-pages-logging.xml.gz From 6e0fedc277f8cf29ce5f59bbb3a27bfdbdb602c7 Mon Sep 17 00:00:00 2001 From: Eli Youngs Date: Tue, 8 Feb 2022 20:19:13 -0800 Subject: [PATCH 093/161] Fix panic when canonicalizing a nonexistent path --- src/uucore/src/lib/features/fs.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index ccfd8318c..0452af5b3 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -299,9 +299,8 @@ pub fn canonicalize>( let original = if original.is_absolute() { original.to_path_buf() } else { - dunce::canonicalize(env::current_dir().unwrap()) - .unwrap() - .join(original) + let current_dir = env::current_dir()?; + dunce::canonicalize(current_dir)?.join(original) }; let mut result = PathBuf::new(); From 30d7a4b16796e93b1715a84ccb7fba3d1adcec0b Mon Sep 17 00:00:00 2001 From: Shreyans Jain Date: Thu, 10 Feb 2022 12:46:44 +0530 Subject: [PATCH 094/161] hashsum: Add BLAKE3 to Hashing Algorithms Signed-off-by: Shreyans Jain --- Cargo.lock | 30 +++++++++++++++++++++++++- src/uu/hashsum/Cargo.toml | 1 + src/uu/hashsum/src/digest.rs | 23 ++++++++++++++++++++ src/uu/hashsum/src/hashsum.rs | 10 +++++++++ tests/by-util/test_hashsum.rs | 1 + tests/fixtures/hashsum/b3sum.checkfile | 1 + tests/fixtures/hashsum/b3sum.expected | 1 + 7 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/hashsum/b3sum.checkfile create mode 100644 tests/fixtures/hashsum/b3sum.expected diff --git a/Cargo.lock b/Cargo.lock index cc7c3967b..b2a545e20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "atty" version = "0.2.14" @@ -123,10 +129,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.5.2", "constant_time_eq", ] +[[package]] +name = "blake3" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" +dependencies = [ + "arrayref", + "arrayvec 0.7.2", + "cc", + "cfg-if 1.0.0", + "constant_time_eq", + "digest", +] + [[package]] name = "block-buffer" version = "0.10.0" @@ -670,6 +690,7 @@ dependencies = [ "block-buffer", "crypto-common", "generic-array", + "subtle", ] [[package]] @@ -1850,6 +1871,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.86" @@ -2375,6 +2402,7 @@ name = "uu_hashsum" version = "0.0.12" dependencies = [ "blake2b_simd", + "blake3", "clap 3.0.10", "digest", "hex", diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index 6032a9428..495e15972 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -27,6 +27,7 @@ sha1 = "0.6.0" sha2 = "0.10.1" sha3 = "0.10.0" blake2b_simd = "0.5.11" +blake3 = "1.3.1" uucore = { version=">=0.0.11", package="uucore", path="../../uucore" } [[bin]] diff --git a/src/uu/hashsum/src/digest.rs b/src/uu/hashsum/src/digest.rs index 23fe84e77..c06834c74 100644 --- a/src/uu/hashsum/src/digest.rs +++ b/src/uu/hashsum/src/digest.rs @@ -81,6 +81,29 @@ impl Digest for blake2b_simd::State { } } +impl Digest for blake3::Hasher { + fn new() -> Self { + Self::new() + } + + fn input(&mut self, input: &[u8]) { + self.update(input); + } + + fn result(&mut self, out: &mut [u8]) { + let hash_result = &self.finalize(); + out.copy_from_slice(hash_result.as_bytes()); + } + + fn reset(&mut self) { + *self = Self::new(); + } + + fn output_bits(&self) -> usize { + 256 + } +} + impl Digest for sha1::Sha1 { fn new() -> Self { Self::new() diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 30c9d5b92..fe607b554 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -70,6 +70,7 @@ fn is_custom_binary(program: &str) -> bool { | "shake128sum" | "shake256sum" | "b2sum" + | "b3sum" ) } @@ -93,6 +94,11 @@ fn detect_algo( Box::new(blake2b_simd::State::new()) as Box, 512, ), + "b3sum" => ( + "BLAKE3", + Box::new(blake3::Hasher::new()) as Box, + 256, + ), "sha3sum" => match matches.value_of("bits") { Some(bits_str) => match (bits_str).parse::() { Ok(224) => ( @@ -196,6 +202,9 @@ fn detect_algo( if matches.is_present("b2sum") { set_or_crash("BLAKE2", Box::new(blake2b_simd::State::new()), 512); } + if matches.is_present("b3sum") { + set_or_crash("BLAKE3", Box::new(blake3::Hasher::new()), 256); + } if matches.is_present("sha3") { match matches.value_of("bits") { Some(bits_str) => match (bits_str).parse::() { @@ -433,6 +442,7 @@ pub fn uu_app_custom<'a>() -> App<'a> { "work with SHAKE256 using BITS for the output size", ), ("b2sum", "work with BLAKE2"), + ("b3sum", "work with BLAKE3"), ]; for (name, desc) in algorithms { diff --git a/tests/by-util/test_hashsum.rs b/tests/by-util/test_hashsum.rs index 545b4ee78..293270a77 100644 --- a/tests/by-util/test_hashsum.rs +++ b/tests/by-util/test_hashsum.rs @@ -80,4 +80,5 @@ test_digest! { shake128_256 shake128 256 shake256_512 shake256 512 b2sum b2sum 512 + b3sum b3sum 256 } diff --git a/tests/fixtures/hashsum/b3sum.checkfile b/tests/fixtures/hashsum/b3sum.checkfile new file mode 100644 index 000000000..64a9bf7a4 --- /dev/null +++ b/tests/fixtures/hashsum/b3sum.checkfile @@ -0,0 +1 @@ +a1a55887535397bf461902491c8779188a5dd1f8c3951b3d9cf6ecba194e87b0 input.txt \ No newline at end of file diff --git a/tests/fixtures/hashsum/b3sum.expected b/tests/fixtures/hashsum/b3sum.expected new file mode 100644 index 000000000..a56e54432 --- /dev/null +++ b/tests/fixtures/hashsum/b3sum.expected @@ -0,0 +1 @@ +a1a55887535397bf461902491c8779188a5dd1f8c3951b3d9cf6ecba194e87b0 \ No newline at end of file From 3176ad5c1b722509c295d8fe0c713414d5bcc16e Mon Sep 17 00:00:00 2001 From: Shreyans Jain Date: Thu, 10 Feb 2022 13:55:53 +0530 Subject: [PATCH 095/161] tests/hashsum: Fix missing space in checkfile --- tests/fixtures/hashsum/b3sum.checkfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/hashsum/b3sum.checkfile b/tests/fixtures/hashsum/b3sum.checkfile index 64a9bf7a4..f8d34d0b6 100644 --- a/tests/fixtures/hashsum/b3sum.checkfile +++ b/tests/fixtures/hashsum/b3sum.checkfile @@ -1 +1 @@ -a1a55887535397bf461902491c8779188a5dd1f8c3951b3d9cf6ecba194e87b0 input.txt \ No newline at end of file +a1a55887535397bf461902491c8779188a5dd1f8c3951b3d9cf6ecba194e87b0 input.txt From c3b4d898eed0735c4cb01f399732b3d4af3ab159 Mon Sep 17 00:00:00 2001 From: Ivan Majeru Date: Thu, 10 Feb 2022 18:34:27 +0200 Subject: [PATCH 096/161] dd: allow multiple occurences for iflag, oflag and conv The iflag, oflag and conv cli arguments take a list of values and the correct behavior is to collect all values from multiple occurences of theme. For example if we call `dd --iflag=directory --iflag=skip_bytes` this should collect the two values, `directory` and `skip_bytes` for iflag. The unittest was added for this case. --- src/uu/dd/src/dd.rs | 15 ++++-- src/uu/dd/src/parseargs.rs | 15 ++---- src/uu/dd/src/parseargs/unit_tests.rs | 68 +++++++++++++-------------- 3 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 296c6c6b1..a9d89dfca 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -1060,8 +1060,11 @@ Printing performance stats is also triggered by the INFO signal (where supported .arg( Arg::new(options::CONV) .long(options::CONV) - .overrides_with(options::CONV) .takes_value(true) + .multiple_occurrences(true) + .use_delimiter(true) + .require_delimiter(true) + .multiple_values(true) .require_equals(true) .value_name("CONV") .help("(alternatively conv=CONV[,CONV]) specifies a comma-separated list of conversion options or (for legacy reasons) file flags. Conversion options and file flags may be intermixed. @@ -1098,8 +1101,11 @@ Conversion Flags: .arg( Arg::new(options::IFLAG) .long(options::IFLAG) - .overrides_with(options::IFLAG) .takes_value(true) + .multiple_occurrences(true) + .use_delimiter(true) + .require_delimiter(true) + .multiple_values(true) .require_equals(true) .value_name("FLAG") .help("(alternatively iflag=FLAG[,FLAG]) a comma separated list of input flags which specify how the input source is treated. FLAG may be any of the input-flags or general-flags specified below. @@ -1125,8 +1131,11 @@ General-Flags .arg( Arg::new(options::OFLAG) .long(options::OFLAG) - .overrides_with(options::OFLAG) .takes_value(true) + .multiple_occurrences(true) + .use_delimiter(true) + .require_delimiter(true) + .multiple_values(true) .require_equals(true) .value_name("FLAG") .help("(alternatively oflag=FLAG[,FLAG]) a comma separated list of output flags which specify how the output source is treated. FLAG may be any of the output-flags or general-flags specified below. diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index 492ab70cb..73f731b24 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -414,16 +414,11 @@ fn parse_flag_list>( tag: &str, matches: &Matches, ) -> Result, ParseError> { - let mut flags = Vec::new(); - - if let Some(comma_str) = matches.value_of(tag) { - for s in comma_str.split(',') { - let flag = s.parse()?; - flags.push(flag); - } - } - - Ok(flags) + matches + .values_of(tag) + .unwrap_or_default() + .map(|f| f.parse()) + .collect() } /// Parse Conversion Options (Input Variety) diff --git a/src/uu/dd/src/parseargs/unit_tests.rs b/src/uu/dd/src/parseargs/unit_tests.rs index 64da4640f..1e5b4b930 100644 --- a/src/uu/dd/src/parseargs/unit_tests.rs +++ b/src/uu/dd/src/parseargs/unit_tests.rs @@ -300,7 +300,39 @@ fn test_status_level_noxfer() { } #[test] -fn test_override_multiple_levels() { +fn test_multiple_flags_options() { + let args = vec![ + String::from("dd"), + String::from("--iflag=fullblock,directory"), + String::from("--iflag=skip_bytes"), + String::from("--oflag=direct"), + String::from("--oflag=dsync"), + String::from("--conv=ascii,ucase"), + String::from("--conv=unblock"), + ]; + let matches = uu_app().try_get_matches_from(args).unwrap(); + + // iflag + let iflags = parse_flag_list::(options::IFLAG, &matches).unwrap(); + assert_eq!( + vec![Flag::FullBlock, Flag::Directory, Flag::SkipBytes], + iflags + ); + + // oflag + let oflags = parse_flag_list::(options::OFLAG, &matches).unwrap(); + assert_eq!(vec![Flag::Direct, Flag::Dsync], oflags); + + // conv + let conv = parse_flag_list::(options::CONV, &matches).unwrap(); + assert_eq!( + vec![ConvFlag::FmtEtoA, ConvFlag::UCase, ConvFlag::Unblock], + conv + ); +} + +#[test] +fn test_override_multiple_options() { let args = vec![ String::from("dd"), String::from("--if=foo.file"), @@ -321,12 +353,6 @@ fn test_override_multiple_levels() { String::from("--status=noxfer"), String::from("--count=512"), String::from("--count=1024"), - String::from("--conv=ascii,ucase"), - String::from("--conv=ebcdic,lcase,unblock"), - String::from("--iflag=direct,nocache"), - String::from("--iflag=count_bytes,skip_bytes"), - String::from("--oflag=append,direct"), - String::from("--oflag=append,seek_bytes"), ]; let matches = uu_app().try_get_matches_from(args).unwrap(); @@ -368,14 +394,6 @@ fn test_override_multiple_levels() { .unwrap() ); - // conv - let exp = vec![ConvFlag::FmtEtoA, ConvFlag::LCase, ConvFlag::Unblock]; - let act = parse_flag_list::("conv", &matches).unwrap(); - assert_eq!(exp.len(), act.len()); - for cf in &exp { - assert!(exp.contains(cf)); - } - // count assert_eq!( CountType::Bytes(1024), @@ -389,26 +407,6 @@ fn test_override_multiple_levels() { .unwrap() .unwrap() ); - - // iflag - assert_eq!( - IFlags { - count_bytes: true, - skip_bytes: true, - ..IFlags::default() - }, - parse_iflags(&matches).unwrap() - ); - - // oflag - assert_eq!( - OFlags { - seek_bytes: true, - append: true, - ..OFlags::default() - }, - parse_oflags(&matches).unwrap() - ); } // ----- IConvFlags/Output ----- From 3f6fe7f3886b8dddbda57f51703c97092413051e Mon Sep 17 00:00:00 2001 From: Abhishek C Sharma Date: Thu, 10 Feb 2022 12:35:20 -0800 Subject: [PATCH 097/161] ls: add new optional arguments to --classify flag (#3041) * ls: add new optional arguments to --classify flag The --classify flag in ls now takes an option when argument that may have the values always, auto and none. Modified clap argument to allow an optional parameter and changed the classify flag value parsing logic to account for this change. * ls: add test for indicator-style, ind and classify with value none * ls: require option paramter to --classify to use a = to specify flag value * ls: account for all the undocumented possible values for the --classify flag Added the other values for the --classify flag along with modifications to tests. Also documented the inconsistency between GNU coreutils because we accept the flag value even for the short version of the flag. --- src/uu/ls/src/ls.rs | 36 +++++++++++++++++++++++++++++++++--- tests/by-util/test_ls.rs | 21 +++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 7fdec53f0..d82915c85 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -583,8 +583,19 @@ impl Config { "slash" => IndicatorStyle::Slash, &_ => IndicatorStyle::None, } - } else if options.is_present(options::indicator_style::CLASSIFY) { - IndicatorStyle::Classify + } else if let Some(field) = options.value_of(options::indicator_style::CLASSIFY) { + match field { + "never" | "no" | "none" => IndicatorStyle::None, + "always" | "yes" | "force" => IndicatorStyle::Classify, + "auto" | "tty" | "if-tty" => { + if atty::is(atty::Stream::Stdout) { + IndicatorStyle::Classify + } else { + IndicatorStyle::None + } + } + &_ => IndicatorStyle::None, + } } else if options.is_present(options::indicator_style::SLASH) { IndicatorStyle::Slash } else if options.is_present(options::indicator_style::FILE_TYPE) { @@ -1202,6 +1213,11 @@ only ignore '.' and '..'.", ]), ) .arg( + // The --classify flag can take an optional when argument to + // control its behavior from version 9 of GNU coreutils. + // There is currently an inconsistency where GNU coreutils allows only + // the long form of the flag to take the argument while we allow it + // for both the long and short form of the flag. Arg::new(options::indicator_style::CLASSIFY) .short('F') .long(options::indicator_style::CLASSIFY) @@ -1209,8 +1225,22 @@ only ignore '.' and '..'.", "Append a character to each file name indicating the file type. Also, for \ regular files that are executable, append '*'. The file type indicators are \ '/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \ - '>' for doors, and nothing for regular files.", + '>' for doors, and nothing for regular files. when may be omitted, or one of:\n\ + \tnone - Do not classify. This is the default.\n\ + \tauto - Only classify if standard output is a terminal.\n\ + \talways - Always classify.\n\ + Specifying --classify and no when is equivalent to --classify=always. This will not follow\ + symbolic links listed on the command line unless the --dereference-command-line (-H),\ + --dereference (-L), or --dereference-command-line-symlink-to-dir options are specified.", ) + .takes_value(true) + .value_name("when") + .possible_values(&[ + "always", "yes", "force", "auto", "tty", "if-tty", "never", "no", "none", + ]) + .default_missing_value("always") + .require_equals(true) + .min_values(0) .overrides_with_all(&[ options::indicator_style::FILE_TYPE, options::indicator_style::SLASH, diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index f61611390..19947c05a 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -1555,6 +1555,9 @@ fn test_ls_indicator_style() { "--indicator-style=slash", "--ind=slash", "--classify", + "--classify=always", + "--classify=yes", + "--classify=force", "--class", "--file-type", "--file", @@ -1564,6 +1567,24 @@ fn test_ls_indicator_style() { scene.ucmd().arg(opt).succeeds().stdout_contains(&"/"); } + // Classify, Indicator options should not contain any indicators when value is none. + for opt in [ + "--indicator-style=none", + "--ind=none", + "--classify=none", + "--classify=never", + "--classify=no", + ] { + // Verify that there are no indicators for any of the file types. + scene + .ucmd() + .arg(opt) + .succeeds() + .stdout_does_not_contain(&"/") + .stdout_does_not_contain(&"@") + .stdout_does_not_contain(&"|"); + } + // Classify and File-Type all contain indicators for pipes and links. let options = vec!["classify", "file-type"]; for opt in options { From 2f65b29866d78da1fb3b6559ad6c2ad810c74341 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Thu, 10 Feb 2022 19:16:49 -0500 Subject: [PATCH 098/161] split: error when --additional-suffix contains / Make `split` terminate with a usage error when the `--additional-suffix` argument contains a directory separator character. --- src/uu/split/src/split.rs | 19 +++++++++++++++++-- tests/by-util/test_split.rs | 8 ++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 1b6680142..56b70bc00 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -264,6 +264,9 @@ enum SettingsError { /// Invalid suffix length parameter. SuffixLength(String), + /// Suffix contains a directory separator, which is not allowed. + SuffixContainsSeparator(String), + /// The `--filter` option is not supported on Windows. #[cfg(windows)] NotSupported, @@ -272,7 +275,10 @@ enum SettingsError { impl SettingsError { /// Whether the error demands a usage message. fn requires_usage(&self) -> bool { - matches!(self, Self::Strategy(StrategyError::MultipleWays)) + matches!( + self, + Self::Strategy(StrategyError::MultipleWays) | Self::SuffixContainsSeparator(_) + ) } } @@ -281,6 +287,11 @@ impl fmt::Display for SettingsError { match self { Self::Strategy(e) => e.fmt(f), Self::SuffixLength(s) => write!(f, "invalid suffix length: {}", s.quote()), + Self::SuffixContainsSeparator(s) => write!( + f, + "invalid suffix {}, contains directory separator", + s.quote() + ), #[cfg(windows)] Self::NotSupported => write!( f, @@ -294,13 +305,17 @@ impl fmt::Display for SettingsError { impl Settings { /// Parse a strategy from the command-line arguments. fn from(matches: &ArgMatches) -> Result { + let additional_suffix = matches.value_of(OPT_ADDITIONAL_SUFFIX).unwrap().to_string(); + if additional_suffix.contains('/') { + return Err(SettingsError::SuffixContainsSeparator(additional_suffix)); + } let suffix_length_str = matches.value_of(OPT_SUFFIX_LENGTH).unwrap(); let result = Self { suffix_length: suffix_length_str .parse() .map_err(|_| SettingsError::SuffixLength(suffix_length_str.to_string()))?, numeric_suffix: matches.occurrences_of(OPT_NUMERIC_SUFFIXES) > 0, - additional_suffix: matches.value_of(OPT_ADDITIONAL_SUFFIX).unwrap().to_owned(), + additional_suffix, verbose: matches.occurrences_of("verbose") > 0, strategy: Strategy::from(matches).map_err(SettingsError::Strategy)?, input: matches.value_of(ARG_INPUT).unwrap().to_owned(), diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 911a7bf30..59b84fdf8 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -228,6 +228,14 @@ fn test_split_additional_suffix() { assert_eq!(glob.collate(), at.read_bytes(name)); } +#[test] +fn test_additional_suffix_no_slash() { + new_ucmd!() + .args(&["--additional-suffix", "a/b"]) + .fails() + .usage_error("invalid suffix 'a/b', contains directory separator"); +} + // note: the test_filter* tests below are unix-only // windows support has been waived for now because of the difficulty of getting // the `cmd` call right From 6391f4c28aad3a6e384e411f213c1fd13054462c Mon Sep 17 00:00:00 2001 From: Shreyans Jain Date: Fri, 11 Feb 2022 14:18:56 +0530 Subject: [PATCH 099/161] util/build-gnu.sh: Add b3sum Signed-off-by: Shreyans Jain --- util/build-gnu.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index a52d42107..8f6e431a6 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -20,7 +20,7 @@ make PROFILE=release BUILDDIR="$PWD/target/release/" cp "${BUILDDIR}/install" "${BUILDDIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target # Create *sum binaries -for sum in b2sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum +for sum in b2sum b3sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum do sum_path="${BUILDDIR}/${sum}" test -f "${sum_path}" || cp "${BUILDDIR}/hashsum" "${sum_path}" From c2bb9dd433cc7b735dfb6ef8b75bf960e8bcbe90 Mon Sep 17 00:00:00 2001 From: 353fc443 <353fc443@pm.me> Date: Fri, 11 Feb 2022 13:02:06 +0000 Subject: [PATCH 100/161] Fix clippy octal-escapes warning --- tests/by-util/test_cut.rs | 4 ++-- tests/by-util/test_head.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index 3a1b577ef..da707ac3a 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -128,9 +128,9 @@ fn test_complement() { fn test_zero_terminated() { new_ucmd!() .args(&["-d_", "-z", "-f", "1"]) - .pipe_in("9_1\n8_2\n\07_3") + .pipe_in("9_1\n8_2\n\x007_3") .succeeds() - .stdout_only("9\07\0"); + .stdout_only("9\x007\0"); } #[test] diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 25410d76f..e3f4e79aa 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -220,9 +220,9 @@ fn test_zero_terminated() { fn test_obsolete_extras() { new_ucmd!() .args(&["-5zv"]) - .pipe_in("1\02\03\04\05\06") + .pipe_in("1\x002\x003\x004\x005\x006") .succeeds() - .stdout_is("==> standard input <==\n1\02\03\04\05\0"); + .stdout_is("==> standard input <==\n1\x002\x003\x004\x005\0"); } #[test] From f37e78c25a5d68053f85f7df4f660aaaf6397f09 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Thu, 10 Feb 2022 20:51:33 -0500 Subject: [PATCH 101/161] touch: show error on -h with nonexistent file Show an error message when running `touch -h` on a nonexistent file. --- src/uu/touch/src/touch.rs | 14 ++++++++++++-- tests/by-util/test_touch.rs | 13 +++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index e27dbfc18..f58d9e6d8 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -83,8 +83,18 @@ Try 'touch --help' for more information."##, for filename in files { let path = Path::new(filename); if !path.exists() { - // no-dereference included here for compatibility - if matches.is_present(options::NO_CREATE) || matches.is_present(options::NO_DEREF) { + if matches.is_present(options::NO_CREATE) { + continue; + } + + if matches.is_present(options::NO_DEREF) { + show!(USimpleError::new( + 1, + format!( + "setting times of {}: No such file or directory", + filename.quote() + ) + )); continue; } diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index dd4a0b6cc..ac82a81ea 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -539,3 +539,16 @@ fn test_touch_no_args() { Try 'touch --help' for more information."##, ); } + +#[test] +fn test_no_dereference_no_file() { + new_ucmd!() + .args(&["-h", "not-a-file"]) + .fails() + .stderr_contains("setting times of 'not-a-file': No such file or directory"); + new_ucmd!() + .args(&["-h", "not-a-file-1", "not-a-file-2"]) + .fails() + .stderr_contains("setting times of 'not-a-file-1': No such file or directory") + .stderr_contains("setting times of 'not-a-file-2': No such file or directory"); +} From aacff13ec3afb87c0ac41cd0103c43dfcc305532 Mon Sep 17 00:00:00 2001 From: Pat Laster Date: Fri, 11 Feb 2022 23:02:57 -0600 Subject: [PATCH 102/161] seq: Eliminated special handling of -0.0 --- src/uu/seq/src/extendedbigdecimal.rs | 16 ++-------- src/uu/seq/src/seq.rs | 46 ++++++++++------------------ 2 files changed, 19 insertions(+), 43 deletions(-) diff --git a/src/uu/seq/src/extendedbigdecimal.rs b/src/uu/seq/src/extendedbigdecimal.rs index 3c7e3df53..77d7fa423 100644 --- a/src/uu/seq/src/extendedbigdecimal.rs +++ b/src/uu/seq/src/extendedbigdecimal.rs @@ -130,14 +130,7 @@ impl Display for ExtendedBigDecimal { } ExtendedBigDecimal::Infinity => f32::INFINITY.fmt(f), ExtendedBigDecimal::MinusInfinity => f32::NEG_INFINITY.fmt(f), - ExtendedBigDecimal::MinusZero => { - // FIXME In Rust version 1.53.0 and later, the display - // of floats was updated to allow displaying negative - // zero. See - // https://github.com/rust-lang/rust/pull/78618. Currently, - // this just formats "0.0". - (0.0f32).fmt(f) - } + ExtendedBigDecimal::MinusZero => (-0.0f32).fmt(f), ExtendedBigDecimal::Nan => "nan".fmt(f), } } @@ -280,11 +273,6 @@ mod tests { assert_eq!(format!("{}", ExtendedBigDecimal::Infinity), "inf"); assert_eq!(format!("{}", ExtendedBigDecimal::MinusInfinity), "-inf"); assert_eq!(format!("{}", ExtendedBigDecimal::Nan), "nan"); - // FIXME In Rust version 1.53.0 and later, the display of floats - // was updated to allow displaying negative zero. Until then, we - // just display `MinusZero` as "0.0". - // - // assert_eq!(format!("{}", ExtendedBigDecimal::MinusZero), "-0.0"); - // + assert_eq!(format!("{}", ExtendedBigDecimal::MinusZero), "-0"); } } diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index af961a493..3646effc1 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -198,41 +198,29 @@ fn done_printing(next: &T, increment: &T, last: &T) -> boo } /// Write a big decimal formatted according to the given parameters. -/// -/// This method is an adapter to support displaying negative zero on -/// Rust versions earlier than 1.53.0. After that version, we should be -/// able to display negative zero using the default formatting provided -/// by `-0.0f32`, for example. fn write_value_float( writer: &mut impl Write, value: &ExtendedBigDecimal, width: usize, precision: usize, - is_first_iteration: bool, + _is_first_iteration: bool, ) -> std::io::Result<()> { - let value_as_str = if *value == ExtendedBigDecimal::MinusZero && is_first_iteration { - format!( - "-{value:>0width$.precision$}", - value = value, - width = if width > 0 { width - 1 } else { width }, - precision = precision, - ) - } else if *value == ExtendedBigDecimal::Infinity || *value == ExtendedBigDecimal::MinusInfinity - { - format!( - "{value:>width$.precision$}", - value = value, - width = width, - precision = precision, - ) - } else { - format!( - "{value:>0width$.precision$}", - value = value, - width = width, - precision = precision, - ) - }; + let value_as_str = + if *value == ExtendedBigDecimal::Infinity || *value == ExtendedBigDecimal::MinusInfinity { + format!( + "{value:>width$.precision$}", + value = value, + width = width, + precision = precision, + ) + } else { + format!( + "{value:>0width$.precision$}", + value = value, + width = width, + precision = precision, + ) + }; write!(writer, "{}", value_as_str) } From ad1954bd16f7c60eae3c764363cb3e8f027bb6d7 Mon Sep 17 00:00:00 2001 From: Tevfik Serhan Sekman Date: Sat, 12 Feb 2022 08:02:38 +0300 Subject: [PATCH 103/161] pr: add missing about and version to documentation --- src/uu/pr/src/pr.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index 6282be454..c167770c0 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -32,6 +32,8 @@ type IOError = std::io::Error; const NAME: &str = "pr"; const VERSION: &str = env!("CARGO_PKG_VERSION"); +const ABOUT: &str = + "Write content of given file or standard input to standard output with pagination filter"; const TAB: char = '\t'; const LINES_PER_PAGE: usize = 66; const LINES_PER_PAGE_FOR_FORM_FEED: usize = 63; @@ -172,7 +174,10 @@ quick_error! { } pub fn uu_app<'a>() -> App<'a> { - App::new(uucore::util_name()).setting(AppSettings::InferLongArgs) + App::new(uucore::util_name()) + .version(VERSION) + .about(ABOUT) + .setting(AppSettings::InferLongArgs) } #[uucore::main] From 45a1b7e4bb71039098a6c07d39b73d10183d6f40 Mon Sep 17 00:00:00 2001 From: Hanif Ariffin Date: Sat, 12 Feb 2022 18:39:17 +0800 Subject: [PATCH 104/161] ls: refactor out padding calculations (#3072) * Refactor padding calculations into a function * Propagate all write and (most) flush errors --- src/uu/ls/src/ls.rs | 222 +++++++++++++++++++++----------------------- 1 file changed, 105 insertions(+), 117 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index d82915c85..427829c22 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -324,16 +324,16 @@ struct LongFormat { struct PaddingCollection { #[cfg(unix)] - longest_inode_len: usize, - longest_link_count_len: usize, - longest_uname_len: usize, - longest_group_len: usize, - longest_context_len: usize, - longest_size_len: usize, + inode: usize, + link_count: usize, + uname: usize, + group: usize, + context: usize, + size: usize, #[cfg(unix)] - longest_major_len: usize, + major: usize, #[cfg(unix)] - longest_minor_len: usize, + minor: usize, } impl Config { @@ -1305,9 +1305,9 @@ only ignore '.' and '..'.", ) } -/// Represents a Path along with it's associated data -/// Any data that will be reused several times makes sense to be added to this structure -/// Caching data here helps eliminate redundant syscalls to fetch same information +/// Represents a Path along with it's associated data. +/// Any data that will be reused several times makes sense to be added to this structure. +/// Caching data here helps eliminate redundant syscalls to fetch same information. #[derive(Debug)] struct PathData { // Result got from symlink_metadata() or metadata() based on config @@ -1708,92 +1708,10 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter { - let _ = write!(out, " {}", pad_left(&size, padding.longest_size_len),); + let _ = write!(out, " {}", pad_left(&size, padding.size),); } SizeOrDeviceId::Device(major, minor) => { let _ = write!( @@ -2035,10 +1949,10 @@ fn display_item_long( #[cfg(not(unix))] 0usize, #[cfg(unix)] - padding.longest_major_len.max( + padding.major.max( padding - .longest_size_len - .saturating_sub(padding.longest_minor_len.saturating_add(2usize)) + .size + .saturating_sub(padding.minor.saturating_add(2usize)) ) ), pad_left( @@ -2046,7 +1960,7 @@ fn display_item_long( #[cfg(not(unix))] 0usize, #[cfg(unix)] - padding.longest_minor_len, + padding.minor, ), ); } @@ -2060,7 +1974,7 @@ fn display_item_long( #[cfg(unix)] { if config.inode { - let _ = write!(out, "{} ", pad_left("?", padding.longest_inode_len),); + let _ = write!(out, "{} ", pad_left("?", padding.inode),); } } @@ -2108,29 +2022,29 @@ fn display_item_long( } else { "" }, - pad_left("?", padding.longest_link_count_len), + pad_left("?", padding.link_count), ); if config.long.owner { - let _ = write!(out, " {}", pad_right("?", padding.longest_uname_len)); + let _ = write!(out, " {}", pad_right("?", padding.uname)); } if config.long.group { - let _ = write!(out, " {}", pad_right("?", padding.longest_group_len)); + let _ = write!(out, " {}", pad_right("?", padding.group)); } if config.context { let _ = write!( out, " {}", - pad_right(&item.security_context, padding.longest_context_len) + pad_right(&item.security_context, padding.context) ); } // Author is only different from owner on GNU/Hurd, so we reuse // the owner, since GNU/Hurd is not currently supported by Rust. if config.long.author { - let _ = write!(out, " {}", pad_right("?", padding.longest_uname_len)); + let _ = write!(out, " {}", pad_right("?", padding.uname)); } let dfn = display_file_name(item, config, None, 0, out).contents; @@ -2139,7 +2053,7 @@ fn display_item_long( let _ = writeln!( out, " {} {} {}", - pad_left("?", padding.longest_size_len), + pad_left("?", padding.size), pad_left("?", date_len), dfn, ); @@ -2594,3 +2508,77 @@ fn get_security_context(config: &Config, p_buf: &Path, must_dereference: bool) - substitute_string } } + +#[cfg(unix)] +fn calculate_padding_collection( + items: &[PathData], + config: &Config, + out: &mut BufWriter, +) -> PaddingCollection { + let mut padding_collections = PaddingCollection { + inode: 1, + link_count: 1, + uname: 1, + group: 1, + context: 1, + size: 1, + major: 1, + minor: 1, + }; + + for item in items { + let context_len = item.security_context.len(); + let (link_count_len, uname_len, group_len, size_len, major_len, minor_len, inode_len) = + display_dir_entry_size(item, config, out); + padding_collections.inode = inode_len.max(padding_collections.inode); + padding_collections.link_count = link_count_len.max(padding_collections.link_count); + padding_collections.uname = uname_len.max(padding_collections.uname); + padding_collections.group = group_len.max(padding_collections.group); + if config.context { + padding_collections.context = context_len.max(padding_collections.context); + } + if items.len() == 1usize { + padding_collections.size = 0usize; + padding_collections.major = 0usize; + padding_collections.minor = 0usize; + } else { + padding_collections.major = major_len.max(padding_collections.major); + padding_collections.minor = minor_len.max(padding_collections.minor); + padding_collections.size = size_len + .max(padding_collections.size) + .max(padding_collections.major + padding_collections.minor + 2usize); + } + } + + padding_collections +} + +#[cfg(not(unix))] +fn calculate_padding_collection( + items: &[PathData], + config: &Config, + out: &mut BufWriter, +) -> PaddingCollection { + let mut padding_collections = PaddingCollection { + link_count: 1, + uname: 1, + group: 1, + context: 1, + size: 1, + }; + + for item in items { + let context_len = item.security_context.len(); + let (link_count_len, uname_len, group_len, size_len, _major_len, _minor_len, _inode_len) = + display_dir_entry_size(item, config, out); + padding_collections.link_count = link_count_len.max(padding_collections.link_count); + padding_collections.uname = uname_len.max(padding_collections.uname); + padding_collections.group = group_len.max(padding_collections.group); + if config.context { + padding_collections.context = context_len.max(padding_collections.context); + } + padding_collections.size = size_len.max(padding_collections.size); + } + + padding_collections +} From d4a4c5426f73d9ddb23eaf0dedbe35d37873cca5 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 11 Feb 2022 19:16:33 +0100 Subject: [PATCH 105/161] make: add clean target for docs --- docs/Makefile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index f56df90fb..dd700bcb0 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,5 +1,4 @@ -UseGNU=gmake $* -all: - @$(UseGNU) -.DEFAULT: - @$(UseGNU) +clean: + rm -rf _build + rm -f src/SUMMARY.md + rm -f src/utils/* From d9c2acc2ed4c1273dbe77b76d79b3e0eb5c8c393 Mon Sep 17 00:00:00 2001 From: alextibbles <45136886+alextibbles@users.noreply.github.com> Date: Sat, 12 Feb 2022 12:12:02 -0500 Subject: [PATCH 106/161] update to sha 0.10.0 (#3110) * update to sha 0.10.0 * correct formatting --- Cargo.lock | 21 ++++++++++++--------- Cargo.toml | 3 ++- src/uu/hashsum/Cargo.toml | 2 +- src/uu/hashsum/src/digest.rs | 8 ++++---- tests/by-util/test_factor.rs | 22 ++++++++++++++++++---- 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef1cd2054..90b71d2a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,6 +312,7 @@ dependencies = [ "conv", "filetime", "glob", + "hex-literal", "lazy_static", "libc", "nix 0.23.1", @@ -903,6 +904,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6a22814455d41612f41161581c2883c0c6a1c41852729b17d5ed88f01e153aa" +[[package]] +name = "hex-literal" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" + [[package]] name = "hostname" version = "0.3.1" @@ -1737,19 +1744,15 @@ dependencies = [ [[package]] name = "sha1" -version = "0.6.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +checksum = "04cc229fb94bcb689ffc39bd4ded842f6ff76885efede7c6d1ffb62582878bea" dependencies = [ - "sha1_smol", + "cfg-if 1.0.0", + "cpufeatures", + "digest", ] -[[package]] -name = "sha1_smol" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - [[package]] name = "sha2" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index e9fbe42fb..336729813 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -372,13 +372,14 @@ libc = "0.2" pretty_assertions = "1" rand = "0.8" regex = "1.0" -sha1 = { version="0.6", features=["std"] } +sha1 = { version="0.10", features=["std"] } tempfile = "3.2.0" time = "0.1" unindent = "0.1" uucore = { version=">=0.0.11", package="uucore", path="src/uucore", features=["entries", "process"] } walkdir = "2.2" atty = "0.2" +hex-literal = "0.3.1" [target.'cfg(target_os = "linux")'.dev-dependencies] rlimit = "0.4.0" diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index 495e15972..d3170689a 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -23,7 +23,7 @@ memchr = "2" md5 = "0.3.5" regex = "1.0.1" regex-syntax = "0.6.7" -sha1 = "0.6.0" +sha1 = "0.10.0" sha2 = "0.10.1" sha3 = "0.10.0" blake2b_simd = "0.5.11" diff --git a/src/uu/hashsum/src/digest.rs b/src/uu/hashsum/src/digest.rs index c06834c74..678c44886 100644 --- a/src/uu/hashsum/src/digest.rs +++ b/src/uu/hashsum/src/digest.rs @@ -106,19 +106,19 @@ impl Digest for blake3::Hasher { impl Digest for sha1::Sha1 { fn new() -> Self { - Self::new() + Self::default() } fn input(&mut self, input: &[u8]) { - self.update(input); + digest::Digest::update(self, input); } fn result(&mut self, out: &mut [u8]) { - out.copy_from_slice(&self.digest().bytes()); + digest::Digest::finalize_into_reset(self, out.into()); } fn reset(&mut self) { - self.reset(); + *self = Self::new(); } fn output_bits(&self) -> usize { diff --git a/tests/by-util/test_factor.rs b/tests/by-util/test_factor.rs index bd265f4ce..7c1e540b6 100644 --- a/tests/by-util/test_factor.rs +++ b/tests/by-util/test_factor.rs @@ -29,6 +29,8 @@ const NUM_TESTS: usize = 100; #[test] fn test_parallel() { + use hex_literal::hex; + use sha1::{Digest, Sha1}; // factor should only flush the buffer at line breaks let n_integers = 100_000; let mut input_string = String::new(); @@ -60,13 +62,20 @@ fn test_parallel() { .ccmd("sort") .arg(tmp_dir.plus("output")) .succeeds(); - let hash_check = sha1::Sha1::from(result.stdout()).hexdigest(); - assert_eq!(hash_check, "cc743607c0ff300ff575d92f4ff0c87d5660c393"); + let mut hasher = Sha1::new(); + hasher.update(result.stdout()); + let hash_check = hasher.finalize(); + assert_eq!( + hash_check[..], + hex!("cc743607c0ff300ff575d92f4ff0c87d5660c393") + ); } #[test] fn test_first_100000_integers() { extern crate sha1; + use hex_literal::hex; + use sha1::{Digest, Sha1}; let n_integers = 100_000; let mut input_string = String::new(); @@ -78,8 +87,13 @@ fn test_first_100000_integers() { let result = new_ucmd!().pipe_in(input_string.as_bytes()).succeeds(); // `seq 0 100000 | factor | sha1sum` => "4ed2d8403934fa1c76fe4b84c5d4b8850299c359" - let hash_check = sha1::Sha1::from(result.stdout()).hexdigest(); - assert_eq!(hash_check, "4ed2d8403934fa1c76fe4b84c5d4b8850299c359"); + let mut hasher = Sha1::new(); + hasher.update(result.stdout()); + let hash_check = hasher.finalize(); + assert_eq!( + hash_check[..], + hex!("4ed2d8403934fa1c76fe4b84c5d4b8850299c359") + ); } #[test] From 25490b21008b23415fffdac7edf15189ec6ee049 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 12 Feb 2022 19:20:17 +0100 Subject: [PATCH 107/161] gnu/test: add the iso en_us locale to help with some tests --- .github/workflows/GnuTests.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 69a26608c..f7af71a3e 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -71,6 +71,19 @@ jobs: ## Install dependencies sudo apt-get update sudo apt-get install autoconf autopoint bison texinfo gperf gcc g++ gdb python-pyinotify jq + - name: Add various locales + shell: bash + run: | + echo "Before:" + locale -a + ## Some tests fail with 'cannot change locale (en_US.ISO-8859-1): No such file or directory' + ## Some others need a French locale + sudo locale-gen + sudo locale-gen fr_FR + sudo locale-gen fr_FR.UTF-8 + sudo update-locale + echo "After:" + locale -a - name: Build binaries shell: bash run: | From b13718e742f83b289938ebd327dd390a1ba3e3ec Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sat, 12 Feb 2022 14:45:45 -0500 Subject: [PATCH 108/161] head: use Self instead of enum name Mode in method --- src/uu/head/src/head.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index ac2e4561e..9e581a582 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -124,17 +124,17 @@ impl Mode { let (n, all_but_last) = parse::parse_num(v).map_err(|err| format!("invalid number of bytes: {}", err))?; if all_but_last { - Ok(Mode::AllButLastBytes(n)) + Ok(Self::AllButLastBytes(n)) } else { - Ok(Mode::FirstBytes(n)) + Ok(Self::FirstBytes(n)) } } else if let Some(v) = matches.value_of(options::LINES_NAME) { let (n, all_but_last) = parse::parse_num(v).map_err(|err| format!("invalid number of lines: {}", err))?; if all_but_last { - Ok(Mode::AllButLastLines(n)) + Ok(Self::AllButLastLines(n)) } else { - Ok(Mode::FirstLines(n)) + Ok(Self::FirstLines(n)) } } else { Ok(Default::default()) From ee40e9943774e2145d80b80259dd4239e97ce0b6 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Tue, 8 Feb 2022 11:11:15 -0600 Subject: [PATCH 109/161] maint/CICD ~ (GnuTests) use last 'completed' GnuTests on default branch as reference --- .github/workflows/GnuTests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 69a26608c..3ecac8520 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -53,10 +53,13 @@ jobs: fetch-depth: 0 # full depth checkout (o/w gnu gets upset if gnulib is a shallow checkout) - name: Retrieve reference artifacts uses: dawidd6/action-download-artifact@v2 + # ref: continue-on-error: true ## don't break the build for missing reference artifacts (may be expired or just not generated yet) with: workflow: GnuTests.yml branch: "${{ steps.vars.outputs.repo_reference_branch }}" + # workflow_conclusion: success ## (default); * but, if commit with failed GnuTests is merged into the default branch, future commits will all show regression errors in GnuTests CI until o/w fixed + workflow_conclusion: completed ## continually recalibrates to last commit of default branch with a successful GnuTests (ie, "self-heals" from GnuTest regressions, but needs more supervision for/of regressions) path: "${{ steps.vars.outputs.path_reference }}" - name: Install `rust` toolchain uses: actions-rs/toolchain@v1 From fb4b52335327d3c3443cb2551f2dd27ba3ed71f7 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sun, 6 Feb 2022 12:14:12 -0600 Subject: [PATCH 110/161] maint/CICD ~ (GnuTests) add 'repo_default_branch' to VARs --- .github/workflows/GnuTests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 3ecac8520..98bb99ecd 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -23,10 +23,11 @@ jobs: path_reference="reference" outputs path_GNU path_GNU_tests path_GNULIB path_reference path_UUTILS # + repo_default_branch="${{ github.event.repository.default_branch }}" repo_GNU_ref="v9.0" repo_GNULIB_ref="8e99f24c0931a38880c6ee9b8287c7da80b0036b" repo_reference_branch="${{ github.event.repository.default_branch }}" - outputs repo_GNU_ref repo_GNULIB_ref repo_reference_branch + outputs repo_default_branch repo_GNU_ref repo_GNULIB_ref repo_reference_branch # SUITE_LOG_FILE="${path_GNU_tests}/test-suite.log" TEST_LOGS_GLOB="${path_GNU_tests}/**/*.log" ## note: not usable at bash CLI; [why] double globstar not enabled by default b/c MacOS includes only bash v3 which doesn't have double globstar support @@ -160,7 +161,7 @@ jobs: do if ! grep -Fxq $LINE<<<"$REF_FAILING" then - echo "::error ::GNU test failed: $LINE. $LINE is passing on 'main'. Maybe you have to rebase?" + echo "::error ::GNU test failed: $LINE. $LINE is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?" fi done else From 1711ea0f5bb56298e2f55a82da30a34563caf815 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sun, 6 Feb 2022 13:21:13 -0600 Subject: [PATCH 111/161] maint/dev ~ update EditorConfig --- .editorconfig | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/.editorconfig b/.editorconfig index d93fa7c0e..53ccc4f9a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# EditorConfig (is awesome): http://EditorConfig.org +# EditorConfig (is awesome!; ref: http://EditorConfig.org; v2022.02.11 [rivy]) # * top-most EditorConfig file root = true @@ -13,27 +13,49 @@ insert_final_newline = true max_line_length = 100 trim_trailing_whitespace = true -[[Mm]akefile{,.*}, *.{mk,[Mm][Kk]}] +[{[Mm]akefile{,.*},*.{mak,mk,[Mm][Aa][Kk],[Mm][Kk]},[Gg][Nn][Uu]makefile}] # makefiles ~ TAB-style indentation indent_style = tab +[*.bash] +# `bash` shell scripts +indent_size = 4 +indent_style = space +# * ref: +# shell_variant = bash ## allow `shellcheck` to decide via script hash-bang/sha-bang line +switch_case_indent = true + [*.{bat,cmd,[Bb][Aa][Tt],[Cc][Mm][Dd]}] # BAT/CMD ~ DOS/Win requires BAT/CMD files to have CRLF EOLNs end_of_line = crlf +[*.{cjs,cjx,cts,ctx,js,jsx,mjs,mts,mtx,ts,tsx,json,jsonc}] +# js/ts/json ~ Prettier/XO-style == TAB indention + SPACE alignment +indent_size = 2 +indent_style = tab + [*.go] # go ~ TAB-style indentation (SPACE-style alignment); ref: @@ indent_style = tab -[*.{cjs,js,json,mjs,ts}] -# js/ts -indent_size = 2 - [*.{markdown,md,mkd,[Mm][Dd],[Mm][Kk][Dd],[Mm][Dd][Oo][Ww][Nn],[Mm][Kk][Dd][Oo][Ww][Nn],[Mm][Aa][Rr][Kk][Dd][Oo][Ww][Nn]}] # markdown indent_size = 2 indent_style = space +[*.sh] +# POSIX shell scripts +indent_size = 4 +indent_style = space +# * ref: +# shell_variant = posix ## allow `shellcheck` to decide via script hash-bang/sha-bang line +switch_case_indent = true + +[*.{sln,vc{,x}proj{,.*},[Ss][Ln][Nn],[Vv][Cc]{,[Xx]}[Pp][Rr][Oo][Jj]{,.*}}] +# MSVC sln/vcproj/vcxproj files, when used, will persistantly revert to CRLF EOLNs and eat final EOLs +end_of_line = crlf +insert_final_newline = false + [*.{yaml,yml,[Yy][Mm][Ll],[Yy][Aa][Mm][Ll]}] # YAML indent_size = 2 From 0b8f54b739e21adf44b79ce480cddbc74e7e52b6 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sun, 6 Feb 2022 13:41:48 -0600 Subject: [PATCH 112/161] maint/dev ~ (VSCode) add shell script formatter to recommendations --- .vscode/extensions.json | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a02baee69..bd9dcf485 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,10 +1,13 @@ // spell-checker:ignore (misc) matklad // see for the documentation about the extensions.json format +// * +// "foxundermoon.shell-format" ~ shell script formatting ; note: ENABLE "Use EditorConfig" +// "matklad.rust-analyzer" ~ `rust` language support +// "streetsidesoftware.code-spell-checker" ~ `cspell` spell-checker support { - "recommendations": [ - // Rust language support - "matklad.rust-analyzer", - // `cspell` spell-checker support - "streetsidesoftware.code-spell-checker" - ] + "recommendations": [ + "matklad.rust-analyzer", + "streetsidesoftware.code-spell-checker", + "foxundermoon.shell-format" + ] } From 3a13857dc35e8b2a9b75bef099da2898b80c64bb Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sun, 6 Feb 2022 16:58:11 -0600 Subject: [PATCH 113/161] maint/util ~ add `dwr` (for interactive removal of workflow runs from CLI) --- util/dwr.sh | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 util/dwr.sh diff --git a/util/dwr.sh b/util/dwr.sh new file mode 100644 index 000000000..ff1f81170 --- /dev/null +++ b/util/dwr.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +# `dwr` - delete workflow runs (by DJ Adams) +# ref: +# ref: [Mass deletion of GitHub Actions workflow runs](https://qmacro.org/autodidactics/2021/03/26/mass-deletion-of-github-actions-workflow-runs) @@ + +# LICENSE: "Feel free to steal, modify, or make fun of" (from ) + +# spell-checker:ignore (options) multi ; (people) DJ Adams * qmacro ; (words) gsub + +# Given an "owner/repo" name, such as "qmacro/thinking-aloud", +# retrieve the workflow runs for that repo and present them in a +# list. Selected runs will be deleted. Uses the GitHub API. + +# Requires gh (GitHub CLI) and jq (JSON processor) + +# First version + +set -o errexit +set -o pipefail + +declare repo=${1:?No owner/repo specified} + +jq_script() { + + cat < Date: Sun, 6 Feb 2022 16:58:50 -0600 Subject: [PATCH 114/161] maint/polish ~ (util) `shfmt -w -i=4 -ci` --- util/build-code_coverage.sh | 9 +++++---- util/publish.sh | 10 ++++++---- util/show-code_coverage.sh | 8 ++++---- util/show-utils.sh | 16 ++++++++-------- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/util/build-code_coverage.sh b/util/build-code_coverage.sh index b92b7eb48..fdd68a504 100755 --- a/util/build-code_coverage.sh +++ b/util/build-code_coverage.sh @@ -12,7 +12,7 @@ ME_dir="$(dirname -- "$(readlink -fm -- "$0")")" REPO_main_dir="$(dirname -- "${ME_dir}")" cd "${REPO_main_dir}" && -echo "[ \"$PWD\" ]" + echo "[ \"$PWD\" ]" #shellcheck disable=SC2086 UTIL_LIST=$("${ME_dir}"/show-utils.sh ${FEATURES_OPTION}) @@ -26,13 +26,14 @@ done # cargo clean export CARGO_INCREMENTAL=0 -export RUSTC_WRAPPER="" ## NOTE: RUSTC_WRAPPER=='sccache' breaks code coverage calculations (uu_*.gcno files are not created during build) +export RUSTC_WRAPPER="" ## NOTE: RUSTC_WRAPPER=='sccache' breaks code coverage calculations (uu_*.gcno files are not created during build) # export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads" export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" export RUSTDOCFLAGS="-Cpanic=abort" export RUSTUP_TOOLCHAIN="nightly-gnu" #shellcheck disable=SC2086 -{ cargo build ${FEATURES_OPTION} +{ + cargo build ${FEATURES_OPTION} cargo test --no-run ${FEATURES_OPTION} cargo test --quiet ${FEATURES_OPTION} cargo test --quiet ${FEATURES_OPTION} ${CARGO_INDIVIDUAL_PACKAGE_OPTIONS} @@ -55,4 +56,4 @@ if genhtml --version 2>/dev/null 1>&2; then else grcov . --output-type html --output-path "${COVERAGE_REPORT_DIR}" --branch --ignore build.rs --ignore '/*' --ignore '[A-Za-z]:/*' --ignore 'C:/Users/*' --excl-br-line '^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()' fi -if [ $? -ne 0 ]; then exit 1 ; fi +if [ $? -ne 0 ]; then exit 1; fi diff --git a/util/publish.sh b/util/publish.sh index 6f4d9f237..edd9779ef 100755 --- a/util/publish.sh +++ b/util/publish.sh @@ -3,11 +3,12 @@ set -e ARG="" if test "$1" != "--do-it"; then - ARG="--dry-run --allow-dirty" + ARG="--dry-run --allow-dirty" fi -for dir in src/uucore/ src/uucore_procs/ src/uu/stdbuf/src/libstdbuf/ ; do - ( cd "$dir" +for dir in src/uucore/ src/uucore_procs/ src/uu/stdbuf/src/libstdbuf/; do + ( + cd "$dir" #shellcheck disable=SC2086 cargo publish $ARG ) @@ -16,7 +17,8 @@ done PROGS=$(ls -1d src/uu/*/) for p in $PROGS; do - ( cd "$p" + ( + cd "$p" #shellcheck disable=SC2086 cargo publish $ARG ) diff --git a/util/show-code_coverage.sh b/util/show-code_coverage.sh index 6226d856b..14042a056 100755 --- a/util/show-code_coverage.sh +++ b/util/show-code_coverage.sh @@ -7,9 +7,9 @@ REPO_main_dir="$(dirname -- "${ME_dir}")" export COVERAGE_REPORT_DIR="${REPO_main_dir}/target/debug/coverage-nix" -if ! "${ME_dir}/build-code_coverage.sh"; then exit 1 ; fi +if ! "${ME_dir}/build-code_coverage.sh"; then exit 1; fi case ";$OSID_tags;" in - *";wsl;"* ) powershell.exe -c "$(wslpath -w "${COVERAGE_REPORT_DIR}"/index.html)" ;; - * ) xdg-open --version >/dev/null 2>&1 && xdg-open "${COVERAGE_REPORT_DIR}"/index.html || echo "report available at '\"${COVERAGE_REPORT_DIR}\"/index.html'" ;; -esac ; + *";wsl;"*) powershell.exe -c "$(wslpath -w "${COVERAGE_REPORT_DIR}"/index.html)" ;; + *) xdg-open --version >/dev/null 2>&1 && xdg-open "${COVERAGE_REPORT_DIR}"/index.html || echo "report available at '\"${COVERAGE_REPORT_DIR}\"/index.html'" ;; +esac diff --git a/util/show-utils.sh b/util/show-utils.sh index f69b42678..0db72e1c4 100755 --- a/util/show-utils.sh +++ b/util/show-utils.sh @@ -17,11 +17,11 @@ project_main_dir="${ME_parent_dir_abs}" # printf 'project_main_dir="%s"\n' "${project_main_dir}" cd "${project_main_dir}" && -# `jq` available? -if ! jq --version 1>/dev/null 2>&1; then - echo "WARN: missing \`jq\` (install with \`sudo apt install jq\`); falling back to default (only fully cross-platform) utility list" 1>&2 - echo "$default_utils" -else - cargo metadata "$*" --format-version 1 | jq -r "[.resolve.nodes[] | { id: .id, deps: [.deps[] | { name:.name, pkg:.pkg }] }] | .[] | select(.id|startswith(\"coreutils\")) | [.deps[] | select((.name|startswith(\"uu_\")) or (.pkg|startswith(\"uu_\")))] | [.[].pkg | match(\"^\\\w+\";\"g\")] | [.[].string | sub(\"^uu_\"; \"\")] | sort | join(\" \")" - # cargo metadata "$*" --format-version 1 | jq -r "[.resolve.nodes[] | { id: .id, deps: [.deps[] | { name:.name, pkg:.pkg }] }] | .[] | select(.id|startswith(\"coreutils\")) | [.deps[] | select((.name|startswith(\"uu_\")) or (.pkg|startswith(\"uu_\")))] | [.[].pkg | match(\"^\\\w+\";\"g\")] | [.[].string] | sort | join(\" \")" -fi + # `jq` available? + if ! jq --version 1>/dev/null 2>&1; then + echo "WARN: missing \`jq\` (install with \`sudo apt install jq\`); falling back to default (only fully cross-platform) utility list" 1>&2 + echo "$default_utils" + else + cargo metadata "$*" --format-version 1 | jq -r "[.resolve.nodes[] | { id: .id, deps: [.deps[] | { name:.name, pkg:.pkg }] }] | .[] | select(.id|startswith(\"coreutils\")) | [.deps[] | select((.name|startswith(\"uu_\")) or (.pkg|startswith(\"uu_\")))] | [.[].pkg | match(\"^\\\w+\";\"g\")] | [.[].string | sub(\"^uu_\"; \"\")] | sort | join(\" \")" + # cargo metadata "$*" --format-version 1 | jq -r "[.resolve.nodes[] | { id: .id, deps: [.deps[] | { name:.name, pkg:.pkg }] }] | .[] | select(.id|startswith(\"coreutils\")) | [.deps[] | select((.name|startswith(\"uu_\")) or (.pkg|startswith(\"uu_\")))] | [.[].pkg | match(\"^\\\w+\";\"g\")] | [.[].string] | sort | join(\" \")" + fi From a970c8d45d31528ff2eec899b2aaddc1c17b4934 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sun, 6 Feb 2022 17:01:50 -0600 Subject: [PATCH 115/161] maint/util ~ fix `shellcheck` complaints --- util/build-code_coverage.sh | 1 + util/show-code_coverage.sh | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/util/build-code_coverage.sh b/util/build-code_coverage.sh index fdd68a504..d0f464805 100755 --- a/util/build-code_coverage.sh +++ b/util/build-code_coverage.sh @@ -56,4 +56,5 @@ if genhtml --version 2>/dev/null 1>&2; then else grcov . --output-type html --output-path "${COVERAGE_REPORT_DIR}" --branch --ignore build.rs --ignore '/*' --ignore '[A-Za-z]:/*' --ignore 'C:/Users/*' --excl-br-line '^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()' fi +# shellcheck disable=SC2181 if [ $? -ne 0 ]; then exit 1; fi diff --git a/util/show-code_coverage.sh b/util/show-code_coverage.sh index 14042a056..2701d6466 100755 --- a/util/show-code_coverage.sh +++ b/util/show-code_coverage.sh @@ -9,7 +9,21 @@ export COVERAGE_REPORT_DIR="${REPO_main_dir}/target/debug/coverage-nix" if ! "${ME_dir}/build-code_coverage.sh"; then exit 1; fi -case ";$OSID_tags;" in +# WSL? +if [ -z "${OSID_tags}" ]; then + if [ -e '/proc/sys/fs/binfmt_misc/WSLInterop' ] && (grep '^enabled$' '/proc/sys/fs/binfmt_misc/WSLInterop' >/dev/null); then + __="wsl" + case ";${OSID_tags};" in ";;") OSID_tags="$__" ;; *";$__;"*) ;; *) OSID_tags="$__;$OSID_tags" ;; esac + unset __ + # Windows version == ... + # Release ID; see [Release ID/Version vs Build](https://winreleaseinfoprod.blob.core.windows.net/winreleaseinfoprod/en-US.html)[`@`](https://archive.is/GOj1g) + OSID_wsl_build="$(uname -r | sed 's/^[0-9.][0-9.]*-\([0-9][0-9]*\)-.*$/\1/g')" + OSID_wsl_revision="$(uname -v | sed 's/^#\([0-9.][0-9.]*\)-.*$/\1/g')" + export OSID_wsl_build OSID_wsl_revision + fi +fi + +case ";${OSID_tags};" in *";wsl;"*) powershell.exe -c "$(wslpath -w "${COVERAGE_REPORT_DIR}"/index.html)" ;; *) xdg-open --version >/dev/null 2>&1 && xdg-open "${COVERAGE_REPORT_DIR}"/index.html || echo "report available at '\"${COVERAGE_REPORT_DIR}\"/index.html'" ;; esac From f75cfbdebc5920023fcafc94f33620f935d25d60 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sun, 6 Feb 2022 17:05:12 -0600 Subject: [PATCH 116/161] docs ~ (CICD/util) add/revise spell-checker exceptions --- .github/workflows/CICD.yml | 2 +- .github/workflows/GnuTests.yml | 2 +- util/build-code_coverage.sh | 2 +- util/build-gnu.sh | 23 +++++++++-------------- util/run-gnu-test.sh | 2 +- util/show-code_coverage.sh | 2 +- 6 files changed, 14 insertions(+), 19 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 81147c8dc..b47540ed9 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -4,7 +4,7 @@ name: CICD # spell-checker:ignore (env/flags) Awarnings Ccodegen Coverflow Cpanic Dwarnings RUSTDOCFLAGS RUSTFLAGS Zpanic # spell-checker:ignore (jargon) SHAs deps dequote softprops subshell toolchain # spell-checker:ignore (names) CodeCOV MacOS MinGW Peltoche rivy -# spell-checker:ignore (shell/tools) choco clippy dmake dpkg esac fakeroot gmake grcov halium lcov libssl mkdir popd printf pushd rustc rustfmt rustup shopt xargs +# spell-checker:ignore (shell/tools) choco clippy dmake dpkg esac fakeroot gmake grcov halium lcov libssl mkdir popd printf pushd rsync rustc rustfmt rustup shopt xargs # spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils gnueabihf issuecomment maint nullglob onexitbegin onexitend pell runtest tempfile testsuite uutils DESTDIR sizemulti # ToDO: [2021-06; rivy] change from `cargo-tree` to `cargo tree` once MSRV is >= 1.45 diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 98bb99ecd..738a80e84 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -1,6 +1,6 @@ name: GnuTests -# spell-checker:ignore (names) gnulib ; (people) Dawid Dziurla * dawidd6 ; (utils) autopoint chksum gperf pyinotify shopt texinfo ; (vars) FILESET XPASS +# spell-checker:ignore (names) gnulib ; (jargon) submodules ; (people) Dawid Dziurla * dawidd ; (utils) autopoint chksum gperf pyinotify shopt texinfo ; (vars) FILESET XPASS on: [push, pull_request] diff --git a/util/build-code_coverage.sh b/util/build-code_coverage.sh index d0f464805..083248d96 100755 --- a/util/build-code_coverage.sh +++ b/util/build-code_coverage.sh @@ -4,7 +4,7 @@ # spell-checker:ignore (jargon) toolchain # spell-checker:ignore (rust) Ccodegen Cinline Coverflow Cpanic RUSTC RUSTDOCFLAGS RUSTFLAGS RUSTUP Zpanic # spell-checker:ignore (shell) OSID esac -# spell-checker:ignore (utils) genhtml grcov lcov readlink sccache uutils +# spell-checker:ignore (utils) genhtml grcov lcov readlink sccache shellcheck uutils FEATURES_OPTION="--features feat_os_unix" diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 8f6e431a6..1589188b3 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -1,6 +1,6 @@ #!/bin/bash -# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall gnulib inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode ; (vars/env) BUILDDIR SRCDIR +# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall gnulib inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules ; (vars/env) BUILDDIR SRCDIR set -e if test ! -d ../gnu; then @@ -14,14 +14,12 @@ if test ! -d ../gnulib; then exit 1 fi - pushd "$PWD" make PROFILE=release BUILDDIR="$PWD/target/release/" cp "${BUILDDIR}/install" "${BUILDDIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target # Create *sum binaries -for sum in b2sum b3sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum -do +for sum in b2sum b3sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum; do sum_path="${BUILDDIR}/${sum}" test -f "${sum_path}" || cp "${BUILDDIR}/hashsum" "${sum_path}" done @@ -31,10 +29,12 @@ GNULIB_SRCDIR="$PWD/../gnulib" pushd ../gnu/ # Any binaries that aren't built become `false` so their tests fail -for binary in $(./build-aux/gen-lists-of-programs.sh --list-progs) -do +for binary in $(./build-aux/gen-lists-of-programs.sh --list-progs); do bin_path="${BUILDDIR}/${binary}" - test -f "${bin_path}" || { echo "'${binary}' was not built with uutils, using the 'false' program"; cp "${BUILDDIR}/false" "${bin_path}"; } + test -f "${bin_path}" || { + echo "'${binary}' was not built with uutils, using the 'false' program" + cp "${BUILDDIR}/false" "${bin_path}" + } done ./bootstrap --gnulib-srcdir="$GNULIB_SRCDIR" @@ -47,18 +47,15 @@ sed -i 's| tr | /usr/bin/tr |' tests/init.sh make -j "$(nproc)" # Generate the factor tests, so they can be fixed # Used to be 36. Reduced to 20 to decrease the log size -for i in {00..20} -do +for i in {00..20}; do make "tests/factor/t${i}.sh" done # strip the long stuff -for i in {21..36} -do +for i in {21..36}; do sed -i -e "s/\$(tf)\/t${i}.sh//g" Makefile done - grep -rl 'path_prepend_' tests/* | xargs sed -i 's| path_prepend_ ./src||' sed -i -e 's|^seq |/usr/bin/seq |' -e 's|sha1sum |/usr/bin/sha1sum |' tests/factor/t*sh @@ -97,11 +94,9 @@ sed -i 's|seq |/usr/bin/seq |' tests/misc/sort-discrim.sh # Add specific timeout to tests that currently hang to limit time spent waiting sed -i 's|\(^\s*\)seq \$|\1/usr/bin/timeout 0.1 seq \$|' tests/misc/seq-precision.sh tests/misc/seq-long-double.sh - # Remove dup of /usr/bin/ when executed several times grep -rlE '/usr/bin/\s?/usr/bin' init.cfg tests/* | xargs --no-run-if-empty sed -Ei 's|/usr/bin/\s?/usr/bin/|/usr/bin/|g' - #### Adjust tests to make them work with Rust/coreutils # in some cases, what we are doing in rust/coreutils is good (or better) # we should not regress our project just to match what GNU is going. diff --git a/util/run-gnu-test.sh b/util/run-gnu-test.sh index 123c4dab2..1900bb523 100755 --- a/util/run-gnu-test.sh +++ b/util/run-gnu-test.sh @@ -1,7 +1,7 @@ #!/bin/bash # `$0 [TEST]` # run GNU test (or all tests if TEST is missing/null) -# spell-checker:ignore (env/vars) BUILDDIR GNULIB SUBDIRS +# spell-checker:ignore (env/vars) GNULIB SUBDIRS ; (utils) shellcheck ME_dir="$(dirname -- "$(readlink -fm -- "$0")")" REPO_main_dir="$(dirname -- "${ME_dir}")" diff --git a/util/show-code_coverage.sh b/util/show-code_coverage.sh index 2701d6466..4be056ccc 100755 --- a/util/show-code_coverage.sh +++ b/util/show-code_coverage.sh @@ -1,6 +1,6 @@ #!/bin/sh -# spell-checker:ignore (vars) OSID +# spell-checker:ignore (vars) OSID binfmt ME_dir="$(dirname -- "$(readlink -fm -- "$0")")" REPO_main_dir="$(dirname -- "${ME_dir}")" From f477a41aeedee4bdec6efa61172bf8d8255a3dc6 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sun, 6 Feb 2022 17:06:21 -0600 Subject: [PATCH 117/161] maint/dev ~ add *empty* rustfmt configuration prompt devs to use `cargo fmt` --- .rustfmt.toml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .rustfmt.toml diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 000000000..2a0c75141 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +# * using all default `cargo fmt`/`rustfmt` options From b7676c07e9436a663f498803a9a840415c037a90 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sun, 6 Feb 2022 17:03:57 -0600 Subject: [PATCH 118/161] maint/refactor ~ (util) minor refactoring of util shell scripts --- util/GHA-delete-GNU-workflow-logs.sh | 23 ++++++++++++++--------- util/build-code_coverage.sh | 3 ++- util/show-code_coverage.sh | 3 ++- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/util/GHA-delete-GNU-workflow-logs.sh b/util/GHA-delete-GNU-workflow-logs.sh index f7406b831..ebeb7cfe8 100755 --- a/util/GHA-delete-GNU-workflow-logs.sh +++ b/util/GHA-delete-GNU-workflow-logs.sh @@ -2,10 +2,10 @@ # spell-checker:ignore (utils) gitsome jq ; (gh) repos -ME="${0}" -ME_dir="$(dirname -- "${ME}")" -ME_parent_dir="$(dirname -- "${ME_dir}")" -ME_parent_dir_abs="$(realpath -mP -- "${ME_parent_dir}")" +# ME="${0}" +# ME_dir="$(dirname -- "${ME}")" +# ME_parent_dir="$(dirname -- "${ME_dir}")" +# ME_parent_dir_abs="$(realpath -mP -- "${ME_parent_dir}")" # ref: @@ -33,12 +33,17 @@ if [ -z "${GH}" ] || [ -z "${JQ}" ]; then exit 1 fi -dry_run=true +case "${dry_run}" in + '0' | 'f' | 'false' | 'no' | 'never' | 'none') unset dry_run ;; + *) dry_run="true" ;; +esac -USER_NAME=uutils -REPO_NAME=coreutils -WORK_NAME=GNU +USER_NAME="${USER_NAME:-uutils}" +REPO_NAME="${REPO_NAME:-coreutils}" +WORK_NAME="${WORK_NAME:-GNU}" # * `--paginate` retrieves all pages # gh api --paginate "repos/${USER_NAME}/${REPO_NAME}/actions/runs" | jq -r ".workflow_runs[] | select(.name == \"${WORK_NAME}\") | (.id)" | xargs -n1 sh -c "for arg do { echo gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; if [ -z "$dry_run" ]; then gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; fi ; } ; done ;" _ -gh api "repos/${USER_NAME}/${REPO_NAME}/actions/runs" | jq -r ".workflow_runs[] | select(.name == \"${WORK_NAME}\") | (.id)" | xargs -n1 sh -c "for arg do { echo gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; if [ -z "$dry_run" ]; then gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; fi ; } ; done ;" _ +gh api "repos/${USER_NAME}/${REPO_NAME}/actions/runs" | + jq -r ".workflow_runs[] | select(.name == \"${WORK_NAME}\") | (.id)" | + xargs -n1 sh -c "for arg do { echo gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; if [ -z \"${dry_run}\" ]; then gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; fi ; } ; done ;" _ diff --git a/util/build-code_coverage.sh b/util/build-code_coverage.sh index 083248d96..4082bc13d 100755 --- a/util/build-code_coverage.sh +++ b/util/build-code_coverage.sh @@ -8,7 +8,8 @@ FEATURES_OPTION="--features feat_os_unix" -ME_dir="$(dirname -- "$(readlink -fm -- "$0")")" +ME="${0}" +ME_dir="$(dirname -- "$(readlink -fm -- "${ME}")")" REPO_main_dir="$(dirname -- "${ME_dir}")" cd "${REPO_main_dir}" && diff --git a/util/show-code_coverage.sh b/util/show-code_coverage.sh index 4be056ccc..3f51462c9 100755 --- a/util/show-code_coverage.sh +++ b/util/show-code_coverage.sh @@ -2,7 +2,8 @@ # spell-checker:ignore (vars) OSID binfmt -ME_dir="$(dirname -- "$(readlink -fm -- "$0")")" +ME="${0}" +ME_dir="$(dirname -- "$(readlink -fm -- "${ME}")")" REPO_main_dir="$(dirname -- "${ME_dir}")" export COVERAGE_REPORT_DIR="${REPO_main_dir}/target/debug/coverage-nix" From 29679ba337c94e616a81c858d156296d613b41ef Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Fri, 11 Feb 2022 00:52:28 -0600 Subject: [PATCH 119/161] maint/CICD ~ (GnuTests) refactor GnuTests GHA config - combine gnu/gnulib into single repository checkout - code consolidation - DRY changes - variable consolidation and renaming - job/step naming normalization --- .github/workflows/GnuTests.yml | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 738a80e84..bd72faf9d 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -17,11 +17,10 @@ jobs: outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } # * config path_GNU="gnu" - path_GNULIB="gnulib" - path_GNU_tests="gnu/tests" + path_GNU_tests="${path_GNU}/tests" path_UUTILS="uutils" path_reference="reference" - outputs path_GNU path_GNU_tests path_GNULIB path_reference path_UUTILS + outputs path_GNU path_GNU_tests path_reference path_UUTILS # repo_default_branch="${{ github.event.repository.default_branch }}" repo_GNU_ref="v9.0" @@ -35,23 +34,17 @@ jobs: TEST_FILESET_SUFFIX='.txt' TEST_SUMMARY_FILE='gnu-result.json' outputs SUITE_LOG_FILE TEST_FILESET_PREFIX TEST_FILESET_SUFFIX TEST_LOGS_GLOB TEST_SUMMARY_FILE - - name: Checkout code uutil + - name: Checkout code (uutil) uses: actions/checkout@v2 with: path: '${{ steps.vars.outputs.path_UUTILS }}' - - name: Checkout GNU coreutils + - name: Checkout code (GNU coreutils) uses: actions/checkout@v2 with: repository: 'coreutils/coreutils' path: '${{ steps.vars.outputs.path_GNU }}' ref: ${{ steps.vars.outputs.repo_GNU_ref }} - - name: Checkout GNU coreutils library (gnulib) - uses: actions/checkout@v2 - with: - repository: 'coreutils/gnulib' - path: '${{ steps.vars.outputs.path_GNULIB }}' - ref: ${{ steps.vars.outputs.repo_GNULIB_ref }} - fetch-depth: 0 # full depth checkout (o/w gnu gets upset if gnulib is a shallow checkout) + submodules: recursive - name: Retrieve reference artifacts uses: dawidd6/action-download-artifact@v2 # ref: @@ -85,7 +78,6 @@ jobs: shell: bash run: | path_GNU='${{ steps.vars.outputs.path_GNU }}' - path_GNULIB='${{ steps.vars.outputs.path_GNULIB }}' path_UUTILS='${{ steps.vars.outputs.path_UUTILS }}' bash "${path_UUTILS}/util/run-gnu-test.sh" - name: Extract/summarize testing info From c2e17e5f37d04af17256e2dd9a65b44c4add526d Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sun, 6 Feb 2022 17:23:02 -0600 Subject: [PATCH 120/161] maint/util ~ improve/refactor 'build-gnu' & 'run-gnu-test' - add more logging for better fault tracking - generalize for use in either RELEASE or DEBUG build mode (default to 'release') - improve variable naming precision/specificity --- util/build-gnu.sh | 75 +++++++++++++++++++++++++++++--------------- util/run-gnu-test.sh | 33 ++++++++++--------- 2 files changed, 69 insertions(+), 39 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 1589188b3..3455241a2 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -1,48 +1,73 @@ #!/bin/bash +# `build-gnu.bash` ~ builds GNU coreutils (from supplied sources) +# +# UU_MAKE_PROFILE == 'debug' | 'release' ## build profile for *uutils* build; may be supplied by caller, defaults to 'debug' -# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall gnulib inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules ; (vars/env) BUILDDIR SRCDIR +# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall gnulib inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules ; (vars/env) SRCDIR set -e -if test ! -d ../gnu; then - echo "Could not find ../gnu" - echo "git clone https://github.com/coreutils/coreutils.git gnu" - exit 1 -fi -if test ! -d ../gnulib; then - echo "Could not find ../gnulib" - echo "git clone https://github.com/coreutils/gnulib.git gnulib" + +ME="${0}" +ME_dir="$(dirname -- "$(readlink -fm -- "${ME}")")" +REPO_main_dir="$(dirname -- "${ME_dir}")" + +echo "ME='${ME}'" +echo "ME_dir='${ME_dir}'" +echo "REPO_main_dir='${REPO_main_dir}'" + +### * config (from environment with fallback defaults); note: GNU and GNULIB are expected to be sibling repo directories + +path_UUTILS=${path_UUTILS:-${REPO_main_dir}} +path_GNU="$(readlink -fm -- "${path_GNU:-${path_UUTILS}/../gnu}")" + +echo "path_UUTILS='${path_UUTILS}'" +echo "path_GNU='${path_GNU}'" + +### + +if test ! -d "${path_GNU}"; then + echo "Could not find GNU (expected at '${path_GNU}')" + echo "git clone --recurse-submodules https://github.com/coreutils/coreutils.git \"${path_GNU}\"" exit 1 fi -pushd "$PWD" -make PROFILE=release -BUILDDIR="$PWD/target/release/" -cp "${BUILDDIR}/install" "${BUILDDIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target +### + +UU_MAKE_PROFILE=${UU_MAKE_PROFILE:-release} +echo "UU_MAKE_PROFILE='${UU_MAKE_PROFILE}'" + +UU_BUILD_DIR="${path_UUTILS}/target/${UU_MAKE_PROFILE}" +echo "UU_BUILD_DIR='${UU_BUILD_DIR}'" + +cd "${path_UUTILS}" && echo "[ pwd:'${PWD}' ]" +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 # Create *sum binaries -for sum in b2sum b3sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum; do - sum_path="${BUILDDIR}/${sum}" - test -f "${sum_path}" || cp "${BUILDDIR}/hashsum" "${sum_path}" +for sum in b2sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum; do + sum_path="${UU_BUILD_DIR}/${sum}" + test -f "${sum_path}" || cp "${UU_BUILD_DIR}/hashsum" "${sum_path}" done -test -f "${BUILDDIR}/[" || cp "${BUILDDIR}/test" "${BUILDDIR}/[" -popd -GNULIB_SRCDIR="$PWD/../gnulib" -pushd ../gnu/ +test -f "${UU_BUILD_DIR}/[" || cp "${UU_BUILD_DIR}/test" "${UU_BUILD_DIR}/[" + +## + +cd "${path_GNU}" && echo "[ pwd:'${PWD}' ]" # Any binaries that aren't built become `false` so their tests fail for binary in $(./build-aux/gen-lists-of-programs.sh --list-progs); do - bin_path="${BUILDDIR}/${binary}" + bin_path="${UU_BUILD_DIR}/${binary}" test -f "${bin_path}" || { echo "'${binary}' was not built with uutils, using the 'false' program" - cp "${BUILDDIR}/false" "${bin_path}" + cp "${UU_BUILD_DIR}/false" "${bin_path}" } done -./bootstrap --gnulib-srcdir="$GNULIB_SRCDIR" +./bootstrap ./configure --quiet --disable-gcc-warnings #Add timeout to to protect against hangs sed -i 's|^"\$@|/usr/bin/timeout 600 "\$@|' build-aux/test-driver # Change the PATH in the Makefile to test the uutils coreutils instead of the GNU coreutils -sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${BUILDDIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" Makefile +sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" Makefile sed -i 's| tr | /usr/bin/tr |' tests/init.sh make -j "$(nproc)" # Generate the factor tests, so they can be fixed @@ -112,7 +137,7 @@ sed -i -e "s|rm: cannot remove 'a/1'|rm: cannot remove 'a'|g" tests/rm/rm2.sh sed -i -e "s|removed directory 'a/'|removed directory 'a'|g" tests/rm/v-slash.sh -test -f "${BUILDDIR}/getlimits" || cp src/getlimits "${BUILDDIR}" +test -f "${UU_BUILD_DIR}/getlimits" || cp src/getlimits "${UU_BUILD_DIR}" # When decoding an invalid base32/64 string, gnu writes everything it was able to decode until # it hit the decode error, while we don't write anything if the input is invalid. diff --git a/util/run-gnu-test.sh b/util/run-gnu-test.sh index 1900bb523..53ec4fc98 100755 --- a/util/run-gnu-test.sh +++ b/util/run-gnu-test.sh @@ -1,28 +1,30 @@ -#!/bin/bash -# `$0 [TEST]` +#!/bin/sh +# `run-gnu-test.bash [TEST]` # run GNU test (or all tests if TEST is missing/null) -# spell-checker:ignore (env/vars) GNULIB SUBDIRS ; (utils) shellcheck +# +# UU_MAKE_PROFILE == 'debug' | 'release' ## build profile used for *uutils* build; may be supplied by caller, defaults to 'debug' + +# spell-checker:ignore (env/vars) GNULIB SRCDIR SUBDIRS ; (utils) shellcheck ME_dir="$(dirname -- "$(readlink -fm -- "$0")")" REPO_main_dir="$(dirname -- "${ME_dir}")" +echo "ME_dir='${ME_dir}'" +echo "REPO_main_dir='${REPO_main_dir}'" + set -e -### * config (from environment with fallback defaults) +### * config (from environment with fallback defaults); note: GNU and GNULIB are expected to be sibling repo directories path_UUTILS=${path_UUTILS:-${REPO_main_dir}} -path_GNU=${path_GNU:-${path_UUTILS}/../gnu} -path_GNULIB=${path_GNULIB:-${path_UUTILS}/../gnulib} +path_GNU="$(readlink -fm -- "${path_GNU:-${path_UUTILS}/../gnu}")" + +echo "path_UUTILS='${path_UUTILS}'" +echo "path_GNU='${path_GNU}'" ### -BUILD_DIR="$(realpath -- "${path_UUTILS}/target/release")" -GNULIB_DIR="$(realpath -- "${path_GNULIB}")" - -export BUILD_DIR -export GNULIB_DIR - -pushd "$(realpath -- "${path_GNU}")" +cd "${path_GNU}" && echo "[ pwd:'${PWD}' ]" export RUST_BACKTRACE=1 @@ -31,5 +33,8 @@ if test -n "$1"; then export RUN_TEST="TESTS=$1" fi +# * timeout used to kill occasionally errant/"stuck" processes (note: 'release' testing takes ~1 hour; 'debug' testing takes ~2.5 hours) +# * `gl_public_submodule_commit=` disables testing for use of a "public" gnulib commit (which will fail when using shallow gnulib checkouts) +# * `srcdir=..` specifies the GNU source directory for tests (fixing failing/confused 'tests/factor/tNN.sh' tests and causing no harm to other tests) #shellcheck disable=SC2086 -timeout -sKILL 2h make -j "$(nproc)" check $RUN_TEST SUBDIRS=. RUN_EXPENSIVE_TESTS=yes RUN_VERY_EXPENSIVE_TESTS=yes VERBOSE=no || : # Kill after 4 hours in case something gets stuck in make +timeout -sKILL 2h make -j "$(nproc)" check ${RUN_TEST} SUBDIRS=. RUN_EXPENSIVE_TESTS=yes RUN_VERY_EXPENSIVE_TESTS=yes VERBOSE=no gl_public_submodule_commit="" srcdir="${path_GNU}" || : # Kill after 4 hours in case something gets stuck in make From ba2bf79099535e6fbb36d24e27affc2f56ac9cde Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Fri, 11 Feb 2022 23:40:34 -0600 Subject: [PATCH 121/161] maint/dev ~ (VSCode) update `cspell` settings --- .vscode/cSpell.json | 4 ++-- .vscode/settings.json | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json index 2ff4d4b7e..6dfb3b666 100644 --- a/.vscode/cSpell.json +++ b/.vscode/cSpell.json @@ -1,7 +1,7 @@ // `cspell` settings { - // version of the setting file (always 0.1) - "version": "0.1", + // version of the setting file + "version": "0.2", // spelling language "language": "en", diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..54df63a5b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{ "cSpell.import": [".vscode/cspell.json"] } From 38ac68ff33db1655b0fd68bd38f228d9df89f430 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sat, 12 Feb 2022 11:13:04 -0600 Subject: [PATCH 122/161] maint/CICD ~ (GnuTests) remove unneeded GNULIB references --- .github/workflows/GnuTests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index bd72faf9d..f95166c94 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -24,9 +24,8 @@ jobs: # repo_default_branch="${{ github.event.repository.default_branch }}" repo_GNU_ref="v9.0" - repo_GNULIB_ref="8e99f24c0931a38880c6ee9b8287c7da80b0036b" repo_reference_branch="${{ github.event.repository.default_branch }}" - outputs repo_default_branch repo_GNU_ref repo_GNULIB_ref repo_reference_branch + outputs repo_default_branch repo_GNU_ref repo_reference_branch # SUITE_LOG_FILE="${path_GNU_tests}/test-suite.log" TEST_LOGS_GLOB="${path_GNU_tests}/**/*.log" ## note: not usable at bash CLI; [why] double globstar not enabled by default b/c MacOS includes only bash v3 which doesn't have double globstar support From 40b9ebf90e98e542b0b9d87302da2c2381656dcd Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sat, 12 Feb 2022 11:14:02 -0600 Subject: [PATCH 123/161] docs ~ (util) remove outdated GNULIB comments --- util/build-gnu.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 3455241a2..842e1129b 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -3,7 +3,7 @@ # # UU_MAKE_PROFILE == 'debug' | 'release' ## build profile for *uutils* build; may be supplied by caller, defaults to 'debug' -# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall gnulib inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules ; (vars/env) SRCDIR +# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules ; (vars/env) SRCDIR set -e @@ -15,7 +15,7 @@ echo "ME='${ME}'" echo "ME_dir='${ME_dir}'" echo "REPO_main_dir='${REPO_main_dir}'" -### * config (from environment with fallback defaults); note: GNU and GNULIB are expected to be sibling repo directories +### * config (from environment with fallback defaults); note: GNU is expected to be a sibling repo directory path_UUTILS=${path_UUTILS:-${REPO_main_dir}} path_GNU="$(readlink -fm -- "${path_GNU:-${path_UUTILS}/../gnu}")" From 02aa5ea7847769f4590ae5b7bc7953a6d8844196 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sat, 12 Feb 2022 11:47:12 -0600 Subject: [PATCH 124/161] maint/util ~ (build-gnu) fix missing 'b3sum' for *sum binary creation --- util/build-gnu.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 842e1129b..2ab23ddc7 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -43,7 +43,7 @@ cd "${path_UUTILS}" && echo "[ pwd:'${PWD}' ]" 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 # Create *sum binaries -for sum in b2sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum; do +for sum in b2sum b3sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum; do sum_path="${UU_BUILD_DIR}/${sum}" test -f "${sum_path}" || cp "${UU_BUILD_DIR}/hashsum" "${sum_path}" done From 988fd658aecb3f6dea3b75b860108bee27c47bff Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sat, 12 Feb 2022 11:47:55 -0600 Subject: [PATCH 125/161] maint/util ~ (run-gnu-tests/docs) fix incorrect comment --- util/run-gnu-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/run-gnu-test.sh b/util/run-gnu-test.sh index 53ec4fc98..9f8cc1923 100755 --- a/util/run-gnu-test.sh +++ b/util/run-gnu-test.sh @@ -2,7 +2,7 @@ # `run-gnu-test.bash [TEST]` # run GNU test (or all tests if TEST is missing/null) # -# UU_MAKE_PROFILE == 'debug' | 'release' ## build profile used for *uutils* build; may be supplied by caller, defaults to 'debug' +# UU_MAKE_PROFILE == 'debug' | 'release' ## build profile used for *uutils* build; may be supplied by caller, defaults to 'release' # spell-checker:ignore (env/vars) GNULIB SRCDIR SUBDIRS ; (utils) shellcheck From dc223309e7cd84475c8126d0b5a39d336ffcea7b Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sat, 12 Feb 2022 11:48:38 -0600 Subject: [PATCH 126/161] maint/util ~ (run-gnu-tests) increase timeout to allow for longer 'debug' test runs --- util/run-gnu-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/run-gnu-test.sh b/util/run-gnu-test.sh index 9f8cc1923..4ff266833 100755 --- a/util/run-gnu-test.sh +++ b/util/run-gnu-test.sh @@ -37,4 +37,4 @@ fi # * `gl_public_submodule_commit=` disables testing for use of a "public" gnulib commit (which will fail when using shallow gnulib checkouts) # * `srcdir=..` specifies the GNU source directory for tests (fixing failing/confused 'tests/factor/tNN.sh' tests and causing no harm to other tests) #shellcheck disable=SC2086 -timeout -sKILL 2h make -j "$(nproc)" check ${RUN_TEST} SUBDIRS=. RUN_EXPENSIVE_TESTS=yes RUN_VERY_EXPENSIVE_TESTS=yes VERBOSE=no gl_public_submodule_commit="" srcdir="${path_GNU}" || : # Kill after 4 hours in case something gets stuck in make +timeout -sKILL 4h make -j "$(nproc)" check ${RUN_TEST} SUBDIRS=. RUN_EXPENSIVE_TESTS=yes RUN_VERY_EXPENSIVE_TESTS=yes VERBOSE=no gl_public_submodule_commit="" srcdir="${path_GNU}" || : # Kill after 4 hours in case something gets stuck in make From 042e537ca835245083ae864a7448f86763acb2ef Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sat, 12 Feb 2022 20:54:47 -0500 Subject: [PATCH 127/161] df: refactor is_included(), mount_info_lt() funcs Factor two helper functions, `is_included()` and `mount_info_lt()`, from the `filter_mount_list()` function. --- src/uu/df/src/df.rs | 94 ++++++++++++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 35 deletions(-) diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index a51966d9e..b4153f1af 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -161,6 +161,63 @@ impl Filesystem { } } +/// Whether to display the mount info given the inclusion settings. +fn is_included(mi: &MountInfo, paths: &[String], opt: &Options) -> bool { + // Don't show remote filesystems if `--local` has been given. + if mi.remote && opt.show_local_fs { + return false; + } + + // Don't show pseudo filesystems unless `--all` has been given. + if mi.dummy && !opt.show_all_fs && !opt.show_listed_fs { + return false; + } + + // Don't show filesystems if they have been explicitly excluded. + if !opt.fs_selector.should_select(&mi.fs_type) { + return false; + } + + // Don't show filesystems other than the ones specified on the + // command line, if any. + if !paths.is_empty() && !paths.contains(&mi.mount_dir) { + return false; + } + + true +} + +/// Whether the mount info in `m2` should be prioritized over `m1`. +/// +/// The "lt" in the function name is in analogy to the +/// [`std::cmp::PartialOrd::lt`]. +fn mount_info_lt(m1: &MountInfo, m2: &MountInfo) -> bool { + // let "real" devices with '/' in the name win. + if m1.dev_name.starts_with('/') && !m2.dev_name.starts_with('/') { + return false; + } + + let m1_nearer_root = m1.mount_dir.len() < m2.mount_dir.len(); + // With bind mounts, prefer items nearer the root of the source + let m2_below_root = !m1.mount_root.is_empty() + && !m2.mount_root.is_empty() + && m1.mount_root.len() > m2.mount_root.len(); + // let points towards the root of the device win. + if m1_nearer_root && !m2_below_root { + return false; + } + + // let an entry over-mounted on a new device win, but only when + // matching an existing mnt point, to avoid problematic + // replacement when given inaccurate mount lists, seen with some + // chroot environments for example. + if m1.dev_name != m2.dev_name && m1.mount_dir == m2.mount_dir { + return false; + } + + true +} + /// Keep only the specified subset of [`MountInfo`] instances. /// /// If `paths` is non-empty, this function excludes any [`MountInfo`] @@ -174,24 +231,7 @@ impl Filesystem { fn filter_mount_list(vmi: Vec, paths: &[String], opt: &Options) -> Vec { let mut mount_info_by_id = HashMap::>::new(); for mi in vmi { - // Don't show remote filesystems if `--local` has been given. - if mi.remote && opt.show_local_fs { - continue; - } - - // Don't show pseudo filesystems unless `--all` has been given. - if mi.dummy && !opt.show_all_fs && !opt.show_listed_fs { - continue; - } - - // Don't show filesystems if they have been explicitly excluded. - if !opt.fs_selector.should_select(&mi.fs_type) { - continue; - } - - // Don't show filesystems other than the ones specified on the - // command line, if any. - if !paths.is_empty() && !paths.contains(&mi.mount_dir) { + if !is_included(&mi, paths, opt) { continue; } @@ -207,23 +247,7 @@ fn filter_mount_list(vmi: Vec, paths: &[String], opt: &Options) -> Ve // then check if we need to update it or keep the previously // seen one. let seen = mount_info_by_id[&id].replace(mi.clone()); - let target_nearer_root = seen.mount_dir.len() > mi.mount_dir.len(); - // With bind mounts, prefer items nearer the root of the source - let source_below_root = !seen.mount_root.is_empty() - && !mi.mount_root.is_empty() - && seen.mount_root.len() < mi.mount_root.len(); - // let "real" devices with '/' in the name win. - if (!mi.dev_name.starts_with('/') || seen.dev_name.starts_with('/')) - // let points towards the root of the device win. - && (!target_nearer_root || source_below_root) - // let an entry over-mounted on a new device win... - && (seen.dev_name == mi.dev_name - /* ... but only when matching an existing mnt point, - to avoid problematic replacement when given - inaccurate mount lists, seen with some chroot - environments for example. */ - || seen.mount_dir != mi.mount_dir) - { + if mount_info_lt(&mi, &seen) { mount_info_by_id[&id].replace(seen); } } From b6d1aa3e734f0ad23863d36af2576485721ac1c9 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sat, 12 Feb 2022 21:02:09 -0500 Subject: [PATCH 128/161] df: always produce the same order in output table Change the `filter_mount_list()` function so that it always produces the same order of `MountInfo` objects. This change ultimately results in `df` printing its table of filesystems in the same order on each execution. Previously, the table was in an arbitrary order because the `MountInfo` objects were read from a `HashMap`. Fixes #3086. --- src/uu/df/src/df.rs | 50 ++++++++++++++++++---------------------- tests/by-util/test_df.rs | 21 +++++++++++++++++ 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index b4153f1af..90f1b0c9a 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -14,8 +14,6 @@ use uucore::fsext::{read_fs_list, FsUsage, MountInfo}; use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; -use std::cell::Cell; -use std::collections::HashMap; use std::collections::HashSet; #[cfg(unix)] use std::ffi::CString; @@ -218,6 +216,19 @@ fn mount_info_lt(m1: &MountInfo, m2: &MountInfo) -> bool { true } +/// Whether to prioritize given mount info over all others on the same device. +/// +/// This function decides whether the mount info `mi` is better than +/// all others in `previous` that mount the same device as `mi`. +fn is_best(previous: &[MountInfo], mi: &MountInfo) -> bool { + for seen in previous { + if seen.dev_id == mi.dev_id && mount_info_lt(mi, seen) { + return false; + } + } + true +} + /// Keep only the specified subset of [`MountInfo`] instances. /// /// If `paths` is non-empty, this function excludes any [`MountInfo`] @@ -229,35 +240,18 @@ fn mount_info_lt(m1: &MountInfo, m2: &MountInfo) -> bool { /// Finally, if there are duplicate entries, the one with the shorter /// path is kept. fn filter_mount_list(vmi: Vec, paths: &[String], opt: &Options) -> Vec { - let mut mount_info_by_id = HashMap::>::new(); + let mut result = vec![]; for mi in vmi { - if !is_included(&mi, paths, opt) { - continue; - } - - // If the device ID has not been encountered yet, just store it. - let id = mi.dev_id.clone(); - #[allow(clippy::map_entry)] - if !mount_info_by_id.contains_key(&id) { - mount_info_by_id.insert(id, Cell::new(mi)); - continue; - } - - // Otherwise, if we have seen the current device ID before, - // then check if we need to update it or keep the previously - // seen one. - let seen = mount_info_by_id[&id].replace(mi.clone()); - if mount_info_lt(&mi, &seen) { - mount_info_by_id[&id].replace(seen); + // TODO The running time of the `is_best()` function is linear + // in the length of `result`. That makes the running time of + // this loop quadratic in the length of `vmi`. This could be + // improved by a more efficient implementation of `is_best()`, + // but `vmi` is probably not very long in practice. + if is_included(&mi, paths, opt) && is_best(&result, &mi) { + result.push(mi); } } - - // Take ownership of the `MountInfo` instances and collect them - // into a `Vec`. - mount_info_by_id - .into_values() - .map(|m| m.into_inner()) - .collect() + result } #[uucore::main] diff --git a/tests/by-util/test_df.rs b/tests/by-util/test_df.rs index 8fca984a9..00f1ecf3a 100644 --- a/tests/by-util/test_df.rs +++ b/tests/by-util/test_df.rs @@ -37,4 +37,25 @@ fn test_df_output() { } } +/// Test that the order of rows in the table does not change across executions. +#[test] +fn test_order_same() { + // TODO When #3057 is resolved, we should just use + // + // new_ucmd!().arg("--output=source").succeeds().stdout_move_str(); + // + // instead of parsing the entire `df` table as a string. + let output1 = new_ucmd!().succeeds().stdout_move_str(); + let output2 = new_ucmd!().succeeds().stdout_move_str(); + let output1: Vec = output1 + .lines() + .map(|l| String::from(l.split_once(' ').unwrap().0)) + .collect(); + let output2: Vec = output2 + .lines() + .map(|l| String::from(l.split_once(' ').unwrap().0)) + .collect(); + assert_eq!(output1, output2); +} + // ToDO: more tests... From 3ada6af19d261b03893d6f90a99847428ebb1a6a Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sat, 12 Feb 2022 22:38:25 -0500 Subject: [PATCH 129/161] dd: correctly account for partial record written Correct the accounting for partial records written by `dd` to the output file. After this commit, if fewer than `obs` bytes are written, then that is counted as a partial record. For example, $ printf 'abc' | dd bs=2 status=noxfer > /dev/null 1+1 records in 1+1 records out That is, one complete record and one partial record are read from the input, one complete record and one partial record are written to the output. Previously, `dd` reported two complete records and zero partial records written to the output in this case. --- src/uu/dd/src/dd.rs | 15 ++++++--------- tests/by-util/test_dd.rs | 10 ++++++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index eb38e542e..9d9b426b7 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -334,16 +334,13 @@ where let mut bytes_total = 0; for chunk in buf.chunks(self.obs) { - match self.write(chunk)? { - wlen if wlen < chunk.len() => { - writes_partial += 1; - bytes_total += wlen; - } - wlen => { - writes_complete += 1; - bytes_total += wlen; - } + let wlen = self.write(chunk)?; + if wlen < self.obs { + writes_partial += 1; + } else { + writes_complete += 1; } + bytes_total += wlen; } Ok(WriteStat { diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 70153621f..f9e4120b4 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -692,5 +692,15 @@ fn test_seek_do_not_overwrite() { assert_eq!(at.read("outfile"), "a2"); } +#[test] +fn test_partial_records_out() { + new_ucmd!() + .args(&["bs=2", "status=noxfer"]) + .pipe_in("abc") + .succeeds() + .stdout_is("abc") + .stderr_is("1+1 records in\n1+1 records out\n"); +} + // conv=[ascii,ebcdic,ibm], conv=[ucase,lcase], conv=[block,unblock], conv=sync // TODO: Move conv tests from unit test module From 11688408a1bdc526f5130ac6a68575eac3947b9d Mon Sep 17 00:00:00 2001 From: Davide Cavalca Date: Sat, 12 Feb 2022 21:31:39 -0800 Subject: [PATCH 130/161] uucore, uucore_procs: use the correct URLs in the crate manifest --- src/uucore/Cargo.toml | 2 +- src/uucore_procs/Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 5bd5994cc..949ec25f6 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -6,7 +6,7 @@ license = "MIT" description = "uutils ~ 'core' uutils code library (cross-platform)" homepage = "https://github.com/uutils/coreutils" -repository = "https://github.com/uutils/coreutils/tree/master/src/uu/arch" +repository = "https://github.com/uutils/coreutils/tree/master/src/uucore" # readme = "README.md" keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] categories = ["command-line-utilities"] diff --git a/src/uucore_procs/Cargo.toml b/src/uucore_procs/Cargo.toml index 800fc289f..2da22dbac 100644 --- a/src/uucore_procs/Cargo.toml +++ b/src/uucore_procs/Cargo.toml @@ -5,8 +5,8 @@ authors = ["Roy Ivy III "] license = "MIT" description = "uutils ~ 'uucore' proc-macros" -homepage = "https://github.com/uutils/uucore/uucore_procs" -repository = "https://github.com/uutils/uucore/uucore_procs" +homepage = "https://github.com/uutils/coreutils" +repository = "https://github.com/uutils/coreutils/tree/master/src/uucore_procs" # readme = "README.md" keywords = ["cross-platform", "proc-macros", "uucore", "uutils"] # categories = ["os"] From 7225fb6c24f66a8c9b76ae6903bd0dbf86e2a826 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 13 Feb 2022 14:10:48 +0100 Subject: [PATCH 131/161] expr: Use chars().count() as we can have some multibytes chars Partially fixes #3132 Fixes one of the test of tests/misc/expr-multibyte --- src/uu/expr/src/syntax_tree.rs | 6 ++++-- tests/by-util/test_expr.rs | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/uu/expr/src/syntax_tree.rs b/src/uu/expr/src/syntax_tree.rs index c11689a07..4dafffbf0 100644 --- a/src/uu/expr/src/syntax_tree.rs +++ b/src/uu/expr/src/syntax_tree.rs @@ -10,7 +10,7 @@ //! * `` //! -// spell-checker:ignore (ToDO) binop binops ints paren prec +// spell-checker:ignore (ToDO) binop binops ints paren prec multibytes use num_bigint::BigInt; use num_traits::{One, Zero}; @@ -465,7 +465,9 @@ fn operator_match(values: &[String]) -> Result { fn prefix_operator_length(values: &[String]) -> String { assert!(values.len() == 1); - values[0].len().to_string() + // Use chars().count() as we can have some multibytes chars + // See https://github.com/uutils/coreutils/issues/3132 + values[0].chars().count().to_string() } fn prefix_operator_index(values: &[String]) -> String { diff --git a/tests/by-util/test_expr.rs b/tests/by-util/test_expr.rs index 30e3016a3..3a753aa1b 100644 --- a/tests/by-util/test_expr.rs +++ b/tests/by-util/test_expr.rs @@ -1,3 +1,5 @@ +// spell-checker:ignore Ξ±bcdef + use crate::common::util::*; #[test] @@ -95,6 +97,27 @@ fn test_and() { new_ucmd!().args(&["", "&", "1"]).run().stdout_is("0\n"); } +#[test] +fn test_length_fail() { + new_ucmd!().args(&["length", "Ξ±bcdef", "1"]).fails(); +} + +#[test] +fn test_length() { + new_ucmd!() + .args(&["length", "abcdef"]) + .succeeds() + .stdout_only("6\n"); +} + +#[test] +fn test_length_mb() { + new_ucmd!() + .args(&["length", "Ξ±bcdef"]) + .succeeds() + .stdout_only("6\n"); +} + #[test] fn test_substr() { new_ucmd!() From 7fbd80571352f958e9a0ad5721bb17f6e74cc83a Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sun, 30 Jan 2022 21:35:43 -0500 Subject: [PATCH 132/161] split: refactor to add SuffixType enum Refactor the code to use a `SuffixType` enumeration with two members, `Alphabetic` and `NumericDecimal`, representing the two currently supported ways of producing filename suffixes. This prepares the code to more easily support other formats, like numeric hexadecimal. --- src/uu/split/src/filenames.rs | 62 ++++++++++++++++++++++++----------- src/uu/split/src/split.rs | 20 ++++++++--- 2 files changed, 58 insertions(+), 24 deletions(-) diff --git a/src/uu/split/src/filenames.rs b/src/uu/split/src/filenames.rs index 3e2db3606..1b89190c7 100644 --- a/src/uu/split/src/filenames.rs +++ b/src/uu/split/src/filenames.rs @@ -13,12 +13,13 @@ //! //! ```rust,ignore //! use crate::filenames::FilenameIterator; +//! use crate::filenames::SuffixType; //! //! let prefix = "chunk_".to_string(); //! let suffix = ".txt".to_string(); //! let width = 2; -//! let use_numeric_suffix = false; -//! let it = FilenameIterator::new(prefix, suffix, width, use_numeric_suffix); +//! let suffix_type = SuffixType::Alphabetic; +//! let it = FilenameIterator::new(prefix, suffix, width, suffix_type); //! //! assert_eq!(it.next().unwrap(), "chunk_aa.txt"); //! assert_eq!(it.next().unwrap(), "chunk_ab.txt"); @@ -28,6 +29,26 @@ use crate::number::DynamicWidthNumber; use crate::number::FixedWidthNumber; use crate::number::Number; +/// The format to use for suffixes in the filename for each output chunk. +#[derive(Clone, Copy)] +pub enum SuffixType { + /// Lowercase ASCII alphabetic characters. + Alphabetic, + + /// Decimal numbers. + NumericDecimal, +} + +impl SuffixType { + /// The radix to use when representing the suffix string as digits. + fn radix(&self) -> u8 { + match self { + SuffixType::Alphabetic => 26, + SuffixType::NumericDecimal => 10, + } + } +} + /// Compute filenames from a given index. /// /// This iterator yields filenames for use with ``split``. @@ -42,8 +63,8 @@ use crate::number::Number; /// width in characters. In that case, after the iterator yields each /// string of that width, the iterator is exhausted. /// -/// Finally, if `use_numeric_suffix` is `true`, then numbers will be -/// used instead of lowercase ASCII alphabetic characters. +/// Finally, `suffix_type` controls which type of suffix to produce, +/// alphabetic or numeric. /// /// # Examples /// @@ -52,28 +73,30 @@ use crate::number::Number; /// /// ```rust,ignore /// use crate::filenames::FilenameIterator; +/// use crate::filenames::SuffixType; /// /// let prefix = "chunk_".to_string(); /// let suffix = ".txt".to_string(); /// let width = 2; -/// let use_numeric_suffix = false; -/// let it = FilenameIterator::new(prefix, suffix, width, use_numeric_suffix); +/// let suffix_type = SuffixType::Alphabetic; +/// let it = FilenameIterator::new(prefix, suffix, width, suffix_type); /// /// assert_eq!(it.next().unwrap(), "chunk_aa.txt"); /// assert_eq!(it.next().unwrap(), "chunk_ab.txt"); /// assert_eq!(it.next().unwrap(), "chunk_ac.txt"); /// ``` /// -/// For numeric filenames, set `use_numeric_suffix` to `true`: +/// For numeric filenames, use `SuffixType::NumericDecimal`: /// /// ```rust,ignore /// use crate::filenames::FilenameIterator; +/// use crate::filenames::SuffixType; /// /// let prefix = "chunk_".to_string(); /// let suffix = ".txt".to_string(); /// let width = 2; -/// let use_numeric_suffix = true; -/// let it = FilenameIterator::new(prefix, suffix, width, use_numeric_suffix); +/// let suffix_type = SuffixType::NumericDecimal; +/// let it = FilenameIterator::new(prefix, suffix, width, suffix_type); /// /// assert_eq!(it.next().unwrap(), "chunk_00.txt"); /// assert_eq!(it.next().unwrap(), "chunk_01.txt"); @@ -91,9 +114,9 @@ impl<'a> FilenameIterator<'a> { prefix: &'a str, additional_suffix: &'a str, suffix_length: usize, - use_numeric_suffix: bool, + suffix_type: SuffixType, ) -> FilenameIterator<'a> { - let radix = if use_numeric_suffix { 10 } else { 26 }; + let radix = suffix_type.radix(); let number = if suffix_length == 0 { Number::DynamicWidth(DynamicWidthNumber::new(radix)) } else { @@ -130,39 +153,40 @@ impl<'a> Iterator for FilenameIterator<'a> { mod tests { use crate::filenames::FilenameIterator; + use crate::filenames::SuffixType; #[test] fn test_filename_iterator_alphabetic_fixed_width() { - let mut it = FilenameIterator::new("chunk_", ".txt", 2, false); + let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Alphabetic); assert_eq!(it.next().unwrap(), "chunk_aa.txt"); assert_eq!(it.next().unwrap(), "chunk_ab.txt"); assert_eq!(it.next().unwrap(), "chunk_ac.txt"); - let mut it = FilenameIterator::new("chunk_", ".txt", 2, false); + let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Alphabetic); assert_eq!(it.nth(26 * 26 - 1).unwrap(), "chunk_zz.txt"); assert_eq!(it.next(), None); } #[test] fn test_filename_iterator_numeric_fixed_width() { - let mut it = FilenameIterator::new("chunk_", ".txt", 2, true); + let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::NumericDecimal); assert_eq!(it.next().unwrap(), "chunk_00.txt"); assert_eq!(it.next().unwrap(), "chunk_01.txt"); assert_eq!(it.next().unwrap(), "chunk_02.txt"); - let mut it = FilenameIterator::new("chunk_", ".txt", 2, true); + let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::NumericDecimal); assert_eq!(it.nth(10 * 10 - 1).unwrap(), "chunk_99.txt"); assert_eq!(it.next(), None); } #[test] fn test_filename_iterator_alphabetic_dynamic_width() { - let mut it = FilenameIterator::new("chunk_", ".txt", 0, false); + let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Alphabetic); assert_eq!(it.next().unwrap(), "chunk_aa.txt"); assert_eq!(it.next().unwrap(), "chunk_ab.txt"); assert_eq!(it.next().unwrap(), "chunk_ac.txt"); - let mut it = FilenameIterator::new("chunk_", ".txt", 0, false); + let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Alphabetic); assert_eq!(it.nth(26 * 25 - 1).unwrap(), "chunk_yz.txt"); assert_eq!(it.next().unwrap(), "chunk_zaaa.txt"); assert_eq!(it.next().unwrap(), "chunk_zaab.txt"); @@ -170,12 +194,12 @@ mod tests { #[test] fn test_filename_iterator_numeric_dynamic_width() { - let mut it = FilenameIterator::new("chunk_", ".txt", 0, true); + let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::NumericDecimal); assert_eq!(it.next().unwrap(), "chunk_00.txt"); assert_eq!(it.next().unwrap(), "chunk_01.txt"); assert_eq!(it.next().unwrap(), "chunk_02.txt"); - let mut it = FilenameIterator::new("chunk_", ".txt", 0, true); + let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::NumericDecimal); assert_eq!(it.nth(10 * 9 - 1).unwrap(), "chunk_89.txt"); assert_eq!(it.next().unwrap(), "chunk_9000.txt"); assert_eq!(it.next().unwrap(), "chunk_9001.txt"); diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 57953ae27..e9dab1725 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -12,6 +12,7 @@ mod number; mod platform; use crate::filenames::FilenameIterator; +use crate::filenames::SuffixType; use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; use std::env; use std::fmt; @@ -240,13 +241,22 @@ impl Strategy { } } +/// Parse the suffix type from the command-line arguments. +fn suffix_type_from(matches: &ArgMatches) -> SuffixType { + if matches.occurrences_of(OPT_NUMERIC_SUFFIXES) > 0 { + SuffixType::NumericDecimal + } else { + SuffixType::Alphabetic + } +} + /// Parameters that control how a file gets split. /// /// You can convert an [`ArgMatches`] instance into a [`Settings`] /// instance by calling [`Settings::from`]. struct Settings { prefix: String, - numeric_suffix: bool, + suffix_type: SuffixType, suffix_length: usize, additional_suffix: String, input: String, @@ -314,7 +324,7 @@ impl Settings { suffix_length: suffix_length_str .parse() .map_err(|_| SettingsError::SuffixLength(suffix_length_str.to_string()))?, - numeric_suffix: matches.occurrences_of(OPT_NUMERIC_SUFFIXES) > 0, + suffix_type: suffix_type_from(matches), additional_suffix, verbose: matches.occurrences_of("verbose") > 0, strategy: Strategy::from(matches).map_err(SettingsError::Strategy)?, @@ -374,7 +384,7 @@ impl<'a> ByteChunkWriter<'a> { &settings.prefix, &settings.additional_suffix, settings.suffix_length, - settings.numeric_suffix, + settings.suffix_type, ); let filename = filename_iterator.next()?; if settings.verbose { @@ -502,7 +512,7 @@ impl<'a> LineChunkWriter<'a> { &settings.prefix, &settings.additional_suffix, settings.suffix_length, - settings.numeric_suffix, + settings.suffix_type, ); let filename = filename_iterator.next()?; if settings.verbose { @@ -594,7 +604,7 @@ where &settings.prefix, &settings.additional_suffix, settings.suffix_length, - settings.numeric_suffix, + settings.suffix_type, ); // Create one writer for each chunk. This will create each From 494dc7ec573dc3d5754fbb707a0c410ef8befe6a Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sun, 16 Jan 2022 10:36:26 -0500 Subject: [PATCH 133/161] split: add SuffixType::NumericHexadecimal Add a `NumericHexadecimal` member to the `SuffixType` enum so that a future commit can add support for hexadecimal filename suffixes to the `split` program. --- src/uu/split/src/filenames.rs | 6 +- src/uu/split/src/number.rs | 119 +++++++++++++++++++++++++++------- 2 files changed, 101 insertions(+), 24 deletions(-) diff --git a/src/uu/split/src/filenames.rs b/src/uu/split/src/filenames.rs index 1b89190c7..0121ba87f 100644 --- a/src/uu/split/src/filenames.rs +++ b/src/uu/split/src/filenames.rs @@ -37,6 +37,9 @@ pub enum SuffixType { /// Decimal numbers. NumericDecimal, + + /// Hexadecimal numbers. + NumericHexadecimal, } impl SuffixType { @@ -45,6 +48,7 @@ impl SuffixType { match self { SuffixType::Alphabetic => 26, SuffixType::NumericDecimal => 10, + SuffixType::NumericHexadecimal => 16, } } } @@ -86,7 +90,7 @@ impl SuffixType { /// assert_eq!(it.next().unwrap(), "chunk_ac.txt"); /// ``` /// -/// For numeric filenames, use `SuffixType::NumericDecimal`: +/// For decimal numeric filenames, use `SuffixType::NumericDecimal`: /// /// ```rust,ignore /// use crate::filenames::FilenameIterator; diff --git a/src/uu/split/src/number.rs b/src/uu/split/src/number.rs index ef3ccbc4b..d5427e2ca 100644 --- a/src/uu/split/src/number.rs +++ b/src/uu/split/src/number.rs @@ -40,13 +40,19 @@ impl Error for Overflow {} /// specifically for the `split` program. See the /// [`DynamicWidthNumber`] documentation for more information. /// -/// Numbers of radix 10 are displayable and rendered as decimal -/// numbers (for example, "00" or "917"). Numbers of radix 26 are -/// displayable and rendered as lowercase ASCII alphabetic characters -/// (for example, "aa" or "zax"). Numbers of other radices cannot be -/// displayed. The display of a [`DynamicWidthNumber`] includes a -/// prefix whose length depends on the width of the number. See the -/// [`DynamicWidthNumber`] documentation for more information. +/// Numbers of radix +/// +/// * 10 are displayable and rendered as decimal numbers (for example, +/// "00" or "917"), +/// * 16 are displayable and rendered as hexadecimal numbers (for example, +/// "00" or "e7f"), +/// * 26 are displayable and rendered as lowercase ASCII alphabetic +/// characters (for example, "aa" or "zax"). +/// +/// Numbers of other radices cannot be displayed. The display of a +/// [`DynamicWidthNumber`] includes a prefix whose length depends on +/// the width of the number. See the [`DynamicWidthNumber`] +/// documentation for more information. /// /// The digits of a number are accessible via the [`Number::digits`] /// method. The digits are represented as a [`Vec`] with the most @@ -169,12 +175,12 @@ impl Display for Number { /// /// # Displaying /// -/// This number is only displayable if `radix` is 10 or `radix` is -/// 26. If `radix` is 10, then the digits are concatenated and -/// displayed as a fixed-width decimal number. If `radix` is 26, then -/// each digit is translated to the corresponding lowercase ASCII -/// alphabetic character (that is, 'a', 'b', 'c', etc.) and -/// concatenated. +/// This number is only displayable if `radix` is 10, 26, or 26. If +/// `radix` is 10 or 16, then the digits are concatenated and +/// displayed as a fixed-width decimal or hexadecimal number, +/// respectively. If `radix` is 26, then each digit is translated to +/// the corresponding lowercase ASCII alphabetic character (that is, +/// 'a', 'b', 'c', etc.) and concatenated. #[derive(Clone)] pub struct FixedWidthNumber { radix: u8, @@ -228,6 +234,14 @@ impl Display for FixedWidthNumber { let digits: String = self.digits.iter().map(|d| (b'0' + d) as char).collect(); write!(f, "{}", digits) } + 16 => { + let digits: String = self + .digits + .iter() + .map(|d| (if *d < 10 { b'0' + d } else { b'a' + (d - 10) }) as char) + .collect(); + write!(f, "{}", digits) + } 26 => { let digits: String = self.digits.iter().map(|d| (b'a' + d) as char).collect(); write!(f, "{}", digits) @@ -264,14 +278,15 @@ impl Display for FixedWidthNumber { /// /// # Displaying /// -/// This number is only displayable if `radix` is 10 or `radix` is -/// 26. If `radix` is 10, then the digits are concatenated and -/// displayed as a fixed-width decimal number with a prefix of `n - 2` -/// instances of the character '9', where `n` is the number of digits. -/// If `radix` is 26, then each digit is translated to the -/// corresponding lowercase ASCII alphabetic character (that is, 'a', -/// 'b', 'c', etc.) and concatenated with a prefix of `n - 2` -/// instances of the character 'z'. +/// This number is only displayable if `radix` is 10, 16, or 26. If +/// `radix` is 10 or 16, then the digits are concatenated and +/// displayed as a fixed-width decimal or hexadecimal number, +/// respectively, with a prefix of `n - 2` instances of the character +/// '9' of 'f', respectively, where `n` is the number of digits. If +/// `radix` is 26, then each digit is translated to the corresponding +/// lowercase ASCII alphabetic character (that is, 'a', 'b', 'c', +/// etc.) and concatenated with a prefix of `n - 2` instances of the +/// character 'z'. /// /// This notion of displaying the number is specific to the `split` /// program. @@ -349,6 +364,21 @@ impl Display for DynamicWidthNumber { digits = digits, ) } + 16 => { + let num_fill_chars = self.digits.len() - 2; + let digits: String = self + .digits + .iter() + .map(|d| (if *d < 10 { b'0' + d } else { b'a' + (d - 10) }) as char) + .collect(); + write!( + f, + "{empty:f { let num_fill_chars = self.digits.len() - 2; let digits: String = self.digits.iter().map(|d| (b'a' + d) as char).collect(); @@ -424,7 +454,7 @@ mod tests { } #[test] - fn test_dynamic_width_number_display_numeric() { + fn test_dynamic_width_number_display_numeric_decimal() { fn num(n: usize) -> Number { let mut number = Number::DynamicWidth(DynamicWidthNumber::new(10)); for _ in 0..n { @@ -444,6 +474,30 @@ mod tests { assert_eq!(format!("{}", num(10 * 99 + 1)), "990001"); } + #[test] + fn test_dynamic_width_number_display_numeric_hexadecimal() { + fn num(n: usize) -> Number { + let mut number = Number::DynamicWidth(DynamicWidthNumber::new(16)); + for _ in 0..n { + number.increment().unwrap() + } + number + } + + assert_eq!(format!("{}", num(0)), "00"); + assert_eq!(format!("{}", num(15)), "0f"); + assert_eq!(format!("{}", num(16)), "10"); + assert_eq!(format!("{}", num(17)), "11"); + assert_eq!(format!("{}", num(18)), "12"); + + assert_eq!(format!("{}", num(16 * 15 - 1)), "ef"); + assert_eq!(format!("{}", num(16 * 15)), "f000"); + assert_eq!(format!("{}", num(16 * 15 + 1)), "f001"); + assert_eq!(format!("{}", num(16 * 255 - 1)), "feff"); + assert_eq!(format!("{}", num(16 * 255)), "ff0000"); + assert_eq!(format!("{}", num(16 * 255 + 1)), "ff0001"); + } + #[test] fn test_fixed_width_number_increment() { let mut n = Number::FixedWidth(FixedWidthNumber::new(3, 2)); @@ -493,7 +547,7 @@ mod tests { } #[test] - fn test_fixed_width_number_display_numeric() { + fn test_fixed_width_number_display_numeric_decimal() { fn num(n: usize) -> Result { let mut number = Number::FixedWidth(FixedWidthNumber::new(10, 2)); for _ in 0..n { @@ -510,4 +564,23 @@ mod tests { assert_eq!(format!("{}", num(10 * 10 - 1).unwrap()), "99"); assert!(num(10 * 10).is_err()); } + + #[test] + fn test_fixed_width_number_display_numeric_hexadecimal() { + fn num(n: usize) -> Result { + let mut number = Number::FixedWidth(FixedWidthNumber::new(16, 2)); + for _ in 0..n { + number.increment()?; + } + Ok(number) + } + + assert_eq!(format!("{}", num(0).unwrap()), "00"); + assert_eq!(format!("{}", num(15).unwrap()), "0f"); + assert_eq!(format!("{}", num(17).unwrap()), "11"); + assert_eq!(format!("{}", num(16 * 15 - 1).unwrap()), "ef"); + assert_eq!(format!("{}", num(16 * 15).unwrap()), "f0"); + assert_eq!(format!("{}", num(16 * 16 - 1).unwrap()), "ff"); + assert!(num(16 * 16).is_err()); + } } From a4955b4e06c04445d74490517c119107ae2d30b7 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sun, 16 Jan 2022 10:41:52 -0500 Subject: [PATCH 134/161] split: add support for -x option (hex suffixes) Add support for the `-x` command-line option to `split`. This option causes `split` to produce filenames with hexadecimal suffixes instead of the default alphabetic suffixes. --- src/uu/split/src/split.rs | 11 +++++++++ tests/by-util/test_split.rs | 24 ++++++++++++++++++- .../split/twohundredfortyonebytes.txt | 1 + 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/split/twohundredfortyonebytes.txt diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index e9dab1725..2c344f4d3 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -32,6 +32,7 @@ static OPT_ADDITIONAL_SUFFIX: &str = "additional-suffix"; static OPT_FILTER: &str = "filter"; static OPT_NUMBER: &str = "number"; static OPT_NUMERIC_SUFFIXES: &str = "numeric-suffixes"; +static OPT_HEX_SUFFIXES: &str = "hex-suffixes"; static OPT_SUFFIX_LENGTH: &str = "suffix-length"; static OPT_DEFAULT_SUFFIX_LENGTH: &str = "0"; static OPT_VERBOSE: &str = "verbose"; @@ -140,6 +141,14 @@ pub fn uu_app<'a>() -> App<'a> { .default_value(OPT_DEFAULT_SUFFIX_LENGTH) .help("use suffixes of length N (default 2)"), ) + .arg( + Arg::new(OPT_HEX_SUFFIXES) + .short('x') + .long(OPT_HEX_SUFFIXES) + .takes_value(true) + .default_missing_value("0") + .help("use hex suffixes starting at 0, not alphabetic"), + ) .arg( Arg::new(OPT_VERBOSE) .long(OPT_VERBOSE) @@ -245,6 +254,8 @@ impl Strategy { fn suffix_type_from(matches: &ArgMatches) -> SuffixType { if matches.occurrences_of(OPT_NUMERIC_SUFFIXES) > 0 { SuffixType::NumericDecimal + } else if matches.occurrences_of(OPT_HEX_SUFFIXES) > 0 { + SuffixType::NumericHexadecimal } else { SuffixType::Alphabetic } diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 0291d1f4a..e30e29acc 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -2,7 +2,7 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// spell-checker:ignore xzaaa sixhundredfiftyonebytes ninetyonebytes asciilowercase fghij klmno pqrst uvwxyz fivelines +// spell-checker:ignore xzaaa sixhundredfiftyonebytes ninetyonebytes asciilowercase fghij klmno pqrst uvwxyz fivelines twohundredfortyonebytes extern crate rand; extern crate regex; @@ -409,6 +409,28 @@ fn test_numeric_dynamic_suffix_length() { assert_eq!(file_read(&at, "x9000"), "a"); } +#[test] +fn test_hex_dynamic_suffix_length() { + let (at, mut ucmd) = at_and_ucmd!(); + // Split into chunks of one byte each, use hexadecimal digits + // instead of letters as file suffixes. + // + // The input file has (16^2) - 16 + 1 = 241 bytes. This is just + // enough to force `split` to dynamically increase the length of + // the filename for the very last chunk. + // + // x00, x01, x02, ..., xed, xee, xef, xf000 + // + ucmd.args(&["-x", "-b", "1", "twohundredfortyonebytes.txt"]) + .succeeds(); + for i in 0..240 { + let filename = format!("x{:02x}", i); + let contents = file_read(&at, &filename); + assert_eq!(contents, "a"); + } + assert_eq!(file_read(&at, "xf000"), "a"); +} + #[test] fn test_suffixes_exhausted() { new_ucmd!() diff --git a/tests/fixtures/split/twohundredfortyonebytes.txt b/tests/fixtures/split/twohundredfortyonebytes.txt new file mode 100644 index 000000000..10a53a61c --- /dev/null +++ b/tests/fixtures/split/twohundredfortyonebytes.txt @@ -0,0 +1 @@ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file From 6c1a655512b38f8d2d07f8c32ac7fe6310b1016e Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Mon, 14 Feb 2022 02:09:11 +0530 Subject: [PATCH 135/161] This commit removes empty line from USAGE string in src/uu/od/src/od.rs. (#3074) This change is needed to fix missing USAGE section for `od` in user docs. With reference to this issue https://github.com/uutils/coreutils/issues/2991, and missing USAGE section from `od docs` at https://uutils.github.io/coreutils-docs/user/utils/od.html, it was found that the USAGE for od app was starts with an empty line and uudoc only takes 1st line for using in USAGE section in docs. This resulted in empty line in usage section for `od` --- src/uu/od/src/od.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 16abb20fc..3db1a00f3 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -51,8 +51,7 @@ use uucore::InvalidEncodingHandling; const PEEK_BUFFER_SIZE: usize = 4; // utf-8 can be 4 bytes static ABOUT: &str = "dump files in octal and other formats"; -static USAGE: &str = r#" - od [OPTION]... [--] [FILENAME]... +static USAGE: &str = r#"od [OPTION]... [--] [FILENAME]... od [-abcdDefFhHiIlLoOsxX] [FILENAME] [[+][0x]OFFSET[.][b]] od --traditional [OPTION]... [FILENAME] [[+][0x]OFFSET[.][b] [[+][0x]LABEL[.][b]]]"#; From 4f7f4445cb3d714dcafe8134f6ed9ac207341cfb Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sun, 13 Feb 2022 21:45:43 +0100 Subject: [PATCH 136/161] docs: allow for multiline usage --- src/bin/uudoc.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 412a2dd48..0d4187f4d 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -67,7 +67,14 @@ fn write_version(w: &mut impl Write, app: &App) -> io::Result<()> { fn write_usage(w: &mut impl Write, app: &mut App, name: &str) -> io::Result<()> { writeln!(w, "\n```")?; - let mut usage: String = app.render_usage().lines().nth(1).unwrap().trim().into(); + let mut usage: String = app + .render_usage() + .lines() + .skip(1) + .map(|l| l.trim()) + .filter(|l| !l.is_empty()) + .collect::>() + .join("\n"); usage = usage.replace(app.get_name(), name); writeln!(w, "{}", usage)?; writeln!(w, "```") From 477b40f1e53b95ae0bfcc51c882c1e6a73cbae7a Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sun, 13 Feb 2022 21:58:48 +0100 Subject: [PATCH 137/161] shuf: correct execution phrase for --help --- src/uu/shuf/src/shuf.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 3dcd7b0e2..eb3268f0b 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -14,7 +14,7 @@ use std::fs::File; use std::io::{stdin, stdout, BufReader, BufWriter, Read, Write}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; -use uucore::InvalidEncodingHandling; +use uucore::{execution_phrase, InvalidEncodingHandling}; mod rand_read_adapter; @@ -26,14 +26,9 @@ enum Mode { static NAME: &str = "shuf"; static USAGE: &str = r#"shuf [OPTION]... [FILE] - or: shuf -e [OPTION]... [ARG]... - or: shuf -i LO-HI [OPTION]... -Write a random permutation of the input lines to standard output. - -With no FILE, or when FILE is -, read standard input. -"#; + or: shuf -e [OPTION]... [ARG]... + or: shuf -i LO-HI [OPTION]..."#; static ABOUT: &str = "Shuffle the input by outputting a random permutation of input lines. Each output permutation is equally likely."; -static TEMPLATE: &str = "Usage: {usage}\nMandatory arguments to long options are mandatory for short options too.\n{options}"; struct Options { head_count: usize, @@ -60,7 +55,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let matches = uu_app().get_matches_from(args); + let matches = uu_app() + .override_usage(&USAGE.replace(NAME, execution_phrase())[..]) + .get_matches_from(args); let mode = if let Some(args) = matches.values_of(options::ECHO) { Mode::Echo(args.map(String::from).collect()) @@ -125,7 +122,6 @@ pub fn uu_app<'a>() -> App<'a> { .name(NAME) .about(ABOUT) .version(crate_version!()) - .help_template(TEMPLATE) .override_usage(USAGE) .setting(AppSettings::InferLongArgs) .arg( From ac11d8793ef44ff7650b4a884478ea867633080c Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sun, 13 Feb 2022 16:51:24 +0100 Subject: [PATCH 138/161] docs: add page with test coverage --- .github/workflows/GnuTests.yml | 13 +++++- docs/src/test_coverage.css | 46 ++++++++++++++++++++ docs/src/test_coverage.js | 77 ++++++++++++++++++++++++++++++++++ docs/src/test_coverage.md | 19 +++++++++ src/bin/uudoc.rs | 1 + util/gnu-json-result.py | 27 ++++++++++++ 6 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 docs/src/test_coverage.css create mode 100644 docs/src/test_coverage.js create mode 100644 docs/src/test_coverage.md create mode 100644 util/gnu-json-result.py diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index e57204213..eef8567c7 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -32,7 +32,8 @@ jobs: TEST_FILESET_PREFIX='test-fileset-IDs.sha1#' TEST_FILESET_SUFFIX='.txt' TEST_SUMMARY_FILE='gnu-result.json' - outputs SUITE_LOG_FILE TEST_FILESET_PREFIX TEST_FILESET_SUFFIX TEST_LOGS_GLOB TEST_SUMMARY_FILE + TEST_FULL_SUMMARY_FILE='gnu-full-result.json' + outputs SUITE_LOG_FILE TEST_FILESET_PREFIX TEST_FILESET_SUFFIX TEST_LOGS_GLOB TEST_SUMMARY_FILE TEST_FULL_SUMMARY_FILE - name: Checkout code (uutil) uses: actions/checkout@v2 with: @@ -92,6 +93,11 @@ jobs: path_GNU='${{ steps.vars.outputs.path_GNU }}' path_UUTILS='${{ steps.vars.outputs.path_UUTILS }}' bash "${path_UUTILS}/util/run-gnu-test.sh" + - name: Extract testing info into JSON + shell: bash + run : | + path_UUTILS='${{ steps.vars.outputs.path_UUTILS }}' + python ${path_UUTILS}/util/gnu-json-result.py ${{ steps.vars.outputs.path_GNU_tests }} > ${{ steps.vars.outputs.TEST_FULL_SUMMARY_FILE }} - name: Extract/summarize testing info id: summary shell: bash @@ -146,6 +152,11 @@ jobs: with: name: test-logs path: "${{ steps.vars.outputs.TEST_LOGS_GLOB }}" + - name: Upload full json results + uses: actions/upload-artifact@v2 + with: + name: gnu-full-result.json + path: ${{ steps.vars.outputs.TEST_FULL_SUMMARY_FILE }} - name: Compare test failures VS reference shell: bash run: | diff --git a/docs/src/test_coverage.css b/docs/src/test_coverage.css new file mode 100644 index 000000000..37a658695 --- /dev/null +++ b/docs/src/test_coverage.css @@ -0,0 +1,46 @@ +:root { + --PASS: #44AF69; + --ERROR: #F8333C; + --FAIL: #F8333C; + --SKIP: #d3c994; +} +.PASS { + color: var(--PASS); +} +.ERROR { + color: var(--ERROR); +} +.FAIL { + color: var(--FAIL); +} +.SKIP { + color: var(--SKIP); +} +.testSummary { + display: inline-flex; + align-items: center; + justify-content: space-between; + width: 90%; +} +.progress { + width: 80%; + display: flex; + justify-content: right; + align-items: center; +} +.progress-bar { + height: 10px; + width: calc(100% - 15ch); + border-radius: 5px; +} +.result { + font-weight: bold; + width: 7ch; + display: inline-block; +} +.result-line { + margin: 8px; +} +.counts { + margin-right: 10px; +} \ No newline at end of file diff --git a/docs/src/test_coverage.js b/docs/src/test_coverage.js new file mode 100644 index 000000000..814eef6da --- /dev/null +++ b/docs/src/test_coverage.js @@ -0,0 +1,77 @@ +// spell-checker:ignore hljs +function progressBar(totals) { + const bar = document.createElement("div"); + bar.className = "progress-bar"; + let totalTests = 0; + for (const [key, value] of Object.entries(totals)) { + totalTests += value; + } + const passPercentage = Math.round(100 * totals["PASS"] / totalTests); + const skipPercentage = passPercentage + Math.round(100 * totals["PASS"] / totalTests); + bar.style = `background: linear-gradient( + to right, + var(--PASS) ${passPercentage}%, + var(--SKIP) ${passPercentage}%, + var(--SKIP) ${skipPercentage}%, + var(--FAIL) 0)`; + + const progress = document.createElement("div"); + progress.className = "progress" + progress.innerHTML = ` + + ${totals["PASS"]} + / + ${totals["SKIP"]} + / + ${totals["FAIL"] + totals["ERROR"]} + + `; + progress.appendChild(bar); + return progress +} + +function parse_result(parent, obj) { + const totals = { + PASS: 0, + SKIP: 0, + FAIL: 0, + ERROR: 0, + }; + for (const [category, content] of Object.entries(obj)) { + if (typeof content === "string") { + const p = document.createElement("p"); + p.className = "result-line"; + totals[content]++; + p.innerHTML = `${content} ${category}`; + parent.appendChild(p); + } else { + const categoryName = document.createElement("code"); + categoryName.innerHTML = category; + categoryName.className = "hljs"; + + const details = document.createElement("details"); + const subtotals = parse_result(details, content); + for (const [subtotal, count] of Object.entries(subtotals)) { + totals[subtotal] += count; + } + const summaryDiv = document.createElement("div"); + summaryDiv.className = "testSummary"; + summaryDiv.appendChild(categoryName); + summaryDiv.appendChild(progressBar(subtotals)); + + const summary = document.createElement("summary"); + summary.appendChild(summaryDiv); + + details.appendChild(summary); + parent.appendChild(details); + } + } + return totals; +} + +fetch("https://github.com/uutils/coreutils-tracking/blob/main/gnu-full-result.json") + .then((r) => r.json()) + .then((obj) => { + let parent = document.getElementById("test-cov"); + parse_result(parent, obj); + }); diff --git a/docs/src/test_coverage.md b/docs/src/test_coverage.md new file mode 100644 index 000000000..bf4c72129 --- /dev/null +++ b/docs/src/test_coverage.md @@ -0,0 +1,19 @@ +# GNU Test Coverage + +uutils is actively tested against the GNU coreutils test suite. The results +below are automatically updated every day. + +## Coverage per category + +Click on the categories to see the names of the tests. Green indicates a passing +test, yellow indicates a skipped test and red means that the test either failed +or resulted in an error. + + + + +
+ +## Progress over time + + diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 412a2dd48..71bbb2684 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -26,6 +26,7 @@ fn main() -> io::Result<()> { [Introduction](index.md)\n\ * [Installation](installation.md)\n\ * [Contributing](contributing.md)\n\ + * [GNU test coverage](test_coverage.md)\n\ \n\ # Reference\n\ * [Multi-call binary](multicall.md)\n", diff --git a/util/gnu-json-result.py b/util/gnu-json-result.py new file mode 100644 index 000000000..a51aa7d94 --- /dev/null +++ b/util/gnu-json-result.py @@ -0,0 +1,27 @@ +""" +Extract the GNU logs into a JSON file. +""" + +import json +from pathlib import Path +import sys +from os import environ + +out = {} + +test_dir = Path(sys.argv[1]) +for filepath in test_dir.glob("**/*.log"): + path = Path(filepath) + current = out + for key in path.parent.relative_to(test_dir).parts: + if key not in current: + current[key] = {} + current = current[key] + try: + with open(path) as f: + content = f.read() + current[path.name] = content.split("\n")[-2].split(" ")[0] + except: + pass + +print(json.dumps(out, indent=2, sort_keys=True)) \ No newline at end of file From 6d6371741af92c6fba48711a9b41b17dc898f370 Mon Sep 17 00:00:00 2001 From: DevSabb <97408111+DevSabb@users.noreply.github.com> Date: Mon, 14 Feb 2022 13:47:18 -0500 Subject: [PATCH 139/161] include io-blksize parameter (#3064) * include io-blksize parameter * format changes for including io-blksize Co-authored-by: DevSabb Co-authored-by: Sylvestre Ledru --- src/uu/split/src/split.rs | 10 ++++++++++ tests/by-util/test_split.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 57953ae27..29559f8b8 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -34,6 +34,9 @@ static OPT_NUMERIC_SUFFIXES: &str = "numeric-suffixes"; static OPT_SUFFIX_LENGTH: &str = "suffix-length"; static OPT_DEFAULT_SUFFIX_LENGTH: &str = "0"; static OPT_VERBOSE: &str = "verbose"; +//The ---io-blksize parameter is consumed and ignored. +//The parameter is included to make GNU coreutils tests pass. +static OPT_IO_BLKSIZE: &str = "-io-blksize"; static ARG_INPUT: &str = "input"; static ARG_PREFIX: &str = "prefix"; @@ -144,6 +147,13 @@ pub fn uu_app<'a>() -> App<'a> { .long(OPT_VERBOSE) .help("print a diagnostic just before each output file is opened"), ) + .arg( + Arg::new(OPT_IO_BLKSIZE) + .long(OPT_IO_BLKSIZE) + .alias(OPT_IO_BLKSIZE) + .takes_value(true) + .hide(true), + ) .arg( Arg::new(ARG_INPUT) .takes_value(true) diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 0291d1f4a..35f96d2a3 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -449,6 +449,35 @@ fn test_number() { assert_eq!(file_read("xae"), "uvwxyz\n"); } +#[test] +fn test_split_number_with_io_blksize() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_read = |f| { + let mut s = String::new(); + at.open(f).read_to_string(&mut s).unwrap(); + s + }; + ucmd.args(&["-n", "5", "asciilowercase.txt", "---io-blksize", "1024"]) + .succeeds(); + assert_eq!(file_read("xaa"), "abcde"); + assert_eq!(file_read("xab"), "fghij"); + assert_eq!(file_read("xac"), "klmno"); + assert_eq!(file_read("xad"), "pqrst"); + assert_eq!(file_read("xae"), "uvwxyz"); +} + +#[test] +fn test_split_default_with_io_blksize() { + let (at, mut ucmd) = at_and_ucmd!(); + let name = "split_default_with_io_blksize"; + RandomFile::new(&at, name).add_lines(2000); + ucmd.args(&[name, "---io-blksize", "2M"]).succeeds(); + + let glob = Glob::new(&at, ".", r"x[[:alpha:]][[:alpha:]]$"); + assert_eq!(glob.count(), 2); + assert_eq!(glob.collate(), at.read_bytes(name)); +} + #[test] fn test_invalid_suffix_length() { new_ucmd!() From 63fa3c81eda7d7dd064498228d01d9e684b03505 Mon Sep 17 00:00:00 2001 From: DevSabb Date: Mon, 14 Feb 2022 20:41:58 -0500 Subject: [PATCH 140/161] fix failure in test_split --- tests/by-util/test_split.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 35f96d2a3..b5d799d72 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -463,7 +463,7 @@ fn test_split_number_with_io_blksize() { assert_eq!(file_read("xab"), "fghij"); assert_eq!(file_read("xac"), "klmno"); assert_eq!(file_read("xad"), "pqrst"); - assert_eq!(file_read("xae"), "uvwxyz"); + assert_eq!(file_read("xae"), "uvwxyz\n"); } #[test] From 11e428d471fd52e9099ee4a0d362923b63cbd196 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 15 Feb 2022 19:27:33 +0100 Subject: [PATCH 141/161] docs: fix url for full test report --- docs/src/test_coverage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/test_coverage.js b/docs/src/test_coverage.js index 814eef6da..b696ed6a9 100644 --- a/docs/src/test_coverage.js +++ b/docs/src/test_coverage.js @@ -69,7 +69,7 @@ function parse_result(parent, obj) { return totals; } -fetch("https://github.com/uutils/coreutils-tracking/blob/main/gnu-full-result.json") +fetch("https://raw.githubusercontent.com/uutils/coreutils-tracking/main/gnu-full-result.json") .then((r) => r.json()) .then((obj) => { let parent = document.getElementById("test-cov"); From 824c6041abeae13976a4c67cbba6310bbc388ba8 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 15 Feb 2022 19:36:15 +0100 Subject: [PATCH 142/161] docs: fix progress bar sizes for SKIP --- docs/src/test_coverage.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/src/test_coverage.js b/docs/src/test_coverage.js index b696ed6a9..e601229af 100644 --- a/docs/src/test_coverage.js +++ b/docs/src/test_coverage.js @@ -7,13 +7,18 @@ function progressBar(totals) { totalTests += value; } const passPercentage = Math.round(100 * totals["PASS"] / totalTests); - const skipPercentage = passPercentage + Math.round(100 * totals["PASS"] / totalTests); + const skipPercentage = passPercentage + Math.round(100 * totals["SKIP"] / totalTests); + + // The ternary expressions are used for some edge-cases where there are no failing test, + // but still a red (or beige) line shows up because of how CSS draws gradients. bar.style = `background: linear-gradient( to right, - var(--PASS) ${passPercentage}%, - var(--SKIP) ${passPercentage}%, - var(--SKIP) ${skipPercentage}%, - var(--FAIL) 0)`; + var(--PASS) ${passPercentage}%` + + ( passPercentage === 100 ? ", var(--PASS)" : + `, var(--SKIP) ${passPercentage}%, + var(--SKIP) ${skipPercentage}%` + ) + + (skipPercentage === 100 ? ")" : ", var(--FAIL) 0)"); const progress = document.createElement("div"); progress.className = "progress" From 34b18351e2e0bcaf27b8567499ce4728393ab507 Mon Sep 17 00:00:00 2001 From: Michael Lohmann Date: Tue, 1 Feb 2022 21:57:52 +0100 Subject: [PATCH 143/161] cat: cleanup write_tab_to_end duplication of logic The logic for '\n' and '\r' about the number of written characters was duplicated --- src/uu/cat/src/cat.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index e7fd31497..3c89d8434 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -560,13 +560,12 @@ fn write_tab_to_end(mut in_buf: &[u8], writer: &mut W) -> usize { { Some(p) => { writer.write_all(&in_buf[..p]).unwrap(); - if in_buf[p] == b'\n' { - return count + p; - } else if in_buf[p] == b'\t' { + if in_buf[p] == b'\t' { writer.write_all(b"^I").unwrap(); in_buf = &in_buf[p + 1..]; count += p + 1; } else { + // b'\n' or b'\r' return count + p; } } From 46769245327e81375cd22a31cb91b51b09e21a4a Mon Sep 17 00:00:00 2001 From: Michael Lohmann Date: Tue, 1 Feb 2022 22:02:20 +0100 Subject: [PATCH 144/161] cat: cat_path does not need to parse InputType for stdin itself This type is already handled by get_input_type, so we can unify the handling --- src/uu/cat/src/cat.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 3c89d8434..e0e6ed7f0 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -325,15 +325,15 @@ fn cat_path( state: &mut OutputState, out_info: Option<&FileInformation>, ) -> CatResult<()> { - if path == "-" { - let stdin = io::stdin(); - let mut handle = InputHandle { - reader: stdin, - is_interactive: atty::is(atty::Stream::Stdin), - }; - return cat_handle(&mut handle, options, state); - } match get_input_type(path)? { + InputType::StdIn => { + let stdin = io::stdin(); + let mut handle = InputHandle { + reader: stdin, + is_interactive: atty::is(atty::Stream::Stdin), + }; + cat_handle(&mut handle, options, state) + } InputType::Directory => Err(CatError::IsDirectory), #[cfg(unix)] InputType::Socket => { From 3bbfe00791b7874bb9e94bcf37e5c068f836a451 Mon Sep 17 00:00:00 2001 From: Michael Lohmann Date: Tue, 1 Feb 2022 22:16:05 +0100 Subject: [PATCH 145/161] cat: write_nonprint_to_end: be more explicit about printing '?' --- src/uu/cat/src/cat.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index e0e6ed7f0..498e5e8ad 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -588,10 +588,10 @@ fn write_nonprint_to_end(in_buf: &[u8], writer: &mut W, tab: &[u8]) -> 9 => writer.write_all(tab), 0..=8 | 10..=31 => writer.write_all(&[b'^', byte + 64]), 32..=126 => writer.write_all(&[byte]), - 127 => writer.write_all(&[b'^', byte - 64]), + 127 => writer.write_all(&[b'^', b'?']), 128..=159 => writer.write_all(&[b'M', b'-', b'^', byte - 64]), 160..=254 => writer.write_all(&[b'M', b'-', byte - 128]), - _ => writer.write_all(&[b'M', b'-', b'^', 63]), + _ => writer.write_all(&[b'M', b'-', b'^', b'?']), } .unwrap(); count += 1; From e598bf0835639a1c2fe34af00dad646cf4657f28 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Mon, 14 Feb 2022 21:16:37 -0500 Subject: [PATCH 146/161] tests: add CmdResult::stdout_is_fixture_bytes() Add helper method `CmdResult::stdout_is_fixture_bytes()`, which is like `stdout_is_fixture()` but compares stdout to the raw bytes of a given file instead of decoding the contents of the file to a UTF-8 string. --- tests/common/util.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/common/util.rs b/tests/common/util.rs index 4db4e2561..422d36328 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -265,6 +265,28 @@ impl CmdResult { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.stdout_is(String::from_utf8(contents).unwrap()) } + + /// Assert that the bytes of stdout exactly match those of the given file. + /// + /// Contrast this with [`CmdResult::stdout_is_fixture`], which + /// decodes the contents of the file as a UTF-8 [`String`] before + /// comparison with stdout. + /// + /// # Examples + /// + /// Use this method in a unit test like this: + /// + /// ```rust,ignore + /// #[test] + /// fn test_something() { + /// new_ucmd!().succeeds().stdout_is_fixture_bytes("expected.bin"); + /// } + /// ``` + pub fn stdout_is_fixture_bytes>(&self, file_rel_path: T) -> &Self { + let contents = read_scenario_fixture(&self.tmpd, file_rel_path); + self.stdout_is_bytes(contents) + } + /// like stdout_is_fixture(...), but replaces the data in fixture file based on values provided in template_vars /// command output pub fn stdout_is_templated_fixture>( From ba1ce7179bf6cd2e24a522f8b9d14271561483f4 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Mon, 14 Feb 2022 21:20:12 -0500 Subject: [PATCH 147/161] dd: move unit tests into dd.rs and test_dd.rs Clean up unit tests in the `dd` crate to make them easier to manage. This commit does a few things. * move test cases that test the complete functionality of the `dd` program from the `dd_unit_tests` module up to the `tests/by-util/test_dd.rs` module so that they can take advantage of the testing framework and common testing tools provided by uutils, * move test cases that test internal functions of the `dd` implementation into the `tests` module within `dd.rs` so that they live closer to the code they are testing, * replace test cases defined by macros with test cases defined by plain old functions to make the test cases easier to read at a glance. --- src/uu/dd/src/dd.rs | 454 +++++++++++++++++- .../src/dd_unit_tests/block_unblock_tests.rs | 351 -------------- .../dd/src/dd_unit_tests/conv_sync_tests.rs | 106 ---- .../dd/src/dd_unit_tests/conversion_tests.rs | 233 --------- src/uu/dd/src/dd_unit_tests/mod.rs | 89 ---- src/uu/dd/src/dd_unit_tests/sanity_tests.rs | 316 ------------ ...ndom-5828891cb1230748e146f34223bbd3b5.test | Bin 0 -> 74400 bytes .../gnudd-conv-atoe-seq-byte-values.spec | Bin 256 -> 0 bytes .../gnudd-conv-atoibm-seq-byte-values.spec | Bin 256 -> 0 bytes ...nudd-conv-ebcdic-ltou-seq-byte-values.spec | Bin 256 -> 0 bytes ...nudd-conv-ebcdic-utol-seq-byte-values.spec | Bin 256 -> 0 bytes .../gnudd-conv-etoa-seq-byte-values.spec | Bin 256 -> 0 bytes .../gnudd-conv-ibm-ltou-seq-byte-values.spec | Bin 256 -> 0 bytes .../gnudd-conv-ibm-utol-seq-byte-values.spec | Bin 256 -> 0 bytes .../gnudd-conv-ltou-seq-byte-values.spec | Bin 256 -> 0 bytes .../gnudd-conv-utol-seq-byte-values.spec | Bin 256 -> 0 bytes src/uu/dd/test-resources/seq-byte-values.test | Bin 256 -> 0 bytes tests/by-util/test_dd.rs | 363 +++++++++++++- ...hars-37eff01866ba3f538421b30b7cbefcac.test | Bin .../fixtures/dd}/dd-block-cbs16-win.test | 0 .../fixtures/dd}/dd-block-cbs16.spec | 0 .../fixtures/dd}/dd-block-cbs16.test | 0 .../fixtures/dd}/dd-block-cbs8.spec | 0 .../dd}/dd-block-consecutive-nl-cbs16.spec | 0 .../dd}/dd-block-consecutive-nl-win.test | 0 .../fixtures/dd}/dd-block-consecutive-nl.test | 0 .../fixtures/dd}/dd-unblock-cbs16-win.spec | 0 .../fixtures/dd}/dd-unblock-cbs16.spec | 0 .../fixtures/dd}/dd-unblock-cbs16.test | 0 .../fixtures/dd}/dd-unblock-cbs8-win.spec | 0 .../fixtures/dd}/dd-unblock-cbs8.spec | 0 tests/fixtures/dd/deadbeef-16.spec | Bin 0 -> 128 bytes tests/fixtures/dd/deadbeef-16.test | 1 + ...beef-18d99661a1de1fc9af21b0ec2cd67ba3.test | 0 ...d-conv-sync-ibs-1031-obs-521-deadbeef.spec | 0 ...udd-conv-sync-ibs-1031-obs-521-random.spec | Bin ...nudd-conv-sync-ibs-1031-obs-521-zeros.spec | Bin ...d-conv-sync-ibs-521-obs-1031-deadbeef.spec | 0 ...udd-conv-sync-ibs-521-obs-1031-random.spec | Bin ...nudd-conv-sync-ibs-521-obs-1031-zeros.spec | Bin .../dd}/gnudd-deadbeef-first-12345.spec | 0 .../dd}/gnudd-deadbeef-first-16k.spec | 0 .../fixtures/dd}/gnudd-random-first-32k.spec | Bin .../fixtures/dd}/lcase-ascii.test | Bin .../fixtures/dd}/lcase-ebcdic.spec | Bin .../fixtures/dd}/lcase-ebcdic.test | Bin .../fixtures/dd}/lcase-ibm.spec | Bin .../fixtures/dd}/lcase-ibm.test | Bin ...ones-6ae59e64850377ee5470c854761551ea.test | 0 ...ndom-5828891cb1230748e146f34223bbd3b5.test | Bin 0 -> 74400 bytes ...lues-b632a992d3aed5d8d1a59cc5a5a455ba.test | Bin .../fixtures/dd}/seq-byte-values-odd.spec | Bin .../fixtures/dd}/seq-byte-values-odd.test | Bin .../fixtures/dd}/seq-byte-values-swapped.test | Bin .../fixtures/dd}/ucase-ascii.test | Bin .../fixtures/dd}/ucase-ebcdic.spec | Bin .../fixtures/dd}/ucase-ebcdic.test | Bin .../fixtures/dd}/ucase-ibm.spec | Bin .../fixtures/dd}/ucase-ibm.test | Bin ...eros-620f0b67a91f7f74151bc5be745b7110.test | Bin 60 files changed, 813 insertions(+), 1100 deletions(-) delete mode 100644 src/uu/dd/src/dd_unit_tests/block_unblock_tests.rs delete mode 100644 src/uu/dd/src/dd_unit_tests/conv_sync_tests.rs delete mode 100644 src/uu/dd/src/dd_unit_tests/conversion_tests.rs delete mode 100644 src/uu/dd/src/dd_unit_tests/mod.rs delete mode 100644 src/uu/dd/src/dd_unit_tests/sanity_tests.rs create mode 100644 src/uu/dd/test-resources/FAILED-random-5828891cb1230748e146f34223bbd3b5.test delete mode 100644 src/uu/dd/test-resources/gnudd-conv-atoe-seq-byte-values.spec delete mode 100644 src/uu/dd/test-resources/gnudd-conv-atoibm-seq-byte-values.spec delete mode 100644 src/uu/dd/test-resources/gnudd-conv-ebcdic-ltou-seq-byte-values.spec delete mode 100644 src/uu/dd/test-resources/gnudd-conv-ebcdic-utol-seq-byte-values.spec delete mode 100644 src/uu/dd/test-resources/gnudd-conv-etoa-seq-byte-values.spec delete mode 100644 src/uu/dd/test-resources/gnudd-conv-ibm-ltou-seq-byte-values.spec delete mode 100644 src/uu/dd/test-resources/gnudd-conv-ibm-utol-seq-byte-values.spec delete mode 100644 src/uu/dd/test-resources/gnudd-conv-ltou-seq-byte-values.spec delete mode 100644 src/uu/dd/test-resources/gnudd-conv-utol-seq-byte-values.spec delete mode 100644 src/uu/dd/test-resources/seq-byte-values.test rename {src/uu/dd/test-resources => tests/fixtures/dd}/all-valid-ascii-chars-37eff01866ba3f538421b30b7cbefcac.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/dd-block-cbs16-win.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/dd-block-cbs16.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/dd-block-cbs16.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/dd-block-cbs8.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/dd-block-consecutive-nl-cbs16.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/dd-block-consecutive-nl-win.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/dd-block-consecutive-nl.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/dd-unblock-cbs16-win.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/dd-unblock-cbs16.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/dd-unblock-cbs16.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/dd-unblock-cbs8-win.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/dd-unblock-cbs8.spec (100%) create mode 100644 tests/fixtures/dd/deadbeef-16.spec create mode 100644 tests/fixtures/dd/deadbeef-16.test rename {src/uu/dd/test-resources => tests/fixtures/dd}/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/gnudd-conv-sync-ibs-1031-obs-521-deadbeef.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/gnudd-conv-sync-ibs-1031-obs-521-random.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/gnudd-conv-sync-ibs-1031-obs-521-zeros.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/gnudd-conv-sync-ibs-521-obs-1031-deadbeef.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/gnudd-conv-sync-ibs-521-obs-1031-random.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/gnudd-conv-sync-ibs-521-obs-1031-zeros.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/gnudd-deadbeef-first-12345.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/gnudd-deadbeef-first-16k.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/gnudd-random-first-32k.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/lcase-ascii.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/lcase-ebcdic.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/lcase-ebcdic.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/lcase-ibm.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/lcase-ibm.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/ones-6ae59e64850377ee5470c854761551ea.test (100%) create mode 100644 tests/fixtures/dd/random-5828891cb1230748e146f34223bbd3b5.test rename {src/uu/dd/test-resources => tests/fixtures/dd}/seq-byte-values-b632a992d3aed5d8d1a59cc5a5a455ba.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/seq-byte-values-odd.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/seq-byte-values-odd.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/seq-byte-values-swapped.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/ucase-ascii.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/ucase-ebcdic.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/ucase-ebcdic.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/ucase-ibm.spec (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/ucase-ibm.test (100%) rename {src/uu/dd/test-resources => tests/fixtures/dd}/zeros-620f0b67a91f7f74151bc5be745b7110.test (100%) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 9d9b426b7..f8f3b7081 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -7,9 +7,6 @@ // spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat seekable -#[cfg(test)] -mod dd_unit_tests; - mod datastructures; use datastructures::*; @@ -1171,3 +1168,454 @@ General-Flags ") ) } + +#[cfg(test)] +mod tests { + + use crate::datastructures::{IConvFlags, IFlags, OConvFlags}; + use crate::ReadStat; + use crate::{block, calc_bsize, unblock, uu_app, Input, Output, OutputTrait}; + + use std::cmp; + use std::fs; + use std::fs::File; + use std::io; + use std::io::{BufReader, Read}; + + struct LazyReader { + src: R, + } + + impl Read for LazyReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let reduced = cmp::max(buf.len() / 2, 1); + self.src.read(&mut buf[..reduced]) + } + } + + const NEWLINE: u8 = b'\n'; + const SPACE: u8 = b' '; + + #[test] + fn block_test_no_nl() { + let mut rs = ReadStat::default(); + let buf = [0u8, 1u8, 2u8, 3u8]; + let res = block(&buf, 4, &mut rs); + + assert_eq!(res, vec![vec![0u8, 1u8, 2u8, 3u8],]); + } + + #[test] + fn block_test_no_nl_short_record() { + let mut rs = ReadStat::default(); + let buf = [0u8, 1u8, 2u8, 3u8]; + let res = block(&buf, 8, &mut rs); + + assert_eq!( + res, + vec![vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE],] + ); + } + + #[test] + fn block_test_no_nl_trunc() { + let mut rs = ReadStat::default(); + let buf = [0u8, 1u8, 2u8, 3u8, 4u8]; + let res = block(&buf, 4, &mut rs); + + // Commented section(s) should be truncated and appear for reference only. + assert_eq!(res, vec![vec![0u8, 1u8, 2u8, 3u8 /*, 4u8*/],]); + assert_eq!(rs.records_truncated, 1); + } + + #[test] + fn block_test_nl_gt_cbs_trunc() { + let mut rs = ReadStat::default(); + let buf = [ + 0u8, 1u8, 2u8, 3u8, 4u8, NEWLINE, 0u8, 1u8, 2u8, 3u8, 4u8, NEWLINE, 5u8, 6u8, 7u8, 8u8, + ]; + let res = block(&buf, 4, &mut rs); + + assert_eq!( + res, + vec![ + // Commented section(s) should be truncated and appear for reference only. + vec![0u8, 1u8, 2u8, 3u8], + // vec![4u8, SPACE, SPACE, SPACE], + vec![0u8, 1u8, 2u8, 3u8], + // vec![4u8, SPACE, SPACE, SPACE], + vec![5u8, 6u8, 7u8, 8u8], + ] + ); + assert_eq!(rs.records_truncated, 2); + } + + #[test] + fn block_test_surrounded_nl() { + let mut rs = ReadStat::default(); + let buf = [0u8, 1u8, 2u8, 3u8, NEWLINE, 4u8, 5u8, 6u8, 7u8, 8u8]; + let res = block(&buf, 8, &mut rs); + + assert_eq!( + res, + vec![ + vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE], + vec![4u8, 5u8, 6u8, 7u8, 8u8, SPACE, SPACE, SPACE], + ] + ); + } + + #[test] + fn block_test_multiple_nl_same_cbs_block() { + let mut rs = ReadStat::default(); + let buf = [ + 0u8, 1u8, 2u8, 3u8, NEWLINE, 4u8, NEWLINE, 5u8, 6u8, 7u8, 8u8, 9u8, + ]; + let res = block(&buf, 8, &mut rs); + + assert_eq!( + res, + vec![ + vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE], + vec![4u8, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE], + vec![5u8, 6u8, 7u8, 8u8, 9u8, SPACE, SPACE, SPACE], + ] + ); + } + + #[test] + fn block_test_multiple_nl_diff_cbs_block() { + let mut rs = ReadStat::default(); + let buf = [ + 0u8, 1u8, 2u8, 3u8, NEWLINE, 4u8, 5u8, 6u8, 7u8, NEWLINE, 8u8, 9u8, + ]; + let res = block(&buf, 8, &mut rs); + + assert_eq!( + res, + vec![ + vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE], + vec![4u8, 5u8, 6u8, 7u8, SPACE, SPACE, SPACE, SPACE], + vec![8u8, 9u8, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE], + ] + ); + } + + #[test] + fn block_test_end_nl_diff_cbs_block() { + let mut rs = ReadStat::default(); + let buf = [0u8, 1u8, 2u8, 3u8, NEWLINE]; + let res = block(&buf, 4, &mut rs); + + assert_eq!(res, vec![vec![0u8, 1u8, 2u8, 3u8],]); + } + + #[test] + fn block_test_end_nl_same_cbs_block() { + let mut rs = ReadStat::default(); + let buf = [0u8, 1u8, 2u8, NEWLINE]; + let res = block(&buf, 4, &mut rs); + + assert_eq!(res, vec![vec![0u8, 1u8, 2u8, SPACE]]); + } + + #[test] + fn block_test_double_end_nl() { + let mut rs = ReadStat::default(); + let buf = [0u8, 1u8, 2u8, NEWLINE, NEWLINE]; + let res = block(&buf, 4, &mut rs); + + assert_eq!( + res, + vec![vec![0u8, 1u8, 2u8, SPACE], vec![SPACE, SPACE, SPACE, SPACE],] + ); + } + + #[test] + fn block_test_start_nl() { + let mut rs = ReadStat::default(); + let buf = [NEWLINE, 0u8, 1u8, 2u8, 3u8]; + let res = block(&buf, 4, &mut rs); + + assert_eq!( + res, + vec![vec![SPACE, SPACE, SPACE, SPACE], vec![0u8, 1u8, 2u8, 3u8],] + ); + } + + #[test] + fn block_test_double_surrounded_nl_no_trunc() { + let mut rs = ReadStat::default(); + let buf = [0u8, 1u8, 2u8, 3u8, NEWLINE, NEWLINE, 4u8, 5u8, 6u8, 7u8]; + let res = block(&buf, 8, &mut rs); + + assert_eq!( + res, + vec![ + vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE], + vec![SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE], + vec![4u8, 5u8, 6u8, 7u8, SPACE, SPACE, SPACE, SPACE], + ] + ); + } + + #[test] + fn block_test_double_surrounded_nl_double_trunc() { + let mut rs = ReadStat::default(); + let buf = [ + 0u8, 1u8, 2u8, 3u8, NEWLINE, NEWLINE, 4u8, 5u8, 6u8, 7u8, 8u8, + ]; + let res = block(&buf, 4, &mut rs); + + assert_eq!( + res, + vec![ + // Commented section(s) should be truncated and appear for reference only. + vec![0u8, 1u8, 2u8, 3u8], + vec![SPACE, SPACE, SPACE, SPACE], + vec![4u8, 5u8, 6u8, 7u8 /*, 8u8*/], + ] + ); + assert_eq!(rs.records_truncated, 1); + } + + #[test] + fn unblock_test_full_cbs() { + let buf = [0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8]; + let res = unblock(&buf, 8); + + assert_eq!(res, vec![0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, NEWLINE],); + } + + #[test] + fn unblock_test_all_space() { + let buf = [SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE]; + let res = unblock(&buf, 8); + + assert_eq!(res, vec![NEWLINE],); + } + + #[test] + fn unblock_test_decoy_spaces() { + let buf = [0u8, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, 7u8]; + let res = unblock(&buf, 8); + + assert_eq!( + res, + vec![0u8, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, 7u8, NEWLINE], + ); + } + + #[test] + fn unblock_test_strip_single_cbs() { + let buf = [0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE]; + let res = unblock(&buf, 8); + + assert_eq!(res, vec![0u8, 1u8, 2u8, 3u8, NEWLINE],); + } + + #[test] + fn unblock_test_strip_multi_cbs() { + let buf = vec![ + vec![0u8, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE], + vec![0u8, 1u8, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE], + vec![0u8, 1u8, 2u8, SPACE, SPACE, SPACE, SPACE, SPACE], + vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE], + ] + .into_iter() + .flatten() + .collect::>(); + + let res = unblock(&buf, 8); + + let exp = vec![ + vec![0u8, NEWLINE], + vec![0u8, 1u8, NEWLINE], + vec![0u8, 1u8, 2u8, NEWLINE], + vec![0u8, 1u8, 2u8, 3u8, NEWLINE], + ] + .into_iter() + .flatten() + .collect::>(); + + assert_eq!(res, exp); + } + + #[test] + fn bsize_test_primes() { + let (n, m) = (7901, 7919); + let res = calc_bsize(n, m); + assert!(res % n == 0); + assert!(res % m == 0); + + assert_eq!(res, n * m); + } + + #[test] + fn bsize_test_rel_prime_obs_greater() { + let (n, m) = (7 * 5119, 13 * 5119); + let res = calc_bsize(n, m); + assert!(res % n == 0); + assert!(res % m == 0); + + assert_eq!(res, 7 * 13 * 5119); + } + + #[test] + fn bsize_test_rel_prime_ibs_greater() { + let (n, m) = (13 * 5119, 7 * 5119); + let res = calc_bsize(n, m); + assert!(res % n == 0); + assert!(res % m == 0); + + assert_eq!(res, 7 * 13 * 5119); + } + + #[test] + fn bsize_test_3fac_rel_prime() { + let (n, m) = (11 * 13 * 5119, 7 * 11 * 5119); + let res = calc_bsize(n, m); + assert!(res % n == 0); + assert!(res % m == 0); + + assert_eq!(res, 7 * 11 * 13 * 5119); + } + + #[test] + fn bsize_test_ibs_greater() { + let (n, m) = (512 * 1024, 256 * 1024); + let res = calc_bsize(n, m); + assert!(res % n == 0); + assert!(res % m == 0); + + assert_eq!(res, n); + } + + #[test] + fn bsize_test_obs_greater() { + let (n, m) = (256 * 1024, 512 * 1024); + let res = calc_bsize(n, m); + assert!(res % n == 0); + assert!(res % m == 0); + + assert_eq!(res, m); + } + + #[test] + fn bsize_test_bs_eq() { + let (n, m) = (1024, 1024); + let res = calc_bsize(n, m); + assert!(res % n == 0); + assert!(res % m == 0); + + assert_eq!(res, m); + } + + #[test] + #[should_panic] + fn test_nocreat_causes_failure_when_ofile_doesnt_exist() { + let args = vec![ + String::from("dd"), + String::from("--conv=nocreat"), + String::from("--of=not-a-real.file"), + ]; + + let matches = uu_app().try_get_matches_from(args).unwrap(); + let _ = Output::::new(&matches).unwrap(); + } + + #[test] + fn test_deadbeef_16_delayed() { + let input = Input { + src: LazyReader { + src: File::open("./test-resources/deadbeef-16.test").unwrap(), + }, + non_ascii: false, + ibs: 16, + print_level: None, + count: None, + cflags: IConvFlags { + sync: Some(0), + ..IConvFlags::default() + }, + iflags: IFlags::default(), + }; + + let output = Output { + dst: File::create("./test-resources/FAILED-deadbeef-16-delayed.test").unwrap(), + obs: 32, + cflags: OConvFlags::default(), + }; + + output.dd_out(input).unwrap(); + + let tmp_fname = "./test-resources/FAILED-deadbeef-16-delayed.test"; + let spec = File::open("./test-resources/deadbeef-16.spec").unwrap(); + + let res = File::open(tmp_fname).unwrap(); + // Check test file isn't empty (unless spec file is too) + assert_eq!( + res.metadata().unwrap().len(), + spec.metadata().unwrap().len() + ); + + let spec = BufReader::new(spec); + let res = BufReader::new(res); + + // Check all bytes match + for (b_res, b_spec) in res.bytes().zip(spec.bytes()) { + assert_eq!(b_res.unwrap(), b_spec.unwrap()); + } + + fs::remove_file(tmp_fname).unwrap(); + } + + #[test] + fn test_random_73k_test_lazy_fullblock() { + let input = Input { + src: LazyReader { + src: File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test") + .unwrap(), + }, + non_ascii: false, + ibs: 521, + print_level: None, + count: None, + cflags: IConvFlags::default(), + iflags: IFlags { + fullblock: true, + ..IFlags::default() + }, + }; + + let output = Output { + dst: File::create("./test-resources/FAILED-random_73k_test_lazy_fullblock.test") + .unwrap(), + obs: 1031, + cflags: OConvFlags::default(), + }; + + output.dd_out(input).unwrap(); + + let tmp_fname = "./test-resources/FAILED-random_73k_test_lazy_fullblock.test"; + let spec = + File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test").unwrap(); + + let res = File::open(tmp_fname).unwrap(); + // Check test file isn't empty (unless spec file is too) + assert_eq!( + res.metadata().unwrap().len(), + spec.metadata().unwrap().len() + ); + + let spec = BufReader::new(spec); + let res = BufReader::new(res); + + // Check all bytes match + for (b_res, b_spec) in res.bytes().zip(spec.bytes()) { + assert_eq!(b_res.unwrap(), b_spec.unwrap()); + } + + fs::remove_file(tmp_fname).unwrap(); + } +} diff --git a/src/uu/dd/src/dd_unit_tests/block_unblock_tests.rs b/src/uu/dd/src/dd_unit_tests/block_unblock_tests.rs deleted file mode 100644 index 919ddbb9c..000000000 --- a/src/uu/dd/src/dd_unit_tests/block_unblock_tests.rs +++ /dev/null @@ -1,351 +0,0 @@ -// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat - -use super::*; - -#[cfg(unix)] -macro_rules! make_block_test ( - ( $test_id:ident, $test_name:expr, $src:expr, $block:expr, $spec:expr ) => - { - make_spec_test!($test_id, - $test_name, - Input { - src: $src, - non_ascii: false, - ibs: 512, - print_level: None, - count: None, - cflags: IConvFlags { - block: $block, - ..IConvFlags::default() - }, - iflags: IFlags::default(), - }, - Output { - dst: File::create(format!("./test-resources/FAILED-{}.test", $test_name)).unwrap(), - obs: 512, - cflags: OConvFlags::default(), - }, - $spec, - format!("./test-resources/FAILED-{}.test", $test_name) - ); - }; -); - -#[cfg(unix)] -macro_rules! make_unblock_test ( - ( $test_id:ident, $test_name:expr, $src:expr, $unblock:expr, $spec:expr ) => - { - make_spec_test!($test_id, - $test_name, - Input { - src: $src, - non_ascii: false, - ibs: 512, - print_level: None, - count: None, - cflags: IConvFlags { - unblock: $unblock, - ..IConvFlags::default() - }, - iflags: IFlags::default(), - }, - Output { - dst: File::create(format!("./test-resources/FAILED-{}.test", $test_name)).unwrap(), - obs: 512, - cflags: OConvFlags::default(), - }, - $spec, - format!("./test-resources/FAILED-{}.test", $test_name) - ); - }; -); - -#[test] -fn block_test_no_nl() { - let mut rs = ReadStat::default(); - let buf = [0u8, 1u8, 2u8, 3u8]; - let res = block(&buf, 4, &mut rs); - - assert_eq!(res, vec![vec![0u8, 1u8, 2u8, 3u8],]); -} - -#[test] -fn block_test_no_nl_short_record() { - let mut rs = ReadStat::default(); - let buf = [0u8, 1u8, 2u8, 3u8]; - let res = block(&buf, 8, &mut rs); - - assert_eq!( - res, - vec![vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE],] - ); -} - -#[test] -fn block_test_no_nl_trunc() { - let mut rs = ReadStat::default(); - let buf = [0u8, 1u8, 2u8, 3u8, 4u8]; - let res = block(&buf, 4, &mut rs); - - // Commented section(s) should be truncated and appear for reference only. - assert_eq!(res, vec![vec![0u8, 1u8, 2u8, 3u8 /*, 4u8*/],]); - assert_eq!(rs.records_truncated, 1); -} - -#[test] -fn block_test_nl_gt_cbs_trunc() { - let mut rs = ReadStat::default(); - let buf = [ - 0u8, 1u8, 2u8, 3u8, 4u8, NEWLINE, 0u8, 1u8, 2u8, 3u8, 4u8, NEWLINE, 5u8, 6u8, 7u8, 8u8, - ]; - let res = block(&buf, 4, &mut rs); - - assert_eq!( - res, - vec![ - // Commented section(s) should be truncated and appear for reference only. - vec![0u8, 1u8, 2u8, 3u8], - // vec![4u8, SPACE, SPACE, SPACE], - vec![0u8, 1u8, 2u8, 3u8], - // vec![4u8, SPACE, SPACE, SPACE], - vec![5u8, 6u8, 7u8, 8u8], - ] - ); - assert_eq!(rs.records_truncated, 2); -} - -#[test] -fn block_test_surrounded_nl() { - let mut rs = ReadStat::default(); - let buf = [0u8, 1u8, 2u8, 3u8, NEWLINE, 4u8, 5u8, 6u8, 7u8, 8u8]; - let res = block(&buf, 8, &mut rs); - - assert_eq!( - res, - vec![ - vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE], - vec![4u8, 5u8, 6u8, 7u8, 8u8, SPACE, SPACE, SPACE], - ] - ); -} - -#[test] -fn block_test_multiple_nl_same_cbs_block() { - let mut rs = ReadStat::default(); - let buf = [ - 0u8, 1u8, 2u8, 3u8, NEWLINE, 4u8, NEWLINE, 5u8, 6u8, 7u8, 8u8, 9u8, - ]; - let res = block(&buf, 8, &mut rs); - - assert_eq!( - res, - vec![ - vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE], - vec![4u8, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE], - vec![5u8, 6u8, 7u8, 8u8, 9u8, SPACE, SPACE, SPACE], - ] - ); -} - -#[test] -fn block_test_multiple_nl_diff_cbs_block() { - let mut rs = ReadStat::default(); - let buf = [ - 0u8, 1u8, 2u8, 3u8, NEWLINE, 4u8, 5u8, 6u8, 7u8, NEWLINE, 8u8, 9u8, - ]; - let res = block(&buf, 8, &mut rs); - - assert_eq!( - res, - vec![ - vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE], - vec![4u8, 5u8, 6u8, 7u8, SPACE, SPACE, SPACE, SPACE], - vec![8u8, 9u8, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE], - ] - ); -} - -#[test] -fn block_test_end_nl_diff_cbs_block() { - let mut rs = ReadStat::default(); - let buf = [0u8, 1u8, 2u8, 3u8, NEWLINE]; - let res = block(&buf, 4, &mut rs); - - assert_eq!(res, vec![vec![0u8, 1u8, 2u8, 3u8],]); -} - -#[test] -fn block_test_end_nl_same_cbs_block() { - let mut rs = ReadStat::default(); - let buf = [0u8, 1u8, 2u8, NEWLINE]; - let res = block(&buf, 4, &mut rs); - - assert_eq!(res, vec![vec![0u8, 1u8, 2u8, SPACE]]); -} - -#[test] -fn block_test_double_end_nl() { - let mut rs = ReadStat::default(); - let buf = [0u8, 1u8, 2u8, NEWLINE, NEWLINE]; - let res = block(&buf, 4, &mut rs); - - assert_eq!( - res, - vec![vec![0u8, 1u8, 2u8, SPACE], vec![SPACE, SPACE, SPACE, SPACE],] - ); -} - -#[test] -fn block_test_start_nl() { - let mut rs = ReadStat::default(); - let buf = [NEWLINE, 0u8, 1u8, 2u8, 3u8]; - let res = block(&buf, 4, &mut rs); - - assert_eq!( - res, - vec![vec![SPACE, SPACE, SPACE, SPACE], vec![0u8, 1u8, 2u8, 3u8],] - ); -} - -#[test] -fn block_test_double_surrounded_nl_no_trunc() { - let mut rs = ReadStat::default(); - let buf = [0u8, 1u8, 2u8, 3u8, NEWLINE, NEWLINE, 4u8, 5u8, 6u8, 7u8]; - let res = block(&buf, 8, &mut rs); - - assert_eq!( - res, - vec![ - vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE], - vec![SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE], - vec![4u8, 5u8, 6u8, 7u8, SPACE, SPACE, SPACE, SPACE], - ] - ); -} - -#[test] -fn block_test_double_surrounded_nl_double_trunc() { - let mut rs = ReadStat::default(); - let buf = [ - 0u8, 1u8, 2u8, 3u8, NEWLINE, NEWLINE, 4u8, 5u8, 6u8, 7u8, 8u8, - ]; - let res = block(&buf, 4, &mut rs); - - assert_eq!( - res, - vec![ - // Commented section(s) should be truncated and appear for reference only. - vec![0u8, 1u8, 2u8, 3u8], - vec![SPACE, SPACE, SPACE, SPACE], - vec![4u8, 5u8, 6u8, 7u8 /*, 8u8*/], - ] - ); - assert_eq!(rs.records_truncated, 1); -} - -#[cfg(unix)] -make_block_test!( - block_cbs16, - "block-cbs-16", - File::open("./test-resources/dd-block-cbs16.test").unwrap(), - Some(16), - File::open("./test-resources/dd-block-cbs16.spec").unwrap() -); - -#[cfg(unix)] -make_block_test!( - block_cbs16_as_cbs8, - "block-cbs-16-as-cbs8", - File::open("./test-resources/dd-block-cbs16.test").unwrap(), - Some(8), - File::open("./test-resources/dd-block-cbs8.spec").unwrap() -); - -#[cfg(unix)] -make_block_test!( - block_consecutive_nl, - "block-consecutive-nl", - File::open("./test-resources/dd-block-consecutive-nl.test").unwrap(), - Some(16), - File::open("./test-resources/dd-block-consecutive-nl-cbs16.spec").unwrap() -); - -#[test] -fn unblock_test_full_cbs() { - let buf = [0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8]; - let res = unblock(&buf, 8); - - assert_eq!(res, vec![0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, NEWLINE],); -} - -#[test] -fn unblock_test_all_space() { - let buf = [SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE]; - let res = unblock(&buf, 8); - - assert_eq!(res, vec![NEWLINE],); -} - -#[test] -fn unblock_test_decoy_spaces() { - let buf = [0u8, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, 7u8]; - let res = unblock(&buf, 8); - - assert_eq!( - res, - vec![0u8, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, 7u8, NEWLINE], - ); -} - -#[test] -fn unblock_test_strip_single_cbs() { - let buf = [0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE]; - let res = unblock(&buf, 8); - - assert_eq!(res, vec![0u8, 1u8, 2u8, 3u8, NEWLINE],); -} - -#[test] -fn unblock_test_strip_multi_cbs() { - let buf = vec![ - vec![0u8, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE], - vec![0u8, 1u8, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE], - vec![0u8, 1u8, 2u8, SPACE, SPACE, SPACE, SPACE, SPACE], - vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE], - ] - .into_iter() - .flatten() - .collect::>(); - - let res = unblock(&buf, 8); - - let exp = vec![ - vec![0u8, NEWLINE], - vec![0u8, 1u8, NEWLINE], - vec![0u8, 1u8, 2u8, NEWLINE], - vec![0u8, 1u8, 2u8, 3u8, NEWLINE], - ] - .into_iter() - .flatten() - .collect::>(); - - assert_eq!(res, exp); -} - -#[cfg(unix)] -make_unblock_test!( - unblock_multi_16, - "unblock-multi-16", - File::open("./test-resources/dd-unblock-cbs16.test").unwrap(), - Some(16), - File::open("./test-resources/dd-unblock-cbs16.spec").unwrap() -); - -#[cfg(unix)] -make_unblock_test!( - unblock_multi_16_as_8, - "unblock-multi-16-as-8", - File::open("./test-resources/dd-unblock-cbs16.test").unwrap(), - Some(8), - File::open("./test-resources/dd-unblock-cbs8.spec").unwrap() -); diff --git a/src/uu/dd/src/dd_unit_tests/conv_sync_tests.rs b/src/uu/dd/src/dd_unit_tests/conv_sync_tests.rs deleted file mode 100644 index 904a97cf5..000000000 --- a/src/uu/dd/src/dd_unit_tests/conv_sync_tests.rs +++ /dev/null @@ -1,106 +0,0 @@ -// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat - -use super::*; - -macro_rules! make_sync_test ( - ( $test_id:ident, $test_name:expr, $src:expr, $sync:expr, $ibs:expr, $obs:expr, $spec:expr ) => - { - make_spec_test!($test_id, - $test_name, - Input { - src: $src, - non_ascii: false, - ibs: $ibs, - print_level: None, - count: None, - cflags: IConvFlags { - sync: $sync, - ..IConvFlags::default() - }, - iflags: IFlags::default(), - }, - Output { - dst: File::create(format!("./test-resources/FAILED-{}.test", $test_name)).unwrap(), - obs: $obs, - cflags: OConvFlags::default(), - }, - $spec, - format!("./test-resources/FAILED-{}.test", $test_name) - ); - }; -); - -// Zeros -make_sync_test!( - zeros_4k_conv_sync_obs_gt_ibs, - "zeros_4k_conv_sync_obs_gt_ibs", - File::open("./test-resources/zeros-620f0b67a91f7f74151bc5be745b7110.test").unwrap(), - Some(0u8), - 521, - 1031, - File::open("./test-resources/gnudd-conv-sync-ibs-521-obs-1031-zeros.spec").unwrap() -); - -make_sync_test!( - zeros_4k_conv_sync_ibs_gt_obs, - "zeros_4k_conv_sync_ibs_gt_obs", - File::open("./test-resources/zeros-620f0b67a91f7f74151bc5be745b7110.test").unwrap(), - Some(0u8), - 1031, - 521, - File::open("./test-resources/gnudd-conv-sync-ibs-1031-obs-521-zeros.spec").unwrap() -); - -// Deadbeef -make_sync_test!( - deadbeef_32k_conv_sync_obs_gt_ibs, - "deadbeef_32k_conv_sync_obs_gt_ibs", - File::open("./test-resources/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test").unwrap(), - Some(0u8), - 521, - 1031, - File::open("./test-resources/gnudd-conv-sync-ibs-521-obs-1031-deadbeef.spec").unwrap() -); - -make_sync_test!( - deadbeef_32k_conv_sync_ibs_gt_obs, - "deadbeef_32k_conv_sync_ibs_gt_obs", - File::open("./test-resources/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test").unwrap(), - Some(0u8), - 1031, - 521, - File::open("./test-resources/gnudd-conv-sync-ibs-1031-obs-521-deadbeef.spec").unwrap() -); - -// Random -make_sync_test!( - random_73k_test_bs_prime_obs_gt_ibs_sync, - "random-73k-test-bs-prime-obs-gt-ibs-sync", - File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test").unwrap(), - Some(0u8), - 521, - 1031, - File::open("./test-resources/gnudd-conv-sync-ibs-521-obs-1031-random.spec").unwrap() -); - -make_sync_test!( - random_73k_test_bs_prime_ibs_gt_obs_sync, - "random-73k-test-bs-prime-ibs-gt-obs-sync", - File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test").unwrap(), - Some(0u8), - 1031, - 521, - File::open("./test-resources/gnudd-conv-sync-ibs-1031-obs-521-random.spec").unwrap() -); - -make_sync_test!( - deadbeef_16_delayed, - "deadbeef-16-delayed", - LazyReader { - src: File::open("./test-resources/deadbeef-16.test").unwrap() - }, - Some(0u8), - 16, - 32, - File::open("./test-resources/deadbeef-16.spec").unwrap() -); diff --git a/src/uu/dd/src/dd_unit_tests/conversion_tests.rs b/src/uu/dd/src/dd_unit_tests/conversion_tests.rs deleted file mode 100644 index 9255a1a89..000000000 --- a/src/uu/dd/src/dd_unit_tests/conversion_tests.rs +++ /dev/null @@ -1,233 +0,0 @@ -// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat - -use super::*; - -macro_rules! make_conv_test ( - ( $test_id:ident, $test_name:expr, $src:expr, $ctable:expr, $spec:expr ) => - { - make_spec_test!($test_id, - $test_name, - Input { - src: $src, - non_ascii: false, - ibs: 512, - print_level: None, - count: None, - cflags: icf!($ctable), - iflags: IFlags::default(), - }, - Output { - dst: File::create(format!("./test-resources/FAILED-{}.test", $test_name)).unwrap(), - obs: 512, - cflags: OConvFlags::default(), - }, - $spec, - format!("./test-resources/FAILED-{}.test", $test_name) - ); - }; -); - -macro_rules! make_icf_test ( - ( $test_id:ident, $test_name:expr, $src:expr, $icf:expr, $spec:expr ) => - { - make_spec_test!($test_id, - $test_name, - Input { - src: $src, - non_ascii: false, - ibs: 512, - print_level: None, - count: None, - cflags: $icf, - iflags: IFlags::default(), - }, - Output { - dst: File::create(format!("./test-resources/FAILED-{}.test", $test_name)).unwrap(), - obs: 512, - cflags: OConvFlags::default(), - }, - $spec, - format!("./test-resources/FAILED-{}.test", $test_name) - ); - }; -); - -make_conv_test!( - atoe_conv_spec_test, - "atoe-conv-spec-test", - File::open("./test-resources/seq-byte-values-b632a992d3aed5d8d1a59cc5a5a455ba.test").unwrap(), - Some(&ASCII_TO_EBCDIC), - File::open("./test-resources/gnudd-conv-atoe-seq-byte-values.spec").unwrap() -); - -make_conv_test!( - etoa_conv_spec_test, - "etoa-conv-spec-test", - File::open("./test-resources/seq-byte-values-b632a992d3aed5d8d1a59cc5a5a455ba.test").unwrap(), - Some(&EBCDIC_TO_ASCII), - File::open("./test-resources/gnudd-conv-etoa-seq-byte-values.spec").unwrap() -); - -make_conv_test!( - atoibm_conv_spec_test, - "atoibm-conv-spec-test", - File::open("./test-resources/seq-byte-values-b632a992d3aed5d8d1a59cc5a5a455ba.test").unwrap(), - Some(&ASCII_TO_IBM), - File::open("./test-resources/gnudd-conv-atoibm-seq-byte-values.spec").unwrap() -); - -make_conv_test!( - lcase_ascii_to_ucase_ascii, - "lcase_ascii_to_ucase_ascii", - File::open("./test-resources/lcase-ascii.test").unwrap(), - Some(&ASCII_LCASE_TO_UCASE), - File::open("./test-resources/ucase-ascii.test").unwrap() -); - -make_conv_test!( - ucase_ascii_to_lcase_ascii, - "ucase_ascii_to_lcase_ascii", - File::open("./test-resources/ucase-ascii.test").unwrap(), - Some(&ASCII_UCASE_TO_LCASE), - File::open("./test-resources/lcase-ascii.test").unwrap() -); - -make_conv_test!( - // conv=ebcdic,ucase - atoe_and_ucase_conv_spec_test, - "atoe-and-ucase-conv-spec-test", - File::open("./test-resources/seq-byte-values-b632a992d3aed5d8d1a59cc5a5a455ba.test").unwrap(), - Some(&ASCII_TO_EBCDIC_LCASE_TO_UCASE), - File::open("./test-resources/ucase-ebcdic.test").unwrap() -); - -make_conv_test!( - // conv=ebcdic,lcase - atoe_and_lcase_conv_spec_test, - "atoe-and-lcase-conv-spec-test", - File::open("./test-resources/seq-byte-values-b632a992d3aed5d8d1a59cc5a5a455ba.test").unwrap(), - Some(&ASCII_TO_EBCDIC_UCASE_TO_LCASE), - File::open("./test-resources/lcase-ebcdic.test").unwrap() -); - -make_conv_test!( - // conv=ibm,ucase - atoibm_and_ucase_conv_spec_test, - "atoibm-and-ucase-conv-spec-test", - File::open("./test-resources/seq-byte-values-b632a992d3aed5d8d1a59cc5a5a455ba.test").unwrap(), - Some(&ASCII_TO_IBM_UCASE_TO_LCASE), - File::open("./test-resources/lcase-ibm.test").unwrap() -); - -make_conv_test!( - // conv=ibm,lcase - atoibm_and_lcase_conv_spec_test, - "atoibm-and-lcase-conv-spec-test", - File::open("./test-resources/seq-byte-values-b632a992d3aed5d8d1a59cc5a5a455ba.test").unwrap(), - Some(&ASCII_TO_IBM_LCASE_TO_UCASE), - File::open("./test-resources/ucase-ibm.test").unwrap() -); - -#[test] -fn all_valid_ascii_ebcdic_ascii_roundtrip_conv_test() { - // ASCII->EBCDIC - let test_name = "all-valid-ascii-to-ebcdic"; - let tmp_fname_ae = format!("./test-resources/FAILED-{}.test", test_name); - - let i = Input { - src: File::open( - "./test-resources/all-valid-ascii-chars-37eff01866ba3f538421b30b7cbefcac.test", - ) - .unwrap(), - non_ascii: false, - ibs: 128, - print_level: None, - count: None, - cflags: icf!(Some(&ASCII_TO_EBCDIC)), - iflags: IFlags::default(), - }; - - let o = Output { - dst: File::create(&tmp_fname_ae).unwrap(), - obs: 1024, - cflags: OConvFlags::default(), - }; - - o.dd_out(i).unwrap(); - - // EBCDIC->ASCII - let test_name = "all-valid-ebcdic-to-ascii"; - let tmp_fname_ea = format!("./test-resources/FAILED-{}.test", test_name); - - let i = Input { - src: File::open(&tmp_fname_ae).unwrap(), - non_ascii: false, - ibs: 256, - print_level: None, - count: None, - cflags: icf!(Some(&EBCDIC_TO_ASCII)), - iflags: IFlags::default(), - }; - - let o = Output { - dst: File::create(&tmp_fname_ea).unwrap(), - obs: 1024, - cflags: OConvFlags::default(), - }; - - o.dd_out(i).unwrap(); - - // Final Comparison - let res = File::open(&tmp_fname_ea).unwrap(); - let spec = - File::open("./test-resources/all-valid-ascii-chars-37eff01866ba3f538421b30b7cbefcac.test") - .unwrap(); - - assert_eq!( - res.metadata().unwrap().len(), - spec.metadata().unwrap().len() - ); - - let res = BufReader::new(res); - let spec = BufReader::new(spec); - - let res = BufReader::new(res); - - // Check all bytes match - for (b_res, b_spec) in res.bytes().zip(spec.bytes()) { - assert_eq!(b_res.unwrap(), b_spec.unwrap()); - } - - fs::remove_file(&tmp_fname_ae).unwrap(); - fs::remove_file(&tmp_fname_ea).unwrap(); -} - -make_icf_test!( - swab_256_test, - "swab-256", - File::open("./test-resources/seq-byte-values.test").unwrap(), - IConvFlags { - ctable: None, - block: None, - unblock: None, - swab: true, - sync: None, - noerror: false, - }, - File::open("./test-resources/seq-byte-values-swapped.test").unwrap() -); - -make_icf_test!( - swab_257_test, - "swab-257", - File::open("./test-resources/seq-byte-values-odd.test").unwrap(), - IConvFlags { - ctable: None, - block: None, - unblock: None, - swab: true, - sync: None, - noerror: false, - }, - File::open("./test-resources/seq-byte-values-odd.spec").unwrap() -); diff --git a/src/uu/dd/src/dd_unit_tests/mod.rs b/src/uu/dd/src/dd_unit_tests/mod.rs deleted file mode 100644 index 9641c9bba..000000000 --- a/src/uu/dd/src/dd_unit_tests/mod.rs +++ /dev/null @@ -1,89 +0,0 @@ -// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat - -use super::*; - -mod block_unblock_tests; -mod conv_sync_tests; -mod conversion_tests; -mod sanity_tests; - -use std::fs; -use std::io::prelude::*; -use std::io::BufReader; - -struct LazyReader { - src: R, -} - -impl Read for LazyReader { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - let reduced = cmp::max(buf.len() / 2, 1); - self.src.read(&mut buf[..reduced]) - } -} - -#[macro_export] -macro_rules! icf ( - ( $ctable:expr ) => - { - IConvFlags { - ctable: $ctable, - ..IConvFlags::default() - } - }; -); - -#[macro_export] -macro_rules! make_spec_test ( - ( $test_id:ident, $test_name:expr, $src:expr ) => - { - // When spec not given, output should match input - make_spec_test!($test_id, $test_name, $src, $src); - }; - ( $test_id:ident, $test_name:expr, $src:expr, $spec:expr ) => - { - make_spec_test!($test_id, - $test_name, - Input { - src: $src, - non_ascii: false, - ibs: 512, - print_level: None, - count: None, - cflags: IConvFlags::default(), - iflags: IFlags::default(), - }, - Output { - dst: File::create(format!("./test-resources/FAILED-{}.test", $test_name)).unwrap(), - obs: 512, - cflags: OConvFlags::default(), - }, - $spec, - format!("./test-resources/FAILED-{}.test", $test_name) - ); - }; - ( $test_id:ident, $test_name:expr, $i:expr, $o:expr, $spec:expr, $tmp_fname:expr ) => - { - #[test] - fn $test_id() - { - $o.dd_out($i).unwrap(); - - let res = File::open($tmp_fname).unwrap(); - // Check test file isn't empty (unless spec file is too) - assert_eq!(res.metadata().unwrap().len(), $spec.metadata().unwrap().len()); - - let spec = BufReader::new($spec); - let res = BufReader::new(res); - - // Check all bytes match - for (b_res, b_spec) in res.bytes().zip(spec.bytes()) - { - assert_eq!(b_res.unwrap(), - b_spec.unwrap()); - } - - fs::remove_file($tmp_fname).unwrap(); - } - }; -); diff --git a/src/uu/dd/src/dd_unit_tests/sanity_tests.rs b/src/uu/dd/src/dd_unit_tests/sanity_tests.rs deleted file mode 100644 index f58d68c48..000000000 --- a/src/uu/dd/src/dd_unit_tests/sanity_tests.rs +++ /dev/null @@ -1,316 +0,0 @@ -// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat - -use super::*; - -const DST_PLACEHOLDER: Vec = Vec::new(); - -macro_rules! make_io_test ( - ( $test_id:ident, $test_name:expr, $i:expr, $o:expr, $spec:expr ) => - { - make_spec_test!($test_id, - $test_name, - $i, - Output { - dst: File::create(format!("./test-resources/FAILED-{}.test", $test_name)).unwrap(), - obs: $o.obs, - cflags: $o.cflags, - }, - $spec, - format!("./test-resources/FAILED-{}.test", $test_name) - ); - }; -); - -make_spec_test!( - zeros_4k_test, - "zeros-4k", - File::open("./test-resources/zeros-620f0b67a91f7f74151bc5be745b7110.test").unwrap() -); - -make_spec_test!( - ones_4k_test, - "ones-4k", - File::open("./test-resources/ones-6ae59e64850377ee5470c854761551ea.test").unwrap() -); - -make_spec_test!( - deadbeef_32k_test, - "deadbeef-32k", - File::open("./test-resources/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test").unwrap() -); - -make_spec_test!( - random_73k_test, - "random-73k", - File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test").unwrap() -); - -make_io_test!( - random_73k_test_not_a_multiple_obs_gt_ibs, - "random-73k-not-a-multiple-obs-gt-ibs", - Input { - src: File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test").unwrap(), - non_ascii: false, - ibs: 521, - print_level: None, - count: None, - cflags: IConvFlags::default(), - iflags: IFlags::default(), - }, - Output { - dst: DST_PLACEHOLDER, - obs: 1031, - cflags: OConvFlags::default(), - }, - File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test").unwrap() -); - -make_io_test!( - random_73k_test_obs_lt_not_a_multiple_ibs, - "random-73k-obs-lt-not-a-multiple-ibs", - Input { - src: File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test").unwrap(), - non_ascii: false, - ibs: 1031, - print_level: None, - count: None, - cflags: IConvFlags::default(), - iflags: IFlags::default(), - }, - Output { - dst: DST_PLACEHOLDER, - obs: 521, - cflags: OConvFlags::default(), - }, - File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test").unwrap() -); - -make_io_test!( - deadbeef_all_32k_test_count_reads, - "deadbeef_all_32k_test_count_reads", - Input { - src: File::open("./test-resources/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test").unwrap(), - non_ascii: false, - ibs: 1024, - print_level: None, - count: Some(CountType::Reads(32)), - cflags: IConvFlags::default(), - iflags: IFlags::default(), - }, - Output { - dst: DST_PLACEHOLDER, - obs: 1024, - cflags: OConvFlags::default(), - }, - File::open("./test-resources/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test").unwrap() -); - -make_io_test!( - deadbeef_all_32k_test_count_bytes, - "deadbeef_all_32k_test_count_bytes", - Input { - src: File::open("./test-resources/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test").unwrap(), - non_ascii: false, - ibs: 531, - print_level: None, - count: Some(CountType::Bytes(32 * 1024)), - cflags: IConvFlags::default(), - iflags: IFlags::default(), - }, - Output { - dst: DST_PLACEHOLDER, - obs: 1031, - cflags: OConvFlags::default(), - }, - File::open("./test-resources/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test").unwrap() -); - -make_io_test!( - deadbeef_32k_to_16k_test_count_reads, - "deadbeef_32k_test_count_reads", - Input { - src: File::open("./test-resources/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test").unwrap(), - non_ascii: false, - ibs: 1024, - print_level: None, - count: Some(CountType::Reads(16)), - cflags: IConvFlags::default(), - iflags: IFlags::default(), - }, - Output { - dst: DST_PLACEHOLDER, - obs: 1031, - cflags: OConvFlags::default(), - }, - File::open("./test-resources/gnudd-deadbeef-first-16k.spec").unwrap() -); - -make_io_test!( - deadbeef_32k_to_12345_test_count_bytes, - "deadbeef_32k_to_12345_test_count_bytes", - Input { - src: File::open("./test-resources/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test").unwrap(), - non_ascii: false, - ibs: 531, - print_level: None, - count: Some(CountType::Bytes(12345)), - cflags: IConvFlags::default(), - iflags: IFlags::default(), - }, - Output { - dst: DST_PLACEHOLDER, - obs: 1031, - cflags: OConvFlags::default(), - }, - File::open("./test-resources/gnudd-deadbeef-first-12345.spec").unwrap() -); - -make_io_test!( - random_73k_test_count_reads, - "random-73k-test-count-reads", - Input { - src: File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test").unwrap(), - non_ascii: false, - ibs: 1024, - print_level: None, - count: Some(CountType::Reads(32)), - cflags: IConvFlags::default(), - iflags: IFlags::default(), - }, - Output { - dst: DST_PLACEHOLDER, - obs: 1024, - cflags: OConvFlags::default(), - }, - File::open("./test-resources/gnudd-random-first-32k.spec").unwrap() -); - -make_io_test!( - random_73k_test_count_bytes, - "random-73k-test-count-bytes", - Input { - src: File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test").unwrap(), - non_ascii: false, - ibs: 521, - print_level: None, - count: Some(CountType::Bytes(32 * 1024)), - cflags: IConvFlags::default(), - iflags: IFlags::default(), - }, - Output { - dst: DST_PLACEHOLDER, - obs: 1031, - cflags: OConvFlags::default(), - }, - File::open("./test-resources/gnudd-random-first-32k.spec").unwrap() -); - -make_io_test!( - random_73k_test_lazy_fullblock, - "random-73k-test-lazy-fullblock", - Input { - src: LazyReader { - src: File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test") - .unwrap() - }, - non_ascii: false, - ibs: 521, - print_level: None, - count: None, - cflags: IConvFlags::default(), - iflags: IFlags { - fullblock: true, - ..IFlags::default() - }, - }, - Output { - dst: DST_PLACEHOLDER, - obs: 1031, - cflags: OConvFlags::default(), - }, - File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test").unwrap() -); - -// Test internal buffer size fn -#[test] -fn bsize_test_primes() { - let (n, m) = (7901, 7919); - let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); - - assert_eq!(res, n * m); -} - -#[test] -fn bsize_test_rel_prime_obs_greater() { - let (n, m) = (7 * 5119, 13 * 5119); - let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); - - assert_eq!(res, 7 * 13 * 5119); -} - -#[test] -fn bsize_test_rel_prime_ibs_greater() { - let (n, m) = (13 * 5119, 7 * 5119); - let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); - - assert_eq!(res, 7 * 13 * 5119); -} - -#[test] -fn bsize_test_3fac_rel_prime() { - let (n, m) = (11 * 13 * 5119, 7 * 11 * 5119); - let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); - - assert_eq!(res, 7 * 11 * 13 * 5119); -} - -#[test] -fn bsize_test_ibs_greater() { - let (n, m) = (512 * 1024, 256 * 1024); - let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); - - assert_eq!(res, n); -} - -#[test] -fn bsize_test_obs_greater() { - let (n, m) = (256 * 1024, 512 * 1024); - let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); - - assert_eq!(res, m); -} - -#[test] -fn bsize_test_bs_eq() { - let (n, m) = (1024, 1024); - let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); - - assert_eq!(res, m); -} - -#[test] -#[should_panic] -fn test_nocreat_causes_failure_when_ofile_doesnt_exist() { - let args = vec![ - String::from("dd"), - String::from("--conv=nocreat"), - String::from("--of=not-a-real.file"), - ]; - - let matches = uu_app().try_get_matches_from(args).unwrap(); - let _ = Output::::new(&matches).unwrap(); -} diff --git a/src/uu/dd/test-resources/FAILED-random-5828891cb1230748e146f34223bbd3b5.test b/src/uu/dd/test-resources/FAILED-random-5828891cb1230748e146f34223bbd3b5.test new file mode 100644 index 0000000000000000000000000000000000000000..6e997411c30e758e1d8654b02d365940cda4426d GIT binary patch literal 74400 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 Ruin0S|L*s%zkmM!0{}7Ge}Mo1 diff --git a/src/uu/dd/test-resources/gnudd-conv-atoibm-seq-byte-values.spec b/src/uu/dd/test-resources/gnudd-conv-atoibm-seq-byte-values.spec deleted file mode 100644 index 3835d57cfb07e4c9021a7b8e0e2447a212c3032b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 256 zcmZQ%U}n-a*Vkhe<5uP6;pY<+5EinvQ8SX1P`8(rk(Y~dsIJe6t__Iw^@~Z!PW1lx z;q#}jU%r3)`6I5X&L=F{MD=0u35isa}c5AB>BU7X!qJ=_C5 zgM>!owmXl9N(X($g|C3JZ#hN=wSi8Y;TGdVBi&vL?)#J#X%U`KwoXty#Zr&h5-No diff --git a/src/uu/dd/test-resources/gnudd-conv-ebcdic-utol-seq-byte-values.spec b/src/uu/dd/test-resources/gnudd-conv-ebcdic-utol-seq-byte-values.spec deleted file mode 100644 index 13c82e5e3c8500dbc52f6820c5fe57b5ab5e9762..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 256 zcmZQ%U}n-a*Vkhe<5uP6;pY<+5EinvQ8SX1P`8(rk(Y~dsIJe6t__Iw^@~Z!PW1lx z;q#}jU%r3)`6I5X&L=;wv8K7HwWYnSvt!c4DU+v7oiTmU%q5GLEnTsE?aI9ma%VMG z;*vkG@`68myrP1#lBkL}o2G`g7N?Fhm!W~Nl&J}anWcrbl?cm2J10jMXE#?5_dw6! zpwN);u*ityq|}u3w9Jgcg5sjmlJc^Kiteu7p8me933F!8o4a8C>Q!EA*00;RVe_V~ sTeff8xnuXPg?skzJALZxne*pv-@1F}{=L_)-oAPN?)R_1fBydi0I5)U0RR91 diff --git a/src/uu/dd/test-resources/gnudd-conv-etoa-seq-byte-values.spec b/src/uu/dd/test-resources/gnudd-conv-etoa-seq-byte-values.spec deleted file mode 100644 index 433b1f600bebcdb8e30264c727d5e5c7b6eac6af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 256 zcmZQ%U}oZ+Q{UD*ox6{hho4VSKv<}CZaashME@jN8F{(JhUO-&7HRR$j_xkj9(J~g z6DEsIox(D0=8W01L`9_LEl^mraLM9jOIIw{yQ*PRqrFmX^{TaN*00;RK}AthtIk?q zck`yLTeff8xkKl4ysEw3?p=HL?B93rK*AwqD|H7Y+r-1kNvSF6X_*;Ej~qXC@gD!SFT^XdE@r2yLay2d-&k-qu3|UpE*`L zJGr{JyLo!NeDV6#+c#ddKHh%50scXO@85m=@cGmHn2_MG(1`G;$gf|%fBX5v(7@Qp R)WqD((&G28zkmM!0{|_%e}Mo1 diff --git a/src/uu/dd/test-resources/gnudd-conv-ibm-ltou-seq-byte-values.spec b/src/uu/dd/test-resources/gnudd-conv-ibm-ltou-seq-byte-values.spec deleted file mode 100644 index d425e7d012beb1c314cc4e1f72fafed1afe378e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 256 zcmZQ%U}n-a*Vkhe<5uP6;pY<+5EinvQ8SX1P`8(rk(Y~dsIJe6t__Iw^@~Z!PW1lx z;q#}jU%r3)`6I5X&L=_JxWH%1WXt;%u54+FG1C(p-iH#!{vx9A=gl)>a}c5AB>BU7X!qJ=_C5 zgM>!owmXl9N(X($g|C3JZ#hN=wSi8Y;TGdVBi&W=)tgd*0jy^H;B0yJr2mjT<&^ t+PY=?ww*h6@7lX(|Gv|w&Yn4c?)I&_ckbVN{p#(T_wRoH`upeqKL7->hPMC! diff --git a/src/uu/dd/test-resources/gnudd-conv-ibm-utol-seq-byte-values.spec b/src/uu/dd/test-resources/gnudd-conv-ibm-utol-seq-byte-values.spec deleted file mode 100644 index 1dcaccbcdf8adfd826979f9273c87cd9568b0209..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 256 zcmZQ%U}n-a*Vkhe<5uP6;pY<+5EinvQ8SX1P`8(rk(Y~dsIJe6t__Iw^@~Z!PW1lx z;q#}jU%r3)`6I5X&L=;wv8K7HwWYnSvt!c4DU+v7oiTmU%q5GLEnTsE?aI9ma^o8- zamgQ8dBLB3p`wDalBkL}o2G`g7N?Fhm!W~Nl&J}anWcrbl?cm2J10jMXE#?5_dw6! zpwN);u*ityq|}u3w9Jgcg5sjmlJc^Kiteu7p8mdB6XwjGH+RAO)vMO7S-)=MhRvI{ sZrQ$V=Z@XG_U_rg@ARp&XU?Cyee3R>`}bbIdi&=6yWhY5{`vn80K=@5f_t`kd~5_k(X0cP*ze^QCHK{(ALt`(bqFH zFg7wZF*mccu(qcc@%IZ12o4Gj2@i{mh>nVliH~zkz$ITD zRa;YE*Vxe9)Y{VC*4feB)!WnGH*vz`NmHjxpEh&G>{)Z?%%8V#!Qw?rmn>hla>eRZ zYuBt_w{gSfO@5f_t`kd~5_k(X0cP*ze^QCHK{(ALt`(bqFH zFg7wZF*mccu(q{)Z?%%8V#!Qw?rmn>hla>eRZ zYuBt_w{gSfOMC+6cQE@6%&_`l#-T_m6KOcR8m$^Ra4i{)Y8_`)zddH zG%_|ZH8Z!cw6eCbwX=6{baHlab#wRd^z!!c_45x13RUz zF>}`JIdkXDU$Ah|;w4L$Enl&6)#^2C*R9{Mant54TeofBv2)k%J$v` Some(&ASCII_TO_IBM_UCASE_TO_LCASE), +// (ConvFlag::FmtAtoI, ConvFlag::LCase) => Some(&ASCII_TO_IBM_LCASE_TO_UCASE), +// +// If my reading is correct and that is a typo, then the +// UCASE_TO_LCASE and LCASE_TO_UCASE in those lines should be swapped, +// and the expected output for the following two tests should be +// updated accordingly. +#[test] +fn test_atoibm_and_ucase_conv_spec_test() { + new_ucmd!() + .args(&["conv=ibm,ucase"]) + .pipe_in_fixture("seq-byte-values-b632a992d3aed5d8d1a59cc5a5a455ba.test") + .succeeds() + .stdout_is_fixture_bytes("lcase-ibm.test"); +} + +#[test] +fn test_atoibm_and_lcase_conv_spec_test() { + new_ucmd!() + .args(&["conv=ibm,lcase"]) + .pipe_in_fixture("seq-byte-values-b632a992d3aed5d8d1a59cc5a5a455ba.test") + .succeeds() + .stdout_is_fixture_bytes("ucase-ibm.test"); +} + +#[test] +fn test_swab_256_test() { + new_ucmd!() + .args(&["conv=swab"]) + .pipe_in_fixture("seq-byte-values.test") + .succeeds() + .stdout_is_fixture_bytes("seq-byte-values-swapped.test"); +} + +#[test] +fn test_swab_257_test() { + new_ucmd!() + .args(&["conv=swab"]) + .pipe_in_fixture("seq-byte-values-odd.test") + .succeeds() + .stdout_is_fixture_bytes("seq-byte-values-odd.spec"); +} + +#[test] +fn test_zeros_4k_conv_sync_obs_gt_ibs() { + new_ucmd!() + .args(&["conv=sync", "ibs=521", "obs=1031"]) + .pipe_in_fixture("zeros-620f0b67a91f7f74151bc5be745b7110.test") + .succeeds() + .stdout_is_fixture_bytes("gnudd-conv-sync-ibs-521-obs-1031-zeros.spec"); +} + +#[test] +fn test_zeros_4k_conv_sync_ibs_gt_obs() { + new_ucmd!() + .args(&["conv=sync", "ibs=1031", "obs=521"]) + .pipe_in_fixture("zeros-620f0b67a91f7f74151bc5be745b7110.test") + .succeeds() + .stdout_is_fixture_bytes("gnudd-conv-sync-ibs-1031-obs-521-zeros.spec"); +} + +#[test] +fn test_deadbeef_32k_conv_sync_obs_gt_ibs() { + new_ucmd!() + .args(&[ + "conv=sync", + "ibs=521", + "obs=1031", + "if=deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test", + ]) + .succeeds() + .stdout_is_fixture_bytes("gnudd-conv-sync-ibs-521-obs-1031-deadbeef.spec"); +} + +#[test] +fn test_deadbeef_32k_conv_sync_ibs_gt_obs() { + new_ucmd!() + .args(&[ + "conv=sync", + "ibs=1031", + "obs=521", + "if=deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test", + ]) + .succeeds() + .stdout_is_fixture_bytes("gnudd-conv-sync-ibs-1031-obs-521-deadbeef.spec"); +} + +#[test] +fn test_random_73k_test_bs_prime_obs_gt_ibs_sync() { + new_ucmd!() + .args(&[ + "conv=sync", + "ibs=521", + "obs=1031", + "if=random-5828891cb1230748e146f34223bbd3b5.test", + ]) + .succeeds() + .stdout_is_fixture_bytes("gnudd-conv-sync-ibs-521-obs-1031-random.spec"); +} + +#[test] +fn test_random_73k_test_bs_prime_ibs_gt_obs_sync() { + new_ucmd!() + .args(&[ + "conv=sync", + "ibs=1031", + "obs=521", + "if=random-5828891cb1230748e146f34223bbd3b5.test", + ]) + .succeeds() + .stdout_is_fixture_bytes("gnudd-conv-sync-ibs-1031-obs-521-random.spec"); +} + +#[test] +fn test_identity() { + new_ucmd!() + .args(&["if=zeros-620f0b67a91f7f74151bc5be745b7110.test"]) + .succeeds() + .stdout_is_fixture_bytes("zeros-620f0b67a91f7f74151bc5be745b7110.test"); + new_ucmd!() + .args(&["if=ones-6ae59e64850377ee5470c854761551ea.test"]) + .succeeds() + .stdout_is_fixture_bytes("ones-6ae59e64850377ee5470c854761551ea.test"); + new_ucmd!() + .args(&["if=deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test"]) + .succeeds() + .stdout_is_fixture_bytes("deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test"); + new_ucmd!() + .args(&["if=random-5828891cb1230748e146f34223bbd3b5.test"]) + .succeeds() + .stdout_is_fixture_bytes("random-5828891cb1230748e146f34223bbd3b5.test"); +} + +#[test] +fn test_random_73k_test_not_a_multiple_obs_gt_ibs() { + new_ucmd!() + .args(&[ + "ibs=521", + "obs=1031", + "if=random-5828891cb1230748e146f34223bbd3b5.test", + ]) + .succeeds() + .stdout_is_fixture_bytes("random-5828891cb1230748e146f34223bbd3b5.test"); +} + +#[test] +fn test_random_73k_test_obs_lt_not_a_multiple_ibs() { + new_ucmd!() + .args(&[ + "ibs=1031", + "obs=521", + "if=random-5828891cb1230748e146f34223bbd3b5.test", + ]) + .succeeds() + .stdout_is_fixture_bytes("random-5828891cb1230748e146f34223bbd3b5.test"); +} + +#[test] +fn test_deadbeef_all_32k_test_count_reads() { + new_ucmd!() + .args(&[ + "bs=1024", + "count=32", + "if=deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test", + ]) + .succeeds() + .stdout_is_fixture_bytes("deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test"); +} + +#[test] +fn test_deadbeef_all_32k_test_count_bytes() { + new_ucmd!() + .args(&[ + "ibs=531", + "obs=1031", + "count=32x1024", + "oflag=count_bytes", + "if=deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test", + ]) + .succeeds() + .stdout_is_fixture_bytes("deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test"); +} + +#[test] +fn test_deadbeef_32k_to_16k_test_count_reads() { + new_ucmd!() + .args(&[ + "ibs=1024", + "obs=1031", + "count=16", + "if=deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test", + ]) + .succeeds() + .stdout_is_fixture_bytes("gnudd-deadbeef-first-16k.spec"); +} + +#[test] +fn test_deadbeef_32k_to_12345_test_count_bytes() { + new_ucmd!() + .args(&[ + "ibs=531", + "obs=1031", + "count=12345", + "iflag=count_bytes", + "if=deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test", + ]) + .succeeds() + .stdout_is_fixture_bytes("gnudd-deadbeef-first-12345.spec"); +} + +#[test] +fn test_random_73k_test_count_reads() { + new_ucmd!() + .args(&[ + "bs=1024", + "count=32", + "if=random-5828891cb1230748e146f34223bbd3b5.test", + ]) + .succeeds() + .stdout_is_fixture_bytes("gnudd-random-first-32k.spec"); +} + +#[test] +fn test_random_73k_test_count_bytes() { + new_ucmd!() + .args(&[ + "ibs=521", + "obs=1031", + "count=32x1024", + "iflag=count_bytes", + "if=random-5828891cb1230748e146f34223bbd3b5.test", + ]) + .succeeds() + .stdout_is_fixture_bytes("gnudd-random-first-32k.spec"); +} + +#[test] +fn test_all_valid_ascii_ebcdic_ascii_roundtrip_conv_test() { + let tmp = new_ucmd!() + .args(&["ibs=128", "obs=1024", "conv=ebcdic"]) + .pipe_in_fixture("all-valid-ascii-chars-37eff01866ba3f538421b30b7cbefcac.test") + .succeeds() + .stdout_move_bytes(); + new_ucmd!() + .args(&["ibs=256", "obs=1024", "conv=ascii"]) + .pipe_in(tmp) + .succeeds() + .stdout_is_fixture_bytes("all-valid-ascii-chars-37eff01866ba3f538421b30b7cbefcac.test"); +} diff --git a/src/uu/dd/test-resources/all-valid-ascii-chars-37eff01866ba3f538421b30b7cbefcac.test b/tests/fixtures/dd/all-valid-ascii-chars-37eff01866ba3f538421b30b7cbefcac.test similarity index 100% rename from src/uu/dd/test-resources/all-valid-ascii-chars-37eff01866ba3f538421b30b7cbefcac.test rename to tests/fixtures/dd/all-valid-ascii-chars-37eff01866ba3f538421b30b7cbefcac.test diff --git a/src/uu/dd/test-resources/dd-block-cbs16-win.test b/tests/fixtures/dd/dd-block-cbs16-win.test similarity index 100% rename from src/uu/dd/test-resources/dd-block-cbs16-win.test rename to tests/fixtures/dd/dd-block-cbs16-win.test diff --git a/src/uu/dd/test-resources/dd-block-cbs16.spec b/tests/fixtures/dd/dd-block-cbs16.spec similarity index 100% rename from src/uu/dd/test-resources/dd-block-cbs16.spec rename to tests/fixtures/dd/dd-block-cbs16.spec diff --git a/src/uu/dd/test-resources/dd-block-cbs16.test b/tests/fixtures/dd/dd-block-cbs16.test similarity index 100% rename from src/uu/dd/test-resources/dd-block-cbs16.test rename to tests/fixtures/dd/dd-block-cbs16.test diff --git a/src/uu/dd/test-resources/dd-block-cbs8.spec b/tests/fixtures/dd/dd-block-cbs8.spec similarity index 100% rename from src/uu/dd/test-resources/dd-block-cbs8.spec rename to tests/fixtures/dd/dd-block-cbs8.spec diff --git a/src/uu/dd/test-resources/dd-block-consecutive-nl-cbs16.spec b/tests/fixtures/dd/dd-block-consecutive-nl-cbs16.spec similarity index 100% rename from src/uu/dd/test-resources/dd-block-consecutive-nl-cbs16.spec rename to tests/fixtures/dd/dd-block-consecutive-nl-cbs16.spec diff --git a/src/uu/dd/test-resources/dd-block-consecutive-nl-win.test b/tests/fixtures/dd/dd-block-consecutive-nl-win.test similarity index 100% rename from src/uu/dd/test-resources/dd-block-consecutive-nl-win.test rename to tests/fixtures/dd/dd-block-consecutive-nl-win.test diff --git a/src/uu/dd/test-resources/dd-block-consecutive-nl.test b/tests/fixtures/dd/dd-block-consecutive-nl.test similarity index 100% rename from src/uu/dd/test-resources/dd-block-consecutive-nl.test rename to tests/fixtures/dd/dd-block-consecutive-nl.test diff --git a/src/uu/dd/test-resources/dd-unblock-cbs16-win.spec b/tests/fixtures/dd/dd-unblock-cbs16-win.spec similarity index 100% rename from src/uu/dd/test-resources/dd-unblock-cbs16-win.spec rename to tests/fixtures/dd/dd-unblock-cbs16-win.spec diff --git a/src/uu/dd/test-resources/dd-unblock-cbs16.spec b/tests/fixtures/dd/dd-unblock-cbs16.spec similarity index 100% rename from src/uu/dd/test-resources/dd-unblock-cbs16.spec rename to tests/fixtures/dd/dd-unblock-cbs16.spec diff --git a/src/uu/dd/test-resources/dd-unblock-cbs16.test b/tests/fixtures/dd/dd-unblock-cbs16.test similarity index 100% rename from src/uu/dd/test-resources/dd-unblock-cbs16.test rename to tests/fixtures/dd/dd-unblock-cbs16.test diff --git a/src/uu/dd/test-resources/dd-unblock-cbs8-win.spec b/tests/fixtures/dd/dd-unblock-cbs8-win.spec similarity index 100% rename from src/uu/dd/test-resources/dd-unblock-cbs8-win.spec rename to tests/fixtures/dd/dd-unblock-cbs8-win.spec diff --git a/src/uu/dd/test-resources/dd-unblock-cbs8.spec b/tests/fixtures/dd/dd-unblock-cbs8.spec similarity index 100% rename from src/uu/dd/test-resources/dd-unblock-cbs8.spec rename to tests/fixtures/dd/dd-unblock-cbs8.spec diff --git a/tests/fixtures/dd/deadbeef-16.spec b/tests/fixtures/dd/deadbeef-16.spec new file mode 100644 index 0000000000000000000000000000000000000000..4eb7c10f190e85dbf88978936a95194d29658839 GIT binary patch literal 128 Ucmewl1q@IC<^G3q8EDN101#h4!2kdN literal 0 HcmV?d00001 diff --git a/tests/fixtures/dd/deadbeef-16.test b/tests/fixtures/dd/deadbeef-16.test new file mode 100644 index 000000000..85f2b7569 --- /dev/null +++ b/tests/fixtures/dd/deadbeef-16.test @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/uu/dd/test-resources/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test b/tests/fixtures/dd/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test similarity index 100% rename from src/uu/dd/test-resources/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test rename to tests/fixtures/dd/deadbeef-18d99661a1de1fc9af21b0ec2cd67ba3.test diff --git a/src/uu/dd/test-resources/gnudd-conv-sync-ibs-1031-obs-521-deadbeef.spec b/tests/fixtures/dd/gnudd-conv-sync-ibs-1031-obs-521-deadbeef.spec similarity index 100% rename from src/uu/dd/test-resources/gnudd-conv-sync-ibs-1031-obs-521-deadbeef.spec rename to tests/fixtures/dd/gnudd-conv-sync-ibs-1031-obs-521-deadbeef.spec diff --git a/src/uu/dd/test-resources/gnudd-conv-sync-ibs-1031-obs-521-random.spec b/tests/fixtures/dd/gnudd-conv-sync-ibs-1031-obs-521-random.spec similarity index 100% rename from src/uu/dd/test-resources/gnudd-conv-sync-ibs-1031-obs-521-random.spec rename to tests/fixtures/dd/gnudd-conv-sync-ibs-1031-obs-521-random.spec diff --git a/src/uu/dd/test-resources/gnudd-conv-sync-ibs-1031-obs-521-zeros.spec b/tests/fixtures/dd/gnudd-conv-sync-ibs-1031-obs-521-zeros.spec similarity index 100% rename from src/uu/dd/test-resources/gnudd-conv-sync-ibs-1031-obs-521-zeros.spec rename to tests/fixtures/dd/gnudd-conv-sync-ibs-1031-obs-521-zeros.spec diff --git a/src/uu/dd/test-resources/gnudd-conv-sync-ibs-521-obs-1031-deadbeef.spec b/tests/fixtures/dd/gnudd-conv-sync-ibs-521-obs-1031-deadbeef.spec similarity index 100% rename from src/uu/dd/test-resources/gnudd-conv-sync-ibs-521-obs-1031-deadbeef.spec rename to tests/fixtures/dd/gnudd-conv-sync-ibs-521-obs-1031-deadbeef.spec diff --git a/src/uu/dd/test-resources/gnudd-conv-sync-ibs-521-obs-1031-random.spec b/tests/fixtures/dd/gnudd-conv-sync-ibs-521-obs-1031-random.spec similarity index 100% rename from src/uu/dd/test-resources/gnudd-conv-sync-ibs-521-obs-1031-random.spec rename to tests/fixtures/dd/gnudd-conv-sync-ibs-521-obs-1031-random.spec diff --git a/src/uu/dd/test-resources/gnudd-conv-sync-ibs-521-obs-1031-zeros.spec b/tests/fixtures/dd/gnudd-conv-sync-ibs-521-obs-1031-zeros.spec similarity index 100% rename from src/uu/dd/test-resources/gnudd-conv-sync-ibs-521-obs-1031-zeros.spec rename to tests/fixtures/dd/gnudd-conv-sync-ibs-521-obs-1031-zeros.spec diff --git a/src/uu/dd/test-resources/gnudd-deadbeef-first-12345.spec b/tests/fixtures/dd/gnudd-deadbeef-first-12345.spec similarity index 100% rename from src/uu/dd/test-resources/gnudd-deadbeef-first-12345.spec rename to tests/fixtures/dd/gnudd-deadbeef-first-12345.spec diff --git a/src/uu/dd/test-resources/gnudd-deadbeef-first-16k.spec b/tests/fixtures/dd/gnudd-deadbeef-first-16k.spec similarity index 100% rename from src/uu/dd/test-resources/gnudd-deadbeef-first-16k.spec rename to tests/fixtures/dd/gnudd-deadbeef-first-16k.spec diff --git a/src/uu/dd/test-resources/gnudd-random-first-32k.spec b/tests/fixtures/dd/gnudd-random-first-32k.spec similarity index 100% rename from src/uu/dd/test-resources/gnudd-random-first-32k.spec rename to tests/fixtures/dd/gnudd-random-first-32k.spec diff --git a/src/uu/dd/test-resources/lcase-ascii.test b/tests/fixtures/dd/lcase-ascii.test similarity index 100% rename from src/uu/dd/test-resources/lcase-ascii.test rename to tests/fixtures/dd/lcase-ascii.test diff --git a/src/uu/dd/test-resources/lcase-ebcdic.spec b/tests/fixtures/dd/lcase-ebcdic.spec similarity index 100% rename from src/uu/dd/test-resources/lcase-ebcdic.spec rename to tests/fixtures/dd/lcase-ebcdic.spec diff --git a/src/uu/dd/test-resources/lcase-ebcdic.test b/tests/fixtures/dd/lcase-ebcdic.test similarity index 100% rename from src/uu/dd/test-resources/lcase-ebcdic.test rename to tests/fixtures/dd/lcase-ebcdic.test diff --git a/src/uu/dd/test-resources/lcase-ibm.spec b/tests/fixtures/dd/lcase-ibm.spec similarity index 100% rename from src/uu/dd/test-resources/lcase-ibm.spec rename to tests/fixtures/dd/lcase-ibm.spec diff --git a/src/uu/dd/test-resources/lcase-ibm.test b/tests/fixtures/dd/lcase-ibm.test similarity index 100% rename from src/uu/dd/test-resources/lcase-ibm.test rename to tests/fixtures/dd/lcase-ibm.test diff --git a/src/uu/dd/test-resources/ones-6ae59e64850377ee5470c854761551ea.test b/tests/fixtures/dd/ones-6ae59e64850377ee5470c854761551ea.test similarity index 100% rename from src/uu/dd/test-resources/ones-6ae59e64850377ee5470c854761551ea.test rename to tests/fixtures/dd/ones-6ae59e64850377ee5470c854761551ea.test diff --git a/tests/fixtures/dd/random-5828891cb1230748e146f34223bbd3b5.test b/tests/fixtures/dd/random-5828891cb1230748e146f34223bbd3b5.test new file mode 100644 index 0000000000000000000000000000000000000000..6e997411c30e758e1d8654b02d365940cda4426d GIT binary patch literal 74400 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 Date: Thu, 17 Feb 2022 10:27:13 +0100 Subject: [PATCH 148/161] README: add link to GNU tests page in documentation --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 28df6110f..d30fd4c05 100644 --- a/README.md +++ b/README.md @@ -349,6 +349,10 @@ $ make UTILS='UTILITY_1 UTILITY_2' RUNTEST_ARGS='-v' busytest ### Comparing with GNU +Below is the evolution of how many GNU tests uutils passes. A more detailed +breakdown of the GNU test results of the main branch can be found +[in the user manual](https://uutils.github.io/coreutils-docs/user/test_coverage.html). + ![Evolution over time](https://github.com/uutils/coreutils-tracking/blob/main/gnu-results.png?raw=true) To run locally: From f57e3470ae452e23157a375aaf58c9692617a696 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 17 Feb 2022 18:29:05 +0100 Subject: [PATCH 149/161] docs: add examples from tldr-pages --- docs/.gitignore | 1 + docs/Makefile | 4 +++- src/bin/uudoc.rs | 50 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/docs/.gitignore b/docs/.gitignore index f2b5c7168..c836306f5 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,3 +1,4 @@ book src/utils src/SUMMARY.md +tldr/ diff --git a/docs/Makefile b/docs/Makefile index dd700bcb0..23901b755 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,4 +1,6 @@ +# spell-checker:ignore tldr clean: - rm -rf _build + rm -rf book rm -f src/SUMMARY.md rm -f src/utils/* + rm -rf tldr diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 71bbb2684..5658f491c 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -2,15 +2,33 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore tldr use clap::App; use std::ffi::OsString; use std::fs::File; -use std::io::{self, Write}; +use std::io::{self, Read, Write}; +use std::process::Command; include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); fn main() -> io::Result<()> { + let _ = std::fs::create_dir("docs/tldr"); + println!("Downloading tldr archive"); + Command::new("curl") + .arg("https://tldr.sh/assets/tldr.zip") + .arg("--output") + .arg("docs/tldr/tldr.zip") + .output()?; + + println!("Unzipping tldr archive"); + Command::new("unzip") + .arg("-o") + .arg("docs/tldr/tldr.zip") + .arg("-d") + .arg("docs/tldr") + .output()?; + let utils = util_map::>>(); match std::fs::create_dir("docs/src/utils/") { Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), @@ -55,6 +73,7 @@ fn write_markdown(mut w: impl Write, app: &mut App, name: &str) -> io::Result<() write_version(&mut w, app)?; write_usage(&mut w, app, name)?; write_description(&mut w, app)?; + write_examples(&mut w, name)?; write_options(&mut w, app) } @@ -82,6 +101,35 @@ fn write_description(w: &mut impl Write, app: &App) -> io::Result<()> { } } +fn write_examples(w: &mut impl Write, name: &str) -> io::Result<()> { + if let Ok(mut file) = std::fs::File::open(format!("docs/tldr/pages/common/{}.md", name)) + .or_else(|_| std::fs::File::open(format!("docs/tldr/pages/linux/{}.md", name))) + { + let mut content = String::new(); + file.read_to_string(&mut content)?; + + writeln!(w, "## Examples")?; + writeln!(w)?; + for line in content.lines().skip_while(|l| !l.starts_with('-')) { + if let Some(l) = line.strip_prefix("- ") { + writeln!(w, "{}", l)?; + } else if line.starts_with('`') { + writeln!(w, "```shell\n{}\n```", line.trim_matches('`'))?; + } else if line.is_empty() { + writeln!(w)?; + } else { + println!("Not sure what to do with this line:"); + println!("{}", line); + } + } + writeln!(w)?; + writeln!(w, "> The examples are provided by the [tldr-pages project](https://tldr.sh) under the [CC BY 4.0 License](https://github.com/tldr-pages/tldr/blob/main/LICENSE.md).")?; + } else { + println!("No examples found for: {}", name); + } + Ok(()) +} + fn write_options(w: &mut impl Write, app: &App) -> io::Result<()> { writeln!(w, "

Options

")?; write!(w, "
")?; From 0af2c9bafbdc7f9ebf799bed230da832d5bb5cd4 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Sun, 13 Feb 2022 21:46:45 -0600 Subject: [PATCH 150/161] maint/CICD ~ (GnuTests) display sub-step test comparison failures more prominently --- .github/workflows/GnuTests.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index eef8567c7..e901d4cfe 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -160,34 +160,38 @@ jobs: - name: Compare test failures VS reference shell: bash run: | + have_new_failures="" 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' if test -f "${REF_LOG_FILE}"; then - echo "Reference SHA1/ID (of '${REF_SUMMARY_FILE}'): $(sha1sum -- "${REF_SUMMARY_FILE}")" + echo "Reference SHA1/ID: $(sha1sum -- "${REF_SUMMARY_FILE}")" REF_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" "${REF_LOG_FILE}" | sort) NEW_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" '${{ steps.vars.outputs.path_GNU_tests }}/test-suite.log' | sort) - for LINE in $REF_FAILING + for LINE in ${REF_FAILING} do - if ! grep -Fxq $LINE<<<"$NEW_FAILING"; then - echo "::warning ::Congrats! The gnu test $LINE is now passing!" + if ! grep -Fxq ${LINE}<<<"${NEW_FAILING}"; then + echo "::warning ::Congrats! The gnu test ${LINE} is now passing!" fi done - for LINE in $NEW_FAILING + for LINE in ${NEW_FAILING} do - if ! grep -Fxq $LINE<<<"$REF_FAILING" + if ! grep -Fxq ${LINE}<<<"${REF_FAILING}" then - echo "::error ::GNU test failed: $LINE. $LINE is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?" + echo "::error ::GNU test failed: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?" + have_new_failures="true" fi done else echo "::warning ::Skipping test failure comparison; no prior reference test logs are available." fi + if test -n "${have_new_failures}" ; then exit -1 ; fi - name: Compare test summary VS reference + if: success() || failure() # run regardless of prior step success/failure shell: bash run: | REF_SUMMARY_FILE='${{ steps.vars.outputs.path_reference }}/test-summary/gnu-result.json' if test -f "${REF_SUMMARY_FILE}"; then - echo "Reference SHA1/ID (of '${REF_SUMMARY_FILE}'): $(sha1sum -- "${REF_SUMMARY_FILE}")" + echo "Reference SHA1/ID: $(sha1sum -- "${REF_SUMMARY_FILE}")" mv "${REF_SUMMARY_FILE}" main-gnu-result.json python uutils/util/compare_gnu_result.py else From d6424bb354dced58cf314377db00ce2e6a847e99 Mon Sep 17 00:00:00 2001 From: Roy Ivy III Date: Wed, 16 Feb 2022 10:16:42 -0600 Subject: [PATCH 151/161] maint/util ~ remove extra/redundant factor tests for 'debug' builds - avoids "Terminated" timeout errors when using longer running 'debug' `factor` --- util/build-gnu.sh | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 2ab23ddc7..bb8d6e522 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -70,19 +70,38 @@ sed -i 's|^"\$@|/usr/bin/timeout 600 "\$@|' build-aux/test-driver sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" Makefile sed -i 's| tr | /usr/bin/tr |' tests/init.sh make -j "$(nproc)" -# Generate the factor tests, so they can be fixed -# Used to be 36. Reduced to 20 to decrease the log size -for i in {00..20}; do - make "tests/factor/t${i}.sh" -done - -# strip the long stuff -for i in {21..36}; do +first=00 +if test ${UU_MAKE_PROFILE} != "debug"; then + # Generate the factor tests, so they can be fixed + # * reduced to 20 to decrease log size (down from 36 expected by GNU) + # * only for 'release', skipped for 'debug' as redundant and too time consuming (causing timeout errors) + seq=$( + i=${first} + while test "$i" -le 20; do + printf '%02d ' $i + i=$(($i + 1)) + done + ) + for i in ${seq}; do + make "tests/factor/t${i}.sh" + done + sed -i -e 's|^seq |/usr/bin/seq |' -e 's|sha1sum |/usr/bin/sha1sum |' tests/factor/t*sh + first=21 +fi +# strip all (debug) or just the longer (release) factor tests from Makefile +seq=$( + i=${first} + while test "$i" -le 36; do + printf '%02d ' $i + i=$(($i + 1)) + done +) +for i in ${seq}; do + echo "strip t${i}.sh from Makefile" sed -i -e "s/\$(tf)\/t${i}.sh//g" Makefile done grep -rl 'path_prepend_' tests/* | xargs sed -i 's| path_prepend_ ./src||' -sed -i -e 's|^seq |/usr/bin/seq |' -e 's|sha1sum |/usr/bin/sha1sum |' tests/factor/t*sh # Remove tests checking for --version & --help # Not really interesting for us and logs are too big From 89f428b44f704b0fb330a6477a9a71836e751090 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sat, 12 Feb 2022 22:34:39 -0500 Subject: [PATCH 152/161] dd: remove spurious zero multiplier warning Fix a bug in which `dd` was inappropriately showing a warning about a "0x" multiplier when there was no "x" character in the argument. --- src/uu/dd/src/parseargs.rs | 34 +++++++++++++++++++++------------- tests/by-util/test_dd.rs | 16 ++++++++++++++++ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index 1425cae01..028deab74 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -325,6 +325,14 @@ impl std::str::FromStr for StatusLevel { } } +fn show_zero_multiplier_warning() { + show_warning!( + "{} is a zero multiplier; use {} if that is intended", + "0x".quote(), + "00x".quote() + ); +} + /// Parse bytes using str::parse, then map error if needed. fn parse_bytes_only(s: &str) -> Result { s.parse() @@ -357,13 +365,6 @@ fn parse_bytes_only(s: &str) -> Result { /// assert_eq!(parse_bytes_no_x("2k").unwrap(), 2 * 1024); /// ``` fn parse_bytes_no_x(s: &str) -> Result { - if s == "0" { - show_warning!( - "{} is a zero multiplier; use {} if that is intended", - "0x".quote(), - "00x".quote() - ); - } let (num, multiplier) = match (s.find('c'), s.rfind('w'), s.rfind('b')) { (None, None, None) => match uucore::parse_size::parse_size(s) { Ok(n) => (n, 1), @@ -401,13 +402,20 @@ fn parse_bytes_with_opt_multiplier(s: &str) -> Result { // Split on the 'x' characters. Each component will be parsed // individually, then multiplied together. - let mut total = 1; - for part in s.split('x') { - let num = parse_bytes_no_x(part).map_err(|e| e.with_arg(s.to_string()))?; - total *= num; + let parts: Vec<&str> = s.split('x').collect(); + if parts.len() == 1 { + parse_bytes_no_x(parts[0]).map_err(|e| e.with_arg(s.to_string())) + } else { + let mut total = 1; + for part in parts { + if part == "0" { + show_zero_multiplier_warning(); + } + let num = parse_bytes_no_x(part).map_err(|e| e.with_arg(s.to_string()))?; + total *= num; + } + Ok(total) } - - Ok(total) } pub fn parse_ibs(matches: &Matches) -> Result { diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 58c577a7d..22daec60c 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -201,6 +201,13 @@ fn test_x_multiplier() { #[test] fn test_zero_multiplier_warning() { for arg in ["count", "seek", "skip"] { + new_ucmd!() + .args(&[format!("{}=0", arg).as_str(), "status=none"]) + .pipe_in("") + .succeeds() + .no_stdout() + .no_stderr(); + new_ucmd!() .args(&[format!("{}=00x1", arg).as_str(), "status=none"]) .pipe_in("") @@ -1063,3 +1070,12 @@ fn test_all_valid_ascii_ebcdic_ascii_roundtrip_conv_test() { .succeeds() .stdout_is_fixture_bytes("all-valid-ascii-chars-37eff01866ba3f538421b30b7cbefcac.test"); } + +#[test] +fn test_skip_zero() { + new_ucmd!() + .args(&["skip=0", "status=noxfer"]) + .succeeds() + .no_stdout() + .stderr_is("0+0 records in\n0+0 records out\n"); +} From 1e12c46c246d58115a33b5f1279251ea866df9a8 Mon Sep 17 00:00:00 2001 From: ndd7xv Date: Thu, 10 Feb 2022 19:24:16 -0500 Subject: [PATCH 153/161] uucore(memo): replace err_conv with result --- src/uu/printf/src/printf.rs | 2 +- src/uu/seq/src/seq.rs | 12 ++- src/uucore/src/lib/features/memo.rs | 15 +-- src/uucore/src/lib/features/tokenize/sub.rs | 94 +++++++++++++------ src/uucore/src/lib/features/tokenize/token.rs | 4 +- .../lib/features/tokenize/unescaped_text.rs | 6 +- 6 files changed, 90 insertions(+), 43 deletions(-) diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index a30d18c53..5732d4ced 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -284,7 +284,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { None => vec![], }; - memo::Memo::run_all(format_string, &values[..]); + memo::Memo::run_all(format_string, &values[..])?; Ok(()) } diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 3646effc1..28524c55c 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -5,6 +5,7 @@ // TODO: Support -f flag // spell-checker:ignore (ToDO) istr chiter argptr ilen extendedbigdecimal extendedbigint numberparse use std::io::{stdout, ErrorKind, Write}; +use std::process::exit; use clap::{crate_version, App, AppSettings, Arg}; use num_traits::Zero; @@ -12,6 +13,7 @@ use num_traits::Zero; use uucore::error::FromIo; use uucore::error::UResult; use uucore::memo::Memo; +use uucore::show; mod error; mod extendedbigdecimal; @@ -287,7 +289,10 @@ fn print_seq( match format { Some(f) => { let s = format!("{}", value); - Memo::run_all(f, &[s]); + if let Err(x) = Memo::run_all(f, &[s]) { + show!(x); + exit(1); + } } None => write_value_float( &mut stdout, @@ -349,7 +354,10 @@ fn print_seq_integers( match format { Some(f) => { let s = format!("{}", value); - Memo::run_all(f, &[s]); + if let Err(x) = Memo::run_all(f, &[s]) { + show!(x); + exit(1); + } } None => write_value_int(&mut stdout, &value, padding, pad, is_first_iteration)?, } diff --git a/src/uucore/src/lib/features/memo.rs b/src/uucore/src/lib/features/memo.rs index fd57c33b5..0e3a6376c 100644 --- a/src/uucore/src/lib/features/memo.rs +++ b/src/uucore/src/lib/features/memo.rs @@ -6,6 +6,7 @@ //! that prints tokens. use crate::display::Quotable; +use crate::error::UResult; use crate::features::tokenize::sub::Sub; use crate::features::tokenize::token::{Token, Tokenizer}; use crate::features::tokenize::unescaped_text::UnescapedText; @@ -26,17 +27,17 @@ fn warn_excess_args(first_arg: &str) { } impl Memo { - pub fn new(pf_string: &str, pf_args_it: &mut Peekable>) -> Self { + pub fn new(pf_string: &str, pf_args_it: &mut Peekable>) -> UResult { let mut pm = Self { tokens: Vec::new() }; let mut tmp_token: Option>; let mut it = put_back_n(pf_string.chars()); let mut has_sub = false; loop { - tmp_token = UnescapedText::from_it(&mut it, pf_args_it); + tmp_token = UnescapedText::from_it(&mut it, pf_args_it)?; if let Some(x) = tmp_token { pm.tokens.push(x); } - tmp_token = Sub::from_it(&mut it, pf_args_it); + tmp_token = Sub::from_it(&mut it, pf_args_it)?; if let Some(x) = tmp_token { if !has_sub { has_sub = true; @@ -64,19 +65,19 @@ impl Memo { } } } - pm + Ok(pm) } pub fn apply(&self, pf_args_it: &mut Peekable>) { for tkn in &self.tokens { tkn.print(pf_args_it); } } - pub fn run_all(pf_string: &str, pf_args: &[String]) { + pub fn run_all(pf_string: &str, pf_args: &[String]) -> UResult<()> { let mut arg_it = pf_args.iter().peekable(); - let pm = Self::new(pf_string, &mut arg_it); + let pm = Self::new(pf_string, &mut arg_it)?; loop { if arg_it.peek().is_none() { - break; + return Ok(()); } pm.apply(&mut arg_it); } diff --git a/src/uucore/src/lib/features/tokenize/sub.rs b/src/uucore/src/lib/features/tokenize/sub.rs index ac471ae3e..a1c2f3807 100644 --- a/src/uucore/src/lib/features/tokenize/sub.rs +++ b/src/uucore/src/lib/features/tokenize/sub.rs @@ -5,7 +5,7 @@ //! it is created by Sub's implementation of the Tokenizer trait //! Subs which have numeric field chars make use of the num_format //! submodule -use crate::show_error; +use crate::error::{UResult, UUsageError}; use itertools::{put_back_n, PutBackN}; use std::iter::Peekable; use std::process::exit; @@ -20,11 +20,6 @@ use super::unescaped_text::UnescapedText; const EXIT_ERR: i32 = 1; -fn err_conv(sofar: &str) { - show_error!("%{}: invalid conversion specification", sofar); - exit(EXIT_ERR); -} - fn convert_asterisk_arg_int(asterisk_arg: &str) -> isize { // this is a costly way to parse the // args used for asterisk values into integers @@ -116,14 +111,14 @@ impl SubParser { fn from_it( it: &mut PutBackN, args: &mut Peekable>, - ) -> Option> { + ) -> UResult>> { let mut parser = Self::new(); - if parser.sub_vals_retrieved(it) { + if parser.sub_vals_retrieved(it)? { let t: Box = Self::build_token(parser); t.print(args); - Some(t) + Ok(Some(t)) } else { - None + Ok(None) } } fn build_token(parser: Self) -> Box { @@ -151,9 +146,9 @@ impl SubParser { )); t } - fn sub_vals_retrieved(&mut self, it: &mut PutBackN) -> bool { - if !Self::successfully_eat_prefix(it, &mut self.text_so_far) { - return false; + fn sub_vals_retrieved(&mut self, it: &mut PutBackN) -> UResult { + if !Self::successfully_eat_prefix(it, &mut self.text_so_far)? { + return Ok(false); } // this fn in particular is much longer than it needs to be // .could get a lot @@ -177,7 +172,10 @@ impl SubParser { '-' | '*' | '0'..='9' => { if !self.past_decimal { if self.min_width_is_asterisk || self.specifiers_found { - err_conv(&self.text_so_far); + return Err(UUsageError::new( + 1, + format!("%{}: invalid conversion specification", &self.text_so_far), + )); } if self.min_width_tmp.is_none() { self.min_width_tmp = Some(String::new()); @@ -185,7 +183,13 @@ impl SubParser { match self.min_width_tmp.as_mut() { Some(x) => { if (ch == '-' || ch == '*') && !x.is_empty() { - err_conv(&self.text_so_far); + return Err(UUsageError::new( + 1, + format!( + "%{}: invalid conversion specification", + &self.text_so_far + ), + )); } if ch == '*' { self.min_width_is_asterisk = true; @@ -200,7 +204,10 @@ impl SubParser { // second field should never have a // negative value if self.second_field_is_asterisk || ch == '-' || self.specifiers_found { - err_conv(&self.text_so_far); + return Err(UUsageError::new( + 1, + format!("%{}: invalid conversion specification", &self.text_so_far), + )); } if self.second_field_tmp.is_none() { self.second_field_tmp = Some(String::new()); @@ -208,7 +215,13 @@ impl SubParser { match self.second_field_tmp.as_mut() { Some(x) => { if ch == '*' && !x.is_empty() { - err_conv(&self.text_so_far); + return Err(UUsageError::new( + 1, + format!( + "%{}: invalid conversion specification", + &self.text_so_far + ), + )); } if ch == '*' { self.second_field_is_asterisk = true; @@ -225,7 +238,10 @@ impl SubParser { if !self.past_decimal { self.past_decimal = true; } else { - err_conv(&self.text_so_far); + return Err(UUsageError::new( + 1, + format!("%{}: invalid conversion specification", &self.text_so_far), + )); } } x if legal_fields.binary_search(&x).is_ok() => { @@ -242,18 +258,24 @@ impl SubParser { } } _ => { - err_conv(&self.text_so_far); + return Err(UUsageError::new( + 1, + format!("%{}: invalid conversion specification", &self.text_so_far), + )); } } } if self.field_char.is_none() { - err_conv(&self.text_so_far); + return Err(UUsageError::new( + 1, + format!("%{}: invalid conversion specification", &self.text_so_far), + )); } let field_char_retrieved = self.field_char.unwrap(); if self.past_decimal && self.second_field_tmp.is_none() { self.second_field_tmp = Some(String::from("0")); } - self.validate_field_params(field_char_retrieved); + self.validate_field_params(field_char_retrieved)?; // if the dot is provided without a second field // printf interprets it as 0. if let Some(x) = self.second_field_tmp.as_mut() { @@ -262,9 +284,12 @@ impl SubParser { } } - true + Ok(true) } - fn successfully_eat_prefix(it: &mut PutBackN, text_so_far: &mut String) -> bool { + fn successfully_eat_prefix( + it: &mut PutBackN, + text_so_far: &mut String, + ) -> UResult { // get next two chars, // if they're '%%' we're not tokenizing it // else put chars back @@ -274,12 +299,14 @@ impl SubParser { match n_ch { Some(x) => { it.put_back(x); - true + Ok(true) } None => { text_so_far.push('%'); - err_conv(text_so_far); - false + return Err(UUsageError::new( + 1, + format!("%{}: invalid conversion specification", &text_so_far[..]), + )); } } } else { @@ -289,10 +316,10 @@ impl SubParser { if let Some(x) = preface { it.put_back(x); }; - false + Ok(false) } } - fn validate_field_params(&self, field_char: char) { + fn validate_field_params(&self, field_char: char) -> UResult<()> { // check for illegal combinations here when possible vs // on each application so we check less per application // to do: move these checks to Sub::new @@ -304,8 +331,15 @@ impl SubParser { || self.past_decimal || self.second_field_tmp.is_some())) { - err_conv(&self.text_so_far); + // invalid string substitution + // to do: include information about an invalid + // string substitution + return Err(UUsageError::new( + 1, + format!("%{}: invalid conversion specification", &self.text_so_far), + )); } + Ok(()) } } @@ -313,7 +347,7 @@ impl token::Tokenizer for Sub { fn from_it( it: &mut PutBackN, args: &mut Peekable>, - ) -> Option> { + ) -> UResult>> { SubParser::from_it(it, args) } } diff --git a/src/uucore/src/lib/features/tokenize/token.rs b/src/uucore/src/lib/features/tokenize/token.rs index 1d8ee5ead..6a25b620f 100644 --- a/src/uucore/src/lib/features/tokenize/token.rs +++ b/src/uucore/src/lib/features/tokenize/token.rs @@ -4,6 +4,8 @@ use std::iter::Peekable; use std::slice::Iter; use std::str::Chars; +use crate::error::UResult; + // A token object is an object that can print the expected output // of a contiguous segment of the format string, and // requires at most 1 argument @@ -27,5 +29,5 @@ pub trait Tokenizer { fn from_it( it: &mut PutBackN, args: &mut Peekable>, - ) -> Option>; + ) -> UResult>>; } diff --git a/src/uucore/src/lib/features/tokenize/unescaped_text.rs b/src/uucore/src/lib/features/tokenize/unescaped_text.rs index 0ec721b48..d186dbc26 100644 --- a/src/uucore/src/lib/features/tokenize/unescaped_text.rs +++ b/src/uucore/src/lib/features/tokenize/unescaped_text.rs @@ -13,6 +13,8 @@ use std::process::exit; use std::slice::Iter; use std::str::Chars; +use crate::error::UResult; + use super::token; const EXIT_OK: i32 = 0; @@ -266,8 +268,8 @@ impl token::Tokenizer for UnescapedText { fn from_it( it: &mut PutBackN, _: &mut Peekable>, - ) -> Option> { - Self::from_it_core(it, false) + ) -> UResult>> { + Ok(Self::from_it_core(it, false)) } } impl token::Token for UnescapedText { From f04f22b0123505137091134c962bb02915b4ead1 Mon Sep 17 00:00:00 2001 From: ndd7xv Date: Sun, 13 Feb 2022 17:28:33 -0500 Subject: [PATCH 154/161] uucore(memo): refactor error propogation with new SubError enum --- src/uucore/src/lib/features/tokenize/sub.rs | 76 +++++++++------------ 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/src/uucore/src/lib/features/tokenize/sub.rs b/src/uucore/src/lib/features/tokenize/sub.rs index a1c2f3807..f8b5b5caf 100644 --- a/src/uucore/src/lib/features/tokenize/sub.rs +++ b/src/uucore/src/lib/features/tokenize/sub.rs @@ -5,8 +5,10 @@ //! it is created by Sub's implementation of the Tokenizer trait //! Subs which have numeric field chars make use of the num_format //! submodule -use crate::error::{UResult, UUsageError}; +use crate::error::{UError, UResult}; use itertools::{put_back_n, PutBackN}; +use std::error::Error; +use std::fmt::Display; use std::iter::Peekable; use std::process::exit; use std::slice::Iter; @@ -20,6 +22,23 @@ use super::unescaped_text::UnescapedText; const EXIT_ERR: i32 = 1; +#[derive(Debug)] +pub enum SubError { + InvalidSpec(String), +} + +impl Display for SubError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + Self::InvalidSpec(s) => write!(f, "%{}: invalid conversion specification", s), + } + } +} + +impl Error for SubError {} + +impl UError for SubError {} + fn convert_asterisk_arg_int(asterisk_arg: &str) -> isize { // this is a costly way to parse the // args used for asterisk values into integers @@ -172,10 +191,7 @@ impl SubParser { '-' | '*' | '0'..='9' => { if !self.past_decimal { if self.min_width_is_asterisk || self.specifiers_found { - return Err(UUsageError::new( - 1, - format!("%{}: invalid conversion specification", &self.text_so_far), - )); + return Err(SubError::InvalidSpec(self.text_so_far.clone()).into()); } if self.min_width_tmp.is_none() { self.min_width_tmp = Some(String::new()); @@ -183,13 +199,9 @@ impl SubParser { match self.min_width_tmp.as_mut() { Some(x) => { if (ch == '-' || ch == '*') && !x.is_empty() { - return Err(UUsageError::new( - 1, - format!( - "%{}: invalid conversion specification", - &self.text_so_far - ), - )); + return Err( + SubError::InvalidSpec(self.text_so_far.clone()).into() + ); } if ch == '*' { self.min_width_is_asterisk = true; @@ -204,10 +216,7 @@ impl SubParser { // second field should never have a // negative value if self.second_field_is_asterisk || ch == '-' || self.specifiers_found { - return Err(UUsageError::new( - 1, - format!("%{}: invalid conversion specification", &self.text_so_far), - )); + return Err(SubError::InvalidSpec(self.text_so_far.clone()).into()); } if self.second_field_tmp.is_none() { self.second_field_tmp = Some(String::new()); @@ -215,13 +224,9 @@ impl SubParser { match self.second_field_tmp.as_mut() { Some(x) => { if ch == '*' && !x.is_empty() { - return Err(UUsageError::new( - 1, - format!( - "%{}: invalid conversion specification", - &self.text_so_far - ), - )); + return Err( + SubError::InvalidSpec(self.text_so_far.clone()).into() + ); } if ch == '*' { self.second_field_is_asterisk = true; @@ -238,10 +243,7 @@ impl SubParser { if !self.past_decimal { self.past_decimal = true; } else { - return Err(UUsageError::new( - 1, - format!("%{}: invalid conversion specification", &self.text_so_far), - )); + return Err(SubError::InvalidSpec(self.text_so_far.clone()).into()); } } x if legal_fields.binary_search(&x).is_ok() => { @@ -258,18 +260,12 @@ impl SubParser { } } _ => { - return Err(UUsageError::new( - 1, - format!("%{}: invalid conversion specification", &self.text_so_far), - )); + return Err(SubError::InvalidSpec(self.text_so_far.clone()).into()); } } } if self.field_char.is_none() { - return Err(UUsageError::new( - 1, - format!("%{}: invalid conversion specification", &self.text_so_far), - )); + return Err(SubError::InvalidSpec(self.text_so_far.clone()).into()); } let field_char_retrieved = self.field_char.unwrap(); if self.past_decimal && self.second_field_tmp.is_none() { @@ -303,10 +299,7 @@ impl SubParser { } None => { text_so_far.push('%'); - return Err(UUsageError::new( - 1, - format!("%{}: invalid conversion specification", &text_so_far[..]), - )); + Err(SubError::InvalidSpec(text_so_far.clone()).into()) } } } else { @@ -334,10 +327,7 @@ impl SubParser { // invalid string substitution // to do: include information about an invalid // string substitution - return Err(UUsageError::new( - 1, - format!("%{}: invalid conversion specification", &self.text_so_far), - )); + return Err(SubError::InvalidSpec(self.text_so_far.clone()).into()); } Ok(()) } From 69a94e412bcb2f71772884bc11556720fa9bca8f Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 17 Feb 2022 22:24:41 +0100 Subject: [PATCH 155/161] docs: use rust libraries for downloading and unzipping tldr --- .../workspace.wordlist.txt | 1 + Cargo.lock | 302 ++++++++++++++++++ Cargo.toml | 2 + docs/.gitignore | 1 - docs/Makefile | 2 - src/bin/uudoc.rs | 103 +++--- 6 files changed, 364 insertions(+), 47 deletions(-) diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index e41aba979..99ac20ea2 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -44,6 +44,7 @@ termsize termwidth textwrap thiserror +ureq walkdir winapi xattr diff --git a/Cargo.lock b/Cargo.lock index 90b71d2a7..244c56baa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.4.7" @@ -73,6 +79,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bigdecimal" version = "0.3.0" @@ -167,6 +179,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + [[package]] name = "byte-unit" version = "4.0.13" @@ -228,6 +246,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "chunked_transfer" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" + [[package]] name = "clang-sys" version = "1.3.0" @@ -329,6 +353,7 @@ dependencies = [ "time", "unindent", "unix_socket", + "ureq", "users", "uu_arch", "uu_base32", @@ -432,6 +457,7 @@ dependencies = [ "uu_yes", "uucore", "walkdir", + "zip", ] [[package]] @@ -546,6 +572,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "crossbeam-channel" version = "0.5.2" @@ -792,12 +827,34 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + [[package]] name = "fs_extra" version = "1.2.0" @@ -927,6 +984,17 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "if_rust_version" version = "1.0.0" @@ -967,6 +1035,15 @@ dependencies = [ "either", ] +[[package]] +name = "js-sys" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "keccak" version = "0.1.0" @@ -1044,6 +1121,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "md5" version = "0.3.8" @@ -1089,6 +1172,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "mio" version = "0.7.14" @@ -1375,6 +1468,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + [[package]] name = "phf" version = "0.10.1" @@ -1655,6 +1754,21 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53552c6c49e1e13f1a203ef0080ab3bbef0beb570a528993e83df057a9d9bba1" +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "rlimit" version = "0.4.0" @@ -1681,6 +1795,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustls" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b323592e3164322f5b193dc4302e4e36cd8d37158a712d664efae1a5c2791700" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + [[package]] name = "same-file" version = "1.0.6" @@ -1696,6 +1822,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "selinux" version = "0.2.5" @@ -1838,6 +1974,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2009,6 +2151,21 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "toml" version = "0.5.8" @@ -2024,6 +2181,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + [[package]] name = "unicode-linebreak" version = "0.1.2" @@ -2033,6 +2196,15 @@ dependencies = [ "regex", ] +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.8.0" @@ -2073,6 +2245,41 @@ dependencies = [ "libc", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "ureq" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9399fa2f927a3d327187cbd201480cee55bee6ac5d3c77dd27f0c6814cff16d5" +dependencies = [ + "base64", + "chunked_transfer", + "flate2", + "log", + "once_cell", + "rustls", + "url", + "webpki", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + [[package]] name = "users" version = "0.10.0" @@ -3171,6 +3378,89 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wasm-bindgen" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote 1.0.14", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" +dependencies = [ + "quote 1.0.14", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" +dependencies = [ + "proc-macro2", + "quote 1.0.14", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" + +[[package]] +name = "web-sys" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449" +dependencies = [ + "webpki", +] + [[package]] name = "which" version = "4.2.2" @@ -3248,3 +3538,15 @@ name = "z85" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af896e93db81340b74b65f74276a99b210c086f3d34ed0abf433182a462af856" + +[[package]] +name = "zip" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +dependencies = [ + "byteorder", + "crc32fast", + "flate2", + "thiserror", +] diff --git a/Cargo.toml b/Cargo.toml index 336729813..4222f1749 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -252,6 +252,8 @@ lazy_static = { version="1.3" } textwrap = { version="0.14", features=["terminal_size"] } uucore = { version=">=0.0.11", package="uucore", path="src/uucore" } selinux = { version="0.2", optional = true } +ureq = "2.4.0" +zip = { version = "0.5.13", default_features=false, features=["deflate"] } # * uutils uu_test = { optional=true, version="0.0.12", package="uu_test", path="src/uu/test" } # diff --git a/docs/.gitignore b/docs/.gitignore index c836306f5..f2b5c7168 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,4 +1,3 @@ book src/utils src/SUMMARY.md -tldr/ diff --git a/docs/Makefile b/docs/Makefile index 23901b755..7372b3868 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,6 +1,4 @@ -# spell-checker:ignore tldr clean: rm -rf book rm -f src/SUMMARY.md rm -f src/utils/* - rm -rf tldr diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 5658f491c..9e075ccd4 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -7,27 +7,21 @@ use clap::App; use std::ffi::OsString; use std::fs::File; -use std::io::{self, Read, Write}; -use std::process::Command; +use std::io::Cursor; +use std::io::{self, Read, Seek, Write}; +use zip::ZipArchive; include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); fn main() -> io::Result<()> { - let _ = std::fs::create_dir("docs/tldr"); println!("Downloading tldr archive"); - Command::new("curl") - .arg("https://tldr.sh/assets/tldr.zip") - .arg("--output") - .arg("docs/tldr/tldr.zip") - .output()?; - - println!("Unzipping tldr archive"); - Command::new("unzip") - .arg("-o") - .arg("docs/tldr/tldr.zip") - .arg("-d") - .arg("docs/tldr") - .output()?; + let mut zip_reader = ureq::get("https://tldr.sh/assets/tldr.zip") + .call() + .unwrap() + .into_reader(); + let mut buffer = Vec::new(); + zip_reader.read_to_end(&mut buffer).unwrap(); + let mut tldr_zip = ZipArchive::new(Cursor::new(buffer)).unwrap(); let utils = util_map::>>(); match std::fs::create_dir("docs/src/utils/") { @@ -58,7 +52,7 @@ fn main() -> io::Result<()> { } let p = format!("docs/src/utils/{}.md", name); if let Ok(f) = File::create(&p) { - write_markdown(f, &mut app(), name)?; + write_markdown(f, &mut app(), name, &mut tldr_zip)?; println!("Wrote to '{}'", p); } else { println!("Error writing to {}", p); @@ -68,12 +62,17 @@ fn main() -> io::Result<()> { Ok(()) } -fn write_markdown(mut w: impl Write, app: &mut App, name: &str) -> io::Result<()> { +fn write_markdown( + mut w: impl Write, + app: &mut App, + name: &str, + tldr_zip: &mut zip::ZipArchive, +) -> io::Result<()> { write!(w, "# {}\n\n", name)?; write_version(&mut w, app)?; write_usage(&mut w, app, name)?; write_description(&mut w, app)?; - write_examples(&mut w, name)?; + write_examples(&mut w, name, tldr_zip)?; write_options(&mut w, app) } @@ -101,33 +100,49 @@ fn write_description(w: &mut impl Write, app: &App) -> io::Result<()> { } } -fn write_examples(w: &mut impl Write, name: &str) -> io::Result<()> { - if let Ok(mut file) = std::fs::File::open(format!("docs/tldr/pages/common/{}.md", name)) - .or_else(|_| std::fs::File::open(format!("docs/tldr/pages/linux/{}.md", name))) - { - let mut content = String::new(); - file.read_to_string(&mut content)?; - - writeln!(w, "## Examples")?; - writeln!(w)?; - for line in content.lines().skip_while(|l| !l.starts_with('-')) { - if let Some(l) = line.strip_prefix("- ") { - writeln!(w, "{}", l)?; - } else if line.starts_with('`') { - writeln!(w, "```shell\n{}\n```", line.trim_matches('`'))?; - } else if line.is_empty() { - writeln!(w)?; - } else { - println!("Not sure what to do with this line:"); - println!("{}", line); - } - } - writeln!(w)?; - writeln!(w, "> The examples are provided by the [tldr-pages project](https://tldr.sh) under the [CC BY 4.0 License](https://github.com/tldr-pages/tldr/blob/main/LICENSE.md).")?; +fn write_examples( + w: &mut impl Write, + name: &str, + tldr_zip: &mut zip::ZipArchive, +) -> io::Result<()> { + let content = if let Some(f) = get_zip_content(tldr_zip, &format!("pages/common/{}.md", name)) { + f + } else if let Some(f) = get_zip_content(tldr_zip, &format!("pages/linux/{}.md", name)) { + f } else { - println!("No examples found for: {}", name); + return Ok(()); + }; + + writeln!(w, "## Examples")?; + writeln!(w)?; + for line in content.lines().skip_while(|l| !l.starts_with('-')) { + if let Some(l) = line.strip_prefix("- ") { + writeln!(w, "{}", l)?; + } else if line.starts_with('`') { + writeln!(w, "```shell\n{}\n```", line.trim_matches('`'))?; + } else if line.is_empty() { + writeln!(w)?; + } else { + println!("Not sure what to do with this line:"); + println!("{}", line); + } } - Ok(()) + writeln!(w)?; + writeln!( + w, + "> The examples are provided by the [tldr-pages project](https://tldr.sh) under the [CC BY 4.0 License](https://github.com/tldr-pages/tldr/blob/main/LICENSE.md)." + )?; + writeln!(w, ">")?; + writeln!( + w, + "> Please note that, as uutils is a work in progress, some examples might fail." + ) +} + +fn get_zip_content(archive: &mut ZipArchive, name: &str) -> Option { + let mut s = String::new(); + archive.by_name(name).ok()?.read_to_string(&mut s).unwrap(); + Some(s) } fn write_options(w: &mut impl Write, app: &App) -> io::Result<()> { From 766c85fb5efa2ff2bd252c25f314925bc4730943 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Thu, 17 Feb 2022 22:35:30 -0500 Subject: [PATCH 156/161] dd: correct order and phrasing of truncated record Place the "truncated records" line below the "records out" line in the status report produced by `dd` and properly handle the singularization of the word "record" in the case of 1 truncated record. This matches the behavior of GNU `dd`. For example $ printf "ab" | dd cbs=1 conv=block status=noxfer > /dev/null 0+1 records in 0+1 records out 1 truncated record $ printf "ab\ncd\n" | dd cbs=1 conv=block status=noxfer > /dev/null 0+1 records in 0+1 records out 2 truncated records --- src/uu/dd/src/dd.rs | 8 +++++--- tests/by-util/test_dd.rs | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index b24e18049..b38671a9a 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -779,13 +779,15 @@ fn print_io_lines(update: &ProgUpdate) { "{}+{} records in", update.read_stat.reads_complete, update.read_stat.reads_partial ); - if update.read_stat.records_truncated > 0 { - eprintln!("{} truncated records", update.read_stat.records_truncated); - } eprintln!( "{}+{} records out", update.write_stat.writes_complete, update.write_stat.writes_partial ); + match update.read_stat.records_truncated { + 0 => {} + 1 => eprintln!("1 truncated record"), + n => eprintln!("{} truncated records", n), + } } // Print the progress line of a status update: // bytes (, ) copied,
") + writeln!(w, "\n") } From be0b77e7a73dded31deb35f48a8027285ec77cba Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 19 Feb 2022 12:47:29 +0100 Subject: [PATCH 159/161] when help item has an \n, translate it to a
example: https://uutils.github.io/coreutils-docs/user/utils/ls.html / classify --- src/bin/uudoc.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 4020b2787..33e5bf607 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -201,7 +201,11 @@ fn write_options(w: &mut impl Write, app: &App) -> io::Result<()> { write!(w, "")?; } writeln!(w, "")?; - writeln!(w, "
\n\n{}\n\n
", arg.get_help().unwrap_or_default())?; + writeln!( + w, + "
\n\n{}\n\n
", + arg.get_help().unwrap_or_default().replace("\n", "
") + )?; } writeln!(w, "\n") } From b09bae2acfebad4f21b5827b4f5ca3d44d0dbf93 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Wed, 2 Feb 2022 22:36:44 -0500 Subject: [PATCH 160/161] dd: collect progress reporting into its own module Collect structs, implementations, and functions that have to do with reporting number of blocks read and written into their own new module, `progress.rs`. This commit also adds docstrings for everything and unit tests for the significant methods. This commit does not change the behavior of `dd`, just the organization of the code to make it more maintainable and testable. --- src/uu/dd/src/datastructures.rs | 70 ----- src/uu/dd/src/dd.rs | 125 +------- src/uu/dd/src/parseargs.rs | 2 +- src/uu/dd/src/progress.rs | 517 ++++++++++++++++++++++++++++++++ 4 files changed, 524 insertions(+), 190 deletions(-) create mode 100644 src/uu/dd/src/progress.rs diff --git a/src/uu/dd/src/datastructures.rs b/src/uu/dd/src/datastructures.rs index 8380965a9..c9c89e858 100644 --- a/src/uu/dd/src/datastructures.rs +++ b/src/uu/dd/src/datastructures.rs @@ -7,72 +7,11 @@ // spell-checker:ignore ctable, outfile use std::error::Error; -use std::time; use uucore::error::UError; use crate::conversion_tables::*; -pub struct ProgUpdate { - pub read_stat: ReadStat, - pub write_stat: WriteStat, - pub duration: time::Duration, -} - -impl ProgUpdate { - pub(crate) fn new( - read_stat: ReadStat, - write_stat: WriteStat, - duration: time::Duration, - ) -> Self { - Self { - read_stat, - write_stat, - duration, - } - } -} - -#[derive(Clone, Copy, Default)] -pub struct ReadStat { - pub reads_complete: u64, - pub reads_partial: u64, - pub records_truncated: u32, -} - -impl ReadStat { - /// Whether this counter has zero complete reads and zero partial reads. - pub(crate) fn is_empty(&self) -> bool { - self.reads_complete == 0 && self.reads_partial == 0 - } -} - -impl std::ops::AddAssign for ReadStat { - fn add_assign(&mut self, other: Self) { - *self = Self { - reads_complete: self.reads_complete + other.reads_complete, - reads_partial: self.reads_partial + other.reads_partial, - records_truncated: self.records_truncated + other.records_truncated, - } - } -} - -#[derive(Clone, Copy, Default)] -pub struct WriteStat { - pub writes_complete: u64, - pub writes_partial: u64, - pub bytes_total: u128, -} -impl std::ops::AddAssign for WriteStat { - fn add_assign(&mut self, other: Self) { - *self = Self { - writes_complete: self.writes_complete + other.writes_complete, - writes_partial: self.writes_partial + other.writes_partial, - bytes_total: self.bytes_total + other.bytes_total, - } - } -} - type Cbs = usize; /// Stores all Conv Flags that apply to the input @@ -138,15 +77,6 @@ pub struct OFlags { pub seek_bytes: bool, } -/// The value of the status cl-option. -/// Controls printing of transfer stats -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum StatusLevel { - Progress, - Noxfer, - None, -} - /// The value of count=N /// Defaults to Reads(N) /// if iflag=count_bytes diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index b38671a9a..c8004b893 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -5,7 +5,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat seekable +// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, wlen, wstat seekable mod datastructures; use datastructures::*; @@ -16,27 +16,23 @@ use parseargs::Matches; mod conversion_tables; use conversion_tables::*; +mod progress; +use progress::{gen_prog_updater, ProgUpdate, ReadStat, StatusLevel, WriteStat}; + use std::cmp; use std::convert::TryInto; use std::env; -#[cfg(target_os = "linux")] -use std::error::Error; use std::fs::{File, OpenOptions}; use std::io::{self, Read, Seek, Write}; #[cfg(target_os = "linux")] use std::os::unix::fs::OpenOptionsExt; use std::path::Path; use std::sync::mpsc; -#[cfg(target_os = "linux")] -use std::sync::{atomic::AtomicUsize, atomic::Ordering, Arc}; use std::thread; use std::time; -use byte_unit::Byte; use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; use gcd::Gcd; -#[cfg(target_os = "linux")] -use signal_hook::consts::signal; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::show_error; @@ -351,8 +347,8 @@ where fn print_stats(&self, i: &Input, prog_update: &ProgUpdate) { match i.print_level { Some(StatusLevel::None) => {} - Some(StatusLevel::Noxfer) => print_io_lines(prog_update), - Some(StatusLevel::Progress) | None => print_transfer_stats(prog_update), + Some(StatusLevel::Noxfer) => prog_update.print_io_lines(), + Some(StatusLevel::Progress) | None => prog_update.print_transfer_stats(), } } @@ -771,115 +767,6 @@ fn read_helper(i: &mut Input, bsize: usize) -> std::io::Result<(Read } } -// Print io lines of a status update: -// + records in -// + records out -fn print_io_lines(update: &ProgUpdate) { - eprintln!( - "{}+{} records in", - update.read_stat.reads_complete, update.read_stat.reads_partial - ); - eprintln!( - "{}+{} records out", - update.write_stat.writes_complete, update.write_stat.writes_partial - ); - match update.read_stat.records_truncated { - 0 => {} - 1 => eprintln!("1 truncated record"), - n => eprintln!("{} truncated records", n), - } -} -// Print the progress line of a status update: -// bytes (, ) copied,