From 00dd3108e1f7dbe513622e44621f0cc54eeed740 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sun, 29 Mar 2026 15:32:36 -0400 Subject: [PATCH 1/9] chore: modernize cat Switch to the modernized CLI --- cat/Cargo.toml | 6 +- cat/src/main.rs | 144 +++++++++++++++++++++++++++++++----------------- 2 files changed, 98 insertions(+), 52 deletions(-) diff --git a/cat/Cargo.toml b/cat/Cargo.toml index 083724f..bad908f 100644 --- a/cat/Cargo.toml +++ b/cat/Cargo.toml @@ -1,9 +1,13 @@ [package] name = "cat" version = "0.1.0" -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] clap = { workspace = true } +coreutils = { workspace = true } +tabled = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/cat/src/main.rs b/cat/src/main.rs index 9e567c4..b59f247 100644 --- a/cat/src/main.rs +++ b/cat/src/main.rs @@ -2,66 +2,108 @@ use std::io; use std::io::prelude::*; use std::str; -use clap::Parser; +use clap::{Arg, ArgAction}; +// use tabled::{builder::Builder, settings::Style}; -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - /// equivalent to -vET - #[arg(short = 'A', long)] - show_all: bool, +use coreutils::{clap_args, clap_base_command}; - /// number nonempty output lines, overrides -n - #[arg(short = 'b', long)] - number_nonblank: bool, - - /// equivalent to -vE - #[arg(short = 'e')] - e: bool, - - /// display $ at the end of each line - #[arg(short = 'E', long)] - show_ends: bool, - - /// number all output lines - #[arg(short = 'n', long)] - number: bool, - - /// suppress repeated empty output lines - #[arg(short, long)] - squeeze_blank: bool, - - /// equivalent to -vT - #[arg(short = 't')] - t: bool, - - /// display TAB characters as ^I - #[arg(short = 'T', long)] - show_tabs: bool, - - /// ignored - #[arg(short)] - u: bool, - - /// use ^ and M- notation, except for LFD and TAB - #[arg(short = 'v', long)] - show_nonprinting: bool, - - files: Vec, -} +clap_args!(Args { + flag show_all: bool, + flag number_nonblank: bool, + flag e: bool, + flag show_ends: bool, + flag number: bool, + flag squeeze_blank: bool, + flag t: bool, + flag show_tabs: bool, + flag u: bool, + flag show_nonprinting: bool, + multi files: Vec, +}); impl Args { fn tab(&self) -> &'static str { - if self.show_tabs { - "I" - } else { - "\t" - } + if self.show_tabs { "I" } else { "\t" } } } fn main() { - let mut args = Args::parse(); + let matches = clap_base_command() + .arg( + Arg::new("show-all") + .short('A') + .long("show-all") + .action(ArgAction::SetTrue) + .help("equivalent to -vET"), + ) + .arg( + Arg::new("number-nonblank") + .short('b') + .long("number-nonblank") + .action(ArgAction::SetTrue) + .help("number nonempty output lines, overrides -n"), + ) + .arg( + Arg::new("e") + .short('e') + .action(ArgAction::SetTrue) + .help("equivalent to -vE"), + ) + .arg( + Arg::new("show-ends") + .short('E') + .long("show-ends") + .action(ArgAction::SetTrue) + .help("display $ at the end of each line"), + ) + .arg( + Arg::new("number") + .short('n') + .long("number") + .action(ArgAction::SetTrue) + .help("number all output lines"), + ) + .arg( + Arg::new("squeeze-blank") + .short('s') + .long("squeeze-blank") + .action(ArgAction::SetTrue) + .help("suppress repeated empty output lines"), + ) + .arg( + Arg::new("t") + .short('t') + .action(ArgAction::SetTrue) + .help("equivalent to -vT"), + ) + .arg( + Arg::new("show-tabs") + .short('T') + .long("show-tabs") + .action(ArgAction::SetTrue) + .help("display TAB characters as ^I"), + ) + .arg( + Arg::new("u") + .short('u') + .action(ArgAction::SetTrue) + .help("ignored"), + ) + .arg( + Arg::new("show-nonprinting") + .short('v') + .long("show-nonprinting") + .action(ArgAction::SetTrue) + .help("use ^ and M- notation, except for LFD and TAB"), + ) + .arg( + Arg::new("files") + .action(ArgAction::Append) + .help("the file(s) to concatenate"), + ) + .get_matches(); + + let mut args = Args::from_matches(&matches); // do shortcut: -A if args.show_all { From da71a1e6903c254901980314cb25f0500f36dd42 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sun, 29 Mar 2026 15:33:10 -0400 Subject: [PATCH 2/9] Remove extra newline --- basename/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/basename/src/main.rs b/basename/src/main.rs index 07461df..8b8b101 100644 --- a/basename/src/main.rs +++ b/basename/src/main.rs @@ -9,7 +9,6 @@ clap_args!(Args { flag multiple: bool, maybe suffix: Option, multi args: Vec, - }); /// usage: basename string [suffix] From 03ab734a933c73c69fffdffd54d66ff2eb45a1f2 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sun, 29 Mar 2026 19:37:54 -0400 Subject: [PATCH 3/9] fix: bugs and process improvement Fix some bugs with squeeze-lines, add tests to cover the bug and change. Add return code when process exits --- cat/src/main.rs | 228 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 180 insertions(+), 48 deletions(-) diff --git a/cat/src/main.rs b/cat/src/main.rs index b59f247..beaa1c1 100644 --- a/cat/src/main.rs +++ b/cat/src/main.rs @@ -1,9 +1,8 @@ use std::io; use std::io::prelude::*; -use std::str; +use std::process; use clap::{Arg, ArgAction}; -// use tabled::{builder::Builder, settings::Style}; use coreutils::{clap_args, clap_base_command}; @@ -23,21 +22,21 @@ clap_args!(Args { impl Args { fn tab(&self) -> &'static str { - if self.show_tabs { "I" } else { "\t" } + if self.show_tabs { "^I" } else { "\t" } } } fn main() { let matches = clap_base_command() .arg( - Arg::new("show-all") + Arg::new("show_all") .short('A') .long("show-all") .action(ArgAction::SetTrue) .help("equivalent to -vET"), ) .arg( - Arg::new("number-nonblank") + Arg::new("number_nonblank") .short('b') .long("number-nonblank") .action(ArgAction::SetTrue) @@ -50,7 +49,7 @@ fn main() { .help("equivalent to -vE"), ) .arg( - Arg::new("show-ends") + Arg::new("show_ends") .short('E') .long("show-ends") .action(ArgAction::SetTrue) @@ -64,7 +63,7 @@ fn main() { .help("number all output lines"), ) .arg( - Arg::new("squeeze-blank") + Arg::new("squeeze_blank") .short('s') .long("squeeze-blank") .action(ArgAction::SetTrue) @@ -77,7 +76,7 @@ fn main() { .help("equivalent to -vT"), ) .arg( - Arg::new("show-tabs") + Arg::new("show_tabs") .short('T') .long("show-tabs") .action(ArgAction::SetTrue) @@ -90,7 +89,7 @@ fn main() { .help("ignored"), ) .arg( - Arg::new("show-nonprinting") + Arg::new("show_nonprinting") .short('v') .long("show-nonprinting") .action(ArgAction::SetTrue) @@ -124,7 +123,7 @@ fn main() { args.show_tabs = true; } - // let mut blank: i32 = 0; + let mut retval = 0; let mut line_count: usize = 1; let mut stdout = io::stdout().lock(); let mut stderr = io::stderr().lock(); @@ -138,8 +137,9 @@ fn main() { for filename in &args.files { let mut file = match std::fs::File::open(filename) { Ok(f) => f, - Err(_) => { - println!("cat: {}: No such file or directory", filename); + Err(err) => { + retval = 1; + eprintln!("cat: {}: {}", filename, err); continue; } }; @@ -150,64 +150,73 @@ fn main() { stdout.flush().unwrap(); stderr.flush().unwrap(); + + process::exit(retval); } /// Cat a file from argument or stdin -fn cat( +fn cat( file: &mut F, args: &Args, line_count: &mut usize, - stdout: &mut std::io::StdoutLock, - stderr: &mut std::io::StderrLock, + stdout: &mut O, + stderr: &mut E, ) { let mut character_count = 0; - let mut last_line_was_blank = false; + let mut at_line_start = false; let mut in_buffer: [u8; 8 * 8192] = [0; 8 * 8192]; let mut out_buffer: Vec = Vec::with_capacity(24 * 8192); loop { - let n_read = file.read(&mut in_buffer).unwrap(); + let n_read = match file.read(&mut in_buffer) { + Ok(n) => n, + Err(err) => { + writeln!(stderr, "cat: {err}").ok(); + break; + } + }; if n_read == 0 { break; } for &byte in in_buffer[0..n_read].iter() { + // Squeeze blank lines: skip before any output (including line numbers) + if byte == b'\n' && character_count == 0 && args.squeeze_blank && at_line_start { + continue; + } + // If we're tracking line numbers, this is where we'll print them out if character_count == 0 && (args.number || (args.number_nonblank && byte != b'\n')) { - out_buffer - .write_all(format!("{: >6} ", line_count).as_bytes()) - .unwrap(); - - last_line_was_blank = true; + write!(out_buffer, "{: >6} ", line_count).unwrap(); } match byte { 0..=8 | 11..=31 => { if args.show_nonprinting { - push_caret(&mut out_buffer, stderr, byte + 64); - count_character(&mut character_count, args); + push_caret(&mut out_buffer, byte + 64); + } else { + out_buffer.write_all(&[byte]).unwrap(); } + character_count += 1; } 9 => { out_buffer.write_all(args.tab().as_bytes()).unwrap(); - count_character(&mut character_count, args); + character_count += 1; } 10 => { - // increment the line count when we find a newline - if character_count > 0 || !args.number_nonblank { - *line_count += 1; - } + let is_blank = character_count == 0; - if character_count == 0 { - if args.squeeze_blank && last_line_was_blank { - continue; - } else if !last_line_was_blank { - last_line_was_blank = true; - } + if is_blank { + at_line_start = true; } else { - last_line_was_blank = false; + at_line_start = false; character_count = 0; } + // increment line count (skip for blank lines when -b) + if !is_blank || !args.number_nonblank { + *line_count += 1; + } + if args.show_ends { out_buffer.write_all(b"$\n").unwrap(); } else { @@ -216,11 +225,15 @@ fn cat( } 32..=126 => { out_buffer.write_all(&[byte]).unwrap(); - count_character(&mut character_count, args); + character_count += 1; } 127 => { - push_caret(&mut out_buffer, stderr, b'?'); - count_character(&mut character_count, args); + if args.show_nonprinting { + push_caret(&mut out_buffer, b'?'); + } else { + out_buffer.write_all(&[byte]).unwrap(); + } + character_count += 1; } 128..=159 => { if args.show_nonprinting { @@ -229,7 +242,7 @@ fn cat( } else { out_buffer.write_all(&[byte]).unwrap(); } - count_character(&mut character_count, args); + character_count += 1; } _ => { if args.show_nonprinting { @@ -238,7 +251,7 @@ fn cat( } else { out_buffer.write_all(&[byte]).unwrap(); } - count_character(&mut character_count, args); + character_count += 1; } }; } @@ -247,13 +260,132 @@ fn cat( } } -fn count_character(character_count: &mut usize, args: &Args) { - if args.number || args.number_nonblank { - *character_count += 1; - } -} - -fn push_caret(stdout: &mut T, _stderr: &mut std::io::StderrLock, notation: u8) { +fn push_caret(stdout: &mut T, notation: u8) { stdout.write_all(b"^").unwrap(); stdout.write_all(&[notation]).unwrap(); } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + fn default_args() -> Args { + Args { + show_all: false, + number_nonblank: false, + e: false, + show_ends: false, + number: false, + squeeze_blank: false, + t: false, + show_tabs: false, + u: false, + show_nonprinting: false, + files: vec![], + output: None, + } + } + + fn run_cat(input: &[u8], args: &Args) -> String { + let mut cursor = Cursor::new(input); + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + let mut line_count: usize = 1; + cat(&mut cursor, args, &mut line_count, &mut stdout, &mut stderr); + String::from_utf8(stdout).unwrap() + } + + #[test] + fn test_basic() { + let output = run_cat(b"hello\nworld\n", &default_args()); + assert_eq!(output, "hello\nworld\n"); + } + + #[test] + fn test_number_lines() { + let mut args = default_args(); + args.number = true; + let output = run_cat(b"a\nb\nc\n", &args); + assert_eq!(output, " 1 a\n 2 b\n 3 c\n"); + } + + #[test] + fn test_number_nonblank() { + let mut args = default_args(); + args.number_nonblank = true; + let output = run_cat(b"a\n\nb\n", &args); + assert_eq!(output, " 1 a\n\n 2 b\n"); + } + + #[test] + fn test_show_ends() { + let mut args = default_args(); + args.show_ends = true; + let output = run_cat(b"hello\nworld\n", &args); + assert_eq!(output, "hello$\nworld$\n"); + } + + #[test] + fn test_show_tabs() { + let mut args = default_args(); + args.show_tabs = true; + let output = run_cat(b"a\tb\n", &args); + assert_eq!(output, "a^Ib\n"); + } + + #[test] + fn test_squeeze_blank() { + let mut args = default_args(); + args.squeeze_blank = true; + let output = run_cat(b"a\n\n\n\nb\n", &args); + assert_eq!(output, "a\n\nb\n"); + } + + // Regression: squeeze_blank must not skip non-blank lines. + // Bug #1: the squeeze check inside the character_count > 0 branch + // could skip content lines when at_line_start was true. + #[test] + fn test_squeeze_blank_preserves_content() { + let mut args = default_args(); + args.squeeze_blank = true; + let output = run_cat(b"a\n\n\nb\n\n\nc\n", &args); + assert_eq!(output, "a\n\nb\n\nc\n"); + } + + // Regression: with -ns, squeezed blank lines should not be counted. + // Bug #2: line_count was incremented before the squeeze check, + // so squeezed lines consumed line numbers. + #[test] + fn test_squeeze_blank_with_number() { + let mut args = default_args(); + args.squeeze_blank = true; + args.number = true; + let output = run_cat(b"a\n\n\n\nb\n", &args); + // GNU cat -ns output: lines 1(a), 2(blank), 3(b) + // The squeezed blank lines should not consume line numbers. + assert_eq!(output, " 1 a\n 2 \n 3 b\n"); + } + + // Another squeeze+number edge case: multiple groups of blanks + #[test] + fn test_squeeze_blank_with_number_multiple_groups() { + let mut args = default_args(); + args.squeeze_blank = true; + args.number = true; + let output = run_cat(b"a\n\n\nb\n\n\nc\n", &args); + assert_eq!( + output, + " 1 a\n 2 \n 3 b\n 4 \n 5 c\n" + ); + } + + #[test] + fn test_show_nonprinting() { + let mut args = default_args(); + args.show_nonprinting = true; + // 0x01 = ^A, 0x7f = ^? + let output = run_cat(&[0x01, b'a', 0x7f, b'\n'], &args); + assert_eq!(output, "^Aa^?\n"); + } +} From 8df8f1b94d6fdbd3432ecee1de170a2f523e1602 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sun, 29 Mar 2026 19:41:51 -0400 Subject: [PATCH 4/9] fix: rename variable Rename variable to better describe its function --- cat/src/main.rs | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/cat/src/main.rs b/cat/src/main.rs index beaa1c1..a8101a2 100644 --- a/cat/src/main.rs +++ b/cat/src/main.rs @@ -135,16 +135,23 @@ fn main() { cat(&mut stdin, &args, &mut line_count, &mut stdout, &mut stderr); } else { for filename in &args.files { - let mut file = match std::fs::File::open(filename) { - Ok(f) => f, - Err(err) => { - retval = 1; - eprintln!("cat: {}: {}", filename, err); - continue; - } - }; + if filename == "-" { + // read from stdin + let mut stdin = io::stdin().lock(); + + cat(&mut stdin, &args, &mut line_count, &mut stdout, &mut stderr); + } else { + let mut file = match std::fs::File::open(filename) { + Ok(f) => f, + Err(err) => { + retval = 1; + eprintln!("cat: {}: {}", filename, err); + continue; + } + }; - cat(&mut file, &args, &mut line_count, &mut stdout, &mut stderr); + cat(&mut file, &args, &mut line_count, &mut stdout, &mut stderr); + } } } @@ -163,7 +170,7 @@ fn cat( stderr: &mut E, ) { let mut character_count = 0; - let mut at_line_start = false; + let mut prev_line_start = false; let mut in_buffer: [u8; 8 * 8192] = [0; 8 * 8192]; let mut out_buffer: Vec = Vec::with_capacity(24 * 8192); loop { @@ -180,7 +187,7 @@ fn cat( for &byte in in_buffer[0..n_read].iter() { // Squeeze blank lines: skip before any output (including line numbers) - if byte == b'\n' && character_count == 0 && args.squeeze_blank && at_line_start { + if byte == b'\n' && character_count == 0 && args.squeeze_blank && prev_line_start { continue; } @@ -206,9 +213,9 @@ fn cat( let is_blank = character_count == 0; if is_blank { - at_line_start = true; + prev_line_start = true; } else { - at_line_start = false; + prev_line_start = false; character_count = 0; } @@ -344,7 +351,7 @@ mod tests { // Regression: squeeze_blank must not skip non-blank lines. // Bug #1: the squeeze check inside the character_count > 0 branch - // could skip content lines when at_line_start was true. + // could skip content lines when prev_line_start was true. #[test] fn test_squeeze_blank_preserves_content() { let mut args = default_args(); From 1dc2c905d6319dd2085517e713c61890bcd4ff8a Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sun, 29 Mar 2026 20:06:39 -0400 Subject: [PATCH 5/9] Hide output argument Hide the `output` argument, since it doesn't make sense for `cat` to do anything but output the raw (plain) output. --- cat/src/main.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cat/src/main.rs b/cat/src/main.rs index a8101a2..37cb535 100644 --- a/cat/src/main.rs +++ b/cat/src/main.rs @@ -100,6 +100,14 @@ fn main() { .action(ArgAction::Append) .help("the file(s) to concatenate"), ) + .mut_args(|a| { + // Hide the base --output argument, since it doesn't make sense for `cat`. + if a.get_id() == "output" { + a.hide(true) + } else { + a + } + }) .get_matches(); let mut args = Args::from_matches(&matches); From 3c5dbd0c399d4522b2b279f6579f83d1c7f0c6a0 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sun, 29 Mar 2026 20:08:11 -0400 Subject: [PATCH 6/9] fix: more variable clarity --- cat/Cargo.toml | 3 --- cat/src/main.rs | 10 +++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/cat/Cargo.toml b/cat/Cargo.toml index bad908f..ec8b7f4 100644 --- a/cat/Cargo.toml +++ b/cat/Cargo.toml @@ -8,6 +8,3 @@ edition = "2024" [dependencies] clap = { workspace = true } coreutils = { workspace = true } -tabled = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } diff --git a/cat/src/main.rs b/cat/src/main.rs index 37cb535..e1c1a32 100644 --- a/cat/src/main.rs +++ b/cat/src/main.rs @@ -178,7 +178,7 @@ fn cat( stderr: &mut E, ) { let mut character_count = 0; - let mut prev_line_start = false; + let mut prev_line_blank = false; let mut in_buffer: [u8; 8 * 8192] = [0; 8 * 8192]; let mut out_buffer: Vec = Vec::with_capacity(24 * 8192); loop { @@ -195,7 +195,7 @@ fn cat( for &byte in in_buffer[0..n_read].iter() { // Squeeze blank lines: skip before any output (including line numbers) - if byte == b'\n' && character_count == 0 && args.squeeze_blank && prev_line_start { + if byte == b'\n' && character_count == 0 && args.squeeze_blank && prev_line_blank { continue; } @@ -221,9 +221,9 @@ fn cat( let is_blank = character_count == 0; if is_blank { - prev_line_start = true; + prev_line_blank = true; } else { - prev_line_start = false; + prev_line_blank = false; character_count = 0; } @@ -359,7 +359,7 @@ mod tests { // Regression: squeeze_blank must not skip non-blank lines. // Bug #1: the squeeze check inside the character_count > 0 branch - // could skip content lines when prev_line_start was true. + // could skip content lines when prev_line_blank was true. #[test] fn test_squeeze_blank_preserves_content() { let mut args = default_args(); From 7a8d05498bce4380c3e24ffafaa242d36f408f12 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sun, 29 Mar 2026 20:18:32 -0400 Subject: [PATCH 7/9] chore: Add tests + bug fix Add more tests, which also uncovered a bug where byte 255 with -v was outputting raw M-\x7F instead of M-^?. --- cat/src/main.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/cat/src/main.rs b/cat/src/main.rs index e1c1a32..c61da00 100644 --- a/cat/src/main.rs +++ b/cat/src/main.rs @@ -259,7 +259,7 @@ fn cat( } character_count += 1; } - _ => { + 160..=254 => { if args.show_nonprinting { out_buffer.write_all(b"M-").unwrap(); out_buffer.write_all(&[byte - 128]).unwrap(); @@ -268,6 +268,14 @@ fn cat( } character_count += 1; } + 255 => { + if args.show_nonprinting { + out_buffer.write_all(b"M-^?").unwrap(); + } else { + out_buffer.write_all(&[byte]).unwrap(); + } + character_count += 1; + } }; } stdout.write_all(&out_buffer).unwrap(); @@ -403,4 +411,55 @@ mod tests { let output = run_cat(&[0x01, b'a', 0x7f, b'\n'], &args); assert_eq!(output, "^Aa^?\n"); } + + #[test] + fn test_empty_input() { + let output = run_cat(b"", &default_args()); + assert_eq!(output, ""); + } + + #[test] + fn test_no_trailing_newline() { + let output = run_cat(b"hello", &default_args()); + assert_eq!(output, "hello"); + } + + #[test] + fn test_squeeze_blank_with_number_nonblank() { + let mut args = default_args(); + args.squeeze_blank = true; + args.number_nonblank = true; + let output = run_cat(b"a\n\n\n\nb\n\n\nc\n", &args); + assert_eq!(output, " 1 a\n\n 2 b\n\n 3 c\n"); + } + + #[test] + fn test_show_nonprinting_high_bytes() { + let mut args = default_args(); + args.show_nonprinting = true; + // 128 (0x80) = M-^@, 159 (0x9f) = M-^_, 160 (0xa0) = M- , 255 (0xff) = M-^? + let output = run_cat(&[128, 159, 160, 255, b'\n'], &args); + assert_eq!(output, "M-^@M-^_M- M-^?\n"); + } + + #[test] + fn test_line_count_persists_across_files() { + let mut args = default_args(); + args.number = true; + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + let mut line_count: usize = 1; + + let mut file1 = Cursor::new(b"a\nb\n" as &[u8]); + cat(&mut file1, &args, &mut line_count, &mut stdout, &mut stderr); + + let mut file2 = Cursor::new(b"c\nd\n" as &[u8]); + cat(&mut file2, &args, &mut line_count, &mut stdout, &mut stderr); + + let output = String::from_utf8(stdout).unwrap(); + assert_eq!( + output, + " 1 a\n 2 b\n 3 c\n 4 d\n" + ); + } } From de3fb4b5061b5bd1b124bb9aee13c3b287116b9d Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sun, 29 Mar 2026 20:21:11 -0400 Subject: [PATCH 8/9] chore: fmt --- cat/src/main.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cat/src/main.rs b/cat/src/main.rs index c61da00..e18551c 100644 --- a/cat/src/main.rs +++ b/cat/src/main.rs @@ -457,9 +457,6 @@ mod tests { cat(&mut file2, &args, &mut line_count, &mut stdout, &mut stderr); let output = String::from_utf8(stdout).unwrap(); - assert_eq!( - output, - " 1 a\n 2 b\n 3 c\n 4 d\n" - ); + assert_eq!(output, " 1 a\n 2 b\n 3 c\n 4 d\n"); } } From e80dfd8cec009d7c3bdf8545167c56db460ab89e Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sun, 29 Mar 2026 20:22:08 -0400 Subject: [PATCH 9/9] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 97d415c..fe6525e 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ This began as, and continues to be, a learning exercise to better understand the | base32 | :white_check_mark: | | base64 | :white_check_mark: | | basename | :white_check_mark: | -| cat | :white_large_square: | +| cat | :white_check_mark: | | chcon | :white_large_square: | | chgrp | :white_large_square: | | chmod | :white_large_square: |