1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use std::{
7 io::{self, BufRead, Write},
8 process::ExitCode,
9};
10
11const TAB_WIDTH: usize = 8;
12
13#[derive(Parser)]
14#[command(name = "colrm", version, about = "Remove columns from a file")]
15pub struct Args {
16 pub first: Option<usize>,
18 pub last: Option<usize>,
20}
21
22pub fn run(args: Args) -> ExitCode {
23 let first = match args.first {
24 Some(0) => {
25 eprintln!("colrm: first column must be at least 1");
26 return ExitCode::FAILURE;
27 }
28 Some(f) => Some(f),
29 None => None,
30 };
31
32 let last = args.last;
33
34 if let (Some(f), Some(l)) = (first, last)
35 && l < f
36 {
37 eprintln!("colrm: last column must be >= first column");
38 return ExitCode::FAILURE;
39 }
40
41 let stdin = io::stdin();
42 let stdout = io::stdout();
43 let mut out = io::BufWriter::new(stdout.lock());
44
45 for line in stdin.lock().lines() {
46 let line = match line {
47 Ok(l) => l,
48 Err(e) => {
49 eprintln!("colrm: {e}");
50 return ExitCode::FAILURE;
51 }
52 };
53
54 if let Err(e) = process_line(&line, first, last, &mut out) {
55 eprintln!("colrm: {e}");
56 return ExitCode::FAILURE;
57 }
58 }
59
60 ExitCode::SUCCESS
61}
62
63fn process_line(
64 line: &str,
65 first: Option<usize>,
66 last: Option<usize>,
67 out: &mut impl Write,
68) -> io::Result<()> {
69 let first = match first {
70 Some(f) => f,
71 None => {
72 writeln!(out, "{line}")?;
73 return Ok(());
74 }
75 };
76
77 let mut col: usize = 1;
79
80 for ch in line.chars() {
81 if ch == '\t' {
82 let next_tab = next_tab_stop(col);
83 for c in col..next_tab {
87 if c < first || last.is_some_and(|l| c > l) {
88 out.write_all(b" ")?;
89 }
90 }
91 col = next_tab;
92 } else if ch == '\x08' {
93 if col < first || last.is_some_and(|l| col > l) {
95 write!(out, "{ch}")?;
96 }
97 col = col.saturating_sub(1);
98 } else {
99 if col < first || last.is_some_and(|l| col > l) {
100 write!(out, "{ch}")?;
101 }
102 col += 1;
103 }
104 }
105
106 writeln!(out)?;
107 Ok(())
108}
109
110fn next_tab_stop(col: usize) -> usize {
111 col + TAB_WIDTH - ((col - 1) % TAB_WIDTH)
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn run_colrm(
120 input: &str,
121 first: Option<usize>,
122 last: Option<usize>,
123 ) -> String {
124 let mut out = Vec::new();
125 process_line(input, first, last, &mut out).unwrap();
126 String::from_utf8(out).unwrap()
127 }
128
129 #[test]
130 fn no_args_passes_through() {
131 assert_eq!(run_colrm("hello world", None, None), "hello world\n");
132 }
133
134 #[test]
135 fn remove_from_column() {
136 assert_eq!(run_colrm("hello world", Some(3), None), "he\n");
137 }
138
139 #[test]
140 fn remove_range() {
141 assert_eq!(run_colrm("hello world", Some(3), Some(5)), "he world\n");
142 }
143
144 #[test]
145 fn tab_expansion_in_removed_range() {
146 assert_eq!(run_colrm("ab\tcd", Some(3), Some(8)), "abcd\n");
149 }
150
151 #[test]
152 fn tab_partial_removal() {
153 assert_eq!(run_colrm("ab\tcd", Some(4), Some(6)), "ab cd\n");
156 }
157
158 #[test]
159 fn empty_line() {
160 assert_eq!(run_colrm("", Some(1), None), "\n");
161 }
162
163 #[test]
164 fn first_beyond_line_length() {
165 assert_eq!(run_colrm("hello", Some(20), None), "hello\n");
166 }
167
168 #[test]
169 fn remove_first_column() {
170 assert_eq!(run_colrm("hello", Some(1), Some(1)), "ello\n");
171 }
172}