diff --git a/.github/workflows/GNU.yml b/.github/workflows/GNU.yml index 35efccbe5..a68f0a083 100644 --- a/.github/workflows/GNU.yml +++ b/.github/workflows/GNU.yml @@ -80,6 +80,9 @@ jobs: -e '/tests\/misc\/help-version-getopt.sh/ D' \ Makefile + # printf doesn't limit the values used in its arg, so this produced ~2GB of output + sed -i '/INT_OFLOW/ D' tests/misc/printf.sh + # Use the system coreutils where the test fails due to error in a util that is not the one being tested sed -i 's|stat|/usr/bin/stat|' tests/chgrp/basic.sh tests/cp/existing-perm-dir.sh tests/touch/60-seconds.sh tests/misc/sort-compress-proc.sh sed -i 's|ls -|/usr/bin/ls -|' tests/chgrp/posix-H.sh tests/chown/deref.sh tests/cp/same-file.sh tests/misc/mknod.sh tests/mv/part-symlink.sh tests/du/8gb.sh diff --git a/Cargo.lock b/Cargo.lock index d5dbf3508..d4aefd98c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "arrayvec" version = "0.4.12" @@ -127,9 +136,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cast" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0" +checksum = "cc38c385bfd7e444464011bb24820f40dd1c76bcdfa1b78611cb7c2e5cafab75" dependencies = [ "rustc_version", ] @@ -169,7 +178,7 @@ version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ - "ansi_term", + "ansi_term 0.11.0", "atty", "bitflags", "strsim", @@ -212,6 +221,7 @@ dependencies = [ "lazy_static", "libc", "nix 0.20.0", + "pretty_assertions", "rand 0.7.3", "regex", "sha1", @@ -452,9 +462,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -517,6 +527,16 @@ dependencies = [ "memchr 2.3.4", ] +[[package]] +name = "ctor" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" +dependencies = [ + "quote 1.0.9", + "syn", +] + [[package]] name = "custom_derive" version = "0.1.7" @@ -529,6 +549,12 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4f47ca1860a761136924ddd2422ba77b2ea54fe8cc75b9040804a0d9d32ad97" +[[package]] +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + [[package]] name = "digest" version = "0.6.2" @@ -580,7 +606,7 @@ checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.5", + "redox_syscall 0.2.6", "winapi 0.3.9", ] @@ -783,6 +809,15 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "lscolors" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b894c45c9da468621cdd615a5a79ee5e5523dd4f75c76ebc03d458940c16e" +dependencies = [ + "ansi_term 0.12.1", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -897,6 +932,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +[[package]] +name = "once_cell" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" + [[package]] name = "onig" version = "4.3.3" @@ -925,6 +966,15 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "output_vt100" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "paste" version = "0.1.18" @@ -994,6 +1044,18 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "pretty_assertions" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b" +dependencies = [ + "ansi_term 0.12.1", + "ctor", + "diff", + "output_vt100", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -1176,9 +1238,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041" dependencies = [ "bitflags", ] @@ -1189,14 +1251,14 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" dependencies = [ - "redox_syscall 0.2.5", + "redox_syscall 0.2.6", ] [[package]] name = "regex" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" +checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" dependencies = [ "aho-corasick", "memchr 2.3.4", @@ -1400,9 +1462,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" +checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883" dependencies = [ "proc-macro2", "quote 1.0.9", @@ -1460,7 +1522,7 @@ checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" dependencies = [ "libc", "numtoa", - "redox_syscall 0.2.5", + "redox_syscall 0.2.6", "redox_termios", ] @@ -1996,11 +2058,12 @@ dependencies = [ "clap", "globset", "lazy_static", + "lscolors", "number_prefix", + "once_cell", "term_grid", "termsize", "time", - "unicode-width", "uucore", "uucore_procs", ] @@ -2306,6 +2369,7 @@ dependencies = [ "serde_json", "smallvec 1.6.1", "tempdir", + "unicode-width", "uucore", "uucore_procs", ] diff --git a/Cargo.toml b/Cargo.toml index 7e3fb9139..7c1a771fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -335,6 +335,7 @@ filetime = "0.2" glob = "0.3.0" libc = "0.2" nix = "0.20.0" +pretty_assertions = "0.7.2" rand = "0.7" regex = "1.0" sha1 = { version="0.6", features=["std"] } diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 7d56a7485..e507c5acd 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -22,6 +22,12 @@ use std::io::{self, Read, Write}; use thiserror::Error; use uucore::fs::is_stdin_interactive; +/// Linux splice support +#[cfg(any(target_os = "linux", target_os = "android"))] +mod splice; +#[cfg(any(target_os = "linux", target_os = "android"))] +use std::os::unix::io::{AsRawFd, RawFd}; + /// Unix domain socket support #[cfg(unix)] use std::net::Shutdown; @@ -30,14 +36,6 @@ use std::os::unix::fs::FileTypeExt; #[cfg(unix)] use unix_socket::UnixStream; -/// Linux splice support -#[cfg(any(target_os = "linux", target_os = "android"))] -use nix::fcntl::{splice, SpliceFFlags}; -#[cfg(any(target_os = "linux", target_os = "android"))] -use nix::unistd::pipe; -#[cfg(any(target_os = "linux", target_os = "android"))] -use std::os::unix::io::{AsRawFd, RawFd}; - static NAME: &str = "cat"; static VERSION: &str = env!("CARGO_PKG_VERSION"); static SYNTAX: &str = "[OPTION]... [FILE]..."; @@ -395,7 +393,7 @@ fn write_fast(handle: &mut InputHandle) -> CatResult<()> { { // If we're on Linux or Android, try to use the splice() system call // for faster writing. If it works, we're done. - if !write_fast_using_splice(handle, stdout_lock.as_raw_fd())? { + if !splice::write_fast_using_splice(handle, stdout_lock.as_raw_fd())? { return Ok(()); } } @@ -411,75 +409,6 @@ fn write_fast(handle: &mut InputHandle) -> CatResult<()> { Ok(()) } -/// This function is called from `write_fast()` on Linux and Android. The -/// function `splice()` is used to move data between two file descriptors -/// without copying between kernel- and userspace. This results in a large -/// speedup. -/// -/// The `bool` in the result value indicates if we need to fall back to normal -/// copying or not. False means we don't have to. -#[cfg(any(target_os = "linux", target_os = "android"))] -#[inline] -fn write_fast_using_splice(handle: &mut InputHandle, writer: RawFd) -> CatResult { - const BUF_SIZE: usize = 1024 * 16; - - let (pipe_rd, pipe_wr) = pipe()?; - - // We only fall back if splice fails on the first call. - match splice( - handle.file_descriptor, - None, - pipe_wr, - None, - BUF_SIZE, - SpliceFFlags::empty(), - ) { - Ok(n) => { - if n == 0 { - return Ok(false); - } - splice_exact(pipe_rd, writer, n)?; - } - Err(_) => { - return Ok(true); - } - } - - loop { - let n = splice( - handle.file_descriptor, - None, - pipe_wr, - None, - BUF_SIZE, - SpliceFFlags::empty(), - )?; - if n == 0 { - // We read 0 bytes from the input, - // which means we're done copying. - break; - } - splice_exact(pipe_rd, writer, n)?; - } - - Ok(false) -} - -/// Splice wrapper which handles short writes -#[cfg(any(target_os = "linux", target_os = "android"))] -#[inline] -fn splice_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> { - let mut left = num_bytes; - loop { - let written = splice(read_fd, None, write_fd, None, left, SpliceFFlags::empty())?; - left -= written; - if left == 0 { - break; - } - } - Ok(()) -} - /// Outputs file contents to stdout in a line-by-line fashion, /// propagating any errors that might occur. fn write_lines( diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs new file mode 100644 index 000000000..ccc625467 --- /dev/null +++ b/src/uu/cat/src/splice.rs @@ -0,0 +1,91 @@ +use super::{CatResult, InputHandle}; + +use nix::fcntl::{splice, SpliceFFlags}; +use nix::unistd::{self, pipe}; +use std::io::Read; +use std::os::unix::io::RawFd; + +const BUF_SIZE: usize = 1024 * 16; + +/// This function is called from `write_fast()` on Linux and Android. The +/// function `splice()` is used to move data between two file descriptors +/// without copying between kernel- and userspace. This results in a large +/// speedup. +/// +/// The `bool` in the result value indicates if we need to fall back to normal +/// copying or not. False means we don't have to. +#[inline] +pub(super) fn write_fast_using_splice( + handle: &mut InputHandle, + write_fd: RawFd, +) -> CatResult { + let (pipe_rd, pipe_wr) = match pipe() { + Ok(r) => r, + Err(_) => { + // It is very rare that creating a pipe fails, but it can happen. + return Ok(true); + } + }; + + loop { + match splice( + handle.file_descriptor, + None, + pipe_wr, + None, + BUF_SIZE, + SpliceFFlags::empty(), + ) { + Ok(n) => { + if n == 0 { + return Ok(false); + } + if splice_exact(pipe_rd, write_fd, n).is_err() { + // If the first splice manages to copy to the intermediate + // pipe, but the second splice to stdout fails for some reason + // we can recover by copying the data that we have from the + // intermediate pipe to stdout using normal read/write. Then + // we tell the caller to fall back. + copy_exact(pipe_rd, write_fd, n)?; + return Ok(true); + } + } + Err(_) => { + return Ok(true); + } + } + } +} + +/// Splice wrapper which handles short writes. +#[inline] +fn splice_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> { + let mut left = num_bytes; + loop { + let written = splice(read_fd, None, write_fd, None, left, SpliceFFlags::empty())?; + left -= written; + if left == 0 { + break; + } + } + Ok(()) +} + +/// Caller must ensure that `num_bytes <= BUF_SIZE`, otherwise this function +/// will panic. The way we use this function in `write_fast_using_splice` +/// above is safe because `splice` is set to write at most `BUF_SIZE` to the +/// pipe. +#[inline] +fn copy_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> { + let mut left = num_bytes; + let mut buf = [0; BUF_SIZE]; + loop { + let read = unistd::read(read_fd, &mut buf[..left])?; + let written = unistd::write(write_fd, &mut buf[..read])?; + left -= written; + if left == 0 { + break; + } + } + Ok(()) +} diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 4e245b298..ca564e37c 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -155,7 +155,8 @@ pub enum OverwriteMode { NoClobber, } -#[derive(Clone, Eq, PartialEq)] +/// Possible arguments for `--reflink`. +#[derive(Copy, Clone, Eq, PartialEq)] pub enum ReflinkMode { Always, Auto, @@ -210,7 +211,6 @@ pub struct Options { overwrite: OverwriteMode, parents: bool, strip_trailing_slashes: bool, - reflink: bool, reflink_mode: ReflinkMode, preserve_attributes: Vec, recursive: bool, @@ -633,12 +633,12 @@ impl Options { update: matches.is_present(OPT_UPDATE), verbose: matches.is_present(OPT_VERBOSE), strip_trailing_slashes: matches.is_present(OPT_STRIP_TRAILING_SLASHES), - reflink: matches.is_present(OPT_REFLINK), reflink_mode: { if let Some(reflink) = matches.value_of(OPT_REFLINK) { match reflink { "always" => ReflinkMode::Always, "auto" => ReflinkMode::Auto, + "never" => ReflinkMode::Never, value => { return Err(Error::InvalidArgument(format!( "invalid argument '{}' for \'reflink\'", @@ -1193,47 +1193,20 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { Ok(()) } -///Copy the file from `source` to `dest` either using the normal `fs::copy` or the -///`FICLONE` ioctl if --reflink is specified and the filesystem supports it. +/// Copy the file from `source` to `dest` either using the normal `fs::copy` or a +/// copy-on-write scheme if --reflink is specified and the filesystem supports it. fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { - if options.reflink { - #[cfg(not(target_os = "linux"))] - return Err("--reflink is only supported on linux".to_string().into()); + if options.reflink_mode != ReflinkMode::Never { + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + return Err("--reflink is only supported on linux and macOS" + .to_string() + .into()); + + #[cfg(target_os = "macos")] + copy_on_write_macos(source, dest, options.reflink_mode)?; #[cfg(target_os = "linux")] - { - let src_file = File::open(source).unwrap().into_raw_fd(); - let dst_file = OpenOptions::new() - .write(true) - .truncate(false) - .create(true) - .open(dest) - .unwrap() - .into_raw_fd(); - match options.reflink_mode { - ReflinkMode::Always => unsafe { - let result = ficlone(dst_file, src_file as *const i32); - if result != 0 { - return Err(format!( - "failed to clone {:?} from {:?}: {}", - source, - dest, - std::io::Error::last_os_error() - ) - .into()); - } else { - return Ok(()); - } - }, - ReflinkMode::Auto => unsafe { - let result = ficlone(dst_file, src_file as *const i32); - if result != 0 { - fs::copy(source, dest).context(&*context_for(source, dest))?; - } - }, - ReflinkMode::Never => {} - } - } + copy_on_write_linux(source, dest, options.reflink_mode)?; } else if options.no_dereference && fs::symlink_metadata(&source)?.file_type().is_symlink() { // Here, we will copy the symlink itself (actually, just recreate it) let link = fs::read_link(&source)?; @@ -1266,6 +1239,101 @@ fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> Ok(()) } +/// Copies `source` to `dest` using copy-on-write if possible. +#[cfg(target_os = "linux")] +fn copy_on_write_linux(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyResult<()> { + debug_assert!(mode != ReflinkMode::Never); + + let src_file = File::open(source).unwrap().into_raw_fd(); + let dst_file = OpenOptions::new() + .write(true) + .truncate(false) + .create(true) + .open(dest) + .unwrap() + .into_raw_fd(); + match mode { + ReflinkMode::Always => unsafe { + let result = ficlone(dst_file, src_file as *const i32); + if result != 0 { + return Err(format!( + "failed to clone {:?} from {:?}: {}", + source, + dest, + std::io::Error::last_os_error() + ) + .into()); + } else { + return Ok(()); + } + }, + ReflinkMode::Auto => unsafe { + let result = ficlone(dst_file, src_file as *const i32); + if result != 0 { + fs::copy(source, dest).context(&*context_for(source, dest))?; + } + }, + ReflinkMode::Never => unreachable!(), + } + + Ok(()) +} + +/// Copies `source` to `dest` using copy-on-write if possible. +#[cfg(target_os = "macos")] +fn copy_on_write_macos(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyResult<()> { + debug_assert!(mode != ReflinkMode::Never); + + // Extract paths in a form suitable to be passed to a syscall. + // The unwrap() is safe because they come from the command-line and so contain non nul + // character. + use std::os::unix::ffi::OsStrExt; + let src = CString::new(source.as_os_str().as_bytes()).unwrap(); + let dst = CString::new(dest.as_os_str().as_bytes()).unwrap(); + + // clonefile(2) was introduced in macOS 10.12 so we cannot statically link against it + // for backward compatibility. + let clonefile = CString::new("clonefile").unwrap(); + let raw_pfn = unsafe { libc::dlsym(libc::RTLD_NEXT, clonefile.as_ptr()) }; + + let mut error = 0; + if !raw_pfn.is_null() { + // Call clonefile(2). + // Safety: Casting a C function pointer to a rust function value is one of the few + // blessed uses of `transmute()`. + unsafe { + let pfn: extern "C" fn( + src: *const libc::c_char, + dst: *const libc::c_char, + flags: u32, + ) -> libc::c_int = std::mem::transmute(raw_pfn); + error = pfn(src.as_ptr(), dst.as_ptr(), 0); + if std::io::Error::last_os_error().kind() == std::io::ErrorKind::AlreadyExists { + // clonefile(2) fails if the destination exists. Remove it and try again. Do not + // bother to check if removal worked because we're going to try to clone again. + let _ = fs::remove_file(dest); + error = pfn(src.as_ptr(), dst.as_ptr(), 0); + } + } + } + + if raw_pfn.is_null() || error != 0 { + // clonefile(2) is not supported or it error'ed out (possibly because the FS does not + // support COW). + match mode { + ReflinkMode::Always => { + return Err( + format!("failed to clone {:?} from {:?}: {}", source, dest, error).into(), + ) + } + ReflinkMode::Auto => fs::copy(source, dest).context(&*context_for(source, dest))?, + ReflinkMode::Never => unreachable!(), + }; + } + + Ok(()) +} + /// Generate an error message if `target` is not the correct `target_type` pub fn verify_target_type(target: &Path, target_type: &TargetType) -> CopyResult<()> { match (target_type, target.is_dir()) { diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index e01af5195..fa3b3c80a 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -500,7 +500,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { }; let strs = if matches.free.is_empty() { - vec!["./".to_owned()] + vec!["./".to_owned()] // TODO: gnu `du` doesn't use trailing "/" here } else { matches.free.clone() }; diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index a75ce45be..4ce665b80 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -41,6 +41,7 @@ pub struct Behavior { compare: bool, strip: bool, strip_program: String, + create_leading: bool, } #[derive(Clone, Eq, PartialEq)] @@ -70,7 +71,7 @@ static OPT_BACKUP: &str = "backup"; static OPT_BACKUP_2: &str = "backup2"; static OPT_DIRECTORY: &str = "directory"; static OPT_IGNORED: &str = "ignored"; -static OPT_CREATED: &str = "created"; +static OPT_CREATE_LEADING: &str = "create-leading"; static OPT_GROUP: &str = "group"; static OPT_MODE: &str = "mode"; static OPT_OWNER: &str = "owner"; @@ -133,9 +134,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .arg( // TODO implement flag - Arg::with_name(OPT_CREATED) + Arg::with_name(OPT_CREATE_LEADING) .short("D") - .help("(unimplemented) create all leading components of DEST except the last, then copy SOURCE to DEST") + .help("create all leading components of DEST except the last, then copy SOURCE to DEST") ) .arg( Arg::with_name(OPT_GROUP) @@ -266,8 +267,6 @@ fn check_unimplemented<'a>(matches: &ArgMatches) -> Result<(), &'a str> { Err("--backup") } else if matches.is_present(OPT_BACKUP_2) { Err("-b") - } else if matches.is_present(OPT_CREATED) { - Err("-D") } else if matches.is_present(OPT_SUFFIX) { Err("--suffix, -S") } else if matches.is_present(OPT_TARGET_DIRECTORY) { @@ -343,6 +342,7 @@ fn behavior(matches: &ArgMatches) -> Result { .value_of(OPT_STRIP_PROGRAM) .unwrap_or(DEFAULT_STRIP_PROGRAM), ), + create_leading: matches.is_present(OPT_CREATE_LEADING), }) } @@ -410,12 +410,35 @@ fn standard(paths: Vec, b: Behavior) -> i32 { .iter() .map(PathBuf::from) .collect::>(); + let target = Path::new(paths.last().unwrap()); - if (target.is_file() || is_new_file_path(target)) && sources.len() == 1 { - copy_file_to_file(&sources[0], &target.to_path_buf(), &b) - } else { + if sources.len() > 1 || (target.exists() && target.is_dir()) { copy_files_into_dir(sources, &target.to_path_buf(), &b) + } else { + if let Some(parent) = target.parent() { + if !parent.exists() && b.create_leading { + if let Err(e) = fs::create_dir_all(parent) { + show_error!("failed to create {}: {}", parent.display(), e); + return 1; + } + + if mode::chmod(&parent, b.mode()).is_err() { + show_error!("failed to chmod {}", parent.display()); + return 1; + } + } + } + + if target.is_file() || is_new_file_path(target) { + copy_file_to_file(&sources[0], &target.to_path_buf(), &b) + } else { + show_error!( + "invalid target {}: No such file or directory", + target.display() + ); + 1 + } } } diff --git a/src/uu/ls/BENCHMARKING.md b/src/uu/ls/BENCHMARKING.md new file mode 100644 index 000000000..84a0c3d84 --- /dev/null +++ b/src/uu/ls/BENCHMARKING.md @@ -0,0 +1,34 @@ +# Benchmarking ls + +ls majorly involves fetching a lot of details (depending upon what details are requested, eg. time/date, inode details, etc) for each path using system calls. Ideally, any system call should be done only once for each of the paths - not adhering to this principle leads to a lot of system call overhead multiplying and bubbling up, especially for recursive ls, therefore it is important to always benchmark multiple scenarios. +This is an overwiew over what was benchmarked, and if you make changes to `ls`, you are encouraged to check +how performance was affected for the workloads listed below. Feel free to add other workloads to the +list that we should improve / make sure not to regress. + +Run `cargo build --release` before benchmarking after you make a change! + +## Simple recursive ls + +- Get a large tree, for example linux kernel source tree. +- Benchmark simple recursive ls with hyperfine: `hyperfine --warmup 2 "target/release/coreutils ls -R tree > /dev/null"`. + +## Recursive ls with all and long options + +- Same tree as above +- Benchmark recursive ls with -al -R options with hyperfine: `hyperfine --warmup 2 "target/release/coreutils ls -al -R tree > /dev/null"`. + +## Comparing with GNU ls + +Hyperfine accepts multiple commands to run and will compare them. To compare performance with GNU ls +duplicate the string you passed to hyperfine but remove the `target/release/coreutils` bit from it. + +Example: `hyperfine --warmup 2 "target/release/coreutils ls -al -R tree > /dev/null"` becomes +`hyperfine --warmup 2 "target/release/coreutils ls -al -R tree > /dev/null" "ls -al -R tree > /dev/null"` +(This assumes GNU ls is installed as `ls`) + +This can also be used to compare with version of ls built before your changes to ensure your change does not regress this + +## Checking system call count + +- Another thing to look at would be system calls count using strace (on linux) or equivalent on other operating systems. +- Example: `strace -c target/release/coreutils ls -al -R tree` diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index dacdc7cd9..3787d3562 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -16,18 +16,19 @@ path = "src/ls.rs" [dependencies] clap = "2.33" -lazy_static = "1.0.1" number_prefix = "0.4" term_grid = "0.1.5" termsize = "0.1.6" time = "0.1.40" -unicode-width = "0.1.5" globset = "0.4.6" +lscolors = { version="0.7.1", features=["ansi_term"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } +once_cell = "1.7.2" +atty = "0.2" [target.'cfg(unix)'.dependencies] -atty = "0.2" +lazy_static = "1.4.0" [[bin]] name = "ls" diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 514539809..0351227eb 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -7,41 +7,41 @@ // spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf +#[macro_use] +extern crate uucore; #[cfg(unix)] #[macro_use] extern crate lazy_static; -#[macro_use] -extern crate uucore; mod quoting_style; mod version_cmp; use clap::{App, Arg}; use globset::{self, Glob, GlobSet, GlobSetBuilder}; +use lscolors::LsColors; use number_prefix::NumberPrefix; +use once_cell::unsync::OnceCell; use quoting_style::{escape_name, QuotingStyle}; -#[cfg(unix)] -use std::collections::HashMap; -use std::fs; -use std::fs::{DirEntry, FileType, Metadata}; -#[cfg(unix)] -use std::os::unix::fs::FileTypeExt; -#[cfg(any(unix, target_os = "redox"))] -use std::os::unix::fs::MetadataExt; #[cfg(windows)] use std::os::windows::fs::MetadataExt; -use std::path::{Path, PathBuf}; +use std::{ + cmp::Reverse, + fs::{self, DirEntry, FileType, Metadata}, + io::{stdout, BufWriter, Stdout, Write}, + path::{Path, PathBuf}, + process::exit, + time::{SystemTime, UNIX_EPOCH}, +}; #[cfg(unix)] -use std::time::Duration; -use std::time::{SystemTime, UNIX_EPOCH}; -use std::{cmp::Reverse, process::exit}; - +use std::{ + collections::HashMap, + os::unix::fs::{FileTypeExt, MetadataExt}, + time::Duration, +}; use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; use time::{strftime, Timespec}; #[cfg(unix)] -use unicode_width::UnicodeWidthStr; -#[cfg(unix)] -use uucore::libc::{mode_t, S_ISGID, S_ISUID, S_ISVTX, S_IWOTH, S_IXGRP, S_IXOTH, S_IXUSR}; +use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; static VERSION: &str = env!("CARGO_PKG_VERSION"); static ABOUT: &str = " @@ -54,30 +54,6 @@ fn get_usage() -> String { format!("{0} [OPTION]... [FILE]...", executable!()) } -#[cfg(unix)] -static DEFAULT_COLORS: &str = "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:"; - -#[cfg(unix)] -lazy_static! { - static ref LS_COLORS: String = - std::env::var("LS_COLORS").unwrap_or_else(|_| DEFAULT_COLORS.to_string()); - static ref COLOR_MAP: HashMap<&'static str, &'static str> = { - let codes = LS_COLORS.split(':'); - let mut map = HashMap::new(); - for c in codes { - let p: Vec<_> = c.splitn(2, '=').collect(); - if p.len() == 2 { - map.insert(p[0], p[1]); - } - } - map - }; - static ref RESET_CODE: &'static str = COLOR_MAP.get("rs").unwrap_or(&"0"); - static ref LEFT_CODE: &'static str = COLOR_MAP.get("lc").unwrap_or(&"\x1b["); - static ref RIGHT_CODE: &'static str = COLOR_MAP.get("rc").unwrap_or(&"m"); - static ref END_CODE: &'static str = COLOR_MAP.get("ec").unwrap_or(&""); -} - pub mod options { pub mod format { pub static ONELINE: &str = "1"; @@ -113,10 +89,8 @@ pub mod options { pub static C: &str = "quote-name"; } pub static QUOTING_STYLE: &str = "quoting-style"; - pub mod indicator_style { - pub static NONE: &str = "none"; - pub static SLASH: &str = "slash"; + pub static SLASH: &str = "p"; pub static FILE_TYPE: &str = "file-type"; pub static CLASSIFY: &str = "classify"; } @@ -135,9 +109,6 @@ pub mod options { pub static TIME: &str = "time"; pub static IGNORE_BACKUPS: &str = "ignore-backups"; pub static DIRECTORY: &str = "directory"; - pub static CLASSIFY: &str = "classify"; - pub static FILE_TYPE: &str = "file-type"; - pub static SLASH: &str = "p"; pub static INODE: &str = "inode"; pub static REVERSE: &str = "reverse"; pub static RECURSIVE: &str = "recursive"; @@ -212,8 +183,7 @@ struct Config { time: Time, #[cfg(unix)] inode: bool, - #[cfg(unix)] - color: bool, + color: Option, long: LongFormat, width: Option, quoting_style: QuotingStyle, @@ -337,8 +307,7 @@ impl Config { Time::Modification }; - #[cfg(unix)] - let color = match options.value_of(options::COLOR) { + let needs_color = match options.value_of(options::COLOR) { None => options.is_present(options::COLOR), Some(val) => match val { "" | "always" | "yes" | "force" => true, @@ -347,6 +316,12 @@ impl Config { }, }; + let color = if needs_color { + Some(LsColors::from_env().unwrap_or_default()) + } else { + None + }; + let size_format = if options.is_present(options::size::HUMAN_READABLE) { SizeFormat::Binary } else if options.is_present(options::size::SI) { @@ -448,19 +423,11 @@ impl Config { "slash" => IndicatorStyle::Slash, &_ => IndicatorStyle::None, } - } else if options.is_present(options::indicator_style::NONE) { - IndicatorStyle::None - } else if options.is_present(options::indicator_style::CLASSIFY) - || options.is_present(options::CLASSIFY) - { + } else if options.is_present(options::indicator_style::CLASSIFY) { IndicatorStyle::Classify - } else if options.is_present(options::indicator_style::SLASH) - || options.is_present(options::SLASH) - { + } else if options.is_present(options::indicator_style::SLASH) { IndicatorStyle::Slash - } else if options.is_present(options::indicator_style::FILE_TYPE) - || options.is_present(options::FILE_TYPE) - { + } else if options.is_present(options::indicator_style::FILE_TYPE) { IndicatorStyle::FileType } else { IndicatorStyle::None @@ -492,6 +459,10 @@ impl Config { } } + if files == Files::Normal { + ignore_patterns.add(Glob::new(".*").unwrap()); + } + let ignore_patterns = ignore_patterns.build().unwrap(); let dereference = if options.is_present(options::dereference::ALL) { @@ -520,7 +491,6 @@ impl Config { size_format, directory: options.is_present(options::DIRECTORY), time, - #[cfg(unix)] color, #[cfg(unix)] inode: options.is_present(options::INODE), @@ -983,45 +953,45 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .takes_value(true) .possible_values(&["none", "slash", "file-type", "classify"]) .overrides_with_all(&[ - options::FILE_TYPE, - options::SLASH, - options::CLASSIFY, + options::indicator_style::FILE_TYPE, + options::indicator_style::SLASH, + options::indicator_style::CLASSIFY, options::INDICATOR_STYLE, ])) .arg( - Arg::with_name(options::CLASSIFY) + Arg::with_name(options::indicator_style::CLASSIFY) .short("F") - .long(options::CLASSIFY) + .long(options::indicator_style::CLASSIFY) .help("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.") .overrides_with_all(&[ - options::FILE_TYPE, - options::SLASH, - options::CLASSIFY, + options::indicator_style::FILE_TYPE, + options::indicator_style::SLASH, + options::indicator_style::CLASSIFY, options::INDICATOR_STYLE, ]) ) .arg( - Arg::with_name(options::FILE_TYPE) - .long(options::FILE_TYPE) + Arg::with_name(options::indicator_style::FILE_TYPE) + .long(options::indicator_style::FILE_TYPE) .help("Same as --classify, but do not append '*'") .overrides_with_all(&[ - options::FILE_TYPE, - options::SLASH, - options::CLASSIFY, + options::indicator_style::FILE_TYPE, + options::indicator_style::SLASH, + options::indicator_style::CLASSIFY, options::INDICATOR_STYLE, ])) .arg( - Arg::with_name(options::SLASH) - .short(options::SLASH) + Arg::with_name(options::indicator_style::SLASH) + .short(options::indicator_style::SLASH) .help("Append / indicator to directories." ) .overrides_with_all(&[ - options::FILE_TYPE, - options::SLASH, - options::CLASSIFY, + options::indicator_style::FILE_TYPE, + options::indicator_style::SLASH, + options::indicator_style::CLASSIFY, options::INDICATOR_STYLE, ])) @@ -1038,12 +1008,82 @@ pub fn uumain(args: impl uucore::Args) -> i32 { list(locs, Config::from(matches)) } +/// 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 +struct PathData { + // Result got from symlink_metadata() or metadata() based on config + md: OnceCell>, + ft: OnceCell>, + // Name of the file - will be empty for . or .. + file_name: String, + // PathBuf that all above data corresponds to + p_buf: PathBuf, + must_dereference: bool, +} + +impl PathData { + fn new( + p_buf: PathBuf, + file_type: Option>, + config: &Config, + command_line: bool, + ) -> Self { + let name = p_buf + .file_name() + .map_or(String::new(), |s| s.to_string_lossy().into_owned()); + let must_dereference = match &config.dereference { + Dereference::All => true, + Dereference::Args => command_line, + Dereference::DirArgs => { + if command_line { + if let Ok(md) = p_buf.metadata() { + md.is_dir() + } else { + false + } + } else { + false + } + } + Dereference::None => false, + }; + let ft = match file_type { + Some(ft) => OnceCell::from(ft.ok()), + None => OnceCell::new(), + }; + + Self { + md: OnceCell::new(), + ft, + file_name: name, + p_buf, + must_dereference, + } + } + + fn md(&self) -> Option<&Metadata> { + self.md + .get_or_init(|| get_metadata(&self.p_buf, self.must_dereference).ok()) + .as_ref() + } + + fn file_type(&self) -> Option<&FileType> { + self.ft + .get_or_init(|| self.md().map(|md| md.file_type())) + .as_ref() + } +} + fn list(locs: Vec, config: Config) -> i32 { let number_of_locs = locs.len(); - let mut files = Vec::::new(); - let mut dirs = Vec::::new(); + let mut files = Vec::::new(); + let mut dirs = Vec::::new(); let mut has_failed = false; + + let mut out = BufWriter::new(stdout()); + for loc in locs { let p = PathBuf::from(&loc); if !p.exists() { @@ -1054,38 +1094,30 @@ fn list(locs: Vec, config: Config) -> i32 { continue; } - let show_dir_contents = if !config.directory { - match config.dereference { - Dereference::None => { - if let Ok(md) = p.symlink_metadata() { - md.is_dir() - } else { - show_error!("'{}': {}", &loc, "No such file or directory"); - has_failed = true; - continue; - } - } - _ => p.is_dir(), - } + let path_data = PathData::new(p, None, &config, true); + + let show_dir_contents = if let Some(ft) = path_data.file_type() { + !config.directory && ft.is_dir() } else { + has_failed = true; false }; if show_dir_contents { - dirs.push(p); + dirs.push(path_data); } else { - files.push(p); + files.push(path_data); } } sort_entries(&mut files, &config); - display_items(&files, None, &config, true); + display_items(&files, None, &config, &mut out); sort_entries(&mut dirs, &config); for dir in dirs { if number_of_locs > 1 { - println!("\n{}:", dir.to_string_lossy()); + let _ = writeln!(out, "\n{}:", dir.p_buf.display()); } - enter_directory(&dir, &config); + enter_directory(&dir, &config, &mut out); } if has_failed { 1 @@ -1094,22 +1126,21 @@ fn list(locs: Vec, config: Config) -> i32 { } } -fn sort_entries(entries: &mut Vec, config: &Config) { +fn sort_entries(entries: &mut Vec, config: &Config) { match config.sort { Sort::Time => entries.sort_by_key(|k| { Reverse( - get_metadata(k, false) - .ok() + k.md() .and_then(|md| get_system_time(&md, config)) .unwrap_or(UNIX_EPOCH), ) }), Sort::Size => { - entries.sort_by_key(|k| Reverse(get_metadata(k, false).map(|md| md.len()).unwrap_or(0))) + entries.sort_by_key(|k| Reverse(k.md().as_ref().map(|md| md.len()).unwrap_or(0))) } // The default sort in GNU ls is case insensitive - Sort::Name => entries.sort_by_key(|k| k.to_string_lossy().to_lowercase()), - Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(a, b)), + Sort::Name => entries.sort_by_cached_key(|k| k.file_name.to_lowercase()), + Sort::Version => entries.sort_by(|k, j| version_cmp::version_cmp(&k.p_buf, &j.p_buf)), Sort::None => {} } @@ -1122,48 +1153,53 @@ fn sort_entries(entries: &mut Vec, config: &Config) { fn is_hidden(file_path: &DirEntry) -> bool { let metadata = fs::metadata(file_path.path()).unwrap(); let attr = metadata.file_attributes(); - ((attr & 0x2) > 0) || file_path.file_name().to_string_lossy().starts_with('.') -} - -#[cfg(unix)] -fn is_hidden(file_path: &DirEntry) -> bool { - file_path.file_name().to_string_lossy().starts_with('.') + (attr & 0x2) > 0 } fn should_display(entry: &DirEntry, config: &Config) -> bool { let ffi_name = entry.file_name(); - if config.files == Files::Normal && is_hidden(entry) { - return false; + // For unix, the hidden files are already included in the ignore pattern + #[cfg(windows)] + { + if config.files == Files::Normal && is_hidden(entry) { + return false; + } } - if config.ignore_patterns.is_match(&ffi_name) { - return false; - } - true + !config.ignore_patterns.is_match(&ffi_name) } -fn enter_directory(dir: &Path, config: &Config) { - let mut entries: Vec<_> = safe_unwrap!(fs::read_dir(dir).and_then(Iterator::collect)); - - entries.retain(|e| should_display(e, config)); - - let mut entries: Vec<_> = entries.iter().map(DirEntry::path).collect(); - sort_entries(&mut entries, config); - - if config.files == Files::All { - let mut display_entries = entries.clone(); - display_entries.insert(0, dir.join("..")); - display_entries.insert(0, dir.join(".")); - display_items(&display_entries, Some(dir), config, false); +fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter) { + let mut entries: Vec<_> = if config.files == Files::All { + vec![ + PathData::new(dir.p_buf.join("."), None, config, false), + PathData::new(dir.p_buf.join(".."), None, config, false), + ] } else { - display_items(&entries, Some(dir), config, false); - } + vec![] + }; + + let mut temp: Vec<_> = safe_unwrap!(fs::read_dir(&dir.p_buf)) + .map(|res| safe_unwrap!(res)) + .filter(|e| should_display(e, config)) + .map(|e| PathData::new(DirEntry::path(&e), Some(e.file_type()), config, false)) + .collect(); + + sort_entries(&mut temp, config); + + entries.append(&mut temp); + + display_items(&entries, Some(&dir.p_buf), config, out); if config.recursive { - for e in entries.iter().filter(|p| p.is_dir()) { - println!("\n{}:", e.to_string_lossy()); - enter_directory(&e, config); + for e in entries + .iter() + .skip(if config.files == Files::All { 2 } else { 0 }) + .filter(|p| p.file_type().map(|ft| ft.is_dir()).unwrap_or(false)) + { + let _ = writeln!(out, "\n{}:", e.p_buf.display()); + enter_directory(&e, config, out); } } } @@ -1176,8 +1212,8 @@ fn get_metadata(entry: &Path, dereference: bool) -> std::io::Result { } } -fn display_dir_entry_size(entry: &Path, config: &Config) -> (usize, usize) { - if let Ok(md) = get_metadata(entry, false) { +fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize) { + if let Some(md) = entry.md() { ( display_symlink_count(&md).len(), display_file_size(&md, config).len(), @@ -1191,7 +1227,12 @@ fn pad_left(string: String, count: usize) -> String { format!("{:>width$}", string, width = count) } -fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config, command_line: bool) { +fn display_items( + items: &[PathData], + strip: Option<&Path>, + config: &Config, + out: &mut BufWriter, +) { if config.format == Format::Long { let (mut max_links, mut max_size) = (1, 1); for item in items { @@ -1200,58 +1241,59 @@ fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config, comma max_size = size.max(max_size); } for item in items { - display_item_long(item, strip, max_links, max_size, config, command_line); + display_item_long(item, strip, max_links, max_size, config, out); } } else { - let names = items.iter().filter_map(|i| { - let md = get_metadata(i, false); - match md { - Err(e) => { - let filename = get_file_name(i, strip); - show_error!("'{}': {}", filename, e); - None - } - Ok(md) => Some(display_file_name(&i, strip, &md, config)), - } - }); + let names = items + .iter() + .filter_map(|i| display_file_name(&i, strip, config)); match (&config.format, config.width) { - (Format::Columns, Some(width)) => display_grid(names, width, Direction::TopToBottom), - (Format::Across, Some(width)) => display_grid(names, width, Direction::LeftToRight), + (Format::Columns, Some(width)) => { + display_grid(names, width, Direction::TopToBottom, out) + } + (Format::Across, Some(width)) => { + display_grid(names, width, Direction::LeftToRight, out) + } (Format::Commas, width_opt) => { let term_width = width_opt.unwrap_or(1); let mut current_col = 0; let mut names = names; if let Some(name) = names.next() { - print!("{}", name.contents); + let _ = write!(out, "{}", name.contents); current_col = name.width as u16 + 2; } for name in names { let name_width = name.width as u16; if current_col + name_width + 1 > term_width { current_col = name_width + 2; - print!(",\n{}", name.contents); + let _ = write!(out, ",\n{}", name.contents); } else { current_col += name_width + 2; - print!(", {}", name.contents); + let _ = write!(out, ", {}", name.contents); } } // Current col is never zero again if names have been printed. // So we print a newline. if current_col > 0 { - println!(); + let _ = writeln!(out,); } } _ => { for name in names { - println!("{}", name.contents); + let _ = writeln!(out, "{}", name.contents); } } } } } -fn display_grid(names: impl Iterator, width: u16, direction: Direction) { +fn display_grid( + names: impl Iterator, + width: u16, + direction: Direction, + out: &mut BufWriter, +) { let mut grid = Grid::new(GridOptions { filling: Filling::Spaces(2), direction, @@ -1262,56 +1304,44 @@ fn display_grid(names: impl Iterator, width: u16, direction: Direct } match grid.fit_into_width(width as usize) { - Some(output) => print!("{}", output), + Some(output) => { + let _ = write!(out, "{}", output); + } // Width is too small for the grid, so we fit it in one column - None => print!("{}", grid.fit_into_columns(1)), + None => { + let _ = write!(out, "{}", grid.fit_into_columns(1)); + } } } use uucore::fs::display_permissions; fn display_item_long( - item: &Path, + item: &PathData, strip: Option<&Path>, max_links: usize, max_size: usize, config: &Config, - command_line: bool, + out: &mut BufWriter, ) { - let dereference = match &config.dereference { - Dereference::All => true, - Dereference::Args => command_line, - Dereference::DirArgs => { - if command_line { - if let Ok(md) = item.metadata() { - md.is_dir() - } else { - false - } - } else { - false - } - } - Dereference::None => false, - }; - - let md = match get_metadata(item, dereference) { - Err(e) => { - let filename = get_file_name(&item, strip); - show_error!("{}: {}", filename, e); + let md = match item.md() { + None => { + let filename = get_file_name(&item.p_buf, strip); + show_error!("could not show file: {}", filename); return; } - Ok(md) => md, + Some(md) => md, }; #[cfg(unix)] { if config.inode { - print!("{} ", get_inode(&md)); + let _ = write!(out, "{} ", get_inode(&md)); } } - print!( + let _ = write!( + out, "{}{} {}", display_file_type(md.file_type()), display_permissions(&md), @@ -1319,24 +1349,28 @@ fn display_item_long( ); if config.long.owner { - print!(" {}", display_uname(&md, config)); + let _ = write!(out, " {}", display_uname(&md, config)); } if config.long.group { - print!(" {}", display_group(&md, config)); + let _ = write!(out, " {}", display_group(&md, config)); } // 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 { - print!(" {}", display_uname(&md, config)); + let _ = write!(out, " {}", display_uname(&md, config)); } - println!( + let _ = writeln!( + out, " {} {} {}", pad_left(display_file_size(&md, config), max_size), display_date(&md, config), - display_file_name(&item, strip, &md, config).contents, + // unwrap is fine because it fails when metadata is not available + // but we already know that it is because it's checked at the + // start of the function. + display_file_name(&item, strip, config).unwrap().contents, ); } @@ -1348,23 +1382,51 @@ fn get_inode(metadata: &Metadata) -> String { // Currently getpwuid is `linux` target only. If it's broken out into // a posix-compliant attribute this can be updated... #[cfg(unix)] +use std::sync::Mutex; +#[cfg(unix)] use uucore::entries; +#[cfg(unix)] +fn cached_uid2usr(uid: u32) -> String { + lazy_static! { + static ref UID_CACHE: Mutex> = Mutex::new(HashMap::new()); + } + + let mut uid_cache = UID_CACHE.lock().unwrap(); + uid_cache + .entry(uid) + .or_insert_with(|| entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string())) + .clone() +} + #[cfg(unix)] fn display_uname(metadata: &Metadata, config: &Config) -> String { if config.long.numeric_uid_gid { metadata.uid().to_string() } else { - entries::uid2usr(metadata.uid()).unwrap_or_else(|_| metadata.uid().to_string()) + cached_uid2usr(metadata.uid()) } } +#[cfg(unix)] +fn cached_gid2grp(gid: u32) -> String { + lazy_static! { + static ref GID_CACHE: Mutex> = Mutex::new(HashMap::new()); + } + + let mut gid_cache = GID_CACHE.lock().unwrap(); + gid_cache + .entry(gid) + .or_insert_with(|| entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string())) + .clone() +} + #[cfg(unix)] fn display_group(metadata: &Metadata, config: &Config) -> String { if config.long.numeric_uid_gid { metadata.gid().to_string() } else { - entries::gid2grp(metadata.gid()).unwrap_or_else(|_| metadata.gid().to_string()) + cached_gid2grp(metadata.gid()) } } @@ -1374,7 +1436,6 @@ fn display_uname(_metadata: &Metadata, _config: &Config) -> String { } #[cfg(not(unix))] -#[allow(unused_variables)] fn display_group(_metadata: &Metadata, _config: &Config) -> String { "somegroup".to_string() } @@ -1449,13 +1510,13 @@ fn display_file_size(metadata: &Metadata, config: &Config) -> String { } } -fn display_file_type(file_type: FileType) -> String { +fn display_file_type(file_type: FileType) -> char { if file_type.is_dir() { - "d".to_string() + 'd' } else if file_type.is_symlink() { - "l".to_string() + 'l' } else { - "-".to_string() + '-' } } @@ -1470,140 +1531,58 @@ fn get_file_name(name: &Path, strip: Option<&Path>) -> String { name.to_string_lossy().into_owned() } -#[cfg(not(unix))] -fn display_file_name( - path: &Path, - strip: Option<&Path>, - metadata: &Metadata, - config: &Config, -) -> Cell { - let mut name = escape_name(get_file_name(path, strip), &config.quoting_style); - let file_type = metadata.file_type(); - - match config.indicator_style { - IndicatorStyle::Classify | IndicatorStyle::FileType => { - if file_type.is_dir() { - name.push('/'); - } - if file_type.is_symlink() { - name.push('@'); - } - } - IndicatorStyle::Slash => { - if file_type.is_dir() { - name.push('/'); - } - } - _ => (), - }; - - if config.format == Format::Long && metadata.file_type().is_symlink() { - if let Ok(target) = path.read_link() { - // We don't bother updating width here because it's not used for long listings - let target_name = target.to_string_lossy().to_string(); - name.push_str(" -> "); - name.push_str(&target_name); - } - } - - name.into() +#[cfg(unix)] +fn file_is_executable(md: &Metadata) -> bool { + // Mode always returns u32, but the flags might not be, based on the platform + // e.g. linux has u32, mac has u16. + // S_IXUSR -> user has execute permission + // S_IXGRP -> group has execute persmission + // S_IXOTH -> other users have execute permission + md.mode() & ((S_IXUSR | S_IXGRP | S_IXOTH) as u32) != 0 } -#[cfg(unix)] -fn color_name(name: String, typ: &str) -> String { - let mut typ = typ; - if !COLOR_MAP.contains_key(typ) { - if typ == "or" { - typ = "ln"; - } else if typ == "mi" { - typ = "fi"; - } - }; - if let Some(code) = COLOR_MAP.get(typ) { - format!( - "{}{}{}{}{}{}{}{}", - *LEFT_CODE, code, *RIGHT_CODE, name, *END_CODE, *LEFT_CODE, *RESET_CODE, *RIGHT_CODE, - ) +#[allow(clippy::clippy::collapsible_else_if)] +fn classify_file(path: &PathData) -> Option { + let file_type = path.file_type()?; + + if file_type.is_dir() { + Some('/') + } else if file_type.is_symlink() { + Some('@') } else { - name - } -} - -#[cfg(unix)] -macro_rules! has { - ($mode:expr, $perm:expr) => { - $mode & ($perm as mode_t) != 0 - }; -} - -#[cfg(unix)] -#[allow(clippy::cognitive_complexity)] -fn display_file_name( - path: &Path, - strip: Option<&Path>, - metadata: &Metadata, - config: &Config, -) -> Cell { - let mut name = escape_name(get_file_name(path, strip), &config.quoting_style); - if config.format != Format::Long && config.inode { - name = get_inode(metadata) + " " + &name; - } - let mut width = UnicodeWidthStr::width(&*name); - - let ext; - if config.color || config.indicator_style != IndicatorStyle::None { - let file_type = metadata.file_type(); - - let (code, sym) = if file_type.is_dir() { - ("di", Some('/')) - } else if file_type.is_symlink() { - if path.exists() { - ("ln", Some('@')) - } else { - ("or", Some('@')) - } - } else if file_type.is_socket() { - ("so", Some('=')) - } else if file_type.is_fifo() { - ("pi", Some('|')) - } else if file_type.is_block_device() { - ("bd", None) - } else if file_type.is_char_device() { - ("cd", None) - } else if file_type.is_file() { - let mode = metadata.mode() as mode_t; - let sym = if has!(mode, S_IXUSR | S_IXGRP | S_IXOTH) { + #[cfg(unix)] + { + if file_type.is_socket() { + Some('=') + } else if file_type.is_fifo() { + Some('|') + } else if file_type.is_file() && file_is_executable(path.md()?) { Some('*') } else { None - }; - if has!(mode, S_ISUID) { - ("su", sym) - } else if has!(mode, S_ISGID) { - ("sg", sym) - } else if has!(mode, S_ISVTX) && has!(mode, S_IWOTH) { - ("tw", sym) - } else if has!(mode, S_ISVTX) { - ("st", sym) - } else if has!(mode, S_IWOTH) { - ("ow", sym) - } else if has!(mode, S_IXUSR | S_IXGRP | S_IXOTH) { - ("ex", sym) - } else if metadata.nlink() > 1 { - ("mh", sym) - } else if let Some(e) = path.extension() { - ext = format!("*.{}", e.to_string_lossy()); - (ext.as_str(), None) - } else { - ("fi", None) } - } else { - ("", None) - }; - - if config.color { - name = color_name(name, code); } + #[cfg(not(unix))] + None + } +} + +fn display_file_name(path: &PathData, strip: Option<&Path>, config: &Config) -> Option { + let mut name = escape_name(get_file_name(&path.p_buf, strip), &config.quoting_style); + + #[cfg(unix)] + { + if config.format != Format::Long && config.inode { + name = get_inode(path.md()?) + " " + &name; + } + } + + if let Some(ls_colors) = &config.color { + name = color_name(&ls_colors, &path.p_buf, name, path.md()?); + } + + if config.indicator_style != IndicatorStyle::None { + let sym = classify_file(path); let char_opt = match config.indicator_style { IndicatorStyle::Classify => sym, @@ -1626,23 +1605,23 @@ fn display_file_name( if let Some(c) = char_opt { name.push(c); - width += 1; } } - if config.format == Format::Long && metadata.file_type().is_symlink() { - if let Ok(target) = path.read_link() { - // We don't bother updating width here because it's not used for long listings - let code = if target.exists() { "fi" } else { "mi" }; - let target_name = color_name(target.to_string_lossy().to_string(), code); + if config.format == Format::Long && path.file_type()?.is_symlink() { + if let Ok(target) = path.p_buf.read_link() { name.push_str(" -> "); - name.push_str(&target_name); + name.push_str(&target.to_string_lossy()); } } - Cell { - contents: name, - width, + Some(name.into()) +} + +fn color_name(ls_colors: &LsColors, path: &Path, name: String, md: &Metadata) -> String { + match ls_colors.style_for_path_with_metadata(path, Some(&md)) { + Some(style) => style.to_ansi_term_style().paint(name).to_string(), + None => name, } } diff --git a/src/uu/ls/src/quoting_style.rs b/src/uu/ls/src/quoting_style.rs index ceb54466c..49456fc22 100644 --- a/src/uu/ls/src/quoting_style.rs +++ b/src/uu/ls/src/quoting_style.rs @@ -1,6 +1,6 @@ use std::char::from_digit; -const SPECIAL_SHELL_CHARS: &str = "~`#$&*()\\|[]{};'\"<>?! "; +const SPECIAL_SHELL_CHARS: &str = "~`#$&*()|[]{};\\'\"<>?! "; pub(super) enum QuotingStyle { Shell { @@ -27,12 +27,10 @@ pub(super) enum Quotes { // This implementation is heavily inspired by the std::char::EscapeDefault implementation // in the Rust standard library. This custom implementation is needed because the // characters \a, \b, \e, \f & \v are not recognized by Rust. -#[derive(Clone, Debug)] struct EscapedChar { state: EscapeState, } -#[derive(Clone, Debug)] enum EscapeState { Done, Char(char), @@ -41,14 +39,12 @@ enum EscapeState { Octal(EscapeOctal), } -#[derive(Clone, Debug)] struct EscapeOctal { c: char, state: EscapeOctalState, idx: usize, } -#[derive(Clone, Debug)] enum EscapeOctalState { Done, Backslash, @@ -135,7 +131,6 @@ impl EscapedChar { '\x0B' => Backslash('v'), '\x0C' => Backslash('f'), '\r' => Backslash('r'), - '\\' => Backslash('\\'), '\x00'..='\x1F' | '\x7F' => Octal(EscapeOctal::from(c)), '\'' => match quotes { Quotes::Single => Backslash('\''), @@ -511,6 +506,23 @@ mod tests { ], ); + // A control character followed by a special shell character + check_names( + "one\n&two", + vec![ + ("one?&two", "literal"), + ("one\n&two", "literal-show"), + ("one\\n&two", "escape"), + ("\"one\\n&two\"", "c"), + ("'one?&two'", "shell"), + ("'one\n&two'", "shell-show"), + ("'one?&two'", "shell-always"), + ("'one\n&two'", "shell-always-show"), + ("'one'$'\\n''&two'", "shell-escape"), + ("'one'$'\\n''&two'", "shell-escape-always"), + ], + ); + // The first 16 control characters. NUL is also included, even though it is of // no importance for file names. check_names( @@ -627,4 +639,22 @@ mod tests { ], ); } + + #[test] + fn test_backslash() { + // Escaped in C-style, but not in Shell-style escaping + check_names( + "one\\two", + vec![ + ("one\\two", "literal"), + ("one\\two", "literal-show"), + ("one\\\\two", "escape"), + ("\"one\\\\two\"", "c"), + ("'one\\two'", "shell"), + ("\'one\\two\'", "shell-always"), + ("'one\\two'", "shell-escape"), + ("'one\\two'", "shell-escape-always"), + ], + ); + } } diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 1343501bb..98404f89f 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -7,8 +7,6 @@ // spell-checker:ignore (ToDO) parsemode makedev sysmacros makenod newmode perror IFBLK IFCHR IFIFO -mod parsemode; - #[macro_use] extern crate uucore; @@ -98,7 +96,7 @@ for details about the options it supports.", let mut last_umask: mode_t = 0; let mut newmode: mode_t = MODE_RW_UGO; if matches.opt_present("mode") { - match parsemode::parse_mode(matches.opt_str("mode")) { + match uucore::mode::parse_mode(matches.opt_str("mode")) { Ok(parsed) => { if parsed > 0o777 { show_info!("mode must specify only file permission bits"); diff --git a/src/uu/mknod/src/parsemode.rs b/src/uu/mknod/src/parsemode.rs deleted file mode 100644 index 8f8f9af61..000000000 --- a/src/uu/mknod/src/parsemode.rs +++ /dev/null @@ -1,58 +0,0 @@ -// spell-checker:ignore (ToDO) fperm - -use libc::{mode_t, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR}; - -use uucore::mode; - -pub fn parse_mode(mode: Option) -> Result { - let fperm = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; - if let Some(mode) = mode { - let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; - let result = if mode.contains(arr) { - mode::parse_numeric(fperm as u32, mode.as_str()) - } else { - mode::parse_symbolic(fperm as u32, mode.as_str(), true) - }; - result.map(|mode| mode as mode_t) - } else { - Ok(fperm) - } -} - -#[cfg(test)] -mod test { - /// Test if the program is running under WSL - // ref: @@ - // ToDO: test on WSL2 which likely doesn't need special handling; plan change to `is_wsl_1()` if WSL2 is less needy - pub fn is_wsl() -> bool { - #[cfg(target_os = "linux")] - { - if let Ok(b) = std::fs::read("/proc/sys/kernel/osrelease") { - if let Ok(s) = std::str::from_utf8(&b) { - let a = s.to_ascii_lowercase(); - return a.contains("microsoft") || a.contains("wsl"); - } - } - } - false - } - - #[test] - fn symbolic_modes() { - assert_eq!(super::parse_mode(Some("u+x".to_owned())).unwrap(), 0o766); - assert_eq!( - super::parse_mode(Some("+x".to_owned())).unwrap(), - if !is_wsl() { 0o777 } else { 0o776 } - ); - assert_eq!(super::parse_mode(Some("a-w".to_owned())).unwrap(), 0o444); - assert_eq!(super::parse_mode(Some("g-r".to_owned())).unwrap(), 0o626); - } - - #[test] - fn numeric_modes() { - assert_eq!(super::parse_mode(Some("644".to_owned())).unwrap(), 0o644); - assert_eq!(super::parse_mode(Some("+100".to_owned())).unwrap(), 0o766); - assert_eq!(super::parse_mode(Some("-4".to_owned())).unwrap(), 0o662); - assert_eq!(super::parse_mode(None).unwrap(), 0o666); - } -} diff --git a/src/uu/printf/src/cli.rs b/src/uu/printf/src/cli.rs index 12e80a925..a5e9c9775 100644 --- a/src/uu/printf/src/cli.rs +++ b/src/uu/printf/src/cli.rs @@ -18,18 +18,15 @@ pub fn err_msg(msg: &str) { // by default stdout only flushes // to console when a newline is passed. -#[allow(unused_must_use)] pub fn flush_char(c: char) { print!("{}", c); - stdout().flush(); + let _ = stdout().flush(); } -#[allow(unused_must_use)] pub fn flush_str(s: &str) { print!("{}", s); - stdout().flush(); + let _ = stdout().flush(); } -#[allow(unused_must_use)] pub fn flush_bytes(bslice: &[u8]) { - stdout().write(bslice); - stdout().flush(); + let _ = stdout().write(bslice); + let _ = stdout().flush(); } diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index c2952e5a9..d947a7d83 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -1,5 +1,4 @@ #![allow(dead_code)] - // spell-checker:ignore (change!) each's // spell-checker:ignore (ToDO) LONGHELP FORMATSTRING templating parameterizing formatstr @@ -9,7 +8,6 @@ mod tokenize; static NAME: &str = "printf"; static VERSION: &str = env!("CARGO_PKG_VERSION"); -static SHORT_USAGE: &str = "printf: usage: printf [-v var] format [arguments]"; static LONGHELP_LEAD: &str = "printf USAGE: printf FORMATSTRING [ARGUMENT]... diff --git a/src/uu/printf/src/tokenize/num_format/formatters/base_conv/mod.rs b/src/uu/printf/src/tokenize/num_format/formatters/base_conv/mod.rs index 04d33b52c..82971df3e 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/base_conv/mod.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/base_conv/mod.rs @@ -28,8 +28,7 @@ pub fn arrnum_int_mult(arr_num: &[u8], basenum: u8, base_ten_int_fact: u8) -> Ve } } } - #[allow(clippy::map_clone)] - let ret: Vec = ret_rev.iter().rev().map(|x| *x).collect(); + let ret: Vec = ret_rev.into_iter().rev().collect(); ret } @@ -102,70 +101,6 @@ pub fn arrnum_int_div_step( remainder: rem_out, } } -// pub struct ArrFloat { -// pub leading_zeros: u8, -// pub values: Vec, -// pub basenum: u8 -// } -// -// pub struct ArrFloatDivOut { -// pub quotient: u8, -// pub remainder: ArrFloat -// } -// -// pub fn arrfloat_int_div( -// arrfloat_in : &ArrFloat, -// base_ten_int_divisor : u8, -// precision : u16 -// ) -> DivOut { -// -// let mut remainder = ArrFloat { -// basenum: arrfloat_in.basenum, -// leading_zeros: arrfloat_in.leading_zeroes, -// values: Vec::new() -// } -// let mut quotient = 0; -// -// let mut bufferval : u16 = 0; -// let base : u16 = arrfloat_in.basenum as u16; -// let divisor : u16 = base_ten_int_divisor as u16; -// -// let mut it_f = arrfloat_in.values.iter(); -// let mut position = 0 + arrfloat_in.leading_zeroes as u16; -// let mut at_end = false; -// while position< precision { -// let next_digit = match it_f.next() { -// Some(c) => {} -// None => { 0 } -// } -// match u_cur { -// Some(u) => { -// bufferval += u.clone() as u16; -// if bufferval > divisor { -// while bufferval >= divisor { -// quotient+=1; -// bufferval -= divisor; -// } -// if bufferval == 0 { -// rem_out.position +=1; -// } else { -// rem_out.replace = Some(bufferval as u8); -// } -// break; -// } else { -// bufferval *= base; -// } -// }, -// None => { -// break; -// } -// } -// u_cur = it_f.next().clone(); -// rem_out.position+=1; -// } -// ArrFloatDivOut { quotient: quotient, remainder: remainder } -// } -// pub fn arrnum_int_add(arrnum: &[u8], basenum: u8, base_ten_int_term: u8) -> Vec { let mut carry: u16 = u16::from(base_ten_int_term); let mut rem: u16; @@ -193,8 +128,7 @@ pub fn arrnum_int_add(arrnum: &[u8], basenum: u8, base_ten_int_term: u8) -> Vec< } } } - #[allow(clippy::map_clone)] - let ret: Vec = ret_rev.iter().rev().map(|x| *x).collect(); + let ret: Vec = ret_rev.into_iter().rev().collect(); ret } @@ -219,8 +153,7 @@ pub fn unsigned_to_arrnum(src: u16) -> Vec { } // temporary needs-improvement-function -#[allow(unused_variables)] -pub fn base_conv_float(src: &[u8], radix_src: u8, radix_dest: u8) -> f64 { +pub fn base_conv_float(src: &[u8], radix_src: u8, _radix_dest: u8) -> f64 { // it would require a lot of addl code // to implement this for arbitrary string input. // until then, the below operates as an outline @@ -267,7 +200,6 @@ pub fn arrnum_to_str(src: &[u8], radix_def_dest: &dyn RadixDef) -> String { str_out } -#[allow(unused_variables)] pub fn base_conv_str( src: &str, radix_def_src: &dyn RadixDef, diff --git a/src/uu/printf/src/tokenize/num_format/formatters/cninetyninehexfloatf.rs b/src/uu/printf/src/tokenize/num_format/formatters/cninetyninehexfloatf.rs index 10e58cc32..870e64712 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/cninetyninehexfloatf.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/cninetyninehexfloatf.rs @@ -43,45 +43,15 @@ impl Formatter for CninetyNineHexFloatf { // c99 hex has unique requirements of all floating point subs in pretty much every part of building a primitive, from prefix and suffix to need for base conversion (in all other cases if you don't have decimal you must have decimal, here it's the other way around) // on the todo list is to have a trait for get_primitive that is implemented by each float formatter and can override a default. when that happens we can take the parts of get_primitive_dec specific to dec and spin them out to their own functions that can be overridden. -#[allow(unused_variables)] -#[allow(unused_assignments)] fn get_primitive_hex( inprefix: &InPrefix, - str_in: &str, - analysis: &FloatAnalysis, - last_dec_place: usize, + _str_in: &str, + _analysis: &FloatAnalysis, + _last_dec_place: usize, capitalized: bool, ) -> FormatPrimitive { let prefix = Some(String::from(if inprefix.sign == -1 { "-0x" } else { "0x" })); - // assign the digits before and after the decimal points - // to separate slices. If no digits after decimal point, - // assign 0 - let (mut first_segment_raw, second_segment_raw) = match analysis.decimal_pos { - Some(pos) => (&str_in[..pos], &str_in[pos + 1..]), - None => (str_in, "0"), - }; - if first_segment_raw.is_empty() { - first_segment_raw = "0"; - } - // convert to string, hexifying if input is in dec. - // let (first_segment, second_segment) = - // match inprefix.radix_in { - // Base::Ten => { - // (to_hex(first_segment_raw, true), - // to_hex(second_segment_raw, false)) - // } - // _ => { - // (String::from(first_segment_raw), - // String::from(second_segment_raw)) - // } - // }; - // - // - // f.pre_decimal = Some(first_segment); - // f.post_decimal = Some(second_segment); - // - // TODO actual conversion, make sure to get back mantissa. // for hex to hex, it's really just a matter of moving the // decimal point and calculating the mantissa by its initial diff --git a/src/uu/printf/src/tokenize/num_format/formatters/decf.rs b/src/uu/printf/src/tokenize/num_format/formatters/decf.rs index 6b2baa890..448771f22 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/decf.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/decf.rs @@ -22,12 +22,11 @@ fn get_len_fprim(fprim: &FormatPrimitive) -> usize { len } -pub struct Decf { - as_num: f64, -} +pub struct Decf; + impl Decf { pub fn new() -> Decf { - Decf { as_num: 0.0 } + Decf } } impl Formatter for Decf { diff --git a/src/uu/printf/src/tokenize/num_format/formatters/floatf.rs b/src/uu/printf/src/tokenize/num_format/formatters/floatf.rs index 97ceafe8d..b3de2f98a 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/floatf.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/floatf.rs @@ -5,12 +5,10 @@ use super::super::format_field::FormatField; use super::super::formatter::{FormatPrimitive, Formatter, InPrefix}; use super::float_common::{get_primitive_dec, primitive_to_str_common, FloatAnalysis}; -pub struct Floatf { - as_num: f64, -} +pub struct Floatf; impl Floatf { pub fn new() -> Floatf { - Floatf { as_num: 0.0 } + Floatf } } impl Formatter for Floatf { diff --git a/src/uu/printf/src/tokenize/num_format/formatters/intf.rs b/src/uu/printf/src/tokenize/num_format/formatters/intf.rs index 9231bd027..2e4e67047 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/intf.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/intf.rs @@ -11,7 +11,7 @@ use std::i64; use std::u64; pub struct Intf { - a: u32, + _a: u32, } // see the Intf::analyze() function below @@ -24,7 +24,7 @@ struct IntAnalysis { impl Intf { pub fn new() -> Intf { - Intf { a: 0 } + Intf { _a: 0 } } // take a ref to argument string, and basic information // about prefix (offset, radix, sign), and analyze string diff --git a/src/uu/printf/src/tokenize/num_format/formatters/scif.rs b/src/uu/printf/src/tokenize/num_format/formatters/scif.rs index 69a703042..ebac1565e 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/scif.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/scif.rs @@ -5,12 +5,11 @@ use super::super::format_field::FormatField; use super::super::formatter::{FormatPrimitive, Formatter, InPrefix}; use super::float_common::{get_primitive_dec, primitive_to_str_common, FloatAnalysis}; -pub struct Scif { - as_num: f64, -} +pub struct Scif; + impl Scif { pub fn new() -> Scif { - Scif { as_num: 0.0 } + Scif } } impl Formatter for Scif { diff --git a/src/uu/printf/src/tokenize/unescaped_text.rs b/src/uu/printf/src/tokenize/unescaped_text.rs index 3b9f0123e..084014ae9 100644 --- a/src/uu/printf/src/tokenize/unescaped_text.rs +++ b/src/uu/printf/src/tokenize/unescaped_text.rs @@ -242,18 +242,16 @@ impl UnescapedText { } } } -#[allow(unused_variables)] impl token::Tokenizer for UnescapedText { fn from_it( it: &mut PutBackN, - args: &mut Peekable>, + _: &mut Peekable>, ) -> Option> { UnescapedText::from_it_core(it, false) } } -#[allow(unused_variables)] impl token::Token for UnescapedText { - fn print(&self, pf_args_it: &mut Peekable>) { + fn print(&self, _: &mut Peekable>) { cli::flush_bytes(&self.0[..]); } } diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index 12c685e23..02ab385da 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -23,7 +23,8 @@ clap = "2.33" fnv = "1.0.7" itertools = "0.10.0" semver = "0.9.0" -smallvec = { version = "1.6.1", features = ["serde"] } +smallvec = "1.6.1" +unicode-width = "0.1.8" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } tempdir = "0.3.7" diff --git a/src/uu/sort/src/numeric_str_cmp.rs b/src/uu/sort/src/numeric_str_cmp.rs index b15eec988..b74d97867 100644 --- a/src/uu/sort/src/numeric_str_cmp.rs +++ b/src/uu/sort/src/numeric_str_cmp.rs @@ -138,7 +138,15 @@ impl NumInfo { sign: if had_digit { sign } else { Sign::Positive }, exponent: 0, }, - 0..0, + if had_digit { + // In this case there were only zeroes. + // For debug output to work properly, we have to claim to match the end of the number. + num.len()..num.len() + } else { + // This was no number at all. + // For debug output to work properly, we have to claim to match the start of the number. + 0..0 + }, ) } } diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index e77271557..049c09970 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -29,7 +29,6 @@ use rayon::prelude::*; use semver::Version; use serde::{Deserializer, Deserialize, Serialize}; use smallvec::SmallVec; -use std::borrow::Cow; use std::cmp::Ordering; use std::collections::BinaryHeap; use std::env; @@ -38,9 +37,10 @@ use std::fs::File; use std::hash::{Hash, Hasher}; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Lines, Read, Write}; use std::mem::replace; -use std::ops::{Range, RangeInclusive}; -use std::path::{Path, PathBuf}; -use uucore::fs::is_stdin_interactive; // for Iterator::dedup(); +use std::ops::Range; +use std::path::Path; +use unicode_width::UnicodeWidthStr; +use uucore::fs::is_stdin_interactive; // for Iterator::dedup() static NAME: &str = "sort"; static ABOUT: &str = "Display sorted concatenation of all FILE(s)."; @@ -66,6 +66,7 @@ static OPT_DICTIONARY_ORDER: &str = "dictionary-order"; static OPT_MERGE: &str = "merge"; static OPT_CHECK: &str = "check"; static OPT_CHECK_SILENT: &str = "check-silent"; +static OPT_DEBUG: &str = "debug"; static OPT_IGNORE_CASE: &str = "ignore-case"; static OPT_IGNORE_BLANKS: &str = "ignore-blanks"; static OPT_IGNORE_NONPRINTING: &str = "ignore-nonprinting"; @@ -94,7 +95,7 @@ static DEFAULT_TMPDIR: &str = r"/tmp"; // 16GB buffer for Vec before we dump to disk, never used static DEFAULT_BUF_SIZE: usize = 16000000000; -#[derive(Eq, Ord, PartialEq, PartialOrd, Clone)] +#[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy)] enum SortMode { Numeric, HumanNumeric, @@ -106,6 +107,7 @@ enum SortMode { #[derive(Clone)] struct GlobalSettings { mode: SortMode, + debug: bool, ignore_blanks: bool, ignore_case: bool, dictionary_order: bool, @@ -154,6 +156,7 @@ impl Default for GlobalSettings { fn default() -> GlobalSettings { GlobalSettings { mode: SortMode::Default, + debug: false, ignore_blanks: false, ignore_case: false, dictionary_order: false, @@ -190,7 +193,7 @@ struct KeySettings { impl From<&GlobalSettings> for KeySettings { fn from(settings: &GlobalSettings) -> Self { Self { - mode: settings.mode.clone(), + mode: settings.mode, ignore_blanks: settings.ignore_blanks, ignore_case: settings.ignore_case, ignore_non_printing: settings.ignore_non_printing, @@ -236,7 +239,7 @@ impl SelectionRange { #[derive(Debug, Serialize, Deserialize, Clone)] enum NumCache { #[serde(deserialize_with="bailout_parse_f64")] - AsF64(f64), + AsF64(GeneralF64ParseResult), WithInfo(NumInfo), None, } @@ -250,7 +253,7 @@ fn bailout_parse_f64<'de, D>(d: D) -> Result where D: Deserialize } impl NumCache { - fn as_f64(&self) -> f64 { + fn as_f64(&self) -> GeneralF64ParseResult { match self { NumCache::AsF64(n) => *n, _ => unreachable!(), @@ -309,19 +312,14 @@ impl Line { .selectors .iter() .map(|selector| { - let mut range = - if let Some(range) = selector.get_field_selection(&line, fields.as_deref()) { - if let Some(transformed) = - transform(&line[range.to_owned()], &selector.settings) - { - SelectionRange::String(transformed) - } else { - SelectionRange::ByIndex(range.start().to_owned()..range.end() + 1) - } - } else { - // If there is no match, match the empty string. - SelectionRange::ByIndex(0..0) - }; + let range = selector.get_selection(&line, fields.as_deref()); + let mut range = if let Some(transformed) = + transform(&line[range.to_owned()], &selector.settings) + { + SelectionRange::String(transformed) + } else { + SelectionRange::ByIndex(range) + }; let num_cache = if selector.settings.mode == SortMode::Numeric || selector.settings.mode == SortMode::HumanNumeric { @@ -336,7 +334,8 @@ impl Line { range.shorten(num_range); NumCache::WithInfo(info) } else if selector.settings.mode == SortMode::GeneralNumeric && settings.buffer_size == DEFAULT_BUF_SIZE { - NumCache::AsF64(permissive_f64_parse(get_leading_gen(range.get_str(&line)))) + let str = range.get_str(&line); + NumCache::AsF64(general_f64_parse(&str[get_leading_gen(str)])) } else { NumCache::None }; @@ -345,6 +344,129 @@ impl Line { .collect(); Self { line, selections } } + + /// Writes indicators for the selections this line matched. The original line content is NOT expected + /// to be already printed. + fn print_debug( + &self, + settings: &GlobalSettings, + writer: &mut dyn Write, + ) -> std::io::Result<()> { + // We do not consider this function performance critical, as debug output is only useful for small files, + // which are not a performance problem in any case. Therefore there aren't any special performance + // optimizations here. + + let line = self.line.replace('\t', ">"); + writeln!(writer, "{}", line)?; + + let fields = tokenize(&self.line, settings.separator); + for selector in settings.selectors.iter() { + let mut selection = selector.get_selection(&self.line, Some(&fields)); + match selector.settings.mode { + SortMode::Numeric | SortMode::HumanNumeric => { + // find out which range is used for numeric comparisons + let (_, num_range) = NumInfo::parse( + &self.line[selection.clone()], + NumInfoParseSettings { + accept_si_units: selector.settings.mode == SortMode::HumanNumeric, + thousands_separator: Some(THOUSANDS_SEP), + decimal_pt: Some(DECIMAL_PT), + }, + ); + let initial_selection = selection.clone(); + + // Shorten selection to num_range. + selection.start += num_range.start; + selection.end = selection.start + num_range.len(); + + // include a trailing si unit + if selector.settings.mode == SortMode::HumanNumeric + && self.line[selection.end..initial_selection.end] + .starts_with(&['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'][..]) + { + selection.end += 1; + } + + // include leading zeroes, a leading minus or a leading decimal point + while self.line[initial_selection.start..selection.start] + .ends_with(&['-', '0', '.'][..]) + { + selection.start -= 1; + } + } + SortMode::GeneralNumeric => { + let initial_selection = &self.line[selection.clone()]; + + let leading = get_leading_gen(initial_selection); + + // Shorten selection to leading. + selection.start += leading.start; + selection.end = selection.start + leading.len(); + } + SortMode::Month => { + let initial_selection = &self.line[selection.clone()]; + + let month = if month_parse(initial_selection) == Month::Unknown { + // We failed to parse a month, which is equivalent to matching nothing. + 0..0 + } else { + // We parsed a month. Match the three first non-whitespace characters, which must be the month we parsed. + let mut chars = initial_selection + .char_indices() + .skip_while(|(_, c)| c.is_whitespace()); + chars.next().unwrap().0 + ..chars.nth(2).map_or(initial_selection.len(), |(idx, _)| idx) + }; + + // Shorten selection to month. + selection.start += month.start; + selection.end = selection.start + month.len(); + } + _ => {} + } + + write!( + writer, + "{}", + " ".repeat(UnicodeWidthStr::width(&line[..selection.start])) + )?; + + // TODO: Once our minimum supported rust version is at least 1.47, use selection.is_empty() instead. + #[allow(clippy::len_zero)] + { + if selection.len() == 0 { + writeln!(writer, "^ no match for key")?; + } else { + writeln!( + writer, + "{}", + "_".repeat(UnicodeWidthStr::width(&line[selection])) + )?; + } + } + } + if !(settings.random + || settings.stable + || settings.unique + || !(settings.dictionary_order + || settings.ignore_blanks + || settings.ignore_case + || settings.ignore_non_printing + || settings.mode != SortMode::Default)) + { + // A last resort comparator is in use, underline the whole line. + if self.line.is_empty() { + writeln!(writer, "^ no match for key")?; + } else { + writeln!( + writer, + "{}", + "_".repeat(UnicodeWidthStr::width(line.as_str())) + )?; + } + } + Ok(()) + } } /// Transform this line. Returns None if there's no need to transform. @@ -407,20 +529,18 @@ fn tokenize_default(line: &str) -> Vec { /// Split between separators. These separators are not included in fields. fn tokenize_with_separator(line: &str, separator: char) -> Vec { - let mut tokens = vec![0..0]; - let mut previous_was_separator = false; - for (idx, char) in line.char_indices() { - if previous_was_separator { - tokens.push(idx..0); - } - if char == separator { - tokens.last_mut().unwrap().end = idx; - previous_was_separator = true; - } else { - previous_was_separator = false; - } + let mut tokens = vec![]; + let separator_indices = + line.char_indices() + .filter_map(|(i, c)| if c == separator { Some(i) } else { None }); + let mut start = 0; + for sep_idx in separator_indices { + tokens.push(start..sep_idx); + start = sep_idx + 1; + } + if start < line.len() { + tokens.push(start..line.len()); } - tokens.last_mut().unwrap().end = line.len(); tokens } @@ -464,6 +584,28 @@ impl KeyPosition { crash!(1, "invalid option for key: `{}`", c) } } + // All numeric sorts and month sort conflict with dictionary_order and ignore_non_printing. + // Instad of reporting an error, let them overwrite each other. + + // FIXME: This should only override if the overridden flag is a global flag. + // If conflicting flags are attached to the key, GNU sort crashes and we should probably too. + match option { + 'h' | 'n' | 'g' | 'M' => { + settings.dictionary_order = false; + settings.ignore_non_printing = false; + } + 'd' | 'i' => { + settings.mode = match settings.mode { + SortMode::Numeric + | SortMode::HumanNumeric + | SortMode::GeneralNumeric + | SortMode::Month => SortMode::Default, + // Only SortMode::Default and SortMode::Version work with dictionary_order and ignore_non_printing + m @ SortMode::Default | m @ SortMode::Version => m, + } + } + _ => {} + } } // Strip away option characters from the original value so we can parse it later *value_with_options = &value_with_options[..options_start]; @@ -506,13 +648,16 @@ impl FieldSelector { /// Look up the slice that corresponds to this selector for the given line. /// If needs_fields returned false, fields may be None. - fn get_field_selection<'a>( - &self, - line: &'a str, - tokens: Option<&[Field]>, - ) -> Option> { - enum ResolutionErr { + fn get_selection<'a>(&self, line: &'a str, tokens: Option<&[Field]>) -> Range { + enum Resolution { + // The start index of the resolved character, inclusive + StartOfChar(usize), + // The end index of the resolved character, exclusive. + // This is only returned if the character index is 0. + EndOfChar(usize), + // The resolved character would be in front of the first character TooLow, + // The resolved character would be after the last character TooHigh, } @@ -521,15 +666,15 @@ impl FieldSelector { line: &str, tokens: Option<&[Field]>, position: &KeyPosition, - ) -> Result { + ) -> Resolution { if tokens.map_or(false, |fields| fields.len() < position.field) { - Err(ResolutionErr::TooHigh) + Resolution::TooHigh } else if position.char == 0 { let end = tokens.unwrap()[position.field - 1].end; if end == 0 { - Err(ResolutionErr::TooLow) + Resolution::TooLow } else { - Ok(end - 1) + Resolution::EndOfChar(end) } } else { let mut idx = if position.field == 1 { @@ -538,38 +683,52 @@ impl FieldSelector { 0 } else { tokens.unwrap()[position.field - 1].start - } + position.char - - 1; + }; + idx += line[idx..] + .char_indices() + .nth(position.char - 1) + .map_or(line.len(), |(idx, _)| idx); if idx >= line.len() { - Err(ResolutionErr::TooHigh) + Resolution::TooHigh } else { if position.ignore_blanks { - if let Some(not_whitespace) = - line[idx..].chars().position(|c| !c.is_whitespace()) + if let Some((not_whitespace, _)) = + line[idx..].char_indices().find(|(_, c)| !c.is_whitespace()) { idx += not_whitespace; } else { - return Err(ResolutionErr::TooHigh); + return Resolution::TooHigh; } } - Ok(idx) + Resolution::StartOfChar(idx) } } } - if let Ok(from) = resolve_index(line, tokens, &self.from) { - let to = self.to.as_ref().map(|to| resolve_index(line, tokens, &to)); - match to { - Some(Ok(to)) => Some(from..=to), - // If `to` was not given or the match would be after the end of the line, - // match everything until the end of the line. - None | Some(Err(ResolutionErr::TooHigh)) => Some(from..=line.len() - 1), - // If `to` is before the start of the line, report no match. - // This can happen if the line starts with a separator. - Some(Err(ResolutionErr::TooLow)) => None, + match resolve_index(line, tokens, &self.from) { + Resolution::StartOfChar(from) => { + let to = self.to.as_ref().map(|to| resolve_index(line, tokens, &to)); + + match to { + Some(Resolution::StartOfChar(mut to)) => { + to += line[to..].chars().next().map_or(1, |c| c.len_utf8()); + from..to + } + Some(Resolution::EndOfChar(to)) => from..to, + // If `to` was not given or the match would be after the end of the line, + // match everything until the end of the line. + None | Some(Resolution::TooHigh) => from..line.len(), + // If `to` is before the start of the line, report no match. + // This can happen if the line starts with a separator. + Some(Resolution::TooLow) => 0..0, + } } - } else { - None + Resolution::TooLow | Resolution::EndOfChar(_) => { + unreachable!("This should only happen if the field start index is 0, but that should already have caused an error.") + } + // While for comparisons it's only important that this is an empty slice, + // to produce accurate debug output we need to match an empty slice at the end of the line. + Resolution::TooHigh => line.len()..line.len(), } } } @@ -597,7 +756,7 @@ impl<'a> PartialOrd for MergeableFile<'a> { impl<'a> PartialEq for MergeableFile<'a> { fn eq(&self, other: &MergeableFile) -> bool { - Ordering::Equal == compare_by(&self.current_line, &other.current_line, self.settings) + Ordering::Equal == self.cmp(other) } } @@ -628,8 +787,8 @@ impl<'a> FileMerger<'a> { } impl<'a> Iterator for FileMerger<'a> { - type Item = String; - fn next(&mut self) -> Option { + type Item = Line; + fn next(&mut self) -> Option { match self.heap.pop() { Some(mut current) => { match current.lines.next() { @@ -639,12 +798,12 @@ impl<'a> Iterator for FileMerger<'a> { Line::new(next_line, &self.settings), ); self.heap.push(current); - Some(ret.line) + Some(ret) } _ => { // Don't put it back in the heap (it's empty/erroring) // but its first line is still valid. - Some(current.current_line.line) + Some(current.current_line) } } } @@ -708,7 +867,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Arg::with_name(OPT_DICTIONARY_ORDER) .short("d") .long(OPT_DICTIONARY_ORDER) - .help("consider only blanks and alphanumeric characters"), + .help("consider only blanks and alphanumeric characters") + .conflicts_with_all(&[OPT_NUMERIC_SORT, OPT_GENERAL_NUMERIC_SORT, OPT_HUMAN_NUMERIC_SORT, OPT_MONTH_SORT]), ) .arg( Arg::with_name(OPT_MERGE) @@ -736,9 +896,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { ) .arg( Arg::with_name(OPT_IGNORE_NONPRINTING) - .short("-i") + .short("i") .long(OPT_IGNORE_NONPRINTING) - .help("ignore nonprinting characters"), + .help("ignore nonprinting characters") + .conflicts_with_all(&[OPT_NUMERIC_SORT, OPT_GENERAL_NUMERIC_SORT, OPT_HUMAN_NUMERIC_SORT, OPT_MONTH_SORT]), ) .arg( Arg::with_name(OPT_IGNORE_BLANKS) @@ -829,9 +990,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .value_name("NUL_FILES") .multiple(true), ) + .arg( + Arg::with_name(OPT_DEBUG) + .long(OPT_DEBUG) + .help("underline the parts of the line that are actually used for sorting"), + ) .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) .get_matches_from(args); + settings.debug = matches.is_present(OPT_DEBUG); + // check whether user specified a zero terminated list of files for input, otherwise read files from args let mut files: Vec = if matches.is_present(OPT_FILES0_FROM) { let files0_from: Vec = matches @@ -964,6 +1132,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 1, &mut key_settings, ); + if from.char == 0 { + crash!( + 1, + "invalid character index 0 in `{}` for the start position of a field", + key + ) + } let to = from_to .next() .map(|to| KeyPosition::parse(to, 0, &mut key_settings)); @@ -1042,7 +1217,10 @@ fn exec(files: Vec, settings: GlobalSettings) -> i32 { if settings.merge { if settings.unique { - print_sorted(file_merger.dedup(), &settings) + print_sorted( + file_merger.dedup_by(|a, b| compare_by(a, b, settings) == Ordering::Equal), + &settings, + ) } else { print_sorted(file_merger, &settings) } @@ -1050,12 +1228,11 @@ fn exec(files: Vec, settings: GlobalSettings) -> i32 { print_sorted( lines .into_iter() - .dedup_by(|a, b| compare_by(a, b, &settings) == Ordering::Equal) - .map(|line| line.line), + .dedup_by(|a, b| compare_by(a, b, settings) == Ordering::Equal), &settings, ) } else { - print_sorted(lines.into_iter().map(|line| line.line), &settings) + print_sorted(lines.into_iter(), &settings) } 0 @@ -1163,107 +1340,80 @@ fn default_compare(a: &str, b: &str) -> Ordering { a.cmp(b) } -/// This function does the initial detection of numeric lines for FP compares. -// Lines starting with a number or positive or negative sign. -// It also strips the string of any thing that could never -// be a number for the purposes of any type of numeric comparison. -#[inline(always)] -fn leading_num_common(a: &str) -> &str { - let mut s = ""; - - // check whether char is numeric, whitespace or decimal point or thousand separator - for (idx, c) in a.char_indices() { - if !c.is_numeric() - && !c.is_whitespace() - && !c.eq(&THOUSANDS_SEP) - && !c.eq(&DECIMAL_PT) - // check for e notation - && !c.eq(&'e') - && !c.eq(&'E') - // check whether first char is + or - - && !a.chars().next().unwrap_or('\0').eq(&POSITIVE) - && !a.chars().next().unwrap_or('\0').eq(&NEGATIVE) - { - // Strip string of non-numeric trailing chars - s = &a[..idx]; - break; - } - // If line is not a number line, return the line as is - s = &a; - } - s -} - -/// This function cleans up the initial comparison done by leading_num_common for a general numeric compare. +// This function cleans up the initial comparison done by leading_num_common for a general numeric compare. // In contrast to numeric compare, GNU general numeric/FP sort *should* recognize positive signs and // scientific notation, so we strip those lines only after the end of the following numeric string. // For example, 5e10KFD would be 5e10 or 5x10^10 and +10000HFKJFK would become 10000. -fn get_leading_gen(a: &str) -> &str { - // Make this iter peekable to see if next char is numeric - let raw_leading_num = leading_num_common(a); - let mut p_iter = raw_leading_num.chars().peekable(); - let mut result = ""; - // Cleanup raw stripped strings - while let Some(c) = p_iter.next() { - let next_char_numeric = p_iter.peek().unwrap_or(&'\0').is_numeric(); - // Only general numeric recognizes e notation and, see block below, the '+' sign - // Only GNU (non-general) numeric recognize thousands seperators, takes only leading # - if (c.eq(&'e') || c.eq(&'E')) && !next_char_numeric || c.eq(&THOUSANDS_SEP) { - result = a.split(c).next().unwrap_or(""); - break; - // If positive sign and next char is not numeric, split at postive sign at keep trailing numbers - // There is a more elegant way to do this in Rust 1.45, std::str::strip_prefix - } else if c.eq(&POSITIVE) && !next_char_numeric { - result = a.trim().trim_start_matches('+'); - break; +fn get_leading_gen(input: &str) -> Range { + let trimmed = input.trim_start(); + let leading_whitespace_len = input.len() - trimmed.len(); + for allowed_prefix in &["inf", "-inf", "nan"] { + if trimmed.is_char_boundary(allowed_prefix.len()) + && trimmed[..allowed_prefix.len()].eq_ignore_ascii_case(allowed_prefix) + { + return leading_whitespace_len..(leading_whitespace_len + allowed_prefix.len()); } - // If no further processing needed to be done, return the line as-is to be sorted - result = a; } - result + // Make this iter peekable to see if next char is numeric + let mut char_indices = trimmed.char_indices().peekable(); + + let first = char_indices.peek(); + + if first.map_or(false, |&(_, c)| c == NEGATIVE || c == POSITIVE) { + char_indices.next(); + } + + let mut had_e_notation = false; + let mut had_decimal_pt = false; + while let Some((idx, c)) = char_indices.next() { + if c.is_ascii_digit() { + continue; + } + if c == DECIMAL_PT && !had_decimal_pt { + had_decimal_pt = true; + continue; + } + let next_char_numeric = char_indices + .peek() + .map_or(false, |(_, c)| c.is_ascii_digit()); + if (c == 'e' || c == 'E') && !had_e_notation && next_char_numeric { + had_e_notation = true; + continue; + } + return leading_whitespace_len..(leading_whitespace_len + idx); + } + leading_whitespace_len..input.len() } -#[inline(always)] -fn remove_trailing_dec<'a, S: Into>>(input: S) -> Cow<'a, str> { - let input = input.into(); - if let Some(s) = input.find(DECIMAL_PT) { - let (leading, trailing) = input.split_at(s); - let output = [leading, ".", trailing.replace(DECIMAL_PT, "").as_str()].concat(); - Cow::Owned(output) - } else { - input - } +#[derive(Copy, Clone, PartialEq, PartialOrd)] +enum GeneralF64ParseResult { + Invalid, + NaN, + NegInfinity, + Number(f64), + Infinity, } /// Parse the beginning string into an f64, returning -inf instead of NaN on errors. #[inline(always)] -fn permissive_f64_parse(a: &str) -> f64 { - // GNU sort treats "NaN" as non-number in numeric, so it needs special care. - // *Keep this trim before parse* despite what POSIX may say about -b and -n - // because GNU and BSD both seem to require it to match their behavior - // - // Remove any trailing decimals, ie 4568..890... becomes 4568.890 - // Then, we trim whitespace and parse - match remove_trailing_dec(a).trim().parse::() { - Ok(val) if val.is_nan() => std::f64::NEG_INFINITY, - Ok(val) => val, - Err(_) => std::f64::NEG_INFINITY, +fn general_f64_parse(a: &str) -> GeneralF64ParseResult { + // The actual behavior here relies on Rust's implementation of parsing floating points. + // For example "nan", "inf" (ignoring the case) and "infinity" are only parsed to floats starting from 1.53. + // TODO: Once our minimum supported Rust version is 1.53 or above, we should add tests for those cases. + match a.parse::() { + Ok(a) if a.is_nan() => GeneralF64ParseResult::NaN, + Ok(a) if a == std::f64::NEG_INFINITY => GeneralF64ParseResult::NegInfinity, + Ok(a) if a == std::f64::INFINITY => GeneralF64ParseResult::Infinity, + Ok(a) => GeneralF64ParseResult::Number(a), + Err(_) => GeneralF64ParseResult::Invalid, } } /// Compares two floats, with errors and non-numerics assumed to be -inf. /// Stops coercing at the first non-numeric char. /// We explicitly need to convert to f64 in this case. -fn general_numeric_compare(a: f64, b: f64) -> Ordering { - #![allow(clippy::comparison_chain)] - // f64::cmp isn't implemented (due to NaN issues); implement directly instead - if a > b { - Ordering::Greater - } else if a < b { - Ordering::Less - } else { - Ordering::Equal - } +fn general_numeric_compare(a: GeneralF64ParseResult, b: GeneralF64ParseResult) -> Ordering { + a.partial_cmp(&b).unwrap() } fn get_rand_string() -> String { @@ -1290,7 +1440,7 @@ fn random_shuffle(a: &str, b: &str, x: String) -> Ordering { da.cmp(&db) } -#[derive(Eq, Ord, PartialEq, PartialOrd)] +#[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy)] enum Month { Unknown, January, @@ -1309,30 +1459,32 @@ enum Month { /// Parse the beginning string into a Month, returning Month::Unknown on errors. fn month_parse(line: &str) -> Month { - // GNU splits at any 3 letter match "JUNNNN" is JUN - let pattern = if line.trim().len().ge(&3) { - // Split a 3 and get first element of tuple ".0" - line.trim().split_at(3).0 - } else { - "" - }; + let line = line.trim(); - let result = match pattern.to_uppercase().as_ref() { - "JAN" => Month::January, - "FEB" => Month::February, - "MAR" => Month::March, - "APR" => Month::April, - "MAY" => Month::May, - "JUN" => Month::June, - "JUL" => Month::July, - "AUG" => Month::August, - "SEP" => Month::September, - "OCT" => Month::October, - "NOV" => Month::November, - "DEC" => Month::December, - _ => Month::Unknown, - }; - result + const MONTHS: [(&str, Month); 12] = [ + ("JAN", Month::January), + ("FEB", Month::February), + ("MAR", Month::March), + ("APR", Month::April), + ("MAY", Month::May), + ("JUN", Month::June), + ("JUL", Month::July), + ("AUG", Month::August), + ("SEP", Month::September), + ("OCT", Month::October), + ("NOV", Month::November), + ("DEC", Month::December), + ]; + + for (month_str, month) in &MONTHS { + if line.is_char_boundary(month_str.len()) + && line[..month_str.len()].eq_ignore_ascii_case(month_str) + { + return *month; + } + } + + Month::Unknown } fn month_compare(a: &str, b: &str) -> Ordering { @@ -1390,7 +1542,7 @@ fn remove_nonprinting_chars(s: &str) -> String { .collect::() } -fn print_sorted>(iter: T, settings: &GlobalSettings) { +fn print_sorted>(iter: T, settings: &GlobalSettings) { let mut file: Box = match settings.outfile { Some(ref filename) => match File::create(Path::new(&filename)) { Ok(f) => Box::new(BufWriter::new(f)) as Box, @@ -1401,15 +1553,19 @@ fn print_sorted>(iter: T, settings: &GlobalSettings) }, None => Box::new(BufWriter::new(stdout())) as Box, }; - if settings.zero_terminated { + if settings.zero_terminated && !settings.debug { for line in iter { - crash_if_err!(1, file.write_all(line.as_bytes())); + crash_if_err!(1, file.write_all(line.line.as_bytes())); crash_if_err!(1, file.write_all("\0".as_bytes())); } } else { for line in iter { - crash_if_err!(1, file.write_all(line.as_bytes())); - crash_if_err!(1, file.write_all("\n".as_bytes())); + if !settings.debug { + crash_if_err!(1, file.write_all(line.line.as_bytes())); + crash_if_err!(1, file.write_all("\n".as_bytes())); + } else { + crash_if_err!(1, line.print_debug(settings, &mut file)); + } } } crash_if_err!(1, file.flush()); @@ -1504,4 +1660,14 @@ mod tests { vec![0..0, 1..1, 2..2, 3..9, 10..18,] ); } + + #[test] + fn test_tokenize_fields_trailing_custom_separator() { + let line = "a"; + assert_eq!(tokenize(line, Some('a')), vec![0..0]); + let line = "aa"; + assert_eq!(tokenize(line, Some('a')), vec![0..0, 1..1]); + let line = "..a..a"; + assert_eq!(tokenize(line, Some('a')), vec![0..2, 3..5]); + } } diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 666ba3384..1fb6489da 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -91,10 +91,15 @@ fn tac(filenames: Vec, before: bool, _: bool, separator: &str) -> i32 { } else { let path = Path::new(filename); if path.is_dir() || path.metadata().is_err() { - show_error!( - "failed to open '{}' for reading: No such file or directory", - filename - ); + if path.is_dir() { + show_error!("dir: read error: Invalid argument"); + } else { + show_error!( + "failed to open '{}' for reading: No such file or directory", + filename + ); + } + exit_code = 1; continue; } match File::open(path) { diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index ffe27e26c..fec88e841 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -117,6 +117,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .arg( Arg::with_name(options::SLEEP_INT) .short("s") + .takes_value(true) .long(options::SLEEP_INT) .help("Number or seconds to sleep between polling the file when running with -f"), ) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 9cd5865b7..2c232a3d1 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -192,7 +192,8 @@ fn truncate( } fn parse_size(size: &str) -> (u64, TruncateMode) { - let mode = match size.chars().next().unwrap() { + let clean_size = size.replace(" ", ""); + let mode = match clean_size.chars().next().unwrap() { '+' => TruncateMode::Extend, '-' => TruncateMode::Reduce, '<' => TruncateMode::AtMost, @@ -203,9 +204,9 @@ fn parse_size(size: &str) -> (u64, TruncateMode) { }; let bytes = { let mut slice = if mode == TruncateMode::Reference { - size + &clean_size } else { - &size[1..] + &clean_size[1..] }; if slice.chars().last().unwrap().is_alphabetic() { slice = &slice[..slice.len() - 1]; @@ -220,11 +221,11 @@ fn parse_size(size: &str) -> (u64, TruncateMode) { Ok(num) => num, Err(e) => crash!(1, "'{}' is not a valid number: {}", size, e), }; - if size.chars().last().unwrap().is_alphabetic() { - number *= match size.chars().last().unwrap().to_ascii_uppercase() { - 'B' => match size + if clean_size.chars().last().unwrap().is_alphabetic() { + number *= match clean_size.chars().last().unwrap().to_ascii_uppercase() { + 'B' => match clean_size .chars() - .nth(size.len() - 2) + .nth(clean_size.len() - 2) .unwrap() .to_ascii_uppercase() { diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index a61a78a61..7e9862e65 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -61,34 +61,43 @@ impl Uniq { reader: &mut BufReader, writer: &mut BufWriter, ) { - let mut lines: Vec = vec![]; let mut first_line_printed = false; - let delimiters = self.delimiters; + let mut group_count = 1; let line_terminator = self.get_line_terminator(); - // Don't print any delimiting lines before, after or between groups if delimiting method is 'none' - let no_delimiters = delimiters == Delimiters::None; - // The 'prepend' and 'both' delimit methods will cause output to start with delimiter line - let prepend_delimiter = delimiters == Delimiters::Prepend || delimiters == Delimiters::Both; - // The 'append' and 'both' delimit methods will cause output to end with delimiter line - let append_delimiter = delimiters == Delimiters::Append || delimiters == Delimiters::Both; + let mut lines = reader.split(line_terminator).map(get_line_string); + let mut line = match lines.next() { + Some(l) => l, + None => return, + }; - for line in reader.split(line_terminator).map(get_line_string) { - if !lines.is_empty() && self.cmp_keys(&lines[0], &line) { - // Print delimiter if delimit method is not 'none' and any line has been output - // before or if we need to start output with delimiter - let print_delimiter = !no_delimiters && (prepend_delimiter || first_line_printed); - first_line_printed |= self.print_lines(writer, &lines, print_delimiter); - lines.truncate(0); + // compare current `line` with consecutive lines (`next_line`) of the input + // and if needed, print `line` based on the command line options provided + for next_line in lines { + if self.cmp_keys(&line, &next_line) { + if (group_count == 1 && !self.repeats_only) + || (group_count > 1 && !self.uniques_only) + { + self.print_line(writer, &line, group_count, first_line_printed); + first_line_printed = true; + } + line = next_line; + group_count = 1; + } else { + if self.all_repeated { + self.print_line(writer, &line, group_count, first_line_printed); + first_line_printed = true; + line = next_line; + } + group_count += 1; } - lines.push(line); } - if !lines.is_empty() { - // Print delimiter if delimit method is not 'none' and any line has been output - // before or if we need to start output with delimiter - let print_delimiter = !no_delimiters && (prepend_delimiter || first_line_printed); - first_line_printed |= self.print_lines(writer, &lines, print_delimiter); + if (group_count == 1 && !self.repeats_only) || (group_count > 1 && !self.uniques_only) { + self.print_line(writer, &line, group_count, first_line_printed); + first_line_printed = true; } - if append_delimiter && first_line_printed { + if (self.delimiters == Delimiters::Append || self.delimiters == Delimiters::Both) + && first_line_printed + { crash_if_err!(1, writer.write_all(&[line_terminator])); } } @@ -163,27 +172,17 @@ impl Uniq { } } - fn print_lines( - &self, - writer: &mut BufWriter, - lines: &[String], - print_delimiter: bool, - ) -> bool { - let mut first_line_printed = false; - let mut count = if self.all_repeated { 1 } else { lines.len() }; - if lines.len() == 1 && !self.repeats_only || lines.len() > 1 && !self.uniques_only { - self.print_line(writer, &lines[0], count, print_delimiter); - first_line_printed = true; - count += 1; - } - if self.all_repeated { - for line in lines[1..].iter() { - self.print_line(writer, line, count, print_delimiter && !first_line_printed); - first_line_printed = true; - count += 1; - } - } - first_line_printed + fn should_print_delimiter(&self, group_count: usize, first_line_printed: bool) -> bool { + // if no delimiter option is selected then no other checks needed + self.delimiters != Delimiters::None + // print delimiter only before the first line of a group, not between lines of a group + && group_count == 1 + // if at least one line has been output before current group then print delimiter + && (first_line_printed + // or if we need to prepend delimiter then print it even at the start of the output + || self.delimiters == Delimiters::Prepend + // the 'both' delimit mode should prepend and append delimiters + || self.delimiters == Delimiters::Both) } fn print_line( @@ -191,11 +190,11 @@ impl Uniq { writer: &mut BufWriter, line: &str, count: usize, - print_delimiter: bool, + first_line_printed: bool, ) { let line_terminator = self.get_line_terminator(); - if print_delimiter { + if self.should_print_delimiter(count, first_line_printed) { crash_if_err!(1, writer.write_all(&[line_terminator])); } diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index 8b5e71799..1bb79ac03 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -7,6 +7,8 @@ // spell-checker:ignore (vars) fperm srwx +use libc::{mode_t, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR}; + pub fn parse_numeric(fperm: u32, mut mode: &str) -> Result { let (op, pos) = parse_op(mode, Some('='))?; mode = mode[pos..].trim().trim_start_matches('0'); @@ -129,3 +131,41 @@ fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) { } (srwx, pos) } + +pub fn parse_mode(mode: Option) -> Result { + let fperm = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; + if let Some(mode) = mode { + let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + let result = if mode.contains(arr) { + parse_numeric(fperm as u32, mode.as_str()) + } else { + parse_symbolic(fperm as u32, mode.as_str(), true) + }; + result.map(|mode| mode as mode_t) + } else { + Ok(fperm) + } +} + +#[cfg(test)] +mod test { + + #[test] + fn symbolic_modes() { + assert_eq!(super::parse_mode(Some("u+x".to_owned())).unwrap(), 0o766); + assert_eq!( + super::parse_mode(Some("+x".to_owned())).unwrap(), + if !crate::os::is_wsl_1() { 0o777 } else { 0o776 } + ); + assert_eq!(super::parse_mode(Some("a-w".to_owned())).unwrap(), 0o444); + assert_eq!(super::parse_mode(Some("g-r".to_owned())).unwrap(), 0o626); + } + + #[test] + fn numeric_modes() { + assert_eq!(super::parse_mode(Some("644".to_owned())).unwrap(), 0o644); + assert_eq!(super::parse_mode(Some("+100".to_owned())).unwrap(), 0o766); + assert_eq!(super::parse_mode(Some("-4".to_owned())).unwrap(), 0o662); + assert_eq!(super::parse_mode(None).unwrap(), 0o666); + } +} diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 324095b6a..208e9536c 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -26,6 +26,7 @@ mod mods; // core cross-platform modules // * cross-platform modules pub use crate::mods::coreopts; +pub use crate::mods::os; pub use crate::mods::panic; pub use crate::mods::ranges; diff --git a/src/uucore/src/lib/mods.rs b/src/uucore/src/lib/mods.rs index c73909dcc..74725e141 100644 --- a/src/uucore/src/lib/mods.rs +++ b/src/uucore/src/lib/mods.rs @@ -1,5 +1,6 @@ // mods ~ cross-platforms modules (core/bundler file) pub mod coreopts; +pub mod os; pub mod panic; pub mod ranges; diff --git a/src/uucore/src/lib/mods/os.rs b/src/uucore/src/lib/mods/os.rs new file mode 100644 index 000000000..da2002161 --- /dev/null +++ b/src/uucore/src/lib/mods/os.rs @@ -0,0 +1,30 @@ +/// Test if the program is running under WSL +// ref: @@ +pub fn is_wsl_1() -> bool { + #[cfg(target_os = "linux")] + { + if is_wsl_2() { + return false; + } + if let Ok(b) = std::fs::read("/proc/sys/kernel/osrelease") { + if let Ok(s) = std::str::from_utf8(&b) { + let a = s.to_ascii_lowercase(); + return a.contains("microsoft") || a.contains("wsl"); + } + } + } + false +} + +pub fn is_wsl_2() -> bool { + #[cfg(target_os = "linux")] + { + if let Ok(b) = std::fs::read("/proc/sys/kernel/osrelease") { + if let Ok(s) = std::str::from_utf8(&b) { + let a = s.to_ascii_lowercase(); + return a.contains("wsl2"); + } + } + } + false +} diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index eb6cc9148..c8ae29a9d 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -1,4 +1,7 @@ use crate::common::util::*; +#[cfg(unix)] +use std::fs::OpenOptions; +#[cfg(unix)] use std::io::Read; #[test] @@ -26,7 +29,7 @@ fn test_no_options() { } #[test] -#[cfg(unix)] +#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))] fn test_no_options_big_input() { for &n in &[ 0, @@ -54,7 +57,6 @@ fn test_no_options_big_input() { #[test] #[cfg(unix)] fn test_fifo_symlink() { - use std::fs::OpenOptions; use std::io::Write; use std::thread; @@ -85,6 +87,74 @@ fn test_fifo_symlink() { thread.join().unwrap(); } +#[test] +#[cfg(unix)] +fn test_piped_to_regular_file() { + use std::fs::read_to_string; + + for &append in &[true, false] { + let s = TestScenario::new(util_name!()); + let file_path = s.fixtures.plus("file.txt"); + + { + let file = OpenOptions::new() + .create_new(true) + .write(true) + .append(append) + .open(&file_path) + .unwrap(); + + s.ucmd() + .set_stdout(file) + .pipe_in_fixture("alpha.txt") + .succeeds(); + } + let contents = read_to_string(&file_path).unwrap(); + assert_eq!(contents, "abcde\nfghij\nklmno\npqrst\nuvwxyz\n"); + } +} + +#[test] +#[cfg(unix)] +fn test_piped_to_dev_null() { + for &append in &[true, false] { + let s = TestScenario::new(util_name!()); + { + let dev_null = OpenOptions::new() + .write(true) + .append(append) + .open("/dev/null") + .unwrap(); + + s.ucmd() + .set_stdout(dev_null) + .pipe_in_fixture("alpha.txt") + .succeeds(); + } + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +fn test_piped_to_dev_full() { + for &append in &[true, false] { + let s = TestScenario::new(util_name!()); + { + let dev_full = OpenOptions::new() + .write(true) + .append(append) + .open("/dev/full") + .unwrap(); + + s.ucmd() + .set_stdout(dev_full) + .pipe_in_fixture("alpha.txt") + .fails() + .stderr_contains(&"No space left on device".to_owned()); + } + } +} + #[test] fn test_directory() { let s = TestScenario::new(util_name!()); @@ -327,6 +397,7 @@ fn test_dev_full_show_all() { #[cfg(unix)] fn test_domain_socket() { use std::io::prelude::*; + use std::sync::{Arc, Barrier}; use std::thread; use tempdir::TempDir; use unix_socket::UnixListener; @@ -335,17 +406,23 @@ fn test_domain_socket() { let socket_path = dir.path().join("sock"); let listener = UnixListener::bind(&socket_path).expect("failed to create socket"); + // use a barrier to ensure we don't run cat before the listener is setup + let barrier = Arc::new(Barrier::new(2)); + let barrier2 = Arc::clone(&barrier); + let thread = thread::spawn(move || { let mut stream = listener.accept().expect("failed to accept connection").0; + barrier2.wait(); stream .write_all(b"a\tb") .expect("failed to write test data"); }); - new_ucmd!() - .args(&[socket_path]) - .succeeds() - .stdout_only("a\tb"); + let child = new_ucmd!().args(&[socket_path]).run_no_wait(); + barrier.wait(); + let stdout = &child.wait_with_output().unwrap().stdout.clone(); + let output = String::from_utf8_lossy(&stdout); + assert_eq!("a\tb", output); thread.join().unwrap(); } diff --git a/tests/by-util/test_chgrp.rs b/tests/by-util/test_chgrp.rs index 343b336a6..a7848b1b6 100644 --- a/tests/by-util/test_chgrp.rs +++ b/tests/by-util/test_chgrp.rs @@ -104,7 +104,7 @@ fn test_reference() { // skip for root or MS-WSL // * MS-WSL is bugged (as of 2019-12-25), allowing non-root accounts su-level privileges for `chgrp` // * for MS-WSL, succeeds and stdout == 'group of /etc retained as root' - if !(get_effective_gid() == 0 || is_wsl()) { + if !(get_effective_gid() == 0 || uucore::os::is_wsl_1()) { new_ucmd!() .arg("-v") .arg("--reference=/etc/passwd") diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 9eda769f1..3958c0a36 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -47,7 +47,7 @@ fn run_single_test(test: &TestCase, at: AtPath, mut ucmd: UCommand) { ucmd.arg(arg); } let r = ucmd.run(); - if !r.success { + if !r.succeeded() { println!("{}", r.stderr_str()); panic!("{:?}: failed", ucmd.raw); } diff --git a/tests/by-util/test_chroot.rs b/tests/by-util/test_chroot.rs index 05efd23ae..e2e355e14 100644 --- a/tests/by-util/test_chroot.rs +++ b/tests/by-util/test_chroot.rs @@ -4,14 +4,11 @@ use crate::common::util::*; fn test_missing_operand() { let result = new_ucmd!().run(); - assert_eq!( - true, - result - .stderr - .starts_with("error: The following required arguments were not provided") - ); + assert!(result + .stderr_str() + .starts_with("error: The following required arguments were not provided")); - assert_eq!(true, result.stderr.contains("")); + assert!(result.stderr_str().contains("")); } #[test] @@ -20,14 +17,11 @@ fn test_enter_chroot_fails() { at.mkdir("jail"); - let result = ucmd.arg("jail").run(); + let result = ucmd.arg("jail").fails(); - assert_eq!( - true, - result.stderr.starts_with( - "chroot: error: cannot chroot to jail: Operation not permitted (os error 1)" - ) - ) + assert!(result + .stderr_str() + .starts_with("chroot: error: cannot chroot to jail: Operation not permitted (os error 1)")); } #[test] @@ -47,19 +41,18 @@ fn test_invalid_user_spec() { at.mkdir("a"); - let result = ucmd.arg("a").arg("--userspec=ARABA:").run(); + let result = ucmd.arg("a").arg("--userspec=ARABA:").fails(); - assert_eq!( - true, - result.stderr.starts_with("chroot: error: invalid userspec") - ); + assert!(result + .stderr_str() + .starts_with("chroot: error: invalid userspec")); } #[test] fn test_preference_of_userspec() { let scene = TestScenario::new(util_name!()); let result = scene.cmd("whoami").run(); - if is_ci() && result.stderr.contains("No such user/group") { + if is_ci() && result.stderr_str().contains("No such user/group") { // In the CI, some server are failing to return whoami. // As seems to be a configuration issue, ignoring it return; @@ -73,7 +66,7 @@ fn test_preference_of_userspec() { println!("result.stdout = {}", result.stdout_str()); println!("result.stderr = {}", result.stderr_str()); - if is_ci() && result.stderr.contains("cannot find name for user ID") { + if is_ci() && result.stderr_str().contains("cannot find name for user ID") { // In the CI, some server are failing to return id. // As seems to be a configuration issue, ignoring it return; diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index f4aabff3e..1e99da0fb 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -42,13 +42,9 @@ static TEST_MOUNT_OTHER_FILESYSTEM_FILE: &str = "mount/DO_NOT_copy_me.txt"; fn test_cp_cp() { let (at, mut ucmd) = at_and_ucmd!(); // Invoke our binary to make the copy. - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_DEST) - .run(); - - // Check that the exit code represents a successful copy. - assert!(result.success); + .succeeds(); // Check the content of the destination file that was copied. assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); @@ -57,12 +53,9 @@ fn test_cp_cp() { #[test] fn test_cp_existing_target() { let (at, mut ucmd) = at_and_ucmd!(); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_EXISTING_FILE) - .run(); - - assert!(result.success); + .succeeds(); // Check the content of the destination file assert_eq!(at.read(TEST_EXISTING_FILE), "Hello, World!\n"); @@ -74,52 +67,41 @@ fn test_cp_existing_target() { #[test] fn test_cp_duplicate_files() { let (at, mut ucmd) = at_and_ucmd!(); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_COPY_TO_FOLDER) - .run(); - - assert!(result.success); - assert!(result.stderr.contains("specified more than once")); + .succeeds() + .stderr_contains("specified more than once"); assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); } #[test] fn test_cp_multiple_files_target_is_file() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd + new_ucmd!() .arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_EXISTING_FILE) - .run(); - - assert!(!result.success); - assert!(result.stderr.contains("not a directory")); + .fails() + .stderr_contains("not a directory"); } #[test] fn test_cp_directory_not_recursive() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd + new_ucmd!() .arg(TEST_COPY_TO_FOLDER) .arg(TEST_HELLO_WORLD_DEST) - .run(); - - assert!(!result.success); - assert!(result.stderr.contains("omitting directory")); + .fails() + .stderr_contains("omitting directory"); } #[test] fn test_cp_multiple_files() { let (at, mut ucmd) = at_and_ucmd!(); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HOW_ARE_YOU_SOURCE) .arg(TEST_COPY_TO_FOLDER) - .run(); + .succeeds(); - assert!(result.success); assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); assert_eq!(at.read(TEST_HOW_ARE_YOU_DEST), "How are you?\n"); } @@ -129,14 +111,11 @@ fn test_cp_multiple_files() { #[cfg(not(macos))] fn test_cp_recurse() { let (at, mut ucmd) = at_and_ucmd!(); - - let result = ucmd - .arg("-r") + ucmd.arg("-r") .arg(TEST_COPY_FROM_FOLDER) .arg(TEST_COPY_TO_FOLDER_NEW) - .run(); + .succeeds(); - assert!(result.success); // Check the content of the destination file that was copied. assert_eq!(at.read(TEST_COPY_TO_FOLDER_NEW_FILE), "Hello, World!\n"); } @@ -144,14 +123,10 @@ fn test_cp_recurse() { #[test] fn test_cp_with_dirs_t() { let (at, mut ucmd) = at_and_ucmd!(); - - //using -t option - let result_to_dir_t = ucmd - .arg("-t") + ucmd.arg("-t") .arg(TEST_COPY_TO_FOLDER) .arg(TEST_HELLO_WORLD_SOURCE) - .run(); - assert!(result_to_dir_t.success); + .succeeds(); assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); } @@ -162,63 +137,52 @@ fn test_cp_with_dirs() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - //using -t option - let result_to_dir = scene + scene .ucmd() .arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_COPY_TO_FOLDER) - .run(); - assert!(result_to_dir.success); + .succeeds(); assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); - let result_from_dir = scene + scene .ucmd() .arg(TEST_COPY_FROM_FOLDER_FILE) .arg(TEST_HELLO_WORLD_DEST) - .run(); - assert!(result_from_dir.success); + .succeeds(); assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); } #[test] fn test_cp_arg_target_directory() { let (at, mut ucmd) = at_and_ucmd!(); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg("-t") .arg(TEST_COPY_TO_FOLDER) - .run(); + .succeeds(); - assert!(result.success); assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); } #[test] fn test_cp_arg_no_target_directory() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd + new_ucmd!() .arg(TEST_HELLO_WORLD_SOURCE) .arg("-v") .arg("-T") .arg(TEST_COPY_TO_FOLDER) - .run(); - - assert!(!result.success); - assert!(result.stderr.contains("cannot overwrite directory")); + .fails() + .stderr_contains("cannot overwrite directory"); } #[test] fn test_cp_arg_interactive() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd + new_ucmd!() .arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HOW_ARE_YOU_SOURCE) .arg("-i") .pipe_in("N\n") - .run(); - - assert!(result.success); - assert!(result.stderr.contains("Not overwriting")); + .succeeds() + .stderr_contains("Not overwriting"); } #[test] @@ -227,39 +191,33 @@ fn test_cp_arg_link() { use std::os::linux::fs::MetadataExt; let (at, mut ucmd) = at_and_ucmd!(); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg("--link") .arg(TEST_HELLO_WORLD_DEST) - .run(); + .succeeds(); - assert!(result.success); assert_eq!(at.metadata(TEST_HELLO_WORLD_SOURCE).st_nlink(), 2); } #[test] fn test_cp_arg_symlink() { let (at, mut ucmd) = at_and_ucmd!(); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg("--symbolic-link") .arg(TEST_HELLO_WORLD_DEST) - .run(); + .succeeds(); - assert!(result.success); assert!(at.is_symlink(TEST_HELLO_WORLD_DEST)); } #[test] fn test_cp_arg_no_clobber() { let (at, mut ucmd) = at_and_ucmd!(); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg("--no-clobber") .arg(TEST_HOW_ARE_YOU_SOURCE) - .run(); + .succeeds(); - assert!(result.success); assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "How are you?\n"); } @@ -267,34 +225,31 @@ fn test_cp_arg_no_clobber() { fn test_cp_arg_no_clobber_twice() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; + at.touch("source.txt"); - let result = scene + scene .ucmd() .arg("--no-clobber") .arg("source.txt") .arg("dest.txt") - .run(); + .succeeds() + .no_stderr(); - println!("stderr = {:?}", result.stderr_str()); - println!("stdout = {:?}", result.stdout_str()); - assert!(result.success); - assert!(result.stderr.is_empty()); assert_eq!(at.read("source.txt"), ""); at.append("source.txt", "some-content"); - let result = scene + scene .ucmd() .arg("--no-clobber") .arg("source.txt") .arg("dest.txt") - .run(); + .succeeds() + .stdout_does_not_contain("Not overwriting"); - assert!(result.success); assert_eq!(at.read("source.txt"), "some-content"); // Should be empty as the "no-clobber" should keep // the previous version assert_eq!(at.read("dest.txt"), ""); - assert!(!result.stderr.contains("Not overwriting")); } #[test] @@ -311,16 +266,11 @@ fn test_cp_arg_force() { permissions.set_readonly(true); set_permissions(at.plus(TEST_HELLO_WORLD_DEST), permissions).unwrap(); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg("--force") .arg(TEST_HELLO_WORLD_DEST) - .run(); + .succeeds(); - println!("{:?}", result.stderr_str()); - println!("{:?}", result.stdout_str()); - - assert!(result.success); assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); } @@ -342,13 +292,11 @@ fn test_cp_arg_remove_destination() { permissions.set_readonly(true); set_permissions(at.plus(TEST_HELLO_WORLD_DEST), permissions).unwrap(); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg("--remove-destination") .arg(TEST_HELLO_WORLD_DEST) - .run(); + .succeeds(); - assert!(result.success); assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); } @@ -356,13 +304,11 @@ fn test_cp_arg_remove_destination() { fn test_cp_arg_backup() { let (at, mut ucmd) = at_and_ucmd!(); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg("--backup") .arg(TEST_HOW_ARE_YOU_SOURCE) - .run(); + .succeeds(); - assert!(result.success); assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); assert_eq!( at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)), @@ -374,14 +320,12 @@ fn test_cp_arg_backup() { fn test_cp_arg_suffix() { let (at, mut ucmd) = at_and_ucmd!(); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg("--suffix") .arg(".bak") .arg(TEST_HOW_ARE_YOU_SOURCE) - .run(); + .succeeds(); - assert!(result.success); assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); assert_eq!( at.read(&*format!("{}.bak", TEST_HOW_ARE_YOU_SOURCE)), @@ -391,9 +335,8 @@ fn test_cp_arg_suffix() { #[test] fn test_cp_deref_conflicting_options() { - let (_at, mut ucmd) = at_and_ucmd!(); - - ucmd.arg("-LP") + new_ucmd!() + .arg("-LP") .arg(TEST_COPY_TO_FOLDER) .arg(TEST_HELLO_WORLD_SOURCE) .fails(); @@ -401,8 +344,7 @@ fn test_cp_deref_conflicting_options() { #[test] fn test_cp_deref() { - let scene = TestScenario::new(util_name!()); - let at = &scene.fixtures; + let (at, mut ucmd) = at_and_ucmd!(); #[cfg(not(windows))] let _r = fs::symlink( @@ -415,16 +357,12 @@ fn test_cp_deref() { at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK), ); //using -L option - let result = scene - .ucmd() - .arg("-L") + ucmd.arg("-L") .arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE_SYMLINK) .arg(TEST_COPY_TO_FOLDER) - .run(); + .succeeds(); - // Check that the exit code represents a successful copy. - assert!(result.success); let path_to_new_symlink = at .subdir .join(TEST_COPY_TO_FOLDER) @@ -444,8 +382,7 @@ fn test_cp_deref() { } #[test] fn test_cp_no_deref() { - let scene = TestScenario::new(util_name!()); - let at = &scene.fixtures; + let (at, mut ucmd) = at_and_ucmd!(); #[cfg(not(windows))] let _r = fs::symlink( @@ -458,16 +395,12 @@ fn test_cp_no_deref() { at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK), ); //using -P option - let result = scene - .ucmd() - .arg("-P") + ucmd.arg("-P") .arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE_SYMLINK) .arg(TEST_COPY_TO_FOLDER) - .run(); + .succeeds(); - // Check that the exit code represents a successful copy. - assert!(result.success); let path_to_new_symlink = at .subdir .join(TEST_COPY_TO_FOLDER) @@ -490,14 +423,10 @@ fn test_cp_strip_trailing_slashes() { let (at, mut ucmd) = at_and_ucmd!(); //using --strip-trailing-slashes option - let result = ucmd - .arg("--strip-trailing-slashes") + ucmd.arg("--strip-trailing-slashes") .arg(format!("{}/", TEST_HELLO_WORLD_SOURCE)) .arg(TEST_HELLO_WORLD_DEST) - .run(); - - // Check that the exit code represents a successful copy. - assert!(result.success); + .succeeds(); // Check the content of the destination file that was copied. assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); @@ -507,14 +436,11 @@ fn test_cp_strip_trailing_slashes() { fn test_cp_parents() { let (at, mut ucmd) = at_and_ucmd!(); - let result = ucmd - .arg("--parents") + ucmd.arg("--parents") .arg(TEST_COPY_FROM_FOLDER_FILE) .arg(TEST_COPY_TO_FOLDER) - .run(); + .succeeds(); - assert!(result.success); - // Check the content of the destination file that was copied. assert_eq!( at.read(&format!( "{}/{}", @@ -528,14 +454,12 @@ fn test_cp_parents() { fn test_cp_parents_multiple_files() { let (at, mut ucmd) = at_and_ucmd!(); - let result = ucmd - .arg("--parents") + ucmd.arg("--parents") .arg(TEST_COPY_FROM_FOLDER_FILE) .arg(TEST_HOW_ARE_YOU_SOURCE) .arg(TEST_COPY_TO_FOLDER) - .run(); + .succeeds(); - assert!(result.success); assert_eq!( at.read(&format!( "{}/{}", @@ -554,20 +478,12 @@ fn test_cp_parents_multiple_files() { #[test] fn test_cp_parents_dest_not_directory() { - let (_, mut ucmd) = at_and_ucmd!(); - - let result = ucmd + new_ucmd!() .arg("--parents") .arg(TEST_COPY_FROM_FOLDER_FILE) .arg(TEST_HELLO_WORLD_DEST) - .run(); - println!("{:?}", result); - - // Check that we did not succeed in copying. - assert!(!result.success); - assert!(result - .stderr - .contains("with --parents, the destination must be a directory")); + .fails() + .stderr_contains("with --parents, the destination must be a directory"); } #[test] @@ -594,18 +510,14 @@ fn test_cp_deref_folder_to_folder() { assert!(env::set_current_dir(&cwd).is_ok()); //using -P -R option - let result = scene + scene .ucmd() .arg("-L") .arg("-R") .arg("-v") .arg(TEST_COPY_FROM_FOLDER) .arg(TEST_COPY_TO_FOLDER_NEW) - .run(); - println!("cp output {}", result.stdout_str()); - - // Check that the exit code represents a successful copy. - assert!(result.success); + .succeeds(); #[cfg(not(windows))] { @@ -698,18 +610,14 @@ fn test_cp_no_deref_folder_to_folder() { assert!(env::set_current_dir(&cwd).is_ok()); //using -P -R option - let result = scene + scene .ucmd() .arg("-P") .arg("-R") .arg("-v") .arg(TEST_COPY_FROM_FOLDER) .arg(TEST_COPY_TO_FOLDER_NEW) - .run(); - println!("cp output {}", result.stdout_str()); - - // Check that the exit code represents a successful copy. - assert!(result.success); + .succeeds(); #[cfg(not(windows))] { @@ -791,13 +699,11 @@ fn test_cp_archive() { previous, ) .unwrap(); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg("--archive") .arg(TEST_HOW_ARE_YOU_SOURCE) - .run(); + .succeeds(); - assert!(result.success); assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap(); @@ -807,11 +713,10 @@ fn test_cp_archive() { let creation2 = metadata2.modified().unwrap(); let scene2 = TestScenario::new("ls"); - let result = scene2.cmd("ls").arg("-al").arg(at.subdir).run(); + let result = scene2.cmd("ls").arg("-al").arg(at.subdir).succeeds(); println!("ls dest {}", result.stdout_str()); assert_eq!(creation, creation2); - assert!(result.success); } #[test] @@ -850,11 +755,10 @@ fn test_cp_archive_recursive() { // Back to the initial cwd (breaks the other tests) assert!(env::set_current_dir(&cwd).is_ok()); - let resultg = ucmd - .arg("--archive") + ucmd.arg("--archive") .arg(TEST_COPY_TO_FOLDER) .arg(TEST_COPY_TO_FOLDER_NEW) - .run(); + .fails(); // fails for now let scene2 = TestScenario::new("ls"); let result = scene2 @@ -865,7 +769,6 @@ fn test_cp_archive_recursive() { println!("ls dest {}", result.stdout_str()); - let scene2 = TestScenario::new("ls"); let result = scene2 .cmd("ls") .arg("-al") @@ -910,9 +813,6 @@ fn test_cp_archive_recursive() { .join("2.link") .to_string_lossy() )); - - // fails for now - assert!(resultg.success); } #[test] @@ -928,13 +828,11 @@ fn test_cp_preserve_timestamps() { previous, ) .unwrap(); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg("--preserve=timestamps") .arg(TEST_HOW_ARE_YOU_SOURCE) - .run(); + .succeeds(); - assert!(result.success); assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap(); @@ -948,7 +846,6 @@ fn test_cp_preserve_timestamps() { println!("ls dest {}", result.stdout_str()); assert_eq!(creation, creation2); - assert!(result.success); } #[test] @@ -966,13 +863,11 @@ fn test_cp_dont_preserve_timestamps() { .unwrap(); sleep(Duration::from_secs(3)); - let result = ucmd - .arg(TEST_HELLO_WORLD_SOURCE) + ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg("--no-preserve=timestamps") .arg(TEST_HOW_ARE_YOU_SOURCE) - .run(); + .succeeds(); - assert!(result.success); assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap(); @@ -992,7 +887,6 @@ fn test_cp_dont_preserve_timestamps() { // Some margins with time check assert!(res.as_secs() > 3595); assert!(res.as_secs() < 3605); - assert!(result.success); } #[test] @@ -1017,7 +911,7 @@ fn test_cp_one_file_system() { let scene = TestScenario::new(util_name!()); // Test must be run as root (or with `sudo -E`) - if scene.cmd("whoami").run().stdout != "root\n" { + if scene.cmd("whoami").run().stdout_str() != "root\n" { return; } @@ -1042,17 +936,16 @@ fn test_cp_one_file_system() { at_src.touch(TEST_MOUNT_OTHER_FILESYSTEM_FILE); // Begin testing -x flag - let result = scene + scene .ucmd() .arg("-rx") .arg(TEST_MOUNT_COPY_FROM_FOLDER) .arg(TEST_COPY_TO_FOLDER_NEW) - .run(); + .succeeds(); // Ditch the mount before the asserts scene.cmd("umount").arg(mountpoint_path).succeeds(); - assert!(result.success); assert!(!at_dst.file_exists(TEST_MOUNT_OTHER_FILESYSTEM_FILE)); // Check if the other files were copied from the source folder hirerarchy for entry in WalkDir::new(at_src.as_string()) { @@ -1072,3 +965,59 @@ fn test_cp_one_file_system() { } } } + +#[test] +#[cfg(any(target_os = "linux", target_os = "macos"))] +fn test_cp_reflink_always() { + let (at, mut ucmd) = at_and_ucmd!(); + let result = ucmd + .arg("--reflink=always") + .arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_EXISTING_FILE) + .run(); + + if result.succeeded() { + // Check the content of the destination file + assert_eq!(at.read(TEST_EXISTING_FILE), "Hello, World!\n"); + } else { + // Older Linux versions do not support cloning. + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "macos"))] +fn test_cp_reflink_auto() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.arg("--reflink=auto") + .arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_EXISTING_FILE) + .succeeds(); + + // Check the content of the destination file + assert_eq!(at.read(TEST_EXISTING_FILE), "Hello, World!\n"); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "macos"))] +fn test_cp_reflink_never() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.arg("--reflink=never") + .arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_EXISTING_FILE) + .succeeds(); + + // Check the content of the destination file + assert_eq!(at.read(TEST_EXISTING_FILE), "Hello, World!\n"); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "macos"))] +fn test_cp_reflink_bad() { + let (_, mut ucmd) = at_and_ucmd!(); + let _result = ucmd + .arg("--reflink=bad") + .arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_EXISTING_FILE) + .fails() + .stderr_contains("invalid argument"); +} diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 1933fdba3..0ca0a74ea 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -7,174 +7,147 @@ use rust_users::*; #[test] fn test_date_email() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg("--rfc-email").run(); - assert!(result.success); + new_ucmd!().arg("--rfc-email").succeeds(); } #[test] fn test_date_email2() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg("-R").run(); - assert!(result.success); + new_ucmd!().arg("-R").succeeds(); } #[test] fn test_date_rfc_3339() { let scene = TestScenario::new(util_name!()); - let mut result = scene.ucmd().arg("--rfc-3339=ns").succeeds(); + let rfc_regexp = concat!( + r#"(\d+)-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])\s([01]\d|2[0-3]):"#, + r#"([0-5]\d):([0-5]\d|60)(\.\d+)?(([Zz])|([\+|\-]([01]\d|2[0-3])))"# + ); + let re = Regex::new(rfc_regexp).unwrap(); // Check that the output matches the regexp - let rfc_regexp = r"(\d+)-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])\s([01]\d|2[0-3]):([0-5]\d):([0-5]\d|60)(\.\d+)?(([Zz])|([\+|\-]([01]\d|2[0-3])))"; - let re = Regex::new(rfc_regexp).unwrap(); - assert!(re.is_match(&result.stdout_str().trim())); + scene + .ucmd() + .arg("--rfc-3339=ns") + .succeeds() + .stdout_matches(&re); - result = scene.ucmd().arg("--rfc-3339=seconds").succeeds(); - - // Check that the output matches the regexp - let re = Regex::new(rfc_regexp).unwrap(); - assert!(re.is_match(&result.stdout_str().trim())); + scene + .ucmd() + .arg("--rfc-3339=seconds") + .succeeds() + .stdout_matches(&re); } #[test] fn test_date_rfc_8601() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg("--iso-8601=ns").run(); - assert!(result.success); + new_ucmd!().arg("--iso-8601=ns").succeeds(); } #[test] fn test_date_rfc_8601_second() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg("--iso-8601=second").run(); - assert!(result.success); + new_ucmd!().arg("--iso-8601=second").succeeds(); } #[test] fn test_date_utc() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg("--utc").run(); - assert!(result.success); + new_ucmd!().arg("--utc").succeeds(); } #[test] fn test_date_universal() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg("--universal").run(); - assert!(result.success); + new_ucmd!().arg("--universal").succeeds(); } #[test] fn test_date_format_y() { let scene = TestScenario::new(util_name!()); - let mut result = scene.ucmd().arg("+%Y").succeeds(); - - assert!(result.success); let mut re = Regex::new(r"^\d{4}$").unwrap(); - assert!(re.is_match(&result.stdout_str().trim())); + scene.ucmd().arg("+%Y").succeeds().stdout_matches(&re); - result = scene.ucmd().arg("+%y").succeeds(); - - assert!(result.success); re = Regex::new(r"^\d{2}$").unwrap(); - assert!(re.is_match(&result.stdout_str().trim())); + scene.ucmd().arg("+%y").succeeds().stdout_matches(&re); } #[test] fn test_date_format_m() { let scene = TestScenario::new(util_name!()); - let mut result = scene.ucmd().arg("+%b").succeeds(); - - assert!(result.success); let mut re = Regex::new(r"\S+").unwrap(); - assert!(re.is_match(&result.stdout_str().trim())); + scene.ucmd().arg("+%b").succeeds().stdout_matches(&re); - result = scene.ucmd().arg("+%m").succeeds(); - - assert!(result.success); re = Regex::new(r"^\d{2}$").unwrap(); - assert!(re.is_match(&result.stdout_str().trim())); + scene.ucmd().arg("+%m").succeeds().stdout_matches(&re); } #[test] fn test_date_format_day() { let scene = TestScenario::new(util_name!()); - let mut result = scene.ucmd().arg("+%a").succeeds(); - - assert!(result.success); let mut re = Regex::new(r"\S+").unwrap(); - assert!(re.is_match(&result.stdout_str().trim())); - - result = scene.ucmd().arg("+%A").succeeds(); - - assert!(result.success); + scene.ucmd().arg("+%a").succeeds().stdout_matches(&re); re = Regex::new(r"\S+").unwrap(); - assert!(re.is_match(&result.stdout_str().trim())); + scene.ucmd().arg("+%A").succeeds().stdout_matches(&re); - result = scene.ucmd().arg("+%u").succeeds(); - - assert!(result.success); re = Regex::new(r"^\d{1}$").unwrap(); - assert!(re.is_match(&result.stdout_str().trim())); + scene.ucmd().arg("+%u").succeeds().stdout_matches(&re); } #[test] fn test_date_format_full_day() { - let scene = TestScenario::new(util_name!()); - - let result = scene.ucmd().arg("+'%a %Y-%m-%d'").succeeds(); - - assert!(result.success); let re = Regex::new(r"\S+ \d{4}-\d{2}-\d{2}").unwrap(); - assert!(re.is_match(&result.stdout_str().trim())); + new_ucmd!() + .arg("+'%a %Y-%m-%d'") + .succeeds() + .stdout_matches(&re); } #[test] #[cfg(all(unix, not(target_os = "macos")))] fn test_date_set_valid() { if get_effective_uid() == 0 { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd + new_ucmd!() .arg("--set") .arg("2020-03-12 13:30:00+08:00") - .succeeds(); - result.no_stdout().no_stderr(); + .succeeds() + .no_stdout() + .no_stderr(); } } #[test] #[cfg(any(windows, all(unix, not(target_os = "macos"))))] fn test_date_set_invalid() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg("--set").arg("123abcd").fails(); - let result = result.no_stdout(); - assert!(result.stderr.starts_with("date: invalid date ")); + let result = new_ucmd!().arg("--set").arg("123abcd").fails(); + result.no_stdout(); + assert!(result.stderr_str().starts_with("date: invalid date ")); } #[test] #[cfg(all(unix, not(target_os = "macos")))] fn test_date_set_permissions_error() { - if !(get_effective_uid() == 0 || is_wsl()) { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg("--set").arg("2020-03-11 21:45:00+08:00").fails(); - let result = result.no_stdout(); - assert!(result.stderr.starts_with("date: cannot set date: ")); + if !(get_effective_uid() == 0 || uucore::os::is_wsl_1()) { + let result = new_ucmd!() + .arg("--set") + .arg("2020-03-11 21:45:00+08:00") + .fails(); + result.no_stdout(); + assert!(result.stderr_str().starts_with("date: cannot set date: ")); } } #[test] #[cfg(target_os = "macos")] fn test_date_set_mac_unavailable() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg("--set").arg("2020-03-11 21:45:00+08:00").fails(); - let result = result.no_stdout(); + let result = new_ucmd!() + .arg("--set") + .arg("2020-03-11 21:45:00+08:00") + .fails(); + result.no_stdout(); assert!(result - .stderr + .stderr_str() .starts_with("date: setting the date is not supported by macOS")); } @@ -183,13 +156,12 @@ fn test_date_set_mac_unavailable() { /// TODO: expected to fail currently; change to succeeds() when required. fn test_date_set_valid_2() { if get_effective_uid() == 0 { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd + let result = new_ucmd!() .arg("--set") .arg("Sat 20 Mar 2021 14:53:01 AWST") .fails(); - let result = result.no_stdout(); - assert!(result.stderr.starts_with("date: invalid date ")); + result.no_stdout(); + assert!(result.stderr_str().starts_with("date: invalid date ")); } } @@ -198,13 +170,12 @@ fn test_date_set_valid_2() { /// TODO: expected to fail currently; change to succeeds() when required. fn test_date_set_valid_3() { if get_effective_uid() == 0 { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd + let result = new_ucmd!() .arg("--set") .arg("Sat 20 Mar 2021 14:53:01") // Local timezone .fails(); - let result = result.no_stdout(); - assert!(result.stderr.starts_with("date: invalid date ")); + result.no_stdout(); + assert!(result.stderr_str().starts_with("date: invalid date ")); } } @@ -213,12 +184,11 @@ fn test_date_set_valid_3() { /// TODO: expected to fail currently; change to succeeds() when required. fn test_date_set_valid_4() { if get_effective_uid() == 0 { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd + let result = new_ucmd!() .arg("--set") .arg("2020-03-11 21:45:00") // Local timezone .fails(); - let result = result.no_stdout(); - assert!(result.stderr.starts_with("date: invalid date ")); + result.no_stdout(); + assert!(result.stderr_str().starts_with("date: invalid date ")); } } diff --git a/tests/by-util/test_df.rs b/tests/by-util/test_df.rs index f79d1beb5..0ae8d2339 100644 --- a/tests/by-util/test_df.rs +++ b/tests/by-util/test_df.rs @@ -2,30 +2,22 @@ use crate::common::util::*; #[test] fn test_df_compatible_no_size_arg() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg("-a").run(); - assert!(result.success); + new_ucmd!().arg("-a").succeeds(); } #[test] fn test_df_compatible() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg("-ah").run(); - assert!(result.success); + new_ucmd!().arg("-ah").succeeds(); } #[test] fn test_df_compatible_type() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg("-aT").run(); - assert!(result.success); + new_ucmd!().arg("-aT").succeeds(); } #[test] fn test_df_compatible_si() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg("-aH").run(); - assert!(result.success); + new_ucmd!().arg("-aH").succeeds(); } // ToDO: more tests... diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index ea6b18937..111f2dc90 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -10,7 +10,7 @@ fn test_du_basics() { new_ucmd!().succeeds().no_stderr(); } #[cfg(target_vendor = "apple")] -fn _du_basics(s: String) { +fn _du_basics(s: &str) { let answer = "32\t./subdir 8\t./subdir/deeper 24\t./subdir/links @@ -30,11 +30,18 @@ fn _du_basics(s: &str) { #[test] fn test_du_basics_subdir() { - let (_at, mut ucmd) = at_and_ucmd!(); + let scene = TestScenario::new(util_name!()); - let result = ucmd.arg(SUB_DIR).run(); - assert!(result.success); - assert_eq!(result.stderr, ""); + let result = scene.ucmd().arg(SUB_DIR).succeeds(); + + #[cfg(target_os = "linux")] + { + let result_reference = scene.cmd("du").arg(SUB_DIR).run(); + if result_reference.succeeded() { + assert_eq!(result.stdout_str(), result_reference.stdout_str()); + return; + } + } _du_basics_subdir(result.stdout_str()); } @@ -49,7 +56,7 @@ fn _du_basics_subdir(s: &str) { #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] fn _du_basics_subdir(s: &str) { // MS-WSL linux has altered expected output - if !is_wsl() { + if !uucore::os::is_wsl_1() { assert_eq!(s, "8\tsubdir/deeper\n"); } else { assert_eq!(s, "0\tsubdir/deeper\n"); @@ -58,26 +65,29 @@ fn _du_basics_subdir(s: &str) { #[test] fn test_du_basics_bad_name() { - let (_at, mut ucmd) = at_and_ucmd!(); - - let result = ucmd.arg("bad_name").run(); - assert_eq!(result.stdout_str(), ""); - assert_eq!( - result.stderr, - "du: error: bad_name: No such file or directory\n" - ); + new_ucmd!() + .arg("bad_name") + .succeeds() // TODO: replace with ".fails()" once `du` is fixed + .stderr_only("du: error: bad_name: No such file or directory\n"); } #[test] fn test_du_soft_link() { - let ts = TestScenario::new("du"); + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; - let link = ts.ccmd("ln").arg("-s").arg(SUB_FILE).arg(SUB_LINK).run(); - assert!(link.success); + at.symlink_file(SUB_FILE, SUB_LINK); - let result = ts.ucmd().arg(SUB_DIR_LINKS).run(); - assert!(result.success); - assert_eq!(result.stderr, ""); + let result = scene.ucmd().arg(SUB_DIR_LINKS).succeeds(); + + #[cfg(target_os = "linux")] + { + let result_reference = scene.cmd("du").arg(SUB_DIR_LINKS).run(); + if result_reference.succeeded() { + assert_eq!(result.stdout_str(), result_reference.stdout_str()); + return; + } + } _du_soft_link(result.stdout_str()); } @@ -93,7 +103,7 @@ fn _du_soft_link(s: &str) { #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] fn _du_soft_link(s: &str) { // MS-WSL linux has altered expected output - if !is_wsl() { + if !uucore::os::is_wsl_1() { assert_eq!(s, "16\tsubdir/links\n"); } else { assert_eq!(s, "8\tsubdir/links\n"); @@ -102,14 +112,23 @@ fn _du_soft_link(s: &str) { #[test] fn test_du_hard_link() { - let ts = TestScenario::new("du"); + let scene = TestScenario::new(util_name!()); - let link = ts.ccmd("ln").arg(SUB_FILE).arg(SUB_LINK).run(); - assert!(link.success); + let result_ln = scene.cmd("ln").arg(SUB_FILE).arg(SUB_LINK).run(); + if !result_ln.succeeded() { + scene.ccmd("ln").arg(SUB_FILE).arg(SUB_LINK).succeeds(); + } - let result = ts.ucmd().arg(SUB_DIR_LINKS).run(); - assert!(result.success); - assert_eq!(result.stderr, ""); + let result = scene.ucmd().arg(SUB_DIR_LINKS).succeeds(); + + #[cfg(target_os = "linux")] + { + let result_reference = scene.cmd("du").arg(SUB_DIR_LINKS).run(); + if result_reference.succeeded() { + assert_eq!(result.stdout_str(), result_reference.stdout_str()); + return; + } + } // We do not double count hard links as the inodes are identical _du_hard_link(result.stdout_str()); } @@ -125,7 +144,7 @@ fn _du_hard_link(s: &str) { #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] fn _du_hard_link(s: &str) { // MS-WSL linux has altered expected output - if !is_wsl() { + if !uucore::os::is_wsl_1() { assert_eq!(s, "16\tsubdir/links\n"); } else { assert_eq!(s, "8\tsubdir/links\n"); @@ -134,11 +153,23 @@ fn _du_hard_link(s: &str) { #[test] fn test_du_d_flag() { - let ts = TestScenario::new("du"); + let scene = TestScenario::new(util_name!()); - let result = ts.ucmd().arg("-d").arg("1").run(); - assert!(result.success); - assert_eq!(result.stderr, ""); + let result = scene.ucmd().arg("-d1").succeeds(); + + #[cfg(target_os = "linux")] + { + let result_reference = scene.cmd("du").arg("-d1").run(); + if result_reference.succeeded() { + assert_eq!( + // TODO: gnu `du` doesn't use trailing "/" here + // result.stdout_str(), result_reference.stdout_str() + result.stdout_str().trim_end_matches("/\n"), + result_reference.stdout_str().trim_end_matches("\n") + ); + return; + } + } _du_d_flag(result.stdout_str()); } @@ -153,7 +184,7 @@ fn _du_d_flag(s: &str) { #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] fn _du_d_flag(s: &str) { // MS-WSL linux has altered expected output - if !is_wsl() { + if !uucore::os::is_wsl_1() { assert_eq!(s, "28\t./subdir\n36\t./\n"); } else { assert_eq!(s, "8\t./subdir\n8\t./\n"); @@ -162,9 +193,7 @@ fn _du_d_flag(s: &str) { #[test] fn test_du_h_flag_empty_file() { - let ts = TestScenario::new("du"); - - ts.ucmd() + new_ucmd!() .arg("-h") .arg("empty.txt") .succeeds() @@ -174,54 +203,61 @@ fn test_du_h_flag_empty_file() { #[cfg(feature = "touch")] #[test] fn test_du_time() { - let ts = TestScenario::new("du"); + let scene = TestScenario::new(util_name!()); - let touch = ts + scene .ccmd("touch") .arg("-a") .arg("-m") .arg("-t") .arg("201505150000") .arg("date_test") - .run(); - assert!(touch.success); + .succeeds(); - let result = ts.ucmd().arg("--time").arg("date_test").run(); - - // cleanup by removing test file - ts.cmd("rm").arg("date_test").run(); - - assert!(result.success); - assert_eq!(result.stderr, ""); - assert_eq!(result.stdout, "0\t2015-05-15 00:00\tdate_test\n"); + scene + .ucmd() + .arg("--time") + .arg("date_test") + .succeeds() + .stdout_only("0\t2015-05-15 00:00\tdate_test\n"); } #[cfg(not(target_os = "windows"))] #[cfg(feature = "chmod")] #[test] fn test_du_no_permission() { - let ts = TestScenario::new("du"); + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; - let chmod = ts.ccmd("chmod").arg("-r").arg(SUB_DIR_LINKS).run(); - println!("chmod output: {:?}", chmod); - assert!(chmod.success); - let result = ts.ucmd().arg(SUB_DIR_LINKS).run(); + at.mkdir_all(SUB_DIR_LINKS); - ts.ccmd("chmod").arg("+r").arg(SUB_DIR_LINKS).run(); + scene.ccmd("chmod").arg("-r").arg(SUB_DIR_LINKS).succeeds(); - assert!(result.success); - assert_eq!( - result.stderr, - "du: cannot read directory ‘subdir/links‘: Permission denied (os error 13)\n" + let result = scene.ucmd().arg(SUB_DIR_LINKS).run(); // TODO: replace with ".fails()" once `du` is fixed + result.stderr_contains( + "du: cannot read directory ‘subdir/links‘: Permission denied (os error 13)", ); - _du_no_permission(result.stdout); + + #[cfg(target_os = "linux")] + { + let result_reference = scene.cmd("du").arg(SUB_DIR_LINKS).fails(); + if result_reference + .stderr_str() + .contains("du: cannot read directory 'subdir/links': Permission denied") + { + assert_eq!(result.stdout_str(), result_reference.stdout_str()); + return; + } + } + + _du_no_permission(result.stdout_str()); } #[cfg(target_vendor = "apple")] -fn _du_no_permission(s: String) { +fn _du_no_permission(s: &str) { assert_eq!(s, "0\tsubdir/links\n"); } #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] -fn _du_no_permission(s: String) { +fn _du_no_permission(s: &str) { assert_eq!(s, "4\tsubdir/links\n"); } diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 39baf473b..e86a41783 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -140,8 +140,11 @@ fn test_unset_variable() { #[test] fn test_fail_null_with_program() { - let out = new_ucmd!().arg("--null").arg("cd").fails().stderr; - assert!(out.contains("cannot specify --null (-0) with command")); + new_ucmd!() + .arg("--null") + .arg("cd") + .fails() + .stderr_contains("cannot specify --null (-0) with command"); } #[cfg(not(windows))] diff --git a/tests/by-util/test_fmt.rs b/tests/by-util/test_fmt.rs index f962a9137..21a5f3396 100644 --- a/tests/by-util/test_fmt.rs +++ b/tests/by-util/test_fmt.rs @@ -29,7 +29,7 @@ fn test_fmt_w_too_big() { .run(); //.stdout_is_fixture("call_graph.expected"); assert_eq!( - result.stderr.trim(), + result.stderr_str().trim(), "fmt: error: invalid width: '2501': Numerical result out of range" ); } diff --git a/tests/by-util/test_groups.rs b/tests/by-util/test_groups.rs index 32a16cc1a..cee13bdc3 100644 --- a/tests/by-util/test_groups.rs +++ b/tests/by-util/test_groups.rs @@ -10,7 +10,7 @@ fn test_groups() { // As seems to be a configuration issue, ignoring it return; } - assert!(result.success); + result.success(); assert!(!result.stdout_str().trim().is_empty()); } @@ -30,16 +30,12 @@ fn test_groups_arg() { println!("result.stdout = {}", result.stdout_str()); println!("result.stderr = {}", result.stderr_str()); - assert!(result.success); + result.success(); assert!(!result.stdout_str().is_empty()); let username = result.stdout_str().trim(); // call groups with the user name to check that we // are getting something - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.arg(username).run(); - println!("result.stdout = {}", result.stdout_str()); - println!("result.stderr = {}", result.stderr_str()); - assert!(result.success); + new_ucmd!().arg(username).succeeds(); assert!(!result.stdout_str().is_empty()); } diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index d91cc1289..4f009c800 100755 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -156,14 +156,10 @@ fn test_negative_zero_bytes() { } #[test] fn test_no_such_file_or_directory() { - let result = new_ucmd!().arg("no_such_file.toml").run(); - - assert_eq!( - true, - result - .stderr - .contains("cannot open 'no_such_file.toml' for reading: No such file or directory") - ) + new_ucmd!() + .arg("no_such_file.toml") + .fails() + .stderr_contains("cannot open 'no_such_file.toml' for reading: No such file or directory"); } // there was a bug not caught by previous tests diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index dfaaabce6..fc4459072 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -11,12 +11,10 @@ use std::thread::sleep; fn test_install_help() { let (_, mut ucmd) = at_and_ucmd!(); - assert!(ucmd - .arg("--help") + ucmd.arg("--help") .succeeds() .no_stderr() - .stdout - .contains("FLAGS:")); + .stdout_contains("FLAGS:"); } #[test] @@ -59,13 +57,11 @@ fn test_install_failing_not_dir() { at.touch(file1); at.touch(file2); at.touch(file3); - assert!(ucmd - .arg(file1) + ucmd.arg(file1) .arg(file2) .arg(file3) .fails() - .stderr - .contains("not a directory")); + .stderr_contains("not a directory"); } #[test] @@ -77,13 +73,11 @@ fn test_install_unimplemented_arg() { at.touch(file); at.mkdir(dir); - assert!(ucmd - .arg(context_arg) + ucmd.arg(context_arg) .arg(file) .arg(dir) .fails() - .stderr - .contains("Unimplemented")); + .stderr_contains("Unimplemented"); assert!(!at.file_exists(&format!("{}/{}", dir, file))); } @@ -231,13 +225,11 @@ fn test_install_mode_failing() { at.touch(file); at.mkdir(dir); - assert!(ucmd - .arg(file) + ucmd.arg(file) .arg(dir) .arg(mode_arg) .fails() - .stderr - .contains("Invalid mode string: invalid digit found in string")); + .stderr_contains("Invalid mode string: invalid digit found in string"); let dest_file = &format!("{}/{}", dir, file); assert!(at.file_exists(file)); @@ -336,7 +328,7 @@ fn test_install_target_new_file_with_owner() { .arg(format!("{}/{}", dir, file)) .run(); - if is_ci() && result.stderr.contains("error: no such user:") { + if is_ci() && result.stderr_str().contains("error: no such user:") { // In the CI, some server are failing to return the user id. // As seems to be a configuration issue, ignoring it return; @@ -359,7 +351,7 @@ fn test_install_target_new_file_failing_nonexistent_parent() { ucmd.arg(file1) .arg(format!("{}/{}", dir, file2)) .fails() - .stderr_contains(&"not a directory"); + .stderr_contains(&"No such file or directory"); } #[test] @@ -619,33 +611,64 @@ fn test_install_and_strip_with_program() { #[test] #[cfg(not(windows))] fn test_install_and_strip_with_invalid_program() { - let scene = TestScenario::new(util_name!()); - - let stderr = scene - .ucmd() + new_ucmd!() .arg("-s") .arg("--strip-program") .arg("/bin/date") .arg(strip_source_file()) .arg(STRIP_TARGET_FILE) .fails() - .stderr; - assert!(stderr.contains("strip program failed")); + .stderr_contains("strip program failed"); } #[test] #[cfg(not(windows))] fn test_install_and_strip_with_non_existent_program() { - let scene = TestScenario::new(util_name!()); - - let stderr = scene - .ucmd() + new_ucmd!() .arg("-s") .arg("--strip-program") .arg("/usr/bin/non_existent_program") .arg(strip_source_file()) .arg(STRIP_TARGET_FILE) .fails() - .stderr; - assert!(stderr.contains("No such file or directory")); + .stderr_contains("No such file or directory"); +} + +#[test] +fn test_install_creating_leading_dirs() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let source = "create_leading_test_file"; + let target = "dir1/dir2/dir3/test_file"; + + at.touch(source); + + scene + .ucmd() + .arg("-D") + .arg(source) + .arg(at.plus(target)) + .succeeds() + .no_stderr(); +} + +#[test] +#[cfg(not(windows))] +fn test_install_creating_leading_dir_fails_on_long_name() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let source = "create_leading_test_file"; + let target = format!("{}/test_file", "d".repeat(libc::PATH_MAX as usize + 1)); + + at.touch(source); + + scene + .ucmd() + .arg("-D") + .arg(source) + .arg(at.plus(target.as_str())) + .fails() + .stderr_contains("failed to create"); } diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index d7a13b0d4..646091b09 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -299,13 +299,11 @@ fn test_symlink_overwrite_dir_fail() { at.touch(path_a); at.mkdir(path_b); - assert!( - ucmd.args(&["-s", "-T", path_a, path_b]) - .fails() - .stderr - .len() - > 0 - ); + assert!(!ucmd + .args(&["-s", "-T", path_a, path_b]) + .fails() + .stderr_str() + .is_empty()); } #[test] @@ -358,7 +356,11 @@ fn test_symlink_target_only() { at.mkdir(dir); - assert!(ucmd.args(&["-s", "-t", dir]).fails().stderr.len() > 0); + assert!(!ucmd + .args(&["-s", "-t", dir]) + .fails() + .stderr_str() + .is_empty()); } #[test] diff --git a/tests/by-util/test_logname.rs b/tests/by-util/test_logname.rs index 8d1996e63..bd9d04a50 100644 --- a/tests/by-util/test_logname.rs +++ b/tests/by-util/test_logname.rs @@ -9,7 +9,7 @@ fn test_normal() { for (key, value) in env::vars() { println!("{}: {}", key, value); } - if (is_ci() || is_wsl()) && result.stderr_str().contains("error: no login name") { + if (is_ci() || uucore::os::is_wsl_1()) && result.stderr_str().contains("error: no login name") { // ToDO: investigate WSL failure // In the CI, some server are failing to return logname. // As seems to be a configuration issue, ignoring it diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index d810cdc29..09e02f264 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -103,6 +103,20 @@ fn test_ls_width() { .succeeds() .stdout_only("test-width-1\ntest-width-2\ntest-width-3\ntest-width-4\n"); } + + scene + .ucmd() + .arg("-w=bad") + .fails() + .stderr_contains("invalid line width"); + + for option in &["-w 1a", "-w=1a", "--width=1a", "--width 1a"] { + scene + .ucmd() + .args(&option.split(" ").collect::>()) + .fails() + .stderr_only("ls: error: invalid line width: ‘1a’"); + } } #[test] @@ -436,6 +450,39 @@ fn test_ls_deref() { assert!(!re.is_match(result.stdout_str().trim())); } +#[test] +fn test_ls_sort_none() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("test-3"); + at.touch("test-1"); + at.touch("test-2"); + + // Order is not specified so we just check that it doesn't + // give any errors. + scene.ucmd().arg("--sort=none").succeeds(); + scene.ucmd().arg("-U").succeeds(); +} + +#[test] +fn test_ls_sort_name() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("test-3"); + at.touch("test-1"); + at.touch("test-2"); + + let sep = if cfg!(unix) { "\n" } else { " " }; + + scene + .ucmd() + .arg("--sort=name") + .succeeds() + .stdout_is(["test-1", "test-2", "test-3\n"].join(sep)); +} + #[test] fn test_ls_order_size() { let scene = TestScenario::new(util_name!()); @@ -464,6 +511,18 @@ fn test_ls_order_size() { result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n"); #[cfg(windows)] result.stdout_only("test-1 test-2 test-3 test-4\n"); + + let result = scene.ucmd().arg("--sort=size").succeeds(); + #[cfg(not(windows))] + result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n"); + #[cfg(windows)] + result.stdout_only("test-4 test-3 test-2 test-1\n"); + + let result = scene.ucmd().arg("--sort=size").arg("-r").succeeds(); + #[cfg(not(windows))] + result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n"); + #[cfg(windows)] + result.stdout_only("test-1 test-2 test-3 test-4\n"); } #[test] @@ -472,13 +531,16 @@ fn test_ls_long_ctime() { let at = &scene.fixtures; at.touch("test-long-ctime-1"); - let result = scene.ucmd().arg("-lc").succeeds(); - // Should show the time on Unix, but question marks on windows. - #[cfg(unix)] - result.stdout_contains(":"); - #[cfg(not(unix))] - result.stdout_contains("???"); + for arg in &["-c", "--time=ctime", "--time=status"] { + let result = scene.ucmd().arg("-l").arg(arg).succeeds(); + + // Should show the time on Unix, but question marks on windows. + #[cfg(unix)] + result.stdout_contains(":"); + #[cfg(not(unix))] + result.stdout_contains("???"); + } } #[test] @@ -519,32 +581,46 @@ fn test_ls_order_time() { #[cfg(windows)] result.stdout_only("test-4 test-3 test-2 test-1\n"); + let result = scene.ucmd().arg("--sort=time").succeeds(); + #[cfg(not(windows))] + result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n"); + #[cfg(windows)] + result.stdout_only("test-4 test-3 test-2 test-1\n"); + let result = scene.ucmd().arg("-tr").succeeds(); #[cfg(not(windows))] result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n"); #[cfg(windows)] result.stdout_only("test-1 test-2 test-3 test-4\n"); + let result = scene.ucmd().arg("--sort=time").arg("-r").succeeds(); + #[cfg(not(windows))] + result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n"); + #[cfg(windows)] + result.stdout_only("test-1 test-2 test-3 test-4\n"); + // 3 was accessed last in the read // So the order should be 2 3 4 1 - let result = scene.ucmd().arg("-tu").succeeds(); - let file3_access = at.open("test-3").metadata().unwrap().accessed().unwrap(); - let file4_access = at.open("test-4").metadata().unwrap().accessed().unwrap(); + 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(); - // It seems to be dependent on the platform whether the access time is actually set - if file3_access > file4_access { - if cfg!(not(windows)) { - result.stdout_only("test-3\ntest-4\ntest-2\ntest-1\n"); + // It seems to be dependent on the platform whether the access time is actually set + if file3_access > file4_access { + if cfg!(not(windows)) { + result.stdout_only("test-3\ntest-4\ntest-2\ntest-1\n"); + } else { + result.stdout_only("test-3 test-4 test-2 test-1\n"); + } } else { - result.stdout_only("test-3 test-4 test-2 test-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 - if cfg!(not(windows)) { - result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n"); - } else { - result.stdout_only("test-4 test-3 test-2 test-1\n"); + // Access time does not seem to be set on Windows and some other + // systems so the order is 4 3 2 1 + if cfg!(not(windows)) { + result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n"); + } else { + result.stdout_only("test-4 test-3 test-2 test-1\n"); + } } } @@ -621,20 +697,27 @@ fn test_ls_recursive() { result.stdout_contains(&"a\\b:\nb"); } -#[cfg(unix)] #[test] -fn test_ls_ls_color() { +fn test_ls_color() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; at.mkdir("a"); - at.mkdir("a/nested_dir"); + let nested_dir = Path::new("a") + .join("nested_dir") + .to_string_lossy() + .to_string(); + at.mkdir(&nested_dir); at.mkdir("z"); - at.touch(&at.plus_as_string("a/nested_file")); + let nested_file = Path::new("a") + .join("nested_file") + .to_string_lossy() + .to_string(); + at.touch(&nested_file); at.touch("test-color"); - let a_with_colors = "\x1b[01;34ma\x1b[0m"; - let z_with_colors = "\x1b[01;34mz\x1b[0m"; - let nested_dir_with_colors = "\x1b[01;34mnested_dir\x1b[0m"; + let a_with_colors = "\x1b[1;34ma\x1b[0m"; + let z_with_colors = "\x1b[1;34mz\x1b[0m"; + let nested_dir_with_colors = "\x1b[1;34mnested_dir\x1b[0m"; // Color is disabled by default let result = scene.ucmd().succeeds(); @@ -670,14 +753,6 @@ fn test_ls_ls_color() { .succeeds() .stdout_contains(nested_dir_with_colors); - // Color has no effect - scene - .ucmd() - .arg("--color=always") - .arg("a/nested_file") - .succeeds() - .stdout_contains("a/nested_file\n"); - // No output scene .ucmd() @@ -817,7 +892,7 @@ fn test_ls_indicator_style() { let options = vec!["classify", "file-type", "slash"]; for opt in options { // Verify that classify and file-type both contain indicators for symlinks. - let result = scene + scene .ucmd() .arg(format!("--indicator-style={}", opt)) .succeeds() @@ -827,7 +902,7 @@ fn test_ls_indicator_style() { // Same test as above, but with the alternate flags. let options = vec!["--classify", "--file-type", "-p"]; for opt in options { - let result = scene + scene .ucmd() .arg(format!("{}", opt)) .succeeds() @@ -838,7 +913,7 @@ fn test_ls_indicator_style() { let options = vec!["classify", "file-type"]; for opt in options { // Verify that classify and file-type both contain indicators for symlinks. - let result = scene + scene .ucmd() .arg(format!("--indicator-style={}", opt)) .succeeds() @@ -962,7 +1037,7 @@ fn test_ls_hidden_windows() { let result = scene.ucmd().succeeds(); assert!(!result.stdout_str().contains(file)); - let result = scene.ucmd().arg("-a").succeeds().stdout_contains(file); + scene.ucmd().arg("-a").succeeds().stdout_contains(file); } #[test] @@ -1052,9 +1127,11 @@ fn test_ls_quoting_style() { at.touch("one"); // It seems that windows doesn't allow \n in filenames. + // And it also doesn't like \, of course. #[cfg(unix)] { at.touch("one\ntwo"); + at.touch("one\\two"); // Default is shell-escape scene .ucmd() @@ -1116,6 +1193,42 @@ fn test_ls_quoting_style() { .succeeds() .stdout_only(format!("{}\n", correct)); } + + for (arg, correct) in &[ + ("--quoting-style=literal", "one\\two"), + ("-N", "one\\two"), + ("--quoting-style=c", "\"one\\\\two\""), + ("-Q", "\"one\\\\two\""), + ("--quote-name", "\"one\\\\two\""), + ("--quoting-style=escape", "one\\\\two"), + ("-b", "one\\\\two"), + ("--quoting-style=shell-escape", "'one\\two'"), + ("--quoting-style=shell-escape-always", "'one\\two'"), + ("--quoting-style=shell", "'one\\two'"), + ("--quoting-style=shell-always", "'one\\two'"), + ] { + scene + .ucmd() + .arg(arg) + .arg("one\\two") + .succeeds() + .stdout_only(format!("{}\n", correct)); + } + + // Tests for a character that forces quotation in shell-style escaping + // after a character in a dollar expression + at.touch("one\n&two"); + for (arg, correct) in &[ + ("--quoting-style=shell-escape", "'one'$'\\n''&two'"), + ("--quoting-style=shell-escape-always", "'one'$'\\n''&two'"), + ] { + scene + .ucmd() + .arg(arg) + .arg("one\n&two") + .succeeds() + .stdout_only(format!("{}\n", correct)); + } } scene @@ -1316,6 +1429,43 @@ fn test_ls_ignore_hide() { .stdout_is("CONTRIBUTING.md\nREADME.md\nREADMECAREFULLY.md\nsome_other_file\n"); } +#[test] +fn test_ls_ignore_backups() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("somefile"); + at.touch("somebackup~"); + at.touch(".somehiddenfile"); + at.touch(".somehiddenbackup~"); + + scene.ucmd().arg("-B").succeeds().stdout_is("somefile\n"); + scene + .ucmd() + .arg("--ignore-backups") + .succeeds() + .stdout_is("somefile\n"); + + scene + .ucmd() + .arg("-aB") + .succeeds() + .stdout_contains(".somehiddenfile") + .stdout_contains("somefile") + .stdout_does_not_contain("somebackup") + .stdout_does_not_contain(".somehiddenbackup~"); + + scene + .ucmd() + .arg("-a") + .arg("--ignore-backups") + .succeeds() + .stdout_contains(".somehiddenfile") + .stdout_contains("somefile") + .stdout_does_not_contain("somebackup") + .stdout_does_not_contain(".somehiddenbackup~"); +} + #[test] fn test_ls_directory() { let scene = TestScenario::new(util_name!()); diff --git a/tests/by-util/test_mkfifo.rs b/tests/by-util/test_mkfifo.rs index f60c0a4b8..23108d976 100644 --- a/tests/by-util/test_mkfifo.rs +++ b/tests/by-util/test_mkfifo.rs @@ -19,8 +19,7 @@ fn test_create_one_fifo_with_invalid_mode() { .arg("-m") .arg("invalid") .fails() - .stderr - .contains("invalid mode"); + .stderr_contains("invalid mode"); } #[test] diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index 736fb6956..9245733ca 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -2,18 +2,15 @@ use crate::common::util::*; #[test] fn test_more_no_arg() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.run(); - assert!(!result.success); + // stderr = more: Reading from stdin isn't supported yet. + new_ucmd!().fails(); } #[test] fn test_more_dir_arg() { - let (_, mut ucmd) = at_and_ucmd!(); - ucmd.arg("."); - let result = ucmd.run(); - assert!(!result.success); + let result = new_ucmd!().arg(".").run(); + result.failure(); const EXPECTED_ERROR_MESSAGE: &str = "more: '.' is a directory.\nTry 'more --help' for more information."; - assert_eq!(result.stderr.trim(), EXPECTED_ERROR_MESSAGE); + assert_eq!(result.stderr_str().trim(), EXPECTED_ERROR_MESSAGE); } diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 0caeb1ef1..e8ba43282 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -476,16 +476,9 @@ fn test_mv_overwrite_nonempty_dir() { // GNU: "mv: cannot move ‘a’ to ‘b’: Directory not empty" // Verbose output for the move should not be shown on failure - assert!( - ucmd.arg("-vT") - .arg(dir_a) - .arg(dir_b) - .fails() - .no_stdout() - .stderr - .len() - > 0 - ); + let result = ucmd.arg("-vT").arg(dir_a).arg(dir_b).fails(); + result.no_stdout(); + assert!(!result.stderr_str().is_empty()); assert!(at.dir_exists(dir_a)); assert!(at.dir_exists(dir_b)); @@ -526,15 +519,15 @@ fn test_mv_errors() { // $ mv -T -t a b // mv: cannot combine --target-directory (-t) and --no-target-directory (-T) - let result = scene + scene .ucmd() .arg("-T") .arg("-t") .arg(dir) .arg(file_a) .arg(file_b) - .fails(); - assert!(result.stderr.contains("cannot be used with")); + .fails() + .stderr_contains("cannot be used with"); // $ at.touch file && at.mkdir dir // $ mv -T file dir @@ -553,7 +546,13 @@ fn test_mv_errors() { // $ at.mkdir dir && at.touch file // $ mv dir file // err == mv: cannot overwrite non-directory ‘file’ with directory ‘dir’ - assert!(scene.ucmd().arg(dir).arg(file_a).fails().stderr.len() > 0); + assert!(!scene + .ucmd() + .arg(dir) + .arg(file_a) + .fails() + .stderr_str() + .is_empty()); } #[test] diff --git a/tests/by-util/test_nice.rs b/tests/by-util/test_nice.rs index 7e704fc00..d3457c686 100644 --- a/tests/by-util/test_nice.rs +++ b/tests/by-util/test_nice.rs @@ -16,7 +16,7 @@ fn test_negative_adjustment() { let res = new_ucmd!().args(&["-n", "-1", "true"]).run(); assert!(res - .stderr + .stderr_str() .starts_with("nice: warning: setpriority: Permission denied")); } diff --git a/tests/by-util/test_shuf.rs b/tests/by-util/test_shuf.rs index 717971bd4..f925f8357 100644 --- a/tests/by-util/test_shuf.rs +++ b/tests/by-util/test_shuf.rs @@ -9,35 +9,28 @@ fn test_output_is_random_permutation() { .collect::>() .join("\n"); - let result = new_ucmd!() - .pipe_in(input.as_bytes()) - .succeeds() - .no_stderr() - .stdout - .clone(); + let result = new_ucmd!().pipe_in(input.as_bytes()).succeeds(); + result.no_stderr(); let mut result_seq: Vec = result + .stdout_str() .split("\n") .filter(|x| !x.is_empty()) .map(|x| x.parse().unwrap()) .collect(); result_seq.sort(); - assert_ne!(result, input, "Output is not randomised"); + assert_ne!(result.stdout_str(), input, "Output is not randomised"); assert_eq!(result_seq, input_seq, "Output is not a permutation"); } #[test] fn test_zero_termination() { let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - let result = new_ucmd!() - .arg("-z") - .arg("-i1-10") - .succeeds() - .no_stderr() - .stdout - .clone(); + let result = new_ucmd!().arg("-z").arg("-i1-10").succeeds(); + result.no_stderr(); let mut result_seq: Vec = result + .stdout_str() .split("\0") .filter(|x| !x.is_empty()) .map(|x| x.parse().unwrap()) @@ -57,12 +50,11 @@ fn test_echo() { .map(|x| x.to_string()) .collect::>(), ) - .succeeds() - .no_stderr() - .stdout - .clone(); + .succeeds(); + result.no_stderr(); let mut result_seq: Vec = result + .stdout_str() .split("\n") .filter(|x| !x.is_empty()) .map(|x| x.parse().unwrap()) @@ -84,12 +76,11 @@ fn test_head_count() { let result = new_ucmd!() .args(&["-n", &repeat_limit.to_string()]) .pipe_in(input.as_bytes()) - .succeeds() - .no_stderr() - .stdout - .clone(); + .succeeds(); + result.no_stderr(); let mut result_seq: Vec = result + .stdout_str() .split("\n") .filter(|x| !x.is_empty()) .map(|x| x.parse().unwrap()) @@ -99,7 +90,7 @@ fn test_head_count() { assert!( result_seq.iter().all(|x| input_seq.contains(x)), "Output includes element not from input: {}", - result + result.stdout_str() ) } @@ -117,12 +108,11 @@ fn test_repeat() { .arg("-r") .args(&["-n", &repeat_limit.to_string()]) .pipe_in(input.as_bytes()) - .succeeds() - .no_stderr() - .stdout - .clone(); + .succeeds(); + result.no_stderr(); let result_seq: Vec = result + .stdout_str() .split("\n") .filter(|x| !x.is_empty()) .map(|x| x.parse().unwrap()) @@ -146,14 +136,11 @@ fn test_repeat() { fn test_file_input() { let expected_seq = vec![11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; - let result = new_ucmd!() - .arg("file_input.txt") - .succeeds() - .no_stderr() - .stdout - .clone(); + let result = new_ucmd!().arg("file_input.txt").succeeds(); + result.no_stderr(); let mut result_seq: Vec = result + .stdout_str() .split("\n") .filter(|x| !x.is_empty()) .map(|x| x.parse().unwrap()) @@ -164,52 +151,50 @@ fn test_file_input() { #[test] fn test_shuf_echo_and_input_range_not_allowed() { - let result = new_ucmd!().args(&["-e", "0", "-i", "0-2"]).run(); - - assert!(!result.success); - assert!(result - .stderr - .contains("The argument '--input-range ' cannot be used with '--echo ...'")); + new_ucmd!() + .args(&["-e", "0", "-i", "0-2"]) + .fails() + .stderr_contains( + "The argument '--input-range ' cannot be used with '--echo ...'", + ); } #[test] fn test_shuf_input_range_and_file_not_allowed() { - let result = new_ucmd!().args(&["-i", "0-9", "file"]).run(); - - assert!(!result.success); - assert!(result - .stderr - .contains("The argument '' cannot be used with '--input-range '")); + new_ucmd!() + .args(&["-i", "0-9", "file"]) + .fails() + .stderr_contains("The argument '' cannot be used with '--input-range '"); } #[test] fn test_shuf_invalid_input_range_one() { - let result = new_ucmd!().args(&["-i", "0"]).run(); - - assert!(!result.success); - assert!(result.stderr.contains("invalid input range")); + new_ucmd!() + .args(&["-i", "0"]) + .fails() + .stderr_contains("invalid input range"); } #[test] fn test_shuf_invalid_input_range_two() { - let result = new_ucmd!().args(&["-i", "a-9"]).run(); - - assert!(!result.success); - assert!(result.stderr.contains("invalid input range: 'a'")); + new_ucmd!() + .args(&["-i", "a-9"]) + .fails() + .stderr_contains("invalid input range: 'a'"); } #[test] fn test_shuf_invalid_input_range_three() { - let result = new_ucmd!().args(&["-i", "0-b"]).run(); - - assert!(!result.success); - assert!(result.stderr.contains("invalid input range: 'b'")); + new_ucmd!() + .args(&["-i", "0-b"]) + .fails() + .stderr_contains("invalid input range: 'b'"); } #[test] fn test_shuf_invalid_input_line_count() { - let result = new_ucmd!().args(&["-n", "a"]).run(); - - assert!(!result.success); - assert!(result.stderr.contains("invalid line count: 'a'")); + new_ucmd!() + .args(&["-n", "a"]) + .fails() + .stderr_contains("invalid line count: 'a'"); } diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 63883cd63..894626c55 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -2,10 +2,17 @@ use crate::common::util::*; fn test_helper(file_name: &str, args: &str) { new_ucmd!() - .arg(args) .arg(format!("{}.txt", file_name)) + .args(&args.split(' ').collect::>()) .succeeds() .stdout_is_fixture(format!("{}.expected", file_name)); + + new_ucmd!() + .arg(format!("{}.txt", file_name)) + .arg("--debug") + .args(&args.split(' ').collect::>()) + .succeeds() + .stdout_is_fixture(format!("{}.expected.debug", file_name)); } // FYI, the initialization size of our Line struct is 96 bytes. @@ -65,11 +72,7 @@ fn test_extsort_as64_bailout() { #[test] fn test_multiple_decimals_general() { - new_ucmd!() - .arg("-g") - .arg("multiple_decimals_general.txt") - .succeeds() - .stdout_is("\n\n\n\n\n\n\n\nCARAvan\n-2028789030\n-896689\n-8.90880\n-1\n-.05\n000\n00000001\n1\n1.040000000\n1.444\n1.58590\n8.013\n45\n46.89\n576,446.88800000\n576,446.890\n 4567.\n4567.1\n4567.34\n\t\t\t\t\t\t\t\t\t\t4567..457\n\t\t\t\t37800\n\t\t\t\t\t\t45670.89079.098\n\t\t\t\t\t\t45670.89079.1\n4798908.340000000000\n4798908.45\n4798908.8909800\n"); + test_helper("multiple_decimals_general", "-g") } #[test] @@ -99,7 +102,7 @@ fn test_check_zero_terminated_success() { #[test] fn test_random_shuffle_len() { // check whether output is the same length as the input - const FILE: &'static str = "default_unsorted_ints.expected"; + const FILE: &str = "default_unsorted_ints.expected"; let (at, _ucmd) = at_and_ucmd!(); let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str(); let expected = at.read(FILE); @@ -111,7 +114,7 @@ fn test_random_shuffle_len() { #[test] fn test_random_shuffle_contains_all_lines() { // check whether lines of input are all in output - const FILE: &'static str = "default_unsorted_ints.expected"; + const FILE: &str = "default_unsorted_ints.expected"; let (at, _ucmd) = at_and_ucmd!(); let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str(); let expected = at.read(FILE); @@ -126,7 +129,22 @@ fn test_random_shuffle_two_runs_not_the_same() { // check to verify that two random shuffles are not equal; this has the // potential to fail in the very unlikely event that the random order is the same // as the starting order, or if both random sorts end up having the same order. - const FILE: &'static str = "default_unsorted_ints.expected"; + const FILE: &str = "default_unsorted_ints.expected"; + let (at, _ucmd) = at_and_ucmd!(); + let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str(); + let expected = at.read(FILE); + let unexpected = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str(); + + assert_ne!(result, expected); + assert_ne!(result, unexpected); +} + +#[test] +fn test_random_shuffle_contains_two_runs_not_the_same() { + // check to verify that two random shuffles are not equal; this has the + // potential to fail in the unlikely event that random order is the same + // as the starting order, or if both random sorts end up having the same order. + const FILE: &str = "default_unsorted_ints.expected"; let (at, _ucmd) = at_and_ucmd!(); let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str(); let expected = at.read(FILE); @@ -228,6 +246,11 @@ fn test_non_printing_chars() { } } +#[test] +fn test_exponents_positive_general_fixed() { + test_helper("exponents_general", "-g"); +} + #[test] fn test_exponents_positive_numeric() { test_helper("exponents-positive-numeric", "-n"); @@ -344,62 +367,32 @@ fn test_numeric_unique_ints2() { #[test] fn test_keys_open_ended() { - let input = "aa bb cc\ndd aa ff\ngg aa cc\n"; - new_ucmd!() - .args(&["-k", "2.2"]) - .pipe_in(input) - .succeeds() - .stdout_only("gg aa cc\ndd aa ff\naa bb cc\n"); + test_helper("keys_open_ended", "-k 2.3"); } #[test] fn test_keys_closed_range() { - let input = "aa bb cc\ndd aa ff\ngg aa cc\n"; - new_ucmd!() - .args(&["-k", "2.2,2.2"]) - .pipe_in(input) - .succeeds() - .stdout_only("dd aa ff\ngg aa cc\naa bb cc\n"); + test_helper("keys_closed_range", "-k 2.2,2.2"); } #[test] fn test_keys_multiple_ranges() { - let input = "aa bb cc\ndd aa ff\ngg aa cc\n"; - new_ucmd!() - .args(&["-k", "2,2", "-k", "3,3"]) - .pipe_in(input) - .succeeds() - .stdout_only("gg aa cc\ndd aa ff\naa bb cc\n"); + test_helper("keys_multiple_ranges", "-k 2,2 -k 3,3"); } #[test] fn test_keys_no_field_match() { - let input = "aa aa aa aa\naa bb cc\ndd aa ff\n"; - new_ucmd!() - .args(&["-k", "4,4"]) - .pipe_in(input) - .succeeds() - .stdout_only("aa bb cc\ndd aa ff\naa aa aa aa\n"); + test_helper("keys_no_field_match", "-k 4,4"); } #[test] fn test_keys_no_char_match() { - let input = "aaa\nba\nc\n"; - new_ucmd!() - .args(&["-k", "1.2"]) - .pipe_in(input) - .succeeds() - .stdout_only("c\nba\naaa\n"); + test_helper("keys_no_char_match", "-k 1.2"); } #[test] fn test_keys_custom_separator() { - let input = "aaxbbxcc\nddxaaxff\nggxaaxcc\n"; - new_ucmd!() - .args(&["-k", "2.2,2.2", "-t", "x"]) - .pipe_in(input) - .succeeds() - .stdout_only("ddxaaxff\nggxaaxcc\naaxbbxcc\n"); + test_helper("keys_custom_separator", "-k 2.2,2.2 -t x"); } #[test] @@ -426,6 +419,13 @@ fn test_keys_invalid_field_zero() { .stderr_only("sort: error: field index was 0"); } +#[test] +fn test_keys_invalid_char_zero() { + new_ucmd!().args(&["-k", "1.0"]).fails().stderr_only( + "sort: error: invalid character index 0 in `1.0` for the start position of a field", + ); +} + #[test] fn test_keys_with_options() { let input = "aa 3 cc\ndd 1 ff\ngg 2 cc\n"; @@ -591,3 +591,39 @@ fn test_check_silent() { .fails() .stdout_is(""); } + +#[test] +fn test_dictionary_and_nonprinting_conflicts() { + let conflicting_args = ["n", "h", "g", "M"]; + for restricted_arg in &["d", "i"] { + for conflicting_arg in &conflicting_args { + new_ucmd!() + .arg(&format!("-{}{}", restricted_arg, conflicting_arg)) + .fails(); + } + for conflicting_arg in &conflicting_args { + new_ucmd!() + .args(&[ + format!("-{}", restricted_arg).as_str(), + "-k", + &format!("1,1{}", conflicting_arg), + ]) + .succeeds(); + } + for conflicting_arg in &conflicting_args { + // FIXME: this should ideally fail. + new_ucmd!() + .args(&["-k", &format!("1{},1{}", restricted_arg, conflicting_arg)]) + .succeeds(); + } + } +} + +#[test] +fn test_trailing_separator() { + new_ucmd!() + .args(&["-t", "x", "-k", "1,1"]) + .pipe_in("aax\naaa\n") + .succeeds() + .stdout_is("aax\naaa\n"); +} diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index 376b3db51..60d735c51 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -337,5 +337,5 @@ fn expected_result(args: &[&str]) -> String { .env("LANGUAGE", "C") .args(args) .run() - .stdout + .stdout_move_str() } diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index 3733adbec..a8adbb28e 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -52,18 +52,19 @@ fn test_single_non_newline_separator_before() { #[test] fn test_invalid_input() { - let (_, mut ucmd) = at_and_ucmd!(); + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; - ucmd.arg("b") - .run() - .stderr - .contains("tac: error: failed to open 'b' for reading"); - - let (at, mut ucmd) = at_and_ucmd!(); + scene + .ucmd() + .arg("b") + .fails() + .stderr_contains("failed to open 'b' for reading: No such file or directory"); at.mkdir("a"); - ucmd.arg("a") - .run() - .stderr - .contains("tac: error: failed to read 'a'"); + scene + .ucmd() + .arg("a") + .fails() + .stderr_contains("dir: read error: Invalid argument"); } diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 6e9eb4a17..1c025cf4c 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -343,3 +343,8 @@ fn test_negative_indexing() { assert_eq!(positive_lines_index.stdout(), negative_lines_index.stdout()); assert_eq!(positive_bytes_index.stdout(), negative_bytes_index.stdout()); } + +#[test] +fn test_sleep_interval() { + new_ucmd!().arg("-s").arg("10").arg(FOOBAR_TXT).succeeds(); +} diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 9f2c079b0..40fbb8aa9 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -367,7 +367,58 @@ fn test_touch_mtime_dst_succeeds() { let target_time = str_to_filetime("%Y%m%d%H%M", "202103140300"); let (_, mtime) = get_file_times(&at, file); - eprintln!("target_time: {:?}", target_time); - eprintln!("mtime: {:?}", mtime); assert!(target_time == mtime); } + +// is_dst_switch_hour returns true if timespec ts is just before the switch +// to Daylight Saving Time. +// For example, in EST (UTC-5), Timespec { sec: 1583647200, nsec: 0 } +// for March 8 2020 01:00:00 AM +// is just before the switch because on that day clock jumps by 1 hour, +// so 1 minute after 01:59:00 is 03:00:00. +fn is_dst_switch_hour(ts: time::Timespec) -> bool { + let ts_after = ts + time::Duration::hours(1); + let tm = time::at(ts); + let tm_after = time::at(ts_after); + tm_after.tm_hour == tm.tm_hour + 2 +} + +// get_dstswitch_hour returns date string for which touch -m -t fails. +// For example, in EST (UTC-5), that will be "202003080200" so +// touch -m -t 202003080200 somefile +// fails (that date/time does not exist). +// In other locales it will be a different date/time, and in some locales +// it doesn't exist at all, in which case this function will return None. +fn get_dstswitch_hour() -> Option { + let now = time::now(); + // Start from January 1, 2020, 00:00. + let mut tm = time::strptime("20200101-0000", "%Y%m%d-%H%M").unwrap(); + tm.tm_isdst = -1; + tm.tm_utcoff = now.tm_utcoff; + let mut ts = tm.to_timespec(); + // Loop through all hours in year 2020 until we find the hour just + // before the switch to DST. + for _i in 0..(366 * 24) { + if is_dst_switch_hour(ts) { + let mut tm = time::at(ts); + tm.tm_hour = tm.tm_hour + 1; + let s = time::strftime("%Y%m%d%H%M", &tm).unwrap().to_string(); + return Some(s); + } + ts = ts + time::Duration::hours(1); + } + None +} + +#[test] +fn test_touch_mtime_dst_fails() { + let (_at, mut ucmd) = at_and_ucmd!(); + let file = "test_touch_set_mtime_dst_fails"; + + match get_dstswitch_hour() { + Some(s) => { + ucmd.args(&["-m", "-t", &s, file]).fails(); + } + None => (), + } +} diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index a1500bcf6..630c305c6 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -120,19 +120,15 @@ fn test_truncate_with_set1_shorter_than_set2() { #[test] fn missing_args_fails() { let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.run(); - - assert!(!result.success); - assert!(result.stderr.contains("missing operand")); + ucmd.fails().stderr_contains("missing operand"); } #[test] fn missing_required_second_arg_fails() { let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.args(&["foo"]).run(); - - assert!(!result.success); - assert!(result.stderr.contains("missing operand after")); + ucmd.args(&["foo"]) + .fails() + .stderr_contains("missing operand after"); } #[test] diff --git a/tests/by-util/test_truncate.rs b/tests/by-util/test_truncate.rs index ce7964d57..64573f2c0 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -53,6 +53,16 @@ fn test_decrease_file_size() { assert!(file.seek(SeekFrom::Current(0)).unwrap() == 6); } +#[test] +fn test_space_in_size() { + let (at, mut ucmd) = at_and_ucmd!(); + let mut file = at.make_file(TFILE2); + file.write_all(b"1234567890").unwrap(); + ucmd.args(&["--size", " 4", TFILE2]).succeeds(); + file.seek(SeekFrom::End(0)).unwrap(); + assert!(file.seek(SeekFrom::Current(0)).unwrap() == 4); +} + #[test] fn test_failed() { new_ucmd!().fails(); diff --git a/tests/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index 0ea6de281..0da6f44e4 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -18,33 +18,35 @@ fn test_sort_self_loop() { #[test] fn test_no_such_file() { - let result = new_ucmd!().arg("invalid_file_txt").run(); - - assert_eq!(true, result.stderr.contains("No such file or directory")); + new_ucmd!() + .arg("invalid_file_txt") + .fails() + .stderr_contains("No such file or directory"); } #[test] fn test_version_flag() { - let version_short = new_ucmd!().arg("-V").run(); - let version_long = new_ucmd!().arg("--version").run(); + let version_short = new_ucmd!().arg("-V").succeeds(); + let version_long = new_ucmd!().arg("--version").succeeds(); - assert_eq!(version_short.stdout(), version_long.stdout()); + assert_eq!(version_short.stdout_str(), version_long.stdout_str()); } #[test] fn test_help_flag() { - let help_short = new_ucmd!().arg("-h").run(); - let help_long = new_ucmd!().arg("--help").run(); + let help_short = new_ucmd!().arg("-h").succeeds(); + let help_long = new_ucmd!().arg("--help").succeeds(); - assert_eq!(help_short.stdout(), help_long.stdout()); + assert_eq!(help_short.stdout_str(), help_long.stdout_str()); } #[test] fn test_multiple_arguments() { - let result = new_ucmd!() + new_ucmd!() .arg("call_graph.txt") - .arg("invalid_file.txt") - .run(); - - assert_eq!(true, result.stderr.contains("error: Found argument 'invalid_file.txt' which wasn't expected, or isn't valid in this context")) + .arg("invalid_file") + .fails() + .stderr_contains( + "Found argument 'invalid_file' which wasn't expected, or isn't valid in this context", + ); } diff --git a/tests/by-util/test_who.rs b/tests/by-util/test_who.rs index 89b7cec93..32d2427e0 100644 --- a/tests/by-util/test_who.rs +++ b/tests/by-util/test_who.rs @@ -23,7 +23,7 @@ fn test_heading() { for opt in vec!["-H"] { // allow whitespace variation // * minor whitespace differences occur between platform built-in outputs; specifically number of TABs between "TIME" and "COMMENT" may be variant - let actual = new_ucmd!().arg(opt).run().stdout; + let actual = new_ucmd!().arg(opt).run().stdout_move_str(); let expect = expected_result(opt); println!("actual: {:?}", actual); println!("expect: {:?}", expect); @@ -80,5 +80,5 @@ fn expected_result(arg: &str) -> String { .env("LANGUAGE", "C") .args(&[arg]) .run() - .stdout + .stdout_move_str() } diff --git a/tests/common/util.rs b/tests/common/util.rs index 55e121737..93bbccc24 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -2,6 +2,7 @@ #[cfg(not(windows))] use libc; +use pretty_assertions::assert_eq; use std::env; #[cfg(not(windows))] use std::ffi::CString; @@ -42,22 +43,6 @@ pub fn is_ci() -> bool { .eq_ignore_ascii_case("true") } -/// Test if the program is running under WSL -// ref: @@ -// ToDO: test on WSL2 which likely doesn't need special handling; plan change to `is_wsl_1()` if WSL2 is less needy -pub fn is_wsl() -> bool { - #[cfg(target_os = "linux")] - { - if let Ok(b) = std::fs::read("/proc/sys/kernel/osrelease") { - if let Ok(s) = std::str::from_utf8(&b) { - let a = s.to_ascii_lowercase(); - return a.contains("microsoft") || a.contains("wsl"); - } - } - } - false -} - /// Read a test scenario fixture, returning its bytes fn read_scenario_fixture>(tmpd: &Option>, file_rel_path: S) -> Vec { let tmpdir_path = tmpd.as_ref().unwrap().as_ref().path(); @@ -74,11 +59,11 @@ pub struct CmdResult { code: Option, /// zero-exit from running the Command? /// see [`success`] - pub success: bool, + success: bool, /// captured standard output after running the Command - pub stdout: String, + stdout: String, /// captured standard error after running the Command - pub stderr: String, + stderr: String, } impl CmdResult { @@ -237,7 +222,7 @@ impl CmdResult { /// like stdout_is(...), but expects the contents of the file at the provided relative path pub fn stdout_is_fixture>(&self, file_rel_path: T) -> &CmdResult { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stdout_is_bytes(contents) + self.stdout_is(String::from_utf8(contents).unwrap()) } /// asserts that the command resulted in stderr stream output that equals the @@ -261,7 +246,7 @@ impl CmdResult { /// Like stdout_is_fixture, but for stderr pub fn stderr_is_fixture>(&self, file_rel_path: T) -> &CmdResult { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stderr_is_bytes(contents) + self.stderr_is(String::from_utf8(contents).unwrap()) } /// asserts that @@ -329,14 +314,14 @@ impl CmdResult { } pub fn stdout_matches(&self, regex: ®ex::Regex) -> &CmdResult { - if !regex.is_match(self.stdout_str()) { + if !regex.is_match(self.stdout_str().trim()) { panic!("Stdout does not match regex:\n{}", self.stdout_str()) } self } pub fn stdout_does_not_match(&self, regex: ®ex::Regex) -> &CmdResult { - if regex.is_match(self.stdout_str()) { + if regex.is_match(self.stdout_str().trim()) { panic!("Stdout matches regex:\n{}", self.stdout_str()) } self @@ -635,7 +620,7 @@ impl TestScenario { }, util_name: String::from(util_name), fixtures: AtPath::new(tmpd.as_ref().path()), - tmpd: tmpd, + tmpd, }; let mut fixture_path_builder = env::current_dir().unwrap(); fixture_path_builder.push(TESTS_DIR); @@ -696,8 +681,11 @@ pub struct UCommand { comm_string: String, tmpd: Option>, has_run: bool, - stdin: Option>, ignore_stdin_write_error: bool, + stdin: Option, + stdout: Option, + stderr: Option, + bytes_into_stdin: Option>, } impl UCommand { @@ -726,8 +714,11 @@ impl UCommand { cmd }, comm_string: String::from(arg.as_ref().to_str().unwrap()), - stdin: None, ignore_stdin_write_error: false, + bytes_into_stdin: None, + stdin: None, + stdout: None, + stderr: None, } } @@ -738,6 +729,21 @@ impl UCommand { ucmd } + pub fn set_stdin>(&mut self, stdin: T) -> &mut UCommand { + self.stdin = Some(stdin.into()); + self + } + + pub fn set_stdout>(&mut self, stdout: T) -> &mut UCommand { + self.stdout = Some(stdout.into()); + self + } + + pub fn set_stderr>(&mut self, stderr: T) -> &mut UCommand { + self.stderr = Some(stderr.into()); + self + } + /// Add a parameter to the invocation. Path arguments are treated relative /// to the test environment directory. pub fn arg>(&mut self, arg: S) -> &mut UCommand { @@ -767,10 +773,10 @@ impl UCommand { /// provides stdinput to feed in to the command when spawned pub fn pipe_in>>(&mut self, input: T) -> &mut UCommand { - if self.stdin.is_some() { + if self.bytes_into_stdin.is_some() { panic!("{}", MULTIPLE_STDIN_MEANINGLESS); } - self.stdin = Some(input.into()); + self.bytes_into_stdin = Some(input.into()); self } @@ -784,7 +790,7 @@ impl UCommand { /// This is typically useful to test non-standard workflows /// like feeding something to a command that does not read it pub fn ignore_stdin_write_error(&mut self) -> &mut UCommand { - if self.stdin.is_none() { + if self.bytes_into_stdin.is_none() { panic!("{}", NO_STDIN_MEANINGLESS); } self.ignore_stdin_write_error = true; @@ -813,13 +819,13 @@ impl UCommand { log_info("run", &self.comm_string); let mut child = self .raw - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) + .stdin(self.stdin.take().unwrap_or_else(|| Stdio::piped())) + .stdout(self.stdout.take().unwrap_or_else(|| Stdio::piped())) + .stderr(self.stderr.take().unwrap_or_else(|| Stdio::piped())) .spawn() .unwrap(); - if let Some(ref input) = self.stdin { + if let Some(ref input) = self.bytes_into_stdin { let write_result = child .stdin .take() diff --git a/tests/fixtures/sort/default_unsorted_ints.expected.debug b/tests/fixtures/sort/default_unsorted_ints.expected.debug new file mode 100644 index 000000000..2bf082d3b --- /dev/null +++ b/tests/fixtures/sort/default_unsorted_ints.expected.debug @@ -0,0 +1,200 @@ +1 +_ +10 +__ +100 +___ +11 +__ +12 +__ +13 +__ +14 +__ +15 +__ +16 +__ +17 +__ +18 +__ +19 +__ +2 +_ +20 +__ +21 +__ +22 +__ +23 +__ +24 +__ +25 +__ +26 +__ +27 +__ +28 +__ +29 +__ +3 +_ +30 +__ +31 +__ +32 +__ +33 +__ +34 +__ +35 +__ +36 +__ +37 +__ +38 +__ +39 +__ +4 +_ +40 +__ +41 +__ +42 +__ +43 +__ +44 +__ +45 +__ +46 +__ +47 +__ +48 +__ +49 +__ +5 +_ +50 +__ +51 +__ +52 +__ +53 +__ +54 +__ +55 +__ +56 +__ +57 +__ +58 +__ +59 +__ +6 +_ +60 +__ +61 +__ +62 +__ +63 +__ +64 +__ +65 +__ +66 +__ +67 +__ +68 +__ +69 +__ +7 +_ +70 +__ +71 +__ +72 +__ +73 +__ +74 +__ +75 +__ +76 +__ +77 +__ +78 +__ +79 +__ +8 +_ +80 +__ +81 +__ +82 +__ +83 +__ +84 +__ +85 +__ +86 +__ +87 +__ +88 +__ +89 +__ +9 +_ +90 +__ +91 +__ +92 +__ +93 +__ +94 +__ +95 +__ +96 +__ +97 +__ +98 +__ +99 +__ diff --git a/tests/fixtures/sort/dictionary_order.expected.debug b/tests/fixtures/sort/dictionary_order.expected.debug new file mode 100644 index 000000000..f4a2d17db --- /dev/null +++ b/tests/fixtures/sort/dictionary_order.expected.debug @@ -0,0 +1,9 @@ +bbb +___ +___ +./bbc +_____ +_____ +bbd +___ +___ diff --git a/tests/fixtures/sort/exponents-positive-numeric.expected.debug b/tests/fixtures/sort/exponents-positive-numeric.expected.debug new file mode 100644 index 000000000..f5a32bad1 --- /dev/null +++ b/tests/fixtures/sort/exponents-positive-numeric.expected.debug @@ -0,0 +1,36 @@ + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key ++100000 +^ no match for key +_______ +10E +__ +___ +50e10 +__ +_____ +100E6 +___ +_____ +1000EDKLD +____ +_________ +10000K78 +_____ +________ diff --git a/tests/fixtures/sort/exponents_general.expected b/tests/fixtures/sort/exponents_general.expected new file mode 100644 index 000000000..524b6e67f --- /dev/null +++ b/tests/fixtures/sort/exponents_general.expected @@ -0,0 +1,19 @@ + + + + + + + + +5.5.5.5 +10E +1000EDKLD +10000K78 ++100000 ++100000 +100E6 +100E6 +10e10e10e10 +50e10 +50e10 diff --git a/tests/fixtures/sort/exponents_general.expected.debug b/tests/fixtures/sort/exponents_general.expected.debug new file mode 100644 index 000000000..4dea45c39 --- /dev/null +++ b/tests/fixtures/sort/exponents_general.expected.debug @@ -0,0 +1,57 @@ + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key +5.5.5.5 +___ +_______ +10E +__ +___ +1000EDKLD +____ +_________ +10000K78 +_____ +________ ++100000 +_______ +_______ ++100000 +_______ +_______ +100E6 +_____ +_____ +100E6 +_____ +_____ +10e10e10e10 +_____ +___________ +50e10 +_____ +_____ +50e10 +_____ +_____ diff --git a/tests/fixtures/sort/exponents_general.txt b/tests/fixtures/sort/exponents_general.txt new file mode 100644 index 000000000..de2a6c31b --- /dev/null +++ b/tests/fixtures/sort/exponents_general.txt @@ -0,0 +1,19 @@ +100E6 + +50e10 ++100000 + +10000K78 +10E + + +1000EDKLD + + +100E6 + +50e10 ++100000 + +10e10e10e10 +5.5.5.5 diff --git a/tests/fixtures/sort/human-numeric-whitespace.expected.debug b/tests/fixtures/sort/human-numeric-whitespace.expected.debug new file mode 100644 index 000000000..66afcda66 --- /dev/null +++ b/tests/fixtures/sort/human-numeric-whitespace.expected.debug @@ -0,0 +1,33 @@ + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key +456K +____ +____ +4568K +_____ +_____ +>>>456M + ____ +_______ + 6.2G + ____ +__________________ diff --git a/tests/fixtures/sort/human_block_sizes.expected.debug b/tests/fixtures/sort/human_block_sizes.expected.debug new file mode 100644 index 000000000..5f4860a85 --- /dev/null +++ b/tests/fixtures/sort/human_block_sizes.expected.debug @@ -0,0 +1,33 @@ +844K +____ +____ +981K +____ +____ +11M +___ +___ +13M +___ +___ +14M +___ +___ +16M +___ +___ +18M +___ +___ +19M +___ +___ +20M +___ +___ +981T +____ +____ +20P +___ +___ diff --git a/tests/fixtures/sort/ignore_case.expected.debug b/tests/fixtures/sort/ignore_case.expected.debug new file mode 100644 index 000000000..08f0abb8d --- /dev/null +++ b/tests/fixtures/sort/ignore_case.expected.debug @@ -0,0 +1,21 @@ +aaa +___ +___ +BBB +___ +___ +ccc +___ +___ +DDD +___ +___ +eee +___ +___ +FFF +___ +___ +ggg +___ +___ diff --git a/tests/fixtures/sort/keys_closed_range.expected b/tests/fixtures/sort/keys_closed_range.expected new file mode 100644 index 000000000..45005621b --- /dev/null +++ b/tests/fixtures/sort/keys_closed_range.expected @@ -0,0 +1,6 @@ +dd aa ff +gg aa cc +aa bb cc +èè éé èè +👩‍🔬 👩‍🔬 👩‍🔬 +💣💣 💣💣 💣💣 diff --git a/tests/fixtures/sort/keys_closed_range.expected.debug b/tests/fixtures/sort/keys_closed_range.expected.debug new file mode 100644 index 000000000..b78db4af1 --- /dev/null +++ b/tests/fixtures/sort/keys_closed_range.expected.debug @@ -0,0 +1,18 @@ +dd aa ff + _ +________ +gg aa cc + _ +________ +aa bb cc + _ +________ +èè éé èè + _ +________ +👩‍🔬 👩‍🔬 👩‍🔬 + __ +______________ +💣💣 💣💣 💣💣 + __ +______________ diff --git a/tests/fixtures/sort/keys_closed_range.txt b/tests/fixtures/sort/keys_closed_range.txt new file mode 100644 index 000000000..d6bf40785 --- /dev/null +++ b/tests/fixtures/sort/keys_closed_range.txt @@ -0,0 +1,6 @@ +aa bb cc +dd aa ff +gg aa cc +èè éé èè +💣💣 💣💣 💣💣 +👩‍🔬 👩‍🔬 👩‍🔬 \ No newline at end of file diff --git a/tests/fixtures/sort/keys_custom_separator.expected b/tests/fixtures/sort/keys_custom_separator.expected new file mode 100644 index 000000000..2aba42033 --- /dev/null +++ b/tests/fixtures/sort/keys_custom_separator.expected @@ -0,0 +1,3 @@ +ddxaaxff +ggxaaxcc +aaxbbxcc diff --git a/tests/fixtures/sort/keys_custom_separator.expected.debug b/tests/fixtures/sort/keys_custom_separator.expected.debug new file mode 100644 index 000000000..5d4dbc776 --- /dev/null +++ b/tests/fixtures/sort/keys_custom_separator.expected.debug @@ -0,0 +1,9 @@ +ddxaaxff + _ +________ +ggxaaxcc + _ +________ +aaxbbxcc + _ +________ diff --git a/tests/fixtures/sort/keys_custom_separator.txt b/tests/fixtures/sort/keys_custom_separator.txt new file mode 100644 index 000000000..a8f473061 --- /dev/null +++ b/tests/fixtures/sort/keys_custom_separator.txt @@ -0,0 +1,3 @@ +aaxbbxcc +ddxaaxff +ggxaaxcc diff --git a/tests/fixtures/sort/keys_multiple_ranges.expected b/tests/fixtures/sort/keys_multiple_ranges.expected new file mode 100644 index 000000000..09e4e8729 --- /dev/null +++ b/tests/fixtures/sort/keys_multiple_ranges.expected @@ -0,0 +1,6 @@ +gg aa cc +dd aa ff +aa bb cc +èè éé èè +👩‍🔬 👩‍🔬 👩‍🔬 +💣💣 💣💣 💣💣 diff --git a/tests/fixtures/sort/keys_multiple_ranges.expected.debug b/tests/fixtures/sort/keys_multiple_ranges.expected.debug new file mode 100644 index 000000000..830e9afd0 --- /dev/null +++ b/tests/fixtures/sort/keys_multiple_ranges.expected.debug @@ -0,0 +1,24 @@ +gg aa cc + ___ + ___ +________ +dd aa ff + ___ + ___ +________ +aa bb cc + ___ + ___ +________ +èè éé èè + ___ + ___ +________ +👩‍🔬 👩‍🔬 👩‍🔬 + _____ + _____ +______________ +💣💣 💣💣 💣💣 + _____ + _____ +______________ diff --git a/tests/fixtures/sort/keys_multiple_ranges.txt b/tests/fixtures/sort/keys_multiple_ranges.txt new file mode 100644 index 000000000..d6bf40785 --- /dev/null +++ b/tests/fixtures/sort/keys_multiple_ranges.txt @@ -0,0 +1,6 @@ +aa bb cc +dd aa ff +gg aa cc +èè éé èè +💣💣 💣💣 💣💣 +👩‍🔬 👩‍🔬 👩‍🔬 \ No newline at end of file diff --git a/tests/fixtures/sort/keys_no_char_match.expected b/tests/fixtures/sort/keys_no_char_match.expected new file mode 100644 index 000000000..dcb361837 --- /dev/null +++ b/tests/fixtures/sort/keys_no_char_match.expected @@ -0,0 +1,3 @@ +c +ba +aaa diff --git a/tests/fixtures/sort/keys_no_char_match.expected.debug b/tests/fixtures/sort/keys_no_char_match.expected.debug new file mode 100644 index 000000000..5287a0de9 --- /dev/null +++ b/tests/fixtures/sort/keys_no_char_match.expected.debug @@ -0,0 +1,9 @@ +c + ^ no match for key +_ +ba + _ +__ +aaa + __ +___ diff --git a/tests/fixtures/sort/keys_no_char_match.txt b/tests/fixtures/sort/keys_no_char_match.txt new file mode 100644 index 000000000..1c952a6b8 --- /dev/null +++ b/tests/fixtures/sort/keys_no_char_match.txt @@ -0,0 +1,3 @@ +aaa +ba +c diff --git a/tests/fixtures/sort/keys_no_field_match.expected b/tests/fixtures/sort/keys_no_field_match.expected new file mode 100644 index 000000000..e2f183e13 --- /dev/null +++ b/tests/fixtures/sort/keys_no_field_match.expected @@ -0,0 +1,6 @@ +aa bb cc +dd aa ff +gg aa cc +èè éé èè +👩‍🔬 👩‍🔬 👩‍🔬 +💣💣 💣💣 💣💣 diff --git a/tests/fixtures/sort/keys_no_field_match.expected.debug b/tests/fixtures/sort/keys_no_field_match.expected.debug new file mode 100644 index 000000000..60197b1de --- /dev/null +++ b/tests/fixtures/sort/keys_no_field_match.expected.debug @@ -0,0 +1,18 @@ +aa bb cc + ^ no match for key +________ +dd aa ff + ^ no match for key +________ +gg aa cc + ^ no match for key +________ +èè éé èè + ^ no match for key +________ +👩‍🔬 👩‍🔬 👩‍🔬 + ^ no match for key +______________ +💣💣 💣💣 💣💣 + ^ no match for key +______________ diff --git a/tests/fixtures/sort/keys_no_field_match.txt b/tests/fixtures/sort/keys_no_field_match.txt new file mode 100644 index 000000000..d6bf40785 --- /dev/null +++ b/tests/fixtures/sort/keys_no_field_match.txt @@ -0,0 +1,6 @@ +aa bb cc +dd aa ff +gg aa cc +èè éé èè +💣💣 💣💣 💣💣 +👩‍🔬 👩‍🔬 👩‍🔬 \ No newline at end of file diff --git a/tests/fixtures/sort/keys_open_ended.expected b/tests/fixtures/sort/keys_open_ended.expected new file mode 100644 index 000000000..09e4e8729 --- /dev/null +++ b/tests/fixtures/sort/keys_open_ended.expected @@ -0,0 +1,6 @@ +gg aa cc +dd aa ff +aa bb cc +èè éé èè +👩‍🔬 👩‍🔬 👩‍🔬 +💣💣 💣💣 💣💣 diff --git a/tests/fixtures/sort/keys_open_ended.expected.debug b/tests/fixtures/sort/keys_open_ended.expected.debug new file mode 100644 index 000000000..d3a56ffd6 --- /dev/null +++ b/tests/fixtures/sort/keys_open_ended.expected.debug @@ -0,0 +1,18 @@ +gg aa cc + ____ +________ +dd aa ff + ____ +________ +aa bb cc + ____ +________ +èè éé èè + ____ +________ +👩‍🔬 👩‍🔬 👩‍🔬 + _______ +______________ +💣💣 💣💣 💣💣 + _______ +______________ diff --git a/tests/fixtures/sort/keys_open_ended.txt b/tests/fixtures/sort/keys_open_ended.txt new file mode 100644 index 000000000..d6bf40785 --- /dev/null +++ b/tests/fixtures/sort/keys_open_ended.txt @@ -0,0 +1,6 @@ +aa bb cc +dd aa ff +gg aa cc +èè éé èè +💣💣 💣💣 💣💣 +👩‍🔬 👩‍🔬 👩‍🔬 \ No newline at end of file diff --git a/tests/fixtures/sort/mixed_floats_ints_chars_numeric.expected.debug b/tests/fixtures/sort/mixed_floats_ints_chars_numeric.expected.debug new file mode 100644 index 000000000..dbe295a1c --- /dev/null +++ b/tests/fixtures/sort/mixed_floats_ints_chars_numeric.expected.debug @@ -0,0 +1,90 @@ +-2028789030 +___________ +___________ +-896689 +_______ +_______ +-8.90880 +________ +________ +-1 +__ +__ +-.05 +____ +____ + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key +000 +___ +___ +CARAvan +^ no match for key +_______ +00000001 +________ +________ +1 +_ +_ +1.040000000 +___________ +___________ +1.444 +_____ +_____ +1.58590 +_______ +_______ +8.013 +_____ +_____ +45 +__ +__ +46.89 +_____ +_____ + 4567. + _____ +____________________ +>>>>37800 + _____ +_________ +576,446.88800000 +________________ +________________ +576,446.890 +___________ +___________ +4798908.340000000000 +____________________ +____________________ +4798908.45 +__________ +__________ +4798908.8909800 +_______________ +_______________ diff --git a/tests/fixtures/sort/mixed_floats_ints_chars_numeric_stable.expected.debug b/tests/fixtures/sort/mixed_floats_ints_chars_numeric_stable.expected.debug new file mode 100644 index 000000000..b2782d93d --- /dev/null +++ b/tests/fixtures/sort/mixed_floats_ints_chars_numeric_stable.expected.debug @@ -0,0 +1,60 @@ +-2028789030 +___________ +-896689 +_______ +-8.90880 +________ +-1 +__ +-.05 +____ + +^ no match for key + +^ no match for key + +^ no match for key + +^ no match for key + +^ no match for key +CARAvan +^ no match for key + +^ no match for key + +^ no match for key + +^ no match for key +000 +___ +1 +_ +00000001 +________ +1.040000000 +___________ +1.444 +_____ +1.58590 +_______ +8.013 +_____ +45 +__ +46.89 +_____ + 4567. + _____ +>>>>37800 + _____ +576,446.88800000 +________________ +576,446.890 +___________ +4798908.340000000000 +____________________ +4798908.45 +__________ +4798908.8909800 +_______________ diff --git a/tests/fixtures/sort/mixed_floats_ints_chars_numeric_unique.expected.debug b/tests/fixtures/sort/mixed_floats_ints_chars_numeric_unique.expected.debug new file mode 100644 index 000000000..782a77724 --- /dev/null +++ b/tests/fixtures/sort/mixed_floats_ints_chars_numeric_unique.expected.debug @@ -0,0 +1,40 @@ +-2028789030 +___________ +-896689 +_______ +-8.90880 +________ +-1 +__ +-.05 +____ + +^ no match for key +1 +_ +1.040000000 +___________ +1.444 +_____ +1.58590 +_______ +8.013 +_____ +45 +__ +46.89 +_____ + 4567. + _____ +>>>>37800 + _____ +576,446.88800000 +________________ +576,446.890 +___________ +4798908.340000000000 +____________________ +4798908.45 +__________ +4798908.8909800 +_______________ diff --git a/tests/fixtures/sort/mixed_floats_ints_chars_numeric_unique_reverse.expected.debug b/tests/fixtures/sort/mixed_floats_ints_chars_numeric_unique_reverse.expected.debug new file mode 100644 index 000000000..e0389c1d5 --- /dev/null +++ b/tests/fixtures/sort/mixed_floats_ints_chars_numeric_unique_reverse.expected.debug @@ -0,0 +1,40 @@ +4798908.8909800 +_______________ +4798908.45 +__________ +4798908.340000000000 +____________________ +576,446.890 +___________ +576,446.88800000 +________________ +>>>>37800 + _____ + 4567. + _____ +46.89 +_____ +45 +__ +8.013 +_____ +1.58590 +_______ +1.444 +_____ +1.040000000 +___________ +1 +_ + +^ no match for key +-.05 +____ +-1 +__ +-8.90880 +________ +-896689 +_______ +-2028789030 +___________ diff --git a/tests/fixtures/sort/month_default.expected.debug b/tests/fixtures/sort/month_default.expected.debug new file mode 100644 index 000000000..2c55a0e2a --- /dev/null +++ b/tests/fixtures/sort/month_default.expected.debug @@ -0,0 +1,30 @@ +N/A Ut enim ad minim veniam, quis +^ no match for key +_________________________________ +Jan Lorem ipsum dolor sit amet +___ +______________________________ +mar laboris nisi ut aliquip ex ea +___ +_________________________________ +May sed do eiusmod tempor incididunt +___ +____________________________________ +JUN nostrud exercitation ullamco +___ +________________________________ +Jul 1 should remain 2,1,3 +___ +_________________________ +Jul 2 these three lines +___ +_______________________ +Jul 3 if --stable is provided +___ +_____________________________ +Oct ut labore et dolore magna aliqua +___ +____________________________________ +Dec consectetur adipiscing elit +___ +_______________________________ diff --git a/tests/fixtures/sort/month_stable.expected.debug b/tests/fixtures/sort/month_stable.expected.debug new file mode 100644 index 000000000..4163ba39a --- /dev/null +++ b/tests/fixtures/sort/month_stable.expected.debug @@ -0,0 +1,20 @@ +N/A Ut enim ad minim veniam, quis +^ no match for key +Jan Lorem ipsum dolor sit amet +___ +mar laboris nisi ut aliquip ex ea +___ +May sed do eiusmod tempor incididunt +___ +JUN nostrud exercitation ullamco +___ +Jul 2 these three lines +___ +Jul 1 should remain 2,1,3 +___ +Jul 3 if --stable is provided +___ +Oct ut labore et dolore magna aliqua +___ +Dec consectetur adipiscing elit +___ diff --git a/tests/fixtures/sort/months-dedup.expected.debug b/tests/fixtures/sort/months-dedup.expected.debug new file mode 100644 index 000000000..aded4b951 --- /dev/null +++ b/tests/fixtures/sort/months-dedup.expected.debug @@ -0,0 +1,12 @@ + +^ no match for key +JAN +___ +apr +___ +MAY +___ +JUNNNN +___ +AUG +___ diff --git a/tests/fixtures/sort/months-whitespace.expected.debug b/tests/fixtures/sort/months-whitespace.expected.debug new file mode 100644 index 000000000..ef626f505 --- /dev/null +++ b/tests/fixtures/sort/months-whitespace.expected.debug @@ -0,0 +1,24 @@ + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key +JAN +___ +___ + FEb + ___ +_____ + apr + ___ +____ + apr + ___ +____ +>>>JUNNNN + ___ +_________ +AUG +___ +____ diff --git a/tests/fixtures/sort/multiple_decimals_general.expected b/tests/fixtures/sort/multiple_decimals_general.expected new file mode 100644 index 000000000..b08ada324 --- /dev/null +++ b/tests/fixtures/sort/multiple_decimals_general.expected @@ -0,0 +1,37 @@ + + + + + + + +CARAvan + NaN + -inf +-2028789030 +-896689 +-8.90880 +-1 +-.05 +000 +00000001 +1 +1.040000000 +1.444 +1.58590 +8.013 +45 +46.89 +576,446.88800000 +576,446.890 + 4567..457 + 4567. +4567.1 +4567.34 + 37800 + 45670.89079.098 + 45670.89079.1 +4798908.340000000000 +4798908.45 +4798908.8909800 +inf diff --git a/tests/fixtures/sort/multiple_decimals_general.expected.debug b/tests/fixtures/sort/multiple_decimals_general.expected.debug new file mode 100644 index 000000000..1bf5d2669 --- /dev/null +++ b/tests/fixtures/sort/multiple_decimals_general.expected.debug @@ -0,0 +1,111 @@ + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key +CARAvan +^ no match for key +_______ + NaN + ___ +_____ +>-inf + ____ +_____ +-2028789030 +___________ +___________ +-896689 +_______ +_______ +-8.90880 +________ +________ +-1 +__ +__ +-.05 +____ +____ +000 +___ +___ +00000001 +________ +________ +1 +_ +_ +1.040000000 +___________ +___________ +1.444 +_____ +_____ +1.58590 +_______ +_______ +8.013 +_____ +_____ +45 +__ +__ +46.89 +_____ +_____ +576,446.88800000 +___ +________________ +576,446.890 +___ +___________ +>>>>>>>>>>4567..457 + _____ +___________________ + 4567. + _____ +____________________ +4567.1 +______ +______ +4567.34 +_______ +_______ +>>>>37800 + _____ +_________ +>>>>>>45670.89079.098 + ___________ +_____________________ +>>>>>>45670.89079.1 + ___________ +___________________ +4798908.340000000000 +____________________ +____________________ +4798908.45 +__________ +__________ +4798908.8909800 +_______________ +_______________ +inf +___ +___ diff --git a/tests/fixtures/sort/multiple_decimals_general.txt b/tests/fixtures/sort/multiple_decimals_general.txt index 4e65ecfda..0feb0ce7f 100644 --- a/tests/fixtures/sort/multiple_decimals_general.txt +++ b/tests/fixtures/sort/multiple_decimals_general.txt @@ -32,4 +32,6 @@ CARAvan 8.013 000 - + NaN +inf + -inf diff --git a/tests/fixtures/sort/multiple_decimals_numeric.expected.debug b/tests/fixtures/sort/multiple_decimals_numeric.expected.debug new file mode 100644 index 000000000..f40ade9aa --- /dev/null +++ b/tests/fixtures/sort/multiple_decimals_numeric.expected.debug @@ -0,0 +1,105 @@ +-2028789030 +___________ +___________ +-896689 +_______ +_______ +-8.90880 +________ +________ +-1 +__ +__ +-.05 +____ +____ + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key +000 +___ +___ +CARAvan +^ no match for key +_______ +00000001 +________ +________ +1 +_ +_ +1.040000000 +___________ +___________ +1.444 +_____ +_____ +1.58590 +_______ +_______ +8.013 +_____ +_____ +45 +__ +__ +46.89 +_____ +_____ +>>>>>>>>>>4567..457 + _____ +___________________ + 4567. + _____ +____________________ +4567.1 +______ +______ +4567.34 +_______ +_______ +>>>>37800 + _____ +_________ +>>>>>>45670.89079.098 + ___________ +_____________________ +>>>>>>45670.89079.1 + ___________ +___________________ +576,446.88800000 +________________ +________________ +576,446.890 +___________ +___________ +4798908.340000000000 +____________________ +____________________ +4798908.45 +__________ +__________ +4798908.8909800 +_______________ +_______________ diff --git a/tests/fixtures/sort/numeric-floats-with-nan2.expected.debug b/tests/fixtures/sort/numeric-floats-with-nan2.expected.debug new file mode 100644 index 000000000..b5a2c2f64 --- /dev/null +++ b/tests/fixtures/sort/numeric-floats-with-nan2.expected.debug @@ -0,0 +1,69 @@ +-8.90880 +________ +________ +-.05 +____ +____ + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key +Karma +^ no match for key +_____ +1 +_ +_ +1.0/0.0 +___ +_______ +1.040000000 +___________ +___________ +1.2 +___ +___ +1.444 +_____ +_____ +1.58590 +_______ +_______ diff --git a/tests/fixtures/sort/numeric_fixed_floats.expected.debug b/tests/fixtures/sort/numeric_fixed_floats.expected.debug new file mode 100644 index 000000000..fa8a909c5 --- /dev/null +++ b/tests/fixtures/sort/numeric_fixed_floats.expected.debug @@ -0,0 +1,6 @@ +.00 +___ +___ +.01 +___ +___ diff --git a/tests/fixtures/sort/numeric_floats.expected.debug b/tests/fixtures/sort/numeric_floats.expected.debug new file mode 100644 index 000000000..e24056376 --- /dev/null +++ b/tests/fixtures/sort/numeric_floats.expected.debug @@ -0,0 +1,6 @@ +.02 +___ +___ +.03 +___ +___ diff --git a/tests/fixtures/sort/numeric_floats_and_ints.expected.debug b/tests/fixtures/sort/numeric_floats_and_ints.expected.debug new file mode 100644 index 000000000..c43d6bfb6 --- /dev/null +++ b/tests/fixtures/sort/numeric_floats_and_ints.expected.debug @@ -0,0 +1,6 @@ +0 +_ +_ +.02 +___ +___ diff --git a/tests/fixtures/sort/numeric_floats_with_nan.expected.debug b/tests/fixtures/sort/numeric_floats_with_nan.expected.debug new file mode 100644 index 000000000..07e72db53 --- /dev/null +++ b/tests/fixtures/sort/numeric_floats_with_nan.expected.debug @@ -0,0 +1,9 @@ +NaN +^ no match for key +___ +.02 +___ +___ +.03 +___ +___ diff --git a/tests/fixtures/sort/numeric_unfixed_floats.expected.debug b/tests/fixtures/sort/numeric_unfixed_floats.expected.debug new file mode 100644 index 000000000..a60daf623 --- /dev/null +++ b/tests/fixtures/sort/numeric_unfixed_floats.expected.debug @@ -0,0 +1,6 @@ +.000 +____ +____ +.01 +___ +___ diff --git a/tests/fixtures/sort/numeric_unique.expected.debug b/tests/fixtures/sort/numeric_unique.expected.debug new file mode 100644 index 000000000..79ec70364 --- /dev/null +++ b/tests/fixtures/sort/numeric_unique.expected.debug @@ -0,0 +1,4 @@ +-10 bb +___ +aa +^ no match for key diff --git a/tests/fixtures/sort/numeric_unsorted_ints.expected.debug b/tests/fixtures/sort/numeric_unsorted_ints.expected.debug new file mode 100644 index 000000000..b16931f5e --- /dev/null +++ b/tests/fixtures/sort/numeric_unsorted_ints.expected.debug @@ -0,0 +1,300 @@ +1 +_ +_ +2 +_ +_ +3 +_ +_ +4 +_ +_ +5 +_ +_ +6 +_ +_ +7 +_ +_ +8 +_ +_ +9 +_ +_ +10 +__ +__ +11 +__ +__ +12 +__ +__ +13 +__ +__ +14 +__ +__ +15 +__ +__ +16 +__ +__ +17 +__ +__ +18 +__ +__ +19 +__ +__ +20 +__ +__ +21 +__ +__ +22 +__ +__ +23 +__ +__ +24 +__ +__ +25 +__ +__ +26 +__ +__ +27 +__ +__ +28 +__ +__ +29 +__ +__ +30 +__ +__ +31 +__ +__ +32 +__ +__ +33 +__ +__ +34 +__ +__ +35 +__ +__ +36 +__ +__ +37 +__ +__ +38 +__ +__ +39 +__ +__ +40 +__ +__ +41 +__ +__ +42 +__ +__ +43 +__ +__ +44 +__ +__ +45 +__ +__ +46 +__ +__ +47 +__ +__ +48 +__ +__ +49 +__ +__ +50 +__ +__ +51 +__ +__ +52 +__ +__ +53 +__ +__ +54 +__ +__ +55 +__ +__ +56 +__ +__ +57 +__ +__ +58 +__ +__ +59 +__ +__ +60 +__ +__ +61 +__ +__ +62 +__ +__ +63 +__ +__ +64 +__ +__ +65 +__ +__ +66 +__ +__ +67 +__ +__ +68 +__ +__ +69 +__ +__ +70 +__ +__ +71 +__ +__ +72 +__ +__ +73 +__ +__ +74 +__ +__ +75 +__ +__ +76 +__ +__ +77 +__ +__ +78 +__ +__ +79 +__ +__ +80 +__ +__ +81 +__ +__ +82 +__ +__ +83 +__ +__ +84 +__ +__ +85 +__ +__ +86 +__ +__ +87 +__ +__ +88 +__ +__ +89 +__ +__ +90 +__ +__ +91 +__ +__ +92 +__ +__ +93 +__ +__ +94 +__ +__ +95 +__ +__ +96 +__ +__ +97 +__ +__ +98 +__ +__ +99 +__ +__ +100 +___ +___ diff --git a/tests/fixtures/sort/numeric_unsorted_ints_unique.expected.debug b/tests/fixtures/sort/numeric_unsorted_ints_unique.expected.debug new file mode 100644 index 000000000..072a57ccf --- /dev/null +++ b/tests/fixtures/sort/numeric_unsorted_ints_unique.expected.debug @@ -0,0 +1,8 @@ +1 +_ +2 +_ +3 +_ +4 +_ diff --git a/tests/fixtures/sort/version.expected.debug b/tests/fixtures/sort/version.expected.debug new file mode 100644 index 000000000..2d922b5c0 --- /dev/null +++ b/tests/fixtures/sort/version.expected.debug @@ -0,0 +1,12 @@ +1.2.3-alpha +___________ +___________ +1.2.3-alpha2 +____________ +____________ +1.12.4 +______ +______ +11.2.3 +______ +______ diff --git a/tests/fixtures/sort/words_unique.expected.debug b/tests/fixtures/sort/words_unique.expected.debug new file mode 100644 index 000000000..0c32daf74 --- /dev/null +++ b/tests/fixtures/sort/words_unique.expected.debug @@ -0,0 +1,6 @@ +aaa +___ +bbb +___ +zzz +___ diff --git a/tests/fixtures/sort/zero-terminated.expected.debug b/tests/fixtures/sort/zero-terminated.expected.debug new file mode 100644 index 000000000..fbef272b0 --- /dev/null +++ b/tests/fixtures/sort/zero-terminated.expected.debug @@ -0,0 +1,84 @@ +../.. +_____ +../../by-util +_____________ +../../common +____________ +../../fixtures +______________ +../../fixtures/cat +__________________ +../../fixtures/cksum +____________________ +../../fixtures/comm +___________________ +../../fixtures/cp +_________________ +../../fixtures/cp/dir_with_mount +________________________________ +../../fixtures/cp/dir_with_mount/copy_me +________________________________________ +../../fixtures/cp/hello_dir +___________________________ +../../fixtures/cp/hello_dir_with_file +_____________________________________ +../../fixtures/csplit +_____________________ +../../fixtures/cut +__________________ +../../fixtures/cut/sequences +____________________________ +../../fixtures/dircolors +________________________ +../../fixtures/du +_________________ +../../fixtures/du/subdir +________________________ +../../fixtures/du/subdir/deeper +_______________________________ +../../fixtures/du/subdir/links +______________________________ +../../fixtures/env +__________________ +../../fixtures/expand +_____________________ +../../fixtures/fmt +__________________ +../../fixtures/fold +___________________ +../../fixtures/hashsum +______________________ +../../fixtures/head +___________________ +../../fixtures/join +___________________ +../../fixtures/mv +_________________ +../../fixtures/nl +_________________ +../../fixtures/numfmt +_____________________ +../../fixtures/od +_________________ +../../fixtures/paste +____________________ +../../fixtures/ptx +__________________ +../../fixtures/shuf +___________________ +../../fixtures/sort +___________________ +../../fixtures/sum +__________________ +../../fixtures/tac +__________________ +../../fixtures/tail +___________________ +../../fixtures/tsort +____________________ +../../fixtures/unexpand +_______________________ +../../fixtures/uniq +___________________ +../../fixtures/wc +_________________