1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-08-02 14:07:46 +00:00

Merge branch 'master' of https://github.com/uutils/coreutils into hbina-tr-reimplement-expansion

Signed-off-by: Hanif Bin Ariffin <hanif.ariffin.4326@gmail.com>
This commit is contained in:
Hanif Bin Ariffin 2021-10-21 13:31:39 +08:00
commit 6c67f19df4
9 changed files with 287 additions and 85 deletions

10
Cargo.lock generated
View file

@ -1062,6 +1062,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "memmap2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4647a11b578fead29cdbb34d4adef8dd3dc35b876c9c6d5240d83f205abfe96e"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.6.4" version = "0.6.4"
@ -3046,6 +3055,7 @@ version = "0.0.7"
dependencies = [ dependencies = [
"clap", "clap",
"memchr 2.4.0", "memchr 2.4.0",
"memmap2",
"regex", "regex",
"uucore", "uucore",
"uucore_procs", "uucore_procs",

View file

@ -189,13 +189,31 @@ pub struct DigestWriter<'a> {
/// "\n" before passing input bytes to the [`digest`]. /// "\n" before passing input bytes to the [`digest`].
#[allow(dead_code)] #[allow(dead_code)]
binary: bool, binary: bool,
// TODO This is dead code only on non-Windows operating systems. It
// might be better to use a `#[cfg(windows)]` guard here. /// Whether the previous
#[allow(dead_code)]
was_last_character_carriage_return: bool,
// TODO These are dead code only on non-Windows operating systems.
// It might be better to use a `#[cfg(windows)]` guard here.
} }
impl<'a> DigestWriter<'a> { impl<'a> DigestWriter<'a> {
pub fn new(digest: &'a mut Box<dyn Digest>, binary: bool) -> DigestWriter { pub fn new(digest: &'a mut Box<dyn Digest>, binary: bool) -> DigestWriter {
DigestWriter { digest, binary } let was_last_character_carriage_return = false;
DigestWriter {
digest,
binary,
was_last_character_carriage_return,
}
}
pub fn finalize(&mut self) -> bool {
if self.was_last_character_carriage_return {
self.digest.input(&[b'\r']);
true
} else {
false
}
} }
} }
@ -213,22 +231,40 @@ impl<'a> Write for DigestWriter<'a> {
return Ok(buf.len()); return Ok(buf.len());
} }
// In Windows text mode, replace each occurrence of "\r\n" // The remaining code handles Windows text mode, where we must
// with "\n". // replace each occurrence of "\r\n" with "\n".
// //
// Find all occurrences of "\r\n", inputting the slice just // First, if the last character written was "\r" and the first
// before the "\n" in the previous instance of "\r\n" and // character in the current buffer to write is not "\n", then we
// the beginning of this "\r\n". // need to write the "\r" that we buffered from the previous
// // call to `write()`.
// FIXME This fails if one call to `write()` ends with the
// "\r" and the next call to `write()` begins with the "\n".
let n = buf.len(); let n = buf.len();
if self.was_last_character_carriage_return && n > 0 && buf[0] != b'\n' {
self.digest.input(&[b'\r']);
}
// Next, find all occurrences of "\r\n", inputting the slice
// just before the "\n" in the previous instance of "\r\n" and
// the beginning of this "\r\n".
let mut i_prev = 0; let mut i_prev = 0;
for i in memmem::find_iter(buf, b"\r\n") { for i in memmem::find_iter(buf, b"\r\n") {
self.digest.input(&buf[i_prev..i]); self.digest.input(&buf[i_prev..i]);
i_prev = i + 1; i_prev = i + 1;
} }
self.digest.input(&buf[i_prev..n]);
// Finally, check whether the last character is "\r". If so,
// buffer it until we know that the next character is not "\n",
// which can only be known on the next call to `write()`.
//
// This all assumes that `write()` will be called on adjacent
// blocks of the input.
if n > 0 && buf[n - 1] == b'\r' {
self.was_last_character_carriage_return = true;
self.digest.input(&buf[i_prev..n - 1]);
} else {
self.was_last_character_carriage_return = false;
self.digest.input(&buf[i_prev..n]);
}
// Even though we dropped a "\r" for each "\r\n" we found, we // Even though we dropped a "\r" for each "\r\n" we found, we
// still report the number of bytes written as `n`. This is // still report the number of bytes written as `n`. This is
@ -243,3 +279,36 @@ impl<'a> Write for DigestWriter<'a> {
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
/// Test for replacing a "\r\n" sequence with "\n" when the "\r" is
/// at the end of one block and the "\n" is at the beginning of the
/// next block, when reading in blocks.
#[cfg(windows)]
#[test]
fn test_crlf_across_blocks() {
use std::io::Write;
use crate::digest::Digest;
use crate::digest::DigestWriter;
// Writing "\r" in one call to `write()`, and then "\n" in another.
let mut digest = Box::new(md5::Context::new()) as Box<dyn Digest>;
let mut writer_crlf = DigestWriter::new(&mut digest, false);
writer_crlf.write_all(&[b'\r']).unwrap();
writer_crlf.write_all(&[b'\n']).unwrap();
writer_crlf.finalize();
let result_crlf = digest.result_str();
// We expect "\r\n" to be replaced with "\n" in text mode on Windows.
let mut digest = Box::new(md5::Context::new()) as Box<dyn Digest>;
let mut writer_lf = DigestWriter::new(&mut digest, false);
writer_lf.write_all(&[b'\n']).unwrap();
writer_lf.finalize();
let result_lf = digest.result_str();
assert_eq!(result_crlf, result_lf);
}
}

