I reimplemented cat(1)
for fun. I followed the Open Group Base Specifications (Issue 7, 2018 edition), not the GNU variant and its command line arguments.
Buffered and -u
nbuffered behaviour
While the specification defines the behaviour for -u
, it doesn't define how the arguments should be concatenated when the -u
nbuffered argument is missing. To get used to BufRead
and BufWriter
, I used a buffered approach that simply uses Rust's already existing methods.
I'm not entirely happy about the return code issue. I currently use io::Result
in main
to return the last error if it occurred, however, this also means that the last error will get reported twice. I could use std::process::exit
, but that would need a wrapper around main
.
Argument parsing and additional dependencies
I'll admit, the argument handling part of main
won't win a beauty contest. However, I didn't want to add clap
or other argument handling libraries and instead focus on using std
. The program should follow the Utility Argument Syntax, however, it doesn't follow guidelines 5 (e.g. -uuuu
is not the same as -u -u -u -u
) and 9 (-u
does not need to be the first argument). Argument grouping is a non-goal for this toy program, however.
Also, comparing to GNU cat
, unknown options are interpreted as files names, whereas GNU cat
will exit with an error message. I'm not sure whether that violates the specifications, though.
Implementation
The application is split into two parts, namely main.rs
(mostly argument parsing) and lib.rs
(the actual implementation). Any comments on the organisation are fine.
// main.rs
use std::ffi::OsString;
use std::path::Path;
use cat::{cat_buffered_single, cat_unbuffered_single};
fn main() -> std::io::Result<()> {
let mut args: Vec<OsString> = std::env::args_os().skip(1).collect();
// Only parse arguments up to "--"
let args_up_to = if let Some(index) = args.iter().position(|arg| arg == "--") {
args.remove(index);
index
} else {
args.len()
};
// Keep all arguments after "--" as-is
let verbatim_args = args.split_off(args_up_to);
// Check for "-u" in valid positions and remove the first one
let cat_func = if args.iter().any(|arg| arg == "-u") {
args = args.into_iter().filter(|x| x != "-u").collect();
cat_unbuffered_single
} else {
cat_buffered_single
};
// Recombine arguments
args.extend(verbatim_args);
// Fallback to stdin behaviour
if args.is_empty() {
args.push("-".into());
}
let mut result = Ok(());
for arg in args {
let path = Path::new(&arg);
match cat_func(path) {
Ok(()) => continue,
Err(e) => {
eprintln!("cat: {}: {}", path.to_string_lossy(), e);
result = Err(e);
}
}
}
result
}
// lib.rs
use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Read, Write};
use std::path::Path;
// Dumps all bytes from `src` into `dest`, using both buffer functionalities.
fn dump_buffered_single(src: &mut dyn BufRead, dest: &mut BufWriter<impl Write>) -> io::Result<()> {
loop {
let buf = src.fill_buf()?;
if buf.is_empty() {
break;
}
dest.write_all(buf)?;
let bytes = buf.len();
src.consume(bytes);
}
dest.flush()
}
/// Dumps the file given by `path` on `stdout` using buffered IO.
///
/// If `path` is `"-"`, then `stdin` is used as input instead of a file.
///
/// Example
/// ```
/// # use cat::cat_buffered_single;
/// cat_buffered_single("hello.txt".as_ref());
/// ```
pub fn cat_buffered_single(path: &Path) -> io::Result<()> {
let stdout = io::stdout();
let handle = stdout.lock();
let mut writer = io::BufWriter::new(handle);
if path == Path::new("-") {
dump_buffered_single(&mut io::stdin().lock(), &mut writer)?;
} else {
let input = BufReader::new(File::open(path)?);
let mut reader = BufReader::new(input);
dump_buffered_single(&mut reader, &mut writer)?;
}
Ok(writer.flush()?)
}
// Dumps all bytes from `src` into `dest`, byte by byte.
fn dump_unbuffered_single(src: &mut dyn Read, dest: &mut dyn Write) -> io::Result<()> {
for byte in src.bytes() {
dest.write_all(std::slice::from_ref(&byte?))?;
}
Ok(())
}
/// Dumps the file given by `path` on `stdout` without buffering
///
/// If `path` is `"-"`, then `stdin` is used as input instead of a file.
///
/// Example
/// ```
/// # use cat::cat_unbuffered_single;
/// cat_unbuffered_single("hello.txt".as_ref());
/// ```
pub fn cat_unbuffered_single(path: &Path) -> io::Result<()> {
let stdout = io::stdout();
let mut handle = stdout.lock();
if path == Path::new("-") {
dump_unbuffered_single(&mut io::stdin().lock(), &mut handle)?;
} else {
let mut file = File::open(path)?;
dump_unbuffered_single(&mut file, &mut handle)?;
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
fn test_dump_buffered_single(test_bytes: &[u8]) {
let src = test_bytes;
let mut reader = BufReader::new(src);
let mut dest = vec![];
{
let mut writer = BufWriter::new(&mut dest);
dump_buffered_single(&mut reader, &mut writer).unwrap();
}
assert_eq!(dest, src);
}
#[test]
fn dumps_all_buffered_data() {
test_dump_buffered_single(b"Hello, World");
}
#[test]
fn dumps_no_buffered_data() {
test_dump_buffered_single(b"");
}
fn test_dump_unbuffered_single(bytes: &[u8]) {
let source = bytes;
let mut reader = source.clone();
let mut destination = vec![];
dump_unbuffered_single(&mut reader, &mut destination).unwrap();
assert_eq!(destination, source);
}
#[test]
fn dumps_all_unbuffered_data() {
test_dump_unbuffered_single(b"Hello, World");
}
#[test]
fn dumps_no_unbuffered_data() {
test_dump_unbuffered_single(b"");
}
}
I've used cargo fmt
and cargo clippy
on the code above. By the way, I see myself as a Rust beginner, so feel free to comment any part of the code.