Files
oh-my-pi/crates/pi-builtins/src/head.rs
T
can1357 edc0caeb0f feat(builtins): sweep of GNU/BSD compat fixes + str performance opts
Addresses a broad audit of built-in shell utilities against their real counterparts: timeout gains signal delivery, -s/-k/--preserve-status/--foreground/-v, and GNU exit codes; diff defaults to normal format and gains -w/-b/-B/-i/-c/-x/-L/-s/--strip-trailing-cr and proper -r gating; find fixes -newerXY timestamp comparison direction, anchors -regex to whole paths, and gains BSD -perm +mode, -type lists, -size T/P suffixes, -E/-x/-s flags; date gains BSD -r epoch, -v adjustments, -j -f strptime, and non-greedy -I; tail/head accept obsolete -N/+N at any position with any file count; rg resolves case flags by last occurrence and gains --path-separator and clean -0 output; stat prints integer epochs for %X/%Y/%Z and gains BSD -s/-x/-t; cksum is registered as a builtin; truncate implements -o/--io-blocks and b/= size suffixes; sleep/timeout accept infinity; yes/errno/kill accept hyphen-prefixed operands; nohup -- cmd no longer runs --; which gains BSD -s.
2026-08-20 02:33:10 +02:00

1629 lines
53 KiB
Rust