View file

@ -611,8 +611,16 @@ fn digest_reader<T: Read>(
// If `binary` is `false` and the operating system is Windows, then // If `binary` is `false` and the operating system is Windows, then
// `DigestWriter` replaces "\r\n" with "\n" before it writes the // `DigestWriter` replaces "\r\n" with "\n" before it writes the
// bytes into `digest`. Otherwise, it just inserts the bytes as-is. // bytes into `digest`. Otherwise, it just inserts the bytes as-is.
//
// In order to support replacing "\r\n", we must call `finalize()`
// in order to support the possibility that the last character read
// from the reader was "\r". (This character gets buffered by
// `DigestWriter` and only written if the following character is
// "\n". But when "\r" is the last character read, we need to force
// it to be written.)
let mut digest_writer = DigestWriter::new(digest, binary); let mut digest_writer = DigestWriter::new(digest, binary);
std::io::copy(reader, &mut digest_writer)?; std::io::copy(reader, &mut digest_writer)?;
digest_writer.finalize();
if digest.output_bits() > 0 { if digest.output_bits() > 0 {
Ok(digest.result_str()) Ok(digest.result_str())

View file

@ -19,7 +19,6 @@ const BUF_SIZE: usize = 65536;
const ABOUT: &str = "\ const ABOUT: &str = "\
Print the first 10 lines of each FILE to standard output.\n\ Print the first 10 lines of each FILE to standard output.\n\
With more than one FILE, precede each with a header giving the file name.\n\ With more than one FILE, precede each with a header giving the file name.\n\
\n\
With no FILE, or when FILE is -, read standard input.\n\ With no FILE, or when FILE is -, read standard input.\n\
\n\ \n\
Mandatory arguments to long flags are mandatory for short flags too.\ Mandatory arguments to long flags are mandatory for short flags too.\
@ -108,6 +107,12 @@ enum Modes {
Bytes(usize), Bytes(usize),
} }
impl Default for Modes {
fn default() -> Self {
Modes::Lines(10)
}
}
fn parse_mode<F>(src: &str, closure: F) -> Result<(Modes, bool), String> fn parse_mode<F>(src: &str, closure: F) -> Result<(Modes, bool), String>
where where
F: FnOnce(usize) -> Modes, F: FnOnce(usize) -> Modes,
@ -144,7 +149,7 @@ fn arg_iterate<'a>(
} }
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Default)]
struct HeadOptions { struct HeadOptions {
pub quiet: bool, pub quiet: bool,
pub verbose: bool, pub verbose: bool,
@ -155,22 +160,11 @@ struct HeadOptions {
} }
impl HeadOptions { impl HeadOptions {
pub fn new() -> HeadOptions {
HeadOptions {
quiet: false,
verbose: false,
zeroed: false,
all_but_last: false,
mode: Modes::Lines(10),
files: Vec::new(),
}
}
///Construct options from matches ///Construct options from matches
pub fn get_from(args: impl uucore::Args) -> Result<Self, String> { pub fn get_from(args: impl uucore::Args) -> Result<Self, String> {
let matches = uu_app().get_matches_from(arg_iterate(args)?); let matches = uu_app().get_matches_from(arg_iterate(args)?);
let mut options = HeadOptions::new(); let mut options: HeadOptions = Default::default();
options.quiet = matches.is_present(options::QUIET_NAME); options.quiet = matches.is_present(options::QUIET_NAME);
options.verbose = matches.is_present(options::VERBOSE_NAME); options.verbose = matches.is_present(options::VERBOSE_NAME);
@ -197,12 +191,6 @@ impl HeadOptions {
Ok(options) Ok(options)
} }
} }
// to make clippy shut up
impl Default for HeadOptions {
fn default() -> Self {
Self::new()
}
}
fn read_n_bytes<R>(input: R, n: usize) -> std::io::Result<()> fn read_n_bytes<R>(input: R, n: usize) -> std::io::Result<()>
where where
@ -523,17 +511,13 @@ mod tests {
assert!(options("-c IsThisJustFantasy").is_err()); assert!(options("-c IsThisJustFantasy").is_err());
} }
#[test] #[test]
#[allow(clippy::bool_comparison)]
fn test_options_correct_defaults() { fn test_options_correct_defaults() {
let opts = HeadOptions::new(); let opts: HeadOptions = Default::default();
let opts2: HeadOptions = Default::default();
assert_eq!(opts, opts2); assert!(!opts.verbose);
assert!(!opts.quiet);
assert!(opts.verbose == false); assert!(!opts.zeroed);
assert!(opts.quiet == false); assert!(!opts.all_but_last);
assert!(opts.zeroed == false);
assert!(opts.all_but_last == false);
assert_eq!(opts.mode, Modes::Lines(10)); assert_eq!(opts.mode, Modes::Lines(10));
assert!(opts.files.is_empty()); assert!(opts.files.is_empty());
} }

View file

@ -0,0 +1,25 @@
## Benchmarking `tac`
<!-- spell-checker:ignore wikidatawiki -->
`tac` is often used to process log files in reverse chronological order, i.e. from newer towards older entries. In this case, the performance target to yield results as fast as possible, i.e. without reading in the whole file that is to be reversed line-by-line. Therefore, a sensible benchmark is to read a large log file containing N lines and measure how long it takes to produce the last K lines from that file.
Large text files can for example be found in the [Wikipedia database dumps](https://dumps.wikimedia.org/wikidatawiki/latest/), usually sized at multiple gigabytes and comprising more than 100M lines.
After you have obtained and uncompressed such a file, you need to build `tac` in release mode
```shell
$ cargo build --release --package uu_tac
```
and then you can time how it long it takes to extract the last 10M lines by running
```shell
$ /usr/bin/time ./target/release/tac wikidatawiki-20211001-pages-logging.xml | head -n10000000 >/dev/null
```
For more systematic measurements that include warm-ups, repetitions and comparisons, [Hyperfine](https://github.com/sharkdp/hyperfine) can be helpful. For example, to compare this implementation to the one provided by your distribution run
```shell
$ hyperfine "./target/release/tac wikidatawiki-20211001-pages-logging.xml | head -n10000000 >/dev/null" "/usr/bin/tac wikidatawiki-20211001-pages-logging.xml | head -n10000000 >/dev/null"
```

View file

@ -1,3 +1,5 @@
# spell-checker:ignore memmap
[package] [package]
name = "uu_tac" name = "uu_tac"
version = "0.0.7" version = "0.0.7"
@ -16,6 +18,7 @@ path = "src/tac.rs"
[dependencies] [dependencies]
memchr = "2" memchr = "2"
memmap2 = "0.5"
regex = "1" regex = "1"
clap = { version = "2.33", features = ["wrap_help"] } clap = { version = "2.33", features = ["wrap_help"] }
uucore = { version=">=0.0.9", package="uucore", path="../../uucore" } uucore = { version=">=0.0.9", package="uucore", path="../../uucore" }

View file

@ -5,15 +5,19 @@
// * For the full copyright and license information, please view the LICENSE // * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code. // * file that was distributed with this source code.
// spell-checker:ignore (ToDO) sbytes slen dlen memmem // spell-checker:ignore (ToDO) sbytes slen dlen memmem memmap Mmap mmap SIGBUS
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use clap::{crate_version, App, Arg}; use clap::{crate_version, App, Arg};
use memchr::memmem; use memchr::memmem;
use std::io::{stdin, stdout, BufReader, Read, Write}; use memmap2::Mmap;
use std::{fs::File, path::Path}; use std::io::{stdin, stdout, BufWriter, Read, Write};
use std::{
fs::{read, File},
path::Path,
};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
@ -44,9 +48,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
raw_separator raw_separator
}; };
let files: Vec<String> = match matches.values_of(options::FILE) { let files: Vec<&str> = match matches.values_of(options::FILE) {
Some(v) => v.map(|v| v.to_owned()).collect(), Some(v) => v.collect(),
None => vec!["-".to_owned()], None => vec!["-"],
}; };
tac(files, before, regex, separator) tac(files, before, regex, separator)
@ -102,10 +106,11 @@ pub fn uu_app() -> App<'static, 'static> {
/// returns [`std::io::Error`]. /// returns [`std::io::Error`].
fn buffer_tac_regex( fn buffer_tac_regex(
data: &[u8], data: &[u8],
pattern: regex::bytes::Regex, pattern: &regex::bytes::Regex,
before: bool, before: bool,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let mut out = stdout(); let out = stdout();
let mut out = BufWriter::new(out.lock());
// The index of the line separator for the current line. // The index of the line separator for the current line.
// //
@ -171,7 +176,8 @@ fn buffer_tac_regex(
/// `separator` appears at the beginning of each line, as in /// `separator` appears at the beginning of each line, as in
/// `"/abc/def"`. /// `"/abc/def"`.
fn buffer_tac(data: &[u8], before: bool, separator: &str) -> std::io::Result<()> { fn buffer_tac(data: &[u8], before: bool, separator: &str) -> std::io::Result<()> {
let mut out = stdout(); let out = stdout();
let mut out = BufWriter::new(out.lock());
// The number of bytes in the line separator. // The number of bytes in the line separator.
let slen = separator.as_bytes().len(); let slen = separator.as_bytes().len();
@ -208,12 +214,33 @@ fn buffer_tac(data: &[u8], before: bool, separator: &str) -> std::io::Result<()>
Ok(()) Ok(())
} }
fn tac(filenames: Vec<String>, before: bool, regex: bool, separator: &str) -> i32 { fn tac(filenames: Vec<&str>, before: bool, regex: bool, separator: &str) -> i32 {
let mut exit_code = 0; let mut exit_code = 0;
for filename in &filenames { let pattern = if regex {
let mut file = BufReader::new(if filename == "-" { Some(crash_if_err!(1, regex::bytes::Regex::new(separator)))
Box::new(stdin()) as Box<dyn Read> } else {
None
};
for &filename in &filenames {
let mmap;
let buf;
let data: &[u8] = if filename == "-" {
if let Some(mmap1) = try_mmap_stdin() {
mmap = mmap1;
&mmap
} else {
let mut buf1 = Vec::new();
if let Err(e) = stdin().read_to_end(&mut buf1) {
show_error!("failed to read from stdin: {}", e);
exit_code = 1;
continue;
}
buf = buf1;
&buf
}
} else { } else {
let path = Path::new(filename); let path = Path::new(filename);
if path.is_dir() || path.metadata().is_err() { if path.is_dir() || path.metadata().is_err() {
@ -228,29 +255,47 @@ fn tac(filenames: Vec<String>, before: bool, regex: bool, separator: &str) -> i3
exit_code = 1; exit_code = 1;
continue; continue;
} }
match File::open(path) {
Ok(f) => Box::new(f) as Box<dyn Read>, if let Some(mmap1) = try_mmap_path(path) {
Err(e) => { mmap = mmap1;
show_error!("failed to open {} for reading: {}", filename.quote(), e); &mmap
exit_code = 1; } else {
continue; match read(path) {
Ok(buf1) => {
buf = buf1;
&buf
}
Err(e) => {
show_error!("failed to read {}: {}", filename.quote(), e);
exit_code = 1;
continue;
}
} }
} }
});
let mut data = Vec::new();
if let Err(e) = file.read_to_end(&mut data) {
show_error!("failed to read {}: {}", filename.quote(), e);
exit_code = 1;
continue;
}; };
if regex {
let pattern = crash_if_err!(1, regex::bytes::Regex::new(separator)); if let Some(pattern) = &pattern {
buffer_tac_regex(&data, pattern, before) buffer_tac_regex(data, pattern, before)
} else { } else {
buffer_tac(&data, before, separator) buffer_tac(data, before, separator)
} }
.unwrap_or_else(|e| crash!(1, "failed to write to stdout: {}", e)); .unwrap_or_else(|e| crash!(1, "failed to write to stdout: {}", e));
} }
exit_code exit_code
} }
fn try_mmap_stdin() -> Option<Mmap> {
// SAFETY: If the file is truncated while we map it, SIGBUS will be raised
// and our process will be terminated, thus preventing access of invalid memory.
unsafe { Mmap::map(&stdin()).ok() }
}
fn try_mmap_path(path: &Path) -> Option<Mmap> {
let file = File::open(path).ok()?;
// SAFETY: If the file is truncated while we map it, SIGBUS will be raised
// and our process will be terminated, thus preventing access of invalid memory.
let mmap = unsafe { Mmap::map(&file).ok()? };
Some(mmap)
}

View file

@ -35,6 +35,15 @@ use crate::platform::stdin_is_pipe_or_fifo;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
const ABOUT: &str = "\
Print the last 10 lines of each FILE to standard output.\n\
With more than one FILE, precede each with a header giving the file name.\n\
With no FILE, or when FILE is -, read standard input.\n\
\n\
Mandatory arguments to long flags are mandatory for short flags too.\
";
const USAGE: &str = "tail [FLAG]... [FILE]...";
pub mod options { pub mod options {
pub mod verbosity { pub mod verbosity {
pub static QUIET: &str = "quiet"; pub static QUIET: &str = "quiet";
@ -218,8 +227,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
pub fn uu_app() -> App<'static, 'static> { pub fn uu_app() -> App<'static, 'static> {
App::new(uucore::util_name()) App::new(uucore::util_name())
.version(crate_version!()) .version(crate_version!())
.about("output the last part of files") .about(ABOUT)
// TODO: add usage .usage(USAGE)
.arg( .arg(
Arg::with_name(options::BYTES) Arg::with_name(options::BYTES)
.short("c") .short("c")

View file

@ -510,43 +510,86 @@ impl AtPath {
} }
pub fn write(&self, name: &str, contents: &str) { pub fn write(&self, name: &str, contents: &str) {
log_info("open(write)", self.plus_as_string(name)); log_info("write(default)", self.plus_as_string(name));
std::fs::write(self.plus(name), contents) std::fs::write(self.plus(name), contents)
.unwrap_or_else(|e| panic!("Couldn't write {}: {}", name, e)); .unwrap_or_else(|e| panic!("Couldn't write {}: {}", name, e));
} }
pub fn write_bytes(&self, name: &str, contents: &[u8]) { pub fn write_bytes(&self, name: &str, contents: &[u8]) {
log_info("open(write)", self.plus_as_string(name)); log_info("write(default)", self.plus_as_string(name));
std::fs::write(self.plus(name), contents) std::fs::write(self.plus(name), contents)
.unwrap_or_else(|e| panic!("Couldn't write {}: {}", name, e)); .unwrap_or_else(|e| panic!("Couldn't write {}: {}", name, e));
} }
pub fn append(&self, name: &str, contents: &str) { pub fn append(&self, name: &str, contents: &str) {
log_info("open(append)", self.plus_as_string(name)); log_info("write(append)", self.plus_as_string(name));
let mut f = OpenOptions::new() let mut f = OpenOptions::new()
.write(true) .write(true)
.append(true) .append(true)
.create(true)
.open(self.plus(name)) .open(self.plus(name))
.unwrap(); .unwrap();
f.write_all(contents.as_bytes()) f.write_all(contents.as_bytes())
.unwrap_or_else(|e| panic!("Couldn't write {}: {}", name, e)); .unwrap_or_else(|e| panic!("Couldn't write(append) {}: {}", name, e));
} }
pub fn append_bytes(&self, name: &str, contents: &[u8]) { pub fn append_bytes(&self, name: &str, contents: &[u8]) {
log_info("open(append)", self.plus_as_string(name)); log_info("write(append)", self.plus_as_string(name));
let mut f = OpenOptions::new() let mut f = OpenOptions::new()
.write(true) .write(true)
.append(true) .append(true)
.create(true)
.open(self.plus(name)) .open(self.plus(name))
.unwrap(); .unwrap();
f.write_all(contents) f.write_all(contents)
.unwrap_or_else(|e| panic!("Couldn't append to {}: {}", name, e)); .unwrap_or_else(|e| panic!("Couldn't write(append) to {}: {}", name, e));
}
pub fn truncate(&self, name: &str, contents: &str) {
log_info("write(truncate)", self.plus_as_string(name));
let mut f = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(self.plus(name))
.unwrap();
f.write_all(contents.as_bytes())
.unwrap_or_else(|e| panic!("Couldn't write(truncate) {}: {}", name, e));
}
pub fn rename(&self, source: &str, target: &str) {
let source = self.plus(source);
let target = self.plus(target);
log_info("rename", format!("{:?} {:?}", source, target));
std::fs::rename(&source, &target)
.unwrap_or_else(|e| panic!("Couldn't rename {:?} -> {:?}: {}", source, target, e));
}
pub fn remove(&self, source: &str) {
let source = self.plus(source);
log_info("remove", format!("{:?}", source));
std::fs::remove_file(&source)
.unwrap_or_else(|e| panic!("Couldn't remove {:?}: {}", source, e));
}
pub fn copy(&self, source: &str, target: &str) {
let source = self.plus(source);
let target = self.plus(target);
log_info("copy", format!("{:?} {:?}", source, target));
std::fs::copy(&source, &target)
.unwrap_or_else(|e| panic!("Couldn't copy {:?} -> {:?}: {}", source, target, e));
}
pub fn rmdir(&self, dir: &str) {
log_info("rmdir", self.plus_as_string(dir));
fs::remove_dir(&self.plus(dir)).unwrap();
} }
pub fn mkdir(&self, dir: &str) { pub fn mkdir(&self, dir: &str) {
log_info("mkdir", self.plus_as_string(dir)); log_info("mkdir", self.plus_as_string(dir));
fs::create_dir(&self.plus(dir)).unwrap(); fs::create_dir(&self.plus(dir)).unwrap();
} }
pub fn mkdir_all(&self, dir: &str) { pub fn mkdir_all(&self, dir: &str) {
log_info("mkdir_all", self.plus_as_string(dir)); log_info("mkdir_all", self.plus_as_string(dir));
fs::create_dir_all(self.plus(dir)).unwrap(); fs::create_dir_all(self.plus(dir)).unwrap();
@ -864,7 +907,7 @@ impl UCommand {
/// Add a parameter to the invocation. Path arguments are treated relative /// Add a parameter to the invocation. Path arguments are treated relative
/// to the test environment directory. /// to the test environment directory.
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut UCommand { pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut UCommand {
assert!(!self.has_run, ALREADY_RUN); assert!(!self.has_run, "{}", ALREADY_RUN);
self.comm_string.push(' '); self.comm_string.push(' ');
self.comm_string self.comm_string
.push_str(arg.as_ref().to_str().unwrap_or_default()); .push_str(arg.as_ref().to_str().unwrap_or_default());
@ -875,7 +918,7 @@ impl UCommand {
/// Add multiple parameters to the invocation. Path arguments are treated relative /// Add multiple parameters to the invocation. Path arguments are treated relative
/// to the test environment directory. /// to the test environment directory.
pub fn args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut UCommand { pub fn args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut UCommand {
assert!(!self.has_run, MULTIPLE_STDIN_MEANINGLESS); assert!(!self.has_run, "{}", MULTIPLE_STDIN_MEANINGLESS);
let strings = args let strings = args
.iter() .iter()
.map(|s| s.as_ref().to_os_string()) .map(|s| s.as_ref().to_os_string())
@ -893,7 +936,11 @@ impl UCommand {
/// provides standard input to feed in to the command when spawned /// provides standard input to feed in to the command when spawned
pub fn pipe_in<T: Into<Vec<u8>>>(&mut self, input: T) -> &mut UCommand { pub fn pipe_in<T: Into<Vec<u8>>>(&mut self, input: T) -> &mut UCommand {
assert!(!self.bytes_into_stdin.is_some(), MULTIPLE_STDIN_MEANINGLESS); assert!(
!self.bytes_into_stdin.is_some(),
"{}",
MULTIPLE_STDIN_MEANINGLESS
);
self.bytes_into_stdin = Some(input.into()); self.bytes_into_stdin = Some(input.into());
self self
} }
@ -908,7 +955,7 @@ impl UCommand {
/// This is typically useful to test non-standard workflows /// This is typically useful to test non-standard workflows
/// like feeding something to a command that does not read it /// like feeding something to a command that does not read it
pub fn ignore_stdin_write_error(&mut self) -> &mut UCommand { pub fn ignore_stdin_write_error(&mut self) -> &mut UCommand {
assert!(!self.bytes_into_stdin.is_none(), NO_STDIN_MEANINGLESS); assert!(!self.bytes_into_stdin.is_none(), "{}", NO_STDIN_MEANINGLESS);
self.ignore_stdin_write_error = true; self.ignore_stdin_write_error = true;
self self
} }
@ -918,7 +965,7 @@ impl UCommand {
K: AsRef<OsStr>, K: AsRef<OsStr>,
V: AsRef<OsStr>, V: AsRef<OsStr>,
{ {
assert!(!self.has_run, ALREADY_RUN); assert!(!self.has_run, "{}", ALREADY_RUN);
self.raw.env(key, val); self.raw.env(key, val);
self self
} }
@ -937,7 +984,7 @@ impl UCommand {
/// Spawns the command, feeds the stdin if any, and returns the /// Spawns the command, feeds the stdin if any, and returns the
/// child process immediately. /// child process immediately.
pub fn run_no_wait(&mut self) -> Child { pub fn run_no_wait(&mut self) -> Child {
assert!(!self.has_run, ALREADY_RUN); assert!(!self.has_run, "{}", ALREADY_RUN);
self.has_run = true; self.has_run = true;
log_info("run", &self.comm_string); log_info("run", &self.comm_string);
let mut child = self let mut child = self
@ -1020,6 +1067,8 @@ impl UCommand {
} }
} }
/// Wrapper for `child.stdout.read_exact()`.
/// Careful, this blocks indefinitely if `size` bytes is never reached.
pub fn read_size(child: &mut Child, size: usize) -> String { pub fn read_size(child: &mut Child, size: usize) -> String {
let mut output = Vec::new(); let mut output = Vec::new();
output.resize(size, 0); output.resize(size, 0);