Skip to content

Commit 4f1e0fb

Browse files
author
Hunter2718
committed
VERSION 1.0.0: It now does everything cat does
1 parent dd31472 commit 4f1e0fb

File tree

4 files changed

+334
-23
lines changed

4 files changed

+334
-23
lines changed

kitten/assets/helpfile.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Kitten 🐾 - A Rusty clone of the Unix 'cat' command.
2+
3+
Usage:
4+
kitten <file1> <file2> ... Read and print contents of one or more files.
5+
kitten Read from standard input (pipe data in).
6+
7+
Options:
8+
-h, --help Show this help message and exit.
9+
10+
Examples:
11+
kitten file.txt Prints file.txt to stdout.
12+
kitten file1.txt file2.txt Prints both files in order.
13+
echo "hello" | kitten Prints stdin input to stdout.
14+
15+
About:
16+
Kitten is a learning project by https://github.com/Hunter2718
17+
It's a safe and fast reimplementation of 'cat' in Rust.
18+

kitten/assets/test.txt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
3+
4+
5+
hi
6+
7+
8+
9+
hi
10+
11+
12+
ni
13+
14+
15+
BYE
16+
17+
18+
HI
19+
20+
21+
22+
23+
24+
25+
26+
27+
28+
29+
30+
31+
32+
33+
34+
35+
HI HI = hh
36+
37+
38+
39+
40+
alhjsdf
41+

kitten/assets/versionfile.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kitten 0.1.0
2+
3+
by https://github.com/Hunter2718

kitten/src/main.rs

Lines changed: 272 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,291 @@
1-
use std::env;
21
use std::fs;
3-
use std::io::{self, Error, BufRead, IsTerminal, Write};
2+
use std::io::{self, Error, BufRead, IsTerminal, ErrorKind};
43
use std::path::Path;
5-
use std::os::unix::io::AsRawFd;
4+
use std::env;
5+
6+
const HELP_FILE_PATH: &'static str = "assets/helpfile.txt";
7+
const VERSION_INFO_FILE_PATH: &'static str = "assets/versionfile.txt";
8+
9+
struct Config {
10+
show_help: bool,
11+
show_version: bool,
12+
show_line_numbers: bool,
13+
show_line_numbers_nonblank: bool,
14+
squeeze_blank: bool,
15+
show_ends: bool,
16+
show_tabs: bool,
17+
show_nonprint: bool,
18+
files: Vec<String>,
19+
}
620

