This is a clone of the cat utility. The goal of this exercise was to get comfortable doing basic tasks in a new language, figure out how to set up testing, etc. So I'm really looking for feedback on writing more idiomatic Rust, if there are better ways to approach things, etc. In particular, my TextFixture class I use to manage lifetimes so I can easily create a Cat object and retreive the output streams, seems like not the best way to do things.Conversely, issues like "numbering will be broken if you have more than 1,000 lines" are not very important to me for this exercise.
use std::fs::File;
use std::io;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::str;
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
#[structopt(
name = "cat",
about = "concatenate files and print on the standard output"
)]
struct CatConfig {
/// Input file
#[structopt(name = "FILE", parse(from_os_str))]
input: Vec<PathBuf>,
/// display $ at the end of each line
#[structopt(short = "E", long = "show-ends")]
show_ends: bool,
/// display TAB characters as ^I
#[structopt(short = "T", long = "show-tabs")]
show_tabs: bool,
/// number all output lines
#[structopt(short = "n", long = "number")]
number: bool,
}
impl Default for CatConfig {
fn default() -> CatConfig {
CatConfig {
input: vec![],
show_tabs: false,
show_ends: false,
number: false,
}
}
}
struct Cat<'a> {
stdout: &'a mut dyn io::Write,
stderr: &'a mut dyn io::Write,
}
impl Cat<'_> {
fn new<'a>(stdout: &'a mut dyn io::Write, stderr: &'a mut dyn io::Write) -> Cat<'a> {
Cat { stdout, stderr }
}
fn cat_all(&mut self, config: CatConfig) -> () {
for path in &config.input {
match File::open(path) {
Ok(file) => {
let mut input = BufReader::new(file);
self.cat(&config, &mut input);
}
Err(_) => {
let filename = path.to_str().unwrap();
self.stderr.write(filename.as_bytes()).unwrap();
self.stderr.write(b": No such file or directory\n").unwrap();
}
}
}
}
fn cat(&mut self, config: &CatConfig, input: &mut dyn BufRead) -> () {
let mut line_number = 1;
for line in input.lines() {
let string_line = line.unwrap();
let line_ending: &str = if config.show_ends { "$\n" } else { "\n" };
let mut line_to_write = [string_line, line_ending.to_string()].concat();
if config.show_tabs {
line_to_write = line_to_write.replace("\t", "^I");
}
if config.number {
let number_string = line_number.to_string();
line_to_write = [" ", &number_string, " ", &line_to_write].concat();
}
line_number = line_number + 1;
self.stdout.write(&line_to_write.as_bytes()).unwrap();
}
}
}
fn main() -> () {
let opt = CatConfig::from_args();
Cat::new(&mut io::stdout(), &mut io::stderr()).cat_all(opt);
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::path::Path;
use tempfile::NamedTempFile;
struct TestFixture {
stdout: io::Cursor<Vec<u8>>,
stderr: io::Cursor<Vec<u8>>,
}
impl TestFixture {
fn new() -> TestFixture {
let stdout = io::Cursor::new(Vec::new());
let stderr = io::Cursor::new(Vec::new());
TestFixture { stdout, stderr }
}
fn make_cat(&mut self) -> Cat {
return Cat {
stdout: &mut self.stdout,
stderr: &mut self.stderr,
};
}
fn get_output(&self) -> String {
let clone = self.stdout.clone();
let output_vec = clone.into_inner();
String::from_utf8(output_vec).unwrap()
}
fn get_error(&self) -> String {
let clone = self.stderr.clone();
let output_vec = clone.into_inner();
String::from_utf8(output_vec).unwrap()
}
}
#[test]
fn cat_with_no_options() {
let mut source = b"abc\ndef\n" as &[u8];
let expected_output = "abc\ndef\n";
let config = CatConfig::default();
check_cat_output(&mut source, expected_output, config);
}
#[test]
fn cat_from_nonexistant_file() {
let mut fixture = TestFixture::new();
let mut catter = fixture.make_cat();
let file = Path::new("file_does_not_exist.txt");
let config = CatConfig {
input: vec![file.to_path_buf()],
..Default::default()
};
catter.cat_all(config);
let expected_error = "file_does_not_exist.txt: No such file or directory\n";
assert_eq!(expected_error, fixture.get_error());
}
#[test]
fn cat_from_file() {
let mut fixture = TestFixture::new();
let mut catter = fixture.make_cat();
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "File contents").unwrap();
let config = CatConfig {
input: vec![file.path().to_path_buf()],
..Default::default()
};
catter.cat_all(config);
assert_eq!("File contents\n", fixture.get_output());
}
#[test]
fn test_cat_shows_ends() {
let mut source = b"abc\ndef\n" as &[u8];
let expected_output = "abc$\ndef$\n";
let config = CatConfig {
show_ends: true,
..Default::default()
};
check_cat_output(&mut source, expected_output, config);
}
#[test]
fn test_cat_shows_tabs() {
let mut source = b"abc\tdef\n" as &[u8];
let expected_output = "abc^Idef\n";
let config = CatConfig {
show_tabs: true,
..Default::default()
};
check_cat_output(&mut source, expected_output, config);
}
fn check_cat_output(source: &mut dyn BufRead, expected_output: &str, config: CatConfig) {
let mut fixture = TestFixture::new();
let mut catter = fixture.make_cat();
catter.cat(&config, source);
assert_eq!(expected_output, fixture.get_output());
}
#[test]
fn test_cat_number_lines() {
let mut source = b"abc\ndef\n" as &[u8];
let expected_output = " 1 abc\n 2 def\n";
let config = CatConfig {
number: true,
..Default::default()
};
check_cat_output(&mut source, expected_output, config);
}
}