//! `head` builtin: print the first part of files.
//!
//! Ported from uutils coreutils 0.8.0.
use std::{
ffi::OsString,
fs::File,
io::{self, BufWriter, Read, Seek, SeekFrom, Write},
num::TryFromIntError,
path::PathBuf,
};
use brush_core::{ShellExtensions, builtins::Registration};
use clap::ArgMatches;
use memchr::memrchr_iter;
use thiserror::Error;
use uucore::{display::Quotable, line_ending::LineEnding};
use crate::host::{Host, Utility, matches_parser, util};
const BUF_SIZE: usize = 65536;
mod cli {
//
use std::ffi::OsString;
use clap::{Arg, ArgAction, Command};
use crate::host::format_usage;
pub(super) mod options {
pub const BYTES: &str = "BYTES";
pub const LINES: &str = "LINES";
pub const QUIET: &str = "QUIET";
pub const VERBOSE: &str = "VERBOSE";
pub const ZERO: &str = "ZERO";
pub const FILES: &str = "FILE";
pub const PRESUME_INPUT_PIPE: &str = "-PRESUME-INPUT-PIPE";
}
pub(super) fn uu_app() -> Command {
Command::new("head")
.version("0.8.0")
.about(
"Print the first 10 lines of each FILE to standard output.\nWith more than one FILE, \
precede each with a header giving the file name.\nWith no FILE, or when FILE is -, read \
standard input.",
)
.override_usage(format_usage("head [FLAG]... [FILE]..."))
.infer_long_args(true)
.arg(
Arg::new(options::BYTES)
.short('c')
.long("bytes")
.value_name("[-]NUM")
.help(
"print the first NUM bytes of each file;\nwith a leading '-', print all but the \
last\nNUM bytes of each file",
)
.overrides_with_all([options::BYTES, options::LINES])
.allow_hyphen_values(true),
)
.arg(
Arg::new(options::LINES)
.short('n')
.long("lines")
.value_name("[-]NUM")
.help(
"print the first NUM lines instead of the first 10;\nwith a leading '-', print all \
but the last\nNUM lines of each file",
)
.overrides_with_all([options::LINES, options::BYTES])
.allow_hyphen_values(true),
)
.arg(
Arg::new(options::QUIET)
.short('q')
.long("quiet")
.visible_alias("silent")
.help("never print headers giving file names")
.overrides_with_all([options::VERBOSE, options::QUIET])
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::VERBOSE)
.short('v')
.long("verbose")
.help("always print headers giving file names")
.overrides_with_all([options::QUIET, options::VERBOSE])
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::PRESUME_INPUT_PIPE)
.long("presume-input-pipe")
.alias("-presume-input-pipe")
.hide(true)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::ZERO)
.short('z')
.long("zero-terminated")
.help("line delimiter is NUL, not newline")
.overrides_with(options::ZERO)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::FILES)
.action(ArgAction::Append)
.value_parser(clap::value_parser!(OsString))
.value_hint(clap::ValueHint::FilePath),
)
}
}
use cli::{options, uu_app};
mod parse {
//
use std::ffi::OsString;
use uucore::parser::{
parse_signed_num::{SignPrefix, parse_signed_num_max},
parse_size::ParseSizeError,
};
#[derive(PartialEq, Eq, Debug)]
pub(super) struct ParseError;
/// Parses obsolete syntax
pub(super) fn parse_obsolete(src: &str) -> Option<Result<Vec<OsString>, ParseError>> {
let mut chars = src.char_indices();
if let Some((mut num_start, '-')) = chars.next() {
num_start += 1;
let mut num_end = src.len();
let mut has_num = false;
let mut plus_possible = false;
let mut last_char = 0 as char;
for (n, c) in &mut chars {
if c.is_ascii_digit() {
has_num = true;
plus_possible = false;
} else if c == '+' && plus_possible {
plus_possible = false;
num_start += 1;
} else {
num_end = n;
last_char = c;
break;
}
}
if has_num {
Some(process_num_block(&src[num_start..num_end], last_char, &mut chars))
} else {
None
}
} else {
None
}
}
/// Processes the numeric block of the input string to generate the appropriate
/// options.
fn process_num_block(
src: &str,
last_char: char,
chars: &mut std::str::CharIndices,
) -> Result<Vec<OsString>, ParseError> {
let num = match src.parse::<usize>() {
Ok(n) => n,
Err(e) if *e.kind() == std::num::IntErrorKind::PosOverflow => usize::MAX,
_ => return Err(ParseError),
};
let mut quiet = false;
let mut verbose = false;
let mut zero_terminated = false;
// Lowercase suffixes are byte multipliers (obsolete BSD `-Nc`/`-Nb`/`-Nk`/`-Nm`);
// uppercase suffixes mirror the modern `-n NUM<suffix>` form and scale the
// line count (`head -10K` == `head -n 10240`).
let mut multiplier = None;
let mut line_multiplier: usize = 1;
let mut c = last_char;
loop {
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),
'K' => {
line_multiplier = 1024;
multiplier = None;
},
'M' => {
line_multiplier = 1024 * 1024;
multiplier = None;
},
'G' => {
line_multiplier = 1024 * 1024 * 1024;
multiplier = None;
},
'\0' => {},
_ => return Err(ParseError),
}
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 = num.saturating_mul(n);
options.push(OsString::from(format!("{num}")));
} else {
options.push(OsString::from("-n"));
let num = num.saturating_mul(line_multiplier);
options.push(OsString::from(format!("{num}")));
}
Ok(options)
}
/// Parses an -c or -n argument,
/// the bool specifies whether to read from the end (all but last N)
pub(super) fn parse_num(src: &str) -> Result<(u64, bool), ParseSizeError> {
let result = parse_signed_num_max(src)?;
// head: '-' means "all but last N"
let all_but_last = result.sign == Some(SignPrefix::Minus);
Ok((result.value, all_but_last))
}
#[cfg(test)]
mod tests {
use super::*;
fn obsolete(src: &str) -> Option<Result<Vec<String>, ParseError>> {
let r = parse_obsolete(src);
match r {
Some(s) => match s {
Ok(v) => Some(Ok(v
.into_iter()
.map(|s| s.to_str().unwrap().to_owned())
.collect())),
Err(e) => Some(Err(e)),
},
None => None,
}
}
#[expect(clippy::unnecessary_wraps, reason = "test helper")]
fn obsolete_result(src: &[&str]) -> Option<Result<Vec<String>, ParseError>> {
Some(Ok(src.iter().map(|&s| s.to_string()).collect()))
}
#[test]
#[allow(clippy::cognitive_complexity, reason = "table-style parser coverage")]
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("-10K"), obsolete_result(&["-n", "10240"]));
assert_eq!(obsolete("-1M"), obsolete_result(&["-n", "1048576"]));
assert_eq!(obsolete("-1vz"), obsolete_result(&["-v", "-z", "-n", "1"]));
assert_eq!(
obsolete("-1vzqvq"),
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)));
assert_eq!(obsolete("-5c5"), Some(Err(ParseError)));
}
#[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"), obsolete_result(&["-c", "18446744073709551615"]));
assert_eq!(
obsolete("-10000000000000000000000"),
obsolete_result(&["-n", "18446744073709551615"])
);
}
#[test]
#[cfg(target_pointer_width = "32")]
fn test_parse_obsolete_overflow_x32() {
assert_eq!(obsolete("-42949672960"), obsolete_result(&["-n", "4294967295"]));
assert_eq!(obsolete("-42949672k"), obsolete_result(&["-c", "4294967295"]));
}
}
}
mod take {
//
//! Take all but the last elements of an iterator.
use std::{
collections::VecDeque,
io::{ErrorKind, Read, Write},
};
use memchr::memchr_iter;
const BUF_SIZE: usize = 65536;
struct TakeAllBuffer {
buffer: Vec<u8>,
start_index: usize,
}
impl TakeAllBuffer {
fn new() -> Self {
Self { buffer: vec![], start_index: 0 }
}
fn fill_buffer(&mut self, reader: &mut impl Read) -> std::io::Result<usize> {
self.buffer.resize(BUF_SIZE, 0);
self.start_index = 0;
loop {
match reader.read(&mut self.buffer[..]) {
Ok(n) => {
self.buffer.truncate(n);
return Ok(n);
},
Err(e) if e.kind() == ErrorKind::Interrupted => (),
Err(e) => return Err(e),
}
}
}
fn write_bytes_exact(&mut self, writer: &mut impl Write, bytes: usize) -> std::io::Result<()> {
let buffer_to_write = &self.remaining_buffer()[..bytes];
writer.write_all(buffer_to_write)?;
self.start_index += bytes;
assert!(self.start_index <= self.buffer.len());
Ok(())
}
fn write_all(&mut self, writer: &mut impl Write) -> std::io::Result<usize> {
let remaining_bytes = self.remaining_bytes();
self.write_bytes_exact(writer, remaining_bytes)?;
Ok(remaining_bytes)
}
fn write_bytes_limit(
&mut self,
writer: &mut impl Write,
max_bytes: usize,
) -> std::io::Result<usize> {
let bytes_to_write = self.remaining_bytes().min(max_bytes);
self.write_bytes_exact(writer, bytes_to_write)?;
Ok(bytes_to_write)
}
fn remaining_buffer(&self) -> &[u8] {
&self.buffer[self.start_index..]
}
fn remaining_bytes(&self) -> usize {
self.remaining_buffer().len()
}
fn is_empty(&self) -> bool {
assert!(self.start_index <= self.buffer.len());
self.start_index == self.buffer.len()
}
}
/// Function to copy all but `n` bytes from the reader to the writer.
///
/// If `n` exceeds the number of bytes in the input file then nothing is copied.
/// If no errors are encountered then the function returns the number of bytes
/// copied.
///
/// Algorithm for this function is as follows...
/// 1 - Chunks of the input file are read into a queue of [`TakeAllBuffer`]
/// instances. Chunks are read until at least we have enough data to write
/// out the entire contents of the first [`TakeAllBuffer`] in the queue
/// whilst still retaining at least `n` bytes in the queue. If we hit `EoF`
/// at any point, stop reading. 2 - Assess whether we managed to queue up
/// greater-than `n` bytes. If not, we must be done, in which case break and
/// return. 3 - Write either the full first buffer of data, or just enough bytes
/// to get back down to having the required `n` bytes of data queued.
/// 4 - Go back to (1).
pub(super) fn copy_all_but_n_bytes(
reader: &mut impl Read,
writer: &mut impl Write,
n: usize,
) -> std::io::Result<usize> {
let mut buffers: VecDeque<TakeAllBuffer> = VecDeque::new();
let mut empty_buffer_pool: Vec<TakeAllBuffer> = vec![];
let mut buffered_bytes: usize = 0;
let mut total_bytes_copied = 0;
loop {
loop {
// Try to buffer at least enough to write the entire first buffer.
let front_buffer = buffers.front();
if let Some(front_buffer) = front_buffer
&& buffered_bytes >= n + front_buffer.remaining_bytes()
{
break;
}
let mut new_buffer = empty_buffer_pool.pop().unwrap_or_else(TakeAllBuffer::new);
let filled_bytes = new_buffer.fill_buffer(reader)?;
if filled_bytes == 0 {
// filled_bytes==0 => Eof
break;
}
buffers.push_back(new_buffer);
buffered_bytes += filled_bytes;
}
// If we've got <=n bytes buffered here we have nothing left to do.
if buffered_bytes <= n {
break;
}
let excess_buffered_bytes = buffered_bytes - n;
// Since we have some data buffered, can assume we have >=1 buffer - i.e. safe
// to unwrap.
let front_buffer = buffers.front_mut().unwrap();
let bytes_written = front_buffer.write_bytes_limit(writer, excess_buffered_bytes)?;
buffered_bytes -= bytes_written;
total_bytes_copied += bytes_written;
// If the front buffer is empty (which it probably is), push it into the
// empty-buffer-pool.
if front_buffer.is_empty() {
empty_buffer_pool.push(buffers.pop_front().unwrap());
}
}
Ok(total_bytes_copied)
}
struct TakeAllLinesBuffer {
inner: TakeAllBuffer,
terminated_lines: usize,
partial_line: bool,
}
struct BytesAndLines {
bytes: usize,
terminated_lines: usize,
}
impl TakeAllLinesBuffer {
fn new() -> Self {
Self { inner: TakeAllBuffer::new(), terminated_lines: 0, partial_line: false }
}
fn fill_buffer(
&mut self,
reader: &mut impl Read,
separator: u8,
) -> std::io::Result<BytesAndLines> {
self.partial_line = false;
let bytes_read = self.inner.fill_buffer(reader)?;
// Count the number of lines...
self.terminated_lines = memchr_iter(separator, self.inner.remaining_buffer()).count();
if let Some(last_char) = self.inner.remaining_buffer().last()
&& *last_char != separator
{
self.partial_line = true;
}
Ok(BytesAndLines { bytes: bytes_read, terminated_lines: self.terminated_lines })
}
fn write_lines(
&mut self,
writer: &mut impl Write,
max_lines: usize,
separator: u8,
) -> std::io::Result<BytesAndLines> {
assert!(max_lines > 0, "Must request at least 1 line.");
let ret;
if max_lines > self.terminated_lines {
ret = BytesAndLines {
bytes: self.inner.write_all(writer)?,
terminated_lines: self.terminated_lines,
};
self.terminated_lines = 0;
} else {
let index = memchr_iter(separator, self.inner.remaining_buffer()).nth(max_lines - 1);
assert!(
index.is_some(),
"Somehow we're being asked to write more lines than we have, that's a bug in \
copy_all_but_lines."
);
let index = index.unwrap();
// index is the offset of the separator character, zero indexed. Need to add 1
// to get the number of bytes to write.
let bytes_to_write = index + 1;
self.inner.write_bytes_exact(writer, bytes_to_write)?;
ret = BytesAndLines { bytes: bytes_to_write, terminated_lines: max_lines };
self.terminated_lines -= max_lines;
}
Ok(ret)
}
fn is_empty(&self) -> bool {
self.inner.is_empty()
}
fn terminated_lines(&self) -> usize {
self.terminated_lines
}
fn partial_line(&self) -> bool {
self.partial_line
}
}
/// Function to copy all but `n` lines from the reader to the writer.
///
/// Lines are inferred from the `separator` value passed in by the client.
/// If `n` exceeds the number of lines in the input file then nothing is copied.
/// The last line in the file is not required to end with a `separator`
/// character. If no errors are encountered then they function returns the
/// number of bytes copied.
///
/// Algorithm for this function is as follows...
/// 1 - Chunks of the input file are read into a queue of [`TakeAllLinesBuffer`]
/// instances. Chunks are read until at least we have enough lines that we
/// can write out the entire contents of the first [`TakeAllLinesBuffer`] in
/// the queue whilst still retaining at least `n` lines in the queue.
/// If we hit `EoF` at any point, stop reading.
/// 2 - Asses whether we managed to queue up greater-than `n` lines. If not, we
/// must be done, in which case break and return.
/// 3 - Write either the full first buffer of data, or just enough lines to get
/// back down to having the required `n` lines of data queued.
/// 4 - Go back to (1).
///
/// Note that lines will regularly straddle multiple [`TakeAllLinesBuffer`]
/// instances. The `partial_line` flag on [`TakeAllLinesBuffer`] tracks this,
/// and we use that to ensure that we write out enough lines in the case that
/// the input file doesn't end with a `separator` character.
pub(super) fn copy_all_but_n_lines<R: Read, W: Write>(
mut reader: R,
writer: &mut W,
n: usize,
separator: u8,
) -> std::io::Result<usize> {
// This function requires `n` > 0. Assert it!
assert!(n > 0);
let mut buffers: VecDeque<TakeAllLinesBuffer> = VecDeque::new();
let mut buffered_terminated_lines: usize = 0;
let mut empty_buffers = vec![];
let mut total_bytes_copied = 0;
loop {
// Try to buffer enough such that we can write out the entire first buffer.
loop {
// First check if we have enough lines buffered that we can write out the entire
// front buffer. If so, break.
let front_buffer = buffers.front();
if let Some(front_buffer) = front_buffer
&& buffered_terminated_lines > n + front_buffer.terminated_lines()
{
break;
}
// Else we need to try to buffer more data...
let mut new_buffer = empty_buffers.pop().unwrap_or_else(TakeAllLinesBuffer::new);
let fill_result = new_buffer.fill_buffer(&mut reader, separator)?;
if fill_result.bytes == 0 {
// fill_result.bytes == 0 => EoF.
break;
}
buffered_terminated_lines += fill_result.terminated_lines;
buffers.push_back(new_buffer);
}
// If we've not buffered more lines than we need to hold back we must be done.
if buffered_terminated_lines < n
|| (buffered_terminated_lines == n && !buffers.back().unwrap().partial_line())
{
break;
}
let excess_buffered_terminated_lines = buffered_terminated_lines - n;
// Since we have some data buffered can assume we have at least 1 buffer, so
// safe to unwrap.
let lines_to_write = if buffers.back().unwrap().partial_line() {
excess_buffered_terminated_lines + 1
} else {
excess_buffered_terminated_lines
};
let front_buffer = buffers.front_mut().unwrap();
let write_result = front_buffer.write_lines(writer, lines_to_write, separator)?;
buffered_terminated_lines -= write_result.terminated_lines;
total_bytes_copied += write_result.bytes;
// If the front buffer is empty (which it probably is), push it into the
// empty-buffer-pool.
if front_buffer.is_empty() {
empty_buffers.push(buffers.pop_front().unwrap());
}
}
Ok(total_bytes_copied)
}
/// Like `std::io::Take`, but for lines instead of bytes.
///
/// This struct is generally created by calling [`take_lines`] on a
/// reader. Please see the documentation of [`take_lines`] for more
/// details.
pub(super) struct TakeLines<T> {
inner: T,
limit: u64,
separator: u8,
}
impl<T: Read> Read for TakeLines<T> {
/// Read bytes from a buffer up to the requested number of lines.
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.limit == 0 {
return Ok(0);
}
match self.inner.read(buf) {
Ok(0) => Ok(0),
Ok(n) => {
for i in memchr_iter(self.separator, &buf[..n]) {
self.limit -= 1;
if self.limit == 0 {
return Ok(i + 1);
}
}
Ok(n)
},
Err(e) => Err(e),
}
}
}
/// Create an adaptor that will read at most `limit` lines from a given reader.
///
/// This function returns a new instance of `Read` that will read at
/// most `limit` lines, after which it will always return EOF
/// (`Ok(0)`).
///
/// The `separator` defines the character to interpret as the line
/// ending. For the usual notion of "line", set this to `b'\n'`.
pub(super) fn take_lines<R>(reader: R, limit: u64, separator: u8) -> TakeLines<R> {
TakeLines { inner: reader, limit, separator }
}
#[cfg(test)]
mod tests {
use std::io::{BufRead, BufReader};
use super::{
TakeAllBuffer, TakeAllLinesBuffer, copy_all_but_n_bytes, copy_all_but_n_lines, take_lines,
};
#[test]
fn test_take_all_buffer_exact_bytes() {
let input_buffer = "abc";
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut take_all_buffer = TakeAllBuffer::new();
let bytes_read = take_all_buffer.fill_buffer(&mut input_reader).unwrap();
assert_eq!(bytes_read, input_buffer.len());
assert_eq!(take_all_buffer.remaining_bytes(), input_buffer.len());
assert_eq!(take_all_buffer.remaining_buffer(), input_buffer.as_bytes());
assert!(!take_all_buffer.is_empty());
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
for (index, c) in input_buffer.bytes().enumerate() {
take_all_buffer
.write_bytes_exact(&mut output_reader, 1)
.unwrap();
let buf_ref = output_reader.get_ref();
assert_eq!(buf_ref.len(), index + 1);
assert_eq!(buf_ref[index], c);
assert_eq!(take_all_buffer.remaining_bytes(), input_buffer.len() - (index + 1));
assert_eq!(take_all_buffer.remaining_buffer(), &input_buffer.as_bytes()[index + 1..]);
}
assert!(take_all_buffer.is_empty());
assert_eq!(take_all_buffer.remaining_bytes(), 0);
assert_eq!(take_all_buffer.remaining_buffer(), "".as_bytes());
}
#[test]
fn test_take_all_buffer_all_bytes() {
let input_buffer = "abc";
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut take_all_buffer = TakeAllBuffer::new();
let bytes_read = take_all_buffer.fill_buffer(&mut input_reader).unwrap();
assert_eq!(bytes_read, input_buffer.len());
assert_eq!(take_all_buffer.remaining_bytes(), input_buffer.len());
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let bytes_written = take_all_buffer.write_all(&mut output_reader).unwrap();
assert_eq!(bytes_written, input_buffer.len());
assert_eq!(output_reader.get_ref().as_slice(), input_buffer.as_bytes());
assert!(take_all_buffer.is_empty());
assert_eq!(take_all_buffer.remaining_bytes(), 0);
assert_eq!(take_all_buffer.remaining_buffer(), "".as_bytes());
// Now do a write_all on an empty TakeAllBuffer. Confirm correct behavior.
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let bytes_written = take_all_buffer.write_all(&mut output_reader).unwrap();
assert_eq!(bytes_written, 0);
assert_eq!(output_reader.get_ref().as_slice().len(), 0);
}
#[test]
fn test_take_all_buffer_limit_bytes() {
let input_buffer = "abc";
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut take_all_buffer = TakeAllBuffer::new();
let bytes_read = take_all_buffer.fill_buffer(&mut input_reader).unwrap();
assert_eq!(bytes_read, input_buffer.len());
assert_eq!(take_all_buffer.remaining_bytes(), input_buffer.len());
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
// Write all but 1 bytes.
let bytes_to_write = input_buffer.len() - 1;
let bytes_written = take_all_buffer
.write_bytes_limit(&mut output_reader, bytes_to_write)
.unwrap();
assert_eq!(bytes_written, bytes_to_write);
assert_eq!(output_reader.get_ref().as_slice(), &input_buffer.as_bytes()[..bytes_to_write]);
assert!(!take_all_buffer.is_empty());
assert_eq!(take_all_buffer.remaining_bytes(), 1);
assert_eq!(take_all_buffer.remaining_buffer(), &input_buffer.as_bytes()[bytes_to_write..]);
// Write 1 more byte - i.e. last byte in buffer.
let bytes_to_write = 1;
let bytes_written = take_all_buffer
.write_bytes_limit(&mut output_reader, bytes_to_write)
.unwrap();
assert_eq!(bytes_written, bytes_to_write);
assert_eq!(output_reader.get_ref().as_slice(), input_buffer.as_bytes());
assert!(take_all_buffer.is_empty());
assert_eq!(take_all_buffer.remaining_bytes(), 0);
assert_eq!(take_all_buffer.remaining_buffer(), "".as_bytes());
// Write 1 more byte - i.e. confirm behavior on already empty buffer.
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let bytes_to_write = 1;
let bytes_written = take_all_buffer
.write_bytes_limit(&mut output_reader, bytes_to_write)
.unwrap();
assert_eq!(bytes_written, 0);
assert_eq!(output_reader.get_ref().as_slice().len(), 0);
assert!(take_all_buffer.is_empty());
assert_eq!(take_all_buffer.remaining_bytes(), 0);
assert_eq!(take_all_buffer.remaining_buffer(), "".as_bytes());
}
#[test]
#[allow(clippy::cognitive_complexity, reason = "exercises every buffer transition")]
fn test_take_all_lines_buffer() {
// 3 lines with new-lines and one partial line.
let input_buffer = "a\nb\nc\ndef";
let separator = b'\n';
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut take_all_lines_buffer = TakeAllLinesBuffer::new();
let fill_result = take_all_lines_buffer
.fill_buffer(&mut input_reader, separator)
.unwrap();
assert_eq!(fill_result.bytes, input_buffer.len());
assert_eq!(fill_result.terminated_lines, 3);
assert_eq!(take_all_lines_buffer.terminated_lines(), 3);
assert!(!take_all_lines_buffer.is_empty());
assert!(take_all_lines_buffer.partial_line());
// Write 1st line.
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let lines_to_write = 1;
let write_result = take_all_lines_buffer
.write_lines(&mut output_reader, lines_to_write, separator)
.unwrap();
assert_eq!(write_result.bytes, 2);
assert_eq!(write_result.terminated_lines, lines_to_write);
assert_eq!(output_reader.get_ref().as_slice(), "a\n".as_bytes());
assert!(!take_all_lines_buffer.is_empty());
assert_eq!(take_all_lines_buffer.terminated_lines(), 2);
// Write 2nd line.
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let lines_to_write = 1;
let write_result = take_all_lines_buffer
.write_lines(&mut output_reader, lines_to_write, separator)
.unwrap();
assert_eq!(write_result.bytes, 2);
assert_eq!(write_result.terminated_lines, lines_to_write);
assert_eq!(output_reader.get_ref().as_slice(), "b\n".as_bytes());
assert!(!take_all_lines_buffer.is_empty());
assert_eq!(take_all_lines_buffer.terminated_lines(), 1);
// Now try to write 3 lines even though we have only 1 line remaining. Should
// write everything left in the buffer.
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let lines_to_write = 3;
let write_result = take_all_lines_buffer
.write_lines(&mut output_reader, lines_to_write, separator)
.unwrap();
assert_eq!(write_result.bytes, 5);
assert_eq!(write_result.terminated_lines, 1);
assert_eq!(output_reader.get_ref().as_slice(), "c\ndef".as_bytes());
assert!(take_all_lines_buffer.is_empty());
assert_eq!(take_all_lines_buffer.terminated_lines(), 0);
// Test empty buffer.
let input_buffer = "";
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut take_all_lines_buffer = TakeAllLinesBuffer::new();
let fill_result = take_all_lines_buffer
.fill_buffer(&mut input_reader, separator)
.unwrap();
assert_eq!(fill_result.bytes, 0);
assert_eq!(fill_result.terminated_lines, 0);
assert_eq!(take_all_lines_buffer.terminated_lines(), 0);
assert!(take_all_lines_buffer.is_empty());
assert!(!take_all_lines_buffer.partial_line());
// Test buffer that ends with newline.
let input_buffer = "\n";
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut take_all_lines_buffer = TakeAllLinesBuffer::new();
let fill_result = take_all_lines_buffer
.fill_buffer(&mut input_reader, separator)
.unwrap();
assert_eq!(fill_result.bytes, 1);
assert_eq!(fill_result.terminated_lines, 1);
assert_eq!(take_all_lines_buffer.terminated_lines(), 1);
assert!(!take_all_lines_buffer.is_empty());
assert!(!take_all_lines_buffer.partial_line());
}
#[test]
fn test_copy_all_but_n_bytes() {
// Test the copy_all_but_bytes fn. Test several scenarios...
// 1 - Hold back more bytes than the input will provide. Should have nothing
// written to output.
let input_buffer = "a\nb\nc\ndef";
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let bytes_copied =
copy_all_but_n_bytes(&mut input_reader, &mut output_reader, input_buffer.len() + 1)
.unwrap();
assert_eq!(bytes_copied, 0);
// 2 - Hold back exactly the number of bytes the input will provide. Should have
// nothing written to output.
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let bytes_copied =
copy_all_but_n_bytes(&mut input_reader, &mut output_reader, input_buffer.len()).unwrap();
assert_eq!(bytes_copied, 0);
// 3 - Hold back 1 fewer byte than input will provide. Should have one byte
// written to output.
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let bytes_copied =
copy_all_but_n_bytes(&mut input_reader, &mut output_reader, input_buffer.len() - 1)
.unwrap();
assert_eq!(bytes_copied, 1);
assert_eq!(output_reader.get_ref()[..], input_buffer.as_bytes()[0..1]);
}
#[test]
fn test_copy_all_but_n_lines() {
// Test the copy_all_but_lines fn. Test several scenarios...
// 1 - Hold back more lines than the input will provide. Should have nothing
// written to output.
let input_buffer = "a\nb\nc\ndef";
let separator = b'\n';
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let bytes_copied =
copy_all_but_n_lines(&mut input_reader, &mut output_reader, 5, separator).unwrap();
assert_eq!(bytes_copied, 0);
// 2 - Hold back exactly the number of lines the input will provide. Should have
// nothing written to output.
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let bytes_copied =
copy_all_but_n_lines(&mut input_reader, &mut output_reader, 4, separator).unwrap();
assert_eq!(bytes_copied, 0);
// 3 - Hold back 1 fewer lines than input will provide. Should have one line
// written to output.
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let bytes_copied =
copy_all_but_n_lines(&mut input_reader, &mut output_reader, 3, separator).unwrap();
assert_eq!(bytes_copied, 2);
assert_eq!(output_reader.get_ref()[..], input_buffer.as_bytes()[0..2]);
// Now test again with an input that has a new-line ending...
// 4 - Hold back more lines than the input will provide. Should have nothing
// written to output.
let input_buffer = "a\nb\nc\ndef\n";
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let bytes_copied =
copy_all_but_n_lines(&mut input_reader, &mut output_reader, 5, separator).unwrap();
assert_eq!(bytes_copied, 0);
// 5 - Hold back exactly the number of lines the input will provide. Should have
// nothing written to output.
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let bytes_copied =
copy_all_but_n_lines(&mut input_reader, &mut output_reader, 4, separator).unwrap();
assert_eq!(bytes_copied, 0);
// 6 - Hold back 1 fewer lines than input will provide. Should have one line
// written to output.
let mut input_reader = std::io::Cursor::new(input_buffer);
let mut output_reader = std::io::Cursor::new(vec![0x10; 0]);
let bytes_copied =
copy_all_but_n_lines(&mut input_reader, &mut output_reader, 3, separator).unwrap();
assert_eq!(bytes_copied, 2);
assert_eq!(output_reader.get_ref()[..], input_buffer.as_bytes()[0..2]);
}
#[test]
fn test_zero_lines() {
let input_reader = std::io::Cursor::new("a\nb\nc\n");
let output_reader = BufReader::new(take_lines(input_reader, 0, b'\n'));
let mut iter = output_reader.lines().map(|l| l.unwrap());
assert_eq!(None, iter.next());
}
#[test]
fn test_fewer_lines() {
let input_reader = std::io::Cursor::new("a\nb\nc\n");
let output_reader = BufReader::new(take_lines(input_reader, 2, b'\n'));
let mut iter = output_reader.lines().map(|l| l.unwrap());
assert_eq!(Some(String::from("a")), iter.next());
assert_eq!(Some(String::from("b")), iter.next());
assert_eq!(None, iter.next());
}
#[test]
fn test_more_lines() {
let input_reader = std::io::Cursor::new("a\nb\nc\n");
let output_reader = BufReader::new(take_lines(input_reader, 4, b'\n'));
let mut iter = output_reader.lines().map(|l| l.unwrap());
assert_eq!(Some(String::from("a")), iter.next());
assert_eq!(Some(String::from("b")), iter.next());
assert_eq!(Some(String::from("c")), iter.next());
assert_eq!(None, iter.next());
}
}
}
use take::{copy_all_but_n_bytes, copy_all_but_n_lines, take_lines};
#[derive(Error, Debug)]
enum HeadError {
/// Wrapper around `io::Error`
#[error("error reading {}: {}", name.quote(), err)]
Io { name: PathBuf, err: io::Error },
#[error("{0}")]
ParseError(String),
#[error("number of -bytes or -lines is too large")]
NumTooLarge(#[from] TryFromIntError),
#[error("{0}")]
MatchOption(String),
}
type HeadResult<T> = Result<T, HeadError>;
#[derive(Debug, PartialEq)]
enum Mode {
FirstLines(u64),
AllButLastLines(u64),
FirstBytes(u64),
AllButLastBytes(u64),
}
impl Default for Mode {
fn default() -> Self {
Self::FirstLines(10)
}
}
impl Mode {
fn from(matches: &ArgMatches) -> Result<Self, String> {
if let Some(v) = matches.get_one::<String>(options::BYTES) {
let (n, all_but_last) =
parse::parse_num(v).map_err(|err| format!("invalid number of bytes: {err}"))?;
if all_but_last {
Ok(Self::AllButLastBytes(n))
} else {
Ok(Self::FirstBytes(n))
}
} else if let Some(v) = matches.get_one::<String>(options::LINES) {
let (n, all_but_last) =
parse::parse_num(v).map_err(|err| format!("invalid number of lines: {err}"))?;
if all_but_last {
Ok(Self::AllButLastLines(n))
} else {
Ok(Self::FirstLines(n))
}
} else {
Ok(Self::default())
}
}
}
/// True when `token` is an option that takes its value from the *next* argv
/// token, so that value must never be mistaken for an obsolete `-NUM` form
/// (e.g. the `-5` in `head -n -5 file`).
fn consumes_separate_value(token: &str) -> bool {
if let Some(long) = token.strip_prefix("--") {
if long.is_empty() || long.contains('=') {
return false;
}
// clap infers unambiguous long-option prefixes.
return ["lines", "bytes"].iter().any(|name| name.starts_with(long));
}
let Some(cluster) = token.strip_prefix('-') else {
return false;
};
let mut chars = cluster.chars();
while let Some(c) = chars.next() {
match c {
// Value-taking shorts: a trailing `-n`/`-c` consumes the next
// token; anything after them in the cluster is an attached value.
'n' | 'c' => return chars.next().is_none(),
'q' | 'v' | 'z' => {},
_ => return false,
}
}
false
}
/// Rewrites every obsolete `-NUM[suffix]` token (before `--`) into modern
/// options, wherever it appears among flags and operands: GNU/BSD accept
/// `head -q -5 file`, `head file -5`, and `head -5 -20 file`.
fn arg_iterate(argv: Vec<OsString>) -> HeadResult<Vec<OsString>> {
let mut rewritten = Vec::with_capacity(argv.len() + 1);
let mut iter = argv.into_iter();
// argv[0] is always present
rewritten.extend(iter.next());
let mut skip_value = false;
let mut seen_ddash = false;
for arg in iter {
if skip_value || seen_ddash {
skip_value = false;
rewritten.push(arg);
continue;
}
let Some(token) = arg.to_str() else {
// Non-UTF-8 can't be an obsolete option like "-5"; treat it as a
// regular file argument.
rewritten.push(arg);
continue;
};
if token == "--" {
seen_ddash = true;
rewritten.push(arg);
continue;
}
if matches!(token.as_bytes(), [b'-', b'0'..=b'9', ..]) {
match parse::parse_obsolete(token) {
Some(Ok(options)) => {
rewritten.extend(options);
continue;
},
Some(Err(parse::ParseError)) => {
return Err(HeadError::ParseError(format!(
"bad argument format: {}",
token.quote()
)));
},
None => {},
}
}
if token.len() > 1 && token.starts_with('-') {
skip_value = consumes_separate_value(token);
}
rewritten.push(arg);
}
Ok(rewritten)
}
#[derive(Debug, PartialEq, Default)]
struct HeadOptions {
pub quiet: bool,
pub verbose: bool,
pub line_ending: LineEnding,
pub presume_input_pipe: bool,
pub mode: Mode,
pub files: Vec<OsString>,
}
impl HeadOptions {
///Construct options from matches
pub fn get_from(matches: &ArgMatches) -> Result<Self, String> {
let mut options = Self::default();
options.quiet = matches.get_flag(options::QUIET);
options.verbose = matches.get_flag(options::VERBOSE);
options.line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO));
options.presume_input_pipe = matches.get_flag(options::PRESUME_INPUT_PIPE);
options.mode = Mode::from(matches)?;
options.files = match matches.get_many::<OsString>(options::FILES) {
Some(v) => v.cloned().collect(),
None => vec![OsString::from("-")],
};
Ok(options)
}
}
#[inline]
fn wrap_in_stdout_error(err: io::Error) -> io::Error {
io::Error::new(err.kind(), format!("error writing 'standard output': {err}"))
}
fn read_n_bytes(input: impl Read, output: &mut impl Write, n: u64) -> io::Result<u64> {
let mut reader = input.take(n);
let bytes_written = io::copy(&mut reader, output).map_err(wrap_in_stdout_error)?;
output.flush().map_err(wrap_in_stdout_error)?;
Ok(bytes_written)
}
fn read_n_lines(
input: &mut impl io::BufRead,
output: &mut impl Write,
n: u64,
separator: u8,
) -> io::Result<u64> {
let mut reader = take_lines(input, n, separator);
let mut writer = BufWriter::with_capacity(BUF_SIZE, output);
let bytes_written = io::copy(&mut reader, &mut writer).map_err(wrap_in_stdout_error)?;
writer.flush().map_err(wrap_in_stdout_error)?;
Ok(bytes_written)
}
fn catch_too_large_numbers_in_backwards_bytes_or_lines(n: u64) -> Option<usize> {
usize::try_from(n).ok()
}
fn read_but_last_n_bytes(mut input: impl Read, output: &mut impl Write, n: u64) -> io::Result<u64> {
let mut bytes_written: u64 = 0;
if let Some(n) = catch_too_large_numbers_in_backwards_bytes_or_lines(n) {
bytes_written = copy_all_but_n_bytes(&mut input, output, n)
.map_err(wrap_in_stdout_error)?
.try_into()
.unwrap();
// Make sure we finish writing everything to the target before
// exiting. Otherwise, when Rust is implicitly flushing, any
// error will be silently ignored.
output.flush().map_err(wrap_in_stdout_error)?;
}
Ok(bytes_written)
}
fn read_but_last_n_lines(mut input: impl Read, output: &mut impl Write, n: u64, separator: u8) -> io::Result<u64> {
if n == 0 {
return io::copy(&mut input, output).map_err(wrap_in_stdout_error);
}
let mut bytes_written: u64 = 0;
if let Some(n) = catch_too_large_numbers_in_backwards_bytes_or_lines(n) {
bytes_written = copy_all_but_n_lines(input, output, n, separator)
.map_err(wrap_in_stdout_error)?
.try_into()
.unwrap();
// Make sure we finish writing everything to the target before
// exiting. Otherwise, when Rust is implicitly flushing, any
// error will be silently ignored.
output.flush().map_err(wrap_in_stdout_error)?;
}
Ok(bytes_written)
}
/// Return the index in `input` just after the `n`th line from the end.
///
/// If `n` exceeds the number of lines in this file, then return 0.
/// This function rewinds the cursor to the
/// beginning of the input just before returning unless there is an
/// I/O error.
///
/// # Errors
///
/// This function returns an error if there is a problem seeking
/// through or reading the input.
///
/// # Examples
///
/// The function returns the index of the byte immediately following
/// the line ending character of the `n`th line from the end of the
/// input:
///
/// ```rust,ignore
/// let mut input = Cursor::new("x\ny\nz\n");
/// assert_eq!(find_nth_line_from_end(&mut input, 0, false).unwrap(), 6);
/// assert_eq!(find_nth_line_from_end(&mut input, 1, false).unwrap(), 4);
/// assert_eq!(find_nth_line_from_end(&mut input, 2, false).unwrap(), 2);
/// ```
///
/// If `n` exceeds the number of lines in the file, always return 0:
///
/// ```rust,ignore
/// let mut input = Cursor::new("x\ny\nz\n");
/// assert_eq!(find_nth_line_from_end(&mut input, 3, false).unwrap(), 0);
/// assert_eq!(find_nth_line_from_end(&mut input, 4, false).unwrap(), 0);
/// assert_eq!(find_nth_line_from_end(&mut input, 1000, false).unwrap(), 0);
/// ```
fn find_nth_line_from_end<R>(input: &mut R, n: u64, separator: u8) -> io::Result<u64>
where
R: Read + Seek,
{
let file_size = input.seek(SeekFrom::End(0))?;
let mut buffer = [0u8; BUF_SIZE];
let mut lines = 0u64;
let mut check_last_byte_first_loop = true;
let mut bytes_remaining_to_search = file_size;
loop {
// the casts here are ok, `buffer.len()` should never be above a few k
let bytes_to_read_this_loop = bytes_remaining_to_search.min(buffer.len().try_into().unwrap());
let read_start_offset = bytes_remaining_to_search - bytes_to_read_this_loop;
let buffer = &mut buffer[..bytes_to_read_this_loop.try_into().unwrap()];
bytes_remaining_to_search -= bytes_to_read_this_loop;
input.seek(SeekFrom::Start(read_start_offset))?;
input.read_exact(buffer)?;
// Unfortunately need special handling for the case that the input file doesn't
// have a terminating `separator` character.
// If the input file doesn't end with a `separator` character, add an extra line
// to our `line` counter. In the case that `n` is 0 we need to return here
// since we've obviously found our 0th-line-from-the-end offset.
if check_last_byte_first_loop {
check_last_byte_first_loop = false;
if let Some(last_byte_of_file) = buffer.last()
&& last_byte_of_file != &separator
{
if n == 0 {
input.rewind()?;
return Ok(file_size);
}
assert_eq!(lines, 0);
lines = 1;
}
}
for separator_offset in memrchr_iter(separator, &buffer[..]) {
lines += 1;
if lines == n + 1 {
input.rewind()?;
return Ok(read_start_offset + TryInto::<u64>::try_into(separator_offset).unwrap() + 1);
}
}
if read_start_offset == 0 {
input.rewind()?;
return Ok(0);
}
}
}
fn is_seekable(input: &mut File) -> bool {
let current_pos = input.stream_position();
current_pos.is_ok()
&& input.seek(SeekFrom::End(0)).is_ok()
&& input.seek(SeekFrom::Start(current_pos.unwrap())).is_ok()
}
fn head_backwards_file(input: &mut File, output: &mut impl Write, options: &HeadOptions) -> io::Result<u64> {
let st = input.metadata()?;
let seekable = is_seekable(input);
let blksize_limit = uucore::fs::sane_blksize::sane_blksize_from_metadata(&st);
if !seekable || st.len() <= blksize_limit || options.presume_input_pipe {
head_backwards_without_seek_file(input, output, options)
} else {
head_backwards_on_seekable_file(input, output, options)
}
}
fn head_backwards_without_seek_file(input: &mut File, output: &mut impl Write, options: &HeadOptions) -> io::Result<u64> {
match options.mode {
Mode::AllButLastBytes(n) => read_but_last_n_bytes(input, output, n),
Mode::AllButLastLines(n) => read_but_last_n_lines(input, output, n, options.line_ending.into()),
_ => unreachable!(),
}
}
fn head_backwards_on_seekable_file(input: &mut File, output: &mut impl Write, options: &HeadOptions) -> io::Result<u64> {
match options.mode {
Mode::AllButLastBytes(n) => {
let size = input.metadata()?.len();
if n >= size {
Ok(0)
} else {
read_n_bytes(input, output, size - n)
}
},
Mode::AllButLastLines(n) => {
let found = find_nth_line_from_end(input, n, options.line_ending.into())?;
read_n_bytes(input, output, found)
},
_ => unreachable!(),
}
}
fn head_file(input: &mut File, output: &mut impl Write, options: &HeadOptions) -> io::Result<u64> {
match options.mode {
Mode::FirstBytes(n) => read_n_bytes(input, output, n),
Mode::FirstLines(n) => read_n_lines(
&mut io::BufReader::with_capacity(BUF_SIZE, input),
output,
n,
options.line_ending.into(),
),
Mode::AllButLastBytes(_) | Mode::AllButLastLines(_) => head_backwards_file(input, output, options),
}
}
/// Parsed `head` invocation.
pub(crate) struct Head {
matches: ArgMatches,
}
matches_parser!(Head, uu_app);
impl Utility for Head {
const NAME: &'static str = "head";
// Normalize GNU's obsolete `-NUM` syntax before clap sees argv; clap
// otherwise treats it as an unknown short-option cluster.
fn rewrite_argv(argv: Vec<OsString>) -> Result<Vec<OsString>, String> {
arg_iterate(argv).map_err(|err| err.to_string())
}
fn run(self, host: &mut Host) -> i32 {
let options = match HeadOptions::get_from(&self.matches) {
Ok(options) => options,
Err(err) => {
host.error(HeadError::MatchOption(err), 1);
return 1;
},
};
let print_headers = (options.files.len() > 1 && !options.quiet) || options.verbose;
// GNU head only emits the blank separator line before a header when a
// previous file actually produced output; open failures print nothing
// and must not flip `first`.
let mut first = true;
fn print_header(out: &mut impl Write, name: &[u8], first: &mut bool) {
if !*first {
let _ = writeln!(out);
}
let _ = out.write_all(b"==> ");
let _ = out.write_all(name);
let _ = out.write_all(b" <==\n");
*first = false;
}
for file in &options.files {
let result = if file == "-" {
if print_headers {
print_header(&mut host.stdout, b"standard input", &mut first);
}
let mut input = io::BufReader::with_capacity(BUF_SIZE, &mut host.stdin);
match options.mode {
Mode::FirstBytes(n) => read_n_bytes(&mut input, &mut host.stdout, n),
Mode::AllButLastBytes(n) => {
read_but_last_n_bytes(&mut input, &mut host.stdout, n)
},
Mode::FirstLines(n) => {
read_n_lines(&mut input, &mut host.stdout, n, options.line_ending.into())
},
Mode::AllButLastLines(n) => read_but_last_n_lines(
&mut input,
&mut host.stdout,
n,
options.line_ending.into(),
),
}
} else {
let resolved = host.resolve(file);
if resolved.is_dir() {
// GNU prints the header before reporting the read error,
// and that header counts as produced output.
if print_headers {
print_header(&mut host.stdout, file.as_encoded_bytes(), &mut first);
}
host.error(format!("error reading {}: Is a directory", file.quote()), 1);
continue;
}
let mut input = match File::open(&resolved) {
Ok(input) => input,
Err(err) => {
host.error(format!("cannot open {} for reading: {err}", file.quote()), 1);
continue;
},
};
if print_headers {
print_header(&mut host.stdout, file.as_encoded_bytes(), &mut first);
}
head_file(&mut input, &mut host.stdout, &options)
};
if let Err(err) = result {
let name = if file == "-" {
PathBuf::from("standard input")
} else {
PathBuf::from(file)
};
// A dead pipe ends the whole invocation; any other I/O error
// only fails this operand, and GNU keeps going.
let broken_pipe = err.kind() == io::ErrorKind::BrokenPipe;
host.error(HeadError::Io { name, err }, 1);
if broken_pipe {
return 1;
}
}
}
host.exit_code()
}
}
/// Creates the `head` builtin registration.
pub(crate) fn head_builtin<SE: ShellExtensions>() -> Registration<SE> {
util::<Head, SE>()
}
#[cfg(test)]
mod tests {
use std::{ffi::OsString, io::Cursor};
use clap::Parser;
use tempfile::tempdir;
use super::*;
use crate::host::run_util;
fn options(args: &str) -> Result<HeadOptions, String> {
let combined = "head ".to_owned() + args;
let argv = combined.split_whitespace().map(OsString::from).collect();
let argv = Head::rewrite_argv(argv)?;
let parsed = Head::try_parse_from(argv).map_err(|err| err.to_string())?;
HeadOptions::get_from(&parsed.matches)
}
#[test]
fn argument_modes_and_obsolete_syntax() {
let args = options("-n -10M -vz").unwrap();
assert_eq!(args.line_ending, LineEnding::Nul);
assert!(args.verbose);
assert_eq!(args.mode, Mode::AllButLastLines(10 * 1024 * 1024));
assert_eq!(options("-5").unwrap().mode, Mode::FirstLines(5));
assert_eq!(options("-2b").unwrap().mode, Mode::FirstBytes(1024));
assert_eq!(options("-5 -c 1").unwrap().mode, Mode::FirstBytes(1));
}
#[test]
fn options_follow_gnu_last_one_wins_rules() {
let args = options("-n 1 -c 1 -n 5 -c kiB -vqvqv").unwrap();
assert_eq!(args.mode, Mode::FirstBytes(1024));
assert!(args.verbose);
assert!(options("--silent").unwrap().quiet);
assert_eq!(options("--zero-terminated").unwrap().line_ending, LineEnding::Nul);
}
#[test]
fn option_errors_are_reported() {
assert!(options("-n IsThisTheRealLife?").is_err());
assert!(options("-c IsThisJustFantasy").is_err());
let (code, capture) = run_util::<Head>(&["-123FooBar"], "", "/");
assert_eq!(code, 1);
assert_eq!(capture.err(), "head: bad argument format: '-123FooBar'\n");
}
fn rewritten(argv: &[&str]) -> Vec<String> {
Head::rewrite_argv(argv.iter().map(OsString::from).collect())
.unwrap()
.into_iter()
.map(|arg| arg.to_str().unwrap().to_owned())
.collect()
}
// Failure mode: obsolete `-NUM` was only recognized as argv[1], so
// `head -q -5 file`, `head file -5`, and repeated counts were parse errors.
#[test]
fn obsolete_num_is_rewritten_at_any_position() {
assert_eq!(rewritten(&["head", "-q", "-5", "f"]), ["head", "-q", "-n", "5", "f"]);
assert_eq!(rewritten(&["head", "-v", "-20", "f"]), ["head", "-v", "-n", "20", "f"]);
assert_eq!(rewritten(&["head", "f", "-5"]), ["head", "f", "-n", "5"]);
assert_eq!(
rewritten(&["head", "-5", "-20", "f"]),
["head", "-n", "5", "-n", "20", "f"]
);
assert_eq!(rewritten(&["head", "-5qz", "f"]), ["head", "-q", "-z", "-n", "5", "f"]);
}
// Failure mode: `-5` following a value-taking option is that option's
// value, and rewriting it would corrupt the invocation.
#[test]
fn option_values_and_post_ddash_operands_are_not_rewritten() {
assert_eq!(rewritten(&["head", "-n", "-5", "f"]), ["head", "-n", "-5", "f"]);
assert_eq!(rewritten(&["head", "-c", "-5", "f"]), ["head", "-c", "-5", "f"]);
assert_eq!(rewritten(&["head", "--lines", "-5", "f"]), ["head", "--lines", "-5", "f"]);
assert_eq!(rewritten(&["head", "--", "-5"]), ["head", "--", "-5"]);
assert_eq!(rewritten(&["head", "-n5", "-", "f"]), ["head", "-n5", "-", "f"]);
}
// Failure mode: uppercase suffixes in the obsolete form were rejected
// even though `head -n 10K` accepts them.
#[test]
fn obsolete_uppercase_suffixes_scale_lines() {
assert_eq!(options("-10K").unwrap().mode, Mode::FirstLines(10 * 1024));
assert_eq!(options("-1M").unwrap().mode, Mode::FirstLines(1024 * 1024));
assert_eq!(options("-1G").unwrap().mode, Mode::FirstLines(1024 * 1024 * 1024));
// Lowercase suffixes keep their historical byte meaning.
assert_eq!(options("-1k").unwrap().mode, Mode::FirstBytes(1024));
}
// Failure mode: an unreadable operand flipped the separator state and the
// next header gained a spurious leading blank line; a mid-list open error
// must also not abort the remaining operands.
#[test]
fn open_error_produces_no_separator_and_processing_continues() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("f1"), b"a\n").unwrap();
std::fs::write(dir.path().join("f2"), b"b\n").unwrap();
let (code, capture) = run_util::<Head>(&["-n", "1", "missing", "f1", "f2"], "", dir.path());
assert_eq!(code, 1);
assert_eq!(capture.out(), "==> f1 <==\na\n\n==> f2 <==\nb\n");
assert!(capture.err().contains("cannot open 'missing' for reading"));
}
// Failure mode: the `==> dir <==` header was suppressed before the
// Is-a-directory diagnostic; GNU prints it and counts it as output.
#[test]
fn directory_operand_prints_header_before_error() {
let dir = tempdir().unwrap();
std::fs::create_dir(dir.path().join("d")).unwrap();
std::fs::write(dir.path().join("f"), b"a\n").unwrap();
let (code, capture) = run_util::<Head>(&["-n", "1", "d", "f"], "", dir.path());
assert_eq!(code, 1);
assert_eq!(capture.out(), "==> d <==\n\n==> f <==\na\n");
assert_eq!(capture.err(), "head: error reading 'd': Is a directory\n");
}
#[test]
fn defaults_to_ten_lines_from_stdin() {
let input = (1..=12).map(|n| format!("{n}\n")).collect::<String>();
let (code, capture) = run_util::<Head>(&[], &input, "/");
assert_eq!(code, 0);
assert_eq!(capture.out(), (1..=10).map(|n| format!("{n}\n")).collect::<String>());
}
#[test]
fn resolves_operands_against_host_cwd_but_prints_supplied_name() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("input"), b"a\nb\n").unwrap();
let (code, capture) = run_util::<Head>(&["-v", "input"], "", dir.path());
assert_eq!(code, 0);
assert_eq!(capture.out(), "==> input <==\na\nb\n");
}
#[test]
fn negative_line_count_on_file_uses_resolved_handle() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("input"), b"a\nb\nc\n").unwrap();
let (code, capture) = run_util::<Head>(&["-n", "-1", "input"], "", dir.path());
assert_eq!(code, 0);
assert_eq!(capture.out(), "a\nb\n");
}
#[test]
fn find_nth_line_from_end_handles_large_and_unterminated_inputs() {
let mut input_buffer = Vec::new();
while input_buffer.len() < BUF_SIZE * 4 {
input_buffer.extend_from_slice(b"aaaa\n");
}
let input_length = input_buffer.len() as u64;
let lines = input_length / 5;
let mut input = Cursor::new(input_buffer);
for n in (0..lines).filter(|v| v % 511 == 0) {
assert_eq!(find_nth_line_from_end(&mut input, n, b'\n').unwrap(), input_length - 5 * n);
}
assert_eq!(find_nth_line_from_end(&mut input, lines, b'\n').unwrap(), 0);
let mut input = Cursor::new("a\nb");
assert_eq!(find_nth_line_from_end(&mut input, 0, b'\n').unwrap(), 3);
assert_eq!(find_nth_line_from_end(&mut input, 1, b'\n').unwrap(), 2);
}
}