1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00
This commit is contained in:
electricboogie 2021-04-25 10:11:27 -05:00
commit 4c395146dd
116 changed files with 3865 additions and 1614 deletions

View file

@ -80,6 +80,9 @@ jobs:
-e '/tests\/misc\/help-version-getopt.sh/ D' \ -e '/tests\/misc\/help-version-getopt.sh/ D' \
Makefile 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 # 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|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 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

94
Cargo.lock generated
View file

@ -28,6 +28,15 @@ dependencies = [
"winapi 0.3.9", "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]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.4.12" version = "0.4.12"
@ -127,9 +136,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]] [[package]]
name = "cast" name = "cast"
version = "0.2.3" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0" checksum = "cc38c385bfd7e444464011bb24820f40dd1c76bcdfa1b78611cb7c2e5cafab75"
dependencies = [ dependencies = [
"rustc_version", "rustc_version",
] ]
@ -169,7 +178,7 @@ version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [ dependencies = [
"ansi_term", "ansi_term 0.11.0",
"atty", "atty",
"bitflags", "bitflags",
"strsim", "strsim",
@ -212,6 +221,7 @@ dependencies = [
"lazy_static", "lazy_static",
"libc", "libc",
"nix 0.20.0", "nix 0.20.0",
"pretty_assertions",
"rand 0.7.3", "rand 0.7.3",
"regex", "regex",
"sha1", "sha1",
@ -452,9 +462,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.0" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"crossbeam-utils", "crossbeam-utils",
@ -517,6 +527,16 @@ dependencies = [
"memchr 2.3.4", "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]] [[package]]
name = "custom_derive" name = "custom_derive"
version = "0.1.7" version = "0.1.7"
@ -529,6 +549,12 @@ version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f47ca1860a761136924ddd2422ba77b2ea54fe8cc75b9040804a0d9d32ad97" checksum = "f4f47ca1860a761136924ddd2422ba77b2ea54fe8cc75b9040804a0d9d32ad97"
[[package]]
name = "diff"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.6.2" version = "0.6.2"
@ -580,7 +606,7 @@ checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"redox_syscall 0.2.5", "redox_syscall 0.2.6",
"winapi 0.3.9", "winapi 0.3.9",
] ]
@ -783,6 +809,15 @@ dependencies = [
"cfg-if 1.0.0", "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]] [[package]]
name = "match_cfg" name = "match_cfg"
version = "0.1.0" version = "0.1.0"
@ -897,6 +932,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
[[package]]
name = "once_cell"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
[[package]] [[package]]
name = "onig" name = "onig"
version = "4.3.3" version = "4.3.3"
@ -925,6 +966,15 @@ version = "11.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" 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]] [[package]]
name = "paste" name = "paste"
version = "0.1.18" version = "0.1.18"
@ -994,6 +1044,18 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 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]] [[package]]
name = "proc-macro-hack" name = "proc-macro-hack"
version = "0.5.19" version = "0.5.19"
@ -1176,9 +1238,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
@ -1189,14 +1251,14 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
dependencies = [ dependencies = [
"redox_syscall 0.2.5", "redox_syscall 0.2.6",
] ]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.4.5" version = "1.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr 2.3.4", "memchr 2.3.4",
@ -1400,9 +1462,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.69" version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.9", "quote 1.0.9",
@ -1460,7 +1522,7 @@ checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
dependencies = [ dependencies = [
"libc", "libc",
"numtoa", "numtoa",
"redox_syscall 0.2.5", "redox_syscall 0.2.6",
"redox_termios", "redox_termios",
] ]
@ -1996,11 +2058,12 @@ dependencies = [
"clap", "clap",
"globset", "globset",
"lazy_static", "lazy_static",
"lscolors",
"number_prefix", "number_prefix",
"once_cell",
"term_grid", "term_grid",
"termsize", "termsize",
"time", "time",
"unicode-width",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
] ]
@ -2306,6 +2369,7 @@ dependencies = [
"serde_json", "serde_json",
"smallvec 1.6.1", "smallvec 1.6.1",
"tempdir", "tempdir",
"unicode-width",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
] ]

View file

@ -335,6 +335,7 @@ filetime = "0.2"
glob = "0.3.0" glob = "0.3.0"
libc = "0.2" libc = "0.2"
nix = "0.20.0" nix = "0.20.0"
pretty_assertions = "0.7.2"
rand = "0.7" rand = "0.7"
regex = "1.0" regex = "1.0"
sha1 = { version="0.6", features=["std"] } sha1 = { version="0.6", features=["std"] }

View file

