use std::io::{BufReader, BufWriter, Read, Write}; pub use crate::cli::{Cli, Opts}; pub use crate::error::{Error, Result}; pub use crate::input::Input; mod cli; mod error; mod input; pub const HELP: &str = r#"Usage: cat [OPTION]... [FILE]... Concatenate FILE(s) to standard output. With no FILE, or when FILE is -, read standard input. -A, --show-all equivalent to -vET -b, --number-nonblank number nonempty output lines, overrides -n -e equivalent to -vE -E, --show-ends display $ at end of each line -n, --number number all output lines -s, --squeeze-blank suppress repeated empty output lines -t equivalent to -vT -T, --show-tabs display TAB characters as ^I -u (ignored) -v, --show-nonprinting use ^ and M- notation, except for LFD and TAB --help display this help and exit --version output version information and exit Examples: cat f - g Output f's contents, then standard input, then g's contents. cat Copy standard input to standard output."#; pub fn run(cli: Cli) -> Result<()> { let stdout = std::io::stdout(); let mut writer = stdout.lock(); for file in cli.files.into_iter() { process_input(&mut file.reader()?, &mut writer, &cli.opts)?; } Ok(()) } fn process_input(reader: impl Read, writer: &mut impl Write, opts: &Opts) -> Result<()> { let mut reader = BufReader::new(reader); let mut writer = BufWriter::new(writer); let mut nonblank_line_nr = 0; let mut buf = Vec::new(); reader.read_to_end(&mut buf)?; for (index, arr) in buf.split(|c| *c == b'\n').enumerate() { if arr.is_empty() && opts.squeeze_blank { continue; } let mut line = Vec::new(); for c in arr { let parsed = match *c { b'\t' if opts.show_tabs => b"^I".to_vec(), b'\t' => b"\t".to_vec(), c if opts.show_nonprinting => process_nonprinting(c), c => [c].to_vec(), }; line.extend(parsed) } if opts.show_ends { line.push(b'$'); } line.push(b'\n'); if opts.number_nonblank { if !line.is_empty() { nonblank_line_nr += 1; write!(writer, "{:>6} ", nonblank_line_nr)?; } } else if opts.number { write!(writer, "{:>6} ", index + 1)?; } writer.write_all(&line)? } Ok(()) } fn process_nonprinting(mut c: u8) -> Vec { let mut buf = Vec::new(); if c >= 128 { c -= 128; buf.extend(b"M-"); } match c { c @ 0..=31 => buf.extend([b'^', c + 64]), c @ 32..=126 => buf.push(c), 127.. => buf.extend(b"^?"), }; buf } #[cfg(test)] mod test { use crate::cli::Opt; use super::*; use std::process::{Command, Stdio}; #[test] fn test_process_input() { let input = (0..255_u8) .map(|i| format!("{}", i as char)) .collect::>() .join("\n"); let cat_child = Command::new("cat") .arg("-") .arg("-v") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .unwrap(); cat_child .stdin .as_ref() .unwrap() .write_all(input.as_bytes()) .unwrap(); let cat_output = cat_child.wait_with_output().unwrap(); let cat_result = String::from_utf8(cat_output.stdout).unwrap(); let mut opts = Opts::default(); opts.set_opt(Opt::ShowNonprinting); let mut writer = Vec::new(); process_input(input.as_bytes(), &mut writer, &opts).unwrap(); let result = String::from_utf8(writer).unwrap(); cat_result .lines() .zip(result.lines()) .for_each(|(c_cat, c)| assert_eq!(c_cat, c)); } }