diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c40e5dfd..bcb1f8fff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,7 @@ search the issues to make sure no one else is working on it. 1. Make sure that the code coverage is covering all of the cases, including errors. 1. The code must be clippy-warning-free and rustfmt-compliant. 1. Don't hesitate to move common functions into uucore if they can be reused by other binaries. +1. Unsafe code should be documented with Safety comments. ## Commit messages diff --git a/Cargo.lock b/Cargo.lock index e094f1f48..461716b1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1362,6 +1362,12 @@ dependencies = [ "maybe-uninit", ] +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + [[package]] name = "strsim" version = "0.8.0" @@ -1388,9 +1394,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87" +checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" dependencies = [ "proc-macro2", "quote 1.0.9", @@ -1610,7 +1616,8 @@ name = "uu_cat" version = "0.0.6" dependencies = [ "clap", - "quick-error", + "nix 0.20.0", + "thiserror", "unix_socket", "uucore", "uucore_procs", @@ -1816,7 +1823,7 @@ dependencies = [ "quickcheck", "rand 0.7.3", "rand_chacha", - "smallvec", + "smallvec 0.6.14", "uucore", "uucore_procs", ] @@ -2285,10 +2292,11 @@ version = "0.0.6" dependencies = [ "clap", "fnv", - "itertools 0.8.2", + "itertools 0.10.0", "rand 0.7.3", "rayon", "semver", + "smallvec 1.6.1", "uucore", "uucore_procs", ] @@ -2316,7 +2324,7 @@ dependencies = [ name = "uu_stdbuf" version = "0.0.6" dependencies = [ - "getopts", + "clap", "tempfile", "uu_stdbuf_libstdbuf", "uucore", @@ -2500,7 +2508,7 @@ dependencies = [ name = "uu_unlink" version = "0.0.6" dependencies = [ - "getopts", + "clap", "libc", "uucore", "uucore_procs", diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index 84521bdd1..7b02a7a83 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -111,6 +111,7 @@ fn basename(fullname: &str, suffix: &str) -> String { } } +#[allow(clippy::manual_strip)] // can be replaced with strip_suffix once the minimum rust version is 1.45 fn strip_suffix(name: &str, suffix: &str) -> String { if name == suffix { return name.to_owned(); diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index e44a874c1..09b289253 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -16,13 +16,16 @@ path = "src/cat.rs" [dependencies] clap = "2.33" -quick-error = "1.2.3" +thiserror = "1.0" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [target.'cfg(unix)'.dependencies] unix_socket = "0.5.0" +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +nix = "0.20" + [[bin]] name = "cat" path = "src/main.rs" diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index cf5a384a4..7d56a7485 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -3,14 +3,13 @@ // (c) Jordi Boggiano // (c) Evgeniy Klyuchikov // (c) Joshua S. Miller +// (c) Árni Dagur // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore (ToDO) nonprint nonblank nonprinting -#[macro_use] -extern crate quick_error; #[cfg(unix)] extern crate unix_socket; #[macro_use] @@ -18,9 +17,9 @@ extern crate uucore; // last synced with: cat (GNU coreutils) 8.13 use clap::{App, Arg}; -use quick_error::ResultExt; use std::fs::{metadata, File}; -use std::io::{self, stderr, stdin, stdout, BufWriter, Read, Write}; +use std::io::{self, Read, Write}; +use thiserror::Error; use uucore::fs::is_stdin_interactive; /// Unix domain socket support @@ -31,12 +30,41 @@ 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]..."; static SUMMARY: &str = "Concatenate FILE(s), or standard input, to standard output With no FILE, or when FILE is -, read standard input."; +#[derive(Error, Debug)] +enum CatError { + /// Wrapper around `io::Error` + #[error("{0}")] + Io(#[from] io::Error), + /// Wrapper around `nix::Error` + #[cfg(any(target_os = "linux", target_os = "android"))] + #[error("{0}")] + Nix(#[from] nix::Error), + /// Unknown file type; it's not a regular file, socket, etc. + #[error("unknown filetype: {}", ft_debug)] + UnknownFiletype { + /// A debug print of the file type + ft_debug: String, + }, + #[error("Is a directory")] + IsDirectory, +} + +type CatResult = Result; + #[derive(PartialEq)] enum NumberingMode { None, @@ -44,39 +72,6 @@ enum NumberingMode { All, } -quick_error! { - #[derive(Debug)] - enum CatError { - /// Wrapper for io::Error with path context - Input(err: io::Error, path: String) { - display("cat: {0}: {1}", path, err) - context(path: &'a str, err: io::Error) -> (err, path.to_owned()) - cause(err) - } - - /// Wrapper for io::Error with no context - Output(err: io::Error) { - display("cat: {0}", err) from() - cause(err) - } - - /// Unknown Filetype classification - UnknownFiletype(path: String) { - display("cat: {0}: unknown filetype", path) - } - - /// At least one error was encountered in reading or writing - EncounteredErrors(count: usize) { - display("cat: encountered {0} errors", count) - } - - /// Denotes an error caused by trying to `cat` a directory - IsDirectory(path: String) { - display("cat: {0}: Is a directory", path) - } - } -} - struct OutputOptions { /// Line numbering mode number: NumberingMode, @@ -87,21 +82,56 @@ struct OutputOptions { /// display TAB characters as `tab` show_tabs: bool, - /// If `show_tabs == true`, this string will be printed in the - /// place of tabs - tab: String, - - /// Can be set to show characters other than '\n' a the end of - /// each line, e.g. $ - end_of_line: String, + /// Show end of lines + show_ends: bool, /// use ^ and M- notation, except for LF (\\n) and TAB (\\t) show_nonprint: bool, } +impl OutputOptions { + fn tab(&self) -> &'static str { + if self.show_tabs { + "^I" + } else { + "\t" + } + } + + fn end_of_line(&self) -> &'static str { + if self.show_ends { + "$\n" + } else { + "\n" + } + } + + /// We can write fast if we can simply copy the contents of the file to + /// stdout, without augmenting the output with e.g. line numbers. + fn can_write_fast(&self) -> bool { + !(self.show_tabs + || self.show_nonprint + || self.show_ends + || self.squeeze_blank + || self.number != NumberingMode::None) + } +} + +/// State that persists between output of each file. This struct is only used +/// when we can't write fast. +struct OutputState { + /// The current line number + line_number: usize, + + /// Whether the output cursor is at the beginning of a new line + at_line_start: bool, +} + /// Represents an open file handle, stream, or other device -struct InputHandle { - reader: Box, +struct InputHandle { + #[cfg(any(target_os = "linux", target_os = "android"))] + file_descriptor: RawFd, + reader: R, is_interactive: bool, } @@ -124,8 +154,6 @@ enum InputType { Socket, } -type CatResult = Result; - mod options { pub static FILE: &str = "file"; pub static SHOW_ALL: &str = "show-all"; @@ -243,30 +271,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 { None => vec!["-".to_owned()], }; - let can_write_fast = !(show_tabs - || show_nonprint - || show_ends - || squeeze_blank - || number_mode != NumberingMode::None); - - let success = if can_write_fast { - write_fast(files).is_ok() - } else { - let tab = if show_tabs { "^I" } else { "\t" }.to_owned(); - - let end_of_line = if show_ends { "$\n" } else { "\n" }.to_owned(); - - let options = OutputOptions { - end_of_line, - number: number_mode, - show_nonprint, - show_tabs, - squeeze_blank, - tab, - }; - - write_lines(files, &options).is_ok() + let options = OutputOptions { + show_ends, + number: number_mode, + show_nonprint, + show_tabs, + squeeze_blank, }; + let success = cat_files(files, &options).is_ok(); if success { 0 @@ -275,6 +287,76 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } +fn cat_handle( + handle: &mut InputHandle, + options: &OutputOptions, + state: &mut OutputState, +) -> CatResult<()> { + if options.can_write_fast() { + write_fast(handle) + } else { + write_lines(handle, &options, state) + } +} + +fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> CatResult<()> { + if path == "-" { + let stdin = io::stdin(); + let mut handle = InputHandle { + #[cfg(any(target_os = "linux", target_os = "android"))] + file_descriptor: stdin.as_raw_fd(), + reader: stdin, + is_interactive: is_stdin_interactive(), + }; + return cat_handle(&mut handle, &options, state); + } + match get_input_type(path)? { + InputType::Directory => Err(CatError::IsDirectory), + #[cfg(unix)] + InputType::Socket => { + let socket = UnixStream::connect(path)?; + socket.shutdown(Shutdown::Write)?; + let mut handle = InputHandle { + #[cfg(any(target_os = "linux", target_os = "android"))] + file_descriptor: socket.as_raw_fd(), + reader: socket, + is_interactive: false, + }; + cat_handle(&mut handle, &options, state) + } + _ => { + let file = File::open(path)?; + let mut handle = InputHandle { + #[cfg(any(target_os = "linux", target_os = "android"))] + file_descriptor: file.as_raw_fd(), + reader: file, + is_interactive: false, + }; + cat_handle(&mut handle, &options, state) + } + } +} + +fn cat_files(files: Vec, options: &OutputOptions) -> Result<(), u32> { + let mut error_count = 0; + let mut state = OutputState { + line_number: 1, + at_line_start: true, + }; + + for path in &files { + if let Err(err) = cat_path(path, &options, &mut state) { + show_info!("{}: {}", path, err); + error_count += 1; + } + } + if error_count == 0 { + Ok(()) + } else { + Err(error_count) + } +} + /// Classifies the `InputType` of file at `path` if possible /// /// # Arguments @@ -285,7 +367,8 @@ fn get_input_type(path: &str) -> CatResult { return Ok(InputType::StdIn); } - match metadata(path).context(path)?.file_type() { + let ft = metadata(path)?.file_type(); + match ft { #[cfg(unix)] ft if ft.is_block_device() => Ok(InputType::BlockDevice), #[cfg(unix)] @@ -297,125 +380,116 @@ fn get_input_type(path: &str) -> CatResult { ft if ft.is_dir() => Ok(InputType::Directory), ft if ft.is_file() => Ok(InputType::File), ft if ft.is_symlink() => Ok(InputType::SymLink), - _ => Err(CatError::UnknownFiletype(path.to_owned())), + _ => Err(CatError::UnknownFiletype { + ft_debug: format!("{:?}", ft), + }), } } -/// Returns an InputHandle from which a Reader can be accessed or an -/// error -/// -/// # Arguments -/// -/// * `path` - `InputHandler` will wrap a reader from this file path -fn open(path: &str) -> CatResult { - if path == "-" { - let stdin = stdin(); - return Ok(InputHandle { - reader: Box::new(stdin) as Box, - is_interactive: is_stdin_interactive(), - }); - } - - match get_input_type(path)? { - InputType::Directory => Err(CatError::IsDirectory(path.to_owned())), - #[cfg(unix)] - InputType::Socket => { - let socket = UnixStream::connect(path).context(path)?; - socket.shutdown(Shutdown::Write).context(path)?; - Ok(InputHandle { - reader: Box::new(socket) as Box, - is_interactive: false, - }) - } - _ => { - let file = File::open(path).context(path)?; - Ok(InputHandle { - reader: Box::new(file) as Box, - is_interactive: false, - }) +/// Writes handle to stdout with no configuration. This allows a +/// simple memory copy. +fn write_fast(handle: &mut InputHandle) -> CatResult<()> { + let stdout = io::stdout(); + let mut stdout_lock = stdout.lock(); + #[cfg(any(target_os = "linux", target_os = "android"))] + { + // 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())? { + return Ok(()); } } + // If we're not on Linux or Android, or the splice() call failed, + // fall back on slower writing. + let mut buf = [0; 1024 * 64]; + while let Ok(n) = handle.reader.read(&mut buf) { + if n == 0 { + break; + } + stdout_lock.write_all(&buf[..n])?; + } + Ok(()) } -/// Writes files to stdout with no configuration. This allows a -/// simple memory copy. Returns `Ok(())` if no errors were -/// encountered, or an error with the number of errors encountered. +/// 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. /// -/// # Arguments -/// -/// * `files` - There is no short circuit when encountering an error -/// reading a file in this vector -fn write_fast(files: Vec) -> CatResult<()> { - let mut writer = stdout(); - let mut in_buf = [0; 1024 * 64]; - let mut error_count = 0; +/// The `bool` in the result value indicates if we need to fall back to normal +/// copying or not. False means we don't have to. +#[cfg(any(target_os = "linux", target_os = "android"))] +#[inline] +fn write_fast_using_splice(handle: &mut InputHandle, writer: RawFd) -> CatResult { + const BUF_SIZE: usize = 1024 * 16; - for file in files { - match open(&file[..]) { - Ok(mut handle) => { - while let Ok(n) = handle.reader.read(&mut in_buf) { - if n == 0 { - break; - } - writer.write_all(&in_buf[..n]).context(&file[..])?; - } - } - Err(error) => { - writeln!(&mut stderr(), "{}", error)?; - error_count += 1; + 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); } } - match error_count { - 0 => Ok(()), - _ => Err(CatError::EncounteredErrors(error_count)), + 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) } -/// State that persists between output of each file -struct OutputState { - /// The current line number - line_number: usize, - - /// Whether the output cursor is at the beginning of a new line - at_line_start: bool, -} - -/// Writes files to stdout with `options` as configuration. Returns -/// `Ok(())` if no errors were encountered, or an error with the -/// number of errors encountered. -/// -/// # Arguments -/// -/// * `files` - There is no short circuit when encountering an error -/// reading a file in this vector -fn write_lines(files: Vec, options: &OutputOptions) -> CatResult<()> { - let mut error_count = 0; - let mut state = OutputState { - line_number: 1, - at_line_start: true, - }; - - for file in files { - if let Err(error) = write_file_lines(&file, options, &mut state) { - writeln!(&mut stderr(), "{}", error).context(&file[..])?; - error_count += 1; +/// 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; } } - - match error_count { - 0 => Ok(()), - _ => Err(CatError::EncounteredErrors(error_count)), - } + Ok(()) } /// Outputs file contents to stdout in a line-by-line fashion, /// propagating any errors that might occur. -fn write_file_lines(file: &str, options: &OutputOptions, state: &mut OutputState) -> CatResult<()> { - let mut handle = open(file)?; +fn write_lines( + handle: &mut InputHandle, + options: &OutputOptions, + state: &mut OutputState, +) -> CatResult<()> { let mut in_buf = [0; 1024 * 31]; - let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); + let stdout = io::stdout(); + let mut writer = stdout.lock(); let mut one_blank_kept = false; while let Ok(n) = handle.reader.read(&mut in_buf) { @@ -433,9 +507,9 @@ fn write_file_lines(file: &str, options: &OutputOptions, state: &mut OutputState write!(&mut writer, "{0:6}\t", state.line_number)?; state.line_number += 1; } - writer.write_all(options.end_of_line.as_bytes())?; + writer.write_all(options.end_of_line().as_bytes())?; if handle.is_interactive { - writer.flush().context(file)?; + writer.flush()?; } } state.at_line_start = true; @@ -450,7 +524,7 @@ fn write_file_lines(file: &str, options: &OutputOptions, state: &mut OutputState // print to end of line or end of buffer let offset = if options.show_nonprint { - write_nonprint_to_end(&in_buf[pos..], &mut writer, options.tab.as_bytes()) + write_nonprint_to_end(&in_buf[pos..], &mut writer, options.tab().as_bytes()) } else if options.show_tabs { write_tab_to_end(&in_buf[pos..], &mut writer) } else { @@ -462,7 +536,7 @@ fn write_file_lines(file: &str, options: &OutputOptions, state: &mut OutputState break; } // print suitable end of line - writer.write_all(options.end_of_line.as_bytes())?; + writer.write_all(options.end_of_line().as_bytes())?; if handle.is_interactive { writer.flush()?; } diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index b4c3360c5..592a0a905 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -286,7 +286,7 @@ impl Chgrper { ret = match wrap_chgrp(path, &meta, self.dest_gid, follow, self.verbosity.clone()) { Ok(n) => { - if n != "" { + if !n.is_empty() { show_info!("{}", n); } 0 diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index d9d8c8cf2..dc11be7b8 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -171,13 +171,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 { // of a prefix '-' if it's associated with MODE // e.g. "chmod -v -xw -R FILE" -> "chmod -v xw -R FILE" pub fn strip_minus_from_mode(args: &mut Vec) -> bool { - for i in 0..args.len() { - if args[i].starts_with("-") { - if let Some(second) = args[i].chars().nth(1) { + for arg in args { + if arg.starts_with('-') { + if let Some(second) = arg.chars().nth(1) { match second { 'r' | 'w' | 'x' | 'X' | 's' | 't' | 'u' | 'g' | 'o' | '0'..='7' => { // TODO: use strip_prefix() once minimum rust version reaches 1.45.0 - args[i] = args[i][1..args[i].len()].to_string(); + *arg = arg[1..arg.len()].to_string(); return true; } _ => {} diff --git a/src/uu/chown/src/chown.rs b/src/uu/chown/src/chown.rs index 42010de03..0e3273b3b 100644 --- a/src/uu/chown/src/chown.rs +++ b/src/uu/chown/src/chown.rs @@ -391,7 +391,7 @@ impl Chowner { self.verbosity.clone(), ) { Ok(n) => { - if n != "" { + if !n.is_empty() { show_info!("{}", n); } 0 @@ -446,7 +446,7 @@ impl Chowner { self.verbosity.clone(), ) { Ok(n) => { - if n != "" { + if !n.is_empty() { show_info!("{}", n); } 0 diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index 44c5dfa02..7e672da1e 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -104,7 +104,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { _ => { let mut vector: Vec<&str> = Vec::new(); for (&k, v) in matches.args.iter() { - vector.push(k.clone()); + vector.push(k); vector.push(&v.vals[0].to_str().unwrap()); } vector @@ -133,7 +133,7 @@ fn set_context(root: &Path, options: &clap::ArgMatches) { let userspec = match userspec_str { Some(ref u) => { let s: Vec<&str> = u.split(':').collect(); - if s.len() != 2 || s.iter().any(|&spec| spec == "") { + if s.len() != 2 || s.iter().any(|&spec| spec.is_empty()) { crash!(1, "invalid userspec: `{}`", u) }; s @@ -142,16 +142,16 @@ fn set_context(root: &Path, options: &clap::ArgMatches) { }; let (user, group) = if userspec.is_empty() { - (&user_str[..], &group_str[..]) + (user_str, group_str) } else { - (&userspec[0][..], &userspec[1][..]) + (userspec[0], userspec[1]) }; enter_chroot(root); - set_groups_from_str(&groups_str[..]); - set_main_group(&group[..]); - set_user(&user[..]); + set_groups_from_str(groups_str); + set_main_group(group); + set_user(user); } fn enter_chroot(root: &Path) { diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 569ee78bc..4e245b298 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -132,7 +132,9 @@ macro_rules! prompt_yes( pub type CopyResult = Result; pub type Source = PathBuf; +pub type SourceSlice = Path; pub type Target = PathBuf; +pub type TargetSlice = Path; /// Specifies whether when overwrite files #[derive(Clone, Eq, PartialEq)] @@ -547,14 +549,13 @@ impl FromStr for Attribute { } fn add_all_attributes() -> Vec { - let mut attr = Vec::new(); + use Attribute::*; + + let mut attr = vec![Ownership, Timestamps, Context, Xattr, Links]; + #[cfg(unix)] - attr.push(Attribute::Mode); - attr.push(Attribute::Ownership); - attr.push(Attribute::Timestamps); - attr.push(Attribute::Context); - attr.push(Attribute::Xattr); - attr.push(Attribute::Links); + attr.insert(0, Mode); + attr } @@ -665,7 +666,7 @@ impl TargetType { /// /// Treat target as a dir if we have multiple sources or the target /// exists and already is a directory - fn determine(sources: &[Source], target: &Target) -> TargetType { + fn determine(sources: &[Source], target: &TargetSlice) -> TargetType { if sources.len() > 1 || target.is_dir() { TargetType::Directory } else { @@ -714,7 +715,7 @@ fn parse_path_args(path_args: &[String], options: &Options) -> CopyResult<(Vec, - source: &std::path::PathBuf, + source: &std::path::Path, dest: std::path::PathBuf, found_hard_link: &mut bool, ) -> CopyResult<()> { @@ -788,7 +789,7 @@ fn preserve_hardlinks( /// Behavior depends on `options`, see [`Options`] for details. /// /// [`Options`]: ./struct.Options.html -fn copy(sources: &[Source], target: &Target, options: &Options) -> CopyResult<()> { +fn copy(sources: &[Source], target: &TargetSlice, options: &Options) -> CopyResult<()> { let target_type = TargetType::determine(sources, target); verify_target_type(target, &target_type)?; @@ -840,7 +841,7 @@ fn copy(sources: &[Source], target: &Target, options: &Options) -> CopyResult<() fn construct_dest_path( source_path: &Path, - target: &Target, + target: &TargetSlice, target_type: &TargetType, options: &Options, ) -> CopyResult { @@ -870,8 +871,8 @@ fn construct_dest_path( } fn copy_source( - source: &Source, - target: &Target, + source: &SourceSlice, + target: &TargetSlice, target_type: &TargetType, options: &Options, ) -> CopyResult<()> { @@ -912,7 +913,7 @@ fn adjust_canonicalization(p: &Path) -> Cow { /// /// Any errors encountered copying files in the tree will be logged but /// will not cause a short-circuit. -fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult<()> { +fn copy_directory(root: &Path, target: &TargetSlice, options: &Options) -> CopyResult<()> { if !options.recursive { return Err(format!("omitting directory '{}'", root.display()).into()); } @@ -1068,6 +1069,7 @@ fn copy_attribute(source: &Path, dest: &Path, attribute: &Attribute) -> CopyResu } #[cfg(not(windows))] +#[allow(clippy::unnecessary_wraps)] // needed for windows version fn symlink_file(source: &Path, dest: &Path, context: &str) -> CopyResult<()> { match std::os::unix::fs::symlink(source, dest).context(context) { Ok(_) => Ok(()), diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 6b09b91d9..5bf310daa 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -406,7 +406,7 @@ fn cut_files(mut filenames: Vec, mode: Mode) -> i32 { continue; } - if !path.metadata().is_ok() { + if path.metadata().is_err() { show_error!("{}: No such file or directory", filename); continue; } @@ -487,7 +487,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("filter field columns from the input source") .takes_value(true) .allow_hyphen_values(true) - .value_name("LIST") + .value_name("LIST") .display_order(4), ) .arg( @@ -535,40 +535,36 @@ pub fn uumain(args: impl uucore::Args) -> i32 { matches.value_of(options::CHARACTERS), matches.value_of(options::FIELDS), ) { - (Some(byte_ranges), None, None) => { - list_to_ranges(&byte_ranges[..], complement).map(|ranges| { - Mode::Bytes( - ranges, - Options { - out_delim: Some( - matches - .value_of(options::OUTPUT_DELIMITER) - .unwrap_or_default() - .to_owned(), - ), - zero_terminated: matches.is_present(options::ZERO_TERMINATED), - }, - ) - }) - } - (None, Some(char_ranges), None) => { - list_to_ranges(&char_ranges[..], complement).map(|ranges| { - Mode::Characters( - ranges, - Options { - out_delim: Some( - matches - .value_of(options::OUTPUT_DELIMITER) - .unwrap_or_default() - .to_owned(), - ), - zero_terminated: matches.is_present(options::ZERO_TERMINATED), - }, - ) - }) - } + (Some(byte_ranges), None, None) => list_to_ranges(byte_ranges, complement).map(|ranges| { + Mode::Bytes( + ranges, + Options { + out_delim: Some( + matches + .value_of(options::OUTPUT_DELIMITER) + .unwrap_or_default() + .to_owned(), + ), + zero_terminated: matches.is_present(options::ZERO_TERMINATED), + }, + ) + }), + (None, Some(char_ranges), None) => list_to_ranges(char_ranges, complement).map(|ranges| { + Mode::Characters( + ranges, + Options { + out_delim: Some( + matches + .value_of(options::OUTPUT_DELIMITER) + .unwrap_or_default() + .to_owned(), + ), + zero_terminated: matches.is_present(options::ZERO_TERMINATED), + }, + ) + }), (None, None, Some(field_ranges)) => { - list_to_ranges(&field_ranges[..], complement).and_then(|ranges| { + list_to_ranges(field_ranges, complement).and_then(|ranges| { let out_delim = match matches.value_of(options::OUTPUT_DELIMITER) { Some(s) => { if s.is_empty() { diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 57caf7970..e898b187c 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -116,7 +116,6 @@ struct Options { show_listed_fs: bool, show_fs_type: bool, show_inode_instead: bool, - print_grand_total: bool, // block_size: usize, human_readable_base: i64, fs_selector: FsSelector, @@ -286,7 +285,6 @@ impl Options { show_listed_fs: false, show_fs_type: false, show_inode_instead: false, - print_grand_total: false, // block_size: match env::var("BLOCKSIZE") { // Ok(size) => size.parse().unwrap(), // Err(_) => 512, @@ -871,9 +869,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { if matches.is_present(OPT_ALL) { opt.show_all_fs = true; } - if matches.is_present(OPT_TOTAL) { - opt.print_grand_total = true; - } if matches.is_present(OPT_INODES) { opt.show_inode_instead = true; } diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 615b66a4e..e01af5195 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -15,7 +15,7 @@ use chrono::Local; use std::collections::HashSet; use std::env; use std::fs; -use std::io::{stderr, Result, Write}; +use std::io::{stderr, ErrorKind, Result, Write}; use std::iter; #[cfg(not(windows))] use std::os::unix::fs::MetadataExt; @@ -296,7 +296,21 @@ fn du( } } } - Err(error) => show_error!("{}", error), + Err(error) => match error.kind() { + ErrorKind::PermissionDenied => { + let description = format!( + "cannot access '{}'", + entry + .path() + .as_os_str() + .to_str() + .unwrap_or("") + ); + let error_message = "Permission denied"; + show_error_custom_description!(description, "{}", error_message) + } + _ => show_error!("{}", error), + }, }, Err(error) => show_error!("{}", error), } @@ -322,7 +336,7 @@ fn convert_size_human(size: u64, multiplier: u64, _block_size: u64) -> String { } } if size == 0 { - return format!("0"); + return "0".to_string(); } format!("{}B", size) } diff --git a/src/uu/expr/src/expr.rs b/src/uu/expr/src/expr.rs index fee85dfe1..4a13812d3 100644 --- a/src/uu/expr/src/expr.rs +++ b/src/uu/expr/src/expr.rs @@ -51,7 +51,7 @@ fn print_expr_error(expr_error: &str) -> ! { crash!(2, "{}", expr_error) } -fn evaluate_ast(maybe_ast: Result, String>) -> Result { +fn evaluate_ast(maybe_ast: Result, String>) -> Result { if maybe_ast.is_err() { Err(maybe_ast.err().unwrap()) } else { diff --git a/src/uu/expr/src/syntax_tree.rs b/src/uu/expr/src/syntax_tree.rs index 3381c29bd..c81adf0c8 100644 --- a/src/uu/expr/src/syntax_tree.rs +++ b/src/uu/expr/src/syntax_tree.rs @@ -17,10 +17,10 @@ use onig::{Regex, RegexOptions, Syntax}; use crate::tokens::Token; type TokenStack = Vec<(usize, Token)>; -pub type OperandsList = Vec>; +pub type OperandsList = Vec>; #[derive(Debug)] -pub enum ASTNode { +pub enum AstNode { Leaf { token_idx: usize, value: String, @@ -31,7 +31,7 @@ pub enum ASTNode { operands: OperandsList, }, } -impl ASTNode { +impl AstNode { fn debug_dump(&self) { self.debug_dump_impl(1); } @@ -40,7 +40,7 @@ impl ASTNode { print!("\t",); } match *self { - ASTNode::Leaf { + AstNode::Leaf { ref token_idx, ref value, } => println!( @@ -49,7 +49,7 @@ impl ASTNode { token_idx, self.evaluate() ), - ASTNode::Node { + AstNode::Node { ref token_idx, ref op_type, ref operands, @@ -67,23 +67,23 @@ impl ASTNode { } } - fn new_node(token_idx: usize, op_type: &str, operands: OperandsList) -> Box { - Box::new(ASTNode::Node { + fn new_node(token_idx: usize, op_type: &str, operands: OperandsList) -> Box { + Box::new(AstNode::Node { token_idx, op_type: op_type.into(), operands, }) } - fn new_leaf(token_idx: usize, value: &str) -> Box { - Box::new(ASTNode::Leaf { + fn new_leaf(token_idx: usize, value: &str) -> Box { + Box::new(AstNode::Leaf { token_idx, value: value.into(), }) } pub fn evaluate(&self) -> Result { match *self { - ASTNode::Leaf { ref value, .. } => Ok(value.clone()), - ASTNode::Node { ref op_type, .. } => match self.operand_values() { + AstNode::Leaf { ref value, .. } => Ok(value.clone()), + AstNode::Node { ref op_type, .. } => match self.operand_values() { Err(reason) => Err(reason), Ok(operand_values) => match op_type.as_ref() { "+" => infix_operator_two_ints( @@ -161,7 +161,7 @@ impl ASTNode { } } pub fn operand_values(&self) -> Result, String> { - if let ASTNode::Node { ref operands, .. } = *self { + if let AstNode::Node { ref operands, .. } = *self { let mut out = Vec::with_capacity(operands.len()); for operand in operands { match operand.evaluate() { @@ -178,7 +178,7 @@ impl ASTNode { pub fn tokens_to_ast( maybe_tokens: Result, String>, -) -> Result, String> { +) -> Result, String> { if maybe_tokens.is_err() { Err(maybe_tokens.err().unwrap()) } else { @@ -212,7 +212,7 @@ pub fn tokens_to_ast( } } -fn maybe_dump_ast(result: &Result, String>) { +fn maybe_dump_ast(result: &Result, String>) { use std::env; if let Ok(debug_var) = env::var("EXPR_DEBUG_AST") { if debug_var == "1" { @@ -238,11 +238,11 @@ fn maybe_dump_rpn(rpn: &TokenStack) { } } -fn ast_from_rpn(rpn: &mut TokenStack) -> Result, String> { +fn ast_from_rpn(rpn: &mut TokenStack) -> Result, String> { match rpn.pop() { None => Err("syntax error (premature end of expression)".to_owned()), - Some((token_idx, Token::Value { value })) => Ok(ASTNode::new_leaf(token_idx, &value)), + Some((token_idx, Token::Value { value })) => Ok(AstNode::new_leaf(token_idx, &value)), Some((token_idx, Token::InfixOp { value, .. })) => { maybe_ast_node(token_idx, &value, 2, rpn) @@ -262,7 +262,7 @@ fn maybe_ast_node( op_type: &str, arity: usize, rpn: &mut TokenStack, -) -> Result, String> { +) -> Result, String> { let mut operands = Vec::with_capacity(arity); for _ in 0..arity { match ast_from_rpn(rpn) { @@ -271,7 +271,7 @@ fn maybe_ast_node( } } operands.reverse(); - Ok(ASTNode::new_node(token_idx, op_type, operands)) + Ok(AstNode::new_node(token_idx, op_type, operands)) } fn move_rest_of_ops_to_out( diff --git a/src/uu/fmt/src/parasplit.rs b/src/uu/fmt/src/parasplit.rs index f74a25413..950b3f66d 100644 --- a/src/uu/fmt/src/parasplit.rs +++ b/src/uu/fmt/src/parasplit.rs @@ -267,7 +267,7 @@ impl<'a> ParagraphStream<'a> { #[allow(clippy::match_like_matches_macro)] // `matches!(...)` macro not stabilized until rust v1.42 l_slice[..colon_posn].chars().all(|x| match x as usize { - y if y < 33 || y > 126 => false, + y if !(33..=126).contains(&y) => false, _ => true, }) } diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index c35e996f2..fa703eade 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -66,7 +66,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .takes_value(true), ) .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) - .get_matches_from(args.clone()); + .get_matches_from(args); let bytes = matches.is_present(options::BYTES); let spaces = matches.is_present(options::SPACES); diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index ee7d2a0f7..2e31ddd25 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -78,7 +78,7 @@ fn detect_algo<'a>( "sha512sum" => ("SHA512", Box::new(Sha512::new()) as Box, 512), "b2sum" => ("BLAKE2", Box::new(Blake2b::new(64)) as Box, 512), "sha3sum" => match matches.value_of("bits") { - Some(bits_str) => match usize::from_str_radix(&bits_str, 10) { + Some(bits_str) => match (&bits_str).parse::() { Ok(224) => ( "SHA3-224", Box::new(Sha3_224::new()) as Box, @@ -128,7 +128,7 @@ fn detect_algo<'a>( 512, ), "shake128sum" => match matches.value_of("bits") { - Some(bits_str) => match usize::from_str_radix(&bits_str, 10) { + Some(bits_str) => match (&bits_str).parse::() { Ok(bits) => ( "SHAKE128", Box::new(Shake128::new()) as Box, @@ -139,7 +139,7 @@ fn detect_algo<'a>( None => crash!(1, "--bits required for SHAKE-128"), }, "shake256sum" => match matches.value_of("bits") { - Some(bits_str) => match usize::from_str_radix(&bits_str, 10) { + Some(bits_str) => match (&bits_str).parse::() { Ok(bits) => ( "SHAKE256", Box::new(Shake256::new()) as Box, @@ -182,7 +182,7 @@ fn detect_algo<'a>( } if matches.is_present("sha3") { match matches.value_of("bits") { - Some(bits_str) => match usize::from_str_radix(&bits_str, 10) { + Some(bits_str) => match (&bits_str).parse::() { Ok(224) => set_or_crash( "SHA3-224", Box::new(Sha3_224::new()) as Box, @@ -226,7 +226,7 @@ fn detect_algo<'a>( } if matches.is_present("shake128") { match matches.value_of("bits") { - Some(bits_str) => match usize::from_str_radix(&bits_str, 10) { + Some(bits_str) => match (&bits_str).parse::() { Ok(bits) => set_or_crash("SHAKE128", Box::new(Shake128::new()), bits), Err(err) => crash!(1, "{}", err), }, @@ -235,7 +235,7 @@ fn detect_algo<'a>( } if matches.is_present("shake256") { match matches.value_of("bits") { - Some(bits_str) => match usize::from_str_radix(&bits_str, 10) { + Some(bits_str) => match (&bits_str).parse::() { Ok(bits) => set_or_crash("SHAKE256", Box::new(Shake256::new()), bits), Err(err) => crash!(1, "{}", err), }, @@ -253,7 +253,7 @@ fn detect_algo<'a>( // TODO: return custom error type fn parse_bit_num(arg: &str) -> Result { - usize::from_str_radix(arg, 10) + arg.parse() } fn is_valid_bit_num(arg: String) -> Result<(), String> { diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 3500af544..807d04314 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -625,7 +625,7 @@ mod tests { assert_eq!(arg_outputs("head"), Ok("head".to_owned())); } #[test] - #[cfg(linux)] + #[cfg(target_os = "linux")] fn test_arg_iterate_bad_encoding() { let invalid = unsafe { std::str::from_utf8_unchecked(b"\x80\x81") }; // this arises from a conversion from OsString to &str diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index a4f1ca6e6..a75ce45be 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -23,9 +23,11 @@ use std::fs; use std::fs::File; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; +use std::process::Command; use std::result::Result; const DEFAULT_MODE: u32 = 0o755; +const DEFAULT_STRIP_PROGRAM: &str = "strip"; #[allow(dead_code)] pub struct Behavior { @@ -37,6 +39,8 @@ pub struct Behavior { verbose: bool, preserve_timestamps: bool, compare: bool, + strip: bool, + strip_program: String, } #[derive(Clone, Eq, PartialEq)] @@ -164,17 +168,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("apply access/modification times of SOURCE files to corresponding destination files") ) .arg( - // TODO implement flag Arg::with_name(OPT_STRIP) .short("s") .long(OPT_STRIP) - .help("(unimplemented) strip symbol tables") + .help("strip symbol tables (no action Windows)") ) .arg( - // TODO implement flag Arg::with_name(OPT_STRIP_PROGRAM) .long(OPT_STRIP_PROGRAM) - .help("(unimplemented) program used to strip binaries") + .help("program used to strip binaries (no action Windows)") .value_name("PROGRAM") ) .arg( @@ -266,10 +268,6 @@ fn check_unimplemented<'a>(matches: &ArgMatches) -> Result<(), &'a str> { Err("-b") } else if matches.is_present(OPT_CREATED) { Err("-D") - } else if matches.is_present(OPT_STRIP) { - Err("--strip, -s") - } else if matches.is_present(OPT_STRIP_PROGRAM) { - Err("--strip-program") } else if matches.is_present(OPT_SUFFIX) { Err("--suffix, -S") } else if matches.is_present(OPT_TARGET_DIRECTORY) { @@ -304,7 +302,7 @@ fn behavior(matches: &ArgMatches) -> Result { let specified_mode: Option = if matches.is_present(OPT_MODE) { match matches.value_of(OPT_MODE) { - Some(x) => match mode::parse(&x[..], considering_dir) { + Some(x) => match mode::parse(x, considering_dir) { Ok(y) => Some(y), Err(err) => { show_error!("Invalid mode string: {}", err); @@ -339,6 +337,12 @@ fn behavior(matches: &ArgMatches) -> Result { verbose: matches.is_present(OPT_VERBOSE), preserve_timestamps: matches.is_present(OPT_PRESERVE_TIMESTAMPS), compare: matches.is_present(OPT_COMPARE), + strip: matches.is_present(OPT_STRIP), + strip_program: String::from( + matches + .value_of(OPT_STRIP_PROGRAM) + .unwrap_or(DEFAULT_STRIP_PROGRAM), + ), }) } @@ -425,7 +429,7 @@ fn standard(paths: Vec, b: Behavior) -> i32 { /// _files_ must all exist as non-directories. /// _target_dir_ must be a directory. /// -fn copy_files_into_dir(files: &[PathBuf], target_dir: &PathBuf, b: &Behavior) -> i32 { +fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> i32 { if !target_dir.is_dir() { show_error!("target '{}' is not a directory", target_dir.display()); return 1; @@ -449,7 +453,7 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &PathBuf, b: &Behavior) -> continue; } - let mut targetpath = target_dir.clone().to_path_buf(); + let mut targetpath = target_dir.to_path_buf(); let filename = sourcepath.components().last().unwrap(); targetpath.push(filename); @@ -474,7 +478,7 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &PathBuf, b: &Behavior) -> /// _file_ must exist as a non-directory. /// _target_ must be a non-directory /// -fn copy_file_to_file(file: &PathBuf, target: &PathBuf, b: &Behavior) -> i32 { +fn copy_file_to_file(file: &Path, target: &Path, b: &Behavior) -> i32 { if copy(file, &target, b).is_err() { 1 } else { @@ -493,7 +497,7 @@ fn copy_file_to_file(file: &PathBuf, target: &PathBuf, b: &Behavior) -> i32 { /// /// If the copy system call fails, we print a verbose error and return an empty error value. /// -fn copy(from: &PathBuf, to: &PathBuf, b: &Behavior) -> Result<(), ()> { +fn copy(from: &Path, to: &Path, b: &Behavior) -> Result<(), ()> { if b.compare && !need_copy(from, to, b) { return Ok(()); } @@ -521,6 +525,21 @@ fn copy(from: &PathBuf, to: &PathBuf, b: &Behavior) -> Result<(), ()> { return Err(()); } + if b.strip && cfg!(not(windows)) { + match Command::new(&b.strip_program).arg(to).output() { + Ok(o) => { + if !o.status.success() { + crash!( + 1, + "strip program failed: {}", + String::from_utf8(o.stderr).unwrap_or_default() + ); + } + } + Err(e) => crash!(1, "strip program execution failed: {}", e), + } + } + if mode::chmod(&to, b.mode()).is_err() { return Err(()); } @@ -537,7 +556,7 @@ fn copy(from: &PathBuf, to: &PathBuf, b: &Behavior) -> Result<(), ()> { }; let gid = meta.gid(); match wrap_chown( - to.as_path(), + to, &meta, Some(owner_id), Some(gid), @@ -563,7 +582,7 @@ fn copy(from: &PathBuf, to: &PathBuf, b: &Behavior) -> Result<(), ()> { Ok(g) => g, _ => crash!(1, "no such group: {}", b.group), }; - match wrap_chgrp(to.as_path(), &meta, group_id, false, Verbosity::Normal) { + match wrap_chgrp(to, &meta, group_id, false, Verbosity::Normal) { Ok(n) => { if !n.is_empty() { show_info!("{}", n); @@ -582,7 +601,7 @@ fn copy(from: &PathBuf, to: &PathBuf, b: &Behavior) -> Result<(), ()> { let modified_time = FileTime::from_last_modification_time(&meta); let accessed_time = FileTime::from_last_access_time(&meta); - match set_file_times(to.as_path(), accessed_time, modified_time) { + match set_file_times(to, accessed_time, modified_time) { Ok(_) => {} Err(e) => show_info!("{}", e), } @@ -611,7 +630,7 @@ fn copy(from: &PathBuf, to: &PathBuf, b: &Behavior) -> Result<(), ()> { /// /// Crashes the program if a nonexistent owner or group is specified in _b_. /// -fn need_copy(from: &PathBuf, to: &PathBuf, b: &Behavior) -> bool { +fn need_copy(from: &Path, to: &Path, b: &Behavior) -> bool { let from_meta = match fs::metadata(from) { Ok(meta) => meta, Err(_) => return true, diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index 96a0df813..04358a415 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -303,7 +303,7 @@ fn exec(files: &[PathBuf], settings: &Settings) -> i32 { } } -fn link_files_in_dir(files: &[PathBuf], target_dir: &PathBuf, settings: &Settings) -> i32 { +fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) -> i32 { if !target_dir.is_dir() { show_error!("target '{}' is not a directory", target_dir.display()); return 1; @@ -329,7 +329,7 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &PathBuf, settings: &Setting }; } } - target_dir.clone() + target_dir.to_path_buf() } else { match srcpath.as_os_str().to_str() { Some(name) => { @@ -370,7 +370,7 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &PathBuf, settings: &Setting } } -fn relative_path<'a>(src: &PathBuf, dst: &PathBuf) -> Result> { +fn relative_path<'a>(src: &Path, dst: &Path) -> Result> { let abssrc = canonicalize(src, CanonicalizeMode::Normal)?; let absdst = canonicalize(dst, CanonicalizeMode::Normal)?; let suffix_pos = abssrc @@ -390,7 +390,7 @@ fn relative_path<'a>(src: &PathBuf, dst: &PathBuf) -> Result> { Ok(result.into()) } -fn link(src: &PathBuf, dst: &PathBuf, settings: &Settings) -> Result<()> { +fn link(src: &Path, dst: &Path, settings: &Settings) -> Result<()> { let mut backup_path = None; let source: Cow<'_, Path> = if settings.relative { relative_path(&src, dst)? @@ -453,13 +453,13 @@ fn read_yes() -> bool { } } -fn simple_backup_path(path: &PathBuf, suffix: &str) -> PathBuf { +fn simple_backup_path(path: &Path, suffix: &str) -> PathBuf { let mut p = path.as_os_str().to_str().unwrap().to_owned(); p.push_str(suffix); PathBuf::from(p) } -fn numbered_backup_path(path: &PathBuf) -> PathBuf { +fn numbered_backup_path(path: &Path) -> PathBuf { let mut i: u64 = 1; loop { let new_path = simple_backup_path(path, &format!(".~{}~", i)); @@ -470,7 +470,7 @@ fn numbered_backup_path(path: &PathBuf) -> PathBuf { } } -fn existing_backup_path(path: &PathBuf, suffix: &str) -> PathBuf { +fn existing_backup_path(path: &Path, suffix: &str) -> PathBuf { let test_path = simple_backup_path(path, &".~1~".to_owned()); if test_path.exists() { return numbered_backup_path(path); diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index f22c83c48..514539809 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -381,6 +381,7 @@ impl Config { }) .or_else(|| termsize::get().map(|s| s.cols)); + #[allow(clippy::needless_bool)] let show_control = if options.is_present(options::HIDE_CONTROL_CHARS) { false } else if options.is_present(options::SHOW_CONTROL_CHARS) { @@ -1108,7 +1109,7 @@ fn sort_entries(entries: &mut Vec, config: &Config) { } // 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(version_cmp::version_cmp), + Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(a, b)), Sort::None => {} } @@ -1142,7 +1143,7 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool { true } -fn enter_directory(dir: &PathBuf, config: &Config) { +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)); @@ -1167,7 +1168,7 @@ fn enter_directory(dir: &PathBuf, config: &Config) { } } -fn get_metadata(entry: &PathBuf, dereference: bool) -> std::io::Result { +fn get_metadata(entry: &Path, dereference: bool) -> std::io::Result { if dereference { entry.metadata().or_else(|_| entry.symlink_metadata()) } else { @@ -1175,7 +1176,7 @@ fn get_metadata(entry: &PathBuf, dereference: bool) -> std::io::Result } } -fn display_dir_entry_size(entry: &PathBuf, config: &Config) -> (usize, usize) { +fn display_dir_entry_size(entry: &Path, config: &Config) -> (usize, usize) { if let Ok(md) = get_metadata(entry, false) { ( display_symlink_count(&md).len(), @@ -1270,7 +1271,7 @@ fn display_grid(names: impl Iterator, width: u16, direction: Direct use uucore::fs::display_permissions; fn display_item_long( - item: &PathBuf, + item: &Path, strip: Option<&Path>, max_links: usize, max_size: usize, diff --git a/src/uu/ls/src/version_cmp.rs b/src/uu/ls/src/version_cmp.rs index 3cd5989f1..4cd39f916 100644 --- a/src/uu/ls/src/version_cmp.rs +++ b/src/uu/ls/src/version_cmp.rs @@ -1,8 +1,9 @@ -use std::{cmp::Ordering, path::PathBuf}; +use std::cmp::Ordering; +use std::path::Path; -/// Compare pathbufs in a way that matches the GNU version sort, meaning that +/// Compare paths in a way that matches the GNU version sort, meaning that /// numbers get sorted in a natural way. -pub(crate) fn version_cmp(a: &PathBuf, b: &PathBuf) -> Ordering { +pub(crate) fn version_cmp(a: &Path, b: &Path) -> Ordering { let a_string = a.to_string_lossy(); let b_string = b.to_string_lossy(); let mut a = a_string.chars().peekable(); diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index b481aeebc..f57178a09 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -335,7 +335,7 @@ fn exec(files: &[PathBuf], b: Behavior) -> i32 { 0 } -fn move_files_into_dir(files: &[PathBuf], target_dir: &PathBuf, b: &Behavior) -> i32 { +fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> i32 { if !target_dir.is_dir() { show_error!("target ‘{}’ is not a directory", target_dir.display()); return 1; @@ -373,7 +373,7 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &PathBuf, b: &Behavior) -> } } -fn rename(from: &PathBuf, to: &PathBuf, b: &Behavior) -> io::Result<()> { +fn rename(from: &Path, to: &Path, b: &Behavior) -> io::Result<()> { let mut backup_path = None; if to.exists() { @@ -429,7 +429,7 @@ fn rename(from: &PathBuf, to: &PathBuf, b: &Behavior) -> io::Result<()> { /// A wrapper around `fs::rename`, so that if it fails, we try falling back on /// copying and removing. -fn rename_with_fallback(from: &PathBuf, to: &PathBuf) -> io::Result<()> { +fn rename_with_fallback(from: &Path, to: &Path) -> io::Result<()> { if fs::rename(from, to).is_err() { // Get metadata without following symlinks let metadata = from.symlink_metadata()?; @@ -464,7 +464,7 @@ fn rename_with_fallback(from: &PathBuf, to: &PathBuf) -> io::Result<()> { /// Move the given symlink to the given destination. On Windows, dangling /// symlinks return an error. #[inline] -fn rename_symlink_fallback(from: &PathBuf, to: &PathBuf) -> io::Result<()> { +fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> { let path_symlink_points_to = fs::read_link(from)?; #[cfg(unix)] { @@ -507,20 +507,20 @@ fn read_yes() -> bool { } } -fn simple_backup_path(path: &PathBuf, suffix: &str) -> PathBuf { +fn simple_backup_path(path: &Path, suffix: &str) -> PathBuf { let mut p = path.to_string_lossy().into_owned(); p.push_str(suffix); PathBuf::from(p) } -fn numbered_backup_path(path: &PathBuf) -> PathBuf { +fn numbered_backup_path(path: &Path) -> PathBuf { (1_u64..) .map(|i| path.with_extension(format!("~{}~", i))) .find(|p| !p.exists()) .expect("cannot create backup") } -fn existing_backup_path(path: &PathBuf, suffix: &str) -> PathBuf { +fn existing_backup_path(path: &Path, suffix: &str) -> PathBuf { let test_path = path.with_extension("~1~"); if test_path.exists() { numbered_backup_path(path) @@ -529,7 +529,7 @@ fn existing_backup_path(path: &PathBuf, suffix: &str) -> PathBuf { } } -fn is_empty_dir(path: &PathBuf) -> bool { +fn is_empty_dir(path: &Path) -> bool { match fs::read_dir(path) { Ok(contents) => contents.peekable().peek().is_none(), Err(_e) => false, diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index c3b39fca1..36eae66ab 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -118,7 +118,7 @@ struct OdOptions { } impl OdOptions { - fn new<'a>(matches: ArgMatches<'a>, args: Vec) -> Result { + fn new(matches: ArgMatches, args: Vec) -> Result { let byte_order = match matches.value_of(options::ENDIAN) { None => ByteOrder::Native, Some("little") => ByteOrder::Little, diff --git a/src/uu/od/src/parse_inputs.rs b/src/uu/od/src/parse_inputs.rs index 915aa1d92..533f4f106 100644 --- a/src/uu/od/src/parse_inputs.rs +++ b/src/uu/od/src/parse_inputs.rs @@ -63,7 +63,7 @@ pub fn parse_inputs(matches: &dyn CommandLineOpts) -> Result) -> Result Ok(CommandLineInputs::FileAndOffset(( - input_strings[0].clone().to_owned(), + input_strings[0].to_string(), m, None, ))), @@ -118,7 +118,7 @@ pub fn parse_inputs_traditional(input_strings: Vec<&str>) -> Result Ok(CommandLineInputs::FileAndOffset(( - input_strings[0].clone().to_owned(), + input_strings[0].to_string(), n, Some(m), ))), diff --git a/src/uu/pinky/src/pinky.rs b/src/uu/pinky/src/pinky.rs index 772e311d6..851a3cd42 100644 --- a/src/uu/pinky/src/pinky.rs +++ b/src/uu/pinky/src/pinky.rs @@ -15,7 +15,6 @@ use uucore::utmpx::{self, time, Utmpx}; use std::io::prelude::*; use std::io::BufReader; -use std::io::Result as IOResult; use std::fs::File; use std::os::unix::fs::MetadataExt; @@ -136,12 +135,8 @@ The utmp file will be {}", }; if do_short_format { - if let Err(e) = pk.short_pinky() { - show_usage_error!("{}", e); - 1 - } else { - 0 - } + pk.short_pinky(); + 0 } else { pk.long_pinky() } @@ -282,7 +277,7 @@ impl Pinky { println!(); } - fn short_pinky(&self) -> IOResult<()> { + fn short_pinky(&self) { if self.include_heading { self.print_heading(); } @@ -295,7 +290,6 @@ impl Pinky { } } } - Ok(()) } fn long_pinky(&self) -> i32 { diff --git a/src/uu/printf/src/tokenize/num_format/formatters/base_conv/mod.rs b/src/uu/printf/src/tokenize/num_format/formatters/base_conv/mod.rs index 79af9abd5..04d33b52c 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/base_conv/mod.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/base_conv/mod.rs @@ -199,8 +199,7 @@ pub fn arrnum_int_add(arrnum: &[u8], basenum: u8, base_ten_int_term: u8) -> Vec< } pub fn base_conv_vec(src: &[u8], radix_src: u8, radix_dest: u8) -> Vec { - let mut result: Vec = Vec::new(); - result.push(0); + let mut result = vec![0]; for i in src { result = arrnum_int_mult(&result, radix_dest, radix_src); result = arrnum_int_add(&result, radix_dest, *i); @@ -226,8 +225,7 @@ pub fn base_conv_float(src: &[u8], radix_src: u8, radix_dest: u8) -> f64 { // to implement this for arbitrary string input. // until then, the below operates as an outline // of how it would work. - let mut result: Vec = Vec::new(); - result.push(0); + let result: Vec = vec![0]; let mut factor: f64 = 1_f64; let radix_src_float: f64 = f64::from(radix_src); let mut r: f64 = 0_f64; diff --git a/src/uu/printf/src/tokenize/num_format/num_format.rs b/src/uu/printf/src/tokenize/num_format/num_format.rs index 9a519e95e..812f51b5a 100644 --- a/src/uu/printf/src/tokenize/num_format/num_format.rs +++ b/src/uu/printf/src/tokenize/num_format/num_format.rs @@ -263,9 +263,5 @@ pub fn num_format(field: &FormatField, in_str_opt: Option<&String>) -> Option Config { } if matches.is_present(options::WIDTH) { let width_str = matches.value_of(options::WIDTH).expect(err_msg).to_string(); - config.line_width = crash_if_err!(1, usize::from_str_radix(&width_str, 10)); + config.line_width = crash_if_err!(1, (&width_str).parse::()); } if matches.is_present(options::GAP_SIZE) { let gap_str = matches .value_of(options::GAP_SIZE) .expect(err_msg) .to_string(); - config.gap_size = crash_if_err!(1, usize::from_str_radix(&gap_str, 10)); + config.gap_size = crash_if_err!(1, (&gap_str).parse::()); } if matches.is_present(options::FORMAT_ROFF) { config.format = OutFormat::Roff; diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index 727c2cce5..43a4ca656 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -13,7 +13,7 @@ extern crate uucore; use clap::{App, Arg}; use std::fs; use std::io::{stdout, Write}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use uucore::fs::{canonicalize, CanonicalizeMode}; const NAME: &str = "readlink"; @@ -160,8 +160,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } -fn show(path: &PathBuf, no_newline: bool, use_zero: bool) { - let path = path.as_path().to_str().unwrap(); +fn show(path: &Path, no_newline: bool, use_zero: bool) { + let path = path.to_str().unwrap(); if use_zero { print!("{}\0", path); } else if no_newline { diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 5cc8f3d9a..37ff70fb2 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -12,7 +12,7 @@ extern crate uucore; use clap::{App, Arg}; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use uucore::fs::{canonicalize, CanonicalizeMode}; static ABOUT: &str = "print the resolved path"; @@ -82,7 +82,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { retcode } -fn resolve_path(p: &PathBuf, strip: bool, zero: bool, quiet: bool) -> bool { +fn resolve_path(p: &Path, strip: bool, zero: bool, quiet: bool) -> bool { let abs = canonicalize(p, CanonicalizeMode::Normal).unwrap(); if strip { diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 09671768b..94626b4e7 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -176,7 +176,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } else if matches.is_present(OPT_PROMPT_MORE) { InteractiveMode::Once } else if matches.is_present(OPT_INTERACTIVE) { - match &matches.value_of(OPT_INTERACTIVE).unwrap()[..] { + match matches.value_of(OPT_INTERACTIVE).unwrap() { "none" => InteractiveMode::None, "once" => InteractiveMode::Once, "always" => InteractiveMode::Always, diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 671dd7e1c..c3bba1c78 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -102,7 +102,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let mut largest_dec = 0; let mut padding = 0; let first = if numbers.len() > 1 { - let slice = &numbers[0][..]; + let slice = numbers[0]; let len = slice.len(); let dec = slice.find('.').unwrap_or(len); largest_dec = len - dec; @@ -118,7 +118,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 1.0 }; let increment = if numbers.len() > 2 { - let slice = &numbers[1][..]; + let slice = numbers[1]; let len = slice.len(); let dec = slice.find('.').unwrap_or(len); largest_dec = cmp::max(largest_dec, len - dec); @@ -134,11 +134,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 1.0 }; if increment == 0.0 { - show_error!("increment value: '{}'", &numbers[1][..]); + show_error!("increment value: '{}'", numbers[1]); return 1; } let last = { - let slice = &numbers[numbers.len() - 1][..]; + let slice = numbers[numbers.len() - 1]; padding = cmp::max(padding, slice.find('.').unwrap_or_else(|| slice.len())); match parse_float(slice) { Ok(n) => n, diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index d5f910297..b89d48a10 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -259,6 +259,7 @@ static AFTER_HELP: &str = "; pub mod options { + pub const FORCE: &str = "force"; pub const FILE: &str = "file"; pub const ITERATIONS: &str = "iterations"; pub const SIZE: &str = "size"; @@ -278,6 +279,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .about(ABOUT) .after_help(AFTER_HELP) .usage(&usage[..]) + .arg( + Arg::with_name(options::FORCE) + .long(options::FORCE) + .short("f") + .help("change permissions to allow writing if necessary"), + ) .arg( Arg::with_name(options::ITERATIONS) .long(options::ITERATIONS) @@ -354,13 +361,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { // TODO: implement --random-source - // TODO: implement --force - + let force = matches.is_present(options::FORCE); let remove = matches.is_present(options::REMOVE); - let size_arg = match matches.value_of(options::SIZE) { - Some(s) => Some(s.to_string()), - None => None, - }; + let size_arg = matches.value_of(options::SIZE).map(|s| s.to_string()); let size = get_size(size_arg); let exact = matches.is_present(options::EXACT) && size.is_none(); // if -s is given, ignore -x let zero = matches.is_present(options::ZERO); @@ -375,7 +378,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } for path_str in matches.values_of(options::FILE).unwrap() { - wipe_file(&path_str, iterations, remove, size, exact, zero, verbose); + wipe_file( + &path_str, iterations, remove, size, exact, zero, verbose, force, + ); } 0 @@ -431,6 +436,7 @@ fn pass_name(pass_type: PassType) -> String { } } +#[allow(clippy::too_many_arguments)] fn wipe_file( path_str: &str, n_passes: usize, @@ -439,18 +445,37 @@ fn wipe_file( exact: bool, zero: bool, verbose: bool, + force: bool, ) { // Get these potential errors out of the way first let path: &Path = Path::new(path_str); if !path.exists() { - println!("{}: {}: No such file or directory", NAME, path.display()); + show_error!("{}: No such file or directory", path.display()); return; } if !path.is_file() { - println!("{}: {}: Not a file", NAME, path.display()); + show_error!("{}: Not a file", path.display()); return; } + // If force is true, set file permissions to not-readonly. + if force { + let metadata = match fs::metadata(path) { + Ok(m) => m, + Err(e) => { + show_error!("{}", e); + return; + } + }; + + let mut perms = metadata.permissions(); + perms.set_readonly(false); + if let Err(e) = fs::set_permissions(path, perms) { + show_error!("{}", e); + return; + } + } + // Fill up our pass sequence let mut pass_sequence: Vec = Vec::new(); @@ -489,11 +514,13 @@ fn wipe_file( { let total_passes: usize = pass_sequence.len(); - let mut file: File = OpenOptions::new() - .write(true) - .truncate(false) - .open(path) - .expect("Failed to open file for writing"); + let mut file: File = match OpenOptions::new().write(true).truncate(false).open(path) { + Ok(f) => f, + Err(e) => { + show_error!("{}: failed to open for writing: {}", path.display(), e); + return; + } + }; // NOTE: it does not really matter what we set for total_bytes and gen_type here, so just // use bogus values @@ -523,14 +550,23 @@ fn wipe_file( } } // size is an optional argument for exactly how many bytes we want to shred - do_pass(&mut file, path, &mut generator, *pass_type, size) - .expect("File write pass failed"); + match do_pass(&mut file, path, &mut generator, *pass_type, size) { + Ok(_) => {} + Err(e) => { + show_error!("{}: File write pass failed: {}", path.display(), e); + } + } // Ignore failed writes; just keep trying } } if remove { - do_remove(path, path_str, verbose).expect("Failed to remove file"); + match do_remove(path, path_str, verbose) { + Ok(_) => {} + Err(e) => { + show_error!("{}: failed to remove file: {}", path.display(), e); + } + } } } diff --git a/src/uu/sort/BENCHMARKING.md b/src/uu/sort/BENCHMARKING.md new file mode 100644 index 000000000..b20db014d --- /dev/null +++ b/src/uu/sort/BENCHMARKING.md @@ -0,0 +1,33 @@ +# Benchmarking sort + +Most of the time when sorting is spent comparing lines. The comparison functions however differ based +on which arguments are passed to `sort`, therefore it is important to always benchmark multiple scenarios. +This is an overwiew over what was benchmarked, and if you make changes to `sort`, 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! + +## Sorting a wordlist +- Get a wordlist, for example with [words](https://en.wikipedia.org/wiki/Words_(Unix)) on Linux. The exact wordlist + doesn't matter for performance comparisons. In this example I'm using `/usr/share/dict/american-english` as the wordlist. +- Shuffle the wordlist by running `sort -R /usr/share/dict/american-english > shuffled_wordlist.txt`. +- Benchmark sorting the wordlist with hyperfine: `hyperfine "target/release/coreutils sort shuffled_wordlist.txt -o output.txt"`. + +## Sorting a wordlist with ignore_case +- Same wordlist as above +- Benchmark sorting the wordlist ignoring the case with hyperfine: `hyperfine "target/release/coreutils sort shuffled_wordlist.txt -f -o output.txt"`. + +## Sorting numbers +- Generate a list of numbers: `seq 0 100000 | sort -R > shuffled_numbers.txt`. +- Benchmark numeric sorting with hyperfine: `hyperfine "target/release/coreutils sort shuffled_numbers.txt -n -o output.txt"`. + +## Stdout and stdin performance +Try to run the above benchmarks by piping the input through stdin (standard input) and redirect the +output through stdout (standard output): +- Remove the input file from the arguments and add `cat [inputfile] | ` at the beginning. +- Remove `-o output.txt` and add `> output.txt` at the end. + +Example: `hyperfine "target/release/coreutils sort shuffled_numbers.txt -n -o output.txt"` becomes +`hyperfine "cat shuffled_numbers.txt | target/release/coreutils sort -n > output.txt` +- Check that performance is similar to the original benchmark. \ No newline at end of file diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index 814e4bbba..6a9976278 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -19,8 +19,9 @@ rayon = "1.5" rand = "0.7" clap = "2.33" fnv = "1.0.7" -itertools = "0.8.0" +itertools = "0.10.0" semver = "0.9.0" +smallvec = "1.6.1" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 4e0e25d65..8bf6eb1e8 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -2,10 +2,10 @@ // * // * (c) Michael Yin // * (c) Robert Swinford +// * (c) Michael Debertol // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -#![allow(dead_code)] // Although these links don't always seem to describe reality, check out the POSIX and GNU specs: // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sort.html @@ -22,6 +22,8 @@ use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; use rayon::prelude::*; use semver::Version; +use smallvec::SmallVec; +use std::borrow::Cow; use std::cmp::Ordering; use std::collections::BinaryHeap; use std::env; @@ -29,6 +31,7 @@ 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; use uucore::fs::is_stdin_interactive; // for Iterator::dedup() @@ -36,6 +39,16 @@ static NAME: &str = "sort"; static ABOUT: &str = "Display sorted concatenation of all FILE(s)."; static VERSION: &str = env!("CARGO_PKG_VERSION"); +const LONG_HELP_KEYS: &str = "The key format is FIELD[.CHAR][OPTIONS][,FIELD[.CHAR]][OPTIONS]. + +Fields by default are separated by the first whitespace after a non-whitespace character. Use -t to specify a custom separator. +In the default case, whitespace is appended at the beginning of each field. Custom separators however are not included in fields. + +FIELD and CHAR both start at 1 (i.e. they are 1-indexed). If there is no end specified after a comma, the end will be the end of the line. +If CHAR is set 0, it means the end of the field. CHAR defaults to 1 for the start position and to 0 for the end position. + +Valid options are: MbdfhnRrV. They override the global options for this key."; + static OPT_HUMAN_NUMERIC_SORT: &str = "human-numeric-sort"; static OPT_MONTH_SORT: &str = "month-sort"; static OPT_NUMERIC_SORT: &str = "numeric-sort"; @@ -53,6 +66,8 @@ static OPT_OUTPUT: &str = "output"; static OPT_REVERSE: &str = "reverse"; static OPT_STABLE: &str = "stable"; static OPT_UNIQUE: &str = "unique"; +static OPT_KEY: &str = "key"; +static OPT_SEPARATOR: &str = "field-separator"; static OPT_RANDOM: &str = "random-sort"; static OPT_ZERO_TERMINATED: &str = "zero-terminated"; static OPT_PARALLEL: &str = "parallel"; @@ -62,10 +77,11 @@ static ARG_FILES: &str = "files"; static DECIMAL_PT: char = '.'; static THOUSANDS_SEP: char = ','; + static NEGATIVE: char = '-'; static POSITIVE: char = '+'; -#[derive(Eq, Ord, PartialEq, PartialOrd)] +#[derive(Eq, Ord, PartialEq, PartialOrd, Clone)] enum SortMode { Numeric, HumanNumeric, @@ -75,8 +91,12 @@ enum SortMode { Default, } -struct Settings { +struct GlobalSettings { mode: SortMode, + ignore_blanks: bool, + ignore_case: bool, + dictionary_order: bool, + ignore_non_printing: bool, merge: bool, reverse: bool, outfile: Option, @@ -85,17 +105,21 @@ struct Settings { check: bool, check_silent: bool, random: bool, - compare_fn: fn(&str, &str) -> Ordering, - transform_fns: Vec String>, - threads: String, salt: String, + selectors: Vec, + separator: Option, + threads: String, zero_terminated: bool, } -impl Default for Settings { - fn default() -> Settings { - Settings { +impl Default for GlobalSettings { + fn default() -> GlobalSettings { + GlobalSettings { mode: SortMode::Default, + ignore_blanks: false, + ignore_case: false, + dictionary_order: false, + ignore_non_printing: false, merge: false, reverse: false, outfile: None, @@ -104,19 +128,332 @@ impl Default for Settings { check: false, check_silent: false, random: false, - compare_fn: default_compare, - transform_fns: Vec::new(), - threads: String::new(), salt: String::new(), + selectors: vec![], + separator: None, + threads: String::new(), zero_terminated: false, } } } +struct KeySettings { + mode: SortMode, + ignore_blanks: bool, + ignore_case: bool, + dictionary_order: bool, + ignore_non_printing: bool, + random: bool, + reverse: bool, +} + +impl From<&GlobalSettings> for KeySettings { + fn from(settings: &GlobalSettings) -> Self { + Self { + mode: settings.mode.clone(), + ignore_blanks: settings.ignore_blanks, + ignore_case: settings.ignore_case, + ignore_non_printing: settings.ignore_non_printing, + random: settings.random, + reverse: settings.reverse, + dictionary_order: settings.dictionary_order, + } + } +} + +/// Represents the string selected by a FieldSelector. +#[derive(Debug)] +enum Selection { + /// If we had to transform this selection, we have to store a new string. + String(String), + /// If there was no transformation, we can store an index into the line. + ByIndex(Range), +} + +impl Selection { + /// Gets the actual string slice represented by this Selection. + fn get_str<'a>(&'a self, line: &'a Line) -> &'a str { + match self { + Selection::String(string) => string.as_str(), + Selection::ByIndex(range) => &line.line[range.to_owned()], + } + } +} + +type Field = Range; + +#[derive(Debug)] +struct Line { + line: String, + // The common case is not to specify fields. Let's make this fast. + selections: SmallVec<[Selection; 1]>, +} + +impl Line { + fn new(line: String, settings: &GlobalSettings) -> Self { + let fields = if settings + .selectors + .iter() + .any(|selector| selector.needs_tokens()) + { + // Only tokenize if we will need tokens. + Some(tokenize(&line, settings.separator)) + } else { + None + }; + + let selections = settings + .selectors + .iter() + .map(|selector| { + if let Some(range) = selector.get_selection(&line, fields.as_deref()) { + if let Some(transformed) = + transform(&line[range.to_owned()], &selector.settings) + { + Selection::String(transformed) + } else { + Selection::ByIndex(range.start().to_owned()..range.end() + 1) + } + } else { + // If there is no match, match the empty string. + Selection::ByIndex(0..0) + } + }) + .collect(); + Self { line, selections } + } +} + +/// Transform this line. Returns None if there's no need to transform. +fn transform(line: &str, settings: &KeySettings) -> Option { + let mut transformed = None; + if settings.ignore_case { + transformed = Some(line.to_uppercase()); + } + if settings.ignore_blanks { + transformed = Some( + transformed + .as_deref() + .unwrap_or(line) + .trim_start() + .to_string(), + ); + } + if settings.dictionary_order { + transformed = Some(remove_nondictionary_chars( + transformed.as_deref().unwrap_or(line), + )); + } + if settings.ignore_non_printing { + transformed = Some(remove_nonprinting_chars( + transformed.as_deref().unwrap_or(line), + )); + } + transformed +} + +/// Tokenize a line into fields. +fn tokenize(line: &str, separator: Option) -> Vec { + if let Some(separator) = separator { + tokenize_with_separator(line, separator) + } else { + tokenize_default(line) + } +} + +/// By default fields are separated by the first whitespace after non-whitespace. +/// Whitespace is included in fields at the start. +fn tokenize_default(line: &str) -> Vec { + let mut tokens = vec![0..0]; + // pretend that there was whitespace in front of the line + let mut previous_was_whitespace = true; + for (idx, char) in line.char_indices() { + if char.is_whitespace() { + if !previous_was_whitespace { + tokens.last_mut().unwrap().end = idx; + tokens.push(idx..0); + } + previous_was_whitespace = true; + } else { + previous_was_whitespace = false; + } + } + tokens.last_mut().unwrap().end = line.len(); + tokens +} + +/// Split between separators. These separators are not included in fields. +fn tokenize_with_separator(line: &str, separator: char) -> Vec { + let mut tokens = vec![0..0]; + let mut previous_was_separator = false; + for (idx, char) in line.char_indices() { + if previous_was_separator { + tokens.push(idx..0); + } + if char == separator { + tokens.last_mut().unwrap().end = idx; + previous_was_separator = true; + } else { + previous_was_separator = false; + } + } + tokens.last_mut().unwrap().end = line.len(); + tokens +} + +struct KeyPosition { + /// 1-indexed, 0 is invalid. + field: usize, + /// 1-indexed, 0 is end of field. + char: usize, + ignore_blanks: bool, +} + +impl KeyPosition { + fn parse(key: &str, default_char_index: usize, settings: &mut KeySettings) -> Self { + let mut field_and_char = key.split('.'); + let mut field = field_and_char + .next() + .unwrap_or_else(|| crash!(1, "invalid key `{}`", key)); + let mut char = field_and_char.next(); + + // If there is a char index, we expect options to appear after it. Otherwise we expect them after the field index. + let value_with_options = char.as_mut().unwrap_or(&mut field); + + let mut ignore_blanks = settings.ignore_blanks; + if let Some(options_start) = value_with_options.chars().position(char::is_alphabetic) { + for option in value_with_options[options_start..].chars() { + // valid options: MbdfghinRrV + match option { + 'M' => settings.mode = SortMode::Month, + 'b' => ignore_blanks = true, + 'd' => settings.dictionary_order = true, + 'f' => settings.ignore_case = true, + 'g' => settings.mode = SortMode::GeneralNumeric, + 'h' => settings.mode = SortMode::HumanNumeric, + 'i' => settings.ignore_non_printing = true, + 'n' => settings.mode = SortMode::Numeric, + 'R' => settings.random = true, + 'r' => settings.reverse = true, + 'V' => settings.mode = SortMode::Version, + c => { + crash!(1, "invalid option for key: `{}`", c) + } + } + } + // Strip away option characters from the original value so we can parse it later + *value_with_options = &value_with_options[..options_start]; + } + + let field = field + .parse() + .unwrap_or_else(|e| crash!(1, "failed to parse field index for key `{}`: {}", key, e)); + if field == 0 { + crash!(1, "field index was 0"); + } + let char = char.map_or(default_char_index, |char| { + char.parse().unwrap_or_else(|e| { + crash!( + 1, + "failed to parse character index for key `{}`: {}", + key, + e + ) + }) + }); + Self { + field, + char, + ignore_blanks, + } + } +} + +struct FieldSelector { + from: KeyPosition, + to: Option, + settings: KeySettings, +} + +impl FieldSelector { + fn needs_tokens(&self) -> bool { + self.from.field != 1 || self.from.char == 0 || self.to.is_some() + } + + /// Look up the slice that corresponds to this selector for the given line. + /// If needs_fields returned false, fields may be None. + fn get_selection<'a>( + &self, + line: &'a str, + tokens: Option<&[Field]>, + ) -> Option> { + enum ResolutionErr { + TooLow, + TooHigh, + } + + // Get the index for this line given the KeyPosition + fn resolve_index( + line: &str, + tokens: Option<&[Field]>, + position: &KeyPosition, + ) -> Result { + if tokens.map_or(false, |fields| fields.len() < position.field) { + Err(ResolutionErr::TooHigh) + } else if position.char == 0 { + let end = tokens.unwrap()[position.field - 1].end; + if end == 0 { + Err(ResolutionErr::TooLow) + } else { + Ok(end - 1) + } + } else { + let mut idx = if position.field == 1 { + // The first field always starts at 0. + // We don't need tokens for this case. + 0 + } else { + tokens.unwrap()[position.field - 1].start + } + position.char + - 1; + if idx >= line.len() { + Err(ResolutionErr::TooHigh) + } else { + if position.ignore_blanks { + if let Some(not_whitespace) = + line[idx..].chars().position(|c| !c.is_whitespace()) + { + idx += not_whitespace; + } else { + return Err(ResolutionErr::TooHigh); + } + } + Ok(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, + } + } else { + None + } + } +} + struct MergeableFile<'a> { lines: Lines>>, - current_line: String, - settings: &'a Settings, + current_line: Line, + settings: &'a GlobalSettings, } // BinaryHeap depends on `Ord`. Note that we want to pop smallest items @@ -124,7 +461,7 @@ struct MergeableFile<'a> { // trick it into the right order by calling reverse() here. impl<'a> Ord for MergeableFile<'a> { fn cmp(&self, other: &MergeableFile) -> Ordering { - compare_by(&self.current_line, &other.current_line, &self.settings).reverse() + compare_by(&self.current_line, &other.current_line, self.settings).reverse() } } @@ -136,7 +473,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 == compare_by(&self.current_line, &other.current_line, self.settings) } } @@ -144,11 +481,11 @@ impl<'a> Eq for MergeableFile<'a> {} struct FileMerger<'a> { heap: BinaryHeap>, - settings: &'a Settings, + settings: &'a GlobalSettings, } impl<'a> FileMerger<'a> { - fn new(settings: &'a Settings) -> FileMerger<'a> { + fn new(settings: &'a GlobalSettings) -> FileMerger<'a> { FileMerger { heap: BinaryHeap::new(), settings, @@ -158,7 +495,7 @@ impl<'a> FileMerger<'a> { if let Some(Ok(next_line)) = lines.next() { let mergeable_file = MergeableFile { lines, - current_line: next_line, + current_line: Line::new(next_line, &self.settings), settings: &self.settings, }; self.heap.push(mergeable_file); @@ -173,14 +510,17 @@ impl<'a> Iterator for FileMerger<'a> { Some(mut current) => { match current.lines.next() { Some(Ok(next_line)) => { - let ret = replace(&mut current.current_line, next_line); + let ret = replace( + &mut current.current_line, + Line::new(next_line, &self.settings), + ); self.heap.push(current); - Some(ret) + Some(ret.line) } _ => { // Don't put it back in the heap (it's empty/erroring) // but its first line is still valid. - Some(current.current_line) + Some(current.current_line.line) } } } @@ -204,7 +544,7 @@ With no FILE, or when FILE is -, read standard input.", pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); let usage = get_usage(); - let mut settings: Settings = Default::default(); + let mut settings: GlobalSettings = Default::default(); let matches = App::new(executable!()) .version(VERSION) @@ -262,7 +602,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Arg::with_name(OPT_CHECK_SILENT) .short("C") .long(OPT_CHECK_SILENT) - .help("exit successfully if the given file is already sorted, and exit with status 1 otherwise. "), + .help("exit successfully if the given file is already sorted, and exit with status 1 otherwise."), ) .arg( Arg::with_name(OPT_IGNORE_CASE) @@ -315,7 +655,21 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("output only the first of an equal run"), ) .arg( - Arg::with_name(OPT_ZERO_TERMINATED) + Arg::with_name(OPT_KEY) + .short("k") + .long(OPT_KEY) + .help("sort by a key") + .long_help(LONG_HELP_KEYS) + .multiple(true) + .takes_value(true), + ) + .arg( + Arg::with_name(OPT_SEPARATOR) + .short("t") + .long(OPT_SEPARATOR) + .help("custom separator for -k") + .takes_value(true)) + .arg(Arg::with_name(OPT_ZERO_TERMINATED) .short("z") .long(OPT_ZERO_TERMINATED) .help("line delimiter is NUL, not newline"), @@ -323,7 +677,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .arg( Arg::with_name(OPT_PARALLEL) .long(OPT_PARALLEL) - .help("change the number of threads running concurrently to N") + .help("change the number of threads running concurrently to NUM_THREADS") .takes_value(true) .value_name("NUM_THREADS"), ) @@ -349,14 +703,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { for path in &files0_from { let (reader, _) = open(path.as_str()).expect("Could not read from file specified."); let buf_reader = BufReader::new(reader); - for line in buf_reader.split(b'\0') { - if let Ok(n) = line { - files.push( - std::str::from_utf8(&n) - .expect("Could not parse zero terminated string from input.") - .to_string(), - ); - } + for line in buf_reader.split(b'\0').flatten() { + files.push( + std::str::from_utf8(&line) + .expect("Could not parse string from zero terminated input.") + .to_string(), + ); } } files @@ -381,21 +733,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 { SortMode::Default }; + settings.dictionary_order = matches.is_present(OPT_DICTIONARY_ORDER); + settings.ignore_non_printing = matches.is_present(OPT_IGNORE_NONPRINTING); if matches.is_present(OPT_PARALLEL) { // "0" is default - threads = num of cores settings.threads = matches .value_of(OPT_PARALLEL) .map(String::from) - .unwrap_or("0".to_string()); + .unwrap_or_else(|| "0".to_string()); env::set_var("RAYON_NUM_THREADS", &settings.threads); } - if matches.is_present(OPT_DICTIONARY_ORDER) { - settings.transform_fns.push(remove_nondictionary_chars); - } else if matches.is_present(OPT_IGNORE_NONPRINTING) { - settings.transform_fns.push(remove_nonprinting_chars); - } - settings.zero_terminated = matches.is_present(OPT_ZERO_TERMINATED); settings.merge = matches.is_present(OPT_MERGE); @@ -405,13 +753,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { settings.check = true; }; - if matches.is_present(OPT_IGNORE_CASE) { - settings.transform_fns.push(|s| s.to_uppercase()); - } + settings.ignore_case = matches.is_present(OPT_IGNORE_CASE); - if matches.is_present(OPT_IGNORE_BLANKS) { - settings.transform_fns.push(|s| s.trim_start().to_string()); - } + settings.ignore_blanks = matches.is_present(OPT_IGNORE_BLANKS); settings.outfile = matches.value_of(OPT_OUTPUT).map(String::from); settings.reverse = matches.is_present(OPT_REVERSE); @@ -423,27 +767,64 @@ pub fn uumain(args: impl uucore::Args) -> i32 { settings.salt = get_rand_string(); } - //let mut files = matches.free; if files.is_empty() { /* if no file, default to stdin */ files.push("-".to_owned()); } else if settings.check && files.len() != 1 { - crash!(1, "sort: extra operand `{}' not allowed with -c", files[1]) + crash!(1, "extra operand `{}' not allowed with -c", files[1]) } - settings.compare_fn = match settings.mode { - SortMode::Numeric => numeric_compare, - SortMode::GeneralNumeric => general_numeric_compare, - SortMode::HumanNumeric => human_numeric_size_compare, - SortMode::Month => month_compare, - SortMode::Version => version_compare, - SortMode::Default => default_compare, - }; + if let Some(arg) = matches.args.get(OPT_SEPARATOR) { + let separator = arg.vals[0].to_string_lossy(); + let separator = separator; + if separator.len() != 1 { + crash!(1, "separator must be exactly one character long"); + } + settings.separator = Some(separator.chars().next().unwrap()) + } - exec(files, &mut settings) + if matches.is_present(OPT_KEY) { + for key in &matches.args[OPT_KEY].vals { + let key = key.to_string_lossy(); + let mut from_to = key.split(','); + let mut key_settings = KeySettings::from(&settings); + let from = KeyPosition::parse( + from_to + .next() + .unwrap_or_else(|| crash!(1, "invalid key `{}`", key)), + 1, + &mut key_settings, + ); + let to = from_to + .next() + .map(|to| KeyPosition::parse(to, 0, &mut key_settings)); + let field_selector = FieldSelector { + from, + to, + settings: key_settings, + }; + settings.selectors.push(field_selector); + } + } + + if !settings.stable || !matches.is_present(OPT_KEY) { + // add a default selector matching the whole line + let key_settings = KeySettings::from(&settings); + settings.selectors.push(FieldSelector { + from: KeyPosition { + field: 1, + char: 1, + ignore_blanks: key_settings.ignore_blanks, + }, + to: None, + settings: key_settings, + }); + } + + exec(files, &settings) } -fn exec(files: Vec, settings: &mut Settings) -> i32 { +fn exec(files: Vec, settings: &GlobalSettings) -> i32 { let mut lines = Vec::new(); let mut file_merger = FileMerger::new(&settings); @@ -458,26 +839,27 @@ fn exec(files: Vec, settings: &mut Settings) -> i32 { if settings.merge { file_merger.push_file(buf_reader.lines()); } else if settings.zero_terminated { - for line in buf_reader.split(b'\0') { - if let Ok(n) = line { - lines.push( - std::str::from_utf8(&n) - .expect("Could not parse string from zero terminated input.") - .to_string(), - ); - } + for line in buf_reader.split(b'\0').flatten() { + lines.push(Line::new( + std::str::from_utf8(&line) + .expect("Could not parse string from zero terminated input.") + .to_string(), + &settings, + )); } } else { for line in buf_reader.lines() { if let Ok(n) = line { - lines.push(n); + lines.push(Line::new(n, &settings)); + } else { + break; } } } } if settings.check { - return exec_check_file(lines, &settings); + return exec_check_file(&lines, &settings); } else { sort_by(&mut lines, &settings); } @@ -488,28 +870,22 @@ fn exec(files: Vec, settings: &mut Settings) -> i32 { } else { print_sorted(file_merger, &settings) } - } else if settings.mode == SortMode::Month && settings.unique { - print_sorted( - lines - .iter() - .dedup_by(|a, b| get_months_dedup(a) == get_months_dedup(b)), - &settings, - ) } else if settings.unique { print_sorted( lines - .iter() - .dedup_by(|a, b| get_nums_dedup(a) == get_nums_dedup(b)), + .into_iter() + .dedup_by(|a, b| compare_by(a, b, settings) == Ordering::Equal) + .map(|line| line.line), &settings, ) } else { - print_sorted(lines.iter(), &settings) + print_sorted(lines.into_iter().map(|line| line.line), &settings) } 0 } -fn exec_check_file(unwrapped_lines: Vec, settings: &Settings) -> i32 { +fn exec_check_file(unwrapped_lines: &[Line], settings: &GlobalSettings) -> i32 { // errors yields the line before each disorder, // plus the last line (quirk of .coalesce()) let mut errors = @@ -541,51 +917,45 @@ fn exec_check_file(unwrapped_lines: Vec, settings: &Settings) -> i32 { } } -#[inline(always)] -fn transform(line: &str, settings: &Settings) -> String { - let mut transformed = line.to_owned(); - for transform_fn in &settings.transform_fns { - transformed = transform_fn(&transformed); - } - - transformed -} - -#[inline(always)] -fn sort_by(lines: &mut Vec, settings: &Settings) { +fn sort_by(lines: &mut Vec, settings: &GlobalSettings) { lines.par_sort_by(|a, b| compare_by(a, b, &settings)) } -fn compare_by(a: &str, b: &str, settings: &Settings) -> Ordering { - let (a_transformed, b_transformed): (String, String); - let (a, b) = if !settings.transform_fns.is_empty() { - a_transformed = transform(&a, &settings); - b_transformed = transform(&b, &settings); - (a_transformed.as_str(), b_transformed.as_str()) - } else { - (a, b) - }; +fn compare_by(a: &Line, b: &Line, global_settings: &GlobalSettings) -> Ordering { + for (idx, selector) in global_settings.selectors.iter().enumerate() { + let a = a.selections[idx].get_str(a); + let b = b.selections[idx].get_str(b); + let settings = &selector.settings; - // 1st Compare - let mut cmp: Ordering = if settings.random { - random_shuffle(a, b, settings.salt.clone()) - } else { - (settings.compare_fn)(a, b) - }; - - // Call "last resort compare" on any equal - if cmp == Ordering::Equal { - if settings.random || settings.stable || settings.unique { - cmp = Ordering::Equal + let cmp: Ordering = if settings.random { + random_shuffle(a, b, global_settings.salt.clone()) } else { - cmp = default_compare(a, b) + (match settings.mode { + SortMode::Numeric => numeric_compare, + SortMode::GeneralNumeric => general_numeric_compare, + SortMode::HumanNumeric => human_numeric_size_compare, + SortMode::Month => month_compare, + SortMode::Version => version_compare, + SortMode::Default => default_compare, + })(a, b) }; + if cmp != Ordering::Equal { + return if settings.reverse { cmp.reverse() } else { cmp }; + } + } + + // Call "last resort compare" if all selectors returned Equal + + let cmp = if global_settings.random || global_settings.stable || global_settings.unique { + Ordering::Equal + } else { + default_compare(&a.line, &b.line) }; - if settings.reverse { - return cmp.reverse(); + if global_settings.reverse { + cmp.reverse() } else { - return cmp; + cmp } } @@ -603,25 +973,26 @@ fn default_compare(a: &str, b: &str) -> Ordering { #[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() { - // check whether char is numeric, whitespace or decimal point or thousand seperator if !c.is_numeric() && !c.is_whitespace() - && !c.eq(&DECIMAL_PT) && !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().nth(0).unwrap_or('\0').eq(&POSITIVE) - && !a.chars().nth(0).unwrap_or('\0').eq(&NEGATIVE) + && !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 = &a; } s } @@ -633,16 +1004,17 @@ fn leading_num_common(a: &str) -> &str { // not recognize a positive sign or scientific/E notation so we strip those elements here. fn get_leading_num(a: &str) -> &str { let mut s = ""; - let b = leading_num_common(a); + + let a = leading_num_common(a); // GNU numeric sort doesn't recognize '+' or 'e' notation so we strip - for (idx, c) in b.char_indices() { - if c.eq(&'e') || c.eq(&'E') || b.chars().nth(0).unwrap_or('\0').eq(&POSITIVE) { - s = &b[..idx]; + for (idx, c) in a.char_indices() { + if c.eq(&'e') || c.eq(&'E') || a.chars().next().unwrap_or('\0').eq(&POSITIVE) { + s = &a[..idx]; break; } // If no further processing needed to be done, return the line as-is to be sorted - s = b; + s = &a; } // And empty number or non-number lines are to be treated as ‘0’ but only for numeric sort @@ -657,94 +1029,64 @@ fn get_leading_num(a: &str) -> &str { // 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) -> String { +fn get_leading_gen(a: &str) -> &str { // Make this iter peekable to see if next char is numeric - let mut p_iter = leading_num_common(a).chars().peekable(); - let mut r = String::new(); + let raw_leading_num = leading_num_common(a); + let mut p_iter = raw_leading_num.chars().peekable(); + let mut result = ""; // Cleanup raw stripped strings for c in p_iter.to_owned() { let next_char_numeric = p_iter.peek().unwrap_or(&'\0').is_numeric(); // Only general numeric recognizes e notation and, see block below, the '+' sign - if (c.eq(&'e') && !next_char_numeric) || (c.eq(&'E') && !next_char_numeric) { - r = a.split(c).next().unwrap_or("").to_owned(); + // 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 { - let mut v: Vec<&str> = a.split(c).collect(); - let x = v.split_off(1); - r = x.join(""); + result = a.trim().trim_start_matches('+'); break; - // If no further processing needed to be done, return the line as-is to be sorted - } else { - r = a.to_owned(); } + // If no further processing needed to be done, return the line as-is to be sorted + result = a; } - r + result } -fn get_months_dedup(a: &str) -> String { - let pattern = if a.trim().len().ge(&3) { - // Split at 3rd char and get first element of tuple ".0" - a.split_at(3).0 +#[inline(always)] +fn remove_thousands_sep<'a, S: Into>>(input: S) -> Cow<'a, str> { + let input = input.into(); + if input.contains(THOUSANDS_SEP) { + let output = input.replace(THOUSANDS_SEP, ""); + Cow::Owned(output) } else { - "" - }; - - let month = 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, - }; - - if month == Month::Unknown { - "".to_owned() - } else { - pattern.to_uppercase() + input } } -// *For all dedups/uniques we must compare leading numbers* -// Also note numeric compare and unique output is specifically *not* the same as a "sort | uniq" -// See: https://www.gnu.org/software/coreutils/manual/html_node/sort-invocation.html -fn get_nums_dedup(a: &str) -> &str { - // Trim and remove any leading zeros - let s = a.trim().trim_start_matches('0'); - - // Get first char - let c = s.chars().nth(0).unwrap_or('\0'); - - // Empty lines and non-number lines are treated as the same for dedup - if s.is_empty() { - "" - } else if !c.eq(&NEGATIVE) && !c.is_numeric() { - "" - // Prepare lines for comparison of only the numerical leading numbers +#[inline(always)] +fn remove_trailing_dec<'a, S: Into>>(input: S) -> Cow<'a, str> { + let input = input.into(); + if let Some(s) = input.find(DECIMAL_PT) { + let (leading, trailing) = input.split_at(s); + let output = [leading, ".", trailing.replace(DECIMAL_PT, "").as_str()].concat(); + Cow::Owned(output) } else { - get_leading_num(s) + input } } /// Parse the beginning string into an f64, returning -inf instead of NaN on errors. #[inline(always)] fn permissive_f64_parse(a: &str) -> f64 { - // Remove thousands seperators - let a = a.replace(THOUSANDS_SEP, ""); - // 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 - match a.trim().parse::() { + // + // Remove any trailing decimals, ie 4568..890... becomes 4568.890 + // Then, we trim whitespace and parse + match remove_trailing_dec(a).trim().parse::() { Ok(a) if a.is_nan() => std::f64::NEG_INFINITY, Ok(a) => a, Err(_) => std::f64::NEG_INFINITY, @@ -757,8 +1099,13 @@ fn numeric_compare(a: &str, b: &str) -> Ordering { let sa = get_leading_num(a); let sb = get_leading_num(b); - let fa = permissive_f64_parse(sa); - let fb = permissive_f64_parse(sb); + // Avoids a string alloc for every line to remove thousands seperators here + // instead of inside the get_leading_num function, which is a HUGE performance benefit + let ta = remove_thousands_sep(sa); + let tb = remove_thousands_sep(sb); + + let fa = permissive_f64_parse(&ta); + let fb = permissive_f64_parse(&tb); // f64::cmp isn't implemented (due to NaN issues); implement directly instead if fa > fb { @@ -799,8 +1146,8 @@ fn general_numeric_compare(a: &str, b: &str) -> Ordering { // these types of numbers, we rarely care about pure performance. fn human_numeric_convert(a: &str) -> f64 { let num_str = get_leading_num(a); - let suffix = a.trim_start_matches(num_str); - let num_part = permissive_f64_parse(num_str); + let suffix = a.trim_start_matches(&num_str); + let num_part = permissive_f64_parse(&num_str); let suffix: f64 = match suffix.parse().unwrap_or('\0') { // SI Units 'K' => 1E3, @@ -879,7 +1226,7 @@ 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.split_at(3).0 + line.trim().split_at(3).0 } else { "" }; @@ -902,6 +1249,7 @@ fn month_parse(line: &str) -> Month { } fn month_compare(a: &str, b: &str) -> Ordering { + #![allow(clippy::comparison_chain)] let ma = month_parse(a); let mb = month_parse(b); @@ -914,10 +1262,21 @@ fn month_compare(a: &str, b: &str) -> Ordering { } } +fn version_parse(a: &str) -> Version { + let result = Version::parse(a); + + match result { + Ok(vers_a) => vers_a, + // Non-version lines parse to 0.0.0 + Err(_e) => Version::parse("0.0.0").unwrap(), + } +} + fn version_compare(a: &str, b: &str) -> Ordering { #![allow(clippy::comparison_chain)] - let ver_a = Version::parse(a); - let ver_b = Version::parse(b); + let ver_a = version_parse(a); + let ver_b = version_parse(b); + // Version::cmp is not implemented; implement comparison directly if ver_a > ver_b { Ordering::Greater @@ -944,32 +1303,29 @@ fn remove_nonprinting_chars(s: &str) -> String { .collect::() } -fn print_sorted>(iter: T, settings: &Settings) -where - S: std::fmt::Display, -{ +fn print_sorted>(iter: T, settings: &GlobalSettings) { let mut file: Box = match settings.outfile { Some(ref filename) => match File::create(Path::new(&filename)) { Ok(f) => Box::new(BufWriter::new(f)) as Box, Err(e) => { - show_error!("sort: {0}: {1}", filename, e.to_string()); + show_error!("{0}: {1}", filename, e.to_string()); panic!("Could not open output file"); } }, - None => Box::new(stdout()) as Box, + None => Box::new(BufWriter::new(stdout())) as Box, }; - if settings.zero_terminated { for line in iter { - let str = format!("{}\0", line); - crash_if_err!(1, file.write_all(str.as_bytes())); + crash_if_err!(1, file.write_all(line.as_bytes())); + crash_if_err!(1, file.write_all("\0".as_bytes())); } } else { for line in iter { - let str = format!("{}\n", line); - crash_if_err!(1, file.write_all(str.as_bytes())); + crash_if_err!(1, file.write_all(line.as_bytes())); + crash_if_err!(1, file.write_all("\n".as_bytes())); } } + crash_if_err!(1, file.flush()); } // from cat.rs @@ -982,7 +1338,7 @@ fn open(path: &str) -> Option<(Box, bool)> { match File::open(Path::new(path)) { Ok(f) => Some((Box::new(f) as Box, false)), Err(e) => { - show_error!("sort: {0}: {1}", path, e.to_string()); + show_error!("{0}: {1}", path, e.to_string()); None } } @@ -1055,4 +1411,34 @@ mod tests { assert_eq!(Ordering::Less, version_compare(a, b)); } + + #[test] + fn test_random_compare() { + let a = "9"; + let b = "9"; + let c = get_rand_string(); + + assert_eq!(Ordering::Equal, random_shuffle(a, b, c)); + } + + #[test] + fn test_tokenize_fields() { + let line = "foo bar b x"; + assert_eq!(tokenize(line, None), vec![0..3, 3..7, 7..9, 9..14,],); + } + + #[test] + fn test_tokenize_fields_leading_whitespace() { + let line = " foo bar b x"; + assert_eq!(tokenize(line, None), vec![0..7, 7..11, 11..13, 13..18,]); + } + + #[test] + fn test_tokenize_fields_custom_separator() { + let line = "aaa foo bar b x"; + assert_eq!( + tokenize(line, Some('a')), + vec![0..0, 1..1, 2..2, 3..9, 10..18,] + ); + } } diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index 22ce4de6a..884a98785 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/stdbuf.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" tempfile = "3.1" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs index fa36d4ab5..d08427d98 100644 --- a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs +++ b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs @@ -35,8 +35,8 @@ extern "C" { fn set_buffer(stream: *mut FILE, value: &str) { let (mode, size): (c_int, size_t) = match value { - "0" => (_IONBF, 0 as size_t), - "L" => (_IOLBF, 0 as size_t), + "0" => (_IONBF, 0_usize), + "L" => (_IOLBF, 0_usize), input => { let buff_size: usize = match input.parse() { Ok(num) => num, diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index 67ed9a838..8c65a5c7e 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -10,7 +10,8 @@ #[macro_use] extern crate uucore; -use getopts::{Matches, Options}; +use clap::{App, AppSettings, Arg, ArgMatches}; +use std::convert::TryFrom; use std::fs::File; use std::io::{self, Write}; use std::os::unix::process::ExitStatusExt; @@ -19,8 +20,35 @@ use std::process::Command; use tempfile::tempdir; use tempfile::TempDir; -static NAME: &str = "stdbuf"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = + "Run COMMAND, with modified buffering operations for its standard streams.\n\n\ + Mandatory arguments to long options are mandatory for short options too."; +static LONG_HELP: &str = "If MODE is 'L' the corresponding stream will be line buffered.\n\ + This option is invalid with standard input.\n\n\ + If MODE is '0' the corresponding stream will be unbuffered.\n\n\ + Otherwise MODE is a number which may be followed by one of the following:\n\n\ + KB 1000, K 1024, MB 1000*1000, M 1024*1024, and so on for G, T, P, E, Z, Y.\n\ + In this case the corresponding stream will be fully buffered with the buffer size set to \ + MODE bytes.\n\n\ + NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does for e.g.) then \ + that will override corresponding settings changed by 'stdbuf'.\n\ + Also some filters (like 'dd' and 'cat' etc.) don't use streams for I/O, \ + and are thus unaffected by 'stdbuf' settings.\n"; + +mod options { + pub const INPUT: &str = "input"; + pub const INPUT_SHORT: &str = "i"; + pub const OUTPUT: &str = "output"; + pub const OUTPUT_SHORT: &str = "o"; + pub const ERROR: &str = "error"; + pub const ERROR_SHORT: &str = "e"; + pub const COMMAND: &str = "command"; +} + +fn get_usage() -> String { + format!("{0} OPTION... COMMAND", executable!()) +} const STDBUF_INJECT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/libstdbuf.so")); @@ -36,16 +64,19 @@ struct ProgramOptions { stderr: BufferType, } -enum ErrMsg { - Retry, - Fatal, +impl<'a> TryFrom<&ArgMatches<'a>> for ProgramOptions { + type Error = ProgramOptionsError; + + fn try_from(matches: &ArgMatches) -> Result { + Ok(ProgramOptions { + stdin: check_option(&matches, options::INPUT)?, + stdout: check_option(&matches, options::OUTPUT)?, + stderr: check_option(&matches, options::ERROR)?, + }) + } } -enum OkMsg { - Buffering, - Help, - Version, -} +struct ProgramOptionsError(String); #[cfg(any( target_os = "linux", @@ -73,31 +104,6 @@ fn preload_strings() -> (&'static str, &'static str) { crash!(1, "Command not supported for this operating system!") } -fn print_version() { - println!("{} {}", NAME, VERSION); -} - -fn print_usage(opts: &Options) { - let brief = "Run COMMAND, with modified buffering operations for its standard streams\n \ - Mandatory arguments to long options are mandatory for short options too."; - let explanation = "If MODE is 'L' the corresponding stream will be line buffered.\n \ - This option is invalid with standard input.\n\n \ - If MODE is '0' the corresponding stream will be unbuffered.\n\n \ - Otherwise MODE is a number which may be followed by one of the following:\n\n \ - KB 1000, K 1024, MB 1000*1000, M 1024*1024, and so on for G, T, P, E, Z, Y.\n \ - In this case the corresponding stream will be fully buffered with the buffer size set to \ - MODE bytes.\n\n \ - NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does for e.g.) then \ - that will override corresponding settings changed by 'stdbuf'.\n \ - Also some filters (like 'dd' and 'cat' etc.) don't use streams for I/O, \ - and are thus unaffected by 'stdbuf' settings.\n"; - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage: stdbuf OPTION... COMMAND"); - println!(); - println!("{}\n{}", opts.usage(brief), explanation); -} - fn parse_size(size: &str) -> Option { let ext = size.trim_start_matches(|c: char| c.is_digit(10)); let num = size.trim_end_matches(char::is_alphabetic); @@ -133,65 +139,28 @@ fn parse_size(size: &str) -> Option { Some(buf_size * base.pow(power)) } -fn check_option(matches: &Matches, name: &str, modified: &mut bool) -> Option { - match matches.opt_str(name) { - Some(value) => { - *modified = true; - match &value[..] { - "L" => { - if name == "input" { - show_info!("line buffering stdin is meaningless"); - None - } else { - Some(BufferType::Line) - } - } - x => { - let size = match parse_size(x) { - Some(m) => m, - None => { - show_error!("Invalid mode {}", x); - return None; - } - }; - Some(BufferType::Size(size)) +fn check_option(matches: &ArgMatches, name: &str) -> Result { + match matches.value_of(name) { + Some(value) => match value { + "L" => { + if name == options::INPUT { + Err(ProgramOptionsError("line buffering stdin is meaningless".to_string())) + } else { + Ok(BufferType::Line) } } - } - None => Some(BufferType::Default), + x => { + let size = match parse_size(x) { + Some(m) => m, + None => return Err(ProgramOptionsError(format!("invalid mode {}", x))), + }; + Ok(BufferType::Size(size)) + } + }, + None => Ok(BufferType::Default), } } -fn parse_options( - args: &[String], - options: &mut ProgramOptions, - optgrps: &Options, -) -> Result { - let matches = match optgrps.parse(args) { - Ok(m) => m, - Err(_) => return Err(ErrMsg::Retry), - }; - if matches.opt_present("help") { - return Ok(OkMsg::Help); - } - if matches.opt_present("version") { - return Ok(OkMsg::Version); - } - let mut modified = false; - options.stdin = check_option(&matches, "input", &mut modified).ok_or(ErrMsg::Fatal)?; - options.stdout = check_option(&matches, "output", &mut modified).ok_or(ErrMsg::Fatal)?; - options.stderr = check_option(&matches, "error", &mut modified).ok_or(ErrMsg::Fatal)?; - - if matches.free.len() != 1 { - return Err(ErrMsg::Retry); - } - if !modified { - show_error!("you must specify a buffering mode option"); - return Err(ErrMsg::Fatal); - } - Ok(OkMsg::Buffering) -} - fn set_command_env(command: &mut Command, buffer_name: &str, buffer_type: BufferType) { match buffer_type { BufferType::Size(m) => { @@ -215,72 +184,62 @@ fn get_preload_env(tmp_dir: &mut TempDir) -> io::Result<(String, PathBuf)> { } pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(LONG_HELP) + .setting(AppSettings::TrailingVarArg) + .arg( + Arg::with_name(options::INPUT) + .long(options::INPUT) + .short(options::INPUT_SHORT) + .help("adjust standard input stream buffering") + .value_name("MODE") + .required_unless_one(&[options::OUTPUT, options::ERROR]), + ) + .arg( + Arg::with_name(options::OUTPUT) + .long(options::OUTPUT) + .short(options::OUTPUT_SHORT) + .help("adjust standard output stream buffering") + .value_name("MODE") + .required_unless_one(&[options::INPUT, options::ERROR]), + ) + .arg( + Arg::with_name(options::ERROR) + .long(options::ERROR) + .short(options::ERROR_SHORT) + .help("adjust standard error stream buffering") + .value_name("MODE") + .required_unless_one(&[options::INPUT, options::OUTPUT]), + ) + .arg( + Arg::with_name(options::COMMAND) + .multiple(true) + .takes_value(true) + .hidden(true) + .required(true), + ) + .get_matches_from(args); - opts.optopt( - "i", - "input", - "adjust standard input stream buffering", - "MODE", - ); - opts.optopt( - "o", - "output", - "adjust standard output stream buffering", - "MODE", - ); - opts.optopt( - "e", - "error", - "adjust standard error stream buffering", - "MODE", - ); - opts.optflag("", "help", "display this help and exit"); - opts.optflag("", "version", "output version information and exit"); + let options = ProgramOptions::try_from(&matches) + .unwrap_or_else(|e| crash!(125, "{}\nTry 'stdbuf --help' for more information.", e.0)); - let mut options = ProgramOptions { - stdin: BufferType::Default, - stdout: BufferType::Default, - stderr: BufferType::Default, - }; - let mut command_idx: i32 = -1; - for i in 1..=args.len() { - match parse_options(&args[1..i], &mut options, &opts) { - Ok(OkMsg::Buffering) => { - command_idx = (i as i32) - 1; - break; - } - Ok(OkMsg::Help) => { - print_usage(&opts); - return 0; - } - Ok(OkMsg::Version) => { - print_version(); - return 0; - } - Err(ErrMsg::Fatal) => break, - Err(ErrMsg::Retry) => continue, - } - } - if command_idx == -1 { - crash!( - 125, - "Invalid options\nTry 'stdbuf --help' for more information." - ); - } - let command_name = &args[command_idx as usize]; - let mut command = Command::new(command_name); + let mut command_values = matches.values_of::<&str>(options::COMMAND).unwrap(); + let mut command = Command::new(command_values.next().unwrap()); + let command_params: Vec<&str> = command_values.collect(); let mut tmp_dir = tempdir().unwrap(); let (preload_env, libstdbuf) = return_if_err!(1, get_preload_env(&mut tmp_dir)); - command - .args(&args[(command_idx as usize) + 1..]) - .env(preload_env, libstdbuf); + command.env(preload_env, libstdbuf); set_command_env(&mut command, "_STDBUF_I", options.stdin); set_command_env(&mut command, "_STDBUF_O", options.stdout); set_command_env(&mut command, "_STDBUF_E", options.stderr); + command.args(command_params); + let mut process = match command.spawn() { Ok(p) => p, Err(e) => crash!(1, "failed to execute process: {}", e), diff --git a/src/uu/sum/src/sum.rs b/src/uu/sum/src/sum.rs index ed5655a3d..d0fbc7c0d 100644 --- a/src/uu/sum/src/sum.rs +++ b/src/uu/sum/src/sum.rs @@ -75,7 +75,7 @@ fn open(name: &str) -> Result> { "Is a directory", )); }; - if !path.metadata().is_ok() { + if path.metadata().is_err() { return Err(std::io::Error::new( std::io::ErrorKind::NotFound, "No such file or directory", diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 68dae94e2..666ba3384 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -90,7 +90,7 @@ fn tac(filenames: Vec, before: bool, _: bool, separator: &str) -> i32 { Box::new(stdin()) as Box } else { let path = Path::new(filename); - if path.is_dir() || !path.metadata().is_ok() { + if path.is_dir() || path.metadata().is_err() { show_error!( "failed to open '{}' for reading: No such file or directory", filename diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index 4394e4a8e..f882ff5ae 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -55,16 +55,16 @@ fn two(args: &[&[u8]], error: &mut bool) -> bool { b"-d" => path(args[1], PathCondition::Directory), b"-e" => path(args[1], PathCondition::Exists), b"-f" => path(args[1], PathCondition::Regular), - b"-g" => path(args[1], PathCondition::GroupIDFlag), + b"-g" => path(args[1], PathCondition::GroupIdFlag), b"-h" => path(args[1], PathCondition::SymLink), b"-L" => path(args[1], PathCondition::SymLink), b"-n" => one(&args[1..]), - b"-p" => path(args[1], PathCondition::FIFO), + b"-p" => path(args[1], PathCondition::Fifo), b"-r" => path(args[1], PathCondition::Readable), b"-S" => path(args[1], PathCondition::Socket), b"-s" => path(args[1], PathCondition::NonEmpty), b"-t" => isatty(args[1]), - b"-u" => path(args[1], PathCondition::UserIDFlag), + b"-u" => path(args[1], PathCondition::UserIdFlag), b"-w" => path(args[1], PathCondition::Writable), b"-x" => path(args[1], PathCondition::Executable), b"-z" => !one(&args[1..]), @@ -322,13 +322,13 @@ enum PathCondition { Directory, Exists, Regular, - GroupIDFlag, + GroupIdFlag, SymLink, - FIFO, + Fifo, Readable, Socket, NonEmpty, - UserIDFlag, + UserIdFlag, Writable, Executable, } @@ -390,13 +390,13 @@ fn path(path: &[u8], cond: PathCondition) -> bool { PathCondition::Directory => file_type.is_dir(), PathCondition::Exists => true, PathCondition::Regular => file_type.is_file(), - PathCondition::GroupIDFlag => metadata.mode() & S_ISGID != 0, + PathCondition::GroupIdFlag => metadata.mode() & S_ISGID != 0, PathCondition::SymLink => metadata.file_type().is_symlink(), - PathCondition::FIFO => file_type.is_fifo(), + PathCondition::Fifo => file_type.is_fifo(), PathCondition::Readable => perm(metadata, Permission::Read), PathCondition::Socket => file_type.is_socket(), PathCondition::NonEmpty => metadata.size() > 0, - PathCondition::UserIDFlag => metadata.mode() & S_ISUID != 0, + PathCondition::UserIdFlag => metadata.mode() & S_ISUID != 0, PathCondition::Writable => perm(metadata, Permission::Write), PathCondition::Executable => perm(metadata, Permission::Execute), } @@ -416,13 +416,13 @@ fn path(path: &[u8], cond: PathCondition) -> bool { PathCondition::Directory => stat.is_dir(), PathCondition::Exists => true, PathCondition::Regular => stat.is_file(), - PathCondition::GroupIDFlag => false, + PathCondition::GroupIdFlag => false, PathCondition::SymLink => false, - PathCondition::FIFO => false, + PathCondition::Fifo => false, PathCondition::Readable => false, // TODO PathCondition::Socket => false, PathCondition::NonEmpty => stat.len() > 0, - PathCondition::UserIDFlag => false, + PathCondition::UserIdFlag => false, PathCondition::Writable => false, // TODO PathCondition::Executable => false, // TODO } diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 39405900e..b158fdc0e 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -18,6 +18,7 @@ use filetime::*; use std::fs::{self, File}; use std::io::Error; use std::path::Path; +use std::process; static VERSION: &str = env!("CARGO_PKG_VERSION"); static ABOUT: &str = "Update the access and modification times of each FILE to the current time."; @@ -137,7 +138,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let (mut atime, mut mtime) = if matches.is_present(options::sources::REFERENCE) { stat( - &matches.value_of(options::sources::REFERENCE).unwrap()[..], + matches.value_of(options::sources::REFERENCE).unwrap(), !matches.is_present(options::NO_DEREF), ) } else if matches.is_present(options::sources::DATE) @@ -261,7 +262,27 @@ fn parse_timestamp(s: &str) -> FileTime { }; match time::strptime(&ts, format) { - Ok(tm) => local_tm_to_filetime(to_local(tm)), + Ok(tm) => { + let mut local = to_local(tm); + local.tm_isdst = -1; + let ft = local_tm_to_filetime(local); + + // We have to check that ft is valid time. Due to daylight saving + // time switch, local time can jump from 1:59 AM to 3:00 AM, + // in which case any time between 2:00 AM and 2:59 AM is not valid. + // Convert back to local time and see if we got the same value back. + let ts = time::Timespec { + sec: ft.unix_seconds(), + nsec: 0, + }; + let tm2 = time::at(ts); + if tm.tm_hour != tm2.tm_hour { + show_error!("invalid date format {}", s); + process::exit(1); + } + + ft + } Err(e) => panic!("Unable to parse timestamp\n{}", e), } } diff --git a/src/uu/tr/src/expand.rs b/src/uu/tr/src/expand.rs index e71cf262c..73612a065 100644 --- a/src/uu/tr/src/expand.rs +++ b/src/uu/tr/src/expand.rs @@ -24,7 +24,7 @@ use std::ops::RangeInclusive; fn parse_sequence(s: &str) -> (char, usize) { let c = s.chars().next().expect("invalid escape: empty string"); - if '0' <= c && c <= '7' { + if ('0'..='7').contains(&c) { let mut v = c.to_digit(8).unwrap(); let mut consumed = 1; let bits_per_digit = 3; diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 3440972a2..967a9514a 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -16,8 +16,8 @@ use std::io::{stdin, BufRead, BufReader, Read}; use std::path::Path; static VERSION: &str = env!("CARGO_PKG_VERSION"); -static SUMMARY: &str = "Topological sort the strings in FILE. -Strings are defined as any sequence of tokens separated by whitespace (tab, space, or newline). +static SUMMARY: &str = "Topological sort the strings in FILE. +Strings are defined as any sequence of tokens separated by whitespace (tab, space, or newline). If FILE is not passed in, stdin is used instead."; static USAGE: &str = "tsort [OPTIONS] FILE"; @@ -32,13 +32,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .version(VERSION) .usage(USAGE) .about(SUMMARY) - .arg(Arg::with_name(options::FILE).hidden(true)) + .arg( + Arg::with_name(options::FILE) + .default_value("-") + .hidden(true), + ) .get_matches_from(args); - let input = match matches.value_of(options::FILE) { - Some(v) => v, - None => "-", - }; + let input = matches + .value_of(options::FILE) + .expect("Value is required by clap"); let mut stdin_buf; let mut file_buf; diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index 18d69db46..815c6f96b 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -65,9 +65,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } - return if is_stdin_interactive() { + if is_stdin_interactive() { libc::EXIT_SUCCESS } else { libc::EXIT_FAILURE - }; + } } diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index 5b08c33cf..3d80bd6e9 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -149,10 +149,8 @@ fn next_tabstop(tabstops: &[usize], col: usize) -> Option { Some(tabstops[0] - col % tabstops[0]) } else { // find next larger tab - match tabstops.iter().find(|&&t| t > col) { - Some(t) => Some(t - col), - None => None, // if there isn't one in the list, tab becomes a single space - } + // if there isn't one in the list, tab becomes a single space + tabstops.iter().find(|&&t| t > col).map(|t| t-col) } } diff --git a/src/uu/unlink/Cargo.toml b/src/uu/unlink/Cargo.toml index b193bd1b5..08da2624e 100644 --- a/src/uu/unlink/Cargo.toml +++ b/src/uu/unlink/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/unlink.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/unlink/src/unlink.rs b/src/uu/unlink/src/unlink.rs index b85b6ea94..26460e59c 100644 --- a/src/uu/unlink/src/unlink.rs +++ b/src/uu/unlink/src/unlink.rs @@ -12,59 +12,53 @@ #[macro_use] extern crate uucore; -use getopts::Options; +use clap::{App, Arg}; use libc::{lstat, stat, unlink}; use libc::{S_IFLNK, S_IFMT, S_IFREG}; use std::ffi::CString; use std::io::{Error, ErrorKind}; -static NAME: &str = "unlink"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Unlink the file at [FILE]."; +static OPT_PATH: &str = "FILE"; + +fn get_usage() -> String { + format!("{} [OPTION]... FILE", executable!()) +} pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let mut opts = Options::new(); + let usage = get_usage(); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg(Arg::with_name(OPT_PATH).hidden(true).multiple(true)) + .get_matches_from(args); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "invalid options\n{}", f), - }; + let paths: Vec = matches + .values_of(OPT_PATH) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); - if matches.opt_present("help") { - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage:"); - println!(" {} [FILE]... [OPTION]...", NAME); - println!(); - println!("{}", opts.usage("Unlink the file at [FILE].")); - return 0; - } - - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; - } - - if matches.free.is_empty() { + if paths.is_empty() { crash!( 1, "missing operand\nTry '{0} --help' for more information.", - NAME + executable!() ); - } else if matches.free.len() > 1 { + } else if paths.len() > 1 { crash!( 1, "extra operand: '{1}'\nTry '{0} --help' for more information.", - NAME, - matches.free[1] + executable!(), + paths[1] ); } - let c_string = CString::new(matches.free[0].clone()).unwrap(); // unwrap() cannot fail, the string comes from argv so it cannot contain a \0. + let c_string = CString::new(paths[0].clone()).unwrap(); // unwrap() cannot fail, the string comes from argv so it cannot contain a \0. let st_mode = { #[allow(deprecated)] @@ -72,12 +66,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let result = unsafe { lstat(c_string.as_ptr(), &mut buf as *mut stat) }; if result < 0 { - crash!( - 1, - "Cannot stat '{}': {}", - matches.free[0], - Error::last_os_error() - ); + crash!(1, "Cannot stat '{}': {}", paths[0], Error::last_os_error()); } buf.st_mode & S_IFMT @@ -101,7 +90,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { match result { Ok(_) => (), Err(e) => { - crash!(1, "cannot unlink '{0}': {1}", matches.free[0], e); + crash!(1, "cannot unlink '{0}': {1}", paths[0], e); } } diff --git a/src/uu/wc/src/count_bytes.rs b/src/uu/wc/src/count_bytes.rs index dc90f67cc..7f06f8171 100644 --- a/src/uu/wc/src/count_bytes.rs +++ b/src/uu/wc/src/count_bytes.rs @@ -20,6 +20,21 @@ use nix::unistd::pipe; const BUF_SIZE: usize = 16384; +/// 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(()) +} + /// This is a Linux-specific function to count the number of bytes using the /// `splice` system call, which is faster than using `read`. #[inline] @@ -39,7 +54,7 @@ fn count_bytes_using_splice(fd: RawFd) -> nix::Result { break; } byte_count += res; - splice(pipe_rd, None, null, None, res, SpliceFFlags::empty())?; + splice_exact(pipe_rd, null, res)?; } Ok(byte_count) @@ -57,30 +72,27 @@ pub(crate) fn count_bytes_fast(handle: &mut T) -> WcResult { - // If the file is regular, then the `st_size` should hold - // the file's size in bytes. - if (stat.st_mode & S_IFREG) != 0 { - return Ok(stat.st_size as usize); - } - #[cfg(any(target_os = "linux", target_os = "android"))] - { - // Else, if we're on Linux and our file is a FIFO pipe - // (or stdin), we use splice to count the number of bytes. - if (stat.st_mode & S_IFIFO) != 0 { - if let Ok(n) = count_bytes_using_splice(fd) { - return Ok(n); - } + if let Ok(stat) = fstat(fd) { + // If the file is regular, then the `st_size` should hold + // the file's size in bytes. + if (stat.st_mode & S_IFREG) != 0 { + return Ok(stat.st_size as usize); + } + #[cfg(any(target_os = "linux", target_os = "android"))] + { + // Else, if we're on Linux and our file is a FIFO pipe + // (or stdin), we use splice to count the number of bytes. + if (stat.st_mode & S_IFIFO) != 0 { + if let Ok(n) = count_bytes_using_splice(fd) { + return Ok(n); } } } - _ => {} } } // Fall back on `read`, but without the overhead of counting words and lines. - let mut buf = [0 as u8; BUF_SIZE]; + let mut buf = [0_u8; BUF_SIZE]; let mut byte_count = 0; loop { match handle.read(&mut buf) { diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 22463caa4..59ca10141 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -138,11 +138,8 @@ impl AddAssign for WordCount { } impl WordCount { - fn with_title<'a>(self, title: &'a str) -> TitledWordCount<'a> { - return TitledWordCount { - title: title, - count: self, - }; + fn with_title(self, title: &str) -> TitledWordCount { + TitledWordCount { title, count: self } } } @@ -251,7 +248,7 @@ fn is_word_separator(byte: u8) -> bool { fn word_count_from_reader( mut reader: T, settings: &Settings, - path: &String, + path: &str, ) -> WcResult { let only_count_bytes = settings.show_bytes && (!(settings.show_chars @@ -333,18 +330,18 @@ fn word_count_from_reader( }) } -fn word_count_from_path(path: &String, settings: &Settings) -> WcResult { +fn word_count_from_path(path: &str, settings: &Settings) -> WcResult { if path == "-" { let stdin = io::stdin(); let stdin_lock = stdin.lock(); - return Ok(word_count_from_reader(stdin_lock, settings, path)?); + word_count_from_reader(stdin_lock, settings, path) } else { let path_obj = Path::new(path); if path_obj.is_dir() { - return Err(WcError::IsDirectory(path.clone())); + Err(WcError::IsDirectory(path.to_owned())) } else { let file = File::open(path)?; - return Ok(word_count_from_reader(file, settings, path)?); + word_count_from_reader(file, settings, path) } } } @@ -425,7 +422,7 @@ fn print_stats( } if result.title == "-" { - writeln!(stdout_lock, "")?; + writeln!(stdout_lock)?; } else { writeln!(stdout_lock, " {}", result.title)?; } diff --git a/src/uu/who/src/who.rs b/src/uu/who/src/who.rs index 8c7ff3211..9444985dc 100644 --- a/src/uu/who/src/who.rs +++ b/src/uu/who/src/who.rs @@ -222,7 +222,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { need_runlevel, need_users, my_line_only, - has_records: false, args: matches.free, }; @@ -247,7 +246,6 @@ struct Who { need_runlevel: bool, need_users: bool, my_line_only: bool, - has_records: bool, args: Vec, } @@ -321,8 +319,7 @@ impl Who { println!("{}", users.join(" ")); println!("# users={}", users.len()); } else { - let mut records = Utmpx::iter_all_records().read_from(f).peekable(); - self.has_records = records.peek().is_some(); + let records = Utmpx::iter_all_records().read_from(f).peekable(); if self.include_heading { self.print_heading() diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index 66db15451..36f56206d 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -31,9 +31,9 @@ fn chgrp>(path: P, dgid: gid_t, follow: bool) -> IOResult<()> { let s = CString::new(path.as_os_str().as_bytes()).unwrap(); let ret = unsafe { if follow { - libc::chown(s.as_ptr(), (0 as gid_t).wrapping_sub(1), dgid) + libc::chown(s.as_ptr(), 0_u32.wrapping_sub(1), dgid) } else { - lchown(s.as_ptr(), (0 as gid_t).wrapping_sub(1), dgid) + lchown(s.as_ptr(), 0_u32.wrapping_sub(1), dgid) } }; if ret == 0 { diff --git a/src/uucore/src/lib/macros.rs b/src/uucore/src/lib/macros.rs index 24b392ebd..637e91f8f 100644 --- a/src/uucore/src/lib/macros.rs +++ b/src/uucore/src/lib/macros.rs @@ -31,6 +31,14 @@ macro_rules! show_error( ); /// Show a warning to stderr in a silimar style to GNU coreutils. +#[macro_export] +macro_rules! show_error_custom_description ( + ($err:expr,$($args:tt)+) => ({ + eprint!("{}: {}: ", executable!(), $err); + eprintln!($($args)+); + }) +); + #[macro_export] macro_rules! show_warning( ($($args:tt)+) => ({ diff --git a/tests/by-util/test_arch.rs b/tests/by-util/test_arch.rs index d2ec138d9..909e0ee80 100644 --- a/tests/by-util/test_arch.rs +++ b/tests/by-util/test_arch.rs @@ -2,17 +2,13 @@ use crate::common::util::*; #[test] fn test_arch() { - let (_, mut ucmd) = at_and_ucmd!(); - - let result = ucmd.run(); - assert!(result.success); + new_ucmd!().succeeds(); } #[test] fn test_arch_help() { - let (_, mut ucmd) = at_and_ucmd!(); - - let result = ucmd.arg("--help").run(); - assert!(result.success); - assert!(result.stdout.contains("architecture name")); + new_ucmd!() + .arg("--help") + .succeeds() + .stdout_contains("architecture name"); } diff --git a/tests/by-util/test_basename.rs b/tests/by-util/test_basename.rs index fa599644d..3483e800c 100644 --- a/tests/by-util/test_basename.rs +++ b/tests/by-util/test_basename.rs @@ -66,7 +66,7 @@ fn test_zero_param() { } fn expect_error(input: Vec<&str>) { - assert!(new_ucmd!().args(&input).fails().no_stdout().stderr.len() > 0); + assert!(new_ucmd!().args(&input).fails().no_stdout().stderr().len() > 0); } #[test] diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index 481b1683d..eb6cc9148 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -1,7 +1,5 @@ -#[cfg(unix)] -extern crate unix_socket; - use crate::common::util::*; +use std::io::Read; #[test] fn test_output_simple() { @@ -11,6 +9,131 @@ fn test_output_simple() { .stdout_only("abcde\nfghij\nklmno\npqrst\nuvwxyz\n"); } +#[test] +fn test_no_options() { + for fixture in &["empty.txt", "alpha.txt", "nonewline.txt"] { + // Give fixture through command line file argument + new_ucmd!() + .args(&[fixture]) + .succeeds() + .stdout_is_fixture(fixture); + // Give fixture through stdin + new_ucmd!() + .pipe_in_fixture(fixture) + .succeeds() + .stdout_is_fixture(fixture); + } +} + +#[test] +#[cfg(unix)] +fn test_no_options_big_input() { + for &n in &[ + 0, + 1, + 42, + 16 * 1024 - 7, + 16 * 1024 - 1, + 16 * 1024, + 16 * 1024 + 1, + 16 * 1024 + 3, + 32 * 1024, + 64 * 1024, + 80 * 1024, + 96 * 1024, + 112 * 1024, + 128 * 1024, + ] { + let data = vec_of_size(n); + let data2 = data.clone(); + assert_eq!(data.len(), data2.len()); + new_ucmd!().pipe_in(data).succeeds().stdout_is_bytes(&data2); + } +} + +#[test] +#[cfg(unix)] +fn test_fifo_symlink() { + use std::fs::OpenOptions; + use std::io::Write; + use std::thread; + + let s = TestScenario::new(util_name!()); + s.fixtures.mkdir("dir"); + s.fixtures.mkfifo("dir/pipe"); + assert!(s.fixtures.is_fifo("dir/pipe")); + + // Make cat read the pipe through a symlink + s.fixtures.symlink_file("dir/pipe", "sympipe"); + let proc = s.ucmd().args(&["sympipe"]).run_no_wait(); + + let data = vec_of_size(128 * 1024); + let data2 = data.clone(); + + let pipe_path = s.fixtures.plus("dir/pipe"); + let thread = thread::spawn(move || { + let mut pipe = OpenOptions::new() + .write(true) + .create(false) + .open(pipe_path) + .unwrap(); + pipe.write_all(&data).unwrap(); + }); + + let output = proc.wait_with_output().unwrap(); + assert_eq!(&output.stdout, &data2); + thread.join().unwrap(); +} + +#[test] +fn test_directory() { + let s = TestScenario::new(util_name!()); + s.fixtures.mkdir("test_directory"); + s.ucmd() + .args(&["test_directory"]) + .fails() + .stderr_is("cat: test_directory: Is a directory"); +} + +#[test] +fn test_directory_and_file() { + let s = TestScenario::new(util_name!()); + s.fixtures.mkdir("test_directory2"); + for fixture in &["empty.txt", "alpha.txt", "nonewline.txt"] { + s.ucmd() + .args(&["test_directory2", fixture]) + .fails() + .stderr_is("cat: test_directory2: Is a directory") + .stdout_is_fixture(fixture); + } +} + +#[test] +#[cfg(unix)] +fn test_three_directories_and_file_and_stdin() { + let s = TestScenario::new(util_name!()); + s.fixtures.mkdir("test_directory3"); + s.fixtures.mkdir("test_directory3/test_directory4"); + s.fixtures.mkdir("test_directory3/test_directory5"); + s.ucmd() + .args(&[ + "test_directory3/test_directory4", + "alpha.txt", + "-", + "filewhichdoesnotexist.txt", + "nonewline.txt", + "test_directory3/test_directory5", + "test_directory3/../test_directory3/test_directory5", + "test_directory3", + ]) + .pipe_in("stdout bytes") + .fails() + .stderr_is_fixture("three_directories_and_file_and_stdin.stderr.expected") + .stdout_is( + "abcde\nfghij\nklmno\npqrst\nuvwxyz\nstdout bytestext without a trailing newline", + ); +} + #[test] fn test_output_multi_files_print_all_chars() { new_ucmd!() @@ -149,13 +272,64 @@ fn test_squeeze_blank_before_numbering() { } } +/// This tests reading from Unix character devices #[test] -#[cfg(foo)] +#[cfg(unix)] +fn test_dev_random() { + let mut buf = [0; 2048]; + let mut proc = new_ucmd!().args(&["/dev/random"]).run_no_wait(); + let mut proc_stdout = proc.stdout.take().unwrap(); + proc_stdout.read_exact(&mut buf).unwrap(); + + let num_zeroes = buf.iter().fold(0, |mut acc, &n| { + if n == 0 { + acc += 1; + } + acc + }); + // The probability of more than 512 zero bytes is essentially zero if the + // output is truly random. + assert!(num_zeroes < 512); + proc.kill().unwrap(); +} + +/// Reading from /dev/full should return an infinite amount of zero bytes. +/// Wikipedia says there is support on Linux, FreeBSD, and NetBSD. +#[test] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +fn test_dev_full() { + let mut buf = [0; 2048]; + let mut proc = new_ucmd!().args(&["/dev/full"]).run_no_wait(); + let mut proc_stdout = proc.stdout.take().unwrap(); + let expected = [0; 2048]; + proc_stdout.read_exact(&mut buf).unwrap(); + assert_eq!(&buf[..], &expected[..]); + proc.kill().unwrap(); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +fn test_dev_full_show_all() { + let mut buf = [0; 2048]; + let mut proc = new_ucmd!().args(&["-A", "/dev/full"]).run_no_wait(); + let mut proc_stdout = proc.stdout.take().unwrap(); + proc_stdout.read_exact(&mut buf).unwrap(); + + let expected: Vec = (0..buf.len()) + .map(|n| if n & 1 == 0 { b'^' } else { b'@' }) + .collect(); + + assert_eq!(&buf[..], &expected[..]); + proc.kill().unwrap(); +} + +#[test] +#[cfg(unix)] fn test_domain_socket() { - use self::tempdir::TempDir; - use self::unix_socket::UnixListener; use std::io::prelude::*; use std::thread; + use tempdir::TempDir; + use unix_socket::UnixListener; let dir = TempDir::new("unix_socket").expect("failed to create dir"); let socket_path = dir.path().join("sock"); diff --git a/tests/by-util/test_chgrp.rs b/tests/by-util/test_chgrp.rs index 613f52fd2..343b336a6 100644 --- a/tests/by-util/test_chgrp.rs +++ b/tests/by-util/test_chgrp.rs @@ -149,7 +149,7 @@ fn test_big_h() { .arg("bin") .arg("/proc/self/fd") .fails() - .stderr + .stderr_str() .lines() .fold(0, |acc, _| acc + 1) > 1 diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index b85567166..d60b8a50b 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -48,7 +48,7 @@ fn run_single_test(test: &TestCase, at: AtPath, mut ucmd: UCommand) { } let r = ucmd.run(); if !r.success { - println!("{}", r.stderr); + println!("{}", r.stderr_str()); panic!("{:?}: failed", ucmd.raw); } @@ -297,13 +297,14 @@ fn test_chmod_recursive() { mkfile(&at.plus_as_string("a/b/c/c"), 0o100444); mkfile(&at.plus_as_string("z/y"), 0o100444); - let result = ucmd - .arg("-R") + ucmd.arg("-R") .arg("--verbose") .arg("-r,a+w") .arg("a") .arg("z") - .succeeds(); + .succeeds() + .stderr_contains(&"to 333 (-wx-wx-wx)") + .stderr_contains(&"to 222 (-w--w--w-)"); assert_eq!(at.metadata("z/y").permissions().mode(), 0o100222); assert_eq!(at.metadata("a/a").permissions().mode(), 0o100222); @@ -312,8 +313,6 @@ fn test_chmod_recursive() { println!("mode {:o}", at.metadata("a").permissions().mode()); assert_eq!(at.metadata("a").permissions().mode(), 0o40333); assert_eq!(at.metadata("z").permissions().mode(), 0o40333); - assert!(result.stderr.contains("to 333 (-wx-wx-wx)")); - assert!(result.stderr.contains("to 222 (-w--w--w-)")); unsafe { umask(original_umask); @@ -322,30 +321,24 @@ fn test_chmod_recursive() { #[test] fn test_chmod_non_existing_file() { - let (_at, mut ucmd) = at_and_ucmd!(); - let result = ucmd + new_ucmd!() .arg("-R") .arg("--verbose") .arg("-r,a+w") .arg("dont-exist") - .fails(); - assert!(result - .stderr - .contains("cannot access 'dont-exist': No such file or directory")); + .fails() + .stderr_contains(&"cannot access 'dont-exist': No such file or directory"); } #[test] fn test_chmod_preserve_root() { - let (_at, mut ucmd) = at_and_ucmd!(); - let result = ucmd + new_ucmd!() .arg("-R") .arg("--preserve-root") .arg("755") .arg("/") - .fails(); - assert!(result - .stderr - .contains("chmod: error: it is dangerous to operate recursively on '/'")); + .fails() + .stderr_contains(&"chmod: error: it is dangerous to operate recursively on '/'"); } #[test] @@ -362,33 +355,27 @@ fn test_chmod_symlink_non_existing_file() { let expected_stderr = &format!("cannot operate on dangling symlink '{}'", test_symlink); at.symlink_file(non_existing, test_symlink); - let mut result; // this cannot succeed since the symbolic link dangles - result = scene.ucmd().arg("755").arg("-v").arg(test_symlink).fails(); - - println!("stdout = {:?}", result.stdout); - println!("stderr = {:?}", result.stderr); - - assert!(result.stdout.contains(expected_stdout)); - assert!(result.stderr.contains(expected_stderr)); - assert_eq!(result.code, Some(1)); + scene.ucmd() + .arg("755") + .arg("-v") + .arg(test_symlink) + .fails() + .code_is(1) + .stdout_contains(expected_stdout) + .stderr_contains(expected_stderr); // this should be the same than with just '-v' but without stderr - result = scene - .ucmd() + scene.ucmd() .arg("755") .arg("-v") .arg("-f") .arg(test_symlink) - .fails(); - - println!("stdout = {:?}", result.stdout); - println!("stderr = {:?}", result.stderr); - - assert!(result.stdout.contains(expected_stdout)); - assert!(result.stderr.is_empty()); - assert_eq!(result.code, Some(1)); + .run() + .code_is(1) + .no_stderr() + .stdout_contains(expected_stdout); } #[test] @@ -405,18 +392,15 @@ fn test_chmod_symlink_non_existing_file_recursive() { non_existing, &format!("{}/{}", test_directory, test_symlink), ); - let mut result; // this should succeed - result = scene - .ucmd() + scene.ucmd() .arg("-R") .arg("755") .arg(test_directory) - .succeeds(); - assert_eq!(result.code, Some(0)); - assert!(result.stdout.is_empty()); - assert!(result.stderr.is_empty()); + .succeeds() + .no_stderr() + .no_stdout(); let expected_stdout = &format!( "mode of '{}' retained as 0755 (rwxr-xr-x)\nneither symbolic link '{}/{}' nor referent has been changed", @@ -424,37 +408,25 @@ fn test_chmod_symlink_non_existing_file_recursive() { ); // '-v': this should succeed without stderr - result = scene - .ucmd() + scene.ucmd() .arg("-R") .arg("-v") .arg("755") .arg(test_directory) - .succeeds(); - - println!("stdout = {:?}", result.stdout); - println!("stderr = {:?}", result.stderr); - - assert!(result.stdout.contains(expected_stdout)); - assert!(result.stderr.is_empty()); - assert_eq!(result.code, Some(0)); + .succeeds() + .stdout_contains(expected_stdout) + .no_stderr(); // '-vf': this should be the same than with just '-v' - result = scene - .ucmd() + scene.ucmd() .arg("-R") .arg("-v") .arg("-f") .arg("755") .arg(test_directory) - .succeeds(); - - println!("stdout = {:?}", result.stdout); - println!("stderr = {:?}", result.stderr); - - assert!(result.stdout.contains(expected_stdout)); - assert!(result.stderr.is_empty()); - assert_eq!(result.code, Some(0)); + .succeeds() + .stdout_contains(expected_stdout) + .no_stderr(); } #[test] diff --git a/tests/by-util/test_chown.rs b/tests/by-util/test_chown.rs index 7b663e9c9..e27fba3d4 100644 --- a/tests/by-util/test_chown.rs +++ b/tests/by-util/test_chown.rs @@ -53,22 +53,22 @@ fn test_chown_myself() { // test chown username file.txt 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; } - println!("results {}", result.stdout); - let username = result.stdout.trim_end(); + println!("results {}", result.stdout_str()); + let username = result.stdout_str().trim_end(); let (at, mut ucmd) = at_and_ucmd!(); let file1 = "test_install_target_dir_file_a1"; at.touch(file1); let result = ucmd.arg(username).arg(file1).run(); - println!("results stdout {}", result.stdout); - println!("results stderr {}", result.stderr); - if is_ci() && result.stderr.contains("invalid user") { + println!("results stdout {}", result.stdout_str()); + println!("results stderr {}", result.stderr_str()); + if is_ci() && result.stderr_str().contains("invalid user") { // In the CI, some server are failing to return id. // As seems to be a configuration issue, ignoring it return; @@ -81,24 +81,24 @@ fn test_chown_myself_second() { // test chown username: file.txt 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; } - println!("results {}", result.stdout); + println!("results {}", result.stdout_str()); let (at, mut ucmd) = at_and_ucmd!(); let file1 = "test_install_target_dir_file_a1"; at.touch(file1); let result = ucmd - .arg(result.stdout.trim_end().to_owned() + ":") + .arg(result.stdout_str().trim_end().to_owned() + ":") .arg(file1) .run(); - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); assert!(result.success); } @@ -107,31 +107,31 @@ fn test_chown_myself_group() { // test chown username:group file.txt 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; } - println!("user name = {}", result.stdout); - let username = result.stdout.trim_end(); + println!("user name = {}", result.stdout_str()); + let username = result.stdout_str().trim_end(); let result = scene.cmd("id").arg("-gn").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; } - println!("group name = {}", result.stdout); - let group = result.stdout.trim_end(); + println!("group name = {}", result.stdout_str()); + let group = result.stdout_str().trim_end(); let (at, mut ucmd) = at_and_ucmd!(); let file1 = "test_install_target_dir_file_a1"; let perm = username.to_owned() + ":" + group; at.touch(file1); let result = ucmd.arg(perm).arg(file1).run(); - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); - if is_ci() && result.stderr.contains("chown: invalid group:") { + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + if is_ci() && result.stderr_str().contains("chown: invalid group:") { // With some Ubuntu into the CI, we can get this answer return; } @@ -143,27 +143,27 @@ fn test_chown_only_group() { // test chown :group file.txt 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; } - println!("results {}", result.stdout); + println!("results {}", result.stdout_str()); let (at, mut ucmd) = at_and_ucmd!(); let file1 = "test_install_target_dir_file_a1"; - let perm = ":".to_owned() + result.stdout.trim_end(); + let perm = ":".to_owned() + result.stdout_str().trim_end(); at.touch(file1); let result = ucmd.arg(perm).arg(file1).run(); - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); - if is_ci() && result.stderr.contains("Operation not permitted") { + if is_ci() && result.stderr_str().contains("Operation not permitted") { // With ubuntu with old Rust in the CI, we can get an error return; } - if is_ci() && result.stderr.contains("chown: invalid group:") { + if is_ci() && result.stderr_str().contains("chown: invalid group:") { // With mac into the CI, we can get this answer return; } @@ -174,14 +174,14 @@ fn test_chown_only_group() { fn test_chown_only_id() { // test chown 1111 file.txt let result = TestScenario::new("id").ucmd_keepenv().arg("-u").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; } - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); - let id = String::from(result.stdout.trim()); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + let id = String::from(result.stdout_str().trim()); let (at, mut ucmd) = at_and_ucmd!(); let file1 = "test_install_target_dir_file_a1"; @@ -189,9 +189,9 @@ fn test_chown_only_id() { at.touch(file1); let result = ucmd.arg(id).arg(file1).run(); - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); - if is_ci() && result.stderr.contains("chown: invalid user:") { + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + if is_ci() && result.stderr_str().contains("chown: invalid user:") { // With some Ubuntu into the CI, we can get this answer return; } @@ -202,14 +202,14 @@ fn test_chown_only_id() { fn test_chown_only_group_id() { // test chown :1111 file.txt let result = TestScenario::new("id").ucmd_keepenv().arg("-g").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; } - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); - let id = String::from(result.stdout.trim()); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + let id = String::from(result.stdout_str().trim()); let (at, mut ucmd) = at_and_ucmd!(); let file1 = "test_install_target_dir_file_a1"; @@ -219,9 +219,9 @@ fn test_chown_only_group_id() { let result = ucmd.arg(perm).arg(file1).run(); - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); - if is_ci() && result.stderr.contains("chown: invalid group:") { + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + if is_ci() && result.stderr_str().contains("chown: invalid group:") { // With mac into the CI, we can get this answer return; } @@ -232,24 +232,24 @@ fn test_chown_only_group_id() { fn test_chown_both_id() { // test chown 1111:1111 file.txt let result = TestScenario::new("id").ucmd_keepenv().arg("-u").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; } - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); - let id_user = String::from(result.stdout.trim()); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + let id_user = String::from(result.stdout_str().trim()); let result = TestScenario::new("id").ucmd_keepenv().arg("-g").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; } - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); - let id_group = String::from(result.stdout.trim()); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + let id_group = String::from(result.stdout_str().trim()); let (at, mut ucmd) = at_and_ucmd!(); let file1 = "test_install_target_dir_file_a1"; @@ -258,10 +258,10 @@ fn test_chown_both_id() { let perm = id_user + &":".to_owned() + &id_group; let result = ucmd.arg(perm).arg(file1).run(); - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); - if is_ci() && result.stderr.contains("invalid user") { + if is_ci() && result.stderr_str().contains("invalid user") { // In the CI, some server are failing to return id. // As seems to be a configuration issue, ignoring it return; @@ -274,24 +274,24 @@ fn test_chown_both_id() { fn test_chown_both_mix() { // test chown 1111:1111 file.txt let result = TestScenario::new("id").ucmd_keepenv().arg("-u").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; } - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); - let id_user = String::from(result.stdout.trim()); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + let id_user = String::from(result.stdout_str().trim()); let result = TestScenario::new("id").ucmd_keepenv().arg("-gn").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; } - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); - let group_name = String::from(result.stdout.trim()); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + let group_name = String::from(result.stdout_str().trim()); let (at, mut ucmd) = at_and_ucmd!(); let file1 = "test_install_target_dir_file_a1"; @@ -301,7 +301,7 @@ fn test_chown_both_mix() { let result = ucmd.arg(perm).arg(file1).run(); - if is_ci() && result.stderr.contains("invalid user") { + if is_ci() && result.stderr_str().contains("invalid user") { // In the CI, some server are failing to return id. // As seems to be a configuration issue, ignoring it return; @@ -313,14 +313,14 @@ fn test_chown_both_mix() { fn test_chown_recursive() { 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; } - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); - let username = result.stdout.trim_end(); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + let username = result.stdout_str().trim_end(); let (at, mut ucmd) = at_and_ucmd!(); at.mkdir("a"); @@ -339,31 +339,32 @@ fn test_chown_recursive() { .arg("a") .arg("z") .run(); - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); - if is_ci() && result.stderr.contains("invalid user") { + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + if is_ci() && result.stderr_str().contains("invalid user") { // In the CI, some server are failing to return id. // As seems to be a configuration issue, ignoring it return; } - assert!(result.stderr.contains("ownership of 'a/a' retained as")); - assert!(result.stderr.contains("ownership of 'z/y' retained as")); - assert!(result.success); + result + .stderr_contains(&"ownership of 'a/a' retained as") + .stderr_contains(&"ownership of 'z/y' retained as") + .success(); } #[test] fn test_root_preserve() { 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; } - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); - let username = result.stdout.trim_end(); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + let username = result.stdout_str().trim_end(); let result = new_ucmd!() .arg("--preserve-root") @@ -371,9 +372,9 @@ fn test_root_preserve() { .arg(username) .arg("/") .fails(); - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); - if is_ci() && result.stderr.contains("invalid user") { + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + if is_ci() && result.stderr_str().contains("invalid user") { // In the CI, some server are failing to return id. // As seems to be a configuration issue, ignoring it return; diff --git a/tests/by-util/test_chroot.rs b/tests/by-util/test_chroot.rs index 9a8fb71dd..05efd23ae 100644 --- a/tests/by-util/test_chroot.rs +++ b/tests/by-util/test_chroot.rs @@ -64,14 +64,14 @@ fn test_preference_of_userspec() { // As seems to be a configuration issue, ignoring it return; } - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); - let username = result.stdout.trim_end(); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + let username = result.stdout_str().trim_end(); let ts = TestScenario::new("id"); let result = ts.cmd("id").arg("-g").arg("-n").run(); - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); if is_ci() && result.stderr.contains("cannot find name for user ID") { // In the CI, some server are failing to return id. @@ -79,7 +79,7 @@ fn test_preference_of_userspec() { return; } - let group_name = result.stdout.trim_end(); + let group_name = result.stdout_str().trim_end(); let (at, mut ucmd) = at_and_ucmd!(); at.mkdir("a"); @@ -93,6 +93,6 @@ fn test_preference_of_userspec() { .arg(format!("--userspec={}:{}", username, group_name)) .run(); - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); } diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 8b41c782c..1a0915cd5 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -35,14 +35,19 @@ fn test_empty() { } #[test] -#[ignore] fn test_arg_overrides_stdin() { let (at, mut ucmd) = at_and_ucmd!(); let input = "foobarfoobar"; at.touch("a"); - let result = ucmd.arg("a").pipe_in(input.as_bytes()).run(); + let result = ucmd + .arg("a") + .pipe_in(input.as_bytes()) + // the command might have exited before all bytes have been pipe in. + // in that case, we don't care about the error (broken pipe) + .ignore_stdin_write_error() + .run(); println!("{}, {}", result.stdout, result.stderr); diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 1fa8212ca..07880d5c0 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -275,8 +275,8 @@ fn test_cp_arg_no_clobber_twice() { .arg("dest.txt") .run(); - println!("stderr = {:?}", result.stderr); - println!("stdout = {:?}", result.stdout); + println!("stderr = {:?}", result.stderr_str()); + println!("stdout = {:?}", result.stdout_str()); assert!(result.success); assert!(result.stderr.is_empty()); assert_eq!(at.read("source.txt"), ""); @@ -317,8 +317,8 @@ fn test_cp_arg_force() { .arg(TEST_HELLO_WORLD_DEST) .run(); - println!("{:?}", result.stderr); - println!("{:?}", result.stdout); + println!("{:?}", result.stderr_str()); + println!("{:?}", result.stdout_str()); assert!(result.success); assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); @@ -602,7 +602,7 @@ fn test_cp_deref_folder_to_folder() { .arg(TEST_COPY_FROM_FOLDER) .arg(TEST_COPY_TO_FOLDER_NEW) .run(); - println!("cp output {}", result.stdout); + println!("cp output {}", result.stdout_str()); // Check that the exit code represents a successful copy. assert!(result.success); @@ -611,12 +611,12 @@ fn test_cp_deref_folder_to_folder() { { let scene2 = TestScenario::new("ls"); let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run(); - println!("ls source {}", result.stdout); + println!("ls source {}", result.stdout_str()); let path_to_new_symlink = at.subdir.join(TEST_COPY_TO_FOLDER_NEW); let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run(); - println!("ls dest {}", result.stdout); + println!("ls dest {}", result.stdout_str()); } #[cfg(windows)] @@ -706,7 +706,7 @@ fn test_cp_no_deref_folder_to_folder() { .arg(TEST_COPY_FROM_FOLDER) .arg(TEST_COPY_TO_FOLDER_NEW) .run(); - println!("cp output {}", result.stdout); + println!("cp output {}", result.stdout_str()); // Check that the exit code represents a successful copy. assert!(result.success); @@ -715,12 +715,12 @@ fn test_cp_no_deref_folder_to_folder() { { let scene2 = TestScenario::new("ls"); let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run(); - println!("ls source {}", result.stdout); + println!("ls source {}", result.stdout_str()); let path_to_new_symlink = at.subdir.join(TEST_COPY_TO_FOLDER_NEW); let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run(); - println!("ls dest {}", result.stdout); + println!("ls dest {}", result.stdout_str()); } #[cfg(windows)] @@ -809,7 +809,7 @@ fn test_cp_archive() { let scene2 = TestScenario::new("ls"); let result = scene2.cmd("ls").arg("-al").arg(at.subdir).run(); - println!("ls dest {}", result.stdout); + println!("ls dest {}", result.stdout_str()); assert_eq!(creation, creation2); assert!(result.success); } @@ -863,7 +863,7 @@ fn test_cp_archive_recursive() { .arg(&at.subdir.join(TEST_COPY_TO_FOLDER)) .run(); - println!("ls dest {}", result.stdout); + println!("ls dest {}", result.stdout_str()); let scene2 = TestScenario::new("ls"); let result = scene2 @@ -872,7 +872,7 @@ fn test_cp_archive_recursive() { .arg(&at.subdir.join(TEST_COPY_TO_FOLDER_NEW)) .run(); - println!("ls dest {}", result.stdout); + println!("ls dest {}", result.stdout_str()); assert!(at.file_exists( &at.subdir .join(TEST_COPY_TO_FOLDER_NEW) @@ -946,7 +946,7 @@ fn test_cp_preserve_timestamps() { let scene2 = TestScenario::new("ls"); let result = scene2.cmd("ls").arg("-al").arg(at.subdir).run(); - println!("ls dest {}", result.stdout); + println!("ls dest {}", result.stdout_str()); assert_eq!(creation, creation2); assert!(result.success); } @@ -984,7 +984,7 @@ fn test_cp_dont_preserve_timestamps() { let scene2 = TestScenario::new("ls"); let result = scene2.cmd("ls").arg("-al").arg(at.subdir).run(); - println!("ls dest {}", result.stdout); + println!("ls dest {}", result.stdout_str()); println!("creation {:?} / {:?}", creation, creation2); assert_ne!(creation, creation2); diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 5619aed94..1933fdba3 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -28,13 +28,13 @@ fn test_date_rfc_3339() { // 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.trim())); + assert!(re.is_match(&result.stdout_str().trim())); 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.trim())); + assert!(re.is_match(&result.stdout_str().trim())); } #[test] @@ -73,13 +73,13 @@ fn test_date_format_y() { assert!(result.success); let mut re = Regex::new(r"^\d{4}$").unwrap(); - assert!(re.is_match(&result.stdout.trim())); + assert!(re.is_match(&result.stdout_str().trim())); result = scene.ucmd().arg("+%y").succeeds(); assert!(result.success); re = Regex::new(r"^\d{2}$").unwrap(); - assert!(re.is_match(&result.stdout.trim())); + assert!(re.is_match(&result.stdout_str().trim())); } #[test] @@ -90,13 +90,13 @@ fn test_date_format_m() { assert!(result.success); let mut re = Regex::new(r"\S+").unwrap(); - assert!(re.is_match(&result.stdout.trim())); + assert!(re.is_match(&result.stdout_str().trim())); result = scene.ucmd().arg("+%m").succeeds(); assert!(result.success); re = Regex::new(r"^\d{2}$").unwrap(); - assert!(re.is_match(&result.stdout.trim())); + assert!(re.is_match(&result.stdout_str().trim())); } #[test] @@ -107,20 +107,20 @@ fn test_date_format_day() { assert!(result.success); let mut re = Regex::new(r"\S+").unwrap(); - assert!(re.is_match(&result.stdout.trim())); + assert!(re.is_match(&result.stdout_str().trim())); result = scene.ucmd().arg("+%A").succeeds(); assert!(result.success); re = Regex::new(r"\S+").unwrap(); - assert!(re.is_match(&result.stdout.trim())); + assert!(re.is_match(&result.stdout_str().trim())); result = scene.ucmd().arg("+%u").succeeds(); assert!(result.success); re = Regex::new(r"^\d{1}$").unwrap(); - assert!(re.is_match(&result.stdout.trim())); + assert!(re.is_match(&result.stdout_str().trim())); } #[test] @@ -131,7 +131,7 @@ fn test_date_format_full_day() { assert!(result.success); let re = Regex::new(r"\S+ \d{4}-\d{2}-\d{2}").unwrap(); - assert!(re.is_match(&result.stdout.trim())); + assert!(re.is_match(&result.stdout_str().trim())); } #[test] diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 30dcd9bb3..f668edeef 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -7,10 +7,9 @@ const SUB_LINK: &str = "subdir/links/sublink.txt"; #[test] fn test_du_basics() { - let (_at, mut ucmd) = at_and_ucmd!(); - let result = ucmd.run(); - assert!(result.success); - assert_eq!(result.stderr, ""); + new_ucmd!() + .succeeds() + .no_stderr(); } #[cfg(target_vendor = "apple")] fn _du_basics(s: String) { @@ -22,7 +21,7 @@ fn _du_basics(s: String) { assert_eq!(s, answer); } #[cfg(not(target_vendor = "apple"))] -fn _du_basics(s: String) { +fn _du_basics(s: &str) { let answer = "28\t./subdir 8\t./subdir/deeper 16\t./subdir/links @@ -38,19 +37,19 @@ fn test_du_basics_subdir() { let result = ucmd.arg(SUB_DIR).run(); assert!(result.success); assert_eq!(result.stderr, ""); - _du_basics_subdir(result.stdout); + _du_basics_subdir(result.stdout_str()); } #[cfg(target_vendor = "apple")] -fn _du_basics_subdir(s: String) { +fn _du_basics_subdir(s: &str) { assert_eq!(s, "4\tsubdir/deeper\n"); } #[cfg(target_os = "windows")] -fn _du_basics_subdir(s: String) { +fn _du_basics_subdir(s: &str) { assert_eq!(s, "0\tsubdir/deeper\n"); } #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] -fn _du_basics_subdir(s: String) { +fn _du_basics_subdir(s: &str) { // MS-WSL linux has altered expected output if !is_wsl() { assert_eq!(s, "8\tsubdir/deeper\n"); @@ -64,7 +63,7 @@ fn test_du_basics_bad_name() { let (_at, mut ucmd) = at_and_ucmd!(); let result = ucmd.arg("bad_name").run(); - assert_eq!(result.stdout, ""); + assert_eq!(result.stdout_str(), ""); assert_eq!( result.stderr, "du: error: bad_name: No such file or directory\n" @@ -81,20 +80,20 @@ fn test_du_soft_link() { let result = ts.ucmd().arg(SUB_DIR_LINKS).run(); assert!(result.success); assert_eq!(result.stderr, ""); - _du_soft_link(result.stdout); + _du_soft_link(result.stdout_str()); } #[cfg(target_vendor = "apple")] -fn _du_soft_link(s: String) { +fn _du_soft_link(s: &str) { // 'macos' host variants may have `du` output variation for soft links assert!((s == "12\tsubdir/links\n") || (s == "16\tsubdir/links\n")); } #[cfg(target_os = "windows")] -fn _du_soft_link(s: String) { +fn _du_soft_link(s: &str) { assert_eq!(s, "8\tsubdir/links\n"); } #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] -fn _du_soft_link(s: String) { +fn _du_soft_link(s: &str) { // MS-WSL linux has altered expected output if !is_wsl() { assert_eq!(s, "16\tsubdir/links\n"); @@ -114,19 +113,19 @@ fn test_du_hard_link() { assert!(result.success); assert_eq!(result.stderr, ""); // We do not double count hard links as the inodes are identical - _du_hard_link(result.stdout); + _du_hard_link(result.stdout_str()); } #[cfg(target_vendor = "apple")] -fn _du_hard_link(s: String) { +fn _du_hard_link(s: &str) { assert_eq!(s, "12\tsubdir/links\n") } #[cfg(target_os = "windows")] -fn _du_hard_link(s: String) { +fn _du_hard_link(s: &str) { assert_eq!(s, "8\tsubdir/links\n") } #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] -fn _du_hard_link(s: String) { +fn _du_hard_link(s: &str) { // MS-WSL linux has altered expected output if !is_wsl() { assert_eq!(s, "16\tsubdir/links\n"); @@ -142,19 +141,19 @@ fn test_du_d_flag() { let result = ts.ucmd().arg("-d").arg("1").run(); assert!(result.success); assert_eq!(result.stderr, ""); - _du_d_flag(result.stdout); + _du_d_flag(result.stdout_str()); } #[cfg(target_vendor = "apple")] -fn _du_d_flag(s: String) { +fn _du_d_flag(s: &str) { assert_eq!(s, "16\t./subdir\n20\t./\n"); } #[cfg(target_os = "windows")] -fn _du_d_flag(s: String) { +fn _du_d_flag(s: &str) { assert_eq!(s, "8\t./subdir\n8\t./\n"); } #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] -fn _du_d_flag(s: String) { +fn _du_d_flag(s: &str) { // MS-WSL linux has altered expected output if !is_wsl() { assert_eq!(s, "28\t./subdir\n36\t./\n"); @@ -167,10 +166,11 @@ fn _du_d_flag(s: String) { fn test_du_h_flag_empty_file() { let ts = TestScenario::new("du"); - let result = ts.ucmd().arg("-h").arg("empty.txt").run(); - assert!(result.success); - assert_eq!(result.stderr, ""); - assert_eq!(result.stdout, "0\tempty.txt\n"); + ts.ucmd() + .arg("-h") + .arg("empty.txt") + .succeeds() + .stdout_only("0\tempty.txt\n"); } #[cfg(feature = "touch")] @@ -190,3 +190,33 @@ fn test_du_time() { assert_eq!(result.stderr, ""); assert_eq!(result.stdout, "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 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(); + + ts.ccmd("chmod").arg("+r").arg(SUB_DIR_LINKS).run(); + + assert!(result.success); + assert_eq!( + result.stderr, + "du: cannot read directory ‘subdir/links‘: Permission denied (os error 13)\n" + ); + _du_no_permission(result.stdout); +} + +#[cfg(target_vendor = "apple")] +fn _du_no_permission(s: String) { + assert_eq!(s, "0\tsubdir/links\n"); +} +#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] +fn _du_no_permission(s: String) { + assert_eq!(s, "4\tsubdir/links\n"); +} diff --git a/tests/by-util/test_echo.rs b/tests/by-util/test_echo.rs index 7394ffc1e..99c8f3a1e 100644 --- a/tests/by-util/test_echo.rs +++ b/tests/by-util/test_echo.rs @@ -2,22 +2,20 @@ use crate::common::util::*; #[test] fn test_default() { - //CmdResult.stdout_only(...) trims trailing newlines - assert_eq!("hi\n", new_ucmd!().arg("hi").succeeds().no_stderr().stdout); + new_ucmd!() + .arg("hi") + .succeeds() + .stdout_only("hi\n"); } #[test] fn test_no_trailing_newline() { - //CmdResult.stdout_only(...) trims trailing newlines - assert_eq!( - "hi", - new_ucmd!() - .arg("-n") - .arg("hi") - .succeeds() - .no_stderr() - .stdout - ); + new_ucmd!() + .arg("-n") + .arg("hi") + .succeeds() + .no_stderr() + .stdout_only("hi"); } #[test] @@ -192,39 +190,38 @@ fn test_hyphen_values_inside_string() { new_ucmd!() .arg("'\"\n'CXXFLAGS=-g -O2'\n\"'") .succeeds() - .stdout - .contains("CXXFLAGS"); + .stdout_contains("CXXFLAGS"); } #[test] fn test_hyphen_values_at_start() { - let result = new_ucmd!() + new_ucmd!() .arg("-E") .arg("-test") .arg("araba") .arg("-merci") - .run(); - - assert!(result.success); - assert_eq!(false, result.stdout.contains("-E")); - assert_eq!(result.stdout, "-test araba -merci\n"); + .run() + .success() + .stdout_does_not_contain("-E") + .stdout_is("-test araba -merci\n"); } #[test] fn test_hyphen_values_between() { - let result = new_ucmd!().arg("test").arg("-E").arg("araba").run(); + new_ucmd!() + .arg("test") + .arg("-E") + .arg("araba") + .run() + .success() + .stdout_is("test -E araba\n"); - assert!(result.success); - assert_eq!(result.stdout, "test -E araba\n"); - - let result = new_ucmd!() + new_ucmd!() .arg("dumdum ") .arg("dum dum dum") .arg("-e") .arg("dum") - .run(); - - assert!(result.success); - assert_eq!(result.stdout, "dumdum dum dum dum -e dum\n"); - assert_eq!(true, result.stdout.contains("-e")); + .run() + .success() + .stdout_is("dumdum dum dum dum -e dum\n"); } diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 2ffb2bc48..19ecd7afb 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -8,45 +8,35 @@ use tempfile::tempdir; #[test] fn test_env_help() { - assert!(new_ucmd!() + new_ucmd!() .arg("--help") .succeeds() .no_stderr() - .stdout - .contains("OPTIONS:")); + .stdout_contains("OPTIONS:"); } #[test] fn test_env_version() { - assert!(new_ucmd!() + new_ucmd!() .arg("--version") .succeeds() .no_stderr() - .stdout - .contains(util_name!())); + .stdout_contains(util_name!()); } #[test] fn test_echo() { - // assert!(new_ucmd!().arg("printf").arg("FOO-bar").succeeds().no_stderr().stdout.contains("FOO-bar")); - let mut cmd = new_ucmd!(); - cmd.arg("echo").arg("FOO-bar"); - println!("cmd={:?}", cmd); + let result = new_ucmd!() + .arg("echo") + .arg("FOO-bar") + .succeeds(); - let result = cmd.run(); - println!("success={:?}", result.success); - println!("stdout={:?}", result.stdout); - println!("stderr={:?}", result.stderr); - assert!(result.success); - - let out = result.stdout.trim_end(); - - assert_eq!(out, "FOO-bar"); + assert_eq!(result.stdout_str().trim(), "FOO-bar"); } #[test] fn test_file_option() { - let out = new_ucmd!().arg("-f").arg("vars.conf.txt").run().stdout; + let out = new_ucmd!().arg("-f").arg("vars.conf.txt").run().stdout_move_str(); assert_eq!( out.lines() @@ -63,7 +53,7 @@ fn test_combined_file_set() { .arg("vars.conf.txt") .arg("FOO=bar.alt") .run() - .stdout; + .stdout_move_str(); assert_eq!(out.lines().filter(|&line| line == "FOO=bar.alt").count(), 1); } @@ -76,8 +66,8 @@ fn test_combined_file_set_unset() { .arg("-f") .arg("vars.conf.txt") .arg("FOO=bar.alt") - .run() - .stdout; + .succeeds() + .stdout_move_str(); assert_eq!( out.lines() @@ -89,17 +79,17 @@ fn test_combined_file_set_unset() { #[test] fn test_single_name_value_pair() { - let out = new_ucmd!().arg("FOO=bar").run().stdout; + let out = new_ucmd!().arg("FOO=bar").run(); - assert!(out.lines().any(|line| line == "FOO=bar")); + assert!(out.stdout_str().lines().any(|line| line == "FOO=bar")); } #[test] fn test_multiple_name_value_pairs() { - let out = new_ucmd!().arg("FOO=bar").arg("ABC=xyz").run().stdout; + let out = new_ucmd!().arg("FOO=bar").arg("ABC=xyz").run(); assert_eq!( - out.lines() + out.stdout_str().lines() .filter(|&line| line == "FOO=bar" || line == "ABC=xyz") .count(), 2 @@ -110,13 +100,8 @@ fn test_multiple_name_value_pairs() { fn test_ignore_environment() { let scene = TestScenario::new(util_name!()); - let out = scene.ucmd().arg("-i").run().stdout; - - assert_eq!(out, ""); - - let out = scene.ucmd().arg("-").run().stdout; - - assert_eq!(out, ""); + scene.ucmd().arg("-i").run().no_stdout(); + scene.ucmd().arg("-").run().no_stdout(); } #[test] @@ -126,8 +111,8 @@ fn test_null_delimiter() { .arg("--null") .arg("FOO=bar") .arg("ABC=xyz") - .run() - .stdout; + .succeeds() + .stdout_move_str(); let mut vars: Vec<_> = out.split('\0').collect(); assert_eq!(vars.len(), 3); @@ -145,8 +130,8 @@ fn test_unset_variable() { .ucmd_keepenv() .arg("-u") .arg("HOME") - .run() - .stdout; + .succeeds() + .stdout_move_str(); assert_eq!(out.lines().any(|line| line.starts_with("HOME=")), false); } @@ -173,8 +158,8 @@ fn test_change_directory() { .arg("--chdir") .arg(&temporary_path) .arg(pwd) - .run() - .stdout; + .succeeds() + .stdout_move_str(); assert_eq!(out.trim(), temporary_path.as_os_str()) } @@ -193,8 +178,8 @@ fn test_change_directory() { .ucmd() .arg("--chdir") .arg(&temporary_path) - .run() - .stdout; + .succeeds() + .stdout_move_str(); assert_eq!( out.lines() .any(|line| line.ends_with(temporary_path.file_name().unwrap().to_str().unwrap())), @@ -214,6 +199,6 @@ fn test_fail_change_directory() { .arg(some_non_existing_path) .arg("pwd") .fails() - .stderr; + .stderr_move_str(); assert!(out.contains("env: cannot change directory to ")); } diff --git a/tests/by-util/test_expand.rs b/tests/by-util/test_expand.rs index 801bf9d98..834a09736 100644 --- a/tests/by-util/test_expand.rs +++ b/tests/by-util/test_expand.rs @@ -2,57 +2,54 @@ use crate::common::util::*; #[test] fn test_with_tab() { - let (_, mut ucmd) = at_and_ucmd!(); - - let result = ucmd.arg("with-tab.txt").run(); - assert!(result.success); - assert!(result.stdout.contains(" ")); - assert!(!result.stdout.contains("\t")); + new_ucmd!() + .arg("with-tab.txt") + .succeeds() + .stdout_contains(" ") + .stdout_does_not_contain("\t"); } #[test] fn test_with_trailing_tab() { - let (_, mut ucmd) = at_and_ucmd!(); - - let result = ucmd.arg("with-trailing-tab.txt").run(); - assert!(result.success); - assert!(result.stdout.contains("with tabs=> ")); - assert!(!result.stdout.contains("\t")); + new_ucmd!() + .arg("with-trailing-tab.txt") + .succeeds() + .stdout_contains("with tabs=> ") + .stdout_does_not_contain("\t"); } #[test] fn test_with_trailing_tab_i() { - let (_, mut ucmd) = at_and_ucmd!(); - - let result = ucmd.arg("with-trailing-tab.txt").arg("-i").run(); - assert!(result.success); - assert!(result.stdout.contains(" // with tabs=>\t")); + new_ucmd!() + .arg("with-trailing-tab.txt") + .arg("-i") + .succeeds() + .stdout_contains(" // with tabs=>\t"); } #[test] fn test_with_tab_size() { - let (_, mut ucmd) = at_and_ucmd!(); - - let result = ucmd.arg("with-tab.txt").arg("--tabs=10").run(); - assert!(result.success); - assert!(result.stdout.contains(" ")); + new_ucmd!() + .arg("with-tab.txt") + .arg("--tabs=10") + .succeeds() + .stdout_contains(" "); } #[test] fn test_with_space() { - let (_, mut ucmd) = at_and_ucmd!(); - - let result = ucmd.arg("with-spaces.txt").run(); - assert!(result.success); - assert!(result.stdout.contains(" return")); + new_ucmd!() + .arg("with-spaces.txt") + .succeeds() + .stdout_contains(" return"); } #[test] fn test_with_multiple_files() { - let (_, mut ucmd) = at_and_ucmd!(); - - let result = ucmd.arg("with-spaces.txt").arg("with-tab.txt").run(); - assert!(result.success); - assert!(result.stdout.contains(" return")); - assert!(result.stdout.contains(" ")); + new_ucmd!() + .arg("with-spaces.txt") + .arg("with-tab.txt") + .succeeds() + .stdout_contains(" return") + .stdout_contains(" "); } diff --git a/tests/by-util/test_factor.rs b/tests/by-util/test_factor.rs index 5bde17cdb..af2ff4ddb 100644 --- a/tests/by-util/test_factor.rs +++ b/tests/by-util/test_factor.rs @@ -32,13 +32,10 @@ fn test_first_100000_integers() { } println!("STDIN='{}'", instring); - let result = new_ucmd!().pipe_in(instring.as_bytes()).run(); - let stdout = result.stdout; - - assert!(result.success); + let result = new_ucmd!().pipe_in(instring.as_bytes()).succeeds(); // `seq 0 100000 | factor | sha1sum` => "4ed2d8403934fa1c76fe4b84c5d4b8850299c359" - let hash_check = sha1::Sha1::from(stdout.as_bytes()).hexdigest(); + let hash_check = sha1::Sha1::from(result.stdout()).hexdigest(); assert_eq!(hash_check, "4ed2d8403934fa1c76fe4b84c5d4b8850299c359"); } diff --git a/tests/by-util/test_fmt.rs b/tests/by-util/test_fmt.rs index 4533cdf24..f962a9137 100644 --- a/tests/by-util/test_fmt.rs +++ b/tests/by-util/test_fmt.rs @@ -5,7 +5,7 @@ fn test_fmt() { let result = new_ucmd!().arg("one-word-per-line.txt").run(); //.stdout_is_fixture("call_graph.expected"); assert_eq!( - result.stdout.trim(), + result.stdout_str().trim(), "this is a file with one word per line" ); } @@ -15,7 +15,7 @@ fn test_fmt_q() { let result = new_ucmd!().arg("-q").arg("one-word-per-line.txt").run(); //.stdout_is_fixture("call_graph.expected"); assert_eq!( - result.stdout.trim(), + result.stdout_str().trim(), "this is a file with one word per line" ); } @@ -42,7 +42,7 @@ fn test_fmt_w() { .arg("one-word-per-line.txt") .run(); //.stdout_is_fixture("call_graph.expected"); - assert_eq!(result.stdout.trim(), "this is a file with one word per line"); + assert_eq!(result.stdout_str().trim(), "this is a file with one word per line"); } diff --git a/tests/by-util/test_groups.rs b/tests/by-util/test_groups.rs index 5c326fe2d..32a16cc1a 100644 --- a/tests/by-util/test_groups.rs +++ b/tests/by-util/test_groups.rs @@ -2,26 +2,25 @@ use crate::common::util::*; #[test] fn test_groups() { - let (_, mut ucmd) = at_and_ucmd!(); - let result = ucmd.run(); - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); - if is_ci() && result.stdout.trim().is_empty() { + let result = new_ucmd!().run(); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + if is_ci() && result.stdout_str().trim().is_empty() { // In the CI, some server are failing to return the group. // As seems to be a configuration issue, ignoring it return; } assert!(result.success); - assert!(!result.stdout.trim().is_empty()); + assert!(!result.stdout_str().trim().is_empty()); } #[test] fn test_groups_arg() { // get the username with the "id -un" command let result = TestScenario::new("id").ucmd_keepenv().arg("-un").run(); - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); - let s1 = String::from(result.stdout.trim()); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); + let s1 = String::from(result.stdout_str().trim()); if is_ci() && s1.parse::().is_ok() { // In the CI, some server are failing to return id -un. // So, if we are getting a uid, just skip this test @@ -29,18 +28,18 @@ fn test_groups_arg() { return; } - println!("result.stdout {}", result.stdout); - println!("result.stderr = {}", result.stderr); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); assert!(result.success); - assert!(!result.stdout.is_empty()); - let username = result.stdout.trim(); + 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); - println!("result.stderr = {}", result.stderr); + println!("result.stdout = {}", result.stdout_str()); + println!("result.stderr = {}", result.stderr_str()); assert!(result.success); - assert!(!result.stdout.is_empty()); + assert!(!result.stdout_str().is_empty()); } diff --git a/tests/by-util/test_hashsum.rs b/tests/by-util/test_hashsum.rs index 6e7d59107..f059e53f3 100644 --- a/tests/by-util/test_hashsum.rs +++ b/tests/by-util/test_hashsum.rs @@ -17,14 +17,14 @@ macro_rules! test_digest { fn test_single_file() { let ts = TestScenario::new("hashsum"); assert_eq!(ts.fixtures.read(EXPECTED_FILE), - get_hash!(ts.ucmd().arg(DIGEST_ARG).arg(BITS_ARG).arg("input.txt").succeeds().no_stderr().stdout)); + get_hash!(ts.ucmd().arg(DIGEST_ARG).arg(BITS_ARG).arg("input.txt").succeeds().no_stderr().stdout_str())); } #[test] fn test_stdin() { let ts = TestScenario::new("hashsum"); assert_eq!(ts.fixtures.read(EXPECTED_FILE), - get_hash!(ts.ucmd().arg(DIGEST_ARG).arg(BITS_ARG).pipe_in_fixture("input.txt").succeeds().no_stderr().stdout)); + get_hash!(ts.ucmd().arg(DIGEST_ARG).arg(BITS_ARG).pipe_in_fixture("input.txt").succeeds().no_stderr().stdout_str())); } } )*) diff --git a/tests/by-util/test_hostid.rs b/tests/by-util/test_hostid.rs index 17aad4aff..b5b668901 100644 --- a/tests/by-util/test_hostid.rs +++ b/tests/by-util/test_hostid.rs @@ -9,5 +9,5 @@ fn test_normal() { assert!(result.success); let re = Regex::new(r"^[0-9a-f]{8}").unwrap(); - assert!(re.is_match(&result.stdout.trim())); + assert!(re.is_match(&result.stdout_str())); } diff --git a/tests/by-util/test_hostname.rs b/tests/by-util/test_hostname.rs index 804d47642..9fa63241f 100644 --- a/tests/by-util/test_hostname.rs +++ b/tests/by-util/test_hostname.rs @@ -6,8 +6,8 @@ fn test_hostname() { let ls_short_res = new_ucmd!().arg("-s").succeeds(); let ls_domain_res = new_ucmd!().arg("-d").succeeds(); - assert!(ls_default_res.stdout.len() >= ls_short_res.stdout.len()); - assert!(ls_default_res.stdout.len() >= ls_domain_res.stdout.len()); + assert!(ls_default_res.stdout().len() >= ls_short_res.stdout().len()); + assert!(ls_default_res.stdout().len() >= ls_domain_res.stdout().len()); } // FixME: fails for "MacOS" @@ -17,14 +17,14 @@ fn test_hostname_ip() { let result = new_ucmd!().arg("-i").run(); println!("{:#?}", result); assert!(result.success); - assert!(!result.stdout.trim().is_empty()); + assert!(!result.stdout_str().trim().is_empty()); } #[test] fn test_hostname_full() { - let result = new_ucmd!().arg("-f").succeeds(); - assert!(!result.stdout.trim().is_empty()); - let ls_short_res = new_ucmd!().arg("-s").succeeds(); - assert!(result.stdout.trim().contains(ls_short_res.stdout.trim())); + assert!(!ls_short_res.stdout_str().trim().is_empty()); + + new_ucmd!().arg("-f").succeeds() + .stdout_contains(ls_short_res.stdout_str().trim()); } diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index 116c73995..7e2791467 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -9,33 +9,29 @@ fn return_whoami_username() -> String { return String::from(""); } - result.stdout.trim().to_string() + result.stdout_str().trim().to_string() } #[test] fn test_id() { - let scene = TestScenario::new(util_name!()); - - let mut result = scene.ucmd().arg("-u").run(); + let result = new_ucmd!().arg("-u").run(); if result.stderr.contains("cannot find name for user ID") { // In the CI, some server are failing to return whoami. // As seems to be a configuration issue, ignoring it return; } - assert!(result.success); - let uid = String::from(result.stdout.trim()); - result = scene.ucmd().run(); + let uid = result.success().stdout_str().trim(); + let result = new_ucmd!().run(); if is_ci() && result.stderr.contains("cannot find name for user ID") { // In the CI, some server are failing to return whoami. // As seems to be a configuration issue, ignoring it return; } - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); - if !result.stderr.contains("Could not find uid") { + + if !result.stderr_str().contains("Could not find uid") { // Verify that the id found by --user/-u exists in the list - assert!(result.stdout.contains(&uid)); + result.success().stdout_contains(&uid); } } @@ -47,88 +43,64 @@ fn test_id_from_name() { return; } - let scene = TestScenario::new(util_name!()); - let result = scene.ucmd().arg(&username).succeeds(); - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); - assert!(result.success); - let uid = String::from(result.stdout.trim()); - let result = scene.ucmd().succeeds(); - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); - // Verify that the id found by --user/-u exists in the list - assert!(result.stdout.contains(&uid)); - // Verify that the username found by whoami exists in the list - assert!(result.stdout.contains(&username)); + let result = new_ucmd!().arg(&username).succeeds(); + let uid = result.stdout_str().trim(); + + new_ucmd!().succeeds() + // Verify that the id found by --user/-u exists in the list + .stdout_contains(uid) + // Verify that the username found by whoami exists in the list + .stdout_contains(username); } #[test] fn test_id_name_from_id() { - let mut scene = TestScenario::new(util_name!()); - let result = scene.ucmd().arg("-u").run(); - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); - assert!(result.success); - let uid = String::from(result.stdout.trim()); + let result = new_ucmd!().arg("-u").succeeds(); + let uid = result.stdout_str().trim(); - scene = TestScenario::new(util_name!()); - let result = scene.ucmd().arg("-nu").arg(uid).run(); + let result = new_ucmd!().arg("-nu").arg(uid).run(); if is_ci() && result.stderr.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; } - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); - assert!(result.success); - let username_id = String::from(result.stdout.trim()); + let username_id = result + .success() + .stdout_str() + .trim(); - scene = TestScenario::new("whoami"); - let result = scene.cmd("whoami").run(); + let scene = TestScenario::new("whoami"); + let result = scene.cmd("whoami").succeeds(); - let username_whoami = result.stdout.trim(); + let username_whoami = result.stdout_str().trim(); assert_eq!(username_id, username_whoami); } #[test] fn test_id_group() { - let scene = TestScenario::new(util_name!()); - - let mut result = scene.ucmd().arg("-g").succeeds(); - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); - assert!(result.success); - let s1 = String::from(result.stdout.trim()); + let mut result = new_ucmd!().arg("-g").succeeds(); + let s1 = result.stdout_str().trim(); assert!(s1.parse::().is_ok()); - result = scene.ucmd().arg("--group").succeeds(); - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); - assert!(result.success); - let s1 = String::from(result.stdout.trim()); + result = new_ucmd!().arg("--group").succeeds(); + let s1 = result.stdout_str().trim(); assert!(s1.parse::().is_ok()); } #[test] fn test_id_groups() { - let scene = TestScenario::new(util_name!()); - - let result = scene.ucmd().arg("-G").succeeds(); - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); + let result = new_ucmd!().arg("-G").succeeds(); assert!(result.success); - let groups = result.stdout.trim().split_whitespace(); + let groups = result.stdout_str().trim().split_whitespace(); for s in groups { assert!(s.parse::().is_ok()); } - let result = scene.ucmd().arg("--groups").succeeds(); - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); + let result = new_ucmd!().arg("--groups").succeeds(); assert!(result.success); - let groups = result.stdout.trim().split_whitespace(); + let groups = result.stdout_str().trim().split_whitespace(); for s in groups { assert!(s.parse::().is_ok()); } @@ -136,15 +108,12 @@ fn test_id_groups() { #[test] fn test_id_user() { - let scene = TestScenario::new(util_name!()); - - let mut result = scene.ucmd().arg("-u").succeeds(); - assert!(result.success); - let s1 = String::from(result.stdout.trim()); + let mut result = new_ucmd!().arg("-u").succeeds(); + let s1 = result.stdout_str().trim(); assert!(s1.parse::().is_ok()); - result = scene.ucmd().arg("--user").succeeds(); - assert!(result.success); - let s1 = String::from(result.stdout.trim()); + + result = new_ucmd!().arg("--user").succeeds(); + let s1 = result.stdout_str().trim(); assert!(s1.parse::().is_ok()); } @@ -156,17 +125,13 @@ fn test_id_pretty_print() { return; } - let scene = TestScenario::new(util_name!()); - let result = scene.ucmd().arg("-p").run(); - if result.stdout.trim() == "" { + let result = new_ucmd!().arg("-p").run(); + if result.stdout_str().trim() == "" { // Sometimes, the CI is failing here with // old rust versions on Linux return; } - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); - assert!(result.success); - assert!(result.stdout.contains(&username)); + result.success().stdout_contains(username); } #[test] @@ -176,12 +141,7 @@ fn test_id_password_style() { // Sometimes, the CI is failing here return; } - let scene = TestScenario::new(util_name!()); - let result = scene.ucmd().arg("-P").succeeds(); - - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); - assert!(result.success); - assert!(result.stdout.starts_with(&username)); + let result = new_ucmd!().arg("-P").succeeds(); + assert!(result.stdout_str().starts_with(&username)); } diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index 8ac6396fd..32df8d460 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -2,6 +2,8 @@ use crate::common::util::*; use filetime::FileTime; use rust_users::*; use std::os::unix::fs::PermissionsExt; +#[cfg(not(windows))] +use std::process::Command; #[cfg(target_os = "linux")] use std::thread::sleep; @@ -193,12 +195,8 @@ fn test_install_mode_numeric() { let mode_arg = "-m 0333"; at.mkdir(dir2); - let result = scene.ucmd().arg(mode_arg).arg(file).arg(dir2).run(); + scene.ucmd().arg(mode_arg).arg(file).arg(dir2).succeeds(); - println!("stderr = {:?}", result.stderr); - println!("stdout = {:?}", result.stdout); - - assert!(result.success); let dest_file = &format!("{}/{}", dir2, file); assert!(at.file_exists(file)); assert!(at.file_exists(dest_file)); @@ -311,16 +309,13 @@ fn test_install_target_new_file_with_group() { .arg(format!("{}/{}", dir, file)) .run(); - println!("stderr = {:?}", result.stderr); - println!("stdout = {:?}", result.stdout); - - if is_ci() && result.stderr.contains("error: no such group:") { + if is_ci() && result.stderr_str().contains("error: no such group:") { // In the CI, some server are failing to return the group. // As seems to be a configuration issue, ignoring it return; } - assert!(result.success); + result.success(); assert!(at.file_exists(file)); assert!(at.file_exists(&format!("{}/{}", dir, file))); } @@ -341,16 +336,13 @@ fn test_install_target_new_file_with_owner() { .arg(format!("{}/{}", dir, file)) .run(); - println!("stderr = {:?}", result.stderr); - println!("stdout = {:?}", result.stdout); - if is_ci() && result.stderr.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; } - assert!(result.success); + result.success(); assert!(at.file_exists(file)); assert!(at.file_exists(&format!("{}/{}", dir, file))); } @@ -364,13 +356,10 @@ fn test_install_target_new_file_failing_nonexistent_parent() { at.touch(file1); - let err = ucmd - .arg(file1) + ucmd.arg(file1) .arg(format!("{}/{}", dir, file2)) .fails() - .stderr; - - assert!(err.contains("not a directory")) + .stderr_contains(&"not a directory"); } #[test] @@ -415,18 +404,12 @@ fn test_install_copy_file() { #[test] #[cfg(target_os = "linux")] fn test_install_target_file_dev_null() { - let scene = TestScenario::new(util_name!()); - let at = &scene.fixtures; + let (at, mut ucmd) = at_and_ucmd!(); let file1 = "/dev/null"; let file2 = "target_file"; - let result = scene.ucmd().arg(file1).arg(file2).run(); - - println!("stderr = {:?}", result.stderr); - println!("stdout = {:?}", result.stdout); - - assert!(result.success); + ucmd.arg(file1).arg(file2).succeeds(); assert!(at.file_exists(file2)); } @@ -566,3 +549,97 @@ fn test_install_copy_then_compare_file_with_extra_mode() { assert!(after_install_sticky != after_install_sticky_again); } + +const STRIP_TARGET_FILE: &str = "helloworld_installed"; +const SYMBOL_DUMP_PROGRAM: &str = "objdump"; +const STRIP_SOURCE_FILE_SYMBOL: &str = "main"; + +fn strip_source_file() -> &'static str { + if cfg!(target_os = "macos") { + "helloworld_macos" + } else { + "helloworld_linux" + } +} + +#[test] +#[cfg(not(windows))] +fn test_install_and_strip() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + scene + .ucmd() + .arg("-s") + .arg(strip_source_file()) + .arg(STRIP_TARGET_FILE) + .succeeds() + .no_stderr(); + + let output = Command::new(SYMBOL_DUMP_PROGRAM) + .arg("-t") + .arg(at.plus(STRIP_TARGET_FILE)) + .output() + .unwrap(); + + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(!stdout.contains(STRIP_SOURCE_FILE_SYMBOL)); +} + +#[test] +#[cfg(not(windows))] +fn test_install_and_strip_with_program() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + scene + .ucmd() + .arg("-s") + .arg("--strip-program") + .arg("/usr/bin/strip") + .arg(strip_source_file()) + .arg(STRIP_TARGET_FILE) + .succeeds() + .no_stderr(); + + let output = Command::new(SYMBOL_DUMP_PROGRAM) + .arg("-t") + .arg(at.plus(STRIP_TARGET_FILE)) + .output() + .unwrap(); + + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(!stdout.contains(STRIP_SOURCE_FILE_SYMBOL)); +} + +#[test] +#[cfg(not(windows))] +fn test_install_and_strip_with_invalid_program() { + let scene = TestScenario::new(util_name!()); + + let stderr = scene + .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")); +} + +#[test] +#[cfg(not(windows))] +fn test_install_and_strip_with_non_existent_program() { + let scene = TestScenario::new(util_name!()); + + let stderr = scene + .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")); +} diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index 89261036d..d7a13b0d4 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -520,10 +520,7 @@ fn test_symlink_no_deref_dir() { scene.ucmd().args(&["-sn", dir1, link]).fails(); // Try with the no-deref - let result = scene.ucmd().args(&["-sfn", dir1, link]).run(); - println!("stdout {}", result.stdout); - println!("stderr {}", result.stderr); - assert!(result.success); + scene.ucmd().args(&["-sfn", dir1, link]).succeeds(); assert!(at.dir_exists(dir1)); assert!(at.dir_exists(dir2)); assert!(at.is_symlink(link)); @@ -566,10 +563,7 @@ fn test_symlink_no_deref_file() { scene.ucmd().args(&["-sn", file1, link]).fails(); // Try with the no-deref - let result = scene.ucmd().args(&["-sfn", file1, link]).run(); - println!("stdout {}", result.stdout); - println!("stderr {}", result.stderr); - assert!(result.success); + scene.ucmd().args(&["-sfn", file1, link]).succeeds(); assert!(at.file_exists(file1)); assert!(at.file_exists(file2)); assert!(at.is_symlink(link)); diff --git a/tests/by-util/test_logname.rs b/tests/by-util/test_logname.rs index b15941c06..8d1996e63 100644 --- a/tests/by-util/test_logname.rs +++ b/tests/by-util/test_logname.rs @@ -3,23 +3,19 @@ use std::env; #[test] fn test_normal() { - let (_, mut ucmd) = at_and_ucmd!(); - - let result = ucmd.run(); - println!("result.stdout = {}", result.stdout); - println!("result.stderr = {}", result.stderr); + let result = new_ucmd!().run(); println!("env::var(CI).is_ok() = {}", env::var("CI").is_ok()); for (key, value) in env::vars() { println!("{}: {}", key, value); } - if (is_ci() || is_wsl()) && result.stderr.contains("error: no login name") { + if (is_ci() || is_wsl()) && 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 return; } - assert!(result.success); - assert!(!result.stdout.trim().is_empty()); + result.success(); + assert!(!result.stdout_str().trim().is_empty()); } diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index 651491045..de54fae5b 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -1 +1,51 @@ -// ToDO: add tests +use crate::common::util::*; + +#[test] +fn test_shred_remove() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_shred_remove_a"; + let file_b = "test_shred_remove_b"; + + // Create file_a and file_b. + at.touch(file_a); + at.touch(file_b); + + // Shred file_a. + scene.ucmd().arg("-u").arg(file_a).run(); + + // file_a was deleted, file_b exists. + assert!(!at.file_exists(file_a)); + assert!(at.file_exists(file_b)); +} + +#[cfg(not(target_os = "freebsd"))] +#[test] +fn test_shred_force() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file = "test_shred_force"; + + // Create file_a. + at.touch(file); + assert!(at.file_exists(file)); + + // Make file_a readonly. + at.set_readonly(file); + + // Try shred -u. + let result = scene.ucmd().arg("-u").arg(file).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + + // file_a was not deleted because it is readonly. + assert!(at.file_exists(file)); + + // Try shred -u -f. + scene.ucmd().arg("-u").arg("-f").arg(file).run(); + + // file_a was deleted. + assert!(!at.file_exists(file)); +} diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 43aaf1da1..0f8020688 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1,5 +1,50 @@ use crate::common::util::*; +fn test_helper(file_name: &str, args: &str) { + new_ucmd!() + .arg(args) + .arg(format!("{}.txt", file_name)) + .succeeds() + .stdout_is_fixture(format!("{}.expected", file_name)); +} + +#[test] +fn test_months_whitespace() { + test_helper("months-whitespace", "-M"); +} + +#[test] +fn test_version_empty_lines() { + new_ucmd!() + .arg("-V") + .arg("version-empty-lines.txt") + .succeeds() + .stdout_is("\n\n\n\n\n\n\n1.2.3-alpha\n1.2.3-alpha2\n\t\t\t1.12.4\n11.2.3\n"); +} + +#[test] +fn test_human_numeric_whitespace() { + test_helper("human-numeric-whitespace", "-h"); +} + +#[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] +fn test_multiple_decimals_numeric() { + new_ucmd!() + .arg("-n") + .arg("multiple_decimals_numeric.txt") + .succeeds() + .stdout_is("-2028789030\n-896689\n-8.90880\n-1\n-.05\n\n\n\n\n\n\n\n\n000\nCARAvan\n00000001\n1\n1.040000000\n1.444\n1.58590\n8.013\n45\n46.89\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\n576,446.88800000\n576,446.890\n4798908.340000000000\n4798908.45\n4798908.8909800\n"); +} + #[test] fn test_check_zero_terminated_failure() { new_ucmd!() @@ -44,6 +89,21 @@ fn test_random_shuffle_contains_all_lines() { assert_eq!(result_sorted, expected); } +#[test] +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"; + let (at, _ucmd) = at_and_ucmd!(); + let result = new_ucmd!().arg("-R").arg(FILE).run().stdout; + let expected = at.read(FILE); + let unexpected = new_ucmd!().arg("-R").arg(FILE).run().stdout; + + 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 @@ -144,10 +204,10 @@ fn test_dictionary_order2() { fn test_non_printing_chars() { for non_printing_chars_param in vec!["-i"] { new_ucmd!() - .pipe_in("a👦🏻aa b\naaaa b") + .pipe_in("a👦🏻aa\naaaa") .arg(non_printing_chars_param) .succeeds() - .stdout_only("aaaa b\na👦🏻aa b\n"); + .stdout_only("a👦🏻aa\naaaa\n"); } } @@ -182,6 +242,16 @@ fn test_mixed_floats_ints_chars_numeric_unique() { test_helper("mixed_floats_ints_chars_numeric_unique", "-nu"); } +#[test] +fn test_words_unique() { + test_helper("words_unique", "-u"); +} + +#[test] +fn test_numeric_unique() { + test_helper("numeric_unique", "-nu"); +} + #[test] fn test_mixed_floats_ints_chars_numeric_reverse() { test_helper("mixed_floats_ints_chars_numeric_unique_reverse", "-nur"); @@ -266,6 +336,166 @@ 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] +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] +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] +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] +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] +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] +fn test_keys_invalid_field() { + new_ucmd!() + .args(&["-k", "1."]) + .fails() + .stderr_only("sort: error: failed to parse character index for key `1.`: cannot parse integer from empty string"); +} + +#[test] +fn test_keys_invalid_field_option() { + new_ucmd!() + .args(&["-k", "1.1x"]) + .fails() + .stderr_only("sort: error: invalid option for key: `x`"); +} + +#[test] +fn test_keys_invalid_field_zero() { + new_ucmd!() + .args(&["-k", "0.1"]) + .fails() + .stderr_only("sort: error: field index was 0"); +} + +#[test] +fn test_keys_with_options() { + let input = "aa 3 cc\ndd 1 ff\ngg 2 cc\n"; + for param in &[ + &["-k", "2,2n"][..], + &["-k", "2n,2"][..], + &["-k", "2,2", "-n"][..], + ] { + new_ucmd!() + .args(param) + .pipe_in(input) + .succeeds() + .stdout_only("dd 1 ff\ngg 2 cc\naa 3 cc\n"); + } +} + +#[test] +fn test_keys_with_options_blanks_start() { + let input = "aa 3 cc\ndd 1 ff\ngg 2 cc\n"; + for param in &[&["-k", "2b,2"][..], &["-k", "2,2", "-b"][..]] { + new_ucmd!() + .args(param) + .pipe_in(input) + .succeeds() + .stdout_only("dd 1 ff\ngg 2 cc\naa 3 cc\n"); + } +} + +#[test] +fn test_keys_with_options_blanks_end() { + let input = "a b +a b +a b +"; + new_ucmd!() + .args(&["-k", "1,2.1b", "-s"]) + .pipe_in(input) + .succeeds() + .stdout_only( + "a b +a b +a b +", + ); +} + +#[test] +fn test_keys_stable() { + let input = "a b +a b +a b +"; + new_ucmd!() + .args(&["-k", "1,2.1", "-s"]) + .pipe_in(input) + .succeeds() + .stdout_only( + "a b +a b +a b +", + ); +} + +#[test] +fn test_keys_empty_match() { + let input = "a a a a +aaaa +"; + new_ucmd!() + .args(&["-k", "1,1", "-t", "a"]) + .pipe_in(input) + .succeeds() + .stdout_only(input); +} + #[test] fn test_zero_terminated() { test_helper("zero-terminated", "-z"); @@ -355,11 +585,3 @@ fn test_check_silent() { .fails() .stdout_is(""); } - -fn test_helper(file_name: &str, args: &str) { - new_ucmd!() - .arg(args) - .arg(format!("{}{}", file_name, ".txt")) - .succeeds() - .stdout_is_fixture(format!("{}{}", file_name, ".expected")); -} diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index 9adb0cfda..808b7382a 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -1,13 +1,65 @@ +#[cfg(not(target_os = "windows"))] use crate::common::util::*; +#[cfg(not(target_os = "windows"))] #[test] fn test_stdbuf_unbuffered_stdout() { - if cfg!(target_os = "linux") { - // This is a basic smoke test - new_ucmd!() - .args(&["-o0", "head"]) - .pipe_in("The quick brown fox jumps over the lazy dog.") - .run() - .stdout_is("The quick brown fox jumps over the lazy dog."); - } + // This is a basic smoke test + new_ucmd!() + .args(&["-o0", "head"]) + .pipe_in("The quick brown fox jumps over the lazy dog.") + .run() + .stdout_is("The quick brown fox jumps over the lazy dog."); +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn test_stdbuf_line_buffered_stdout() { + new_ucmd!() + .args(&["-oL", "head"]) + .pipe_in("The quick brown fox jumps over the lazy dog.") + .run() + .stdout_is("The quick brown fox jumps over the lazy dog."); +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn test_stdbuf_no_buffer_option_fails() { + new_ucmd!().args(&["head"]).fails().stderr_is( + "error: The following required arguments were not provided:\n \ + --error \n \ + --input \n \ + --output \n\n\ + USAGE:\n \ + stdbuf OPTION... COMMAND\n\n\ + For more information try --help", + ); +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn test_stdbuf_trailing_var_arg() { + new_ucmd!() + .args(&["-i", "1024", "tail", "-1"]) + .pipe_in("The quick brown fox\njumps over the lazy dog.") + .run() + .stdout_is("jumps over the lazy dog."); +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn test_stdbuf_line_buffering_stdin_fails() { + new_ucmd!() + .args(&["-i", "L", "head"]) + .fails() + .stderr_is("stdbuf: error: line buffering stdin is meaningless\nTry 'stdbuf --help' for more information."); +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn test_stdbuf_invalid_mode_fails() { + new_ucmd!() + .args(&["-i", "1024R", "head"]) + .fails() + .stderr_is("stdbuf: error: invalid mode 1024R\nTry 'stdbuf --help' for more information."); } diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 9921c16b5..9f2c079b0 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -29,6 +29,7 @@ fn set_file_times(at: &AtPath, path: &str, atime: FileTime, mtime: FileTime) { fn str_to_filetime(format: &str, s: &str) -> FileTime { let mut tm = time::strptime(s, format).unwrap(); tm.tm_utcoff = time::now().tm_utcoff; + tm.tm_isdst = -1; // Unknown flag DST let ts = tm.to_timespec(); FileTime::from_unix_time(ts.sec as i64, ts.nsec as u32) } @@ -352,3 +353,21 @@ fn test_touch_set_date() { assert_eq!(atime, start_of_year); assert_eq!(mtime, start_of_year); } + +#[test] +fn test_touch_mtime_dst_succeeds() { + let (at, mut ucmd) = at_and_ucmd!(); + let file = "test_touch_set_mtime_dst_succeeds"; + + ucmd.args(&["-m", "-t", "202103140300", file]) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file)); + + 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); +} diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index fc1665efc..075878470 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -1,5 +1,33 @@ use crate::common::util::*; +#[test] +fn test_count_bytes_large_stdin() { + for &n in &[ + 0, + 1, + 42, + 16 * 1024 - 7, + 16 * 1024 - 1, + 16 * 1024, + 16 * 1024 + 1, + 16 * 1024 + 3, + 32 * 1024, + 64 * 1024, + 80 * 1024, + 96 * 1024, + 112 * 1024, + 128 * 1024, + ] { + let data = vec_of_size(n); + let expected = format!("{}\n", n); + new_ucmd!() + .args(&["-c"]) + .pipe_in(data) + .succeeds() + .stdout_is_bytes(&expected.as_bytes()); + } +} + #[test] fn test_stdin_default() { new_ucmd!() diff --git a/tests/common/macros.rs b/tests/common/macros.rs index e8b9c9d5d..81878bf1b 100644 --- a/tests/common/macros.rs +++ b/tests/common/macros.rs @@ -1,40 +1,3 @@ -/// Assertion helper macro for [`CmdResult`] types -/// -/// [`CmdResult`]: crate::tests::common::util::CmdResult -#[macro_export] -macro_rules! assert_empty_stderr( - ($cond:expr) => ( - if $cond.stderr.len() > 0 { - panic!("stderr: {}", $cond.stderr_str()) - } - ); -); - -/// Assertion helper macro for [`CmdResult`] types -/// -/// [`CmdResult`]: crate::tests::common::util::CmdResult -#[macro_export] -macro_rules! assert_empty_stdout( - ($cond:expr) => ( - if $cond.stdout.len() > 0 { - panic!("stdout: {}", $cond.stdout_str()) - } - ); -); - -/// Assertion helper macro for [`CmdResult`] types -/// -/// [`CmdResult`]: crate::tests::common::util::CmdResult -#[macro_export] -macro_rules! assert_no_error( - ($cond:expr) => ( - assert!($cond.success); - if $cond.stderr.len() > 0 { - panic!("stderr: {}", $cond.stderr_str()) - } - ); -); - /// Platform-independent helper for constructing a PathBuf from individual elements #[macro_export] macro_rules! path_concat { diff --git a/tests/common/util.rs b/tests/common/util.rs index 8a09b71c1..c7f46c2a9 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -33,6 +33,8 @@ static ALREADY_RUN: &str = " you have already run this UCommand, if you want to testing();"; static MULTIPLE_STDIN_MEANINGLESS: &str = "Ucommand is designed around a typical use case of: provide args and input stream -> spawn process -> block until completion -> return output streams. For verifying that a particular section of the input stream is what causes a particular behavior, use the Command type directly."; +static NO_STDIN_MEANINGLESS: &str = "Setting this flag has no effect if there is no stdin"; + /// Test if the program is running under CI pub fn is_ci() -> bool { std::env::var("CI") @@ -64,7 +66,7 @@ fn read_scenario_fixture>(tmpd: &Option>, file_rel_p /// A command result is the outputs of a command (streams and status code) /// within a struct which has convenience assertion functions about those outputs -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct CmdResult { //tmpd is used for convenience functions for asserts against fixtures tmpd: Option>, @@ -130,6 +132,11 @@ impl CmdResult { self.code.expect("Program must be run first") } + pub fn code_is(&self, expected_code: i32) -> &CmdResult { + assert_eq!(self.code(), expected_code); + self + } + /// Returns the program's TempDir /// Panics if not present pub fn tmpd(&self) -> Rc { @@ -146,13 +153,25 @@ impl CmdResult { /// asserts that the command resulted in a success (zero) status code pub fn success(&self) -> &CmdResult { - assert!(self.success); + if !self.success { + panic!( + "Command was expected to succeed.\nstdout = {}\n stderr = {}", + self.stdout_str(), + self.stderr_str() + ); + } self } /// asserts that the command resulted in a failure (non-zero) status code pub fn failure(&self) -> &CmdResult { - assert!(!self.success); + if self.success { + panic!( + "Command was expected to fail.\nstdout = {}\n stderr = {}", + self.stdout_str(), + self.stderr_str() + ); + } self } @@ -168,7 +187,12 @@ impl CmdResult { /// 1. you can not know exactly what stdout will be or /// 2. you know that stdout will also be empty pub fn no_stderr(&self) -> &CmdResult { - assert!(self.stderr.is_empty()); + if !self.stderr.is_empty() { + panic!( + "Expected stderr to be empty, but it's:\n{}", + self.stderr_str() + ); + } self } @@ -179,7 +203,12 @@ impl CmdResult { /// 1. you can not know exactly what stderr will be or /// 2. you know that stderr will also be empty pub fn no_stdout(&self) -> &CmdResult { - assert!(self.stdout.is_empty()); + if !self.stdout.is_empty() { + panic!( + "Expected stdout to be empty, but it's:\n{}", + self.stderr_str() + ); + } self } @@ -222,6 +251,12 @@ impl CmdResult { self } + /// Like stdout_is_fixture, but for stderr + pub fn stderr_is_fixture>(&self, file_rel_path: T) -> &CmdResult { + let contents = read_scenario_fixture(&self.tmpd, file_rel_path); + self.stderr_is_bytes(contents) + } + /// asserts that /// 1. the command resulted in stdout stream output that equals the /// passed in value @@ -271,10 +306,34 @@ impl CmdResult { self } - pub fn stderr_contains>(&self, cmp: &T) -> &CmdResult { + pub fn stderr_contains>(&self, cmp: T) -> &CmdResult { assert!(self.stderr_str().contains(cmp.as_ref())); self } + + pub fn stdout_does_not_contain>(&self, cmp: T) -> &CmdResult { + assert!(!self.stdout_str().contains(cmp.as_ref())); + self + } + + pub fn stderr_does_not_contain>(&self, cmp: T) -> &CmdResult { + assert!(!self.stderr_str().contains(cmp.as_ref())); + self + } + + pub fn stdout_matches(&self, regex: ®ex::Regex) -> &CmdResult { + if !regex.is_match(self.stdout_str()) { + panic!("Stdout does not match regex:\n{}", self.stdout_str()) + } + self + } + + pub fn stdout_does_not_match(&self, regex: ®ex::Regex) -> &CmdResult { + if regex.is_match(self.stdout_str()) { + panic!("Stdout matches regex:\n{}", self.stdout_str()) + } + self + } } pub fn log_info, U: AsRef>(msg: T, par: U) { @@ -351,6 +410,13 @@ impl AtPath { String::from(self.minus(name).to_str().unwrap()) } + pub fn set_readonly(&self, name: &str) { + let metadata = fs::metadata(self.plus(name)).unwrap(); + let mut permissions = metadata.permissions(); + permissions.set_readonly(true); + fs::set_permissions(self.plus(name), permissions).unwrap(); + } + pub fn open(&self, name: &str) -> File { log_info("open", self.plus_as_string(name)); File::open(self.plus(name)).unwrap() @@ -624,6 +690,7 @@ pub struct UCommand { tmpd: Option>, has_run: bool, stdin: Option>, + ignore_stdin_write_error: bool, } impl UCommand { @@ -653,6 +720,7 @@ impl UCommand { }, comm_string: String::from(arg.as_ref().to_str().unwrap()), stdin: None, + ignore_stdin_write_error: false, } } @@ -705,6 +773,17 @@ impl UCommand { self.pipe_in(contents) } + /// Ignores error caused by feeding stdin to the command. + /// 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() { + panic!("{}", NO_STDIN_MEANINGLESS); + } + self.ignore_stdin_write_error = true; + self + } + pub fn env(&mut self, key: K, val: V) -> &mut UCommand where K: AsRef, @@ -725,7 +804,7 @@ impl UCommand { } self.has_run = true; log_info("run", &self.comm_string); - let mut result = self + let mut child = self .raw .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -734,15 +813,19 @@ impl UCommand { .unwrap(); if let Some(ref input) = self.stdin { - result + let write_result = child .stdin .take() .unwrap_or_else(|| panic!("Could not take child process stdin")) - .write_all(input) - .unwrap_or_else(|e| panic!("{}", e)); + .write_all(input); + if !self.ignore_stdin_write_error { + if let Err(e) = write_result { + panic!("failed to write to stdin of child: {}", e) + } + } } - result + child } /// Spawns the command, feeds the stdin if any, waits for the result @@ -797,3 +880,220 @@ pub fn read_size(child: &mut Child, size: usize) -> String { .unwrap(); String::from_utf8(output).unwrap() } + +pub fn vec_of_size(n: usize) -> Vec { + let mut result = Vec::new(); + for _ in 0..n { + result.push('a' as u8); + } + assert_eq!(result.len(), n); + result +} + +/// Sanity checks for test utils +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_code_is() { + let res = CmdResult { + tmpd: None, + code: Some(32), + success: false, + stdout: "".into(), + stderr: "".into(), + }; + res.code_is(32); + } + + #[test] + #[should_panic] + fn test_code_is_fail() { + let res = CmdResult { + tmpd: None, + code: Some(32), + success: false, + stdout: "".into(), + stderr: "".into(), + }; + res.code_is(1); + } + + #[test] + fn test_failure() { + let res = CmdResult { + tmpd: None, + code: None, + success: false, + stdout: "".into(), + stderr: "".into(), + }; + res.failure(); + } + + #[test] + #[should_panic] + fn test_failure_fail() { + let res = CmdResult { + tmpd: None, + code: None, + success: true, + stdout: "".into(), + stderr: "".into(), + }; + res.failure(); + } + + #[test] + fn test_success() { + let res = CmdResult { + tmpd: None, + code: None, + success: true, + stdout: "".into(), + stderr: "".into(), + }; + res.success(); + } + + #[test] + #[should_panic] + fn test_success_fail() { + let res = CmdResult { + tmpd: None, + code: None, + success: false, + stdout: "".into(), + stderr: "".into(), + }; + res.success(); + } + + #[test] + fn test_no_std_errout() { + let res = CmdResult { + tmpd: None, + code: None, + success: true, + stdout: "".into(), + stderr: "".into(), + }; + res.no_stderr(); + res.no_stdout(); + } + + #[test] + #[should_panic] + fn test_no_stderr_fail() { + let res = CmdResult { + tmpd: None, + code: None, + success: true, + stdout: "".into(), + stderr: "asdfsadfa".into(), + }; + + res.no_stderr(); + } + + #[test] + #[should_panic] + fn test_no_stdout_fail() { + let res = CmdResult { + tmpd: None, + code: None, + success: true, + stdout: "asdfsadfa".into(), + stderr: "".into(), + }; + + res.no_stdout(); + } + + #[test] + fn test_std_does_not_contain() { + let res = CmdResult { + tmpd: None, + code: None, + success: true, + stdout: "This is a likely error message\n".into(), + stderr: "This is a likely error message\n".into(), + }; + res.stdout_does_not_contain("unlikely"); + res.stderr_does_not_contain("unlikely"); + } + + #[test] + #[should_panic] + fn test_stdout_does_not_contain_fail() { + let res = CmdResult { + tmpd: None, + code: None, + success: true, + stdout: "This is a likely error message\n".into(), + stderr: "".into(), + }; + + res.stdout_does_not_contain("likely"); + } + + #[test] + #[should_panic] + fn test_stderr_does_not_contain_fail() { + let res = CmdResult { + tmpd: None, + code: None, + success: true, + stdout: "".into(), + stderr: "This is a likely error message\n".into(), + }; + + res.stderr_does_not_contain("likely"); + } + + #[test] + fn test_stdout_matches() { + let res = CmdResult { + tmpd: None, + code: None, + success: true, + stdout: "This is a likely error message\n".into(), + stderr: "This is a likely error message\n".into(), + }; + let positive = regex::Regex::new(".*likely.*").unwrap(); + let negative = regex::Regex::new(".*unlikely.*").unwrap(); + res.stdout_matches(&positive); + res.stdout_does_not_match(&negative); + } + + #[test] + #[should_panic] + fn test_stdout_matches_fail() { + let res = CmdResult { + tmpd: None, + code: None, + success: true, + stdout: "This is a likely error message\n".into(), + stderr: "This is a likely error message\n".into(), + }; + let negative = regex::Regex::new(".*unlikely.*").unwrap(); + + res.stdout_matches(&negative); + } + + #[test] + #[should_panic] + fn test_stdout_not_matches_fail() { + let res = CmdResult { + tmpd: None, + code: None, + success: true, + stdout: "This is a likely error message\n".into(), + stderr: "This is a likely error message\n".into(), + }; + let positive = regex::Regex::new(".*likely.*").unwrap(); + + res.stdout_does_not_match(&positive); + } +} diff --git a/tests/fixtures/cat/empty.txt b/tests/fixtures/cat/empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/cat/three_directories_and_file_and_stdin.stderr.expected b/tests/fixtures/cat/three_directories_and_file_and_stdin.stderr.expected new file mode 100644 index 000000000..1a8a33d77 --- /dev/null +++ b/tests/fixtures/cat/three_directories_and_file_and_stdin.stderr.expected @@ -0,0 +1,5 @@ +cat: test_directory3/test_directory4: Is a directory +cat: filewhichdoesnotexist.txt: No such file or directory (os error 2) +cat: test_directory3/test_directory5: Is a directory +cat: test_directory3/../test_directory3/test_directory5: Is a directory +cat: test_directory3: Is a directory diff --git a/tests/fixtures/install/helloworld.rs b/tests/fixtures/install/helloworld.rs new file mode 100644 index 000000000..47ad8c634 --- /dev/null +++ b/tests/fixtures/install/helloworld.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello World!"); +} diff --git a/tests/fixtures/install/helloworld_linux b/tests/fixtures/install/helloworld_linux new file mode 100755 index 000000000..c1c6b9b37 Binary files /dev/null and b/tests/fixtures/install/helloworld_linux differ diff --git a/tests/fixtures/install/helloworld_macos b/tests/fixtures/install/helloworld_macos new file mode 100755 index 000000000..40e6ee515 Binary files /dev/null and b/tests/fixtures/install/helloworld_macos differ diff --git a/tests/fixtures/sort/human-numeric-whitespace.expected b/tests/fixtures/sort/human-numeric-whitespace.expected new file mode 100644 index 000000000..6fb9291ff --- /dev/null +++ b/tests/fixtures/sort/human-numeric-whitespace.expected @@ -0,0 +1,11 @@ + + + + + + + +456K +4568K + 456M + 6.2G diff --git a/tests/fixtures/sort/human-numeric-whitespace.txt b/tests/fixtures/sort/human-numeric-whitespace.txt new file mode 100644 index 000000000..19db648b1 --- /dev/null +++ b/tests/fixtures/sort/human-numeric-whitespace.txt @@ -0,0 +1,11 @@ + + +456K + + 456M + + +4568K + + 6.2G + diff --git a/tests/fixtures/sort/mixed_floats_ints_chars_numeric_unique_reverse_stable.expected b/tests/fixtures/sort/mixed_floats_ints_chars_numeric_unique_reverse_stable.expected new file mode 100644 index 000000000..bbce16934 --- /dev/null +++ b/tests/fixtures/sort/mixed_floats_ints_chars_numeric_unique_reverse_stable.expected @@ -0,0 +1,20 @@ +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 + +-.05 +-1 +-8.90880 +-896689 +-2028789030 diff --git a/tests/fixtures/sort/months-whitespace.expected b/tests/fixtures/sort/months-whitespace.expected new file mode 100644 index 000000000..84a44d564 --- /dev/null +++ b/tests/fixtures/sort/months-whitespace.expected @@ -0,0 +1,8 @@ + + +JAN + FEb + apr + apr + JUNNNN +AUG diff --git a/tests/fixtures/sort/months-whitespace.txt b/tests/fixtures/sort/months-whitespace.txt new file mode 100644 index 000000000..45c477477 --- /dev/null +++ b/tests/fixtures/sort/months-whitespace.txt @@ -0,0 +1,8 @@ +JAN + JUNNNN +AUG + + apr + apr + + FEb diff --git a/tests/fixtures/sort/multiple_decimals.expected b/tests/fixtures/sort/multiple_decimals.expected new file mode 100644 index 000000000..6afbdcaa0 --- /dev/null +++ b/tests/fixtures/sort/multiple_decimals.expected @@ -0,0 +1,33 @@ +-2028789030 +-896689 +-8.90880 +-1 +-.05 + + + + + + + + +000 +CARAvan +00000001 +1 +1.040000000 +1.444 +1.58590 +8.013 +45 +46.89 + 4567..457 + 4567. +4567.1 +4567.34 + 37800 +576,446.88800000 +576,446.890 +4798908.340000000000 +4798908.45 +4798908.8909800 diff --git a/tests/fixtures/sort/multiple_decimals_general.txt b/tests/fixtures/sort/multiple_decimals_general.txt new file mode 100644 index 000000000..4e65ecfda --- /dev/null +++ b/tests/fixtures/sort/multiple_decimals_general.txt @@ -0,0 +1,35 @@ +576,446.890 +576,446.88800000 + +4567.1 + 4567..457 + 45670.89079.1 + 45670.89079.098 +4567.34 + 4567. +45 +46.89 +-1 +1 +00000001 +4798908.340000000000 +4798908.45 +4798908.8909800 + + + 37800 + +-2028789030 +-896689 +CARAvan + +-8.90880 +-.05 +1.444 +1.58590 +1.040000000 + +8.013 + +000 + diff --git a/tests/fixtures/sort/multiple_decimals_numeric.txt b/tests/fixtures/sort/multiple_decimals_numeric.txt new file mode 100644 index 000000000..4e65ecfda --- /dev/null +++ b/tests/fixtures/sort/multiple_decimals_numeric.txt @@ -0,0 +1,35 @@ +576,446.890 +576,446.88800000 + +4567.1 + 4567..457 + 45670.89079.1 + 45670.89079.098 +4567.34 + 4567. +45 +46.89 +-1 +1 +00000001 +4798908.340000000000 +4798908.45 +4798908.8909800 + + + 37800 + +-2028789030 +-896689 +CARAvan + +-8.90880 +-.05 +1.444 +1.58590 +1.040000000 + +8.013 + +000 + diff --git a/tests/fixtures/sort/numeric_unique.expected b/tests/fixtures/sort/numeric_unique.expected new file mode 100644 index 000000000..8a31187f6 --- /dev/null +++ b/tests/fixtures/sort/numeric_unique.expected @@ -0,0 +1,2 @@ +-10 bb +aa diff --git a/tests/fixtures/sort/numeric_unique.txt b/tests/fixtures/sort/numeric_unique.txt new file mode 100644 index 000000000..15cc08022 --- /dev/null +++ b/tests/fixtures/sort/numeric_unique.txt @@ -0,0 +1,3 @@ +aa +-10 bb +-10 aa diff --git a/tests/fixtures/sort/version-empty-lines.expected b/tests/fixtures/sort/version-empty-lines.expected new file mode 100644 index 000000000..c496c0ff5 --- /dev/null +++ b/tests/fixtures/sort/version-empty-lines.expected @@ -0,0 +1,11 @@ + + + + + + + +1.2.3-alpha +1.2.3-alpha2 +11.2.3 + 1.12.4 diff --git a/tests/fixtures/sort/version-empty-lines.txt b/tests/fixtures/sort/version-empty-lines.txt new file mode 100644 index 000000000..9b6b89788 --- /dev/null +++ b/tests/fixtures/sort/version-empty-lines.txt @@ -0,0 +1,11 @@ +11.2.3 + + + +1.2.3-alpha2 + + +1.2.3-alpha + + + 1.12.4 diff --git a/tests/fixtures/sort/words_unique.expected b/tests/fixtures/sort/words_unique.expected new file mode 100644 index 000000000..2444ce1c6 --- /dev/null +++ b/tests/fixtures/sort/words_unique.expected @@ -0,0 +1,3 @@ +aaa +bbb +zzz diff --git a/tests/fixtures/sort/words_unique.txt b/tests/fixtures/sort/words_unique.txt new file mode 100644 index 000000000..9c6666029 --- /dev/null +++ b/tests/fixtures/sort/words_unique.txt @@ -0,0 +1,4 @@ +zzz +aaa +bbb +bbb