@ -22,6 +22,12 @@ use std::io::{self, Read, Write};
use thiserror::Error; use thiserror::Error;
use uucore::fs::is_stdin_interactive; 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 /// Unix domain socket support
#[cfg(unix)] #[cfg(unix)]
use std::net::Shutdown; use std::net::Shutdown;
@ -30,14 +36,6 @@ use std::os::unix::fs::FileTypeExt;
#[cfg(unix)] #[cfg(unix)]
use unix_socket::UnixStream; 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 NAME: &str = "cat";
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static SYNTAX: &str = "[OPTION]... [FILE]..."; static SYNTAX: &str = "[OPTION]... [FILE]...";
@ -395,7 +393,7 @@ fn write_fast<R: Read>(handle: &mut InputHandle<R>) -> CatResult<()> {
{ {
// If we're on Linux or Android, try to use the splice() system call // If we're on Linux or Android, try to use the splice() system call
// for faster writing. If it works, we're done. // 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(()); return Ok(());
} }
} }
@ -411,75 +409,6 @@ fn write_fast<R: Read>(handle: &mut InputHandle<R>) -> CatResult<()> {
Ok(()) 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<R: Read>(handle: &mut InputHandle<R>, writer: RawFd) -> CatResult<bool> {
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, /// Outputs file contents to stdout in a line-by-line fashion,
/// propagating any errors that might occur. /// propagating any errors that might occur.
fn write_lines<R: Read>( fn write_lines<R: Read>(

91
src/uu/cat/src/splice.rs Normal file
View file

@ -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<R: Read>(
handle: &mut InputHandle<R>,
write_fd: RawFd,
) -> CatResult<bool> {
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(())
}

View file

@ -155,7 +155,8 @@ pub enum OverwriteMode {
NoClobber, NoClobber,
} }
#[derive(Clone, Eq, PartialEq)] /// Possible arguments for `--reflink`.
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum ReflinkMode { pub enum ReflinkMode {
Always, Always,
Auto, Auto,
@ -210,7 +211,6 @@ pub struct Options {
overwrite: OverwriteMode, overwrite: OverwriteMode,
parents: bool, parents: bool,
strip_trailing_slashes: bool, strip_trailing_slashes: bool,
reflink: bool,
reflink_mode: ReflinkMode, reflink_mode: ReflinkMode,
preserve_attributes: Vec<Attribute>, preserve_attributes: Vec<Attribute>,
recursive: bool, recursive: bool,
@ -633,12 +633,12 @@ impl Options {
update: matches.is_present(OPT_UPDATE), update: matches.is_present(OPT_UPDATE),
verbose: matches.is_present(OPT_VERBOSE), verbose: matches.is_present(OPT_VERBOSE),
strip_trailing_slashes: matches.is_present(OPT_STRIP_TRAILING_SLASHES), strip_trailing_slashes: matches.is_present(OPT_STRIP_TRAILING_SLASHES),
reflink: matches.is_present(OPT_REFLINK),
reflink_mode: { reflink_mode: {
if let Some(reflink) = matches.value_of(OPT_REFLINK) { if let Some(reflink) = matches.value_of(OPT_REFLINK) {
match reflink { match reflink {
"always" => ReflinkMode::Always, "always" => ReflinkMode::Always,
"auto" => ReflinkMode::Auto, "auto" => ReflinkMode::Auto,
"never" => ReflinkMode::Never,
value => { value => {
return Err(Error::InvalidArgument(format!( return Err(Error::InvalidArgument(format!(
"invalid argument '{}' for \'reflink\'", "invalid argument '{}' for \'reflink\'",
@ -1193,47 +1193,20 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> {
Ok(()) Ok(())
} }
///Copy the file from `source` to `dest` either using the normal `fs::copy` or the /// Copy the file from `source` to `dest` either using the normal `fs::copy` or a
///`FICLONE` ioctl if --reflink is specified and the filesystem supports it. /// copy-on-write scheme if --reflink is specified and the filesystem supports it.
fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> {
if options.reflink { if options.reflink_mode != ReflinkMode::Never {
#[cfg(not(target_os = "linux"))] #[cfg(not(any(target_os = "linux", target_os = "macos")))]
return Err("--reflink is only supported on linux".to_string().into()); 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")] #[cfg(target_os = "linux")]
{ copy_on_write_linux(source, dest, options.reflink_mode)?;
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 => {}
}
}
} else if options.no_dereference && fs::symlink_metadata(&source)?.file_type().is_symlink() { } else if options.no_dereference && fs::symlink_metadata(&source)?.file_type().is_symlink() {
// Here, we will copy the symlink itself (actually, just recreate it) // Here, we will copy the symlink itself (actually, just recreate it)
let link = fs::read_link(&source)?; let link = fs::read_link(&source)?;
@ -1266,6 +1239,101 @@ fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()>
Ok(()) 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` /// Generate an error message if `target` is not the correct `target_type`
pub fn verify_target_type(target: &Path, target_type: &TargetType) -> CopyResult<()> { pub fn verify_target_type(target: &Path, target_type: &TargetType) -> CopyResult<()> {
match (target_type, target.is_dir()) { match (target_type, target.is_dir()) {

View file

@ -500,7 +500,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
}; };
let strs = if matches.free.is_empty() { let strs = if matches.free.is_empty() {
vec!["./".to_owned()] vec!["./".to_owned()] // TODO: gnu `du` doesn't use trailing "/" here
} else { } else {
matches.free.clone() matches.free.clone()
}; };

View file

@ -41,6 +41,7 @@ pub struct Behavior {
compare: bool, compare: bool,
strip: bool, strip: bool,
strip_program: String, strip_program: String,
create_leading: bool,
} }
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
@ -70,7 +71,7 @@ static OPT_BACKUP: &str = "backup";
static OPT_BACKUP_2: &str = "backup2"; static OPT_BACKUP_2: &str = "backup2";
static OPT_DIRECTORY: &str = "directory"; static OPT_DIRECTORY: &str = "directory";
static OPT_IGNORED: &str = "ignored"; static OPT_IGNORED: &str = "ignored";
static OPT_CREATED: &str = "created"; static OPT_CREATE_LEADING: &str = "create-leading";
static OPT_GROUP: &str = "group"; static OPT_GROUP: &str = "group";
static OPT_MODE: &str = "mode"; static OPT_MODE: &str = "mode";
static OPT_OWNER: &str = "owner"; static OPT_OWNER: &str = "owner";
@ -133,9 +134,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.arg( .arg(
// TODO implement flag // TODO implement flag
Arg::with_name(OPT_CREATED) Arg::with_name(OPT_CREATE_LEADING)
.short("D") .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(
Arg::with_name(OPT_GROUP) Arg::with_name(OPT_GROUP)
@ -266,8 +267,6 @@ fn check_unimplemented<'a>(matches: &ArgMatches) -> Result<(), &'a str> {
Err("--backup") Err("--backup")
} else if matches.is_present(OPT_BACKUP_2) { } else if matches.is_present(OPT_BACKUP_2) {
Err("-b") Err("-b")
} else if matches.is_present(OPT_CREATED) {
Err("-D")
} else if matches.is_present(OPT_SUFFIX) { } else if matches.is_present(OPT_SUFFIX) {
Err("--suffix, -S") Err("--suffix, -S")
} else if matches.is_present(OPT_TARGET_DIRECTORY) { } else if matches.is_present(OPT_TARGET_DIRECTORY) {
@ -343,6 +342,7 @@ fn behavior(matches: &ArgMatches) -> Result<Behavior, i32> {
.value_of(OPT_STRIP_PROGRAM) .value_of(OPT_STRIP_PROGRAM)
.unwrap_or(DEFAULT_STRIP_PROGRAM), .unwrap_or(DEFAULT_STRIP_PROGRAM),
), ),
create_leading: matches.is_present(OPT_CREATE_LEADING),
}) })
} }
@ -410,12 +410,35 @@ fn standard(paths: Vec<String>, b: Behavior) -> i32 {
.iter() .iter()
.map(PathBuf::from) .map(PathBuf::from)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let target = Path::new(paths.last().unwrap()); let target = Path::new(paths.last().unwrap());
if (target.is_file() || is_new_file_path(target)) && sources.len() == 1 { 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) copy_file_to_file(&sources[0], &target.to_path_buf(), &b)
} else { } else {
copy_files_into_dir(sources, &target.to_path_buf(), &b) show_error!(
"invalid target {}: No such file or directory",
target.display()
);
1
}
} }
} }

34
src/uu/ls/BENCHMARKING.md Normal file
View file

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

View file

@ -16,18 +16,19 @@ path = "src/ls.rs"
[dependencies] [dependencies]
clap = "2.33" clap = "2.33"
lazy_static = "1.0.1"
number_prefix = "0.4" number_prefix = "0.4"
term_grid = "0.1.5" term_grid = "0.1.5"
termsize = "0.1.6" termsize = "0.1.6"
time = "0.1.40" time = "0.1.40"
unicode-width = "0.1.5"
globset = "0.4.6" 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 = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "fs"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
once_cell = "1.7.2"
atty = "0.2"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
atty = "0.2" lazy_static = "1.4.0"
[[bin]] [[bin]]
name = "ls" name = "ls"

View file

@ -7,41 +7,41 @@
// spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf // spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf
#[macro_use]
extern crate uucore;
#[cfg(unix)] #[cfg(unix)]
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
#[macro_use]
extern crate uucore;
mod quoting_style; mod quoting_style;
mod version_cmp; mod version_cmp;
use clap::{App, Arg}; use clap::{App, Arg};
use globset::{self, Glob, GlobSet, GlobSetBuilder}; use globset::{self, Glob, GlobSet, GlobSetBuilder};
use lscolors::LsColors;
use number_prefix::NumberPrefix; use number_prefix::NumberPrefix;
use once_cell::unsync::OnceCell;
use quoting_style::{escape_name, QuotingStyle}; 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)] #[cfg(windows)]
use std::os::windows::fs::MetadataExt; 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)] #[cfg(unix)]
use std::time::Duration; use std::{
use std::time::{SystemTime, UNIX_EPOCH}; collections::HashMap,
use std::{cmp::Reverse, process::exit}; os::unix::fs::{FileTypeExt, MetadataExt},
time::Duration,
};
use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
use time::{strftime, Timespec}; use time::{strftime, Timespec};
#[cfg(unix)] #[cfg(unix)]
use unicode_width::UnicodeWidthStr; use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR};
#[cfg(unix)]
use uucore::libc::{mode_t, S_ISGID, S_ISUID, S_ISVTX, S_IWOTH, S_IXGRP, S_IXOTH, S_IXUSR};
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static ABOUT: &str = " static ABOUT: &str = "
@ -54,30 +54,6 @@ fn get_usage() -> String {
format!("{0} [OPTION]... [FILE]...", executable!()) 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 options {
pub mod format { pub mod format {
pub static ONELINE: &str = "1"; pub static ONELINE: &str = "1";
@ -113,10 +89,8 @@ pub mod options {
pub static C: &str = "quote-name"; pub static C: &str = "quote-name";
} }
pub static QUOTING_STYLE: &str = "quoting-style"; pub static QUOTING_STYLE: &str = "quoting-style";
pub mod indicator_style { pub mod indicator_style {
pub static NONE: &str = "none"; pub static SLASH: &str = "p";
pub static SLASH: &str = "slash";
pub static FILE_TYPE: &str = "file-type"; pub static FILE_TYPE: &str = "file-type";
pub static CLASSIFY: &str = "classify"; pub static CLASSIFY: &str = "classify";
} }
@ -135,9 +109,6 @@ pub mod options {
pub static TIME: &str = "time"; pub static TIME: &str = "time";
pub static IGNORE_BACKUPS: &str = "ignore-backups"; pub static IGNORE_BACKUPS: &str = "ignore-backups";
pub static DIRECTORY: &str = "directory"; 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 INODE: &str = "inode";
pub static REVERSE: &str = "reverse"; pub static REVERSE: &str = "reverse";
pub static RECURSIVE: &str = "recursive"; pub static RECURSIVE: &str = "recursive";
@ -212,8 +183,7 @@ struct Config {
time: Time, time: Time,
#[cfg(unix)] #[cfg(unix)]
inode: bool, inode: bool,
#[cfg(unix)] color: Option<LsColors>,
color: bool,
long: LongFormat, long: LongFormat,
width: Option<u16>, width: Option<u16>,
quoting_style: QuotingStyle, quoting_style: QuotingStyle,
@ -337,8 +307,7 @@ impl Config {
Time::Modification Time::Modification
}; };
#[cfg(unix)] let needs_color = match options.value_of(options::COLOR) {
let color = match options.value_of(options::COLOR) {
None => options.is_present(options::COLOR), None => options.is_present(options::COLOR),
Some(val) => match val { Some(val) => match val {
"" | "always" | "yes" | "force" => true, "" | "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) { let size_format = if options.is_present(options::size::HUMAN_READABLE) {
SizeFormat::Binary SizeFormat::Binary
} else if options.is_present(options::size::SI) { } else if options.is_present(options::size::SI) {
@ -448,19 +423,11 @@ impl Config {
"slash" => IndicatorStyle::Slash, "slash" => IndicatorStyle::Slash,
&_ => IndicatorStyle::None, &_ => IndicatorStyle::None,
} }
} else if options.is_present(options::indicator_style::NONE) { } else if options.is_present(options::indicator_style::CLASSIFY) {
IndicatorStyle::None
} else if options.is_present(options::indicator_style::CLASSIFY)
|| options.is_present(options::CLASSIFY)
{
IndicatorStyle::Classify IndicatorStyle::Classify
} else if options.is_present(options::indicator_style::SLASH) } else if options.is_present(options::indicator_style::SLASH) {
|| options.is_present(options::SLASH)
{
IndicatorStyle::Slash IndicatorStyle::Slash
} else if options.is_present(options::indicator_style::FILE_TYPE) } else if options.is_present(options::indicator_style::FILE_TYPE) {
|| options.is_present(options::FILE_TYPE)
{
IndicatorStyle::FileType IndicatorStyle::FileType
} else { } else {
IndicatorStyle::None 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 ignore_patterns = ignore_patterns.build().unwrap();
let dereference = if options.is_present(options::dereference::ALL) { let dereference = if options.is_present(options::dereference::ALL) {
@ -520,7 +491,6 @@ impl Config {
size_format, size_format,
directory: options.is_present(options::DIRECTORY), directory: options.is_present(options::DIRECTORY),
time, time,
#[cfg(unix)]
color, color,
#[cfg(unix)] #[cfg(unix)]
inode: options.is_present(options::INODE), inode: options.is_present(options::INODE),
@ -983,45 +953,45 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.takes_value(true) .takes_value(true)
.possible_values(&["none", "slash", "file-type", "classify"]) .possible_values(&["none", "slash", "file-type", "classify"])
.overrides_with_all(&[ .overrides_with_all(&[
options::FILE_TYPE, options::indicator_style::FILE_TYPE,
options::SLASH, options::indicator_style::SLASH,
options::CLASSIFY, options::indicator_style::CLASSIFY,
options::INDICATOR_STYLE, options::INDICATOR_STYLE,
])) ]))
.arg( .arg(
Arg::with_name(options::CLASSIFY) Arg::with_name(options::indicator_style::CLASSIFY)
.short("F") .short("F")
.long(options::CLASSIFY) .long(options::indicator_style::CLASSIFY)
.help("Append a character to each file name indicating the file type. Also, for \ .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 \ regular files that are executable, append '*'. The file type indicators are \
'/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \ '/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \
'>' for doors, and nothing for regular files.") '>' for doors, and nothing for regular files.")
.overrides_with_all(&[ .overrides_with_all(&[
options::FILE_TYPE, options::indicator_style::FILE_TYPE,
options::SLASH, options::indicator_style::SLASH,
options::CLASSIFY, options::indicator_style::CLASSIFY,
options::INDICATOR_STYLE, options::INDICATOR_STYLE,
]) ])
) )
.arg( .arg(
Arg::with_name(options::FILE_TYPE) Arg::with_name(options::indicator_style::FILE_TYPE)
.long(options::FILE_TYPE) .long(options::indicator_style::FILE_TYPE)
.help("Same as --classify, but do not append '*'") .help("Same as --classify, but do not append '*'")
.overrides_with_all(&[ .overrides_with_all(&[
options::FILE_TYPE, options::indicator_style::FILE_TYPE,
options::SLASH, options::indicator_style::SLASH,
options::CLASSIFY, options::indicator_style::CLASSIFY,
options::INDICATOR_STYLE, options::INDICATOR_STYLE,
])) ]))
.arg( .arg(
Arg::with_name(options::SLASH) Arg::with_name(options::indicator_style::SLASH)
.short(options::SLASH) .short(options::indicator_style::SLASH)
.help("Append / indicator to directories." .help("Append / indicator to directories."
) )
.overrides_with_all(&[ .overrides_with_all(&[
options::FILE_TYPE, options::indicator_style::FILE_TYPE,
options::SLASH, options::indicator_style::SLASH,
options::CLASSIFY, options::indicator_style::CLASSIFY,
options::INDICATOR_STYLE, options::INDICATOR_STYLE,
])) ]))
@ -1038,12 +1008,82 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
list(locs, Config::from(matches)) 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<MetaData> got from symlink_metadata() or metadata() based on config
md: OnceCell<Option<Metadata>>,
ft: OnceCell<Option<FileType>>,
// 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<std::io::Result<FileType>>,
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<String>, config: Config) -> i32 { fn list(locs: Vec<String>, config: Config) -> i32 {
let number_of_locs = locs.len(); let number_of_locs = locs.len();
let mut files = Vec::<PathBuf>::new(); let mut files = Vec::<PathData>::new();
let mut dirs = Vec::<PathBuf>::new(); let mut dirs = Vec::<PathData>::new();
let mut has_failed = false; let mut has_failed = false;
let mut out = BufWriter::new(stdout());
for loc in locs { for loc in locs {
let p = PathBuf::from(&loc); let p = PathBuf::from(&loc);
if !p.exists() { if !p.exists() {
@ -1054,38 +1094,30 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
continue; continue;
} }
let show_dir_contents = if !config.directory { let path_data = PathData::new(p, None, &config, true);
match config.dereference {
Dereference::None => { let show_dir_contents = if let Some(ft) = path_data.file_type() {
if let Ok(md) = p.symlink_metadata() { !config.directory && ft.is_dir()
md.is_dir()
} else { } else {
show_error!("'{}': {}", &loc, "No such file or directory");
has_failed = true; has_failed = true;
continue;
}
}
_ => p.is_dir(),
}
} else {
false false
}; };
if show_dir_contents { if show_dir_contents {
dirs.push(p); dirs.push(path_data);
} else { } else {
files.push(p); files.push(path_data);
} }
} }
sort_entries(&mut files, &config); sort_entries(&mut files, &config);
display_items(&files, None, &config, true); display_items(&files, None, &config, &mut out);
sort_entries(&mut dirs, &config); sort_entries(&mut dirs, &config);
for dir in dirs { for dir in dirs {
if number_of_locs > 1 { 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 { if has_failed {
1 1
@ -1094,22 +1126,21 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
} }
} }
fn sort_entries(entries: &mut Vec<PathBuf>, config: &Config) { fn sort_entries(entries: &mut Vec<PathData>, config: &Config) {
match config.sort { match config.sort {
Sort::Time => entries.sort_by_key(|k| { Sort::Time => entries.sort_by_key(|k| {
Reverse( Reverse(
get_metadata(k, false) k.md()
.ok()
.and_then(|md| get_system_time(&md, config)) .and_then(|md| get_system_time(&md, config))
.unwrap_or(UNIX_EPOCH), .unwrap_or(UNIX_EPOCH),
) )
}), }),
Sort::Size => { 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 // The default sort in GNU ls is case insensitive
Sort::Name => entries.sort_by_key(|k| k.to_string_lossy().to_lowercase()), Sort::Name => entries.sort_by_cached_key(|k| k.file_name.to_lowercase()),
Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(a, b)), Sort::Version => entries.sort_by(|k, j| version_cmp::version_cmp(&k.p_buf, &j.p_buf)),
Sort::None => {} Sort::None => {}
} }
@ -1122,48 +1153,53 @@ fn sort_entries(entries: &mut Vec<PathBuf>, config: &Config) {
fn is_hidden(file_path: &DirEntry) -> bool { fn is_hidden(file_path: &DirEntry) -> bool {
let metadata = fs::metadata(file_path.path()).unwrap(); let metadata = fs::metadata(file_path.path()).unwrap();
let attr = metadata.file_attributes(); let attr = metadata.file_attributes();
((attr & 0x2) > 0) || file_path.file_name().to_string_lossy().starts_with('.') (attr & 0x2) > 0
}
#[cfg(unix)]
fn is_hidden(file_path: &DirEntry) -> bool {
file_path.file_name().to_string_lossy().starts_with('.')
} }
fn should_display(entry: &DirEntry, config: &Config) -> bool { fn should_display(entry: &DirEntry, config: &Config) -> bool {
let ffi_name = entry.file_name(); let ffi_name = entry.file_name();
// For unix, the hidden files are already included in the ignore pattern
#[cfg(windows)]
{
if config.files == Files::Normal && is_hidden(entry) { if config.files == Files::Normal && is_hidden(entry) {
return false; return false;
} }
if config.ignore_patterns.is_match(&ffi_name) {
return false;
}
true
} }
fn enter_directory(dir: &Path, config: &Config) { !config.ignore_patterns.is_match(&ffi_name)
let mut entries: Vec<_> = safe_unwrap!(fs::read_dir(dir).and_then(Iterator::collect)); }
entries.retain(|e| should_display(e, config)); fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter<Stdout>) {
let mut entries: Vec<_> = if config.files == Files::All {
let mut entries: Vec<_> = entries.iter().map(DirEntry::path).collect(); vec![
sort_entries(&mut entries, config); PathData::new(dir.p_buf.join("."), None, config, false),
PathData::new(dir.p_buf.join(".."), None, config, false),
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);
} else { } 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 { if config.recursive {
for e in entries.iter().filter(|p| p.is_dir()) { for e in entries
println!("\n{}:", e.to_string_lossy()); .iter()
enter_directory(&e, config); .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<Metadata> {
} }
} }
fn display_dir_entry_size(entry: &Path, config: &Config) -> (usize, usize) { fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize) {
if let Ok(md) = get_metadata(entry, false) { if let Some(md) = entry.md() {
( (
display_symlink_count(&md).len(), display_symlink_count(&md).len(),
display_file_size(&md, config).len(), display_file_size(&md, config).len(),
@ -1191,7 +1227,12 @@ fn pad_left(string: String, count: usize) -> String {
format!("{:>width$}", string, width = count) 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<Stdout>,
) {
if config.format == Format::Long { if config.format == Format::Long {
let (mut max_links, mut max_size) = (1, 1); let (mut max_links, mut max_size) = (1, 1);
for item in items { 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); max_size = size.max(max_size);
} }
for item in items { 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 { } else {
let names = items.iter().filter_map(|i| { let names = items
let md = get_metadata(i, false); .iter()
match md { .filter_map(|i| display_file_name(&i, strip, config));
Err(e) => {
let filename = get_file_name(i, strip);
show_error!("'{}': {}", filename, e);
None
}
Ok(md) => Some(display_file_name(&i, strip, &md, config)),
}
});
match (&config.format, config.width) { match (&config.format, config.width) {
(Format::Columns, Some(width)) => display_grid(names, width, Direction::TopToBottom), (Format::Columns, Some(width)) => {
(Format::Across, Some(width)) => display_grid(names, width, Direction::LeftToRight), display_grid(names, width, Direction::TopToBottom, out)
}
(Format::Across, Some(width)) => {
display_grid(names, width, Direction::LeftToRight, out)
}
(Format::Commas, width_opt) => { (Format::Commas, width_opt) => {
let term_width = width_opt.unwrap_or(1); let term_width = width_opt.unwrap_or(1);
let mut current_col = 0; let mut current_col = 0;
let mut names = names; let mut names = names;
if let Some(name) = names.next() { if let Some(name) = names.next() {
print!("{}", name.contents); let _ = write!(out, "{}", name.contents);
current_col = name.width as u16 + 2; current_col = name.width as u16 + 2;
} }
for name in names { for name in names {
let name_width = name.width as u16; let name_width = name.width as u16;
if current_col + name_width + 1 > term_width { if current_col + name_width + 1 > term_width {
current_col = name_width + 2; current_col = name_width + 2;
print!(",\n{}", name.contents); let _ = write!(out, ",\n{}", name.contents);
} else { } else {
current_col += name_width + 2; current_col += name_width + 2;
print!(", {}", name.contents); let _ = write!(out, ", {}", name.contents);
} }
} }
// Current col is never zero again if names have been printed. // Current col is never zero again if names have been printed.
// So we print a newline. // So we print a newline.
if current_col > 0 { if current_col > 0 {
println!(); let _ = writeln!(out,);
} }
} }
_ => { _ => {
for name in names { for name in names {
println!("{}", name.contents); let _ = writeln!(out, "{}", name.contents);
} }
} }
} }
} }
} }
fn display_grid(names: impl Iterator<Item = Cell>, width: u16, direction: Direction) { fn display_grid(
names: impl Iterator<Item = Cell>,
width: u16,
direction: Direction,
out: &mut BufWriter<Stdout>,
) {
let mut grid = Grid::new(GridOptions { let mut grid = Grid::new(GridOptions {
filling: Filling::Spaces(2), filling: Filling::Spaces(2),
direction, direction,
@ -1262,56 +1304,44 @@ fn display_grid(names: impl Iterator<Item = Cell>, width: u16, direction: Direct
} }
match grid.fit_into_width(width as usize) { 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 // 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; use uucore::fs::display_permissions;
fn display_item_long( fn display_item_long(
item: &Path, item: &PathData,
strip: Option<&Path>, strip: Option<&Path>,
max_links: usize, max_links: usize,
max_size: usize, max_size: usize,
config: &Config, config: &Config,
command_line: bool, out: &mut BufWriter<Stdout>,
) { ) {
let dereference = match &config.dereference { let md = match item.md() {
Dereference::All => true, None => {
Dereference::Args => command_line, let filename = get_file_name(&item.p_buf, strip);
Dereference::DirArgs => { show_error!("could not show file: {}", filename);
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);
return; return;
} }
Ok(md) => md, Some(md) => md,
}; };
#[cfg(unix)] #[cfg(unix)]
{ {
if config.inode { if config.inode {
print!("{} ", get_inode(&md)); let _ = write!(out, "{} ", get_inode(&md));
} }
} }
print!( let _ = write!(
out,
"{}{} {}", "{}{} {}",
display_file_type(md.file_type()), display_file_type(md.file_type()),
display_permissions(&md), display_permissions(&md),
@ -1319,24 +1349,28 @@ fn display_item_long(
); );
if config.long.owner { if config.long.owner {
print!(" {}", display_uname(&md, config)); let _ = write!(out, " {}", display_uname(&md, config));
} }
if config.long.group { 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 // Author is only different from owner on GNU/Hurd, so we reuse
// the owner, since GNU/Hurd is not currently supported by Rust. // the owner, since GNU/Hurd is not currently supported by Rust.
if config.long.author { 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), pad_left(display_file_size(&md, config), max_size),
display_date(&md, config), 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 // Currently getpwuid is `linux` target only. If it's broken out into
// a posix-compliant attribute this can be updated... // a posix-compliant attribute this can be updated...
#[cfg(unix)] #[cfg(unix)]
use std::sync::Mutex;
#[cfg(unix)]
use uucore::entries; use uucore::entries;
#[cfg(unix)]
fn cached_uid2usr(uid: u32) -> String {
lazy_static! {
static ref UID_CACHE: Mutex<HashMap<u32, String>> = 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)] #[cfg(unix)]
fn display_uname(metadata: &Metadata, config: &Config) -> String { fn display_uname(metadata: &Metadata, config: &Config) -> String {
if config.long.numeric_uid_gid { if config.long.numeric_uid_gid {
metadata.uid().to_string() metadata.uid().to_string()
} else { } 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<HashMap<u32, String>> = 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)] #[cfg(unix)]
fn display_group(metadata: &Metadata, config: &Config) -> String { fn display_group(metadata: &Metadata, config: &Config) -> String {
if config.long.numeric_uid_gid { if config.long.numeric_uid_gid {
metadata.gid().to_string() metadata.gid().to_string()
} else { } 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))] #[cfg(not(unix))]
#[allow(unused_variables)]
fn display_group(_metadata: &Metadata, _config: &Config) -> String { fn display_group(_metadata: &Metadata, _config: &Config) -> String {
"somegroup".to_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() { if file_type.is_dir() {
"d".to_string() 'd'
} else if file_type.is_symlink() { } else if file_type.is_symlink() {
"l".to_string() 'l'
} else { } else {
"-".to_string() '-'
} }
} }
@ -1470,140 +1531,58 @@ fn get_file_name(name: &Path, strip: Option<&Path>) -> String {
name.to_string_lossy().into_owned() name.to_string_lossy().into_owned()
} }
#[cfg(not(unix))] #[cfg(unix)]
fn display_file_name( fn file_is_executable(md: &Metadata) -> bool {
path: &Path, // Mode always returns u32, but the flags might not be, based on the platform
strip: Option<&Path>, // e.g. linux has u32, mac has u16.
metadata: &Metadata, // S_IXUSR -> user has execute permission
config: &Config, // S_IXGRP -> group has execute persmission
) -> Cell { // S_IXOTH -> other users have execute permission
let mut name = escape_name(get_file_name(path, strip), &config.quoting_style); md.mode() & ((S_IXUSR | S_IXGRP | S_IXOTH) as u32) != 0
let file_type = metadata.file_type(); }
#[allow(clippy::clippy::collapsible_else_if)]
fn classify_file(path: &PathData) -> Option<char> {
let file_type = path.file_type()?;
match config.indicator_style {
IndicatorStyle::Classify | IndicatorStyle::FileType => {
if file_type.is_dir() { if file_type.is_dir() {
name.push('/'); Some('/')
}
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 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,
)
} 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() { } else if file_type.is_symlink() {
if path.exists() { Some('@')
("ln", Some('@'))
} else { } else {
("or", Some('@')) #[cfg(unix)]
} {
} else if file_type.is_socket() { if file_type.is_socket() {
("so", Some('=')) Some('=')
} else if file_type.is_fifo() { } else if file_type.is_fifo() {
("pi", Some('|')) Some('|')
} else if file_type.is_block_device() { } else if file_type.is_file() && file_is_executable(path.md()?) {
("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) {
Some('*') Some('*')
} else { } else {
None 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) #[cfg(not(unix))]
}; None
}
}
if config.color { fn display_file_name(path: &PathData, strip: Option<&Path>, config: &Config) -> Option<Cell> {
name = color_name(name, code); 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 { let char_opt = match config.indicator_style {
IndicatorStyle::Classify => sym, IndicatorStyle::Classify => sym,
@ -1626,23 +1605,23 @@ fn display_file_name(
if let Some(c) = char_opt { if let Some(c) = char_opt {
name.push(c); name.push(c);
width += 1;
} }
} }
if config.format == Format::Long && metadata.file_type().is_symlink() { if config.format == Format::Long && path.file_type()?.is_symlink() {
if let Ok(target) = path.read_link() { if let Ok(target) = path.p_buf.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);
name.push_str(" -> "); name.push_str(" -> ");
name.push_str(&target_name); name.push_str(&target.to_string_lossy());
} }
} }
Cell { Some(name.into())
contents: name, }
width,
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,
} }
} }

View file

@ -1,6 +1,6 @@
use std::char::from_digit; use std::char::from_digit;
const SPECIAL_SHELL_CHARS: &str = "~`#$&*()\\|[]{};'\"<>?! "; const SPECIAL_SHELL_CHARS: &str = "~`#$&*()|[]{};\\'\"<>?! ";
pub(super) enum QuotingStyle { pub(super) enum QuotingStyle {
Shell { Shell {
@ -27,12 +27,10 @@ pub(super) enum Quotes {
// This implementation is heavily inspired by the std::char::EscapeDefault implementation // This implementation is heavily inspired by the std::char::EscapeDefault implementation
// in the Rust standard library. This custom implementation is needed because the // in the Rust standard library. This custom implementation is needed because the
// characters \a, \b, \e, \f & \v are not recognized by Rust. // characters \a, \b, \e, \f & \v are not recognized by Rust.
#[derive(Clone, Debug)]
struct EscapedChar { struct EscapedChar {
state: EscapeState, state: EscapeState,
} }
#[derive(Clone, Debug)]
enum EscapeState { enum EscapeState {
Done, Done,
Char(char), Char(char),
@ -41,14 +39,12 @@ enum EscapeState {
Octal(EscapeOctal), Octal(EscapeOctal),
} }
#[derive(Clone, Debug)]
struct EscapeOctal { struct EscapeOctal {
c: char, c: char,
state: EscapeOctalState, state: EscapeOctalState,
idx: usize, idx: usize,
} }
#[derive(Clone, Debug)]
enum EscapeOctalState { enum EscapeOctalState {
Done, Done,
Backslash, Backslash,
@ -135,7 +131,6 @@ impl EscapedChar {
'\x0B' => Backslash('v'), '\x0B' => Backslash('v'),
'\x0C' => Backslash('f'), '\x0C' => Backslash('f'),
'\r' => Backslash('r'), '\r' => Backslash('r'),
'\\' => Backslash('\\'),
'\x00'..='\x1F' | '\x7F' => Octal(EscapeOctal::from(c)), '\x00'..='\x1F' | '\x7F' => Octal(EscapeOctal::from(c)),
'\'' => match quotes { '\'' => match quotes {
Quotes::Single => Backslash('\''), 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 // The first 16 control characters. NUL is also included, even though it is of
// no importance for file names. // no importance for file names.
check_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"),
],
);
}
} }

View file

@ -7,8 +7,6 @@
// spell-checker:ignore (ToDO) parsemode makedev sysmacros makenod newmode perror IFBLK IFCHR IFIFO // spell-checker:ignore (ToDO) parsemode makedev sysmacros makenod newmode perror IFBLK IFCHR IFIFO
mod parsemode;
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
@ -98,7 +96,7 @@ for details about the options it supports.",
let mut last_umask: mode_t = 0; let mut last_umask: mode_t = 0;
let mut newmode: mode_t = MODE_RW_UGO; let mut newmode: mode_t = MODE_RW_UGO;
if matches.opt_present("mode") { if matches.opt_present("mode") {
match parsemode::parse_mode(matches.opt_str("mode")) { match uucore::mode::parse_mode(matches.opt_str("mode")) {
Ok(parsed) => { Ok(parsed) => {
if parsed > 0o777 { if parsed > 0o777 {
show_info!("mode must specify only file permission bits"); show_info!("mode must specify only file permission bits");

View file

@ -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<String>) -> Result<mode_t, String> {
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: <https://github.com/microsoft/WSL/issues/4555> @@ <https://archive.is/dP0bz>
// 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);
}
}

View file

@ -18,18 +18,15 @@ pub fn err_msg(msg: &str) {
// by default stdout only flushes // by default stdout only flushes
// to console when a newline is passed. // to console when a newline is passed.
#[allow(unused_must_use)]
pub fn flush_char(c: char) { pub fn flush_char(c: char) {
print!("{}", c); print!("{}", c);
stdout().flush(); let _ = stdout().flush();
} }
#[allow(unused_must_use)]
pub fn flush_str(s: &str) { pub fn flush_str(s: &str) {
print!("{}", s); print!("{}", s);
stdout().flush(); let _ = stdout().flush();
} }
#[allow(unused_must_use)]
pub fn flush_bytes(bslice: &[u8]) { pub fn flush_bytes(bslice: &[u8]) {
stdout().write(bslice); let _ = stdout().write(bslice);
stdout().flush(); let _ = stdout().flush();
} }

View file

@ -1,5 +1,4 @@
#![allow(dead_code)] #![allow(dead_code)]
// spell-checker:ignore (change!) each's // spell-checker:ignore (change!) each's
// spell-checker:ignore (ToDO) LONGHELP FORMATSTRING templating parameterizing formatstr // spell-checker:ignore (ToDO) LONGHELP FORMATSTRING templating parameterizing formatstr
@ -9,7 +8,6 @@ mod tokenize;
static NAME: &str = "printf"; static NAME: &str = "printf";
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static SHORT_USAGE: &str = "printf: usage: printf [-v var] format [arguments]";
static LONGHELP_LEAD: &str = "printf static LONGHELP_LEAD: &str = "printf
USAGE: printf FORMATSTRING [ARGUMENT]... USAGE: printf FORMATSTRING [ARGUMENT]...

View file

@ -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<u8> = ret_rev.into_iter().rev().collect();
let ret: Vec<u8> = ret_rev.iter().rev().map(|x| *x).collect();
ret ret
} }
@ -102,70 +101,6 @@ pub fn arrnum_int_div_step(
remainder: rem_out, remainder: rem_out,
} }
} }
// pub struct ArrFloat {
// pub leading_zeros: u8,
// pub values: Vec<u8>,
// 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<u8>::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<u8> { pub fn arrnum_int_add(arrnum: &[u8], basenum: u8, base_ten_int_term: u8) -> Vec<u8> {
let mut carry: u16 = u16::from(base_ten_int_term); let mut carry: u16 = u16::from(base_ten_int_term);
let mut rem: u16; 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<u8> = ret_rev.into_iter().rev().collect();
let ret: Vec<u8> = ret_rev.iter().rev().map(|x| *x).collect();
ret ret
} }
@ -219,8 +153,7 @@ pub fn unsigned_to_arrnum(src: u16) -> Vec<u8> {
} }
// temporary needs-improvement-function // 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 // it would require a lot of addl code
// to implement this for arbitrary string input. // to implement this for arbitrary string input.
// until then, the below operates as an outline // 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 str_out
} }
#[allow(unused_variables)]
pub fn base_conv_str( pub fn base_conv_str(
src: &str, src: &str,
radix_def_src: &dyn RadixDef, radix_def_src: &dyn RadixDef,

View file

@ -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) // 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. // 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( fn get_primitive_hex(
inprefix: &InPrefix, inprefix: &InPrefix,
str_in: &str, _str_in: &str,
analysis: &FloatAnalysis, _analysis: &FloatAnalysis,
last_dec_place: usize, _last_dec_place: usize,
capitalized: bool, capitalized: bool,
) -> FormatPrimitive { ) -> FormatPrimitive {
let prefix = Some(String::from(if inprefix.sign == -1 { "-0x" } else { "0x" })); 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. // TODO actual conversion, make sure to get back mantissa.
// for hex to hex, it's really just a matter of moving the // for hex to hex, it's really just a matter of moving the
// decimal point and calculating the mantissa by its initial // decimal point and calculating the mantissa by its initial

View file

@ -22,12 +22,11 @@ fn get_len_fprim(fprim: &FormatPrimitive) -> usize {
len len
} }
pub struct Decf { pub struct Decf;
as_num: f64,
}
impl Decf { impl Decf {
pub fn new() -> Decf { pub fn new() -> Decf {
Decf { as_num: 0.0 } Decf
} }
} }
impl Formatter for Decf { impl Formatter for Decf {

View file

@ -5,12 +5,10 @@ use super::super::format_field::FormatField;
use super::super::formatter::{FormatPrimitive, Formatter, InPrefix}; use super::super::formatter::{FormatPrimitive, Formatter, InPrefix};
use super::float_common::{get_primitive_dec, primitive_to_str_common, FloatAnalysis}; use super::float_common::{get_primitive_dec, primitive_to_str_common, FloatAnalysis};
pub struct Floatf { pub struct Floatf;
as_num: f64,
}
impl Floatf { impl Floatf {
pub fn new() -> Floatf { pub fn new() -> Floatf {
Floatf { as_num: 0.0 } Floatf
} }
} }
impl Formatter for Floatf { impl Formatter for Floatf {

View file

@ -11,7 +11,7 @@ use std::i64;
use std::u64; use std::u64;
pub struct Intf { pub struct Intf {
a: u32, _a: u32,
} }
// see the Intf::analyze() function below // see the Intf::analyze() function below
@ -24,7 +24,7 @@ struct IntAnalysis {
impl Intf { impl Intf {
pub fn new() -> Intf { pub fn new() -> Intf {
Intf { a: 0 } Intf { _a: 0 }
} }
// take a ref to argument string, and basic information // take a ref to argument string, and basic information
// about prefix (offset, radix, sign), and analyze string // about prefix (offset, radix, sign), and analyze string

View file

@ -5,12 +5,11 @@ use super::super::format_field::FormatField;
use super::super::formatter::{FormatPrimitive, Formatter, InPrefix}; use super::super::formatter::{FormatPrimitive, Formatter, InPrefix};
use super::float_common::{get_primitive_dec, primitive_to_str_common, FloatAnalysis}; use super::float_common::{get_primitive_dec, primitive_to_str_common, FloatAnalysis};
pub struct Scif { pub struct Scif;
as_num: f64,
}
impl Scif { impl Scif {
pub fn new() -> Scif { pub fn new() -> Scif {
Scif { as_num: 0.0 } Scif
} }
} }
impl Formatter for Scif { impl Formatter for Scif {

View file

@ -242,18 +242,16 @@ impl UnescapedText {
} }
} }
} }
#[allow(unused_variables)]
impl token::Tokenizer for UnescapedText { impl token::Tokenizer for UnescapedText {
fn from_it( fn from_it(
it: &mut PutBackN<Chars>, it: &mut PutBackN<Chars>,
args: &mut Peekable<Iter<String>>, _: &mut Peekable<Iter<String>>,
) -> Option<Box<dyn token::Token>> { ) -> Option<Box<dyn token::Token>> {
UnescapedText::from_it_core(it, false) UnescapedText::from_it_core(it, false)
} }
} }
#[allow(unused_variables)]
impl token::Token for UnescapedText { impl token::Token for UnescapedText {
fn print(&self, pf_args_it: &mut Peekable<Iter<String>>) { fn print(&self, _: &mut Peekable<Iter<String>>) {
cli::flush_bytes(&self.0[..]); cli::flush_bytes(&self.0[..]);
} }
} }

View file

@ -23,7 +23,8 @@ clap = "2.33"
fnv = "1.0.7" fnv = "1.0.7"
itertools = "0.10.0" itertools = "0.10.0"
semver = "0.9.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 = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
tempdir = "0.3.7" tempdir = "0.3.7"

View file

@ -138,7 +138,15 @@ impl NumInfo {
sign: if had_digit { sign } else { Sign::Positive }, sign: if had_digit { sign } else { Sign::Positive },
exponent: 0, 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
},
) )
} }
} }

View file

@ -29,7 +29,6 @@ use rayon::prelude::*;
use semver::Version; use semver::Version;
use serde::{Deserializer, Deserialize, Serialize}; use serde::{Deserializer, Deserialize, Serialize};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::borrow::Cow;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::BinaryHeap; use std::collections::BinaryHeap;
use std::env; use std::env;
@ -38,9 +37,10 @@ use std::fs::File;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Lines, Read, Write}; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Lines, Read, Write};
use std::mem::replace; use std::mem::replace;
use std::ops::{Range, RangeInclusive}; use std::ops::Range;
use std::path::{Path, PathBuf}; use std::path::Path;
use uucore::fs::is_stdin_interactive; // for Iterator::dedup(); use unicode_width::UnicodeWidthStr;
use uucore::fs::is_stdin_interactive; // for Iterator::dedup()
static NAME: &str = "sort"; static NAME: &str = "sort";
static ABOUT: &str = "Display sorted concatenation of all FILE(s)."; 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_MERGE: &str = "merge";
static OPT_CHECK: &str = "check"; static OPT_CHECK: &str = "check";
static OPT_CHECK_SILENT: &str = "check-silent"; static OPT_CHECK_SILENT: &str = "check-silent";
static OPT_DEBUG: &str = "debug";
static OPT_IGNORE_CASE: &str = "ignore-case"; static OPT_IGNORE_CASE: &str = "ignore-case";
static OPT_IGNORE_BLANKS: &str = "ignore-blanks"; static OPT_IGNORE_BLANKS: &str = "ignore-blanks";
static OPT_IGNORE_NONPRINTING: &str = "ignore-nonprinting"; static OPT_IGNORE_NONPRINTING: &str = "ignore-nonprinting";
@ -94,7 +95,7 @@ static DEFAULT_TMPDIR: &str = r"/tmp";
// 16GB buffer for Vec<Line> before we dump to disk, never used // 16GB buffer for Vec<Line> before we dump to disk, never used
static DEFAULT_BUF_SIZE: usize = 16000000000; static DEFAULT_BUF_SIZE: usize = 16000000000;
#[derive(Eq, Ord, PartialEq, PartialOrd, Clone)] #[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy)]
enum SortMode { enum SortMode {
Numeric, Numeric,
HumanNumeric, HumanNumeric,
@ -106,6 +107,7 @@ enum SortMode {
#[derive(Clone)] #[derive(Clone)]
struct GlobalSettings { struct GlobalSettings {
mode: SortMode, mode: SortMode,
debug: bool,
ignore_blanks: bool, ignore_blanks: bool,
ignore_case: bool, ignore_case: bool,
dictionary_order: bool, dictionary_order: bool,
@ -154,6 +156,7 @@ impl Default for GlobalSettings {
fn default() -> GlobalSettings { fn default() -> GlobalSettings {
GlobalSettings { GlobalSettings {
mode: SortMode::Default, mode: SortMode::Default,
debug: false,
ignore_blanks: false, ignore_blanks: false,
ignore_case: false, ignore_case: false,
dictionary_order: false, dictionary_order: false,
@ -190,7 +193,7 @@ struct KeySettings {
impl From<&GlobalSettings> for KeySettings { impl From<&GlobalSettings> for KeySettings {
fn from(settings: &GlobalSettings) -> Self { fn from(settings: &GlobalSettings) -> Self {
Self { Self {
mode: settings.mode.clone(), mode: settings.mode,
ignore_blanks: settings.ignore_blanks, ignore_blanks: settings.ignore_blanks,
ignore_case: settings.ignore_case, ignore_case: settings.ignore_case,
ignore_non_printing: settings.ignore_non_printing, ignore_non_printing: settings.ignore_non_printing,
@ -236,7 +239,7 @@ impl SelectionRange {
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
enum NumCache { enum NumCache {
#[serde(deserialize_with="bailout_parse_f64")] #[serde(deserialize_with="bailout_parse_f64")]
AsF64(f64), AsF64(GeneralF64ParseResult),
WithInfo(NumInfo), WithInfo(NumInfo),
None, None,
} }
@ -250,7 +253,7 @@ fn bailout_parse_f64<'de, D>(d: D) -> Result<f64, D::Error> where D: Deserialize
} }
impl NumCache { impl NumCache {
fn as_f64(&self) -> f64 { fn as_f64(&self) -> GeneralF64ParseResult {
match self { match self {
NumCache::AsF64(n) => *n, NumCache::AsF64(n) => *n,
_ => unreachable!(), _ => unreachable!(),
@ -309,18 +312,13 @@ impl Line {
.selectors .selectors
.iter() .iter()
.map(|selector| { .map(|selector| {
let mut range = let range = selector.get_selection(&line, fields.as_deref());
if let Some(range) = selector.get_field_selection(&line, fields.as_deref()) { let mut range = if let Some(transformed) =
if let Some(transformed) =
transform(&line[range.to_owned()], &selector.settings) transform(&line[range.to_owned()], &selector.settings)
{ {
SelectionRange::String(transformed) SelectionRange::String(transformed)
} else { } else {
SelectionRange::ByIndex(range.start().to_owned()..range.end() + 1) SelectionRange::ByIndex(range)
}
} else {
// If there is no match, match the empty string.
SelectionRange::ByIndex(0..0)
}; };
let num_cache = if selector.settings.mode == SortMode::Numeric let num_cache = if selector.settings.mode == SortMode::Numeric
|| selector.settings.mode == SortMode::HumanNumeric || selector.settings.mode == SortMode::HumanNumeric
@ -336,7 +334,8 @@ impl Line {
range.shorten(num_range); range.shorten(num_range);
NumCache::WithInfo(info) NumCache::WithInfo(info)
} else if selector.settings.mode == SortMode::GeneralNumeric && settings.buffer_size == DEFAULT_BUF_SIZE { } 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 { } else {
NumCache::None NumCache::None
}; };
@ -345,6 +344,129 @@ impl Line {
.collect(); .collect();
Self { line, selections } 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. /// Transform this line. Returns None if there's no need to transform.
@ -407,20 +529,18 @@ fn tokenize_default(line: &str) -> Vec<Field> {
/// Split between separators. These separators are not included in fields. /// Split between separators. These separators are not included in fields.
fn tokenize_with_separator(line: &str, separator: char) -> Vec<Field> { fn tokenize_with_separator(line: &str, separator: char) -> Vec<Field> {
let mut tokens = vec![0..0]; let mut tokens = vec![];
let mut previous_was_separator = false; let separator_indices =
for (idx, char) in line.char_indices() { line.char_indices()
if previous_was_separator { .filter_map(|(i, c)| if c == separator { Some(i) } else { None });
tokens.push(idx..0); let mut start = 0;
for sep_idx in separator_indices {
tokens.push(start..sep_idx);
start = sep_idx + 1;
} }
if char == separator { if start < line.len() {
tokens.last_mut().unwrap().end = idx; tokens.push(start..line.len());
previous_was_separator = true;
} else {
previous_was_separator = false;
} }
}
tokens.last_mut().unwrap().end = line.len();
tokens tokens
} }
@ -464,6 +584,28 @@ impl KeyPosition {
crash!(1, "invalid option for key: `{}`", c) 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 // Strip away option characters from the original value so we can parse it later
*value_with_options = &value_with_options[..options_start]; *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. /// Look up the slice that corresponds to this selector for the given line.
/// If needs_fields returned false, fields may be None. /// If needs_fields returned false, fields may be None.
fn get_field_selection<'a>( fn get_selection<'a>(&self, line: &'a str, tokens: Option<&[Field]>) -> Range<usize> {
&self, enum Resolution {
line: &'a str, // The start index of the resolved character, inclusive
tokens: Option<&[Field]>, StartOfChar(usize),
) -> Option<RangeInclusive<usize>> { // The end index of the resolved character, exclusive.
enum ResolutionErr { // This is only returned if the character index is 0.
EndOfChar(usize),
// The resolved character would be in front of the first character
TooLow, TooLow,
// The resolved character would be after the last character
TooHigh, TooHigh,
} }
@ -521,15 +666,15 @@ impl FieldSelector {
line: &str, line: &str,
tokens: Option<&[Field]>, tokens: Option<&[Field]>,
position: &KeyPosition, position: &KeyPosition,
) -> Result<usize, ResolutionErr> { ) -> Resolution {
if tokens.map_or(false, |fields| fields.len() < position.field) { if tokens.map_or(false, |fields| fields.len() < position.field) {
Err(ResolutionErr::TooHigh) Resolution::TooHigh
} else if position.char == 0 { } else if position.char == 0 {
let end = tokens.unwrap()[position.field - 1].end; let end = tokens.unwrap()[position.field - 1].end;
if end == 0 { if end == 0 {
Err(ResolutionErr::TooLow) Resolution::TooLow
} else { } else {
Ok(end - 1) Resolution::EndOfChar(end)
} }
} else { } else {
let mut idx = if position.field == 1 { let mut idx = if position.field == 1 {
@ -538,38 +683,52 @@ impl FieldSelector {
0 0
} else { } else {
tokens.unwrap()[position.field - 1].start 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() { if idx >= line.len() {
Err(ResolutionErr::TooHigh) Resolution::TooHigh
} else { } else {
if position.ignore_blanks { if position.ignore_blanks {
if let Some(not_whitespace) = if let Some((not_whitespace, _)) =
line[idx..].chars().position(|c| !c.is_whitespace()) line[idx..].char_indices().find(|(_, c)| !c.is_whitespace())
{ {
idx += not_whitespace; idx += not_whitespace;
} else { } else {
return Err(ResolutionErr::TooHigh); return Resolution::TooHigh;
} }
} }
Ok(idx) Resolution::StartOfChar(idx)
} }
} }
} }
if let Ok(from) = resolve_index(line, tokens, &self.from) { match resolve_index(line, tokens, &self.from) {
Resolution::StartOfChar(from) => {
let to = self.to.as_ref().map(|to| resolve_index(line, tokens, &to)); let to = self.to.as_ref().map(|to| resolve_index(line, tokens, &to));
match to { match to {
Some(Ok(to)) => Some(from..=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, // If `to` was not given or the match would be after the end of the line,
// match everything until the end of the line. // match everything until the end of the line.
None | Some(Err(ResolutionErr::TooHigh)) => Some(from..=line.len() - 1), None | Some(Resolution::TooHigh) => from..line.len(),
// If `to` is before the start of the line, report no match. // If `to` is before the start of the line, report no match.
// This can happen if the line starts with a separator. // This can happen if the line starts with a separator.
Some(Err(ResolutionErr::TooLow)) => None, 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> { impl<'a> PartialEq for MergeableFile<'a> {
fn eq(&self, other: &MergeableFile) -> bool { 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> { impl<'a> Iterator for FileMerger<'a> {
type Item = String; type Item = Line;
fn next(&mut self) -> Option<String> { fn next(&mut self) -> Option<Line> {
match self.heap.pop() { match self.heap.pop() {
Some(mut current) => { Some(mut current) => {
match current.lines.next() { match current.lines.next() {
@ -639,12 +798,12 @@ impl<'a> Iterator for FileMerger<'a> {
Line::new(next_line, &self.settings), Line::new(next_line, &self.settings),
); );
self.heap.push(current); self.heap.push(current);
Some(ret.line) Some(ret)
} }
_ => { _ => {
// Don't put it back in the heap (it's empty/erroring) // Don't put it back in the heap (it's empty/erroring)
// but its first line is still valid. // 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) Arg::with_name(OPT_DICTIONARY_ORDER)
.short("d") .short("d")
.long(OPT_DICTIONARY_ORDER) .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(
Arg::with_name(OPT_MERGE) Arg::with_name(OPT_MERGE)
@ -736,9 +896,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
) )
.arg( .arg(
Arg::with_name(OPT_IGNORE_NONPRINTING) Arg::with_name(OPT_IGNORE_NONPRINTING)
.short("-i") .short("i")
.long(OPT_IGNORE_NONPRINTING) .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(
Arg::with_name(OPT_IGNORE_BLANKS) Arg::with_name(OPT_IGNORE_BLANKS)
@ -829,9 +990,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.value_name("NUL_FILES") .value_name("NUL_FILES")
.multiple(true), .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)) .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true))
.get_matches_from(args); .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 // check whether user specified a zero terminated list of files for input, otherwise read files from args
let mut files: Vec<String> = if matches.is_present(OPT_FILES0_FROM) { let mut files: Vec<String> = if matches.is_present(OPT_FILES0_FROM) {
let files0_from: Vec<String> = matches let files0_from: Vec<String> = matches
@ -964,6 +1132,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
1, 1,
&mut key_settings, &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 let to = from_to
.next() .next()
.map(|to| KeyPosition::parse(to, 0, &mut key_settings)); .map(|to| KeyPosition::parse(to, 0, &mut key_settings));
@ -1042,7 +1217,10 @@ fn exec(files: Vec<String>, settings: GlobalSettings) -> i32 {
if settings.merge { if settings.merge {
if settings.unique { 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 { } else {
print_sorted(file_merger, &settings) print_sorted(file_merger, &settings)
} }
@ -1050,12 +1228,11 @@ fn exec(files: Vec<String>, settings: GlobalSettings) -> i32 {
print_sorted( print_sorted(
lines lines
.into_iter() .into_iter()
.dedup_by(|a, b| compare_by(a, b, &settings) == Ordering::Equal) .dedup_by(|a, b| compare_by(a, b, settings) == Ordering::Equal),
.map(|line| line.line),
&settings, &settings,
) )
} else { } else {
print_sorted(lines.into_iter().map(|line| line.line), &settings) print_sorted(lines.into_iter(), &settings)
} }
0 0
@ -1163,107 +1340,80 @@ fn default_compare(a: &str, b: &str) -> Ordering {
a.cmp(b) a.cmp(b)
} }
/// This function does the initial detection of numeric lines for FP compares. // This function cleans up the initial comparison done by leading_num_common for a general numeric compare.
// 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.
// In contrast to numeric compare, GNU general numeric/FP sort *should* recognize positive signs and // 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. // 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. // For example, 5e10KFD would be 5e10 or 5x10^10 and +10000HFKJFK would become 10000.
fn get_leading_gen(a: &str) -> &str { fn get_leading_gen(input: &str) -> Range<usize> {
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());
}
}
// Make this iter peekable to see if next char is numeric // Make this iter peekable to see if next char is numeric
let raw_leading_num = leading_num_common(a); let mut char_indices = trimmed.char_indices().peekable();
let mut p_iter = raw_leading_num.chars().peekable();
let mut result = ""; let first = char_indices.peek();
// Cleanup raw stripped strings
while let Some(c) = p_iter.next() { if first.map_or(false, |&(_, c)| c == NEGATIVE || c == POSITIVE) {
let next_char_numeric = p_iter.peek().unwrap_or(&'\0').is_numeric(); char_indices.next();
// 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;
}
// If no further processing needed to be done, return the line as-is to be sorted
result = a;
}
result
} }
#[inline(always)] let mut had_e_notation = false;
fn remove_trailing_dec<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> { let mut had_decimal_pt = false;
let input = input.into(); while let Some((idx, c)) = char_indices.next() {
if let Some(s) = input.find(DECIMAL_PT) { if c.is_ascii_digit() {
let (leading, trailing) = input.split_at(s); continue;
let output = [leading, ".", trailing.replace(DECIMAL_PT, "").as_str()].concat();
Cow::Owned(output)
} else {
input
} }
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()
}
#[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. /// Parse the beginning string into an f64, returning -inf instead of NaN on errors.
#[inline(always)] #[inline(always)]
fn permissive_f64_parse(a: &str) -> f64 { fn general_f64_parse(a: &str) -> GeneralF64ParseResult {
// GNU sort treats "NaN" as non-number in numeric, so it needs special care. // The actual behavior here relies on Rust's implementation of parsing floating points.
// *Keep this trim before parse* despite what POSIX may say about -b and -n // For example "nan", "inf" (ignoring the case) and "infinity" are only parsed to floats starting from 1.53.
// because GNU and BSD both seem to require it to match their behavior // TODO: Once our minimum supported Rust version is 1.53 or above, we should add tests for those cases.
// match a.parse::<f64>() {
// Remove any trailing decimals, ie 4568..890... becomes 4568.890 Ok(a) if a.is_nan() => GeneralF64ParseResult::NaN,
// Then, we trim whitespace and parse Ok(a) if a == std::f64::NEG_INFINITY => GeneralF64ParseResult::NegInfinity,
match remove_trailing_dec(a).trim().parse::<f64>() { Ok(a) if a == std::f64::INFINITY => GeneralF64ParseResult::Infinity,
Ok(val) if val.is_nan() => std::f64::NEG_INFINITY, Ok(a) => GeneralF64ParseResult::Number(a),
Ok(val) => val, Err(_) => GeneralF64ParseResult::Invalid,
Err(_) => std::f64::NEG_INFINITY,
} }
} }
/// Compares two floats, with errors and non-numerics assumed to be -inf. /// Compares two floats, with errors and non-numerics assumed to be -inf.
/// Stops coercing at the first non-numeric char. /// Stops coercing at the first non-numeric char.
/// We explicitly need to convert to f64 in this case. /// We explicitly need to convert to f64 in this case.
fn general_numeric_compare(a: f64, b: f64) -> Ordering { fn general_numeric_compare(a: GeneralF64ParseResult, b: GeneralF64ParseResult) -> Ordering {
#![allow(clippy::comparison_chain)] a.partial_cmp(&b).unwrap()
// 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 get_rand_string() -> String { fn get_rand_string() -> String {
@ -1290,7 +1440,7 @@ fn random_shuffle(a: &str, b: &str, x: String) -> Ordering {
da.cmp(&db) da.cmp(&db)
} }
#[derive(Eq, Ord, PartialEq, PartialOrd)] #[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy)]
enum Month { enum Month {
Unknown, Unknown,
January, January,
@ -1309,30 +1459,32 @@ enum Month {
/// Parse the beginning string into a Month, returning Month::Unknown on errors. /// Parse the beginning string into a Month, returning Month::Unknown on errors.
fn month_parse(line: &str) -> Month { fn month_parse(line: &str) -> Month {
// GNU splits at any 3 letter match "JUNNNN" is JUN let line = line.trim();
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 result = match pattern.to_uppercase().as_ref() { const MONTHS: [(&str, Month); 12] = [
"JAN" => Month::January, ("JAN", Month::January),
"FEB" => Month::February, ("FEB", Month::February),
"MAR" => Month::March, ("MAR", Month::March),
"APR" => Month::April, ("APR", Month::April),
"MAY" => Month::May, ("MAY", Month::May),
"JUN" => Month::June, ("JUN", Month::June),
"JUL" => Month::July, ("JUL", Month::July),
"AUG" => Month::August, ("AUG", Month::August),
"SEP" => Month::September, ("SEP", Month::September),
"OCT" => Month::October, ("OCT", Month::October),
"NOV" => Month::November, ("NOV", Month::November),
"DEC" => Month::December, ("DEC", Month::December),
_ => Month::Unknown, ];
};
result 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 { fn month_compare(a: &str, b: &str) -> Ordering {
@ -1390,7 +1542,7 @@ fn remove_nonprinting_chars(s: &str) -> String {
.collect::<String>() .collect::<String>()
} }
fn print_sorted<T: Iterator<Item = String>>(iter: T, settings: &GlobalSettings) { fn print_sorted<T: Iterator<Item = Line>>(iter: T, settings: &GlobalSettings) {
let mut file: Box<dyn Write> = match settings.outfile { let mut file: Box<dyn Write> = match settings.outfile {
Some(ref filename) => match File::create(Path::new(&filename)) { Some(ref filename) => match File::create(Path::new(&filename)) {
Ok(f) => Box::new(BufWriter::new(f)) as Box<dyn Write>, Ok(f) => Box::new(BufWriter::new(f)) as Box<dyn Write>,
@ -1401,15 +1553,19 @@ fn print_sorted<T: Iterator<Item = String>>(iter: T, settings: &GlobalSettings)
}, },
None => Box::new(BufWriter::new(stdout())) as Box<dyn Write>, None => Box::new(BufWriter::new(stdout())) as Box<dyn Write>,
}; };
if settings.zero_terminated { if settings.zero_terminated && !settings.debug {
for line in iter { 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())); crash_if_err!(1, file.write_all("\0".as_bytes()));
} }
} else { } else {
for line in iter { for line in iter {
crash_if_err!(1, file.write_all(line.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())); 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()); crash_if_err!(1, file.flush());
@ -1504,4 +1660,14 @@ mod tests {
vec![0..0, 1..1, 2..2, 3..9, 10..18,] 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]);
}
} }

View file

@ -91,10 +91,15 @@ fn tac(filenames: Vec<String>, before: bool, _: bool, separator: &str) -> i32 {
} else { } else {
let path = Path::new(filename); let path = Path::new(filename);
if path.is_dir() || path.metadata().is_err() { if path.is_dir() || path.metadata().is_err() {
if path.is_dir() {
show_error!("dir: read error: Invalid argument");
} else {
show_error!( show_error!(
"failed to open '{}' for reading: No such file or directory", "failed to open '{}' for reading: No such file or directory",
filename filename
); );
}
exit_code = 1;
continue; continue;
} }
match File::open(path) { match File::open(path) {

View file

@ -117,6 +117,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.arg( .arg(
Arg::with_name(options::SLEEP_INT) Arg::with_name(options::SLEEP_INT)
.short("s") .short("s")
.takes_value(true)
.long(options::SLEEP_INT) .long(options::SLEEP_INT)
.help("Number or seconds to sleep between polling the file when running with -f"), .help("Number or seconds to sleep between polling the file when running with -f"),
) )

View file

@ -192,7 +192,8 @@ fn truncate(
} }
fn parse_size(size: &str) -> (u64, TruncateMode) { 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::Extend,
'-' => TruncateMode::Reduce, '-' => TruncateMode::Reduce,
'<' => TruncateMode::AtMost, '<' => TruncateMode::AtMost,
@ -203,9 +204,9 @@ fn parse_size(size: &str) -> (u64, TruncateMode) {
}; };
let bytes = { let bytes = {
let mut slice = if mode == TruncateMode::Reference { let mut slice = if mode == TruncateMode::Reference {
size &clean_size
} else { } else {
&size[1..] &clean_size[1..]
}; };
if slice.chars().last().unwrap().is_alphabetic() { if slice.chars().last().unwrap().is_alphabetic() {
slice = &slice[..slice.len() - 1]; slice = &slice[..slice.len() - 1];
@ -220,11 +221,11 @@ fn parse_size(size: &str) -> (u64, TruncateMode) {
Ok(num) => num, Ok(num) => num,
Err(e) => crash!(1, "'{}' is not a valid number: {}", size, e), Err(e) => crash!(1, "'{}' is not a valid number: {}", size, e),
}; };
if size.chars().last().unwrap().is_alphabetic() { if clean_size.chars().last().unwrap().is_alphabetic() {
number *= match size.chars().last().unwrap().to_ascii_uppercase() { number *= match clean_size.chars().last().unwrap().to_ascii_uppercase() {
'B' => match size 'B' => match clean_size
.chars() .chars()
.nth(size.len() - 2) .nth(clean_size.len() - 2)
.unwrap() .unwrap()
.to_ascii_uppercase() .to_ascii_uppercase()
{ {

View file

@ -61,34 +61,43 @@ impl Uniq {
reader: &mut BufReader<R>, reader: &mut BufReader<R>,
writer: &mut BufWriter<W>, writer: &mut BufWriter<W>,
) { ) {
let mut lines: Vec<String> = vec![];
let mut first_line_printed = false; let mut first_line_printed = false;
let delimiters = self.delimiters; let mut group_count = 1;
let line_terminator = self.get_line_terminator(); let line_terminator = self.get_line_terminator();
// Don't print any delimiting lines before, after or between groups if delimiting method is 'none' let mut lines = reader.split(line_terminator).map(get_line_string);
let no_delimiters = delimiters == Delimiters::None; let mut line = match lines.next() {
// The 'prepend' and 'both' delimit methods will cause output to start with delimiter line Some(l) => l,
let prepend_delimiter = delimiters == Delimiters::Prepend || delimiters == Delimiters::Both; None => return,
// The 'append' and 'both' delimit methods will cause output to end with delimiter line };
let append_delimiter = delimiters == Delimiters::Append || delimiters == Delimiters::Both;
for line in reader.split(line_terminator).map(get_line_string) { // compare current `line` with consecutive lines (`next_line`) of the input
if !lines.is_empty() && self.cmp_keys(&lines[0], &line) { // and if needed, print `line` based on the command line options provided
// Print delimiter if delimit method is not 'none' and any line has been output for next_line in lines {
// before or if we need to start output with delimiter if self.cmp_keys(&line, &next_line) {
let print_delimiter = !no_delimiters && (prepend_delimiter || first_line_printed); if (group_count == 1 && !self.repeats_only)
first_line_printed |= self.print_lines(writer, &lines, print_delimiter); || (group_count > 1 && !self.uniques_only)
lines.truncate(0); {
self.print_line(writer, &line, group_count, first_line_printed);
first_line_printed = true;
} }
lines.push(line); 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;
} }
if !lines.is_empty() { group_count += 1;
// 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 append_delimiter && first_line_printed { }
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 (self.delimiters == Delimiters::Append || self.delimiters == Delimiters::Both)
&& first_line_printed
{
crash_if_err!(1, writer.write_all(&[line_terminator])); crash_if_err!(1, writer.write_all(&[line_terminator]));
} }
} }
@ -163,27 +172,17 @@ impl Uniq {
} }
} }
fn print_lines<W: Write>( fn should_print_delimiter(&self, group_count: usize, first_line_printed: bool) -> bool {
&self, // if no delimiter option is selected then no other checks needed
writer: &mut BufWriter<W>, self.delimiters != Delimiters::None
lines: &[String], // print delimiter only before the first line of a group, not between lines of a group
print_delimiter: bool, && group_count == 1
) -> bool { // if at least one line has been output before current group then print delimiter
let mut first_line_printed = false; && (first_line_printed
let mut count = if self.all_repeated { 1 } else { lines.len() }; // or if we need to prepend delimiter then print it even at the start of the output
if lines.len() == 1 && !self.repeats_only || lines.len() > 1 && !self.uniques_only { || self.delimiters == Delimiters::Prepend
self.print_line(writer, &lines[0], count, print_delimiter); // the 'both' delimit mode should prepend and append delimiters
first_line_printed = true; || self.delimiters == Delimiters::Both)
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 print_line<W: Write>( fn print_line<W: Write>(
@ -191,11 +190,11 @@ impl Uniq {
writer: &mut BufWriter<W>, writer: &mut BufWriter<W>,
line: &str, line: &str,
count: usize, count: usize,
print_delimiter: bool, first_line_printed: bool,
) { ) {
let line_terminator = self.get_line_terminator(); 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])); crash_if_err!(1, writer.write_all(&[line_terminator]));
} }

View file

@ -7,6 +7,8 @@
// spell-checker:ignore (vars) fperm srwx // 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<u32, String> { pub fn parse_numeric(fperm: u32, mut mode: &str) -> Result<u32, String> {
let (op, pos) = parse_op(mode, Some('='))?; let (op, pos) = parse_op(mode, Some('='))?;
mode = mode[pos..].trim().trim_start_matches('0'); 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) (srwx, pos)
} }
pub fn parse_mode(mode: Option<String>) -> Result<mode_t, String> {
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);
}
}

View file

@ -26,6 +26,7 @@ mod mods; // core cross-platform modules
// * cross-platform modules // * cross-platform modules
pub use crate::mods::coreopts; pub use crate::mods::coreopts;
pub use crate::mods::os;
pub use crate::mods::panic; pub use crate::mods::panic;
pub use crate::mods::ranges; pub use crate::mods::ranges;

View file

@ -1,5 +1,6 @@
// mods ~ cross-platforms modules (core/bundler file) // mods ~ cross-platforms modules (core/bundler file)
pub mod coreopts; pub mod coreopts;
pub mod os;
pub mod panic; pub mod panic;
pub mod ranges; pub mod ranges;

View file

@ -0,0 +1,30 @@
/// Test if the program is running under WSL
// ref: <https://github.com/microsoft/WSL/issues/4555> @@ <https://archive.is/dP0bz>
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
}

View file

@ -1,4 +1,7 @@
use crate::common::util::*; use crate::common::util::*;
#[cfg(unix)]
use std::fs::OpenOptions;
#[cfg(unix)]
use std::io::Read; use std::io::Read;
#[test] #[test]
@ -26,7 +29,7 @@ fn test_no_options() {
} }
#[test] #[test]
#[cfg(unix)] #[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
fn test_no_options_big_input() { fn test_no_options_big_input() {
for &n in &[ for &n in &[
0, 0,
@ -54,7 +57,6 @@ fn test_no_options_big_input() {
#[test] #[test]
#[cfg(unix)] #[cfg(unix)]
fn test_fifo_symlink() { fn test_fifo_symlink() {
use std::fs::OpenOptions;
use std::io::Write; use std::io::Write;
use std::thread; use std::thread;
@ -85,6 +87,74 @@ fn test_fifo_symlink() {
thread.join().unwrap(); 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] #[test]
fn test_directory() { fn test_directory() {
let s = TestScenario::new(util_name!()); let s = TestScenario::new(util_name!());
@ -327,6 +397,7 @@ fn test_dev_full_show_all() {
#[cfg(unix)] #[cfg(unix)]
fn test_domain_socket() { fn test_domain_socket() {
use std::io::prelude::*; use std::io::prelude::*;
use std::sync::{Arc, Barrier};
use std::thread; use std::thread;
use tempdir::TempDir; use tempdir::TempDir;
use unix_socket::UnixListener; use unix_socket::UnixListener;
@ -335,17 +406,23 @@ fn test_domain_socket() {
let socket_path = dir.path().join("sock"); let socket_path = dir.path().join("sock");
let listener = UnixListener::bind(&socket_path).expect("failed to create socket"); 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 thread = thread::spawn(move || {
let mut stream = listener.accept().expect("failed to accept connection").0; let mut stream = listener.accept().expect("failed to accept connection").0;
barrier2.wait();
stream stream
.write_all(b"a\tb") .write_all(b"a\tb")
.expect("failed to write test data"); .expect("failed to write test data");
}); });
new_ucmd!() let child = new_ucmd!().args(&[socket_path]).run_no_wait();
.args(&[socket_path]) barrier.wait();
.succeeds() let stdout = &child.wait_with_output().unwrap().stdout.clone();
.stdout_only("a\tb"); let output = String::from_utf8_lossy(&stdout);
assert_eq!("a\tb", output);
thread.join().unwrap(); thread.join().unwrap();
} }

View file

@ -104,7 +104,7 @@ fn test_reference() {
// skip for root or MS-WSL // skip for root or MS-WSL
// * MS-WSL is bugged (as of 2019-12-25), allowing non-root accounts su-level privileges for `chgrp` // * 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' // * 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!() new_ucmd!()
.arg("-v") .arg("-v")
.arg("--reference=/etc/passwd") .arg("--reference=/etc/passwd")

View file

@ -47,7 +47,7 @@ fn run_single_test(test: &TestCase, at: AtPath, mut ucmd: UCommand) {
ucmd.arg(arg); ucmd.arg(arg);
} }
let r = ucmd.run(); let r = ucmd.run();
if !r.success { if !r.succeeded() {
println!("{}", r.stderr_str()); println!("{}", r.stderr_str());
panic!("{:?}: failed", ucmd.raw); panic!("{:?}: failed", ucmd.raw);
} }

View file

@ -4,14 +4,11 @@ use crate::common::util::*;
fn test_missing_operand() { fn test_missing_operand() {
let result = new_ucmd!().run(); let result = new_ucmd!().run();
assert_eq!( assert!(result
true, .stderr_str()
result .starts_with("error: The following required arguments were not provided"));
.stderr
.starts_with("error: The following required arguments were not provided")
);
assert_eq!(true, result.stderr.contains("<newroot>")); assert!(result.stderr_str().contains("<newroot>"));
} }
#[test] #[test]
@ -20,14 +17,11 @@ fn test_enter_chroot_fails() {
at.mkdir("jail"); at.mkdir("jail");
let result = ucmd.arg("jail").run(); let result = ucmd.arg("jail").fails();
assert_eq!( assert!(result
true, .stderr_str()
result.stderr.starts_with( .starts_with("chroot: error: cannot chroot to jail: Operation not permitted (os error 1)"));
"chroot: error: cannot chroot to jail: Operation not permitted (os error 1)"
)
)
} }
#[test] #[test]
@ -47,19 +41,18 @@ fn test_invalid_user_spec() {
at.mkdir("a"); at.mkdir("a");
let result = ucmd.arg("a").arg("--userspec=ARABA:").run(); let result = ucmd.arg("a").arg("--userspec=ARABA:").fails();
assert_eq!( assert!(result
true, .stderr_str()
result.stderr.starts_with("chroot: error: invalid userspec") .starts_with("chroot: error: invalid userspec"));
);
} }
#[test] #[test]
fn test_preference_of_userspec() { fn test_preference_of_userspec() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let result = scene.cmd("whoami").run(); 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. // In the CI, some server are failing to return whoami.
// As seems to be a configuration issue, ignoring it // As seems to be a configuration issue, ignoring it
return; return;
@ -73,7 +66,7 @@ fn test_preference_of_userspec() {
println!("result.stdout = {}", result.stdout_str()); println!("result.stdout = {}", result.stdout_str());
println!("result.stderr = {}", result.stderr_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. // In the CI, some server are failing to return id.
// As seems to be a configuration issue, ignoring it // As seems to be a configuration issue, ignoring it
return; return;

View file

@ -42,13 +42,9 @@ static TEST_MOUNT_OTHER_FILESYSTEM_FILE: &str = "mount/DO_NOT_copy_me.txt";
fn test_cp_cp() { fn test_cp_cp() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
// Invoke our binary to make the copy. // Invoke our binary to make the copy.
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_DEST) .arg(TEST_HELLO_WORLD_DEST)
.run(); .succeeds();
// Check that the exit code represents a successful copy.
assert!(result.success);
// Check the content of the destination file that was copied. // Check the content of the destination file that was copied.
assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n");
@ -57,12 +53,9 @@ fn test_cp_cp() {
#[test] #[test]
fn test_cp_existing_target() { fn test_cp_existing_target() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_EXISTING_FILE) .arg(TEST_EXISTING_FILE)
.run(); .succeeds();
assert!(result.success);
// Check the content of the destination file // Check the content of the destination file
assert_eq!(at.read(TEST_EXISTING_FILE), "Hello, World!\n"); assert_eq!(at.read(TEST_EXISTING_FILE), "Hello, World!\n");
@ -74,52 +67,41 @@ fn test_cp_existing_target() {
#[test] #[test]
fn test_cp_duplicate_files() { fn test_cp_duplicate_files() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_COPY_TO_FOLDER) .arg(TEST_COPY_TO_FOLDER)
.run(); .succeeds()
.stderr_contains("specified more than once");
assert!(result.success);
assert!(result.stderr.contains("specified more than once"));
assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n");
} }
#[test] #[test]
fn test_cp_multiple_files_target_is_file() { fn test_cp_multiple_files_target_is_file() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!()
let result = ucmd
.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_EXISTING_FILE) .arg(TEST_EXISTING_FILE)
.run(); .fails()
.stderr_contains("not a directory");
assert!(!result.success);
assert!(result.stderr.contains("not a directory"));
} }
#[test] #[test]
fn test_cp_directory_not_recursive() { fn test_cp_directory_not_recursive() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!()
let result = ucmd
.arg(TEST_COPY_TO_FOLDER) .arg(TEST_COPY_TO_FOLDER)
.arg(TEST_HELLO_WORLD_DEST) .arg(TEST_HELLO_WORLD_DEST)
.run(); .fails()
.stderr_contains("omitting directory");
assert!(!result.success);
assert!(result.stderr.contains("omitting directory"));
} }
#[test] #[test]
fn test_cp_multiple_files() { fn test_cp_multiple_files() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE) .arg(TEST_HOW_ARE_YOU_SOURCE)
.arg(TEST_COPY_TO_FOLDER) .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_COPY_TO_FOLDER_FILE), "Hello, World!\n");
assert_eq!(at.read(TEST_HOW_ARE_YOU_DEST), "How are you?\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))] #[cfg(not(macos))]
fn test_cp_recurse() { fn test_cp_recurse() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg("-r")
let result = ucmd
.arg("-r")
.arg(TEST_COPY_FROM_FOLDER) .arg(TEST_COPY_FROM_FOLDER)
.arg(TEST_COPY_TO_FOLDER_NEW) .arg(TEST_COPY_TO_FOLDER_NEW)
.run(); .succeeds();
assert!(result.success);
// Check the content of the destination file that was copied. // Check the content of the destination file that was copied.
assert_eq!(at.read(TEST_COPY_TO_FOLDER_NEW_FILE), "Hello, World!\n"); assert_eq!(at.read(TEST_COPY_TO_FOLDER_NEW_FILE), "Hello, World!\n");
} }
@ -144,14 +123,10 @@ fn test_cp_recurse() {
#[test] #[test]
fn test_cp_with_dirs_t() { fn test_cp_with_dirs_t() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg("-t")
//using -t option
let result_to_dir_t = ucmd
.arg("-t")
.arg(TEST_COPY_TO_FOLDER) .arg(TEST_COPY_TO_FOLDER)
.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE)
.run(); .succeeds();
assert!(result_to_dir_t.success);
assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); 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 scene = TestScenario::new(util_name!());
let at = &scene.fixtures; let at = &scene.fixtures;
//using -t option scene
let result_to_dir = scene
.ucmd() .ucmd()
.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_COPY_TO_FOLDER) .arg(TEST_COPY_TO_FOLDER)
.run(); .succeeds();
assert!(result_to_dir.success);
assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n");
let result_from_dir = scene scene
.ucmd() .ucmd()
.arg(TEST_COPY_FROM_FOLDER_FILE) .arg(TEST_COPY_FROM_FOLDER_FILE)
.arg(TEST_HELLO_WORLD_DEST) .arg(TEST_HELLO_WORLD_DEST)
.run(); .succeeds();
assert!(result_from_dir.success);
assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n");
} }
#[test] #[test]
fn test_cp_arg_target_directory() { fn test_cp_arg_target_directory() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("-t") .arg("-t")
.arg(TEST_COPY_TO_FOLDER) .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_COPY_TO_FOLDER_FILE), "Hello, World!\n");
} }
#[test] #[test]
fn test_cp_arg_no_target_directory() { fn test_cp_arg_no_target_directory() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!()
let result = ucmd
.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE)
.arg("-v") .arg("-v")
.arg("-T") .arg("-T")
.arg(TEST_COPY_TO_FOLDER) .arg(TEST_COPY_TO_FOLDER)
.run(); .fails()
.stderr_contains("cannot overwrite directory");
assert!(!result.success);
assert!(result.stderr.contains("cannot overwrite directory"));
} }
#[test] #[test]
fn test_cp_arg_interactive() { fn test_cp_arg_interactive() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!()
let result = ucmd
.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE) .arg(TEST_HOW_ARE_YOU_SOURCE)
.arg("-i") .arg("-i")
.pipe_in("N\n") .pipe_in("N\n")
.run(); .succeeds()
.stderr_contains("Not overwriting");
assert!(result.success);
assert!(result.stderr.contains("Not overwriting"));
} }
#[test] #[test]
@ -227,39 +191,33 @@ fn test_cp_arg_link() {
use std::os::linux::fs::MetadataExt; use std::os::linux::fs::MetadataExt;
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--link") .arg("--link")
.arg(TEST_HELLO_WORLD_DEST) .arg(TEST_HELLO_WORLD_DEST)
.run(); .succeeds();
assert!(result.success);
assert_eq!(at.metadata(TEST_HELLO_WORLD_SOURCE).st_nlink(), 2); assert_eq!(at.metadata(TEST_HELLO_WORLD_SOURCE).st_nlink(), 2);
} }
#[test] #[test]
fn test_cp_arg_symlink() { fn test_cp_arg_symlink() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--symbolic-link") .arg("--symbolic-link")
.arg(TEST_HELLO_WORLD_DEST) .arg(TEST_HELLO_WORLD_DEST)
.run(); .succeeds();
assert!(result.success);
assert!(at.is_symlink(TEST_HELLO_WORLD_DEST)); assert!(at.is_symlink(TEST_HELLO_WORLD_DEST));
} }
#[test] #[test]
fn test_cp_arg_no_clobber() { fn test_cp_arg_no_clobber() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--no-clobber") .arg("--no-clobber")
.arg(TEST_HOW_ARE_YOU_SOURCE) .arg(TEST_HOW_ARE_YOU_SOURCE)
.run(); .succeeds();
assert!(result.success);
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "How are you?\n"); 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() { fn test_cp_arg_no_clobber_twice() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; let at = &scene.fixtures;
at.touch("source.txt"); at.touch("source.txt");
let result = scene scene
.ucmd() .ucmd()
.arg("--no-clobber") .arg("--no-clobber")
.arg("source.txt") .arg("source.txt")
.arg("dest.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"), ""); assert_eq!(at.read("source.txt"), "");
at.append("source.txt", "some-content"); at.append("source.txt", "some-content");
let result = scene scene
.ucmd() .ucmd()
.arg("--no-clobber") .arg("--no-clobber")
.arg("source.txt") .arg("source.txt")
.arg("dest.txt") .arg("dest.txt")
.run(); .succeeds()
.stdout_does_not_contain("Not overwriting");
assert!(result.success);
assert_eq!(at.read("source.txt"), "some-content"); assert_eq!(at.read("source.txt"), "some-content");
// Should be empty as the "no-clobber" should keep // Should be empty as the "no-clobber" should keep
// the previous version // the previous version
assert_eq!(at.read("dest.txt"), ""); assert_eq!(at.read("dest.txt"), "");
assert!(!result.stderr.contains("Not overwriting"));
} }
#[test] #[test]
@ -311,16 +266,11 @@ fn test_cp_arg_force() {
permissions.set_readonly(true); permissions.set_readonly(true);
set_permissions(at.plus(TEST_HELLO_WORLD_DEST), permissions).unwrap(); set_permissions(at.plus(TEST_HELLO_WORLD_DEST), permissions).unwrap();
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--force") .arg("--force")
.arg(TEST_HELLO_WORLD_DEST) .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"); 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); permissions.set_readonly(true);
set_permissions(at.plus(TEST_HELLO_WORLD_DEST), permissions).unwrap(); set_permissions(at.plus(TEST_HELLO_WORLD_DEST), permissions).unwrap();
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--remove-destination") .arg("--remove-destination")
.arg(TEST_HELLO_WORLD_DEST) .arg(TEST_HELLO_WORLD_DEST)
.run(); .succeeds();
assert!(result.success);
assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); 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() { fn test_cp_arg_backup() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--backup") .arg("--backup")
.arg(TEST_HOW_ARE_YOU_SOURCE) .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(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
assert_eq!( assert_eq!(
at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)), at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)),
@ -374,14 +320,12 @@ fn test_cp_arg_backup() {
fn test_cp_arg_suffix() { fn test_cp_arg_suffix() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--suffix") .arg("--suffix")
.arg(".bak") .arg(".bak")
.arg(TEST_HOW_ARE_YOU_SOURCE) .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(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
assert_eq!( assert_eq!(
at.read(&*format!("{}.bak", TEST_HOW_ARE_YOU_SOURCE)), at.read(&*format!("{}.bak", TEST_HOW_ARE_YOU_SOURCE)),
@ -391,9 +335,8 @@ fn test_cp_arg_suffix() {
#[test] #[test]
fn test_cp_deref_conflicting_options() { fn test_cp_deref_conflicting_options() {
let (_at, mut ucmd) = at_and_ucmd!(); new_ucmd!()
.arg("-LP")
ucmd.arg("-LP")
.arg(TEST_COPY_TO_FOLDER) .arg(TEST_COPY_TO_FOLDER)
.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE)
.fails(); .fails();
@ -401,8 +344,7 @@ fn test_cp_deref_conflicting_options() {
#[test] #[test]
fn test_cp_deref() { fn test_cp_deref() {
let scene = TestScenario::new(util_name!()); let (at, mut ucmd) = at_and_ucmd!();
let at = &scene.fixtures;
#[cfg(not(windows))] #[cfg(not(windows))]
let _r = fs::symlink( let _r = fs::symlink(
@ -415,16 +357,12 @@ fn test_cp_deref() {
at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK), at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK),
); );
//using -L option //using -L option
let result = scene ucmd.arg("-L")
.ucmd()
.arg("-L")
.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE_SYMLINK) .arg(TEST_HELLO_WORLD_SOURCE_SYMLINK)
.arg(TEST_COPY_TO_FOLDER) .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 let path_to_new_symlink = at
.subdir .subdir
.join(TEST_COPY_TO_FOLDER) .join(TEST_COPY_TO_FOLDER)
@ -444,8 +382,7 @@ fn test_cp_deref() {
} }
#[test] #[test]
fn test_cp_no_deref() { fn test_cp_no_deref() {
let scene = TestScenario::new(util_name!()); let (at, mut ucmd) = at_and_ucmd!();
let at = &scene.fixtures;
#[cfg(not(windows))] #[cfg(not(windows))]
let _r = fs::symlink( let _r = fs::symlink(
@ -458,16 +395,12 @@ fn test_cp_no_deref() {
at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK), at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK),
); );
//using -P option //using -P option
let result = scene ucmd.arg("-P")
.ucmd()
.arg("-P")
.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE_SYMLINK) .arg(TEST_HELLO_WORLD_SOURCE_SYMLINK)
.arg(TEST_COPY_TO_FOLDER) .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 let path_to_new_symlink = at
.subdir .subdir
.join(TEST_COPY_TO_FOLDER) .join(TEST_COPY_TO_FOLDER)
@ -490,14 +423,10 @@ fn test_cp_strip_trailing_slashes() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
//using --strip-trailing-slashes option //using --strip-trailing-slashes option
let result = ucmd ucmd.arg("--strip-trailing-slashes")
.arg("--strip-trailing-slashes")
.arg(format!("{}/", TEST_HELLO_WORLD_SOURCE)) .arg(format!("{}/", TEST_HELLO_WORLD_SOURCE))
.arg(TEST_HELLO_WORLD_DEST) .arg(TEST_HELLO_WORLD_DEST)
.run(); .succeeds();
// Check that the exit code represents a successful copy.
assert!(result.success);
// Check the content of the destination file that was copied. // Check the content of the destination file that was copied.
assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); 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() { fn test_cp_parents() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
let result = ucmd ucmd.arg("--parents")
.arg("--parents")
.arg(TEST_COPY_FROM_FOLDER_FILE) .arg(TEST_COPY_FROM_FOLDER_FILE)
.arg(TEST_COPY_TO_FOLDER) .arg(TEST_COPY_TO_FOLDER)
.run(); .succeeds();
assert!(result.success);
// Check the content of the destination file that was copied.
assert_eq!( assert_eq!(
at.read(&format!( at.read(&format!(
"{}/{}", "{}/{}",
@ -528,14 +454,12 @@ fn test_cp_parents() {
fn test_cp_parents_multiple_files() { fn test_cp_parents_multiple_files() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
let result = ucmd ucmd.arg("--parents")
.arg("--parents")
.arg(TEST_COPY_FROM_FOLDER_FILE) .arg(TEST_COPY_FROM_FOLDER_FILE)
.arg(TEST_HOW_ARE_YOU_SOURCE) .arg(TEST_HOW_ARE_YOU_SOURCE)
.arg(TEST_COPY_TO_FOLDER) .arg(TEST_COPY_TO_FOLDER)
.run(); .succeeds();
assert!(result.success);
assert_eq!( assert_eq!(
at.read(&format!( at.read(&format!(
"{}/{}", "{}/{}",
@ -554,20 +478,12 @@ fn test_cp_parents_multiple_files() {
#[test] #[test]
fn test_cp_parents_dest_not_directory() { fn test_cp_parents_dest_not_directory() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!()
let result = ucmd
.arg("--parents") .arg("--parents")
.arg(TEST_COPY_FROM_FOLDER_FILE) .arg(TEST_COPY_FROM_FOLDER_FILE)
.arg(TEST_HELLO_WORLD_DEST) .arg(TEST_HELLO_WORLD_DEST)
.run(); .fails()
println!("{:?}", result); .stderr_contains("with --parents, the destination must be a directory");
// Check that we did not succeed in copying.
assert!(!result.success);
assert!(result
.stderr
.contains("with --parents, the destination must be a directory"));
} }
#[test] #[test]
@ -594,18 +510,14 @@ fn test_cp_deref_folder_to_folder() {
assert!(env::set_current_dir(&cwd).is_ok()); assert!(env::set_current_dir(&cwd).is_ok());
//using -P -R option //using -P -R option
let result = scene scene
.ucmd() .ucmd()
.arg("-L") .arg("-L")
.arg("-R") .arg("-R")
.arg("-v") .arg("-v")
.arg(TEST_COPY_FROM_FOLDER) .arg(TEST_COPY_FROM_FOLDER)
.arg(TEST_COPY_TO_FOLDER_NEW) .arg(TEST_COPY_TO_FOLDER_NEW)
.run(); .succeeds();
println!("cp output {}", result.stdout_str());
// Check that the exit code represents a successful copy.
assert!(result.success);
#[cfg(not(windows))] #[cfg(not(windows))]
{ {
@ -698,18 +610,14 @@ fn test_cp_no_deref_folder_to_folder() {
assert!(env::set_current_dir(&cwd).is_ok()); assert!(env::set_current_dir(&cwd).is_ok());
//using -P -R option //using -P -R option
let result = scene scene
.ucmd() .ucmd()
.arg("-P") .arg("-P")
.arg("-R") .arg("-R")
.arg("-v") .arg("-v")
.arg(TEST_COPY_FROM_FOLDER) .arg(TEST_COPY_FROM_FOLDER)
.arg(TEST_COPY_TO_FOLDER_NEW) .arg(TEST_COPY_TO_FOLDER_NEW)
.run(); .succeeds();
println!("cp output {}", result.stdout_str());
// Check that the exit code represents a successful copy.
assert!(result.success);
#[cfg(not(windows))] #[cfg(not(windows))]
{ {
@ -791,13 +699,11 @@ fn test_cp_archive() {
previous, previous,
) )
.unwrap(); .unwrap();
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--archive") .arg("--archive")
.arg(TEST_HOW_ARE_YOU_SOURCE) .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(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap(); 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 creation2 = metadata2.modified().unwrap();
let scene2 = TestScenario::new("ls"); 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()); println!("ls dest {}", result.stdout_str());
assert_eq!(creation, creation2); assert_eq!(creation, creation2);
assert!(result.success);
} }
#[test] #[test]
@ -850,11 +755,10 @@ fn test_cp_archive_recursive() {
// Back to the initial cwd (breaks the other tests) // Back to the initial cwd (breaks the other tests)
assert!(env::set_current_dir(&cwd).is_ok()); assert!(env::set_current_dir(&cwd).is_ok());
let resultg = ucmd ucmd.arg("--archive")
.arg("--archive")
.arg(TEST_COPY_TO_FOLDER) .arg(TEST_COPY_TO_FOLDER)
.arg(TEST_COPY_TO_FOLDER_NEW) .arg(TEST_COPY_TO_FOLDER_NEW)
.run(); .fails(); // fails for now
let scene2 = TestScenario::new("ls"); let scene2 = TestScenario::new("ls");
let result = scene2 let result = scene2
@ -865,7 +769,6 @@ fn test_cp_archive_recursive() {
println!("ls dest {}", result.stdout_str()); println!("ls dest {}", result.stdout_str());
let scene2 = TestScenario::new("ls");
let result = scene2 let result = scene2
.cmd("ls") .cmd("ls")
.arg("-al") .arg("-al")
@ -910,9 +813,6 @@ fn test_cp_archive_recursive() {
.join("2.link") .join("2.link")
.to_string_lossy() .to_string_lossy()
)); ));
// fails for now
assert!(resultg.success);
} }
#[test] #[test]
@ -928,13 +828,11 @@ fn test_cp_preserve_timestamps() {
previous, previous,
) )
.unwrap(); .unwrap();
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--preserve=timestamps") .arg("--preserve=timestamps")
.arg(TEST_HOW_ARE_YOU_SOURCE) .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(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap(); 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()); println!("ls dest {}", result.stdout_str());
assert_eq!(creation, creation2); assert_eq!(creation, creation2);
assert!(result.success);
} }
#[test] #[test]
@ -966,13 +863,11 @@ fn test_cp_dont_preserve_timestamps() {
.unwrap(); .unwrap();
sleep(Duration::from_secs(3)); sleep(Duration::from_secs(3));
let result = ucmd ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--no-preserve=timestamps") .arg("--no-preserve=timestamps")
.arg(TEST_HOW_ARE_YOU_SOURCE) .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(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap(); 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 // Some margins with time check
assert!(res.as_secs() > 3595); assert!(res.as_secs() > 3595);
assert!(res.as_secs() < 3605); assert!(res.as_secs() < 3605);
assert!(result.success);
} }
#[test] #[test]
@ -1017,7 +911,7 @@ fn test_cp_one_file_system() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
// Test must be run as root (or with `sudo -E`) // 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; return;
} }
@ -1042,17 +936,16 @@ fn test_cp_one_file_system() {
at_src.touch(TEST_MOUNT_OTHER_FILESYSTEM_FILE); at_src.touch(TEST_MOUNT_OTHER_FILESYSTEM_FILE);
// Begin testing -x flag // Begin testing -x flag
let result = scene scene
.ucmd() .ucmd()
.arg("-rx") .arg("-rx")
.arg(TEST_MOUNT_COPY_FROM_FOLDER) .arg(TEST_MOUNT_COPY_FROM_FOLDER)
.arg(TEST_COPY_TO_FOLDER_NEW) .arg(TEST_COPY_TO_FOLDER_NEW)
.run(); .succeeds();
// Ditch the mount before the asserts // Ditch the mount before the asserts
scene.cmd("umount").arg(mountpoint_path).succeeds(); scene.cmd("umount").arg(mountpoint_path).succeeds();
assert!(result.success);
assert!(!at_dst.file_exists(TEST_MOUNT_OTHER_FILESYSTEM_FILE)); assert!(!at_dst.file_exists(TEST_MOUNT_OTHER_FILESYSTEM_FILE));
// Check if the other files were copied from the source folder hirerarchy // Check if the other files were copied from the source folder hirerarchy
for entry in WalkDir::new(at_src.as_string()) { 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");
}

View file

@ -7,174 +7,147 @@ use rust_users::*;
#[test] #[test]
fn test_date_email() { fn test_date_email() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!().arg("--rfc-email").succeeds();
let result = ucmd.arg("--rfc-email").run();
assert!(result.success);
} }
#[test] #[test]
fn test_date_email2() { fn test_date_email2() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!().arg("-R").succeeds();
let result = ucmd.arg("-R").run();
assert!(result.success);
} }
#[test] #[test]
fn test_date_rfc_3339() { fn test_date_rfc_3339() {
let scene = TestScenario::new(util_name!()); 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 // 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])))"; scene
let re = Regex::new(rfc_regexp).unwrap(); .ucmd()
assert!(re.is_match(&result.stdout_str().trim())); .arg("--rfc-3339=ns")
.succeeds()
.stdout_matches(&re);
result = scene.ucmd().arg("--rfc-3339=seconds").succeeds(); scene
.ucmd()
// Check that the output matches the regexp .arg("--rfc-3339=seconds")
let re = Regex::new(rfc_regexp).unwrap(); .succeeds()
assert!(re.is_match(&result.stdout_str().trim())); .stdout_matches(&re);
} }
#[test] #[test]
fn test_date_rfc_8601() { fn test_date_rfc_8601() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!().arg("--iso-8601=ns").succeeds();
let result = ucmd.arg("--iso-8601=ns").run();
assert!(result.success);
} }
#[test] #[test]
fn test_date_rfc_8601_second() { fn test_date_rfc_8601_second() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!().arg("--iso-8601=second").succeeds();
let result = ucmd.arg("--iso-8601=second").run();
assert!(result.success);
} }
#[test] #[test]
fn test_date_utc() { fn test_date_utc() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!().arg("--utc").succeeds();
let result = ucmd.arg("--utc").run();
assert!(result.success);
} }
#[test] #[test]
fn test_date_universal() { fn test_date_universal() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!().arg("--universal").succeeds();
let result = ucmd.arg("--universal").run();
assert!(result.success);
} }
#[test] #[test]
fn test_date_format_y() { fn test_date_format_y() {
let scene = TestScenario::new(util_name!()); 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(); 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(); re = Regex::new(r"^\d{2}$").unwrap();
assert!(re.is_match(&result.stdout_str().trim())); scene.ucmd().arg("+%y").succeeds().stdout_matches(&re);
} }
#[test] #[test]
fn test_date_format_m() { fn test_date_format_m() {
let scene = TestScenario::new(util_name!()); 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(); 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(); re = Regex::new(r"^\d{2}$").unwrap();
assert!(re.is_match(&result.stdout_str().trim())); scene.ucmd().arg("+%m").succeeds().stdout_matches(&re);
} }
#[test] #[test]
fn test_date_format_day() { fn test_date_format_day() {
let scene = TestScenario::new(util_name!()); 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(); let mut 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("+%A").succeeds();
assert!(result.success);
re = Regex::new(r"\S+").unwrap(); 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(); re = Regex::new(r"^\d{1}$").unwrap();
assert!(re.is_match(&result.stdout_str().trim())); scene.ucmd().arg("+%u").succeeds().stdout_matches(&re);
} }
#[test] #[test]
fn test_date_format_full_day() { 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(); 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] #[test]
#[cfg(all(unix, not(target_os = "macos")))] #[cfg(all(unix, not(target_os = "macos")))]
fn test_date_set_valid() { fn test_date_set_valid() {
if get_effective_uid() == 0 { if get_effective_uid() == 0 {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!()
let result = ucmd
.arg("--set") .arg("--set")
.arg("2020-03-12 13:30:00+08:00") .arg("2020-03-12 13:30:00+08:00")
.succeeds(); .succeeds()
result.no_stdout().no_stderr(); .no_stdout()
.no_stderr();
} }
} }
#[test] #[test]
#[cfg(any(windows, all(unix, not(target_os = "macos"))))] #[cfg(any(windows, all(unix, not(target_os = "macos"))))]
fn test_date_set_invalid() { fn test_date_set_invalid() {
let (_, mut ucmd) = at_and_ucmd!(); let result = new_ucmd!().arg("--set").arg("123abcd").fails();
let result = ucmd.arg("--set").arg("123abcd").fails(); result.no_stdout();
let result = result.no_stdout(); assert!(result.stderr_str().starts_with("date: invalid date "));
assert!(result.stderr.starts_with("date: invalid date "));
} }
#[test] #[test]
#[cfg(all(unix, not(target_os = "macos")))] #[cfg(all(unix, not(target_os = "macos")))]
fn test_date_set_permissions_error() { fn test_date_set_permissions_error() {
if !(get_effective_uid() == 0 || is_wsl()) { if !(get_effective_uid() == 0 || uucore::os::is_wsl_1()) {
let (_, mut ucmd) = at_and_ucmd!(); let result = new_ucmd!()
let result = ucmd.arg("--set").arg("2020-03-11 21:45:00+08:00").fails(); .arg("--set")
let result = result.no_stdout(); .arg("2020-03-11 21:45:00+08:00")
assert!(result.stderr.starts_with("date: cannot set date: ")); .fails();
result.no_stdout();
assert!(result.stderr_str().starts_with("date: cannot set date: "));
} }
} }
#[test] #[test]
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
fn test_date_set_mac_unavailable() { fn test_date_set_mac_unavailable() {
let (_, mut ucmd) = at_and_ucmd!(); let result = new_ucmd!()
let result = ucmd.arg("--set").arg("2020-03-11 21:45:00+08:00").fails(); .arg("--set")
let result = result.no_stdout(); .arg("2020-03-11 21:45:00+08:00")
.fails();
result.no_stdout();
assert!(result assert!(result
.stderr .stderr_str()
.starts_with("date: setting the date is not supported by macOS")); .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. /// TODO: expected to fail currently; change to succeeds() when required.
fn test_date_set_valid_2() { fn test_date_set_valid_2() {
if get_effective_uid() == 0 { if get_effective_uid() == 0 {
let (_, mut ucmd) = at_and_ucmd!(); let result = new_ucmd!()
let result = ucmd
.arg("--set") .arg("--set")
.arg("Sat 20 Mar 2021 14:53:01 AWST") .arg("Sat 20 Mar 2021 14:53:01 AWST")
.fails(); .fails();
let result = result.no_stdout(); result.no_stdout();
assert!(result.stderr.starts_with("date: invalid date ")); 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. /// TODO: expected to fail currently; change to succeeds() when required.
fn test_date_set_valid_3() { fn test_date_set_valid_3() {
if get_effective_uid() == 0 { if get_effective_uid() == 0 {
let (_, mut ucmd) = at_and_ucmd!(); let result = new_ucmd!()
let result = ucmd
.arg("--set") .arg("--set")
.arg("Sat 20 Mar 2021 14:53:01") // Local timezone .arg("Sat 20 Mar 2021 14:53:01") // Local timezone
.fails(); .fails();
let result = result.no_stdout(); result.no_stdout();
assert!(result.stderr.starts_with("date: invalid date ")); 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. /// TODO: expected to fail currently; change to succeeds() when required.
fn test_date_set_valid_4() { fn test_date_set_valid_4() {
if get_effective_uid() == 0 { if get_effective_uid() == 0 {
let (_, mut ucmd) = at_and_ucmd!(); let result = new_ucmd!()
let result = ucmd
.arg("--set") .arg("--set")
.arg("2020-03-11 21:45:00") // Local timezone .arg("2020-03-11 21:45:00") // Local timezone
.fails(); .fails();
let result = result.no_stdout(); result.no_stdout();
assert!(result.stderr.starts_with("date: invalid date ")); assert!(result.stderr_str().starts_with("date: invalid date "));
} }
} }

View file

@ -2,30 +2,22 @@ use crate::common::util::*;
#[test] #[test]
fn test_df_compatible_no_size_arg() { fn test_df_compatible_no_size_arg() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!().arg("-a").succeeds();
let result = ucmd.arg("-a").run();
assert!(result.success);
} }
#[test] #[test]
fn test_df_compatible() { fn test_df_compatible() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!().arg("-ah").succeeds();
let result = ucmd.arg("-ah").run();
assert!(result.success);
} }
#[test] #[test]
fn test_df_compatible_type() { fn test_df_compatible_type() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!().arg("-aT").succeeds();
let result = ucmd.arg("-aT").run();
assert!(result.success);
} }
#[test] #[test]
fn test_df_compatible_si() { fn test_df_compatible_si() {
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!().arg("-aH").succeeds();
let result = ucmd.arg("-aH").run();
assert!(result.success);
} }
// ToDO: more tests... // ToDO: more tests...

View file

@ -10,7 +10,7 @@ fn test_du_basics() {
new_ucmd!().succeeds().no_stderr(); new_ucmd!().succeeds().no_stderr();
} }
#[cfg(target_vendor = "apple")] #[cfg(target_vendor = "apple")]
fn _du_basics(s: String) { fn _du_basics(s: &str) {
let answer = "32\t./subdir let answer = "32\t./subdir
8\t./subdir/deeper 8\t./subdir/deeper
24\t./subdir/links 24\t./subdir/links
@ -30,11 +30,18 @@ fn _du_basics(s: &str) {
#[test] #[test]
fn test_du_basics_subdir() { 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(); let result = scene.ucmd().arg(SUB_DIR).succeeds();
assert!(result.success);
assert_eq!(result.stderr, ""); #[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()); _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")))] #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))]
fn _du_basics_subdir(s: &str) { fn _du_basics_subdir(s: &str) {
// MS-WSL linux has altered expected output // MS-WSL linux has altered expected output
if !is_wsl() { if !uucore::os::is_wsl_1() {
assert_eq!(s, "8\tsubdir/deeper\n"); assert_eq!(s, "8\tsubdir/deeper\n");
} else { } else {
assert_eq!(s, "0\tsubdir/deeper\n"); assert_eq!(s, "0\tsubdir/deeper\n");
@ -58,26 +65,29 @@ fn _du_basics_subdir(s: &str) {
#[test] #[test]
fn test_du_basics_bad_name() { fn test_du_basics_bad_name() {
let (_at, mut ucmd) = at_and_ucmd!(); new_ucmd!()
.arg("bad_name")
let result = ucmd.arg("bad_name").run(); .succeeds() // TODO: replace with ".fails()" once `du` is fixed
assert_eq!(result.stdout_str(), ""); .stderr_only("du: error: bad_name: No such file or directory\n");
assert_eq!(
result.stderr,
"du: error: bad_name: No such file or directory\n"
);
} }
#[test] #[test]
fn test_du_soft_link() { 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(); at.symlink_file(SUB_FILE, SUB_LINK);
assert!(link.success);
let result = ts.ucmd().arg(SUB_DIR_LINKS).run(); let result = scene.ucmd().arg(SUB_DIR_LINKS).succeeds();
assert!(result.success);
assert_eq!(result.stderr, ""); #[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()); _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")))] #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))]
fn _du_soft_link(s: &str) { fn _du_soft_link(s: &str) {
// MS-WSL linux has altered expected output // MS-WSL linux has altered expected output
if !is_wsl() { if !uucore::os::is_wsl_1() {
assert_eq!(s, "16\tsubdir/links\n"); assert_eq!(s, "16\tsubdir/links\n");
} else { } else {
assert_eq!(s, "8\tsubdir/links\n"); assert_eq!(s, "8\tsubdir/links\n");
@ -102,14 +112,23 @@ fn _du_soft_link(s: &str) {
#[test] #[test]
fn test_du_hard_link() { 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(); let result_ln = scene.cmd("ln").arg(SUB_FILE).arg(SUB_LINK).run();
assert!(link.success); if !result_ln.succeeded() {
scene.ccmd("ln").arg(SUB_FILE).arg(SUB_LINK).succeeds();
}
let result = ts.ucmd().arg(SUB_DIR_LINKS).run(); let result = scene.ucmd().arg(SUB_DIR_LINKS).succeeds();
assert!(result.success);
assert_eq!(result.stderr, ""); #[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 // We do not double count hard links as the inodes are identical
_du_hard_link(result.stdout_str()); _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")))] #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))]
fn _du_hard_link(s: &str) { fn _du_hard_link(s: &str) {
// MS-WSL linux has altered expected output // MS-WSL linux has altered expected output
if !is_wsl() { if !uucore::os::is_wsl_1() {
assert_eq!(s, "16\tsubdir/links\n"); assert_eq!(s, "16\tsubdir/links\n");
} else { } else {
assert_eq!(s, "8\tsubdir/links\n"); assert_eq!(s, "8\tsubdir/links\n");
@ -134,11 +153,23 @@ fn _du_hard_link(s: &str) {
#[test] #[test]
fn test_du_d_flag() { 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(); let result = scene.ucmd().arg("-d1").succeeds();
assert!(result.success);
assert_eq!(result.stderr, ""); #[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()); _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")))] #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))]
fn _du_d_flag(s: &str) { fn _du_d_flag(s: &str) {
// MS-WSL linux has altered expected output // 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"); assert_eq!(s, "28\t./subdir\n36\t./\n");
} else { } else {
assert_eq!(s, "8\t./subdir\n8\t./\n"); assert_eq!(s, "8\t./subdir\n8\t./\n");
@ -162,9 +193,7 @@ fn _du_d_flag(s: &str) {
#[test] #[test]
fn test_du_h_flag_empty_file() { fn test_du_h_flag_empty_file() {
let ts = TestScenario::new("du"); new_ucmd!()
ts.ucmd()
.arg("-h") .arg("-h")
.arg("empty.txt") .arg("empty.txt")
.succeeds() .succeeds()
@ -174,54 +203,61 @@ fn test_du_h_flag_empty_file() {
#[cfg(feature = "touch")] #[cfg(feature = "touch")]
#[test] #[test]
fn test_du_time() { fn test_du_time() {
let ts = TestScenario::new("du"); let scene = TestScenario::new(util_name!());
let touch = ts scene
.ccmd("touch") .ccmd("touch")
.arg("-a") .arg("-a")
.arg("-m") .arg("-m")
.arg("-t") .arg("-t")
.arg("201505150000") .arg("201505150000")
.arg("date_test") .arg("date_test")
.run(); .succeeds();
assert!(touch.success);
let result = ts.ucmd().arg("--time").arg("date_test").run(); scene
.ucmd()
// cleanup by removing test file .arg("--time")
ts.cmd("rm").arg("date_test").run(); .arg("date_test")
.succeeds()
assert!(result.success); .stdout_only("0\t2015-05-15 00:00\tdate_test\n");
assert_eq!(result.stderr, "");
assert_eq!(result.stdout, "0\t2015-05-15 00:00\tdate_test\n");
} }
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
#[cfg(feature = "chmod")] #[cfg(feature = "chmod")]
#[test] #[test]
fn test_du_no_permission() { 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(); at.mkdir_all(SUB_DIR_LINKS);
println!("chmod output: {:?}", chmod);
assert!(chmod.success);
let result = ts.ucmd().arg(SUB_DIR_LINKS).run();
ts.ccmd("chmod").arg("+r").arg(SUB_DIR_LINKS).run(); scene.ccmd("chmod").arg("-r").arg(SUB_DIR_LINKS).succeeds();
assert!(result.success); let result = scene.ucmd().arg(SUB_DIR_LINKS).run(); // TODO: replace with ".fails()" once `du` is fixed
assert_eq!( result.stderr_contains(
result.stderr, "du: cannot read directory subdir/links: Permission denied (os error 13)",
"du: cannot read directory subdir/links: Permission denied (os error 13)\n"
); );
_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")] #[cfg(target_vendor = "apple")]
fn _du_no_permission(s: String) { fn _du_no_permission(s: &str) {
assert_eq!(s, "0\tsubdir/links\n"); assert_eq!(s, "0\tsubdir/links\n");
} }
#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] #[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"); assert_eq!(s, "4\tsubdir/links\n");
} }

View file

@ -140,8 +140,11 @@ fn test_unset_variable() {
#[test] #[test]
fn test_fail_null_with_program() { fn test_fail_null_with_program() {
let out = new_ucmd!().arg("--null").arg("cd").fails().stderr; new_ucmd!()
assert!(out.contains("cannot specify --null (-0) with command")); .arg("--null")
.arg("cd")
.fails()
.stderr_contains("cannot specify --null (-0) with command");
} }
#[cfg(not(windows))] #[cfg(not(windows))]

View file

@ -29,7 +29,7 @@ fn test_fmt_w_too_big() {
.run(); .run();
//.stdout_is_fixture("call_graph.expected"); //.stdout_is_fixture("call_graph.expected");
assert_eq!( assert_eq!(
result.stderr.trim(), result.stderr_str().trim(),
"fmt: error: invalid width: '2501': Numerical result out of range" "fmt: error: invalid width: '2501': Numerical result out of range"
); );
} }

View file

@ -10,7 +10,7 @@ fn test_groups() {
// As seems to be a configuration issue, ignoring it // As seems to be a configuration issue, ignoring it
return; return;
} }
assert!(result.success); result.success();
assert!(!result.stdout_str().trim().is_empty()); assert!(!result.stdout_str().trim().is_empty());
} }
@ -30,16 +30,12 @@ fn test_groups_arg() {
println!("result.stdout = {}", result.stdout_str()); println!("result.stdout = {}", result.stdout_str());
println!("result.stderr = {}", result.stderr_str()); println!("result.stderr = {}", result.stderr_str());
assert!(result.success); result.success();
assert!(!result.stdout_str().is_empty()); assert!(!result.stdout_str().is_empty());
let username = result.stdout_str().trim(); let username = result.stdout_str().trim();
// call groups with the user name to check that we // call groups with the user name to check that we
// are getting something // are getting something
let (_, mut ucmd) = at_and_ucmd!(); new_ucmd!().arg(username).succeeds();
let result = ucmd.arg(username).run();
println!("result.stdout = {}", result.stdout_str());
println!("result.stderr = {}", result.stderr_str());
assert!(result.success);
assert!(!result.stdout_str().is_empty()); assert!(!result.stdout_str().is_empty());
} }

View file

@ -156,14 +156,10 @@ fn test_negative_zero_bytes() {
} }
#[test] #[test]
fn test_no_such_file_or_directory() { fn test_no_such_file_or_directory() {
let result = new_ucmd!().arg("no_such_file.toml").run(); new_ucmd!()
.arg("no_such_file.toml")
assert_eq!( .fails()
true, .stderr_contains("cannot open 'no_such_file.toml' for reading: No such file or directory");
result
.stderr
.contains("cannot open 'no_such_file.toml' for reading: No such file or directory")
)
} }
// there was a bug not caught by previous tests // there was a bug not caught by previous tests

View file

@ -11,12 +11,10 @@ use std::thread::sleep;
fn test_install_help() { fn test_install_help() {
let (_, mut ucmd) = at_and_ucmd!(); let (_, mut ucmd) = at_and_ucmd!();
assert!(ucmd ucmd.arg("--help")
.arg("--help")
.succeeds() .succeeds()
.no_stderr() .no_stderr()
.stdout .stdout_contains("FLAGS:");
.contains("FLAGS:"));
} }
#[test] #[test]
@ -59,13 +57,11 @@ fn test_install_failing_not_dir() {
at.touch(file1); at.touch(file1);
at.touch(file2); at.touch(file2);
at.touch(file3); at.touch(file3);
assert!(ucmd ucmd.arg(file1)
.arg(file1)
.arg(file2) .arg(file2)
.arg(file3) .arg(file3)
.fails() .fails()
.stderr .stderr_contains("not a directory");
.contains("not a directory"));
} }
#[test] #[test]
@ -77,13 +73,11 @@ fn test_install_unimplemented_arg() {
at.touch(file); at.touch(file);
at.mkdir(dir); at.mkdir(dir);
assert!(ucmd ucmd.arg(context_arg)
.arg(context_arg)
.arg(file) .arg(file)
.arg(dir) .arg(dir)
.fails() .fails()
.stderr .stderr_contains("Unimplemented");
.contains("Unimplemented"));
assert!(!at.file_exists(&format!("{}/{}", dir, file))); assert!(!at.file_exists(&format!("{}/{}", dir, file)));
} }
@ -231,13 +225,11 @@ fn test_install_mode_failing() {
at.touch(file); at.touch(file);
at.mkdir(dir); at.mkdir(dir);
assert!(ucmd ucmd.arg(file)
.arg(file)
.arg(dir) .arg(dir)
.arg(mode_arg) .arg(mode_arg)
.fails() .fails()
.stderr .stderr_contains("Invalid mode string: invalid digit found in string");
.contains("Invalid mode string: invalid digit found in string"));
let dest_file = &format!("{}/{}", dir, file); let dest_file = &format!("{}/{}", dir, file);
assert!(at.file_exists(file)); assert!(at.file_exists(file));
@ -336,7 +328,7 @@ fn test_install_target_new_file_with_owner() {
.arg(format!("{}/{}", dir, file)) .arg(format!("{}/{}", dir, file))
.run(); .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. // In the CI, some server are failing to return the user id.
// As seems to be a configuration issue, ignoring it // As seems to be a configuration issue, ignoring it
return; return;
@ -359,7 +351,7 @@ fn test_install_target_new_file_failing_nonexistent_parent() {
ucmd.arg(file1) ucmd.arg(file1)
.arg(format!("{}/{}", dir, file2)) .arg(format!("{}/{}", dir, file2))
.fails() .fails()
.stderr_contains(&"not a directory"); .stderr_contains(&"No such file or directory");
} }
#[test] #[test]
@ -619,33 +611,64 @@ fn test_install_and_strip_with_program() {
#[test] #[test]
#[cfg(not(windows))] #[cfg(not(windows))]
fn test_install_and_strip_with_invalid_program() { fn test_install_and_strip_with_invalid_program() {
let scene = TestScenario::new(util_name!()); new_ucmd!()
let stderr = scene
.ucmd()
.arg("-s") .arg("-s")
.arg("--strip-program") .arg("--strip-program")
.arg("/bin/date") .arg("/bin/date")
.arg(strip_source_file()) .arg(strip_source_file())
.arg(STRIP_TARGET_FILE) .arg(STRIP_TARGET_FILE)
.fails() .fails()
.stderr; .stderr_contains("strip program failed");
assert!(stderr.contains("strip program failed"));
} }
#[test] #[test]
#[cfg(not(windows))] #[cfg(not(windows))]
fn test_install_and_strip_with_non_existent_program() { fn test_install_and_strip_with_non_existent_program() {
let scene = TestScenario::new(util_name!()); new_ucmd!()
let stderr = scene
.ucmd()
.arg("-s") .arg("-s")
.arg("--strip-program") .arg("--strip-program")
.arg("/usr/bin/non_existent_program") .arg("/usr/bin/non_existent_program")
.arg(strip_source_file()) .arg(strip_source_file())
.arg(STRIP_TARGET_FILE) .arg(STRIP_TARGET_FILE)
.fails() .fails()
.stderr; .stderr_contains("No such file or directory");
assert!(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");
} }

View file

@ -299,13 +299,11 @@ fn test_symlink_overwrite_dir_fail() {
at.touch(path_a); at.touch(path_a);
at.mkdir(path_b); at.mkdir(path_b);
assert!( assert!(!ucmd
ucmd.args(&["-s", "-T", path_a, path_b]) .args(&["-s", "-T", path_a, path_b])
.fails() .fails()
.stderr .stderr_str()
.len() .is_empty());
> 0
);
} }
#[test] #[test]
@ -358,7 +356,11 @@ fn test_symlink_target_only() {
at.mkdir(dir); 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] #[test]

View file

@ -9,7 +9,7 @@ fn test_normal() {
for (key, value) in env::vars() { for (key, value) in env::vars() {
println!("{}: {}", key, value); 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 // ToDO: investigate WSL failure
// In the CI, some server are failing to return logname. // In the CI, some server are failing to return logname.
// As seems to be a configuration issue, ignoring it // As seems to be a configuration issue, ignoring it

View file

@ -103,6 +103,20 @@ fn test_ls_width() {
.succeeds() .succeeds()
.stdout_only("test-width-1\ntest-width-2\ntest-width-3\ntest-width-4\n"); .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::<Vec<_>>())
.fails()
.stderr_only("ls: error: invalid line width: 1a");
}
} }
#[test] #[test]
@ -436,6 +450,39 @@ fn test_ls_deref() {
assert!(!re.is_match(result.stdout_str().trim())); 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] #[test]
fn test_ls_order_size() { fn test_ls_order_size() {
let scene = TestScenario::new(util_name!()); 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"); result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n");
#[cfg(windows)] #[cfg(windows)]
result.stdout_only("test-1 test-2 test-3 test-4\n"); 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] #[test]
@ -472,7 +531,9 @@ fn test_ls_long_ctime() {
let at = &scene.fixtures; let at = &scene.fixtures;
at.touch("test-long-ctime-1"); at.touch("test-long-ctime-1");
let result = scene.ucmd().arg("-lc").succeeds();
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. // Should show the time on Unix, but question marks on windows.
#[cfg(unix)] #[cfg(unix)]
@ -480,6 +541,7 @@ fn test_ls_long_ctime() {
#[cfg(not(unix))] #[cfg(not(unix))]
result.stdout_contains("???"); result.stdout_contains("???");
} }
}
#[test] #[test]
fn test_ls_order_time() { fn test_ls_order_time() {
@ -519,15 +581,28 @@ fn test_ls_order_time() {
#[cfg(windows)] #[cfg(windows)]
result.stdout_only("test-4 test-3 test-2 test-1\n"); 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(); let result = scene.ucmd().arg("-tr").succeeds();
#[cfg(not(windows))] #[cfg(not(windows))]
result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n"); result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n");
#[cfg(windows)] #[cfg(windows)]
result.stdout_only("test-1 test-2 test-3 test-4\n"); 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 // 3 was accessed last in the read
// So the order should be 2 3 4 1 // So the order should be 2 3 4 1
let result = scene.ucmd().arg("-tu").succeeds(); 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 file3_access = at.open("test-3").metadata().unwrap().accessed().unwrap();
let file4_access = at.open("test-4").metadata().unwrap().accessed().unwrap(); let file4_access = at.open("test-4").metadata().unwrap().accessed().unwrap();
@ -547,6 +622,7 @@ fn test_ls_order_time() {
result.stdout_only("test-4 test-3 test-2 test-1\n"); result.stdout_only("test-4 test-3 test-2 test-1\n");
} }
} }
}
// test-2 had the last ctime change when the permissions were set // test-2 had the last ctime change when the permissions were set
// So the order should be 2 4 3 1 // So the order should be 2 4 3 1
@ -621,20 +697,27 @@ fn test_ls_recursive() {
result.stdout_contains(&"a\\b:\nb"); result.stdout_contains(&"a\\b:\nb");
} }
#[cfg(unix)]
#[test] #[test]
fn test_ls_ls_color() { fn test_ls_color() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; let at = &scene.fixtures;
at.mkdir("a"); 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.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"); at.touch("test-color");
let a_with_colors = "\x1b[01;34ma\x1b[0m"; let a_with_colors = "\x1b[1;34ma\x1b[0m";
let z_with_colors = "\x1b[01;34mz\x1b[0m"; let z_with_colors = "\x1b[1;34mz\x1b[0m";
let nested_dir_with_colors = "\x1b[01;34mnested_dir\x1b[0m"; let nested_dir_with_colors = "\x1b[1;34mnested_dir\x1b[0m";
// Color is disabled by default // Color is disabled by default
let result = scene.ucmd().succeeds(); let result = scene.ucmd().succeeds();
@ -670,14 +753,6 @@ fn test_ls_ls_color() {
.succeeds() .succeeds()
.stdout_contains(nested_dir_with_colors); .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 // No output
scene scene
.ucmd() .ucmd()
@ -817,7 +892,7 @@ fn test_ls_indicator_style() {
let options = vec!["classify", "file-type", "slash"]; let options = vec!["classify", "file-type", "slash"];
for opt in options { for opt in options {
// Verify that classify and file-type both contain indicators for symlinks. // Verify that classify and file-type both contain indicators for symlinks.
let result = scene scene
.ucmd() .ucmd()
.arg(format!("--indicator-style={}", opt)) .arg(format!("--indicator-style={}", opt))
.succeeds() .succeeds()
@ -827,7 +902,7 @@ fn test_ls_indicator_style() {
// Same test as above, but with the alternate flags. // Same test as above, but with the alternate flags.
let options = vec!["--classify", "--file-type", "-p"]; let options = vec!["--classify", "--file-type", "-p"];
for opt in options { for opt in options {
let result = scene scene
.ucmd() .ucmd()
.arg(format!("{}", opt)) .arg(format!("{}", opt))
.succeeds() .succeeds()
@ -838,7 +913,7 @@ fn test_ls_indicator_style() {
let options = vec!["classify", "file-type"]; let options = vec!["classify", "file-type"];
for opt in options { for opt in options {
// Verify that classify and file-type both contain indicators for symlinks. // Verify that classify and file-type both contain indicators for symlinks.
let result = scene scene
.ucmd() .ucmd()
.arg(format!("--indicator-style={}", opt)) .arg(format!("--indicator-style={}", opt))
.succeeds() .succeeds()
@ -962,7 +1037,7 @@ fn test_ls_hidden_windows() {
let result = scene.ucmd().succeeds(); let result = scene.ucmd().succeeds();
assert!(!result.stdout_str().contains(file)); 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] #[test]
@ -1052,9 +1127,11 @@ fn test_ls_quoting_style() {
at.touch("one"); at.touch("one");
// It seems that windows doesn't allow \n in filenames. // It seems that windows doesn't allow \n in filenames.
// And it also doesn't like \, of course.
#[cfg(unix)] #[cfg(unix)]
{ {
at.touch("one\ntwo"); at.touch("one\ntwo");
at.touch("one\\two");
// Default is shell-escape // Default is shell-escape
scene scene
.ucmd() .ucmd()
@ -1116,6 +1193,42 @@ fn test_ls_quoting_style() {
.succeeds() .succeeds()
.stdout_only(format!("{}\n", correct)); .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 scene
@ -1316,6 +1429,43 @@ fn test_ls_ignore_hide() {
.stdout_is("CONTRIBUTING.md\nREADME.md\nREADMECAREFULLY.md\nsome_other_file\n"); .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] #[test]
fn test_ls_directory() { fn test_ls_directory() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());

View file

@ -19,8 +19,7 @@ fn test_create_one_fifo_with_invalid_mode() {
.arg("-m") .arg("-m")
.arg("invalid") .arg("invalid")
.fails() .fails()
.stderr .stderr_contains("invalid mode");
.contains("invalid mode");
} }
#[test] #[test]

View file

@ -2,18 +2,15 @@ use crate::common::util::*;
#[test] #[test]
fn test_more_no_arg() { fn test_more_no_arg() {
let (_, mut ucmd) = at_and_ucmd!(); // stderr = more: Reading from stdin isn't supported yet.
let result = ucmd.run(); new_ucmd!().fails();
assert!(!result.success);
} }
#[test] #[test]
fn test_more_dir_arg() { fn test_more_dir_arg() {
let (_, mut ucmd) = at_and_ucmd!(); let result = new_ucmd!().arg(".").run();
ucmd.arg("."); result.failure();
let result = ucmd.run();
assert!(!result.success);
const EXPECTED_ERROR_MESSAGE: &str = const EXPECTED_ERROR_MESSAGE: &str =
"more: '.' is a directory.\nTry 'more --help' for more information."; "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);
} }

View file

@ -476,16 +476,9 @@ fn test_mv_overwrite_nonempty_dir() {
// GNU: "mv: cannot move a to b: Directory not empty" // GNU: "mv: cannot move a to b: Directory not empty"
// Verbose output for the move should not be shown on failure // Verbose output for the move should not be shown on failure
assert!( let result = ucmd.arg("-vT").arg(dir_a).arg(dir_b).fails();
ucmd.arg("-vT") result.no_stdout();
.arg(dir_a) assert!(!result.stderr_str().is_empty());
.arg(dir_b)
.fails()
.no_stdout()
.stderr
.len()
> 0
);
assert!(at.dir_exists(dir_a)); assert!(at.dir_exists(dir_a));
assert!(at.dir_exists(dir_b)); assert!(at.dir_exists(dir_b));
@ -526,15 +519,15 @@ fn test_mv_errors() {
// $ mv -T -t a b // $ mv -T -t a b
// mv: cannot combine --target-directory (-t) and --no-target-directory (-T) // mv: cannot combine --target-directory (-t) and --no-target-directory (-T)
let result = scene scene
.ucmd() .ucmd()
.arg("-T") .arg("-T")
.arg("-t") .arg("-t")
.arg(dir) .arg(dir)
.arg(file_a) .arg(file_a)
.arg(file_b) .arg(file_b)
.fails(); .fails()
assert!(result.stderr.contains("cannot be used with")); .stderr_contains("cannot be used with");
// $ at.touch file && at.mkdir dir // $ at.touch file && at.mkdir dir
// $ mv -T file dir // $ mv -T file dir
@ -553,7 +546,13 @@ fn test_mv_errors() {
// $ at.mkdir dir && at.touch file // $ at.mkdir dir && at.touch file
// $ mv dir file // $ mv dir file
// err == mv: cannot overwrite non-directory file with directory dir // 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] #[test]

View file

@ -16,7 +16,7 @@ fn test_negative_adjustment() {
let res = new_ucmd!().args(&["-n", "-1", "true"]).run(); let res = new_ucmd!().args(&["-n", "-1", "true"]).run();
assert!(res assert!(res
.stderr .stderr_str()
.starts_with("nice: warning: setpriority: Permission denied")); .starts_with("nice: warning: setpriority: Permission denied"));
} }

View file

@ -9,35 +9,28 @@ fn test_output_is_random_permutation() {
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n"); .join("\n");
let result = new_ucmd!() let result = new_ucmd!().pipe_in(input.as_bytes()).succeeds();
.pipe_in(input.as_bytes()) result.no_stderr();
.succeeds()
.no_stderr()
.stdout
.clone();
let mut result_seq: Vec<i32> = result let mut result_seq: Vec<i32> = result
.stdout_str()
.split("\n") .split("\n")
.filter(|x| !x.is_empty()) .filter(|x| !x.is_empty())
.map(|x| x.parse().unwrap()) .map(|x| x.parse().unwrap())
.collect(); .collect();
result_seq.sort(); 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"); assert_eq!(result_seq, input_seq, "Output is not a permutation");
} }
#[test] #[test]
fn test_zero_termination() { fn test_zero_termination() {
let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result = new_ucmd!() let result = new_ucmd!().arg("-z").arg("-i1-10").succeeds();
.arg("-z") result.no_stderr();
.arg("-i1-10")
.succeeds()
.no_stderr()
.stdout
.clone();
let mut result_seq: Vec<i32> = result let mut result_seq: Vec<i32> = result
.stdout_str()
.split("\0") .split("\0")
.filter(|x| !x.is_empty()) .filter(|x| !x.is_empty())
.map(|x| x.parse().unwrap()) .map(|x| x.parse().unwrap())
@ -57,12 +50,11 @@ fn test_echo() {
.map(|x| x.to_string()) .map(|x| x.to_string())
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
) )
.succeeds() .succeeds();
.no_stderr() result.no_stderr();
.stdout
.clone();
let mut result_seq: Vec<i32> = result let mut result_seq: Vec<i32> = result
.stdout_str()
.split("\n") .split("\n")
.filter(|x| !x.is_empty()) .filter(|x| !x.is_empty())
.map(|x| x.parse().unwrap()) .map(|x| x.parse().unwrap())
@ -84,12 +76,11 @@ fn test_head_count() {
let result = new_ucmd!() let result = new_ucmd!()
.args(&["-n", &repeat_limit.to_string()]) .args(&["-n", &repeat_limit.to_string()])
.pipe_in(input.as_bytes()) .pipe_in(input.as_bytes())
.succeeds() .succeeds();
.no_stderr() result.no_stderr();
.stdout
.clone();
let mut result_seq: Vec<i32> = result let mut result_seq: Vec<i32> = result
.stdout_str()
.split("\n") .split("\n")
.filter(|x| !x.is_empty()) .filter(|x| !x.is_empty())
.map(|x| x.parse().unwrap()) .map(|x| x.parse().unwrap())
@ -99,7 +90,7 @@ fn test_head_count() {
assert!( assert!(
result_seq.iter().all(|x| input_seq.contains(x)), result_seq.iter().all(|x| input_seq.contains(x)),
"Output includes element not from input: {}", "Output includes element not from input: {}",
result result.stdout_str()
) )
} }
@ -117,12 +108,11 @@ fn test_repeat() {
.arg("-r") .arg("-r")
.args(&["-n", &repeat_limit.to_string()]) .args(&["-n", &repeat_limit.to_string()])
.pipe_in(input.as_bytes()) .pipe_in(input.as_bytes())
.succeeds() .succeeds();
.no_stderr() result.no_stderr();
.stdout
.clone();
let result_seq: Vec<i32> = result let result_seq: Vec<i32> = result
.stdout_str()
.split("\n") .split("\n")
.filter(|x| !x.is_empty()) .filter(|x| !x.is_empty())
.map(|x| x.parse().unwrap()) .map(|x| x.parse().unwrap())
@ -146,14 +136,11 @@ fn test_repeat() {
fn test_file_input() { fn test_file_input() {
let expected_seq = vec![11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; let expected_seq = vec![11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
let result = new_ucmd!() let result = new_ucmd!().arg("file_input.txt").succeeds();
.arg("file_input.txt") result.no_stderr();
.succeeds()
.no_stderr()
.stdout
.clone();
let mut result_seq: Vec<i32> = result let mut result_seq: Vec<i32> = result
.stdout_str()
.split("\n") .split("\n")
.filter(|x| !x.is_empty()) .filter(|x| !x.is_empty())
.map(|x| x.parse().unwrap()) .map(|x| x.parse().unwrap())
@ -164,52 +151,50 @@ fn test_file_input() {
#[test] #[test]
fn test_shuf_echo_and_input_range_not_allowed() { fn test_shuf_echo_and_input_range_not_allowed() {
let result = new_ucmd!().args(&["-e", "0", "-i", "0-2"]).run(); new_ucmd!()
.args(&["-e", "0", "-i", "0-2"])
assert!(!result.success); .fails()
assert!(result .stderr_contains(
.stderr "The argument '--input-range <LO-HI>' cannot be used with '--echo <ARG>...'",
.contains("The argument '--input-range <LO-HI>' cannot be used with '--echo <ARG>...'")); );
} }
#[test] #[test]
fn test_shuf_input_range_and_file_not_allowed() { fn test_shuf_input_range_and_file_not_allowed() {
let result = new_ucmd!().args(&["-i", "0-9", "file"]).run(); new_ucmd!()
.args(&["-i", "0-9", "file"])
assert!(!result.success); .fails()
assert!(result .stderr_contains("The argument '<file>' cannot be used with '--input-range <LO-HI>'");
.stderr
.contains("The argument '<file>' cannot be used with '--input-range <LO-HI>'"));
} }
#[test] #[test]
fn test_shuf_invalid_input_range_one() { fn test_shuf_invalid_input_range_one() {
let result = new_ucmd!().args(&["-i", "0"]).run(); new_ucmd!()
.args(&["-i", "0"])
assert!(!result.success); .fails()
assert!(result.stderr.contains("invalid input range")); .stderr_contains("invalid input range");
} }
#[test] #[test]
fn test_shuf_invalid_input_range_two() { fn test_shuf_invalid_input_range_two() {
let result = new_ucmd!().args(&["-i", "a-9"]).run(); new_ucmd!()
.args(&["-i", "a-9"])
assert!(!result.success); .fails()
assert!(result.stderr.contains("invalid input range: 'a'")); .stderr_contains("invalid input range: 'a'");
} }
#[test] #[test]
fn test_shuf_invalid_input_range_three() { fn test_shuf_invalid_input_range_three() {
let result = new_ucmd!().args(&["-i", "0-b"]).run(); new_ucmd!()
.args(&["-i", "0-b"])
assert!(!result.success); .fails()
assert!(result.stderr.contains("invalid input range: 'b'")); .stderr_contains("invalid input range: 'b'");
} }
#[test] #[test]
fn test_shuf_invalid_input_line_count() { fn test_shuf_invalid_input_line_count() {
let result = new_ucmd!().args(&["-n", "a"]).run(); new_ucmd!()
.args(&["-n", "a"])
assert!(!result.success); .fails()
assert!(result.stderr.contains("invalid line count: 'a'")); .stderr_contains("invalid line count: 'a'");
} }

View file

@ -2,10 +2,17 @@ use crate::common::util::*;
fn test_helper(file_name: &str, args: &str) { fn test_helper(file_name: &str, args: &str) {
new_ucmd!() new_ucmd!()
.arg(args)
.arg(format!("{}.txt", file_name)) .arg(format!("{}.txt", file_name))
.args(&args.split(' ').collect::<Vec<&str>>())
.succeeds() .succeeds()
.stdout_is_fixture(format!("{}.expected", file_name)); .stdout_is_fixture(format!("{}.expected", file_name));
new_ucmd!()
.arg(format!("{}.txt", file_name))
.arg("--debug")
.args(&args.split(' ').collect::<Vec<&str>>())
.succeeds()
.stdout_is_fixture(format!("{}.expected.debug", file_name));
} }
// FYI, the initialization size of our Line struct is 96 bytes. // FYI, the initialization size of our Line struct is 96 bytes.
@ -65,11 +72,7 @@ fn test_extsort_as64_bailout() {
#[test] #[test]
fn test_multiple_decimals_general() { fn test_multiple_decimals_general() {
new_ucmd!() test_helper("multiple_decimals_general", "-g")
.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] #[test]
@ -99,7 +102,7 @@ fn test_check_zero_terminated_success() {
#[test] #[test]
fn test_random_shuffle_len() { fn test_random_shuffle_len() {
// check whether output is the same length as the input // 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 (at, _ucmd) = at_and_ucmd!();
let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str(); let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str();
let expected = at.read(FILE); let expected = at.read(FILE);
@ -111,7 +114,7 @@ fn test_random_shuffle_len() {
#[test] #[test]
fn test_random_shuffle_contains_all_lines() { fn test_random_shuffle_contains_all_lines() {
// check whether lines of input are all in output // 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 (at, _ucmd) = at_and_ucmd!();
let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str(); let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str();
let expected = at.read(FILE); 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 // 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 // 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. // 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 (at, _ucmd) = at_and_ucmd!();
let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str(); let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str();
let expected = at.read(FILE); 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] #[test]
fn test_exponents_positive_numeric() { fn test_exponents_positive_numeric() {
test_helper("exponents-positive-numeric", "-n"); test_helper("exponents-positive-numeric", "-n");
@ -344,62 +367,32 @@ fn test_numeric_unique_ints2() {
#[test] #[test]
fn test_keys_open_ended() { fn test_keys_open_ended() {
let input = "aa bb cc\ndd aa ff\ngg aa cc\n"; test_helper("keys_open_ended", "-k 2.3");
new_ucmd!()
.args(&["-k", "2.2"])
.pipe_in(input)
.succeeds()
.stdout_only("gg aa cc\ndd aa ff\naa bb cc\n");
} }
#[test] #[test]
fn test_keys_closed_range() { fn test_keys_closed_range() {
let input = "aa bb cc\ndd aa ff\ngg aa cc\n"; test_helper("keys_closed_range", "-k 2.2,2.2");
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] #[test]
fn test_keys_multiple_ranges() { fn test_keys_multiple_ranges() {
let input = "aa bb cc\ndd aa ff\ngg aa cc\n"; test_helper("keys_multiple_ranges", "-k 2,2 -k 3,3");
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] #[test]
fn test_keys_no_field_match() { fn test_keys_no_field_match() {
let input = "aa aa aa aa\naa bb cc\ndd aa ff\n"; test_helper("keys_no_field_match", "-k 4,4");
new_ucmd!()
.args(&["-k", "4,4"])
.pipe_in(input)
.succeeds()
.stdout_only("aa bb cc\ndd aa ff\naa aa aa aa\n");
} }
#[test] #[test]
fn test_keys_no_char_match() { fn test_keys_no_char_match() {
let input = "aaa\nba\nc\n"; test_helper("keys_no_char_match", "-k 1.2");
new_ucmd!()
.args(&["-k", "1.2"])
.pipe_in(input)
.succeeds()
.stdout_only("c\nba\naaa\n");
} }
#[test] #[test]
fn test_keys_custom_separator() { fn test_keys_custom_separator() {
let input = "aaxbbxcc\nddxaaxff\nggxaaxcc\n"; test_helper("keys_custom_separator", "-k 2.2,2.2 -t x");
new_ucmd!()
.args(&["-k", "2.2,2.2", "-t", "x"])
.pipe_in(input)
.succeeds()
.stdout_only("ddxaaxff\nggxaaxcc\naaxbbxcc\n");
} }
#[test] #[test]
@ -426,6 +419,13 @@ fn test_keys_invalid_field_zero() {
.stderr_only("sort: error: field index was 0"); .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] #[test]
fn test_keys_with_options() { fn test_keys_with_options() {
let input = "aa 3 cc\ndd 1 ff\ngg 2 cc\n"; let input = "aa 3 cc\ndd 1 ff\ngg 2 cc\n";
@ -591,3 +591,39 @@ fn test_check_silent() {
.fails() .fails()
.stdout_is(""); .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");
}

View file

@ -337,5 +337,5 @@ fn expected_result(args: &[&str]) -> String {
.env("LANGUAGE", "C") .env("LANGUAGE", "C")
.args(args) .args(args)
.run() .run()
.stdout .stdout_move_str()
} }

View file

@ -52,18 +52,19 @@ fn test_single_non_newline_separator_before() {
#[test] #[test]
fn test_invalid_input() { fn test_invalid_input() {
let (_, mut ucmd) = at_and_ucmd!(); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
ucmd.arg("b") scene
.run() .ucmd()
.stderr .arg("b")
.contains("tac: error: failed to open 'b' for reading"); .fails()
.stderr_contains("failed to open 'b' for reading: No such file or directory");
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("a"); at.mkdir("a");
ucmd.arg("a") scene
.run() .ucmd()
.stderr .arg("a")
.contains("tac: error: failed to read 'a'"); .fails()
.stderr_contains("dir: read error: Invalid argument");
} }

View file

@ -343,3 +343,8 @@ fn test_negative_indexing() {
assert_eq!(positive_lines_index.stdout(), negative_lines_index.stdout()); assert_eq!(positive_lines_index.stdout(), negative_lines_index.stdout());
assert_eq!(positive_bytes_index.stdout(), negative_bytes_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();
}

View file

@ -367,7 +367,58 @@ fn test_touch_mtime_dst_succeeds() {
let target_time = str_to_filetime("%Y%m%d%H%M", "202103140300"); let target_time = str_to_filetime("%Y%m%d%H%M", "202103140300");
let (_, mtime) = get_file_times(&at, file); let (_, mtime) = get_file_times(&at, file);
eprintln!("target_time: {:?}", target_time);
eprintln!("mtime: {:?}", mtime);
assert!(target_time == 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<String> {
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 => (),
}
}

View file

@ -120,19 +120,15 @@ fn test_truncate_with_set1_shorter_than_set2() {
#[test] #[test]
fn missing_args_fails() { fn missing_args_fails() {
let (_, mut ucmd) = at_and_ucmd!(); let (_, mut ucmd) = at_and_ucmd!();
let result = ucmd.run(); ucmd.fails().stderr_contains("missing operand");
assert!(!result.success);
assert!(result.stderr.contains("missing operand"));
} }
#[test] #[test]
fn missing_required_second_arg_fails() { fn missing_required_second_arg_fails() {
let (_, mut ucmd) = at_and_ucmd!(); let (_, mut ucmd) = at_and_ucmd!();
let result = ucmd.args(&["foo"]).run(); ucmd.args(&["foo"])
.fails()
assert!(!result.success); .stderr_contains("missing operand after");
assert!(result.stderr.contains("missing operand after"));
} }
#[test] #[test]

View file

@ -53,6 +53,16 @@ fn test_decrease_file_size() {
assert!(file.seek(SeekFrom::Current(0)).unwrap() == 6); 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] #[test]
fn test_failed() { fn test_failed() {
new_ucmd!().fails(); new_ucmd!().fails();

View file

@ -18,33 +18,35 @@ fn test_sort_self_loop() {
#[test] #[test]
fn test_no_such_file() { fn test_no_such_file() {
let result = new_ucmd!().arg("invalid_file_txt").run(); new_ucmd!()
.arg("invalid_file_txt")
assert_eq!(true, result.stderr.contains("No such file or directory")); .fails()
.stderr_contains("No such file or directory");
} }
#[test] #[test]
fn test_version_flag() { fn test_version_flag() {
let version_short = new_ucmd!().arg("-V").run(); let version_short = new_ucmd!().arg("-V").succeeds();
let version_long = new_ucmd!().arg("--version").run(); 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] #[test]
fn test_help_flag() { fn test_help_flag() {
let help_short = new_ucmd!().arg("-h").run(); let help_short = new_ucmd!().arg("-h").succeeds();
let help_long = new_ucmd!().arg("--help").run(); 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] #[test]
fn test_multiple_arguments() { fn test_multiple_arguments() {
let result = new_ucmd!() new_ucmd!()
.arg("call_graph.txt") .arg("call_graph.txt")
.arg("invalid_file.txt") .arg("invalid_file")
.run(); .fails()
.stderr_contains(
assert_eq!(true, result.stderr.contains("error: Found argument 'invalid_file.txt' which wasn't expected, or isn't valid in this context")) "Found argument 'invalid_file' which wasn't expected, or isn't valid in this context",
);
} }

View file

@ -23,7 +23,7 @@ fn test_heading() {
for opt in vec!["-H"] { for opt in vec!["-H"] {
// allow whitespace variation // allow whitespace variation
// * minor whitespace differences occur between platform built-in outputs; specifically number of TABs between "TIME" and "COMMENT" may be variant // * 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); let expect = expected_result(opt);
println!("actual: {:?}", actual); println!("actual: {:?}", actual);
println!("expect: {:?}", expect); println!("expect: {:?}", expect);
@ -80,5 +80,5 @@ fn expected_result(arg: &str) -> String {
.env("LANGUAGE", "C") .env("LANGUAGE", "C")
.args(&[arg]) .args(&[arg])
.run() .run()
.stdout .stdout_move_str()
} }

View file

@ -2,6 +2,7 @@
#[cfg(not(windows))] #[cfg(not(windows))]
use libc; use libc;
use pretty_assertions::assert_eq;
use std::env; use std::env;
#[cfg(not(windows))] #[cfg(not(windows))]
use std::ffi::CString; use std::ffi::CString;
@ -42,22 +43,6 @@ pub fn is_ci() -> bool {
.eq_ignore_ascii_case("true") .eq_ignore_ascii_case("true")
} }
/// Test if the program is running under WSL
// ref: <https://github.com/microsoft/WSL/issues/4555> @@ <https://archive.is/dP0bz>
// 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 /// Read a test scenario fixture, returning its bytes
fn read_scenario_fixture<S: AsRef<OsStr>>(tmpd: &Option<Rc<TempDir>>, file_rel_path: S) -> Vec<u8> { fn read_scenario_fixture<S: AsRef<OsStr>>(tmpd: &Option<Rc<TempDir>>, file_rel_path: S) -> Vec<u8> {
let tmpdir_path = tmpd.as_ref().unwrap().as_ref().path(); let tmpdir_path = tmpd.as_ref().unwrap().as_ref().path();
@ -74,11 +59,11 @@ pub struct CmdResult {
code: Option<i32>, code: Option<i32>,
/// zero-exit from running the Command? /// zero-exit from running the Command?
/// see [`success`] /// see [`success`]
pub success: bool, success: bool,
/// captured standard output after running the Command /// captured standard output after running the Command
pub stdout: String, stdout: String,
/// captured standard error after running the Command /// captured standard error after running the Command
pub stderr: String, stderr: String,
} }
impl CmdResult { impl CmdResult {
@ -237,7 +222,7 @@ impl CmdResult {
/// like stdout_is(...), but expects the contents of the file at the provided relative path /// like stdout_is(...), but expects the contents of the file at the provided relative path
pub fn stdout_is_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &CmdResult { pub fn stdout_is_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &CmdResult {
let contents = read_scenario_fixture(&self.tmpd, file_rel_path); 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 /// 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 /// Like stdout_is_fixture, but for stderr
pub fn stderr_is_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &CmdResult { pub fn stderr_is_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &CmdResult {
let contents = read_scenario_fixture(&self.tmpd, file_rel_path); 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 /// asserts that
@ -329,14 +314,14 @@ impl CmdResult {
} }
pub fn stdout_matches(&self, regex: &regex::Regex) -> &CmdResult { pub fn stdout_matches(&self, regex: &regex::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()) panic!("Stdout does not match regex:\n{}", self.stdout_str())
} }
self self
} }
pub fn stdout_does_not_match(&self, regex: &regex::Regex) -> &CmdResult { pub fn stdout_does_not_match(&self, regex: &regex::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()) panic!("Stdout matches regex:\n{}", self.stdout_str())
} }
self self
@ -635,7 +620,7 @@ impl TestScenario {
}, },
util_name: String::from(util_name), util_name: String::from(util_name),
fixtures: AtPath::new(tmpd.as_ref().path()), fixtures: AtPath::new(tmpd.as_ref().path()),
tmpd: tmpd, tmpd,
}; };
let mut fixture_path_builder = env::current_dir().unwrap(); let mut fixture_path_builder = env::current_dir().unwrap();
fixture_path_builder.push(TESTS_DIR); fixture_path_builder.push(TESTS_DIR);
@ -696,8 +681,11 @@ pub struct UCommand {
comm_string: String, comm_string: String,
tmpd: Option<Rc<TempDir>>, tmpd: Option<Rc<TempDir>>,
has_run: bool, has_run: bool,
stdin: Option<Vec<u8>>,
ignore_stdin_write_error: bool, ignore_stdin_write_error: bool,
stdin: Option<Stdio>,
stdout: Option<Stdio>,
stderr: Option<Stdio>,
bytes_into_stdin: Option<Vec<u8>>,
} }
impl UCommand { impl UCommand {
@ -726,8 +714,11 @@ impl UCommand {
cmd cmd
}, },
comm_string: String::from(arg.as_ref().to_str().unwrap()), comm_string: String::from(arg.as_ref().to_str().unwrap()),
stdin: None,
ignore_stdin_write_error: false, ignore_stdin_write_error: false,
bytes_into_stdin: None,
stdin: None,
stdout: None,
stderr: None,
} }
} }
@ -738,6 +729,21 @@ impl UCommand {
ucmd ucmd
} }
pub fn set_stdin<T: Into<Stdio>>(&mut self, stdin: T) -> &mut UCommand {
self.stdin = Some(stdin.into());
self
}
pub fn set_stdout<T: Into<Stdio>>(&mut self, stdout: T) -> &mut UCommand {
self.stdout = Some(stdout.into());
self
}
pub fn set_stderr<T: Into<Stdio>>(&mut self, stderr: T) -> &mut UCommand {
self.stderr = Some(stderr.into());
self
}
/// Add a parameter to the invocation. Path arguments are treated relative /// Add a parameter to the invocation. Path arguments are treated relative
/// to the test environment directory. /// to the test environment directory.
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut UCommand { pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut UCommand {
@ -767,10 +773,10 @@ impl UCommand {
/// provides stdinput to feed in to the command when spawned /// provides stdinput to feed in to the command when spawned
pub fn pipe_in<T: Into<Vec<u8>>>(&mut self, input: T) -> &mut UCommand { pub fn pipe_in<T: Into<Vec<u8>>>(&mut self, input: T) -> &mut UCommand {
if self.stdin.is_some() { if self.bytes_into_stdin.is_some() {
panic!("{}", MULTIPLE_STDIN_MEANINGLESS); panic!("{}", MULTIPLE_STDIN_MEANINGLESS);
} }
self.stdin = Some(input.into()); self.bytes_into_stdin = Some(input.into());
self self
} }
@ -784,7 +790,7 @@ impl UCommand {
/// This is typically useful to test non-standard workflows /// This is typically useful to test non-standard workflows
/// like feeding something to a command that does not read it /// like feeding something to a command that does not read it
pub fn ignore_stdin_write_error(&mut self) -> &mut UCommand { 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); panic!("{}", NO_STDIN_MEANINGLESS);
} }
self.ignore_stdin_write_error = true; self.ignore_stdin_write_error = true;
@ -813,13 +819,13 @@ impl UCommand {
log_info("run", &self.comm_string); log_info("run", &self.comm_string);
let mut child = self let mut child = self
.raw .raw
.stdin(Stdio::piped()) .stdin(self.stdin.take().unwrap_or_else(|| Stdio::piped()))
.stdout(Stdio::piped()) .stdout(self.stdout.take().unwrap_or_else(|| Stdio::piped()))
.stderr(Stdio::piped()) .stderr(self.stderr.take().unwrap_or_else(|| Stdio::piped()))
.spawn() .spawn()
.unwrap(); .unwrap();
if let Some(ref input) = self.stdin { if let Some(ref input) = self.bytes_into_stdin {
let write_result = child let write_result = child
.stdin .stdin
.take() .take()

View file

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

View file

@ -0,0 +1,9 @@
bbb
___
___
./bbc
_____
_____
bbd
___
___

View file

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

View file

@ -0,0 +1,19 @@
5.5.5.5
10E
1000EDKLD
10000K78
+100000
+100000
100E6
100E6
10e10e10e10
50e10
50e10

View file

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

View file

@ -0,0 +1,19 @@
100E6
50e10
+100000
10000K78
10E
1000EDKLD
100E6
50e10
+100000
10e10e10e10
5.5.5.5

View file

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

View file

@ -0,0 +1,33 @@
844K
____
____
981K
____
____
11M
___
___
13M
___
___
14M
___
___
16M
___
___
18M
___
___
19M
___
___
20M
___
___
981T
____
____
20P
___
___

View file

@ -0,0 +1,21 @@
aaa
___
___
BBB
___
___
ccc
___
___
DDD
___
___
eee
___
___
FFF
___
___
ggg
___
___

View file

@ -0,0 +1,6 @@
dd aa ff
gg aa cc
aa bb cc
èè éé èè
👩‍🔬 👩‍🔬 👩‍🔬
💣💣 💣💣 💣💣

View file

@ -0,0 +1,18 @@
dd aa ff
_
________
gg aa cc
_
________
aa bb cc
_
________
èè éé èè
_
________
👩‍🔬 👩‍🔬 👩‍🔬
__
______________
💣💣 💣💣 💣💣
__
______________

View file

@ -0,0 +1,6 @@
aa bb cc
dd aa ff
gg aa cc
èè éé èè
💣💣 💣💣 💣💣
👩‍🔬 👩‍🔬 👩‍🔬

View file

@ -0,0 +1,3 @@
ddxaaxff
ggxaaxcc
aaxbbxcc

View file

@ -0,0 +1,9 @@
ddxaaxff
_
________
ggxaaxcc
_
________
aaxbbxcc
_
________

View file

@ -0,0 +1,3 @@
aaxbbxcc
ddxaaxff
ggxaaxcc

View file

@ -0,0 +1,6 @@
gg aa cc
dd aa ff
aa bb cc
èè éé èè
👩‍🔬 👩‍🔬 👩‍🔬
💣💣 💣💣 💣💣

View file

@ -0,0 +1,24 @@
gg aa cc
___
___
________
dd aa ff
___
___
________
aa bb cc
___
___
________
èè éé èè
___
___
________
👩‍🔬 👩‍🔬 👩‍🔬
_____
_____
______________
💣💣 💣💣 💣💣
_____
_____
______________

View file

@ -0,0 +1,6 @@
aa bb cc
dd aa ff
gg aa cc
èè éé èè
💣💣 💣💣 💣💣
👩‍🔬 👩‍🔬 👩‍🔬

View file

@ -0,0 +1,3 @@
c
ba
aaa

View file

@ -0,0 +1,9 @@
c
^ no match for key
_
ba
_
__
aaa
__
___

View file

@ -0,0 +1,3 @@
aaa
ba
c

View file

@ -0,0 +1,6 @@
aa bb cc
dd aa ff
gg aa cc
èè éé èè
👩‍🔬 👩‍🔬 👩‍🔬
💣💣 💣💣 💣💣

View file

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

View file

@ -0,0 +1,6 @@
aa bb cc
dd aa ff
gg aa cc
èè éé èè
💣💣 💣💣 💣💣
👩‍🔬 👩‍🔬 👩‍🔬

View file

@ -0,0 +1,6 @@
gg aa cc
dd aa ff
aa bb cc
èè éé èè
👩‍🔬 👩‍🔬 👩‍🔬
💣💣 💣💣 💣💣

View file

@ -0,0 +1,18 @@
gg aa cc
____
________
dd aa ff
____
________
aa bb cc
____
________
èè éé èè
____
________
👩‍🔬 👩‍🔬 👩‍🔬
_______
______________
💣💣 💣💣 💣💣
_______
______________

View file

@ -0,0 +1,6 @@
aa bb cc
dd aa ff
gg aa cc
èè éé èè
💣💣 💣💣 💣💣
👩‍🔬 👩‍🔬 👩‍🔬

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,12 @@
^ no match for key
JAN
___
apr
___
MAY
___
JUNNNN
___
AUG
___

View file

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

Some files were not shown because too many files have changed in this diff Show more