1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27: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' \
Makefile
# printf doesn't limit the values used in its arg, so this produced ~2GB of output
sed -i '/INT_OFLOW/ D' tests/misc/printf.sh
# Use the system coreutils where the test fails due to error in a util that is not the one being tested
sed -i 's|stat|/usr/bin/stat|' tests/chgrp/basic.sh tests/cp/existing-perm-dir.sh tests/touch/60-seconds.sh tests/misc/sort-compress-proc.sh
sed -i 's|ls -|/usr/bin/ls -|' tests/chgrp/posix-H.sh tests/chown/deref.sh tests/cp/same-file.sh tests/misc/mknod.sh tests/mv/part-symlink.sh tests/du/8gb.sh

94
Cargo.lock generated
View file

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

View file

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

View file

@ -22,6 +22,12 @@ use std::io::{self, Read, Write};
use thiserror::Error;
use uucore::fs::is_stdin_interactive;
/// Linux splice support
#[cfg(any(target_os = "linux", target_os = "android"))]
mod splice;
#[cfg(any(target_os = "linux", target_os = "android"))]
use std::os::unix::io::{AsRawFd, RawFd};
/// Unix domain socket support
#[cfg(unix)]
use std::net::Shutdown;
@ -30,14 +36,6 @@ use std::os::unix::fs::FileTypeExt;
#[cfg(unix)]
use unix_socket::UnixStream;
/// Linux splice support
#[cfg(any(target_os = "linux", target_os = "android"))]
use nix::fcntl::{splice, SpliceFFlags};
#[cfg(any(target_os = "linux", target_os = "android"))]
use nix::unistd::pipe;
#[cfg(any(target_os = "linux", target_os = "android"))]
use std::os::unix::io::{AsRawFd, RawFd};
static NAME: &str = "cat";
static VERSION: &str = env!("CARGO_PKG_VERSION");
static SYNTAX: &str = "[OPTION]... [FILE]...";
@ -395,7 +393,7 @@ fn write_fast<R: Read>(handle: &mut InputHandle<R>) -> CatResult<()> {
{
// If we're on Linux or Android, try to use the splice() system call
// for faster writing. If it works, we're done.
if !write_fast_using_splice(handle, stdout_lock.as_raw_fd())? {
if !splice::write_fast_using_splice(handle, stdout_lock.as_raw_fd())? {
return Ok(());
}
}
@ -411,75 +409,6 @@ fn write_fast<R: Read>(handle: &mut InputHandle<R>) -> CatResult<()> {
Ok(())
}
/// This function is called from `write_fast()` on Linux and Android. The
/// function `splice()` is used to move data between two file descriptors
/// without copying between kernel- and userspace. This results in a large
/// speedup.
///
/// The `bool` in the result value indicates if we need to fall back to normal
/// copying or not. False means we don't have to.
#[cfg(any(target_os = "linux", target_os = "android"))]
#[inline]
fn write_fast_using_splice<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,
/// propagating any errors that might occur.
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,
}
#[derive(Clone, Eq, PartialEq)]
/// Possible arguments for `--reflink`.
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum ReflinkMode {
Always,
Auto,
@ -210,7 +211,6 @@ pub struct Options {
overwrite: OverwriteMode,
parents: bool,
strip_trailing_slashes: bool,
reflink: bool,
reflink_mode: ReflinkMode,
preserve_attributes: Vec<Attribute>,
recursive: bool,
@ -633,12 +633,12 @@ impl Options {
update: matches.is_present(OPT_UPDATE),
verbose: matches.is_present(OPT_VERBOSE),
strip_trailing_slashes: matches.is_present(OPT_STRIP_TRAILING_SLASHES),
reflink: matches.is_present(OPT_REFLINK),
reflink_mode: {
if let Some(reflink) = matches.value_of(OPT_REFLINK) {
match reflink {
"always" => ReflinkMode::Always,
"auto" => ReflinkMode::Auto,
"never" => ReflinkMode::Never,
value => {
return Err(Error::InvalidArgument(format!(
"invalid argument '{}' for \'reflink\'",
@ -1193,47 +1193,20 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> {
Ok(())
}
///Copy the file from `source` to `dest` either using the normal `fs::copy` or the
///`FICLONE` ioctl if --reflink is specified and the filesystem supports it.
/// Copy the file from `source` to `dest` either using the normal `fs::copy` or a
/// copy-on-write scheme if --reflink is specified and the filesystem supports it.
fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> {
if options.reflink {
#[cfg(not(target_os = "linux"))]
return Err("--reflink is only supported on linux".to_string().into());
if options.reflink_mode != ReflinkMode::Never {
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
return Err("--reflink is only supported on linux and macOS"
.to_string()
.into());
#[cfg(target_os = "macos")]
copy_on_write_macos(source, dest, options.reflink_mode)?;
#[cfg(target_os = "linux")]
{
let src_file = File::open(source).unwrap().into_raw_fd();
let dst_file = OpenOptions::new()
.write(true)
.truncate(false)
.create(true)
.open(dest)
.unwrap()
.into_raw_fd();
match options.reflink_mode {
ReflinkMode::Always => unsafe {
let result = ficlone(dst_file, src_file as *const i32);
if result != 0 {
return Err(format!(
"failed to clone {:?} from {:?}: {}",
source,
dest,
std::io::Error::last_os_error()
)
.into());
} else {
return Ok(());
}
},
ReflinkMode::Auto => unsafe {
let result = ficlone(dst_file, src_file as *const i32);
if result != 0 {
fs::copy(source, dest).context(&*context_for(source, dest))?;
}
},
ReflinkMode::Never => {}
}
}
copy_on_write_linux(source, dest, options.reflink_mode)?;
} else if options.no_dereference && fs::symlink_metadata(&source)?.file_type().is_symlink() {
// Here, we will copy the symlink itself (actually, just recreate it)
let link = fs::read_link(&source)?;
@ -1266,6 +1239,101 @@ fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()>
Ok(())
}
/// Copies `source` to `dest` using copy-on-write if possible.
#[cfg(target_os = "linux")]
fn copy_on_write_linux(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyResult<()> {
debug_assert!(mode != ReflinkMode::Never);
let src_file = File::open(source).unwrap().into_raw_fd();
let dst_file = OpenOptions::new()
.write(true)
.truncate(false)
.create(true)
.open(dest)
.unwrap()
.into_raw_fd();
match mode {
ReflinkMode::Always => unsafe {
let result = ficlone(dst_file, src_file as *const i32);
if result != 0 {
return Err(format!(
"failed to clone {:?} from {:?}: {}",
source,
dest,
std::io::Error::last_os_error()
)
.into());
} else {
return Ok(());
}
},
ReflinkMode::Auto => unsafe {
let result = ficlone(dst_file, src_file as *const i32);
if result != 0 {
fs::copy(source, dest).context(&*context_for(source, dest))?;
}
},
ReflinkMode::Never => unreachable!(),
}
Ok(())
}
/// Copies `source` to `dest` using copy-on-write if possible.
#[cfg(target_os = "macos")]
fn copy_on_write_macos(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyResult<()> {
debug_assert!(mode != ReflinkMode::Never);
// Extract paths in a form suitable to be passed to a syscall.
// The unwrap() is safe because they come from the command-line and so contain non nul
// character.
use std::os::unix::ffi::OsStrExt;
let src = CString::new(source.as_os_str().as_bytes()).unwrap();
let dst = CString::new(dest.as_os_str().as_bytes()).unwrap();
// clonefile(2) was introduced in macOS 10.12 so we cannot statically link against it
// for backward compatibility.
let clonefile = CString::new("clonefile").unwrap();
let raw_pfn = unsafe { libc::dlsym(libc::RTLD_NEXT, clonefile.as_ptr()) };
let mut error = 0;
if !raw_pfn.is_null() {
// Call clonefile(2).
// Safety: Casting a C function pointer to a rust function value is one of the few
// blessed uses of `transmute()`.
unsafe {
let pfn: extern "C" fn(
src: *const libc::c_char,
dst: *const libc::c_char,
flags: u32,
) -> libc::c_int = std::mem::transmute(raw_pfn);
error = pfn(src.as_ptr(), dst.as_ptr(), 0);
if std::io::Error::last_os_error().kind() == std::io::ErrorKind::AlreadyExists {
// clonefile(2) fails if the destination exists. Remove it and try again. Do not
// bother to check if removal worked because we're going to try to clone again.
let _ = fs::remove_file(dest);
error = pfn(src.as_ptr(), dst.as_ptr(), 0);
}
}
}
if raw_pfn.is_null() || error != 0 {
// clonefile(2) is not supported or it error'ed out (possibly because the FS does not
// support COW).
match mode {
ReflinkMode::Always => {
return Err(
format!("failed to clone {:?} from {:?}: {}", source, dest, error).into(),
)
}
ReflinkMode::Auto => fs::copy(source, dest).context(&*context_for(source, dest))?,
ReflinkMode::Never => unreachable!(),
};
}
Ok(())
}
/// Generate an error message if `target` is not the correct `target_type`
pub fn verify_target_type(target: &Path, target_type: &TargetType) -> CopyResult<()> {
match (target_type, target.is_dir()) {

View file

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

View file

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

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

View file

@ -7,41 +7,41 @@
// spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf
#[macro_use]
extern crate uucore;
#[cfg(unix)]
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate uucore;
mod quoting_style;
mod version_cmp;
use clap::{App, Arg};
use globset::{self, Glob, GlobSet, GlobSetBuilder};
use lscolors::LsColors;
use number_prefix::NumberPrefix;
use once_cell::unsync::OnceCell;
use quoting_style::{escape_name, QuotingStyle};
#[cfg(unix)]
use std::collections::HashMap;
use std::fs;
use std::fs::{DirEntry, FileType, Metadata};
#[cfg(unix)]
use std::os::unix::fs::FileTypeExt;
#[cfg(any(unix, target_os = "redox"))]
use std::os::unix::fs::MetadataExt;
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::{
cmp::Reverse,
fs::{self, DirEntry, FileType, Metadata},
io::{stdout, BufWriter, Stdout, Write},
path::{Path, PathBuf},
process::exit,
time::{SystemTime, UNIX_EPOCH},
};
#[cfg(unix)]
use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{cmp::Reverse, process::exit};
use std::{
collections::HashMap,
os::unix::fs::{FileTypeExt, MetadataExt},
time::Duration,
};
use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
use time::{strftime, Timespec};
#[cfg(unix)]
use unicode_width::UnicodeWidthStr;
#[cfg(unix)]
use uucore::libc::{mode_t, S_ISGID, S_ISUID, S_ISVTX, S_IWOTH, S_IXGRP, S_IXOTH, S_IXUSR};
use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR};
static VERSION: &str = env!("CARGO_PKG_VERSION");
static ABOUT: &str = "
@ -54,30 +54,6 @@ fn get_usage() -> String {
format!("{0} [OPTION]... [FILE]...", executable!())
}
#[cfg(unix)]
static DEFAULT_COLORS: &str = "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:";
#[cfg(unix)]
lazy_static! {
static ref LS_COLORS: String =
std::env::var("LS_COLORS").unwrap_or_else(|_| DEFAULT_COLORS.to_string());
static ref COLOR_MAP: HashMap<&'static str, &'static str> = {
let codes = LS_COLORS.split(':');
let mut map = HashMap::new();
for c in codes {
let p: Vec<_> = c.splitn(2, '=').collect();
if p.len() == 2 {
map.insert(p[0], p[1]);
}
}
map
};
static ref RESET_CODE: &'static str = COLOR_MAP.get("rs").unwrap_or(&"0");
static ref LEFT_CODE: &'static str = COLOR_MAP.get("lc").unwrap_or(&"\x1b[");
static ref RIGHT_CODE: &'static str = COLOR_MAP.get("rc").unwrap_or(&"m");
static ref END_CODE: &'static str = COLOR_MAP.get("ec").unwrap_or(&"");
}
pub mod options {
pub mod format {
pub static ONELINE: &str = "1";
@ -113,10 +89,8 @@ pub mod options {
pub static C: &str = "quote-name";
}
pub static QUOTING_STYLE: &str = "quoting-style";
pub mod indicator_style {
pub static NONE: &str = "none";
pub static SLASH: &str = "slash";
pub static SLASH: &str = "p";
pub static FILE_TYPE: &str = "file-type";
pub static CLASSIFY: &str = "classify";
}
@ -135,9 +109,6 @@ pub mod options {
pub static TIME: &str = "time";
pub static IGNORE_BACKUPS: &str = "ignore-backups";
pub static DIRECTORY: &str = "directory";
pub static CLASSIFY: &str = "classify";
pub static FILE_TYPE: &str = "file-type";
pub static SLASH: &str = "p";
pub static INODE: &str = "inode";
pub static REVERSE: &str = "reverse";
pub static RECURSIVE: &str = "recursive";
@ -212,8 +183,7 @@ struct Config {
time: Time,
#[cfg(unix)]
inode: bool,
#[cfg(unix)]
color: bool,
color: Option<LsColors>,
long: LongFormat,
width: Option<u16>,
quoting_style: QuotingStyle,
@ -337,8 +307,7 @@ impl Config {
Time::Modification
};
#[cfg(unix)]
let color = match options.value_of(options::COLOR) {
let needs_color = match options.value_of(options::COLOR) {
None => options.is_present(options::COLOR),
Some(val) => match val {
"" | "always" | "yes" | "force" => true,
@ -347,6 +316,12 @@ impl Config {
},
};
let color = if needs_color {
Some(LsColors::from_env().unwrap_or_default())
} else {
None
};
let size_format = if options.is_present(options::size::HUMAN_READABLE) {
SizeFormat::Binary
} else if options.is_present(options::size::SI) {
@ -448,19 +423,11 @@ impl Config {
"slash" => IndicatorStyle::Slash,
&_ => IndicatorStyle::None,
}
} else if options.is_present(options::indicator_style::NONE) {
IndicatorStyle::None
} else if options.is_present(options::indicator_style::CLASSIFY)
|| options.is_present(options::CLASSIFY)
{
} else if options.is_present(options::indicator_style::CLASSIFY) {
IndicatorStyle::Classify
} else if options.is_present(options::indicator_style::SLASH)
|| options.is_present(options::SLASH)
{
} else if options.is_present(options::indicator_style::SLASH) {
IndicatorStyle::Slash
} else if options.is_present(options::indicator_style::FILE_TYPE)
|| options.is_present(options::FILE_TYPE)
{
} else if options.is_present(options::indicator_style::FILE_TYPE) {
IndicatorStyle::FileType
} else {
IndicatorStyle::None
@ -492,6 +459,10 @@ impl Config {
}
}
if files == Files::Normal {
ignore_patterns.add(Glob::new(".*").unwrap());
}
let ignore_patterns = ignore_patterns.build().unwrap();
let dereference = if options.is_present(options::dereference::ALL) {
@ -520,7 +491,6 @@ impl Config {
size_format,
directory: options.is_present(options::DIRECTORY),
time,
#[cfg(unix)]
color,
#[cfg(unix)]
inode: options.is_present(options::INODE),
@ -983,45 +953,45 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.takes_value(true)
.possible_values(&["none", "slash", "file-type", "classify"])
.overrides_with_all(&[
options::FILE_TYPE,
options::SLASH,
options::CLASSIFY,
options::indicator_style::FILE_TYPE,
options::indicator_style::SLASH,
options::indicator_style::CLASSIFY,
options::INDICATOR_STYLE,
]))
.arg(
Arg::with_name(options::CLASSIFY)
Arg::with_name(options::indicator_style::CLASSIFY)
.short("F")
.long(options::CLASSIFY)
.long(options::indicator_style::CLASSIFY)
.help("Append a character to each file name indicating the file type. Also, for \
regular files that are executable, append '*'. The file type indicators are \
'/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \
'>' for doors, and nothing for regular files.")
.overrides_with_all(&[
options::FILE_TYPE,
options::SLASH,
options::CLASSIFY,
options::indicator_style::FILE_TYPE,
options::indicator_style::SLASH,
options::indicator_style::CLASSIFY,
options::INDICATOR_STYLE,
])
)
.arg(
Arg::with_name(options::FILE_TYPE)
.long(options::FILE_TYPE)
Arg::with_name(options::indicator_style::FILE_TYPE)
.long(options::indicator_style::FILE_TYPE)
.help("Same as --classify, but do not append '*'")
.overrides_with_all(&[
options::FILE_TYPE,
options::SLASH,
options::CLASSIFY,
options::indicator_style::FILE_TYPE,
options::indicator_style::SLASH,
options::indicator_style::CLASSIFY,
options::INDICATOR_STYLE,
]))
.arg(
Arg::with_name(options::SLASH)
.short(options::SLASH)
Arg::with_name(options::indicator_style::SLASH)
.short(options::indicator_style::SLASH)
.help("Append / indicator to directories."
)
.overrides_with_all(&[
options::FILE_TYPE,
options::SLASH,
options::CLASSIFY,
options::indicator_style::FILE_TYPE,
options::indicator_style::SLASH,
options::indicator_style::CLASSIFY,
options::INDICATOR_STYLE,
]))
@ -1038,12 +1008,82 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
list(locs, Config::from(matches))
}
/// Represents a Path along with it's associated data
/// Any data that will be reused several times makes sense to be added to this structure
/// Caching data here helps eliminate redundant syscalls to fetch same information
struct PathData {
// Result<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 {
let number_of_locs = locs.len();
let mut files = Vec::<PathBuf>::new();
let mut dirs = Vec::<PathBuf>::new();
let mut files = Vec::<PathData>::new();
let mut dirs = Vec::<PathData>::new();
let mut has_failed = false;
let mut out = BufWriter::new(stdout());
for loc in locs {
let p = PathBuf::from(&loc);
if !p.exists() {
@ -1054,38 +1094,30 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
continue;
}
let show_dir_contents = if !config.directory {
match config.dereference {
Dereference::None => {
if let Ok(md) = p.symlink_metadata() {
md.is_dir()
} else {
show_error!("'{}': {}", &loc, "No such file or directory");
has_failed = true;
continue;
}
}
_ => p.is_dir(),
}
let path_data = PathData::new(p, None, &config, true);
let show_dir_contents = if let Some(ft) = path_data.file_type() {
!config.directory && ft.is_dir()
} else {
has_failed = true;
false
};
if show_dir_contents {
dirs.push(p);
dirs.push(path_data);
} else {
files.push(p);
files.push(path_data);
}
}
sort_entries(&mut files, &config);
display_items(&files, None, &config, true);
display_items(&files, None, &config, &mut out);
sort_entries(&mut dirs, &config);
for dir in dirs {
if number_of_locs > 1 {
println!("\n{}:", dir.to_string_lossy());
let _ = writeln!(out, "\n{}:", dir.p_buf.display());
}
enter_directory(&dir, &config);
enter_directory(&dir, &config, &mut out);
}
if has_failed {
1
@ -1094,22 +1126,21 @@ fn list(locs: Vec<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 {
Sort::Time => entries.sort_by_key(|k| {
Reverse(
get_metadata(k, false)
.ok()
k.md()
.and_then(|md| get_system_time(&md, config))
.unwrap_or(UNIX_EPOCH),
)
}),
Sort::Size => {
entries.sort_by_key(|k| Reverse(get_metadata(k, false).map(|md| md.len()).unwrap_or(0)))
entries.sort_by_key(|k| Reverse(k.md().as_ref().map(|md| md.len()).unwrap_or(0)))
}
// The default sort in GNU ls is case insensitive
Sort::Name => entries.sort_by_key(|k| k.to_string_lossy().to_lowercase()),
Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(a, b)),
Sort::Name => entries.sort_by_cached_key(|k| k.file_name.to_lowercase()),
Sort::Version => entries.sort_by(|k, j| version_cmp::version_cmp(&k.p_buf, &j.p_buf)),
Sort::None => {}
}
@ -1122,48 +1153,53 @@ fn sort_entries(entries: &mut Vec<PathBuf>, config: &Config) {
fn is_hidden(file_path: &DirEntry) -> bool {
let metadata = fs::metadata(file_path.path()).unwrap();
let attr = metadata.file_attributes();
((attr & 0x2) > 0) || file_path.file_name().to_string_lossy().starts_with('.')
}
#[cfg(unix)]
fn is_hidden(file_path: &DirEntry) -> bool {
file_path.file_name().to_string_lossy().starts_with('.')
(attr & 0x2) > 0
}
fn should_display(entry: &DirEntry, config: &Config) -> bool {
let ffi_name = entry.file_name();
if config.files == Files::Normal && is_hidden(entry) {
return false;
// For unix, the hidden files are already included in the ignore pattern
#[cfg(windows)]
{
if config.files == Files::Normal && is_hidden(entry) {
return false;
}
}
if config.ignore_patterns.is_match(&ffi_name) {
return false;
}
true
!config.ignore_patterns.is_match(&ffi_name)
}
fn enter_directory(dir: &Path, config: &Config) {
let mut entries: Vec<_> = safe_unwrap!(fs::read_dir(dir).and_then(Iterator::collect));
entries.retain(|e| should_display(e, config));
let mut entries: Vec<_> = entries.iter().map(DirEntry::path).collect();
sort_entries(&mut entries, config);
if config.files == Files::All {
let mut display_entries = entries.clone();
display_entries.insert(0, dir.join(".."));
display_entries.insert(0, dir.join("."));
display_items(&display_entries, Some(dir), config, false);
fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter<Stdout>) {
let mut entries: Vec<_> = if config.files == Files::All {
vec![
PathData::new(dir.p_buf.join("."), None, config, false),
PathData::new(dir.p_buf.join(".."), None, config, false),
]
} else {
display_items(&entries, Some(dir), config, false);
}
vec![]
};
let mut temp: Vec<_> = safe_unwrap!(fs::read_dir(&dir.p_buf))
.map(|res| safe_unwrap!(res))
.filter(|e| should_display(e, config))
.map(|e| PathData::new(DirEntry::path(&e), Some(e.file_type()), config, false))
.collect();
sort_entries(&mut temp, config);
entries.append(&mut temp);
display_items(&entries, Some(&dir.p_buf), config, out);
if config.recursive {
for e in entries.iter().filter(|p| p.is_dir()) {
println!("\n{}:", e.to_string_lossy());
enter_directory(&e, config);
for e in entries
.iter()
.skip(if config.files == Files::All { 2 } else { 0 })
.filter(|p| p.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
{
let _ = writeln!(out, "\n{}:", e.p_buf.display());
enter_directory(&e, config, out);
}
}
}
@ -1176,8 +1212,8 @@ fn get_metadata(entry: &Path, dereference: bool) -> std::io::Result<Metadata> {
}
}
fn display_dir_entry_size(entry: &Path, config: &Config) -> (usize, usize) {
if let Ok(md) = get_metadata(entry, false) {
fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize) {
if let Some(md) = entry.md() {
(
display_symlink_count(&md).len(),
display_file_size(&md, config).len(),
@ -1191,7 +1227,12 @@ fn pad_left(string: String, count: usize) -> String {
format!("{:>width$}", string, width = count)
}
fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config, command_line: bool) {
fn display_items(
items: &[PathData],
strip: Option<&Path>,
config: &Config,
out: &mut BufWriter<Stdout>,
) {
if config.format == Format::Long {
let (mut max_links, mut max_size) = (1, 1);
for item in items {
@ -1200,58 +1241,59 @@ fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config, comma
max_size = size.max(max_size);
}
for item in items {
display_item_long(item, strip, max_links, max_size, config, command_line);
display_item_long(item, strip, max_links, max_size, config, out);
}
} else {
let names = items.iter().filter_map(|i| {
let md = get_metadata(i, false);
match md {
Err(e) => {
let filename = get_file_name(i, strip);
show_error!("'{}': {}", filename, e);
None
}
Ok(md) => Some(display_file_name(&i, strip, &md, config)),
}
});
let names = items
.iter()
.filter_map(|i| display_file_name(&i, strip, config));
match (&config.format, config.width) {
(Format::Columns, Some(width)) => display_grid(names, width, Direction::TopToBottom),
(Format::Across, Some(width)) => display_grid(names, width, Direction::LeftToRight),
(Format::Columns, Some(width)) => {
display_grid(names, width, Direction::TopToBottom, out)
}
(Format::Across, Some(width)) => {
display_grid(names, width, Direction::LeftToRight, out)
}
(Format::Commas, width_opt) => {
let term_width = width_opt.unwrap_or(1);
let mut current_col = 0;
let mut names = names;
if let Some(name) = names.next() {
print!("{}", name.contents);
let _ = write!(out, "{}", name.contents);
current_col = name.width as u16 + 2;
}
for name in names {
let name_width = name.width as u16;
if current_col + name_width + 1 > term_width {
current_col = name_width + 2;
print!(",\n{}", name.contents);
let _ = write!(out, ",\n{}", name.contents);
} else {
current_col += name_width + 2;
print!(", {}", name.contents);
let _ = write!(out, ", {}", name.contents);
}
}
// Current col is never zero again if names have been printed.
// So we print a newline.
if current_col > 0 {
println!();
let _ = writeln!(out,);
}
}
_ => {
for name in names {
println!("{}", name.contents);
let _ = writeln!(out, "{}", name.contents);
}
}
}
}
}
fn display_grid(names: impl Iterator<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 {
filling: Filling::Spaces(2),
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) {
Some(output) => print!("{}", output),
Some(output) => {
let _ = write!(out, "{}", output);
}
// Width is too small for the grid, so we fit it in one column
None => print!("{}", grid.fit_into_columns(1)),
None => {
let _ = write!(out, "{}", grid.fit_into_columns(1));
}
}
}
use uucore::fs::display_permissions;
fn display_item_long(
item: &Path,
item: &PathData,
strip: Option<&Path>,
max_links: usize,
max_size: usize,
config: &Config,
command_line: bool,
out: &mut BufWriter<Stdout>,
) {
let dereference = match &config.dereference {
Dereference::All => true,
Dereference::Args => command_line,
Dereference::DirArgs => {
if command_line {
if let Ok(md) = item.metadata() {
md.is_dir()
} else {
false
}
} else {
false
}
}
Dereference::None => false,
};
let md = match get_metadata(item, dereference) {
Err(e) => {
let filename = get_file_name(&item, strip);
show_error!("{}: {}", filename, e);
let md = match item.md() {
None => {
let filename = get_file_name(&item.p_buf, strip);
show_error!("could not show file: {}", filename);
return;
}
Ok(md) => md,
Some(md) => md,
};
#[cfg(unix)]
{
if config.inode {
print!("{} ", get_inode(&md));
let _ = write!(out, "{} ", get_inode(&md));
}
}
print!(
let _ = write!(
out,
"{}{} {}",
display_file_type(md.file_type()),
display_permissions(&md),
@ -1319,24 +1349,28 @@ fn display_item_long(
);
if config.long.owner {
print!(" {}", display_uname(&md, config));
let _ = write!(out, " {}", display_uname(&md, config));
}
if config.long.group {
print!(" {}", display_group(&md, config));
let _ = write!(out, " {}", display_group(&md, config));
}
// Author is only different from owner on GNU/Hurd, so we reuse
// the owner, since GNU/Hurd is not currently supported by Rust.
if config.long.author {
print!(" {}", display_uname(&md, config));
let _ = write!(out, " {}", display_uname(&md, config));
}
println!(
let _ = writeln!(
out,
" {} {} {}",
pad_left(display_file_size(&md, config), max_size),
display_date(&md, config),
display_file_name(&item, strip, &md, config).contents,
// unwrap is fine because it fails when metadata is not available
// but we already know that it is because it's checked at the
// start of the function.
display_file_name(&item, strip, config).unwrap().contents,
);
}
@ -1348,23 +1382,51 @@ fn get_inode(metadata: &Metadata) -> String {
// Currently getpwuid is `linux` target only. If it's broken out into
// a posix-compliant attribute this can be updated...
#[cfg(unix)]
use std::sync::Mutex;
#[cfg(unix)]
use uucore::entries;
#[cfg(unix)]
fn cached_uid2usr(uid: u32) -> String {
lazy_static! {
static ref UID_CACHE: Mutex<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)]
fn display_uname(metadata: &Metadata, config: &Config) -> String {
if config.long.numeric_uid_gid {
metadata.uid().to_string()
} else {
entries::uid2usr(metadata.uid()).unwrap_or_else(|_| metadata.uid().to_string())
cached_uid2usr(metadata.uid())
}
}
#[cfg(unix)]
fn cached_gid2grp(gid: u32) -> String {
lazy_static! {
static ref GID_CACHE: Mutex<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)]
fn display_group(metadata: &Metadata, config: &Config) -> String {
if config.long.numeric_uid_gid {
metadata.gid().to_string()
} else {
entries::gid2grp(metadata.gid()).unwrap_or_else(|_| metadata.gid().to_string())
cached_gid2grp(metadata.gid())
}
}
@ -1374,7 +1436,6 @@ fn display_uname(_metadata: &Metadata, _config: &Config) -> String {
}
#[cfg(not(unix))]
#[allow(unused_variables)]
fn display_group(_metadata: &Metadata, _config: &Config) -> String {
"somegroup".to_string()
}
@ -1449,13 +1510,13 @@ fn display_file_size(metadata: &Metadata, config: &Config) -> String {
}
}
fn display_file_type(file_type: FileType) -> String {
fn display_file_type(file_type: FileType) -> char {
if file_type.is_dir() {
"d".to_string()
'd'
} else if file_type.is_symlink() {
"l".to_string()
'l'
} else {
"-".to_string()
'-'
}
}
@ -1470,140 +1531,58 @@ fn get_file_name(name: &Path, strip: Option<&Path>) -> String {
name.to_string_lossy().into_owned()
}
#[cfg(not(unix))]
fn display_file_name(
path: &Path,
strip: Option<&Path>,
metadata: &Metadata,
config: &Config,
) -> Cell {
let mut name = escape_name(get_file_name(path, strip), &config.quoting_style);
let file_type = metadata.file_type();
match config.indicator_style {
IndicatorStyle::Classify | IndicatorStyle::FileType => {
if file_type.is_dir() {
name.push('/');
}
if file_type.is_symlink() {
name.push('@');
}
}
IndicatorStyle::Slash => {
if file_type.is_dir() {
name.push('/');
}
}
_ => (),
};
if config.format == Format::Long && metadata.file_type().is_symlink() {
if let Ok(target) = path.read_link() {
// We don't bother updating width here because it's not used for long listings
let target_name = target.to_string_lossy().to_string();
name.push_str(" -> ");
name.push_str(&target_name);
}
}
name.into()
#[cfg(unix)]
fn file_is_executable(md: &Metadata) -> bool {
// Mode always returns u32, but the flags might not be, based on the platform
// e.g. linux has u32, mac has u16.
// S_IXUSR -> user has execute permission
// S_IXGRP -> group has execute persmission
// S_IXOTH -> other users have execute permission
md.mode() & ((S_IXUSR | S_IXGRP | S_IXOTH) as u32) != 0
}
#[cfg(unix)]
fn color_name(name: String, typ: &str) -> String {
let mut typ = typ;
if !COLOR_MAP.contains_key(typ) {
if typ == "or" {
typ = "ln";
} else if typ == "mi" {
typ = "fi";
}
};
if let Some(code) = COLOR_MAP.get(typ) {
format!(
"{}{}{}{}{}{}{}{}",
*LEFT_CODE, code, *RIGHT_CODE, name, *END_CODE, *LEFT_CODE, *RESET_CODE, *RIGHT_CODE,
)
#[allow(clippy::clippy::collapsible_else_if)]
fn classify_file(path: &PathData) -> Option<char> {
let file_type = path.file_type()?;
if file_type.is_dir() {
Some('/')
} else if file_type.is_symlink() {
Some('@')
} else {
name
}
}
#[cfg(unix)]
macro_rules! has {
($mode:expr, $perm:expr) => {
$mode & ($perm as mode_t) != 0
};
}
#[cfg(unix)]
#[allow(clippy::cognitive_complexity)]
fn display_file_name(
path: &Path,
strip: Option<&Path>,
metadata: &Metadata,
config: &Config,
) -> Cell {
let mut name = escape_name(get_file_name(path, strip), &config.quoting_style);
if config.format != Format::Long && config.inode {
name = get_inode(metadata) + " " + &name;
}
let mut width = UnicodeWidthStr::width(&*name);
let ext;
if config.color || config.indicator_style != IndicatorStyle::None {
let file_type = metadata.file_type();
let (code, sym) = if file_type.is_dir() {
("di", Some('/'))
} else if file_type.is_symlink() {
if path.exists() {
("ln", Some('@'))
} else {
("or", Some('@'))
}
} else if file_type.is_socket() {
("so", Some('='))
} else if file_type.is_fifo() {
("pi", Some('|'))
} else if file_type.is_block_device() {
("bd", None)
} else if file_type.is_char_device() {
("cd", None)
} else if file_type.is_file() {
let mode = metadata.mode() as mode_t;
let sym = if has!(mode, S_IXUSR | S_IXGRP | S_IXOTH) {
#[cfg(unix)]
{
if file_type.is_socket() {
Some('=')
} else if file_type.is_fifo() {
Some('|')
} else if file_type.is_file() && file_is_executable(path.md()?) {
Some('*')
} else {
None
};
if has!(mode, S_ISUID) {
("su", sym)
} else if has!(mode, S_ISGID) {
("sg", sym)
} else if has!(mode, S_ISVTX) && has!(mode, S_IWOTH) {
("tw", sym)
} else if has!(mode, S_ISVTX) {
("st", sym)
} else if has!(mode, S_IWOTH) {
("ow", sym)
} else if has!(mode, S_IXUSR | S_IXGRP | S_IXOTH) {
("ex", sym)
} else if metadata.nlink() > 1 {
("mh", sym)
} else if let Some(e) = path.extension() {
ext = format!("*.{}", e.to_string_lossy());
(ext.as_str(), None)
} else {
("fi", None)
}
} else {
("", None)
};
if config.color {
name = color_name(name, code);
}
#[cfg(not(unix))]
None
}
}
fn display_file_name(path: &PathData, strip: Option<&Path>, config: &Config) -> Option<Cell> {
let mut name = escape_name(get_file_name(&path.p_buf, strip), &config.quoting_style);
#[cfg(unix)]
{
if config.format != Format::Long && config.inode {
name = get_inode(path.md()?) + " " + &name;
}
}
if let Some(ls_colors) = &config.color {
name = color_name(&ls_colors, &path.p_buf, name, path.md()?);
}
if config.indicator_style != IndicatorStyle::None {
let sym = classify_file(path);
let char_opt = match config.indicator_style {
IndicatorStyle::Classify => sym,
@ -1626,23 +1605,23 @@ fn display_file_name(
if let Some(c) = char_opt {
name.push(c);
width += 1;
}
}
if config.format == Format::Long && metadata.file_type().is_symlink() {
if let Ok(target) = path.read_link() {
// We don't bother updating width here because it's not used for long listings
let code = if target.exists() { "fi" } else { "mi" };
let target_name = color_name(target.to_string_lossy().to_string(), code);
if config.format == Format::Long && path.file_type()?.is_symlink() {
if let Ok(target) = path.p_buf.read_link() {
name.push_str(" -> ");
name.push_str(&target_name);
name.push_str(&target.to_string_lossy());
}
}
Cell {
contents: name,
width,
Some(name.into())
}
fn color_name(ls_colors: &LsColors, path: &Path, name: String, md: &Metadata) -> String {
match ls_colors.style_for_path_with_metadata(path, Some(&md)) {
Some(style) => style.to_ansi_term_style().paint(name).to_string(),
None => name,
}
}

View file

@ -1,6 +1,6 @@
use std::char::from_digit;
const SPECIAL_SHELL_CHARS: &str = "~`#$&*()\\|[]{};'\"<>?! ";
const SPECIAL_SHELL_CHARS: &str = "~`#$&*()|[]{};\\'\"<>?! ";
pub(super) enum QuotingStyle {
Shell {
@ -27,12 +27,10 @@ pub(super) enum Quotes {
// This implementation is heavily inspired by the std::char::EscapeDefault implementation
// in the Rust standard library. This custom implementation is needed because the
// characters \a, \b, \e, \f & \v are not recognized by Rust.
#[derive(Clone, Debug)]
struct EscapedChar {
state: EscapeState,
}
#[derive(Clone, Debug)]
enum EscapeState {
Done,
Char(char),
@ -41,14 +39,12 @@ enum EscapeState {
Octal(EscapeOctal),
}
#[derive(Clone, Debug)]
struct EscapeOctal {
c: char,
state: EscapeOctalState,
idx: usize,
}
#[derive(Clone, Debug)]
enum EscapeOctalState {
Done,
Backslash,
@ -135,7 +131,6 @@ impl EscapedChar {
'\x0B' => Backslash('v'),
'\x0C' => Backslash('f'),
'\r' => Backslash('r'),
'\\' => Backslash('\\'),
'\x00'..='\x1F' | '\x7F' => Octal(EscapeOctal::from(c)),
'\'' => match quotes {
Quotes::Single => Backslash('\''),
@ -511,6 +506,23 @@ mod tests {
],
);
// A control character followed by a special shell character
check_names(
"one\n&two",
vec![
("one?&two", "literal"),
("one\n&two", "literal-show"),
("one\\n&two", "escape"),
("\"one\\n&two\"", "c"),
("'one?&two'", "shell"),
("'one\n&two'", "shell-show"),
("'one?&two'", "shell-always"),
("'one\n&two'", "shell-always-show"),
("'one'$'\\n''&two'", "shell-escape"),
("'one'$'\\n''&two'", "shell-escape-always"),
],
);
// The first 16 control characters. NUL is also included, even though it is of
// no importance for file names.
check_names(
@ -627,4 +639,22 @@ mod tests {
],
);
}
#[test]
fn test_backslash() {
// Escaped in C-style, but not in Shell-style escaping
check_names(
"one\\two",
vec![
("one\\two", "literal"),
("one\\two", "literal-show"),
("one\\\\two", "escape"),
("\"one\\\\two\"", "c"),
("'one\\two'", "shell"),
("\'one\\two\'", "shell-always"),
("'one\\two'", "shell-escape"),
("'one\\two'", "shell-escape-always"),
],
);
}
}

View file

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

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
// to console when a newline is passed.
#[allow(unused_must_use)]
pub fn flush_char(c: char) {
print!("{}", c);
stdout().flush();
let _ = stdout().flush();
}
#[allow(unused_must_use)]
pub fn flush_str(s: &str) {
print!("{}", s);
stdout().flush();
let _ = stdout().flush();
}
#[allow(unused_must_use)]
pub fn flush_bytes(bslice: &[u8]) {
stdout().write(bslice);
stdout().flush();
let _ = stdout().write(bslice);
let _ = stdout().flush();
}

View file

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

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

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)
// on the todo list is to have a trait for get_primitive that is implemented by each float formatter and can override a default. when that happens we can take the parts of get_primitive_dec specific to dec and spin them out to their own functions that can be overridden.
#[allow(unused_variables)]
#[allow(unused_assignments)]
fn get_primitive_hex(
inprefix: &InPrefix,
str_in: &str,
analysis: &FloatAnalysis,
last_dec_place: usize,
_str_in: &str,
_analysis: &FloatAnalysis,
_last_dec_place: usize,
capitalized: bool,
) -> FormatPrimitive {
let prefix = Some(String::from(if inprefix.sign == -1 { "-0x" } else { "0x" }));
// assign the digits before and after the decimal points
// to separate slices. If no digits after decimal point,
// assign 0
let (mut first_segment_raw, second_segment_raw) = match analysis.decimal_pos {
Some(pos) => (&str_in[..pos], &str_in[pos + 1..]),
None => (str_in, "0"),
};
if first_segment_raw.is_empty() {
first_segment_raw = "0";
}
// convert to string, hexifying if input is in dec.
// let (first_segment, second_segment) =
// match inprefix.radix_in {
// Base::Ten => {
// (to_hex(first_segment_raw, true),
// to_hex(second_segment_raw, false))
// }
// _ => {
// (String::from(first_segment_raw),
// String::from(second_segment_raw))
// }
// };
//
//
// f.pre_decimal = Some(first_segment);
// f.post_decimal = Some(second_segment);
//
// TODO actual conversion, make sure to get back mantissa.
// for hex to hex, it's really just a matter of moving the
// decimal point and calculating the mantissa by its initial

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,7 +23,8 @@ clap = "2.33"
fnv = "1.0.7"
itertools = "0.10.0"
semver = "0.9.0"
smallvec = { version = "1.6.1", features = ["serde"] }
smallvec = "1.6.1"
unicode-width = "0.1.8"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
tempdir = "0.3.7"

View file

@ -138,7 +138,15 @@ impl NumInfo {
sign: if had_digit { sign } else { Sign::Positive },
exponent: 0,
},
0..0,
if had_digit {
// In this case there were only zeroes.
// For debug output to work properly, we have to claim to match the end of the number.
num.len()..num.len()
} else {
// This was no number at all.
// For debug output to work properly, we have to claim to match the start of the number.
0..0
},
)
}
}

View file

@ -29,7 +29,6 @@ use rayon::prelude::*;
use semver::Version;
use serde::{Deserializer, Deserialize, Serialize};
use smallvec::SmallVec;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::BinaryHeap;
use std::env;
@ -38,9 +37,10 @@ use std::fs::File;
use std::hash::{Hash, Hasher};
use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Lines, Read, Write};
use std::mem::replace;
use std::ops::{Range, RangeInclusive};
use std::path::{Path, PathBuf};
use uucore::fs::is_stdin_interactive; // for Iterator::dedup();
use std::ops::Range;
use std::path::Path;
use unicode_width::UnicodeWidthStr;
use uucore::fs::is_stdin_interactive; // for Iterator::dedup()
static NAME: &str = "sort";
static ABOUT: &str = "Display sorted concatenation of all FILE(s).";
@ -66,6 +66,7 @@ static OPT_DICTIONARY_ORDER: &str = "dictionary-order";
static OPT_MERGE: &str = "merge";
static OPT_CHECK: &str = "check";
static OPT_CHECK_SILENT: &str = "check-silent";
static OPT_DEBUG: &str = "debug";
static OPT_IGNORE_CASE: &str = "ignore-case";
static OPT_IGNORE_BLANKS: &str = "ignore-blanks";
static OPT_IGNORE_NONPRINTING: &str = "ignore-nonprinting";
@ -94,7 +95,7 @@ static DEFAULT_TMPDIR: &str = r"/tmp";
// 16GB buffer for Vec<Line> before we dump to disk, never used
static DEFAULT_BUF_SIZE: usize = 16000000000;
#[derive(Eq, Ord, PartialEq, PartialOrd, Clone)]
#[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy)]
enum SortMode {
Numeric,
HumanNumeric,
@ -106,6 +107,7 @@ enum SortMode {
#[derive(Clone)]
struct GlobalSettings {
mode: SortMode,
debug: bool,
ignore_blanks: bool,
ignore_case: bool,
dictionary_order: bool,
@ -154,6 +156,7 @@ impl Default for GlobalSettings {
fn default() -> GlobalSettings {
GlobalSettings {
mode: SortMode::Default,
debug: false,
ignore_blanks: false,
ignore_case: false,
dictionary_order: false,
@ -190,7 +193,7 @@ struct KeySettings {
impl From<&GlobalSettings> for KeySettings {
fn from(settings: &GlobalSettings) -> Self {
Self {
mode: settings.mode.clone(),
mode: settings.mode,
ignore_blanks: settings.ignore_blanks,
ignore_case: settings.ignore_case,
ignore_non_printing: settings.ignore_non_printing,
@ -236,7 +239,7 @@ impl SelectionRange {
#[derive(Debug, Serialize, Deserialize, Clone)]
enum NumCache {
#[serde(deserialize_with="bailout_parse_f64")]
AsF64(f64),
AsF64(GeneralF64ParseResult),
WithInfo(NumInfo),
None,
}
@ -250,7 +253,7 @@ fn bailout_parse_f64<'de, D>(d: D) -> Result<f64, D::Error> where D: Deserialize
}
impl NumCache {
fn as_f64(&self) -> f64 {
fn as_f64(&self) -> GeneralF64ParseResult {
match self {
NumCache::AsF64(n) => *n,
_ => unreachable!(),
@ -309,19 +312,14 @@ impl Line {
.selectors
.iter()
.map(|selector| {
let mut range =
if let Some(range) = selector.get_field_selection(&line, fields.as_deref()) {
if let Some(transformed) =
transform(&line[range.to_owned()], &selector.settings)
{
SelectionRange::String(transformed)
} else {
SelectionRange::ByIndex(range.start().to_owned()..range.end() + 1)
}
} else {
// If there is no match, match the empty string.
SelectionRange::ByIndex(0..0)
};
let range = selector.get_selection(&line, fields.as_deref());
let mut range = if let Some(transformed) =
transform(&line[range.to_owned()], &selector.settings)
{
SelectionRange::String(transformed)
} else {
SelectionRange::ByIndex(range)
};
let num_cache = if selector.settings.mode == SortMode::Numeric
|| selector.settings.mode == SortMode::HumanNumeric
{
@ -336,7 +334,8 @@ impl Line {
range.shorten(num_range);
NumCache::WithInfo(info)
} else if selector.settings.mode == SortMode::GeneralNumeric && settings.buffer_size == DEFAULT_BUF_SIZE {
NumCache::AsF64(permissive_f64_parse(get_leading_gen(range.get_str(&line))))
let str = range.get_str(&line);
NumCache::AsF64(general_f64_parse(&str[get_leading_gen(str)]))
} else {
NumCache::None
};
@ -345,6 +344,129 @@ impl Line {
.collect();
Self { line, selections }
}
/// Writes indicators for the selections this line matched. The original line content is NOT expected
/// to be already printed.
fn print_debug(
&self,
settings: &GlobalSettings,
writer: &mut dyn Write,
) -> std::io::Result<()> {
// We do not consider this function performance critical, as debug output is only useful for small files,
// which are not a performance problem in any case. Therefore there aren't any special performance
// optimizations here.
let line = self.line.replace('\t', ">");
writeln!(writer, "{}", line)?;
let fields = tokenize(&self.line, settings.separator);
for selector in settings.selectors.iter() {
let mut selection = selector.get_selection(&self.line, Some(&fields));
match selector.settings.mode {
SortMode::Numeric | SortMode::HumanNumeric => {
// find out which range is used for numeric comparisons
let (_, num_range) = NumInfo::parse(
&self.line[selection.clone()],
NumInfoParseSettings {
accept_si_units: selector.settings.mode == SortMode::HumanNumeric,
thousands_separator: Some(THOUSANDS_SEP),
decimal_pt: Some(DECIMAL_PT),
},
);
let initial_selection = selection.clone();
// Shorten selection to num_range.
selection.start += num_range.start;
selection.end = selection.start + num_range.len();
// include a trailing si unit
if selector.settings.mode == SortMode::HumanNumeric
&& self.line[selection.end..initial_selection.end]
.starts_with(&['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'][..])
{
selection.end += 1;
}
// include leading zeroes, a leading minus or a leading decimal point
while self.line[initial_selection.start..selection.start]
.ends_with(&['-', '0', '.'][..])
{
selection.start -= 1;
}
}
SortMode::GeneralNumeric => {
let initial_selection = &self.line[selection.clone()];
let leading = get_leading_gen(initial_selection);
// Shorten selection to leading.
selection.start += leading.start;
selection.end = selection.start + leading.len();
}
SortMode::Month => {
let initial_selection = &self.line[selection.clone()];
let month = if month_parse(initial_selection) == Month::Unknown {
// We failed to parse a month, which is equivalent to matching nothing.
0..0
} else {
// We parsed a month. Match the three first non-whitespace characters, which must be the month we parsed.
let mut chars = initial_selection
.char_indices()
.skip_while(|(_, c)| c.is_whitespace());
chars.next().unwrap().0
..chars.nth(2).map_or(initial_selection.len(), |(idx, _)| idx)
};
// Shorten selection to month.
selection.start += month.start;
selection.end = selection.start + month.len();
}
_ => {}
}
write!(
writer,
"{}",
" ".repeat(UnicodeWidthStr::width(&line[..selection.start]))
)?;
// TODO: Once our minimum supported rust version is at least 1.47, use selection.is_empty() instead.
#[allow(clippy::len_zero)]
{
if selection.len() == 0 {
writeln!(writer, "^ no match for key")?;
} else {
writeln!(
writer,
"{}",
"_".repeat(UnicodeWidthStr::width(&line[selection]))
)?;
}
}
}
if !(settings.random
|| settings.stable
|| settings.unique
|| !(settings.dictionary_order
|| settings.ignore_blanks
|| settings.ignore_case
|| settings.ignore_non_printing
|| settings.mode != SortMode::Default))
{
// A last resort comparator is in use, underline the whole line.
if self.line.is_empty() {
writeln!(writer, "^ no match for key")?;
} else {
writeln!(
writer,
"{}",
"_".repeat(UnicodeWidthStr::width(line.as_str()))
)?;
}
}
Ok(())
}
}
/// Transform this line. Returns None if there's no need to transform.
@ -407,20 +529,18 @@ fn tokenize_default(line: &str) -> Vec<Field> {
/// Split between separators. These separators are not included in fields.
fn tokenize_with_separator(line: &str, separator: char) -> Vec<Field> {
let mut tokens = vec![0..0];
let mut previous_was_separator = false;
for (idx, char) in line.char_indices() {
if previous_was_separator {
tokens.push(idx..0);
}
if char == separator {
tokens.last_mut().unwrap().end = idx;
previous_was_separator = true;
} else {
previous_was_separator = false;
}
let mut tokens = vec![];
let separator_indices =
line.char_indices()
.filter_map(|(i, c)| if c == separator { Some(i) } else { None });
let mut start = 0;
for sep_idx in separator_indices {
tokens.push(start..sep_idx);
start = sep_idx + 1;
}
if start < line.len() {
tokens.push(start..line.len());
}
tokens.last_mut().unwrap().end = line.len();
tokens
}
@ -464,6 +584,28 @@ impl KeyPosition {
crash!(1, "invalid option for key: `{}`", c)
}
}
// All numeric sorts and month sort conflict with dictionary_order and ignore_non_printing.
// Instad of reporting an error, let them overwrite each other.
// FIXME: This should only override if the overridden flag is a global flag.
// If conflicting flags are attached to the key, GNU sort crashes and we should probably too.
match option {
'h' | 'n' | 'g' | 'M' => {
settings.dictionary_order = false;
settings.ignore_non_printing = false;
}
'd' | 'i' => {
settings.mode = match settings.mode {
SortMode::Numeric
| SortMode::HumanNumeric
| SortMode::GeneralNumeric
| SortMode::Month => SortMode::Default,
// Only SortMode::Default and SortMode::Version work with dictionary_order and ignore_non_printing
m @ SortMode::Default | m @ SortMode::Version => m,
}
}
_ => {}
}
}
// Strip away option characters from the original value so we can parse it later
*value_with_options = &value_with_options[..options_start];
@ -506,13 +648,16 @@ impl FieldSelector {
/// Look up the slice that corresponds to this selector for the given line.
/// If needs_fields returned false, fields may be None.
fn get_field_selection<'a>(
&self,
line: &'a str,
tokens: Option<&[Field]>,
) -> Option<RangeInclusive<usize>> {
enum ResolutionErr {
fn get_selection<'a>(&self, line: &'a str, tokens: Option<&[Field]>) -> Range<usize> {
enum Resolution {
// The start index of the resolved character, inclusive
StartOfChar(usize),
// The end index of the resolved character, exclusive.
// This is only returned if the character index is 0.
EndOfChar(usize),
// The resolved character would be in front of the first character
TooLow,
// The resolved character would be after the last character
TooHigh,
}
@ -521,15 +666,15 @@ impl FieldSelector {
line: &str,
tokens: Option<&[Field]>,
position: &KeyPosition,
) -> Result<usize, ResolutionErr> {
) -> Resolution {
if tokens.map_or(false, |fields| fields.len() < position.field) {
Err(ResolutionErr::TooHigh)
Resolution::TooHigh
} else if position.char == 0 {
let end = tokens.unwrap()[position.field - 1].end;
if end == 0 {
Err(ResolutionErr::TooLow)
Resolution::TooLow
} else {
Ok(end - 1)
Resolution::EndOfChar(end)
}
} else {
let mut idx = if position.field == 1 {
@ -538,38 +683,52 @@ impl FieldSelector {
0
} else {
tokens.unwrap()[position.field - 1].start
} + position.char
- 1;
};
idx += line[idx..]
.char_indices()
.nth(position.char - 1)
.map_or(line.len(), |(idx, _)| idx);
if idx >= line.len() {
Err(ResolutionErr::TooHigh)
Resolution::TooHigh
} else {
if position.ignore_blanks {
if let Some(not_whitespace) =
line[idx..].chars().position(|c| !c.is_whitespace())
if let Some((not_whitespace, _)) =
line[idx..].char_indices().find(|(_, c)| !c.is_whitespace())
{
idx += not_whitespace;
} else {
return Err(ResolutionErr::TooHigh);
return Resolution::TooHigh;
}
}
Ok(idx)
Resolution::StartOfChar(idx)
}
}
}
if let Ok(from) = resolve_index(line, tokens, &self.from) {
let to = self.to.as_ref().map(|to| resolve_index(line, tokens, &to));
match to {
Some(Ok(to)) => Some(from..=to),
// If `to` was not given or the match would be after the end of the line,
// match everything until the end of the line.
None | Some(Err(ResolutionErr::TooHigh)) => Some(from..=line.len() - 1),
// If `to` is before the start of the line, report no match.
// This can happen if the line starts with a separator.
Some(Err(ResolutionErr::TooLow)) => None,
match resolve_index(line, tokens, &self.from) {
Resolution::StartOfChar(from) => {
let to = self.to.as_ref().map(|to| resolve_index(line, tokens, &to));
match to {
Some(Resolution::StartOfChar(mut to)) => {
to += line[to..].chars().next().map_or(1, |c| c.len_utf8());
from..to
}
Some(Resolution::EndOfChar(to)) => from..to,
// If `to` was not given or the match would be after the end of the line,
// match everything until the end of the line.
None | Some(Resolution::TooHigh) => from..line.len(),
// If `to` is before the start of the line, report no match.
// This can happen if the line starts with a separator.
Some(Resolution::TooLow) => 0..0,
}
}
} else {
None
Resolution::TooLow | Resolution::EndOfChar(_) => {
unreachable!("This should only happen if the field start index is 0, but that should already have caused an error.")
}
// While for comparisons it's only important that this is an empty slice,
// to produce accurate debug output we need to match an empty slice at the end of the line.
Resolution::TooHigh => line.len()..line.len(),
}
}
}
@ -597,7 +756,7 @@ impl<'a> PartialOrd for MergeableFile<'a> {
impl<'a> PartialEq for MergeableFile<'a> {
fn eq(&self, other: &MergeableFile) -> bool {
Ordering::Equal == compare_by(&self.current_line, &other.current_line, self.settings)
Ordering::Equal == self.cmp(other)
}
}
@ -628,8 +787,8 @@ impl<'a> FileMerger<'a> {
}
impl<'a> Iterator for FileMerger<'a> {
type Item = String;
fn next(&mut self) -> Option<String> {
type Item = Line;
fn next(&mut self) -> Option<Line> {
match self.heap.pop() {
Some(mut current) => {
match current.lines.next() {
@ -639,12 +798,12 @@ impl<'a> Iterator for FileMerger<'a> {
Line::new(next_line, &self.settings),
);
self.heap.push(current);
Some(ret.line)
Some(ret)
}
_ => {
// Don't put it back in the heap (it's empty/erroring)
// but its first line is still valid.
Some(current.current_line.line)
Some(current.current_line)
}
}
}
@ -708,7 +867,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
Arg::with_name(OPT_DICTIONARY_ORDER)
.short("d")
.long(OPT_DICTIONARY_ORDER)
.help("consider only blanks and alphanumeric characters"),
.help("consider only blanks and alphanumeric characters")
.conflicts_with_all(&[OPT_NUMERIC_SORT, OPT_GENERAL_NUMERIC_SORT, OPT_HUMAN_NUMERIC_SORT, OPT_MONTH_SORT]),
)
.arg(
Arg::with_name(OPT_MERGE)
@ -736,9 +896,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
)
.arg(
Arg::with_name(OPT_IGNORE_NONPRINTING)
.short("-i")
.short("i")
.long(OPT_IGNORE_NONPRINTING)
.help("ignore nonprinting characters"),
.help("ignore nonprinting characters")
.conflicts_with_all(&[OPT_NUMERIC_SORT, OPT_GENERAL_NUMERIC_SORT, OPT_HUMAN_NUMERIC_SORT, OPT_MONTH_SORT]),
)
.arg(
Arg::with_name(OPT_IGNORE_BLANKS)
@ -829,9 +990,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.value_name("NUL_FILES")
.multiple(true),
)
.arg(
Arg::with_name(OPT_DEBUG)
.long(OPT_DEBUG)
.help("underline the parts of the line that are actually used for sorting"),
)
.arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true))
.get_matches_from(args);
settings.debug = matches.is_present(OPT_DEBUG);
// check whether user specified a zero terminated list of files for input, otherwise read files from args
let mut files: Vec<String> = if matches.is_present(OPT_FILES0_FROM) {
let files0_from: Vec<String> = matches
@ -964,6 +1132,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
1,
&mut key_settings,
);
if from.char == 0 {
crash!(
1,
"invalid character index 0 in `{}` for the start position of a field",
key
)
}
let to = from_to
.next()
.map(|to| KeyPosition::parse(to, 0, &mut key_settings));
@ -1042,7 +1217,10 @@ fn exec(files: Vec<String>, settings: GlobalSettings) -> i32 {
if settings.merge {
if settings.unique {
print_sorted(file_merger.dedup(), &settings)
print_sorted(
file_merger.dedup_by(|a, b| compare_by(a, b, settings) == Ordering::Equal),
&settings,
)
} else {
print_sorted(file_merger, &settings)
}
@ -1050,12 +1228,11 @@ fn exec(files: Vec<String>, settings: GlobalSettings) -> i32 {
print_sorted(
lines
.into_iter()
.dedup_by(|a, b| compare_by(a, b, &settings) == Ordering::Equal)
.map(|line| line.line),
.dedup_by(|a, b| compare_by(a, b, settings) == Ordering::Equal),
&settings,
)
} else {
print_sorted(lines.into_iter().map(|line| line.line), &settings)
print_sorted(lines.into_iter(), &settings)
}
0
@ -1163,107 +1340,80 @@ fn default_compare(a: &str, b: &str) -> Ordering {
a.cmp(b)
}
/// This function does the initial detection of numeric lines for FP compares.
// Lines starting with a number or positive or negative sign.
// It also strips the string of any thing that could never
// be a number for the purposes of any type of numeric comparison.
#[inline(always)]
fn leading_num_common(a: &str) -> &str {
let mut s = "";
// check whether char is numeric, whitespace or decimal point or thousand separator
for (idx, c) in a.char_indices() {
if !c.is_numeric()
&& !c.is_whitespace()
&& !c.eq(&THOUSANDS_SEP)
&& !c.eq(&DECIMAL_PT)
// check for e notation
&& !c.eq(&'e')
&& !c.eq(&'E')
// check whether first char is + or -
&& !a.chars().next().unwrap_or('\0').eq(&POSITIVE)
&& !a.chars().next().unwrap_or('\0').eq(&NEGATIVE)
{
// Strip string of non-numeric trailing chars
s = &a[..idx];
break;
}
// If line is not a number line, return the line as is
s = &a;
}
s
}
/// This function cleans up the initial comparison done by leading_num_common for a general numeric compare.
// This function cleans up the initial comparison done by leading_num_common for a general numeric compare.
// In contrast to numeric compare, GNU general numeric/FP sort *should* recognize positive signs and
// scientific notation, so we strip those lines only after the end of the following numeric string.
// For example, 5e10KFD would be 5e10 or 5x10^10 and +10000HFKJFK would become 10000.
fn get_leading_gen(a: &str) -> &str {
// Make this iter peekable to see if next char is numeric
let raw_leading_num = leading_num_common(a);
let mut p_iter = raw_leading_num.chars().peekable();
let mut result = "";
// Cleanup raw stripped strings
while let Some(c) = p_iter.next() {
let next_char_numeric = p_iter.peek().unwrap_or(&'\0').is_numeric();
// Only general numeric recognizes e notation and, see block below, the '+' sign
// Only GNU (non-general) numeric recognize thousands seperators, takes only leading #
if (c.eq(&'e') || c.eq(&'E')) && !next_char_numeric || c.eq(&THOUSANDS_SEP) {
result = a.split(c).next().unwrap_or("");
break;
// If positive sign and next char is not numeric, split at postive sign at keep trailing numbers
// There is a more elegant way to do this in Rust 1.45, std::str::strip_prefix
} else if c.eq(&POSITIVE) && !next_char_numeric {
result = a.trim().trim_start_matches('+');
break;
fn get_leading_gen(input: &str) -> Range<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());
}
// If no further processing needed to be done, return the line as-is to be sorted
result = a;
}
result
// Make this iter peekable to see if next char is numeric
let mut char_indices = trimmed.char_indices().peekable();
let first = char_indices.peek();
if first.map_or(false, |&(_, c)| c == NEGATIVE || c == POSITIVE) {
char_indices.next();
}
let mut had_e_notation = false;
let mut had_decimal_pt = false;
while let Some((idx, c)) = char_indices.next() {
if c.is_ascii_digit() {
continue;
}
if c == DECIMAL_PT && !had_decimal_pt {
had_decimal_pt = true;
continue;
}
let next_char_numeric = char_indices
.peek()
.map_or(false, |(_, c)| c.is_ascii_digit());
if (c == 'e' || c == 'E') && !had_e_notation && next_char_numeric {
had_e_notation = true;
continue;
}
return leading_whitespace_len..(leading_whitespace_len + idx);
}
leading_whitespace_len..input.len()
}
#[inline(always)]
fn remove_trailing_dec<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> {
let input = input.into();
if let Some(s) = input.find(DECIMAL_PT) {
let (leading, trailing) = input.split_at(s);
let output = [leading, ".", trailing.replace(DECIMAL_PT, "").as_str()].concat();
Cow::Owned(output)
} else {
input
}
#[derive(Copy, Clone, PartialEq, PartialOrd)]
enum GeneralF64ParseResult {
Invalid,
NaN,
NegInfinity,
Number(f64),
Infinity,
}
/// Parse the beginning string into an f64, returning -inf instead of NaN on errors.
#[inline(always)]
fn permissive_f64_parse(a: &str) -> f64 {
// GNU sort treats "NaN" as non-number in numeric, so it needs special care.
// *Keep this trim before parse* despite what POSIX may say about -b and -n
// because GNU and BSD both seem to require it to match their behavior
//
// Remove any trailing decimals, ie 4568..890... becomes 4568.890
// Then, we trim whitespace and parse
match remove_trailing_dec(a).trim().parse::<f64>() {
Ok(val) if val.is_nan() => std::f64::NEG_INFINITY,
Ok(val) => val,
Err(_) => std::f64::NEG_INFINITY,
fn general_f64_parse(a: &str) -> GeneralF64ParseResult {
// The actual behavior here relies on Rust's implementation of parsing floating points.
// For example "nan", "inf" (ignoring the case) and "infinity" are only parsed to floats starting from 1.53.
// TODO: Once our minimum supported Rust version is 1.53 or above, we should add tests for those cases.
match a.parse::<f64>() {
Ok(a) if a.is_nan() => GeneralF64ParseResult::NaN,
Ok(a) if a == std::f64::NEG_INFINITY => GeneralF64ParseResult::NegInfinity,
Ok(a) if a == std::f64::INFINITY => GeneralF64ParseResult::Infinity,
Ok(a) => GeneralF64ParseResult::Number(a),
Err(_) => GeneralF64ParseResult::Invalid,
}
}
/// Compares two floats, with errors and non-numerics assumed to be -inf.
/// Stops coercing at the first non-numeric char.
/// We explicitly need to convert to f64 in this case.
fn general_numeric_compare(a: f64, b: f64) -> Ordering {
#![allow(clippy::comparison_chain)]
// f64::cmp isn't implemented (due to NaN issues); implement directly instead
if a > b {
Ordering::Greater
} else if a < b {
Ordering::Less
} else {
Ordering::Equal
}
fn general_numeric_compare(a: GeneralF64ParseResult, b: GeneralF64ParseResult) -> Ordering {
a.partial_cmp(&b).unwrap()
}
fn get_rand_string() -> String {
@ -1290,7 +1440,7 @@ fn random_shuffle(a: &str, b: &str, x: String) -> Ordering {
da.cmp(&db)
}
#[derive(Eq, Ord, PartialEq, PartialOrd)]
#[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy)]
enum Month {
Unknown,
January,
@ -1309,30 +1459,32 @@ enum Month {
/// Parse the beginning string into a Month, returning Month::Unknown on errors.
fn month_parse(line: &str) -> Month {
// GNU splits at any 3 letter match "JUNNNN" is JUN
let pattern = if line.trim().len().ge(&3) {
// Split a 3 and get first element of tuple ".0"
line.trim().split_at(3).0
} else {
""
};
let line = line.trim();
let result = match pattern.to_uppercase().as_ref() {
"JAN" => Month::January,
"FEB" => Month::February,
"MAR" => Month::March,
"APR" => Month::April,
"MAY" => Month::May,
"JUN" => Month::June,
"JUL" => Month::July,
"AUG" => Month::August,
"SEP" => Month::September,
"OCT" => Month::October,
"NOV" => Month::November,
"DEC" => Month::December,
_ => Month::Unknown,
};
result
const MONTHS: [(&str, Month); 12] = [
("JAN", Month::January),
("FEB", Month::February),
("MAR", Month::March),
("APR", Month::April),
("MAY", Month::May),
("JUN", Month::June),
("JUL", Month::July),
("AUG", Month::August),
("SEP", Month::September),
("OCT", Month::October),
("NOV", Month::November),
("DEC", Month::December),
];
for (month_str, month) in &MONTHS {
if line.is_char_boundary(month_str.len())
&& line[..month_str.len()].eq_ignore_ascii_case(month_str)
{
return *month;
}
}
Month::Unknown
}
fn month_compare(a: &str, b: &str) -> Ordering {
@ -1390,7 +1542,7 @@ fn remove_nonprinting_chars(s: &str) -> String {
.collect::<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 {
Some(ref filename) => match File::create(Path::new(&filename)) {
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>,
};
if settings.zero_terminated {
if settings.zero_terminated && !settings.debug {
for line in iter {
crash_if_err!(1, file.write_all(line.as_bytes()));
crash_if_err!(1, file.write_all(line.line.as_bytes()));
crash_if_err!(1, file.write_all("\0".as_bytes()));
}
} else {
for line in iter {
crash_if_err!(1, file.write_all(line.as_bytes()));
crash_if_err!(1, file.write_all("\n".as_bytes()));
if !settings.debug {
crash_if_err!(1, file.write_all(line.line.as_bytes()));
crash_if_err!(1, file.write_all("\n".as_bytes()));
} else {
crash_if_err!(1, line.print_debug(settings, &mut file));
}
}
}
crash_if_err!(1, file.flush());
@ -1504,4 +1660,14 @@ mod tests {
vec![0..0, 1..1, 2..2, 3..9, 10..18,]
);
}
#[test]
fn test_tokenize_fields_trailing_custom_separator() {
let line = "a";
assert_eq!(tokenize(line, Some('a')), vec![0..0]);
let line = "aa";
assert_eq!(tokenize(line, Some('a')), vec![0..0, 1..1]);
let line = "..a..a";
assert_eq!(tokenize(line, Some('a')), vec![0..2, 3..5]);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,8 @@
// spell-checker:ignore (vars) fperm srwx
use libc::{mode_t, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR};
pub fn parse_numeric(fperm: u32, mut mode: &str) -> Result<u32, String> {
let (op, pos) = parse_op(mode, Some('='))?;
mode = mode[pos..].trim().trim_start_matches('0');
@ -129,3 +131,41 @@ fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) {
}
(srwx, pos)
}
pub fn parse_mode(mode: Option<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
pub use crate::mods::coreopts;
pub use crate::mods::os;
pub use crate::mods::panic;
pub use crate::mods::ranges;

View file

@ -1,5 +1,6 @@
// mods ~ cross-platforms modules (core/bundler file)
pub mod coreopts;
pub mod os;
pub mod panic;
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::*;
#[cfg(unix)]
use std::fs::OpenOptions;
#[cfg(unix)]
use std::io::Read;
#[test]
@ -26,7 +29,7 @@ fn test_no_options() {
}
#[test]
#[cfg(unix)]
#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
fn test_no_options_big_input() {
for &n in &[
0,
@ -54,7 +57,6 @@ fn test_no_options_big_input() {
#[test]
#[cfg(unix)]
fn test_fifo_symlink() {
use std::fs::OpenOptions;
use std::io::Write;
use std::thread;
@ -85,6 +87,74 @@ fn test_fifo_symlink() {
thread.join().unwrap();
}
#[test]
#[cfg(unix)]
fn test_piped_to_regular_file() {
use std::fs::read_to_string;
for &append in &[true, false] {
let s = TestScenario::new(util_name!());
let file_path = s.fixtures.plus("file.txt");
{
let file = OpenOptions::new()
.create_new(true)
.write(true)
.append(append)
.open(&file_path)
.unwrap();
s.ucmd()
.set_stdout(file)
.pipe_in_fixture("alpha.txt")
.succeeds();
}
let contents = read_to_string(&file_path).unwrap();
assert_eq!(contents, "abcde\nfghij\nklmno\npqrst\nuvwxyz\n");
}
}
#[test]
#[cfg(unix)]
fn test_piped_to_dev_null() {
for &append in &[true, false] {
let s = TestScenario::new(util_name!());
{
let dev_null = OpenOptions::new()
.write(true)
.append(append)
.open("/dev/null")
.unwrap();
s.ucmd()
.set_stdout(dev_null)
.pipe_in_fixture("alpha.txt")
.succeeds();
}
}
}
#[test]
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))]
fn test_piped_to_dev_full() {
for &append in &[true, false] {
let s = TestScenario::new(util_name!());
{
let dev_full = OpenOptions::new()
.write(true)
.append(append)
.open("/dev/full")
.unwrap();
s.ucmd()
.set_stdout(dev_full)
.pipe_in_fixture("alpha.txt")
.fails()
.stderr_contains(&"No space left on device".to_owned());
}
}
}
#[test]
fn test_directory() {
let s = TestScenario::new(util_name!());
@ -327,6 +397,7 @@ fn test_dev_full_show_all() {
#[cfg(unix)]
fn test_domain_socket() {
use std::io::prelude::*;
use std::sync::{Arc, Barrier};
use std::thread;
use tempdir::TempDir;
use unix_socket::UnixListener;
@ -335,17 +406,23 @@ fn test_domain_socket() {
let socket_path = dir.path().join("sock");
let listener = UnixListener::bind(&socket_path).expect("failed to create socket");
// use a barrier to ensure we don't run cat before the listener is setup
let barrier = Arc::new(Barrier::new(2));
let barrier2 = Arc::clone(&barrier);
let thread = thread::spawn(move || {
let mut stream = listener.accept().expect("failed to accept connection").0;
barrier2.wait();
stream
.write_all(b"a\tb")
.expect("failed to write test data");
});
new_ucmd!()
.args(&[socket_path])
.succeeds()
.stdout_only("a\tb");
let child = new_ucmd!().args(&[socket_path]).run_no_wait();
barrier.wait();
let stdout = &child.wait_with_output().unwrap().stdout.clone();
let output = String::from_utf8_lossy(&stdout);
assert_eq!("a\tb", output);
thread.join().unwrap();
}

View file

@ -104,7 +104,7 @@ fn test_reference() {
// skip for root or MS-WSL
// * MS-WSL is bugged (as of 2019-12-25), allowing non-root accounts su-level privileges for `chgrp`
// * for MS-WSL, succeeds and stdout == 'group of /etc retained as root'
if !(get_effective_gid() == 0 || is_wsl()) {
if !(get_effective_gid() == 0 || uucore::os::is_wsl_1()) {
new_ucmd!()
.arg("-v")
.arg("--reference=/etc/passwd")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ fn test_normal() {
for (key, value) in env::vars() {
println!("{}: {}", key, value);
}
if (is_ci() || is_wsl()) && result.stderr_str().contains("error: no login name") {
if (is_ci() || uucore::os::is_wsl_1()) && result.stderr_str().contains("error: no login name") {
// ToDO: investigate WSL failure
// In the CI, some server are failing to return logname.
// As seems to be a configuration issue, ignoring it

View file

@ -103,6 +103,20 @@ fn test_ls_width() {
.succeeds()
.stdout_only("test-width-1\ntest-width-2\ntest-width-3\ntest-width-4\n");
}
scene
.ucmd()
.arg("-w=bad")
.fails()
.stderr_contains("invalid line width");
for option in &["-w 1a", "-w=1a", "--width=1a", "--width 1a"] {
scene
.ucmd()
.args(&option.split(" ").collect::<Vec<_>>())
.fails()
.stderr_only("ls: error: invalid line width: 1a");
}
}
#[test]
@ -436,6 +450,39 @@ fn test_ls_deref() {
assert!(!re.is_match(result.stdout_str().trim()));
}
#[test]
fn test_ls_sort_none() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("test-3");
at.touch("test-1");
at.touch("test-2");
// Order is not specified so we just check that it doesn't
// give any errors.
scene.ucmd().arg("--sort=none").succeeds();
scene.ucmd().arg("-U").succeeds();
}
#[test]
fn test_ls_sort_name() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("test-3");
at.touch("test-1");
at.touch("test-2");
let sep = if cfg!(unix) { "\n" } else { " " };
scene
.ucmd()
.arg("--sort=name")
.succeeds()
.stdout_is(["test-1", "test-2", "test-3\n"].join(sep));
}
#[test]
fn test_ls_order_size() {
let scene = TestScenario::new(util_name!());
@ -464,6 +511,18 @@ fn test_ls_order_size() {
result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n");
#[cfg(windows)]
result.stdout_only("test-1 test-2 test-3 test-4\n");
let result = scene.ucmd().arg("--sort=size").succeeds();
#[cfg(not(windows))]
result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n");
#[cfg(windows)]
result.stdout_only("test-4 test-3 test-2 test-1\n");
let result = scene.ucmd().arg("--sort=size").arg("-r").succeeds();
#[cfg(not(windows))]
result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n");
#[cfg(windows)]
result.stdout_only("test-1 test-2 test-3 test-4\n");
}
#[test]
@ -472,13 +531,16 @@ fn test_ls_long_ctime() {
let at = &scene.fixtures;
at.touch("test-long-ctime-1");
let result = scene.ucmd().arg("-lc").succeeds();
// Should show the time on Unix, but question marks on windows.
#[cfg(unix)]
result.stdout_contains(":");
#[cfg(not(unix))]
result.stdout_contains("???");
for arg in &["-c", "--time=ctime", "--time=status"] {
let result = scene.ucmd().arg("-l").arg(arg).succeeds();
// Should show the time on Unix, but question marks on windows.
#[cfg(unix)]
result.stdout_contains(":");
#[cfg(not(unix))]
result.stdout_contains("???");
}
}
#[test]
@ -519,32 +581,46 @@ fn test_ls_order_time() {
#[cfg(windows)]
result.stdout_only("test-4 test-3 test-2 test-1\n");
let result = scene.ucmd().arg("--sort=time").succeeds();
#[cfg(not(windows))]
result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n");
#[cfg(windows)]
result.stdout_only("test-4 test-3 test-2 test-1\n");
let result = scene.ucmd().arg("-tr").succeeds();
#[cfg(not(windows))]
result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n");
#[cfg(windows)]
result.stdout_only("test-1 test-2 test-3 test-4\n");
let result = scene.ucmd().arg("--sort=time").arg("-r").succeeds();
#[cfg(not(windows))]
result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n");
#[cfg(windows)]
result.stdout_only("test-1 test-2 test-3 test-4\n");
// 3 was accessed last in the read
// So the order should be 2 3 4 1
let result = scene.ucmd().arg("-tu").succeeds();
let file3_access = at.open("test-3").metadata().unwrap().accessed().unwrap();
let file4_access = at.open("test-4").metadata().unwrap().accessed().unwrap();
for arg in &["-u", "--time=atime", "--time=access", "--time=use"] {
let result = scene.ucmd().arg("-t").arg(arg).succeeds();
let file3_access = at.open("test-3").metadata().unwrap().accessed().unwrap();
let file4_access = at.open("test-4").metadata().unwrap().accessed().unwrap();
// It seems to be dependent on the platform whether the access time is actually set
if file3_access > file4_access {
if cfg!(not(windows)) {
result.stdout_only("test-3\ntest-4\ntest-2\ntest-1\n");
// It seems to be dependent on the platform whether the access time is actually set
if file3_access > file4_access {
if cfg!(not(windows)) {
result.stdout_only("test-3\ntest-4\ntest-2\ntest-1\n");
} else {
result.stdout_only("test-3 test-4 test-2 test-1\n");
}
} else {
result.stdout_only("test-3 test-4 test-2 test-1\n");
}
} else {
// Access time does not seem to be set on Windows and some other
// systems so the order is 4 3 2 1
if cfg!(not(windows)) {
result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n");
} else {
result.stdout_only("test-4 test-3 test-2 test-1\n");
// Access time does not seem to be set on Windows and some other
// systems so the order is 4 3 2 1
if cfg!(not(windows)) {
result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n");
} else {
result.stdout_only("test-4 test-3 test-2 test-1\n");
}
}
}
@ -621,20 +697,27 @@ fn test_ls_recursive() {
result.stdout_contains(&"a\\b:\nb");
}
#[cfg(unix)]
#[test]
fn test_ls_ls_color() {
fn test_ls_color() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.mkdir("a");
at.mkdir("a/nested_dir");
let nested_dir = Path::new("a")
.join("nested_dir")
.to_string_lossy()
.to_string();
at.mkdir(&nested_dir);
at.mkdir("z");
at.touch(&at.plus_as_string("a/nested_file"));
let nested_file = Path::new("a")
.join("nested_file")
.to_string_lossy()
.to_string();
at.touch(&nested_file);
at.touch("test-color");
let a_with_colors = "\x1b[01;34ma\x1b[0m";
let z_with_colors = "\x1b[01;34mz\x1b[0m";
let nested_dir_with_colors = "\x1b[01;34mnested_dir\x1b[0m";
let a_with_colors = "\x1b[1;34ma\x1b[0m";
let z_with_colors = "\x1b[1;34mz\x1b[0m";
let nested_dir_with_colors = "\x1b[1;34mnested_dir\x1b[0m";
// Color is disabled by default
let result = scene.ucmd().succeeds();
@ -670,14 +753,6 @@ fn test_ls_ls_color() {
.succeeds()
.stdout_contains(nested_dir_with_colors);
// Color has no effect
scene
.ucmd()
.arg("--color=always")
.arg("a/nested_file")
.succeeds()
.stdout_contains("a/nested_file\n");
// No output
scene
.ucmd()
@ -817,7 +892,7 @@ fn test_ls_indicator_style() {
let options = vec!["classify", "file-type", "slash"];
for opt in options {
// Verify that classify and file-type both contain indicators for symlinks.
let result = scene
scene
.ucmd()
.arg(format!("--indicator-style={}", opt))
.succeeds()
@ -827,7 +902,7 @@ fn test_ls_indicator_style() {
// Same test as above, but with the alternate flags.
let options = vec!["--classify", "--file-type", "-p"];
for opt in options {
let result = scene
scene
.ucmd()
.arg(format!("{}", opt))
.succeeds()
@ -838,7 +913,7 @@ fn test_ls_indicator_style() {
let options = vec!["classify", "file-type"];
for opt in options {
// Verify that classify and file-type both contain indicators for symlinks.
let result = scene
scene
.ucmd()
.arg(format!("--indicator-style={}", opt))
.succeeds()
@ -962,7 +1037,7 @@ fn test_ls_hidden_windows() {
let result = scene.ucmd().succeeds();
assert!(!result.stdout_str().contains(file));
let result = scene.ucmd().arg("-a").succeeds().stdout_contains(file);
scene.ucmd().arg("-a").succeeds().stdout_contains(file);
}
#[test]
@ -1052,9 +1127,11 @@ fn test_ls_quoting_style() {
at.touch("one");
// It seems that windows doesn't allow \n in filenames.
// And it also doesn't like \, of course.
#[cfg(unix)]
{
at.touch("one\ntwo");
at.touch("one\\two");
// Default is shell-escape
scene
.ucmd()
@ -1116,6 +1193,42 @@ fn test_ls_quoting_style() {
.succeeds()
.stdout_only(format!("{}\n", correct));
}
for (arg, correct) in &[
("--quoting-style=literal", "one\\two"),
("-N", "one\\two"),
("--quoting-style=c", "\"one\\\\two\""),
("-Q", "\"one\\\\two\""),
("--quote-name", "\"one\\\\two\""),
("--quoting-style=escape", "one\\\\two"),
("-b", "one\\\\two"),
("--quoting-style=shell-escape", "'one\\two'"),
("--quoting-style=shell-escape-always", "'one\\two'"),
("--quoting-style=shell", "'one\\two'"),
("--quoting-style=shell-always", "'one\\two'"),
] {
scene
.ucmd()
.arg(arg)
.arg("one\\two")
.succeeds()
.stdout_only(format!("{}\n", correct));
}
// Tests for a character that forces quotation in shell-style escaping
// after a character in a dollar expression
at.touch("one\n&two");
for (arg, correct) in &[
("--quoting-style=shell-escape", "'one'$'\\n''&two'"),
("--quoting-style=shell-escape-always", "'one'$'\\n''&two'"),
] {
scene
.ucmd()
.arg(arg)
.arg("one\n&two")
.succeeds()
.stdout_only(format!("{}\n", correct));
}
}
scene
@ -1316,6 +1429,43 @@ fn test_ls_ignore_hide() {
.stdout_is("CONTRIBUTING.md\nREADME.md\nREADMECAREFULLY.md\nsome_other_file\n");
}
#[test]
fn test_ls_ignore_backups() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("somefile");
at.touch("somebackup~");
at.touch(".somehiddenfile");
at.touch(".somehiddenbackup~");
scene.ucmd().arg("-B").succeeds().stdout_is("somefile\n");
scene
.ucmd()
.arg("--ignore-backups")
.succeeds()
.stdout_is("somefile\n");
scene
.ucmd()
.arg("-aB")
.succeeds()
.stdout_contains(".somehiddenfile")
.stdout_contains("somefile")
.stdout_does_not_contain("somebackup")
.stdout_does_not_contain(".somehiddenbackup~");
scene
.ucmd()
.arg("-a")
.arg("--ignore-backups")
.succeeds()
.stdout_contains(".somehiddenfile")
.stdout_contains("somefile")
.stdout_does_not_contain("somebackup")
.stdout_does_not_contain(".somehiddenbackup~");
}
#[test]
fn test_ls_directory() {
let scene = TestScenario::new(util_name!());

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,10 +2,17 @@ use crate::common::util::*;
fn test_helper(file_name: &str, args: &str) {
new_ucmd!()
.arg(args)
.arg(format!("{}.txt", file_name))
.args(&args.split(' ').collect::<Vec<&str>>())
.succeeds()
.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.
@ -65,11 +72,7 @@ fn test_extsort_as64_bailout() {
#[test]
fn test_multiple_decimals_general() {
new_ucmd!()
.arg("-g")
.arg("multiple_decimals_general.txt")
.succeeds()
.stdout_is("\n\n\n\n\n\n\n\nCARAvan\n-2028789030\n-896689\n-8.90880\n-1\n-.05\n000\n00000001\n1\n1.040000000\n1.444\n1.58590\n8.013\n45\n46.89\n576,446.88800000\n576,446.890\n 4567.\n4567.1\n4567.34\n\t\t\t\t\t\t\t\t\t\t4567..457\n\t\t\t\t37800\n\t\t\t\t\t\t45670.89079.098\n\t\t\t\t\t\t45670.89079.1\n4798908.340000000000\n4798908.45\n4798908.8909800\n");
test_helper("multiple_decimals_general", "-g")
}
#[test]
@ -99,7 +102,7 @@ fn test_check_zero_terminated_success() {
#[test]
fn test_random_shuffle_len() {
// check whether output is the same length as the input
const FILE: &'static str = "default_unsorted_ints.expected";
const FILE: &str = "default_unsorted_ints.expected";
let (at, _ucmd) = at_and_ucmd!();
let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str();
let expected = at.read(FILE);
@ -111,7 +114,7 @@ fn test_random_shuffle_len() {
#[test]
fn test_random_shuffle_contains_all_lines() {
// check whether lines of input are all in output
const FILE: &'static str = "default_unsorted_ints.expected";
const FILE: &str = "default_unsorted_ints.expected";
let (at, _ucmd) = at_and_ucmd!();
let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str();
let expected = at.read(FILE);
@ -126,7 +129,22 @@ fn test_random_shuffle_two_runs_not_the_same() {
// check to verify that two random shuffles are not equal; this has the
// potential to fail in the very unlikely event that the random order is the same
// as the starting order, or if both random sorts end up having the same order.
const FILE: &'static str = "default_unsorted_ints.expected";
const FILE: &str = "default_unsorted_ints.expected";
let (at, _ucmd) = at_and_ucmd!();
let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str();
let expected = at.read(FILE);
let unexpected = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str();
assert_ne!(result, expected);
assert_ne!(result, unexpected);
}
#[test]
fn test_random_shuffle_contains_two_runs_not_the_same() {
// check to verify that two random shuffles are not equal; this has the
// potential to fail in the unlikely event that random order is the same
// as the starting order, or if both random sorts end up having the same order.
const FILE: &str = "default_unsorted_ints.expected";
let (at, _ucmd) = at_and_ucmd!();
let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str();
let expected = at.read(FILE);
@ -228,6 +246,11 @@ fn test_non_printing_chars() {
}
}
#[test]
fn test_exponents_positive_general_fixed() {
test_helper("exponents_general", "-g");
}
#[test]
fn test_exponents_positive_numeric() {
test_helper("exponents-positive-numeric", "-n");
@ -344,62 +367,32 @@ fn test_numeric_unique_ints2() {
#[test]
fn test_keys_open_ended() {
let input = "aa bb cc\ndd aa ff\ngg aa cc\n";
new_ucmd!()
.args(&["-k", "2.2"])
.pipe_in(input)
.succeeds()
.stdout_only("gg aa cc\ndd aa ff\naa bb cc\n");
test_helper("keys_open_ended", "-k 2.3");
}
#[test]
fn test_keys_closed_range() {
let input = "aa bb cc\ndd aa ff\ngg aa cc\n";
new_ucmd!()
.args(&["-k", "2.2,2.2"])
.pipe_in(input)
.succeeds()
.stdout_only("dd aa ff\ngg aa cc\naa bb cc\n");
test_helper("keys_closed_range", "-k 2.2,2.2");
}
#[test]
fn test_keys_multiple_ranges() {
let input = "aa bb cc\ndd aa ff\ngg aa cc\n";
new_ucmd!()
.args(&["-k", "2,2", "-k", "3,3"])
.pipe_in(input)
.succeeds()
.stdout_only("gg aa cc\ndd aa ff\naa bb cc\n");
test_helper("keys_multiple_ranges", "-k 2,2 -k 3,3");
}
#[test]
fn test_keys_no_field_match() {
let input = "aa aa aa aa\naa bb cc\ndd aa ff\n";
new_ucmd!()
.args(&["-k", "4,4"])
.pipe_in(input)
.succeeds()
.stdout_only("aa bb cc\ndd aa ff\naa aa aa aa\n");
test_helper("keys_no_field_match", "-k 4,4");
}
#[test]
fn test_keys_no_char_match() {
let input = "aaa\nba\nc\n";
new_ucmd!()
.args(&["-k", "1.2"])
.pipe_in(input)
.succeeds()
.stdout_only("c\nba\naaa\n");
test_helper("keys_no_char_match", "-k 1.2");
}
#[test]
fn test_keys_custom_separator() {
let input = "aaxbbxcc\nddxaaxff\nggxaaxcc\n";
new_ucmd!()
.args(&["-k", "2.2,2.2", "-t", "x"])
.pipe_in(input)
.succeeds()
.stdout_only("ddxaaxff\nggxaaxcc\naaxbbxcc\n");
test_helper("keys_custom_separator", "-k 2.2,2.2 -t x");
}
#[test]
@ -426,6 +419,13 @@ fn test_keys_invalid_field_zero() {
.stderr_only("sort: error: field index was 0");
}
#[test]
fn test_keys_invalid_char_zero() {
new_ucmd!().args(&["-k", "1.0"]).fails().stderr_only(
"sort: error: invalid character index 0 in `1.0` for the start position of a field",
);
}
#[test]
fn test_keys_with_options() {
let input = "aa 3 cc\ndd 1 ff\ngg 2 cc\n";
@ -591,3 +591,39 @@ fn test_check_silent() {
.fails()
.stdout_is("");
}
#[test]
fn test_dictionary_and_nonprinting_conflicts() {
let conflicting_args = ["n", "h", "g", "M"];
for restricted_arg in &["d", "i"] {
for conflicting_arg in &conflicting_args {
new_ucmd!()
.arg(&format!("-{}{}", restricted_arg, conflicting_arg))
.fails();
}
for conflicting_arg in &conflicting_args {
new_ucmd!()
.args(&[
format!("-{}", restricted_arg).as_str(),
"-k",
&format!("1,1{}", conflicting_arg),
])
.succeeds();
}
for conflicting_arg in &conflicting_args {
// FIXME: this should ideally fail.
new_ucmd!()
.args(&["-k", &format!("1{},1{}", restricted_arg, conflicting_arg)])
.succeeds();
}
}
}
#[test]
fn test_trailing_separator() {
new_ucmd!()
.args(&["-t", "x", "-k", "1,1"])
.pipe_in("aax\naaa\n")
.succeeds()
.stdout_is("aax\naaa\n");
}

View file

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

View file

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

View file

@ -343,3 +343,8 @@ fn test_negative_indexing() {
assert_eq!(positive_lines_index.stdout(), negative_lines_index.stdout());
assert_eq!(positive_bytes_index.stdout(), negative_bytes_index.stdout());
}
#[test]
fn test_sleep_interval() {
new_ucmd!().arg("-s").arg("10").arg(FOOBAR_TXT).succeeds();
}

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 (_, mtime) = get_file_times(&at, file);
eprintln!("target_time: {:?}", target_time);
eprintln!("mtime: {:?}", mtime);
assert!(target_time == mtime);
}
// is_dst_switch_hour returns true if timespec ts is just before the switch
// to Daylight Saving Time.
// For example, in EST (UTC-5), Timespec { sec: 1583647200, nsec: 0 }
// for March 8 2020 01:00:00 AM
// is just before the switch because on that day clock jumps by 1 hour,
// so 1 minute after 01:59:00 is 03:00:00.
fn is_dst_switch_hour(ts: time::Timespec) -> bool {
let ts_after = ts + time::Duration::hours(1);
let tm = time::at(ts);
let tm_after = time::at(ts_after);
tm_after.tm_hour == tm.tm_hour + 2
}
// get_dstswitch_hour returns date string for which touch -m -t fails.
// For example, in EST (UTC-5), that will be "202003080200" so
// touch -m -t 202003080200 somefile
// fails (that date/time does not exist).
// In other locales it will be a different date/time, and in some locales
// it doesn't exist at all, in which case this function will return None.
fn get_dstswitch_hour() -> Option<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]
fn missing_args_fails() {
let (_, mut ucmd) = at_and_ucmd!();
let result = ucmd.run();
assert!(!result.success);
assert!(result.stderr.contains("missing operand"));
ucmd.fails().stderr_contains("missing operand");
}
#[test]
fn missing_required_second_arg_fails() {
let (_, mut ucmd) = at_and_ucmd!();
let result = ucmd.args(&["foo"]).run();
assert!(!result.success);
assert!(result.stderr.contains("missing operand after"));
ucmd.args(&["foo"])
.fails()
.stderr_contains("missing operand after");
}
#[test]

View file

@ -53,6 +53,16 @@ fn test_decrease_file_size() {
assert!(file.seek(SeekFrom::Current(0)).unwrap() == 6);
}
#[test]
fn test_space_in_size() {
let (at, mut ucmd) = at_and_ucmd!();
let mut file = at.make_file(TFILE2);
file.write_all(b"1234567890").unwrap();
ucmd.args(&["--size", " 4", TFILE2]).succeeds();
file.seek(SeekFrom::End(0)).unwrap();
assert!(file.seek(SeekFrom::Current(0)).unwrap() == 4);
}
#[test]
fn test_failed() {
new_ucmd!().fails();

View file

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

View file

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

View file

@ -2,6 +2,7 @@
#[cfg(not(windows))]
use libc;
use pretty_assertions::assert_eq;
use std::env;
#[cfg(not(windows))]
use std::ffi::CString;
@ -42,22 +43,6 @@ pub fn is_ci() -> bool {
.eq_ignore_ascii_case("true")
}
/// Test if the program is running under WSL
// ref: <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
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();
@ -74,11 +59,11 @@ pub struct CmdResult {
code: Option<i32>,
/// zero-exit from running the Command?
/// see [`success`]
pub success: bool,
success: bool,
/// captured standard output after running the Command
pub stdout: String,
stdout: String,
/// captured standard error after running the Command
pub stderr: String,
stderr: String,
}
impl CmdResult {
@ -237,7 +222,7 @@ impl CmdResult {
/// like stdout_is(...), but expects the contents of the file at the provided relative path
pub fn stdout_is_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &CmdResult {
let contents = read_scenario_fixture(&self.tmpd, file_rel_path);
self.stdout_is_bytes(contents)
self.stdout_is(String::from_utf8(contents).unwrap())
}
/// asserts that the command resulted in stderr stream output that equals the
@ -261,7 +246,7 @@ impl CmdResult {
/// Like stdout_is_fixture, but for stderr
pub fn stderr_is_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &CmdResult {
let contents = read_scenario_fixture(&self.tmpd, file_rel_path);
self.stderr_is_bytes(contents)
self.stderr_is(String::from_utf8(contents).unwrap())
}
/// asserts that
@ -329,14 +314,14 @@ impl CmdResult {
}
pub fn stdout_matches(&self, regex: &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())
}
self
}
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())
}
self
@ -635,7 +620,7 @@ impl TestScenario {
},
util_name: String::from(util_name),
fixtures: AtPath::new(tmpd.as_ref().path()),
tmpd: tmpd,
tmpd,
};
let mut fixture_path_builder = env::current_dir().unwrap();
fixture_path_builder.push(TESTS_DIR);
@ -696,8 +681,11 @@ pub struct UCommand {
comm_string: String,
tmpd: Option<Rc<TempDir>>,
has_run: bool,
stdin: Option<Vec<u8>>,
ignore_stdin_write_error: bool,
stdin: Option<Stdio>,
stdout: Option<Stdio>,
stderr: Option<Stdio>,
bytes_into_stdin: Option<Vec<u8>>,
}
impl UCommand {
@ -726,8 +714,11 @@ impl UCommand {
cmd
},
comm_string: String::from(arg.as_ref().to_str().unwrap()),
stdin: None,
ignore_stdin_write_error: false,
bytes_into_stdin: None,
stdin: None,
stdout: None,
stderr: None,
}
}
@ -738,6 +729,21 @@ impl UCommand {
ucmd
}
pub fn set_stdin<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
/// to the test environment directory.
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
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);
}
self.stdin = Some(input.into());
self.bytes_into_stdin = Some(input.into());
self
}
@ -784,7 +790,7 @@ impl UCommand {
/// This is typically useful to test non-standard workflows
/// like feeding something to a command that does not read it
pub fn ignore_stdin_write_error(&mut self) -> &mut UCommand {
if self.stdin.is_none() {
if self.bytes_into_stdin.is_none() {
panic!("{}", NO_STDIN_MEANINGLESS);
}
self.ignore_stdin_write_error = true;
@ -813,13 +819,13 @@ impl UCommand {
log_info("run", &self.comm_string);
let mut child = self
.raw
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(self.stdin.take().unwrap_or_else(|| Stdio::piped()))
.stdout(self.stdout.take().unwrap_or_else(|| Stdio::piped()))
.stderr(self.stderr.take().unwrap_or_else(|| Stdio::piped()))
.spawn()
.unwrap();
if let Some(ref input) = self.stdin {
if let Some(ref input) = self.bytes_into_stdin {
let write_result = child
.stdin
.take()

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