diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 0372336a7..cab39a872 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -615,11 +615,6 @@ jobs: # staging directory STAGING='_staging' outputs STAGING - ## # check for CODECOV_TOKEN availability (work-around for inaccessible 'secrets' object for 'if'; see ) - ## # note: CODECOV_TOKEN / HAS_CODECOV_TOKEN is not needed for public repositories when using AppVeyor, Azure Pipelines, CircleCI, GitHub Actions, Travis (see ) - ## unset HAS_CODECOV_TOKEN - ## if [ -n $CODECOV_TOKEN ]; then HAS_CODECOV_TOKEN='true' ; fi - ## outputs HAS_CODECOV_TOKEN # target-specific options # * CARGO_FEATURES_OPTION CARGO_FEATURES_OPTION='--all-features' ; ## default to '--all-features' for code coverage diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index 68c55b4cb..09ed3cda6 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -7,19 +7,17 @@ // spell-checker:ignore (ToDO) Chmoder cmode fmode fperm fref ugoa RFILE RFILE's -#[macro_use] -extern crate uucore; - use clap::{crate_version, App, Arg}; use std::fs; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::Path; use uucore::display::Quotable; +use uucore::error::{ExitCode, UResult, USimpleError, UUsageError}; use uucore::fs::display_permissions_unix; use uucore::libc::mode_t; #[cfg(not(windows))] use uucore::mode; -use uucore::InvalidEncodingHandling; +use uucore::{show_error, InvalidEncodingHandling}; use walkdir::WalkDir; static ABOUT: &str = "Change the mode of each FILE to MODE. @@ -50,7 +48,8 @@ fn get_long_usage() -> String { String::from("Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'.") } -pub fn uumain(args: impl uucore::Args) -> i32 { +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut args = args .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); @@ -72,12 +71,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let verbose = matches.is_present(options::VERBOSE); let preserve_root = matches.is_present(options::PRESERVE_ROOT); let recursive = matches.is_present(options::RECURSIVE); - let fmode = matches - .value_of(options::REFERENCE) - .and_then(|fref| match fs::metadata(fref) { + let fmode = match matches.value_of(options::REFERENCE) { + Some(fref) => match fs::metadata(fref) { Ok(meta) => Some(meta.mode()), - Err(err) => crash!(1, "cannot stat attributes of {}: {}", fref.quote(), err), - }); + Err(err) => { + return Err(USimpleError::new( + 1, + format!("cannot stat attributes of {}: {}", fref.quote(), err), + )) + } + }, + None => None, + }; let modes = matches.value_of(options::MODE).unwrap(); // should always be Some because required let cmode = if mode_had_minus_prefix { // clap parsing is finished, now put prefix back @@ -100,7 +105,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { }; if files.is_empty() { - crash!(1, "missing operand"); + return Err(UUsageError::new(1, "missing operand".to_string())); } let chmoder = Chmoder { @@ -112,12 +117,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { fmode, cmode, }; - match chmoder.chmod(files) { - Ok(()) => {} - Err(e) => return e, - } - 0 + chmoder.chmod(files) } pub fn uu_app() -> App<'static, 'static> { @@ -191,7 +192,7 @@ struct Chmoder { } impl Chmoder { - fn chmod(&self, files: Vec) -> Result<(), i32> { + fn chmod(&self, files: Vec) -> UResult<()> { let mut r = Ok(()); for filename in &files { @@ -204,22 +205,30 @@ impl Chmoder { filename.quote() ); if !self.quiet { - show_error!("cannot operate on dangling symlink {}", filename.quote()); + return Err(USimpleError::new( + 1, + format!("cannot operate on dangling symlink {}", filename.quote()), + )); } } else if !self.quiet { - show_error!( - "cannot access {}: No such file or directory", - filename.quote() - ); + return Err(USimpleError::new( + 1, + format!( + "cannot access {}: No such file or directory", + filename.quote() + ), + )); } - return Err(1); + return Err(ExitCode::new(1)); } if self.recursive && self.preserve_root && filename == "/" { - show_error!( - "it is dangerous to operate recursively on {}\nuse --no-preserve-root to override this failsafe", - filename.quote() - ); - return Err(1); + return Err(USimpleError::new( + 1, + format!( + "it is dangerous to operate recursively on {}\nuse --no-preserve-root to override this failsafe", + filename.quote() + ) + )); } if !self.recursive { r = self.chmod_file(file).and(r); @@ -234,14 +243,14 @@ impl Chmoder { } #[cfg(windows)] - fn chmod_file(&self, file: &Path) -> Result<(), i32> { + fn chmod_file(&self, file: &Path) -> UResult<()> { // chmod is useless on Windows // it doesn't set any permissions at all // instead it just sets the readonly attribute on the file - Err(0) + Ok(()) } #[cfg(unix)] - fn chmod_file(&self, file: &Path) -> Result<(), i32> { + fn chmod_file(&self, file: &Path) -> UResult<()> { use uucore::mode::get_umask; let fperm = match fs::metadata(file) { @@ -258,11 +267,13 @@ impl Chmoder { } else if err.kind() == std::io::ErrorKind::PermissionDenied { // These two filenames would normally be conditionally // quoted, but GNU's tests expect them to always be quoted - show_error!("{}: Permission denied", file.quote()); + return Err(USimpleError::new( + 1, + format!("{}: Permission denied", file.quote()), + )); } else { - show_error!("{}: {}", file.quote(), err); + return Err(USimpleError::new(1, format!("{}: {}", file.quote(), err))); } - return Err(1); } }; match self.fmode { @@ -296,22 +307,25 @@ impl Chmoder { } Err(f) => { if !self.quiet { - show_error!("{}", f); + return Err(USimpleError::new(1, f)); + } else { + return Err(ExitCode::new(1)); } - return Err(1); } } } self.change_file(fperm, new_mode, file)?; // if a permission would have been removed if umask was 0, but it wasn't because umask was not 0, print an error and fail if (new_mode & !naively_expected_new_mode) != 0 { - show_error!( - "{}: new permissions are {}, not {}", - file.maybe_quote(), - display_permissions_unix(new_mode as mode_t, false), - display_permissions_unix(naively_expected_new_mode as mode_t, false) - ); - return Err(1); + return Err(USimpleError::new( + 1, + format!( + "{}: new permissions are {}, not {}", + file.maybe_quote(), + display_permissions_unix(new_mode as mode_t, false), + display_permissions_unix(naively_expected_new_mode as mode_t, false) + ), + )); } } } diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 8967a798a..346bf4c8e 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -1,6 +1,7 @@ // This file is part of the uutils coreutils package. // // (c) Jordi Boggiano +// (c) Thomas Queiroz // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. @@ -23,7 +24,7 @@ use std::io::{self, Write}; use std::iter::Iterator; use std::process::Command; use uucore::display::Quotable; -use uucore::error::{UResult, USimpleError}; +use uucore::error::{UResult, USimpleError, UUsageError}; const USAGE: &str = "env [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]"; const AFTER_HELP: &str = "\ @@ -50,7 +51,7 @@ fn print_env(null: bool) { } } -fn parse_name_value_opt<'a>(opts: &mut Options<'a>, opt: &'a str) -> Result { +fn parse_name_value_opt<'a>(opts: &mut Options<'a>, opt: &'a str) -> UResult { // is it a NAME=VALUE like opt ? if let Some(idx) = opt.find('=') { // yes, so push name, value pair @@ -64,17 +65,12 @@ fn parse_name_value_opt<'a>(opts: &mut Options<'a>, opt: &'a str) -> Result(opts: &mut Options<'a>, opt: &'a str) -> Result<(), i32> { +fn parse_program_opt<'a>(opts: &mut Options<'a>, opt: &'a str) -> UResult<()> { if opts.null { - eprintln!( - "{}: cannot specify --null (-0) with command", - uucore::util_name() - ); - eprintln!( - "Type \"{} --help\" for detailed information", - uucore::execution_phrase() - ); - Err(1) + Err(UUsageError::new( + 125, + "cannot specify --null (-0) with command".to_string(), + )) } else { opts.program.push(opt); Ok(()) @@ -93,10 +89,8 @@ fn load_config_file(opts: &mut Options) -> UResult<()> { Ini::load_from_file(file) }; - let conf = conf.map_err(|error| { - show_error!("{}: {}", file.maybe_quote(), error); - 1 - })?; + let conf = + conf.map_err(|e| USimpleError::new(1, format!("{}: {}", file.maybe_quote(), e)))?; for (_, prop) in &conf { // ignore all INI section lines (treat them as comments) @@ -138,7 +132,7 @@ pub fn uu_app() -> App<'static, 'static> { .long("ignore-environment") .help("start with an empty environment")) .arg(Arg::with_name("chdir") - .short("c") + .short("C") // GNU env compatibility .long("chdir") .takes_value(true) .number_of_values(1) @@ -236,6 +230,14 @@ fn run_env(args: impl uucore::Args) -> UResult<()> { } } + // GNU env tests this behavior + if opts.program.is_empty() && running_directory.is_some() { + return Err(UUsageError::new( + 125, + "must specify command with --chdir (-C)".to_string(), + )); + } + // NOTE: we manually set and unset the env vars below rather than using Command::env() to more // easily handle the case where no command is given @@ -251,6 +253,13 @@ fn run_env(args: impl uucore::Args) -> UResult<()> { // unset specified env vars for name in &opts.unsets { + if name.is_empty() || name.contains(0 as char) || name.contains('=') { + return Err(USimpleError::new( + 125, + format!("cannot unset {}: Invalid argument", name.quote()), + )); + } + env::remove_var(name); } diff --git a/src/uu/factor/BENCHMARKING.md b/src/uu/factor/BENCHMARKING.md index 6bf9cbf90..0f9afaff0 100644 --- a/src/uu/factor/BENCHMARKING.md +++ b/src/uu/factor/BENCHMARKING.md @@ -44,7 +44,8 @@ on Daniel Lemire's [*Microbenchmarking calls for idealized conditions*][lemire], which I recommend reading if you want to add benchmarks to `factor`. 1. Select a small, self-contained, deterministic component - `gcd` and `table::factor` are good example of such: + (`gcd` and `table::factor` are good examples): + - no I/O or access to external data structures ; - no call into other components ; - behavior is deterministic: no RNG, no concurrency, ... ; @@ -53,16 +54,19 @@ which I recommend reading if you want to add benchmarks to `factor`. maximizing the numbers of samples we can take in a given time. 2. Benchmarks are immutable (once merged in `uutils`) + Modifying a benchmark means previously-collected values cannot meaningfully be compared, silently giving nonsensical results. If you must modify an existing benchmark, rename it. 3. Test common cases + We are interested in overall performance, rather than specific edge-cases; use **reproducibly-randomized inputs**, sampling from either all possible input values or some subset of interest. 4. Use [`criterion`], `criterion::black_box`, ... + `criterion` isn't perfect, but it is also much better than ad-hoc solutions in each benchmark. diff --git a/src/uu/factor/src/numeric/gcd.rs b/src/uu/factor/src/numeric/gcd.rs index 004ef1515..78197c722 100644 --- a/src/uu/factor/src/numeric/gcd.rs +++ b/src/uu/factor/src/numeric/gcd.rs @@ -52,7 +52,7 @@ pub fn gcd(mut u: u64, mut v: u64) -> u64 { #[cfg(test)] mod tests { use super::*; - use quickcheck::quickcheck; + use quickcheck::{quickcheck, TestResult}; quickcheck! { fn euclidean(a: u64, b: u64) -> bool { @@ -76,13 +76,12 @@ mod tests { gcd(0, a) == a } - fn divisor(a: u64, b: u64) -> () { + fn divisor(a: u64, b: u64) -> TestResult { // Test that gcd(a, b) divides a and b, unless a == b == 0 - if a == 0 && b == 0 { return; } + if a == 0 && b == 0 { return TestResult::discard(); } // restrict test domain to !(a == b == 0) let g = gcd(a, b); - assert_eq!(a % g, 0); - assert_eq!(b % g, 0); + TestResult::from_bool( g != 0 && a % g == 0 && b % g == 0 ) } fn commutative(a: u64, b: u64) -> bool { diff --git a/src/uu/printenv/src/printenv.rs b/src/uu/printenv/src/printenv.rs index 5d32cfbcc..c3f996865 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -9,6 +9,7 @@ use clap::{crate_version, App, Arg}; use std::env; +use uucore::error::UResult; static ABOUT: &str = "Display the values of the specified environment VARIABLE(s), or (with no VARIABLE) display name and value pairs for them all."; @@ -20,7 +21,8 @@ fn usage() -> String { format!("{0} [VARIABLE]... [OPTION]...", uucore::execution_phrase()) } -pub fn uumain(args: impl uucore::Args) -> i32 { +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -40,15 +42,23 @@ pub fn uumain(args: impl uucore::Args) -> i32 { for (env_var, value) in env::vars() { print!("{}={}{}", env_var, value, separator); } - return 0; + return Ok(()); } + let mut not_found = false; for env_var in variables { if let Ok(var) = env::var(env_var) { print!("{}{}", var, separator); + } else { + not_found = true; } } - 0 + + if not_found { + Err(1.into()) + } else { + Ok(()) + } } pub fn uu_app() -> App<'static, 'static> { diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 4dcb18382..202adff27 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -50,6 +50,7 @@ static OPT_PROMPT_MORE: &str = "prompt-more"; static OPT_RECURSIVE: &str = "recursive"; static OPT_RECURSIVE_R: &str = "recursive_R"; static OPT_VERBOSE: &str = "verbose"; +static PRESUME_INPUT_TTY: &str = "presume-input-tty"; static ARG_FILES: &str = "files"; @@ -208,6 +209,17 @@ pub fn uu_app() -> App<'static, 'static> { .long(OPT_VERBOSE) .help("explain what is being done") ) + // From the GNU source code: + // This is solely for testing. + // Do not document. + // It is relatively difficult to ensure that there is a tty on stdin. + // Since rm acts differently depending on that, without this option, + // it'd be harder to test the parts of rm that depend on that setting. + .arg( + Arg::with_name(PRESUME_INPUT_TTY) + .long(PRESUME_INPUT_TTY) + .hidden(true) + ) .arg( Arg::with_name(ARG_FILES) .multiple(true) diff --git a/src/uu/tail/src/parse.rs b/src/uu/tail/src/parse.rs new file mode 100644 index 000000000..929681811 --- /dev/null +++ b/src/uu/tail/src/parse.rs @@ -0,0 +1,161 @@ +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. + +use std::ffi::OsString; + +#[derive(PartialEq, Debug)] +pub enum ParseError { + Syntax, + Overflow, +} +/// Parses obsolete syntax +/// tail -NUM[kmzv] // spell-checker:disable-line +pub fn parse_obsolete(src: &str) -> Option, ParseError>> { + let mut chars = src.char_indices(); + if let Some((_, '-')) = chars.next() { + let mut num_end = 0usize; + let mut has_num = false; + let mut last_char = 0 as char; + for (n, c) in &mut chars { + if c.is_numeric() { + has_num = true; + num_end = n; + } else { + last_char = c; + break; + } + } + if has_num { + match src[1..=num_end].parse::() { + Ok(num) => { + let mut quiet = false; + let mut verbose = false; + let mut zero_terminated = false; + let mut multiplier = None; + let mut c = last_char; + loop { + // not that here, we only match lower case 'k', 'c', and 'm' + match c { + // we want to preserve order + // this also saves us 1 heap allocation + 'q' => { + quiet = true; + verbose = false + } + 'v' => { + verbose = true; + quiet = false + } + 'z' => zero_terminated = true, + 'c' => multiplier = Some(1), + 'b' => multiplier = Some(512), + 'k' => multiplier = Some(1024), + 'm' => multiplier = Some(1024 * 1024), + '\0' => {} + _ => return Some(Err(ParseError::Syntax)), + } + if let Some((_, next)) = chars.next() { + c = next + } else { + break; + } + } + let mut options = Vec::new(); + if quiet { + options.push(OsString::from("-q")) + } + if verbose { + options.push(OsString::from("-v")) + } + if zero_terminated { + options.push(OsString::from("-z")) + } + if let Some(n) = multiplier { + options.push(OsString::from("-c")); + let num = match num.checked_mul(n) { + Some(n) => n, + None => return Some(Err(ParseError::Overflow)), + }; + options.push(OsString::from(format!("{}", num))); + } else { + options.push(OsString::from("-n")); + options.push(OsString::from(format!("{}", num))); + } + Some(Ok(options.into_iter())) + } + Err(_) => Some(Err(ParseError::Overflow)), + } + } else { + None + } + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + fn obsolete(src: &str) -> Option, ParseError>> { + let r = parse_obsolete(src); + match r { + Some(s) => match s { + Ok(v) => Some(Ok(v.map(|s| s.to_str().unwrap().to_owned()).collect())), + Err(e) => Some(Err(e)), + }, + None => None, + } + } + fn obsolete_result(src: &[&str]) -> Option, ParseError>> { + Some(Ok(src.iter().map(|s| s.to_string()).collect())) + } + #[test] + fn test_parse_numbers_obsolete() { + assert_eq!(obsolete("-5"), obsolete_result(&["-n", "5"])); + assert_eq!(obsolete("-100"), obsolete_result(&["-n", "100"])); + assert_eq!(obsolete("-5m"), obsolete_result(&["-c", "5242880"])); + assert_eq!(obsolete("-1k"), obsolete_result(&["-c", "1024"])); + assert_eq!(obsolete("-2b"), obsolete_result(&["-c", "1024"])); + assert_eq!(obsolete("-1mmk"), obsolete_result(&["-c", "1024"])); + assert_eq!(obsolete("-1vz"), obsolete_result(&["-v", "-z", "-n", "1"])); + assert_eq!( + obsolete("-1vzqvq"), // spell-checker:disable-line + obsolete_result(&["-q", "-z", "-n", "1"]) + ); + assert_eq!(obsolete("-1vzc"), obsolete_result(&["-v", "-z", "-c", "1"])); + assert_eq!( + obsolete("-105kzm"), + obsolete_result(&["-z", "-c", "110100480"]) + ); + } + #[test] + fn test_parse_errors_obsolete() { + assert_eq!(obsolete("-5n"), Some(Err(ParseError::Syntax))); + assert_eq!(obsolete("-5c5"), Some(Err(ParseError::Syntax))); + } + #[test] + fn test_parse_obsolete_no_match() { + assert_eq!(obsolete("-k"), None); + assert_eq!(obsolete("asd"), None); + } + #[test] + #[cfg(target_pointer_width = "64")] + fn test_parse_obsolete_overflow_x64() { + assert_eq!( + obsolete("-1000000000000000m"), + Some(Err(ParseError::Overflow)) + ); + assert_eq!( + obsolete("-10000000000000000000000"), + Some(Err(ParseError::Overflow)) + ); + } + #[test] + #[cfg(target_pointer_width = "32")] + fn test_parse_obsolete_overflow_x32() { + assert_eq!(obsolete("-42949672960"), Some(Err(ParseError::Overflow))); + assert_eq!(obsolete("-42949672k"), Some(Err(ParseError::Overflow))); + } +} diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index eaf7bf8bf..d83f02724 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -16,17 +16,21 @@ extern crate clap; extern crate uucore; mod chunks; +mod parse; mod platform; use chunks::ReverseChunks; use clap::{App, Arg}; use std::collections::VecDeque; +use std::ffi::OsString; use std::fmt; use std::fs::{File, Metadata}; use std::io::{stdin, stdout, BufRead, BufReader, Read, Seek, SeekFrom, Write}; use std::path::Path; use std::thread::sleep; use std::time::Duration; +use uucore::display::Quotable; +use uucore::error::{UResult, USimpleError}; use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::ringbuffer::RingBuffer; @@ -58,105 +62,122 @@ pub mod options { pub static ARG_FILES: &str = "files"; } +#[derive(Debug)] enum FilterMode { Bytes(usize), Lines(usize, u8), // (number of lines, delimiter) } +impl Default for FilterMode { + fn default() -> Self { + FilterMode::Lines(10, b'\n') + } +} + +#[derive(Debug, Default)] struct Settings { + quiet: bool, + verbose: bool, mode: FilterMode, sleep_msec: u32, beginning: bool, follow: bool, pid: platform::Pid, + files: Vec, } -impl Default for Settings { - fn default() -> Settings { - Settings { - mode: FilterMode::Lines(10, b'\n'), +impl Settings { + pub fn get_from(args: impl uucore::Args) -> Result { + let matches = uu_app().get_matches_from(arg_iterate(args)?); + + let mut settings: Settings = Settings { sleep_msec: 1000, - beginning: false, - follow: false, - pid: 0, + follow: matches.is_present(options::FOLLOW), + ..Default::default() + }; + + if settings.follow { + if let Some(n) = matches.value_of(options::SLEEP_INT) { + let parsed: Option = n.parse().ok(); + if let Some(m) = parsed { + settings.sleep_msec = m * 1000 + } + } } + + if let Some(pid_str) = matches.value_of(options::PID) { + if let Ok(pid) = pid_str.parse() { + settings.pid = pid; + if pid != 0 { + if !settings.follow { + show_warning!("PID ignored; --pid=PID is useful only when following"); + } + + if !platform::supports_pid_checks(pid) { + show_warning!("--pid=PID is not supported on this system"); + settings.pid = 0; + } + } + } + } + + let mode_and_beginning = if let Some(arg) = matches.value_of(options::BYTES) { + match parse_num(arg) { + Ok((n, beginning)) => (FilterMode::Bytes(n), beginning), + Err(e) => return Err(format!("invalid number of bytes: {}", e)), + } + } else if let Some(arg) = matches.value_of(options::LINES) { + match parse_num(arg) { + Ok((n, beginning)) => (FilterMode::Lines(n, b'\n'), beginning), + Err(e) => return Err(format!("invalid number of lines: {}", e)), + } + } else { + (FilterMode::Lines(10, b'\n'), false) + }; + settings.mode = mode_and_beginning.0; + settings.beginning = mode_and_beginning.1; + + if matches.is_present(options::ZERO_TERM) { + if let FilterMode::Lines(count, _) = settings.mode { + settings.mode = FilterMode::Lines(count, 0); + } + } + + settings.verbose = matches.is_present(options::verbosity::VERBOSE); + settings.quiet = matches.is_present(options::verbosity::QUIET); + + settings.files = match matches.values_of(options::ARG_FILES) { + Some(v) => v.map(|s| s.to_owned()).collect(), + None => vec!["-".to_owned()], + }; + + Ok(settings) } } #[allow(clippy::cognitive_complexity)] -pub fn uumain(args: impl uucore::Args) -> i32 { - let mut settings: Settings = Default::default(); - - let app = uu_app(); - - let matches = app.get_matches_from(args); - - settings.follow = matches.is_present(options::FOLLOW); - if settings.follow { - if let Some(n) = matches.value_of(options::SLEEP_INT) { - let parsed: Option = n.parse().ok(); - if let Some(m) = parsed { - settings.sleep_msec = m * 1000 - } +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = match Settings::get_from(args) { + Ok(o) => o, + Err(s) => { + return Err(USimpleError::new(1, s)); } - } - - if let Some(pid_str) = matches.value_of(options::PID) { - if let Ok(pid) = pid_str.parse() { - settings.pid = pid; - if pid != 0 { - if !settings.follow { - show_warning!("PID ignored; --pid=PID is useful only when following"); - } - - if !platform::supports_pid_checks(pid) { - show_warning!("--pid=PID is not supported on this system"); - settings.pid = 0; - } - } - } - } - - let mode_and_beginning = if let Some(arg) = matches.value_of(options::BYTES) { - match parse_num(arg) { - Ok((n, beginning)) => (FilterMode::Bytes(n), beginning), - Err(e) => crash!(1, "invalid number of bytes: {}", e.to_string()), - } - } else if let Some(arg) = matches.value_of(options::LINES) { - match parse_num(arg) { - Ok((n, beginning)) => (FilterMode::Lines(n, b'\n'), beginning), - Err(e) => crash!(1, "invalid number of lines: {}", e.to_string()), - } - } else { - (FilterMode::Lines(10, b'\n'), false) }; - settings.mode = mode_and_beginning.0; - settings.beginning = mode_and_beginning.1; + uu_tail(&args) +} - if matches.is_present(options::ZERO_TERM) { - if let FilterMode::Lines(count, _) = settings.mode { - settings.mode = FilterMode::Lines(count, 0); - } - } - - let verbose = matches.is_present(options::verbosity::VERBOSE); - let quiet = matches.is_present(options::verbosity::QUIET); - - let files: Vec = matches - .values_of(options::ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_else(|| vec![String::from("-")]); - - let multiple = files.len() > 1; +fn uu_tail(settings: &Settings) -> UResult<()> { + let multiple = settings.files.len() > 1; let mut first_header = true; let mut readers: Vec<(Box, &String)> = Vec::new(); #[cfg(unix)] let stdin_string = String::from("standard input"); - for filename in &files { + for filename in &settings.files { let use_stdin = filename.as_str() == "-"; - if (multiple || verbose) && !quiet { + if (multiple || settings.verbose) && !settings.quiet { if !first_header { println!(); } @@ -170,7 +191,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { if use_stdin { let mut reader = BufReader::new(stdin()); - unbounded_tail(&mut reader, &settings); + unbounded_tail(&mut reader, settings); // Don't follow stdin since there are no checks for pipes/FIFOs // @@ -202,14 +223,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let mut file = File::open(&path).unwrap(); let md = file.metadata().unwrap(); if is_seekable(&mut file) && get_block_size(&md) > 0 { - bounded_tail(&mut file, &settings); + bounded_tail(&mut file, settings); if settings.follow { let reader = BufReader::new(file); readers.push((Box::new(reader), filename)); } } else { let mut reader = BufReader::new(file); - unbounded_tail(&mut reader, &settings); + unbounded_tail(&mut reader, settings); if settings.follow { readers.push((Box::new(reader), filename)); } @@ -218,10 +239,36 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } if settings.follow { - follow(&mut readers[..], &settings); + follow(&mut readers[..], settings); } - 0 + Ok(()) +} + +fn arg_iterate<'a>( + mut args: impl uucore::Args + 'a, +) -> Result + 'a>, String> { + // argv[0] is always present + let first = args.next().unwrap(); + if let Some(second) = args.next() { + if let Some(s) = second.to_str() { + match parse::parse_obsolete(s) { + Some(Ok(iter)) => Ok(Box::new(vec![first].into_iter().chain(iter).chain(args))), + Some(Err(e)) => match e { + parse::ParseError::Syntax => Err(format!("bad argument format: {}", s.quote())), + parse::ParseError::Overflow => Err(format!( + "invalid argument: {} Value too large for defined datatype", + s.quote() + )), + }, + None => Ok(Box::new(vec![first, second].into_iter().chain(args))), + } + } else { + Err("bad argument encoding".to_owned()) + } + } else { + Ok(Box::new(vec![first].into_iter())) + } } pub fn uu_app() -> App<'static, 'static> { diff --git a/src/uu/who/src/who.rs b/src/uu/who/src/who.rs index a975c82ba..14f39536d 100644 --- a/src/uu/who/src/who.rs +++ b/src/uu/who/src/who.rs @@ -7,8 +7,8 @@ // spell-checker:ignore (ToDO) ttyname hostnames runlevel mesg wtmp statted boottime deadprocs initspawn clockchange curr runlvline pidstr exitstr hoststr -#[macro_use] -extern crate uucore; +use uucore::display::Quotable; +use uucore::error::{FromIo, UResult}; use uucore::libc::{ttyname, STDIN_FILENO, S_IWGRP}; use uucore::utmpx::{self, time, Utmpx}; @@ -59,7 +59,8 @@ fn get_long_usage() -> String { ) } -pub fn uumain(args: impl uucore::Args) -> i32 { +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = args .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); @@ -157,9 +158,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { args: files, }; - who.exec(); - - 0 + who.exec() } pub fn uu_app() -> App<'static, 'static> { @@ -326,7 +325,7 @@ fn current_tty() -> String { } impl Who { - fn exec(&mut self) { + fn exec(&mut self) -> UResult<()> { let run_level_chk = |_record: i16| { #[cfg(not(target_os = "linux"))] return false; @@ -362,7 +361,7 @@ impl Who { for ut in records { if !self.my_line_only || cur_tty == ut.tty_device() { if self.need_users && ut.is_user_process() { - self.print_user(&ut); + self.print_user(&ut)?; } else if self.need_runlevel && run_level_chk(ut.record_type()) { if cfg!(target_os = "linux") { self.print_runlevel(&ut); @@ -383,6 +382,7 @@ impl Who { if ut.record_type() == utmpx::BOOT_TIME {} } } + Ok(()) } #[inline] @@ -464,7 +464,7 @@ impl Who { self.print_line("", ' ', "system boot", &time_string(ut), "", "", "", ""); } - fn print_user(&self, ut: &Utmpx) { + fn print_user(&self, ut: &Utmpx) -> UResult<()> { let mut p = PathBuf::from("/dev"); p.push(ut.tty_device().as_str()); let mesg; @@ -491,7 +491,13 @@ impl Who { }; let s = if self.do_lookup { - crash_if_err!(1, ut.canon_host()) + ut.canon_host().map_err_context(|| { + let host = ut.host(); + format!( + "failed to canonicalize {}", + host.split(':').next().unwrap_or(&host).quote() + ) + })? } else { ut.host() }; @@ -507,6 +513,8 @@ impl Who { hoststr.as_str(), "", ); + + Ok(()) } #[allow(clippy::too_many_arguments)] diff --git a/src/uucore/src/lib/features/utmpx.rs b/src/uucore/src/lib/features/utmpx.rs index a96c3f48c..a3078b818 100644 --- a/src/uucore/src/lib/features/utmpx.rs +++ b/src/uucore/src/lib/features/utmpx.rs @@ -236,17 +236,20 @@ impl Utmpx { flags: AI_CANONNAME, ..AddrInfoHints::default() }; - let sockets = getaddrinfo(Some(hostname), None, Some(hints)) - .unwrap() - .collect::>>()?; - for socket in sockets { - if let Some(ai_canonname) = socket.canonname { - return Ok(if display.is_empty() { - ai_canonname - } else { - format!("{}:{}", ai_canonname, display) - }); + if let Ok(sockets) = getaddrinfo(Some(hostname), None, Some(hints)) { + let sockets = sockets.collect::>>()?; + for socket in sockets { + if let Some(ai_canonname) = socket.canonname { + return Ok(if display.is_empty() { + ai_canonname + } else { + format!("{}:{}", ai_canonname, display) + }); + } } + } else { + // GNU coreutils has this behavior + return Ok(hostname.to_string()); } } diff --git a/src/uucore/src/lib/macros.rs b/src/uucore/src/lib/macros.rs index c7b15dba3..275b0afe7 100644 --- a/src/uucore/src/lib/macros.rs +++ b/src/uucore/src/lib/macros.rs @@ -1,3 +1,37 @@ +//! Macros for the uucore utilities. +//! +//! This module bundles all macros used across the uucore utilities. These +//! include macros for reporting errors in various formats, aborting program +//! execution and more. +//! +//! To make use of all macros in this module, they must be imported like so: +//! +//! ```ignore +//! #[macro_use] +//! extern crate uucore; +//! ``` +//! +//! Alternatively, you can import single macros by importing them through their +//! fully qualified name like this: +//! +//! ```no_run +//! use uucore::{show, crash}; +//! ``` +//! +//! Here's an overview of the macros sorted by purpose +//! +//! - Print errors +//! - From types implementing [`crate::error::UError`]: [`show!`], +//! [`show_if_err!`] +//! - From custom messages: [`show_error!`], [`show_usage_error!`] +//! - Print warnings: [`show_warning!`] +//! - Terminate util execution +//! - Terminate regularly: [`exit!`], [`return_if_err!`] +//! - Crash program: [`crash!`], [`crash_if_err!`], [`safe_unwrap!`] +//! - Unwrapping result types: [`safe_unwrap!`] + +// spell-checker:ignore sourcepath targetpath + use std::sync::atomic::AtomicBool; // This file is part of the uutils coreutils package. @@ -12,6 +46,45 @@ pub static UTILITY_IS_SECOND_ARG: AtomicBool = AtomicBool::new(false); //==== +/// Display a [`crate::error::UError`] and set global exit code. +/// +/// Prints the error message contained in an [`crate::error::UError`] to stderr +/// and sets the exit code through [`crate::error::set_exit_code`]. The printed +/// error message is prepended with the calling utility's name. A call to this +/// macro will not finish program execution. +/// +/// # Examples +/// +/// The following example would print a message "Some error occurred" and set +/// the utility's exit code to 2. +/// +/// ``` +/// # #[macro_use] +/// # extern crate uucore; +/// +/// use uucore::error::{self, USimpleError}; +/// +/// fn main() { +/// let err = USimpleError::new(2, "Some error occurred."); +/// show!(err); +/// assert_eq!(error::get_exit_code(), 2); +/// } +/// ``` +/// +/// If not using [`crate::error::UError`], one may achieve the same behavior +/// like this: +/// +/// ``` +/// # #[macro_use] +/// # extern crate uucore; +/// +/// use uucore::error::set_exit_code; +/// +/// fn main() { +/// set_exit_code(2); +/// show_error!("Some error occurred."); +/// } +/// ``` #[macro_export] macro_rules! show( ($err:expr) => ({ @@ -21,6 +94,36 @@ macro_rules! show( }) ); +/// Display an error and set global exit code in error case. +/// +/// Wraps around [`show!`] and takes a [`crate::error::UResult`] instead of a +/// [`crate::error::UError`] type. This macro invokes [`show!`] if the +/// [`crate::error::UResult`] is an `Err`-variant. This can be invoked directly +/// on the result of a function call, like in the `install` utility: +/// +/// ```ignore +/// show_if_err!(copy(sourcepath, &targetpath, b)); +/// ``` +/// +/// # Examples +/// +/// ```ignore +/// # #[macro_use] +/// # extern crate uucore; +/// # use uucore::error::{UError, UIoError, UResult, USimpleError}; +/// +/// # fn main() { +/// let is_ok = Ok(1); +/// // This does nothing at all +/// show_if_err!(is_ok); +/// +/// let is_err = Err(USimpleError::new(1, "I'm an error").into()); +/// // Calls `show!` on the contained USimpleError +/// show_if_err!(is_err); +/// # } +/// ``` +/// +/// #[macro_export] macro_rules! show_if_err( ($res:expr) => ({ @@ -31,6 +134,19 @@ macro_rules! show_if_err( ); /// Show an error to stderr in a similar style to GNU coreutils. +/// +/// Takes a [`format!`]-like input and prints it to stderr. The output is +/// prepended with the current utility's name. +/// +/// # Examples +/// +/// ``` +/// # #[macro_use] +/// # extern crate uucore; +/// # fn main() { +/// show_error!("Couldn't apply {} to {}", "foo", "bar"); +/// # } +/// ``` #[macro_export] macro_rules! show_error( ($($args:tt)+) => ({ @@ -40,6 +156,17 @@ macro_rules! show_error( ); /// Show a warning to stderr in a similar style to GNU coreutils. +/// +/// Is this really required? Used in the following locations: +/// +/// ./src/uu/head/src/head.rs:12 +/// ./src/uu/head/src/head.rs:424 +/// ./src/uu/head/src/head.rs:427 +/// ./src/uu/head/src/head.rs:430 +/// ./src/uu/head/src/head.rs:453 +/// ./src/uu/du/src/du.rs:339 +/// ./src/uu/wc/src/wc.rs:270 +/// ./src/uu/wc/src/wc.rs:273 #[macro_export] macro_rules! show_error_custom_description ( ($err:expr,$($args:tt)+) => ({ @@ -48,6 +175,21 @@ macro_rules! show_error_custom_description ( }) ); +/// Print a warning message to stderr. +/// +/// Takes [`format!`]-compatible input and prepends it with the current +/// utility's name and "warning: " before printing to stderr. +/// +/// # Examples +/// +/// ``` +/// # #[macro_use] +/// # extern crate uucore; +/// # fn main() { +/// // outputs : warning: Couldn't apply foo to bar +/// show_warning!("Couldn't apply {} to {}", "foo", "bar"); +/// # } +/// ``` #[macro_export] macro_rules! show_warning( ($($args:tt)+) => ({ @@ -57,6 +199,21 @@ macro_rules! show_warning( ); /// Show a bad invocation help message in a similar style to GNU coreutils. +/// +/// Takes a [`format!`]-compatible input and prepends it with the current +/// utility's name before printing to stderr. +/// +/// # Examples +/// +/// ``` +/// # #[macro_use] +/// # extern crate uucore; +/// # fn main() { +/// // outputs : Couldn't apply foo to bar +/// // Try ' --help' for more information. +/// show_usage_error!("Couldn't apply {} to {}", "foo", "bar"); +/// # } +/// ``` #[macro_export] macro_rules! show_usage_error( ($($args:tt)+) => ({ @@ -68,7 +225,9 @@ macro_rules! show_usage_error( //==== -/// Calls `exit()` with the provided exit code. +/// Calls [`std::process::exit`] with the provided exit code. +/// +/// Why not call exit directly? #[macro_export] macro_rules! exit( ($exit_code:expr) => ({ @@ -76,7 +235,22 @@ macro_rules! exit( }) ); -/// Display the provided error message, then `exit()` with the provided exit code +/// Display an error and [`exit!`] +/// +/// Displays the provided error message using [`show_error!`], then invokes +/// [`exit!`] with the provided exit code. +/// +/// # Examples +/// +/// ```should_panic +/// # #[macro_use] +/// # extern crate uucore; +/// # fn main() { +/// // outputs : Couldn't apply foo to bar +/// // and terminates execution +/// crash!(1, "Couldn't apply {} to {}", "foo", "bar"); +/// # } +/// ``` #[macro_export] macro_rules! crash( ($exit_code:expr, $($args:tt)+) => ({ @@ -85,8 +259,26 @@ macro_rules! crash( }) ); -/// Unwraps the Result. Instead of panicking, it exists the program with the -/// provided exit code. +/// Unwrap a [`std::result::Result`], crashing instead of panicking. +/// +/// If the result is an `Ok`-variant, returns the value contained inside. If it +/// is an `Err`-variant, invokes [`crash!`] with the formatted error instead. +/// +/// # Examples +/// +/// ```should_panic +/// # #[macro_use] +/// # extern crate uucore; +/// # fn main() { +/// let is_ok: Result = Ok(1); +/// // Does nothing +/// crash_if_err!(1, is_ok); +/// +/// let is_err: Result = Err("This didn't work..."); +/// // Calls `crash!` +/// crash_if_err!(1, is_err); +/// # } +/// ``` #[macro_export] macro_rules! crash_if_err( ($exit_code:expr, $exp:expr) => ( @@ -97,18 +289,42 @@ macro_rules! crash_if_err( ) ); -//==== - +/// Unwrap some Result, crashing instead of panicking. +/// +/// Drop this in favor of `crash_if_err!` #[macro_export] -macro_rules! safe_write( - ($fd:expr, $($args:tt)+) => ( - match write!($fd, $($args)+) { - Ok(_) => {} - Err(f) => panic!("{}", f) +macro_rules! safe_unwrap( + ($exp:expr) => ( + match $exp { + Ok(m) => m, + Err(f) => $crate::crash!(1, "{}", f.to_string()) } ) ); +//==== + +/// Unwraps the Result. Instead of panicking, it shows the error and then +/// returns from the function with the provided exit code. +/// Assumes the current function returns an i32 value. +/// +/// Replace with `crash_if_err`? +#[macro_export] +macro_rules! return_if_err( + ($exit_code:expr, $exp:expr) => ( + match $exp { + Ok(m) => m, + Err(f) => { + $crate::show_error!("{}", f); + return $exit_code; + } + } + ) +); + +//==== + +/// This is used exclusively by du... #[macro_export] macro_rules! safe_writeln( ($fd:expr, $($args:tt)+) => ( @@ -123,6 +339,7 @@ macro_rules! safe_writeln( //-- message templates : (join utility sub-macros) +// used only by "cut" #[macro_export] macro_rules! snippet_list_join_oxford_comma { ($conjunction:expr, $valOne:expr, $valTwo:expr) => ( @@ -133,6 +350,7 @@ macro_rules! snippet_list_join_oxford_comma { ); } +// used only by "cut" #[macro_export] macro_rules! snippet_list_join { ($conjunction:expr, $valOne:expr, $valTwo:expr) => ( @@ -167,6 +385,7 @@ macro_rules! msg_invalid_opt_use { }; } +// Only used by "cut" #[macro_export] macro_rules! msg_opt_only_usable_if { ($clause:expr, $flag:expr) => { @@ -181,6 +400,7 @@ macro_rules! msg_opt_only_usable_if { }; } +// Used only by "cut" #[macro_export] macro_rules! msg_opt_invalid_should_be { ($expects:expr, $received:expr, $flag:expr) => { @@ -200,6 +420,7 @@ macro_rules! msg_opt_invalid_should_be { // -- message templates : invalid input : input combinations +// UNUSED! #[macro_export] macro_rules! msg_expects_one_of { ($valOne:expr $(, $remaining_values:expr)*) => ( @@ -207,6 +428,7 @@ macro_rules! msg_expects_one_of { ); } +// Used only by "cut" #[macro_export] macro_rules! msg_expects_no_more_than_one_of { ($valOne:expr $(, $remaining_values:expr)*) => ( diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index c8348d491..1b0b7131b 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -541,7 +541,7 @@ fn test_no_operands() { .arg("777") .fails() .code_is(1) - .stderr_is("chmod: missing operand"); + .usage_error("missing operand"); } #[test] diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index ecb215f8c..135ee72ef 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -1,7 +1,4 @@ -// spell-checker:ignore (words) bamf chdir - -#[cfg(not(windows))] -use std::fs; +// spell-checker:ignore (words) bamf chdir rlimit prlimit COMSPEC use crate::common::util::*; use std::env; @@ -80,6 +77,20 @@ fn test_combined_file_set_unset() { ); } +#[test] +fn test_unset_invalid_variables() { + use uucore::display::Quotable; + + // Cannot test input with \0 in it, since output will also contain \0. rlimit::prlimit fails + // with this error: Error { kind: InvalidInput, message: "nul byte found in provided data" } + for var in &["", "a=b"] { + new_ucmd!().arg("-u").arg(var).run().stderr_only(format!( + "env: cannot unset {}: Invalid argument", + var.quote() + )); + } +} + #[test] fn test_single_name_value_pair() { let out = new_ucmd!().arg("FOO=bar").run(); @@ -163,7 +174,7 @@ fn test_fail_null_with_program() { fn test_change_directory() { let scene = TestScenario::new(util_name!()); let temporary_directory = tempdir().unwrap(); - let temporary_path = fs::canonicalize(temporary_directory.path()).unwrap(); + let temporary_path = std::fs::canonicalize(temporary_directory.path()).unwrap(); assert_ne!(env::current_dir().unwrap(), temporary_path); // command to print out current working directory @@ -179,27 +190,36 @@ fn test_change_directory() { assert_eq!(out.trim(), temporary_path.as_os_str()) } -// no way to consistently get "current working directory", `cd` doesn't work @ CI -// instead, we test that the unique temporary directory appears somewhere in the printed variables #[cfg(windows)] #[test] fn test_change_directory() { let scene = TestScenario::new(util_name!()); let temporary_directory = tempdir().unwrap(); - let temporary_path = temporary_directory.path(); - assert_ne!(env::current_dir().unwrap(), temporary_path); + let temporary_path = temporary_directory.path(); + let temporary_path = temporary_path + .strip_prefix(r"\\?\") + .unwrap_or(temporary_path); + + let env_cd = env::current_dir().unwrap(); + let env_cd = env_cd.strip_prefix(r"\\?\").unwrap_or(&env_cd); + + assert_ne!(env_cd, temporary_path); + + // COMSPEC is a variable that contains the full path to cmd.exe + let cmd_path = env::var("COMSPEC").unwrap(); + + // command to print out current working directory + let pwd = [&*cmd_path, "/C", "cd"]; let out = scene .ucmd() .arg("--chdir") .arg(&temporary_path) + .args(&pwd) .succeeds() .stdout_move_str(); - - assert!(!out - .lines() - .any(|line| line.ends_with(temporary_path.file_name().unwrap().to_str().unwrap()))); + assert_eq!(out.trim(), temporary_path.as_os_str()) } #[test] diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index 092a5f00d..740c30bdd 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -314,3 +314,39 @@ fn test_rm_verbose_slash() { assert!(!at.dir_exists(dir)); assert!(!at.file_exists(file_a)); } + +#[test] +fn test_rm_silently_accepts_presume_input_tty1() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_1 = "test_rm_silently_accepts_presume_input_tty1"; + + at.touch(file_1); + + ucmd.arg("--presume-input-tty").arg(file_1).succeeds(); + + assert!(!at.file_exists(file_1)); +} + +#[test] +fn test_rm_silently_accepts_presume_input_tty2() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_2 = "test_rm_silently_accepts_presume_input_tty2"; + + at.touch(file_2); + + ucmd.arg("---presume-input-tty").arg(file_2).succeeds(); + + assert!(!at.file_exists(file_2)); +} + +#[test] +fn test_rm_silently_accepts_presume_input_tty3() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_3 = "test_rm_silently_accepts_presume_input_tty3"; + + at.touch(file_3); + + ucmd.arg("----presume-input-tty").arg(file_3).succeeds(); + + assert!(!at.file_exists(file_3)); +} diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 26d8106f0..a020f6235 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -358,6 +358,36 @@ fn test_positive_lines() { .stdout_is("c\nd\ne\n"); } +/// Test for reading all but the first NUM lines: `tail -3`. +#[test] +fn test_obsolete_syntax_positive_lines() { + new_ucmd!() + .args(&["-3"]) + .pipe_in("a\nb\nc\nd\ne\n") + .succeeds() + .stdout_is("c\nd\ne\n"); +} + +/// Test for reading all but the first NUM lines: `tail -n -10`. +#[test] +fn test_small_file() { + new_ucmd!() + .args(&["-n -10"]) + .pipe_in("a\nb\nc\nd\ne\n") + .succeeds() + .stdout_is("a\nb\nc\nd\ne\n"); +} + +/// Test for reading all but the first NUM lines: `tail -10`. +#[test] +fn test_obsolete_syntax_small_file() { + new_ucmd!() + .args(&["-10"]) + .pipe_in("a\nb\nc\nd\ne\n") + .succeeds() + .stdout_is("a\nb\nc\nd\ne\n"); +} + /// Test for reading all lines, specified by `tail -n +0`. #[test] fn test_positive_zero_lines() {