721
fn main() -> Result<(), Error> {
22+
let mut output: String = String::new();
823
let args: Vec<String> = env::args().collect();
9-
10-
if args.len() < 2 {
24+
let config = parse_args(&args);
1125

12-
let stdin = io::stdin();
26+
if config.show_help {
27+
println!("{}", read_file(HELP_FILE_PATH)?);
28+
return Ok(());
29+
}
1330

14-
if stdin.is_terminal() {
15-
eprintln!("Must have a file or stdin with pipes");
16-
std::process::exit(1);
17-
}
31+
if config.show_version {
32+
println!("{}", read_file(VERSION_INFO_FILE_PATH)?);
33+
return Ok(());
34+
}
35+
36+
37+
if config.files.is_empty() {
38+
39+
output.push_str(
40+
&match read_stdin() {
41+
Err(e) => return Err(e),
42+
Ok(c) => c,
43+
}
44+
);
1845

19-
for line in stdin.lock().lines() {
20-
let line = line?;
21-
println!("{}", line);
22-
}
2346

2447
} else {
2548

26-
let mut i = 1;
49+
for file in config.files {
50+
51+
if file == "-" {
52+
output.push_str(
53+
&match read_stdin() {
54+
Err(e) => return Err(e),
55+
Ok(c) => c,
56+
}
57+
);
58+
59+
60+
} else {
61+
output.push_str(
62+
&match read_file(&file) {
63+
Err(e) => {
64+
println!("{}",
65+
match read_file(&HELP_FILE_PATH.to_string()) {
66+
Err(e) => return Err(e),
67+
Ok(c) => c,
68+
}
69+
);
70+
return Err(e);
71+
},
72+
73+
Ok(c) => c,
74+
}
75+
);
2776

28-
while i < args.len() {
2977

30-
println!("{}",
31-
match fs::read_to_string(Path::new(&args[i])) {
32-
Err(e) => return Err(e),
33-
Ok(c) => c,
34-
}
35-
);
78+
}
3679

37-
i += 1;
3880
}
3981
}
4082

83+
84+
if config.squeeze_blank {
85+
output = add_squeeze_blank(output);
86+
}
87+
88+
if config.show_tabs {
89+
output = add_show_tabs(output);
90+
}
91+
92+
if config.show_nonprint {
93+
output = add_show_nonprint(output);
94+
}
95+
96+
if config.show_ends {
97+
output = add_show_ends(output);
98+
}
99+
100+
if config.show_line_numbers {
101+
output = add_line_numbers(output);
102+
}
103+
104+
if config.show_line_numbers_nonblank {
105+
output = add_line_numbers_nonblank(output);
106+
}
107+
108+
109+
110+
print!("{}", output);
111+
41112
Ok(())
42113
}
114+
115+
116+
fn read_file(path_to_file: &str) -> Result<String, Error> {
117+
return fs::read_to_string(Path::new(path_to_file));
118+
}
119+
120+
121+
fn read_stdin() -> Result<String, Error> {
122+
let mut result: String = String::new();
123+
let stdin = io::stdin();
124+
125+
if stdin.is_terminal() {
126+
println!("{}",
127+
match read_file(HELP_FILE_PATH) {
128+
Err(e) => return Err(e),
129+
Ok(c) => c,
130+
}
131+
);
132+
return Err(Error::new(ErrorKind::Other, "Stdin is a TTY"));
133+
}
134+
135+
for line in stdin.lock().lines() {
136+
let line = line?;
137+
result.push_str(&line);
138+
result.push('\n');
139+
}
140+
141+
return Ok(result);
142+
}
143+
144+
145+
fn parse_args(args: &[String]) -> Config {
146+
let mut config = Config {
147+
show_help: false,
148+
show_version: false,
149+
show_line_numbers: false,
150+
show_line_numbers_nonblank: false,
151+
squeeze_blank: false,
152+
show_ends: false,
153+
show_tabs: false,
154+
show_nonprint: false,
155+
files: Vec::new(),
156+
};
157+
158+
for arg in args.iter().skip(1) {
159+
match arg.as_str() {
160+
"--help" => config.show_help = true,
161+
"--version" => config.show_version = true,
162+
"-n" | "--number" => config.show_line_numbers = true,
163+
"-b" | "--number-nonblank" => config.show_line_numbers_nonblank = true,
164+
"-s" | "--squeeze-blank" => config.squeeze_blank = true,
165+
"-E" | "--show-ends" => config.show_ends = true,
166+
"-T" | "--show-tabs" => config.show_tabs = true,
167+
"-v" | "--show-nonprinting" => config.show_nonprint = true,
168+
"-A" | "--show-all" => {
169+
config.show_nonprint = true;
170+
config.show_ends = true;
171+
config.show_tabs = true;
172+
},
173+
"-e" => {
174+
config.show_nonprint = true;
175+
config.show_ends = true;
176+
},
177+
"-t" => {
178+
config.show_nonprint = true;
179+
config.show_tabs = true;
180+
},
181+
"-" => config.files.push(arg.to_string()),
182+
_ if arg.starts_with('-') => {
183+
config.show_help = true;
184+
break;
185+
}
186+
_ => config.files.push(arg.to_string()),
187+
}
188+
}
189+
190+
if config.show_line_numbers_nonblank && config.show_line_numbers {
191+
config.show_line_numbers = false;
192+
}
193+
194+
config
195+
}
196+
197+
198+
fn add_line_numbers(input: String) -> String {
199+
let mut result = String::new();
200+
for (i, line) in input.lines().enumerate() {
201+
result.push_str(&format!("{:>6} {}\n", i + 1, line));
202+
}
203+
result
204+
}
205+
206+
207+
fn add_line_numbers_nonblank(input: String) -> String {
208+
let mut result = String::new();
209+
let mut line_number = 1;
210+
211+
for line in input.lines() {
212+
if line.is_empty() {
213+
result.push_str("\n");
214+
continue;
215+
}
216+
217+
result.push_str(&format!("{:>6} {}\n", line_number, line));
218+
line_number += 1;
219+
}
220+
result
221+
}
222+
223+
224+
fn add_squeeze_blank(input: String) -> String {
225+
let mut result = String::new();
226+
let mut previous_blank = false;
227+
228+
for line in input.lines() {
229+
if line.trim().is_empty() {
230+
if previous_blank {
231+
continue;
232+
} else {
233+
result.push_str("\n");
234+
previous_blank = true;
235+
}
236+
237+
} else {
238+
result.push_str(&format!("{}\n", line));
239+
previous_blank = false;
240+
}
241+
}
242+
243+
result
244+
}
245+
246+
247+
fn add_show_ends(input: String) -> String {
248+
let mut result = String::new();
249+
for line in input.lines() {
250+
result.push_str(&format!("{}$\n", line));
251+
}
252+
result
253+
}
254+
255+
256+
fn add_show_tabs(input: String) -> String {
257+
let mut result = String::new();
258+
for c in input.chars() {
259+
if c == '\t' {
260+
result.push_str("^I");
261+
} else {
262+
result.push(c);
263+
}
264+
}
265+
result
266+
}
267+
268+
269+
fn add_show_nonprint(input: String) -> String {
270+
let mut result = String::new();
271+
272+
for c in input.chars() {
273+
if c == '\t' || c == '\n' {
274+
result.push(c);
275+
} else {
276+
let ascii = c as u32;
277+
278+
if ascii < 32 {
279+
result.push('^');
280+
result.push((ascii + 64) as u8 as char);
281+
} else if ascii == 127 {
282+
result.push('^');
283+
result.push('?');
284+
} else {
285+
result.push(c);
286+
}
287+
}
288+
}
289+
290+
result
291+
}

0 commit comments

Comments
 (0)