From 12886c0aede788c429b7e921eab4264cacf597bc Mon Sep 17 00:00:00 2001 From: Andrew Liebenow Date: Wed, 16 Oct 2024 12:53:16 -0500 Subject: [PATCH 1/3] head: add -c option Also: add a few simple optimizations --- Cargo.lock | 80 ++++++++++++++++++ plib/src/testing.rs | 4 +- text/Cargo.toml | 4 + text/head.rs | 168 ++++++++++++++++++++++++++++-------- text/tests/head/mod.rs | 187 ++++++++++++++++++++++++++++++++++++++--- 5 files changed, 395 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8be7d3464..b89f8b4a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -174,6 +174,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -1085,6 +1100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1444,6 +1460,8 @@ dependencies = [ "libc", "notify-debouncer-full", "plib", + "proptest", + "rand", "regex", "topological-sort", "walkdir", @@ -1523,6 +1541,32 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.6.0", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.8.4", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.37" @@ -1562,6 +1606,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.10.0" @@ -1677,6 +1730,18 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "rustyline" version = "14.0.0" @@ -2078,6 +2143,12 @@ dependencies = [ "libc", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -2124,6 +2195,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/plib/src/testing.rs b/plib/src/testing.rs index 973544235..e4a5e788b 100644 --- a/plib/src/testing.rs +++ b/plib/src/testing.rs @@ -30,7 +30,7 @@ pub struct TestPlanU8 { pub expected_exit_code: i32, } -fn run_test_base(cmd: &str, args: &Vec, stdin_data: &[u8]) -> Output { +pub fn run_test_base(cmd: &str, args: &Vec, stdin_data: &[u8]) -> Output { let relpath = if cfg!(debug_assertions) { format!("target/debug/{}", cmd) } else { @@ -49,7 +49,7 @@ fn run_test_base(cmd: &str, args: &Vec, stdin_data: &[u8]) -> Output { .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() - .expect(format!("failed to spawn command {}", cmd).as_str()); + .unwrap_or_else(|_| panic!("failed to spawn command {cmd}")); // Separate the mutable borrow of stdin from the child process if let Some(mut stdin) = child.stdin.take() { diff --git a/text/Cargo.toml b/text/Cargo.toml index be4f0e7a4..ff4adef92 100644 --- a/text/Cargo.toml +++ b/text/Cargo.toml @@ -21,6 +21,10 @@ dirs = "5.0" deunicode = "1.6" walkdir = "2" +[dev-dependencies] +proptest = "1.5.0" +rand = "0.8.5" + [lints] workspace = true diff --git a/text/head.rs b/text/head.rs index 88b0b1dbc..80aede0a3 100644 --- a/text/head.rs +++ b/text/head.rs @@ -10,70 +10,130 @@ use clap::Parser; use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory}; use plib::PROJECT_NAME; -use std::io::{self, Read, Write}; +use std::error::Error; +use std::io::{self, Read, StdoutLock, Write}; use std::path::PathBuf; -/// head - copy the first part of files +const N_C_GROUP: &str = "N_C_GROUP"; + +/// head - copy the first part of files. +/// If neither -n nor -c are specified, copies the first 10 lines of each file (-n 10). #[derive(Parser)] #[command(version, about)] struct Args { - /// The first lines of each input file shall be copied to standard output. - #[arg(short, default_value_t = 10, value_parser = clap::value_parser!(u64).range(1..))] - n: u64, + /// The first lines of each input file shall be copied to standard output (mutually exclusive with -c) + #[arg(long = "lines", short, value_parser = clap::value_parser!(usize), group = N_C_GROUP)] + n: Option, + + // Note: -c was added to POSIX in POSIX.1-2024, but has been supported on most platforms since the late 1990s + // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/head.html + // + /// The first bytes of each input file shall be copied to standard output (mutually exclusive with -n) + #[arg(long = "bytes", short, value_parser = clap::value_parser!(usize), group = N_C_GROUP)] + c: Option, /// Files to read as input. files: Vec, } -fn head_file(args: &Args, pathname: &PathBuf, first: bool, want_header: bool) -> io::Result<()> { +enum CountType { + Bytes(usize), + Lines(usize), +} + +fn head_file( + count_type: &CountType, + pathname: &PathBuf, + first: bool, + want_header: bool, + stdout_lock: &mut StdoutLock, +) -> Result<(), Box> { + const BUFFER_SIZE: usize = plib::BUFSZ; + // print file header if want_header { if first { - println!("==> {} <==\n", pathname.display()); + writeln!(stdout_lock, "==> {} <==", pathname.display())?; } else { - println!("\n==> {} <==\n", pathname.display()); + writeln!(stdout_lock, "\n==> {} <==", pathname.display())?; } } // open file, or stdin let mut file = plib::io::input_stream(pathname, false)?; - let mut raw_buffer = [0; plib::BUFSZ]; - let mut nl = 0; + let mut raw_buffer = [0_u8; BUFFER_SIZE]; - loop { - // read a chunk of file data - let n_read = file.read(&mut raw_buffer[..])?; - if n_read == 0 { - break; - } + match *count_type { + CountType::Bytes(c) => { + let mut bytes_remaining = c; - // slice of buffer containing file data - let buf = &raw_buffer[0..n_read]; - let mut pos = 0; + loop { + let number_of_bytes_read = { + // Do not read more bytes than necessary + let read_up_to_n_bytes = BUFFER_SIZE.min(bytes_remaining); - // count newlines - for chv in buf { - // LF character encountered - if *chv == 10 { - nl += 1; - } + file.read(&mut raw_buffer[..read_up_to_n_bytes])? + }; + + if number_of_bytes_read == 0_usize { + // Reached EOF + break; + } - pos += 1; + let bytes_to_write = &raw_buffer[..number_of_bytes_read]; - // if user-specified limit reached, stop - if nl >= args.n { - break; + stdout_lock.write_all(bytes_to_write)?; + + bytes_remaining -= number_of_bytes_read; + + if bytes_remaining == 0_usize { + break; + } } } + CountType::Lines(n) => { + let mut nl = 0_usize; + + loop { + // read a chunk of file data + let n_read = file.read(&mut raw_buffer)?; + + if n_read == 0_usize { + // Reached EOF + break; + } + + // slice of buffer containing file data + let buf = &raw_buffer[..n_read]; + + let mut position = 0_usize; + + // count newlines + for &byte in buf { + position += 1; + + // LF character encountered + if byte == b'\n' { + nl += 1; - // output full or partial buffer - let final_buf = &raw_buffer[0..pos]; - io::stdout().write_all(final_buf)?; + // if user-specified limit reached, stop + if nl >= n { + break; + } + } + } - // if user-specified limit reached, stop - if nl >= args.n { - break; + // output full or partial buffer + let bytes_to_write = &raw_buffer[..position]; + + stdout_lock.write_all(bytes_to_write)?; + + // if user-specified limit reached, stop + if nl >= n { + break; + } + } } } @@ -84,6 +144,40 @@ fn main() -> Result<(), Box> { // parse command line arguments let mut args = Args::parse(); + // bsdutils (FreeBSD) enforces n > 0 (and c > 0) + // BusyBox, coreutils' uutils, GNU Core Utilities, and toybox do not (and just print nothing) + // POSIX says: + // "The application shall ensure that the number option-argument is a positive decimal integer." + let count_type = match (args.n, args.c) { + (None, None) => { + // If no arguments are provided, the default is 10 lines + CountType::Lines(10_usize) + } + (Some(n), None) => { + if n == 0_usize { + eprintln!("head: when a value for -n is provided, it must be greater than 0"); + + std::process::exit(1_i32); + } + + CountType::Lines(n) + } + (None, Some(c)) => { + if c == 0_usize { + eprintln!("head: when a value for -c is provided, it must be greater than 0"); + + std::process::exit(1_i32); + } + + CountType::Bytes(c) + } + + (Some(_), Some(_)) => { + // Will be caught by clap + unreachable!(); + } + }; + setlocale(LocaleCategory::LcAll, ""); textdomain(PROJECT_NAME)?; bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; @@ -97,8 +191,10 @@ fn main() -> Result<(), Box> { let want_header = args.files.len() > 1; let mut first = true; + let mut stdout_lock = io::stdout().lock(); + for filename in &args.files { - if let Err(e) = head_file(&args, filename, first, want_header) { + if let Err(e) = head_file(&count_type, filename, first, want_header, &mut stdout_lock) { exit_code = 1; eprintln!("{}: {}", filename.display(), e); } diff --git a/text/tests/head/mod.rs b/text/tests/head/mod.rs index f70378b63..f8968d565 100644 --- a/text/tests/head/mod.rs +++ b/text/tests/head/mod.rs @@ -9,27 +9,194 @@ // use plib::{run_test, TestPlan}; +use rand::{seq::SliceRandom, thread_rng}; -fn head_test(test_data: &str, expected_output: &str) { - run_test(TestPlan { - cmd: String::from("head"), - args: Vec::new(), - stdin_data: String::from(test_data), - expected_out: String::from(expected_output), - expected_err: String::from(""), - expected_exit_code: 0, - }); +fn generate_valid_arguments(n: Option<&str>, c: Option<&str>) -> Vec> { + let mut argument_forms = Vec::>::new(); + + let mut args_outer = Vec::<(String, String)>::new(); + + for n_form in ["-n", "--lines"] { + args_outer.clear(); + + if let Some(n_str) = n { + args_outer.push((n_form.to_owned(), n_str.to_owned())); + }; + + for c_form in ["-c", "--bytes"] { + let mut args_inner = args_outer.clone(); + + if let Some(c_str) = c { + args_inner.push((c_form.to_owned(), c_str.to_owned())); + }; + + argument_forms.push(args_inner); + } + } + + argument_forms.shuffle(&mut thread_rng()); + + let mut flattened = Vec::>::with_capacity(argument_forms.len()); + + for ve in argument_forms { + let mut vec = Vec::::new(); + + for (st, str) in ve { + vec.push(st); + vec.push(str); + } + + flattened.push(vec); + } + + flattened +} + +/* #region Normal tests */ +fn head_test(n: Option<&str>, c: Option<&str>, test_data: &str, expected_output: &str) { + for ve in generate_valid_arguments(n, c) { + run_test(TestPlan { + cmd: "head".to_owned(), + args: ve, + stdin_data: test_data.to_owned(), + expected_out: expected_output.to_owned(), + expected_err: String::new(), + expected_exit_code: 0_i32, + }); + } } #[test] fn test_head_basic() { - head_test("a\nb\nc\nd\n", "a\nb\nc\nd\n"); + head_test(None, None, "a\nb\nc\nd\n", "a\nb\nc\nd\n"); + head_test( + None, + None, "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", ); + head_test( + None, + None, "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\na\n", "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", ); } + +#[test] +fn test_head_explicit_n() { + head_test( + Some("5"), + None, + "\ +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +a +", + "\ +1 +2 +3 +4 +5 +", + ); +} + +#[test] +fn test_head_c() { + head_test(None, Some("3"), "123456789", "123"); +} +/* #endregion */ + +/* #region Property-based tests */ +mod property_tests { + use plib::run_test_base; + use proptest::{prelude::TestCaseError, prop_assert, test_runner::TestRunner}; + + fn get_test_runner(cases: u32) -> TestRunner { + TestRunner::new(proptest::test_runner::Config { + cases, + failure_persistence: None, + + ..proptest::test_runner::Config::default() + }) + } + + fn run_head_and_verify_output( + input: Vec, + true_if_lines_false_if_bytes: bool, + count: usize, + ) -> Result<(), TestCaseError> { + let n_or_c = if true_if_lines_false_if_bytes { + "-n" + } else { + "-c" + }; + + let output = run_test_base( + "head", + &vec![n_or_c.to_owned(), count.to_string()], + input.as_slice(), + ); + + let stdout = &output.stdout; + + if true_if_lines_false_if_bytes { + let new_lines_in_stdout = stdout.iter().filter(|&&ue| ue == b'\n').count(); + + prop_assert!(new_lines_in_stdout <= count); + + prop_assert!(input.starts_with(stdout)); + } else { + prop_assert!(stdout.len() <= count); + + prop_assert!(stdout.as_slice() == &input[..(input.len().min(count))]); + } + + Ok(()) + } + + #[test] + fn property_test_random() { + get_test_runner(16_u32) + .run( + &( + proptest::bool::ANY, + (0_usize..16_384_usize), + proptest::collection::vec(proptest::num::u8::ANY, 0_usize..65_536_usize), + ), + |(true_if_lines_false_if_bytes, count, input)| { + run_head_and_verify_output(input, true_if_lines_false_if_bytes, count) + }, + ) + .unwrap(); + } + + #[test] + fn property_test_small() { + get_test_runner(128_u32) + .run( + &( + proptest::bool::ANY, + (0_usize..1_024_usize), + proptest::collection::vec(proptest::num::u8::ANY, 0_usize..16_384_usize), + ), + |(true_if_lines_false_if_bytes, count, input)| { + run_head_and_verify_output(input, true_if_lines_false_if_bytes, count) + }, + ) + .unwrap(); + } +} +/* #endregion */ From 4867f172a55cfbead6d7160dc8840514f8a64839 Mon Sep 17 00:00:00 2001 From: Andrew Liebenow Date: Wed, 16 Oct 2024 23:12:44 -0500 Subject: [PATCH 2/3] Fix flaky tests --- text/tests/head/mod.rs | 143 ++++++++++++++++++++++++++++++----------- 1 file changed, 105 insertions(+), 38 deletions(-) diff --git a/text/tests/head/mod.rs b/text/tests/head/mod.rs index f8968d565..a26e6f450 100644 --- a/text/tests/head/mod.rs +++ b/text/tests/head/mod.rs @@ -11,49 +11,49 @@ use plib::{run_test, TestPlan}; use rand::{seq::SliceRandom, thread_rng}; -fn generate_valid_arguments(n: Option<&str>, c: Option<&str>) -> Vec> { - let mut argument_forms = Vec::>::new(); +/* #region Normal tests */ +fn head_test(n: Option<&str>, c: Option<&str>, test_data: &str, expected_output: &str) { + fn generate_valid_arguments(n: Option<&str>, c: Option<&str>) -> Vec> { + let mut argument_forms = Vec::>::new(); - let mut args_outer = Vec::<(String, String)>::new(); + let mut args_outer = Vec::<(String, String)>::new(); - for n_form in ["-n", "--lines"] { - args_outer.clear(); + for n_form in ["-n", "--lines"] { + args_outer.clear(); - if let Some(n_str) = n { - args_outer.push((n_form.to_owned(), n_str.to_owned())); - }; + if let Some(n_str) = n { + args_outer.push((n_form.to_owned(), n_str.to_owned())); + }; - for c_form in ["-c", "--bytes"] { - let mut args_inner = args_outer.clone(); + for c_form in ["-c", "--bytes"] { + let mut args_inner = args_outer.clone(); - if let Some(c_str) = c { - args_inner.push((c_form.to_owned(), c_str.to_owned())); - }; + if let Some(c_str) = c { + args_inner.push((c_form.to_owned(), c_str.to_owned())); + }; - argument_forms.push(args_inner); + argument_forms.push(args_inner); + } } - } - argument_forms.shuffle(&mut thread_rng()); + argument_forms.shuffle(&mut thread_rng()); + + let mut flattened = Vec::>::with_capacity(argument_forms.len()); - let mut flattened = Vec::>::with_capacity(argument_forms.len()); + for ve in argument_forms { + let mut vec = Vec::::new(); - for ve in argument_forms { - let mut vec = Vec::::new(); + for (st, str) in ve { + vec.push(st); + vec.push(str); + } - for (st, str) in ve { - vec.push(st); - vec.push(str); + flattened.push(vec); } - flattened.push(vec); + flattened } - flattened -} - -/* #region Normal tests */ -fn head_test(n: Option<&str>, c: Option<&str>, test_data: &str, expected_output: &str) { for ve in generate_valid_arguments(n, c) { run_test(TestPlan { cmd: "head".to_owned(), @@ -123,6 +123,11 @@ fn test_head_c() { mod property_tests { use plib::run_test_base; use proptest::{prelude::TestCaseError, prop_assert, test_runner::TestRunner}; + use std::{ + sync::mpsc::{self, RecvTimeoutError}, + thread::{self}, + time::Duration, + }; fn get_test_runner(cases: u32) -> TestRunner { TestRunner::new(proptest::test_runner::Config { @@ -134,7 +139,7 @@ mod property_tests { } fn run_head_and_verify_output( - input: Vec, + input: &[u8], true_if_lines_false_if_bytes: bool, count: usize, ) -> Result<(), TestCaseError> { @@ -144,11 +149,7 @@ mod property_tests { "-c" }; - let output = run_test_base( - "head", - &vec![n_or_c.to_owned(), count.to_string()], - input.as_slice(), - ); + let output = run_test_base("head", &vec![n_or_c.to_owned(), count.to_string()], input); let stdout = &output.stdout; @@ -167,8 +168,46 @@ mod property_tests { Ok(()) } + fn run_head_and_verify_output_with_timeout( + true_if_lines_false_if_bytes: bool, + count: usize, + input: Vec, + ) -> Result<(), TestCaseError> { + let (sender, receiver) = mpsc::channel::>(); + + let input_len = input.len(); + + thread::spawn(move || { + sender.send(run_head_and_verify_output( + input.as_slice(), + true_if_lines_false_if_bytes, + count, + )) + }); + + match receiver.recv_timeout(Duration::from_secs(60_u64)) { + Ok(result) => result, + Err(RecvTimeoutError::Timeout) => { + eprint!( + "\ +head property test has been running for more than a minute. The spawned process will have to be killed manually. + +true_if_lines_false_if_bytes: {true_if_lines_false_if_bytes} +count: {count} +input_len: {input_len} +" + ); + + Err(TestCaseError::fail("Spawned process did not terminate")) + } + Err(RecvTimeoutError::Disconnected) => { + unreachable!(); + } + } + } + #[test] - fn property_test_random() { + fn test_head_property_test_small_or_large() { get_test_runner(16_u32) .run( &( @@ -177,14 +216,18 @@ mod property_tests { proptest::collection::vec(proptest::num::u8::ANY, 0_usize..65_536_usize), ), |(true_if_lines_false_if_bytes, count, input)| { - run_head_and_verify_output(input, true_if_lines_false_if_bytes, count) + run_head_and_verify_output_with_timeout( + true_if_lines_false_if_bytes, + count, + input, + ) }, ) .unwrap(); } #[test] - fn property_test_small() { + fn test_head_property_test_small() { get_test_runner(128_u32) .run( &( @@ -193,7 +236,31 @@ mod property_tests { proptest::collection::vec(proptest::num::u8::ANY, 0_usize..16_384_usize), ), |(true_if_lines_false_if_bytes, count, input)| { - run_head_and_verify_output(input, true_if_lines_false_if_bytes, count) + run_head_and_verify_output_with_timeout( + true_if_lines_false_if_bytes, + count, + input, + ) + }, + ) + .unwrap(); + } + + #[test] + fn test_head_property_test_very_small() { + get_test_runner(128_u32) + .run( + &( + proptest::bool::ANY, + (0_usize..512_usize), + proptest::collection::vec(proptest::num::u8::ANY, 0_usize..512_usize), + ), + |(true_if_lines_false_if_bytes, count, input)| { + run_head_and_verify_output_with_timeout( + true_if_lines_false_if_bytes, + count, + input, + ) }, ) .unwrap(); From 1e43436c5567f587632ead005158f0e89f29c543 Mon Sep 17 00:00:00 2001 From: Andrew Liebenow Date: Thu, 17 Oct 2024 20:03:15 -0500 Subject: [PATCH 3/3] Update per code review --- text/Cargo.toml | 4 ++-- text/head.rs | 27 +++++++++++++++------------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/text/Cargo.toml b/text/Cargo.toml index ff4adef92..e8860d4c2 100644 --- a/text/Cargo.toml +++ b/text/Cargo.toml @@ -22,8 +22,8 @@ deunicode = "1.6" walkdir = "2" [dev-dependencies] -proptest = "1.5.0" -rand = "0.8.5" +proptest = "1" +rand = "0.8" [lints] workspace = true diff --git a/text/head.rs b/text/head.rs index 80aede0a3..f78596610 100644 --- a/text/head.rs +++ b/text/head.rs @@ -29,8 +29,8 @@ struct Args { // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/head.html // /// The first bytes of each input file shall be copied to standard output (mutually exclusive with -n) - #[arg(long = "bytes", short, value_parser = clap::value_parser!(usize), group = N_C_GROUP)] - c: Option, + #[arg(long = "bytes", short = 'c', value_parser = clap::value_parser!(usize), group = N_C_GROUP)] + bytes_to_copy: Option, /// Files to read as input. files: Vec, @@ -65,8 +65,8 @@ fn head_file( let mut raw_buffer = [0_u8; BUFFER_SIZE]; match *count_type { - CountType::Bytes(c) => { - let mut bytes_remaining = c; + CountType::Bytes(bytes_to_copy) => { + let mut bytes_remaining = bytes_to_copy; loop { let number_of_bytes_read = { @@ -148,7 +148,7 @@ fn main() -> Result<(), Box> { // BusyBox, coreutils' uutils, GNU Core Utilities, and toybox do not (and just print nothing) // POSIX says: // "The application shall ensure that the number option-argument is a positive decimal integer." - let count_type = match (args.n, args.c) { + let count_type = match (args.n, args.bytes_to_copy) { (None, None) => { // If no arguments are provided, the default is 10 lines CountType::Lines(10_usize) @@ -162,14 +162,14 @@ fn main() -> Result<(), Box> { CountType::Lines(n) } - (None, Some(c)) => { - if c == 0_usize { + (None, Some(bytes_to_copy)) => { + if bytes_to_copy == 0_usize { eprintln!("head: when a value for -c is provided, it must be greater than 0"); std::process::exit(1_i32); } - CountType::Bytes(c) + CountType::Bytes(bytes_to_copy) } (Some(_), Some(_)) => { @@ -182,18 +182,21 @@ fn main() -> Result<(), Box> { textdomain(PROJECT_NAME)?; bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; + let files = &mut args.files; + // if no files, read from stdin - if args.files.is_empty() { - args.files.push(PathBuf::new()); + if files.is_empty() { + files.push(PathBuf::new()); } + let want_header = files.len() > 1; + let mut exit_code = 0; - let want_header = args.files.len() > 1; let mut first = true; let mut stdout_lock = io::stdout().lock(); - for filename in &args.files { + for filename in files { if let Err(e) = head_file(&count_type, filename, first, want_header, &mut stdout_lock) { exit_code = 1; eprintln!("{}: {}", filename.display(), e);