summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorToby Vincent <tobyv13@gmail.com>2022-12-01 23:46:05 -0600
committerToby Vincent <tobyv13@gmail.com>2022-12-01 23:47:13 -0600
commit2fd31cf87160ae90cb4aa9e319360b4bfab6da40 (patch)
tree414cd6c5f73a670d3a40124a1df18a8a22dc016c
parent8e216dfe75eceab48d4c372623be25f304c1f4ba (diff)
feat: impl reading from file/stdin and writing to stdout
-rw-r--r--src/cli.rs168
-rw-r--r--src/error.rs32
-rw-r--r--src/input.rs43
-rw-r--r--src/lib.rs7
-rw-r--r--src/main.rs11
5 files changed, 259 insertions, 2 deletions
diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..d47c1e1
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,168 @@
+use std::{
+ env,
+ io::{Read, Write},
+};
+
+use crate::{Error, Input, Result};
+
+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."#;
+
+#[derive(Debug, Default)]
+pub struct Cli {
+ files: Vec<Input>,
+ opts: Opts,
+}
+
+impl Cli {
+ pub fn parse() -> Result<Self> {
+ env::args()
+ .skip(1)
+ .try_fold(Cli::default(), |mut cli, opt| {
+ match opt.as_str() {
+ "-" => cli.files.push(Input::Stdin),
+ o if o.starts_with("--") => cli.opts.parse_long(o)?,
+ o if o.starts_with('-') => cli.opts.parse_short(o)?,
+ s => cli.files.push(s.into()),
+ };
+ Ok(cli)
+ })
+ }
+
+ pub fn run(mut self) -> Result<()> {
+ if self.opts.help {
+ println!("{}", HELP);
+ std::process::exit(0);
+ }
+ if self.opts.version {
+ println!("cat (ported to Rust) {}", env!("CARGO_PKG_VERSION"));
+ std::process::exit(0);
+ }
+
+ if self.files.is_empty() {
+ self.files.push(Input::Stdin);
+ }
+
+ let stdout = std::io::stdout();
+ let mut writer = stdout.lock();
+
+ for file in self.files.into_iter() {
+ let reader = file.lock()?;
+
+ let mut newline = true;
+ let mut line_nr = 0;
+
+ for byte in reader.bytes() {
+ let mut buf = vec![];
+
+ if newline && self.opts.show_ends {
+ buf.push(b'$');
+ newline = false;
+ }
+
+ match byte? as char {
+ '\n' if newline && self.opts.squeeze_blank => {
+ newline = true;
+ continue;
+ }
+ c @ '\n' => {
+ newline = true;
+ buf.push(c as u8)
+ }
+ '\t' if self.opts.show_tabs => buf.extend_from_slice(&[b'^', b'I']),
+ c => buf.push(c as u8),
+ };
+
+ if newline && self.opts.number {
+ line_nr += 1;
+ let fmt_str = format!("\t{} ", line_nr);
+ buf.extend_from_slice(fmt_str.as_bytes());
+ }
+
+ if writer.write(&buf).is_err() {
+ break;
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
+
+#[derive(Debug, Default)]
+pub struct Opts {
+ /// number nonempty output lines, overrides -n
+ number_nonblank: bool,
+
+ /// display $ at end of each line
+ show_ends: bool,
+
+ /// number all output lines
+ number: bool,
+
+ /// suppress repeated empty output lines
+ squeeze_blank: bool,
+
+ /// display TAB characters as ^I
+ show_tabs: bool,
+
+ /// use ^ and M- notation, except for LFD and TAB
+ show_nonprinting: bool,
+
+ /// display help and exit
+ help: bool,
+
+ /// output version information and exit
+ version: bool,
+}
+
+impl Opts {
+ fn parse_long(&mut self, s: &str) -> Result<()> {
+ match s {
+ "--number-nonblank" => self.number_nonblank = true,
+ "--show-ends" => self.show_ends = true,
+ "--number" => self.number = true,
+ "--squeeze-blank" => self.squeeze_blank = true,
+ "--show-tabs" => self.show_tabs = true,
+ "--show-nonprinting" => self.show_nonprinting = true,
+ "--help" => self.help = true,
+ "--version" => self.version = true,
+ s => return Err(Error::Opts(s.to_string())),
+ };
+ Ok(())
+ }
+
+ fn parse_short(&mut self, s: &str) -> Result<()> {
+ for o in s.chars().skip(1) {
+ match o {
+ 'b' => self.number_nonblank = true,
+ 'E' => self.show_ends = true,
+ 'n' => self.number = true,
+ 's' => self.squeeze_blank = true,
+ 'T' => self.show_tabs = true,
+ 'v' => self.show_nonprinting = true,
+ s => return Err(Error::Opts(s.to_string())),
+ };
+ }
+ Ok(())
+ }
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..051a8c3
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,32 @@
+pub type Result<T> = std::result::Result<T, Error>;
+
+#[derive(Debug)]
+pub enum Error {
+ Io(std::io::Error),
+ Utf8(std::str::Utf8Error),
+ Opts(String),
+}
+
+impl std::error::Error for Error {}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Error::Io(e) => write!(f, "cat: {}", e),
+ Error::Utf8(e) => write!(f, "cat: {}", e),
+ Error::Opts(s) => write!(f, "cat: unrecognized option '{}'", s),
+ }
+ }
+}
+
+impl From<std::io::Error> for Error {
+ fn from(value: std::io::Error) -> Self {
+ Error::Io(value)
+ }
+}
+
+impl From<std::str::Utf8Error> for Error {
+ fn from(value: std::str::Utf8Error) -> Self {
+ Error::Utf8(value)
+ }
+}
diff --git a/src/input.rs b/src/input.rs
new file mode 100644
index 0000000..01c73ef
--- /dev/null
+++ b/src/input.rs
@@ -0,0 +1,43 @@
+use std::{
+ fs::File,
+ io::{BufReader, Read, StdinLock},
+ path::PathBuf,
+};
+
+use crate::Result;
+
+#[derive(Debug)]
+pub enum Input {
+ Stdin,
+ File(PathBuf),
+}
+
+impl From<&str> for Input {
+ fn from(value: &str) -> Self {
+ Self::File(PathBuf::from(value))
+ }
+}
+
+impl Input {
+ pub(crate) fn lock(self) -> Result<InputReader<'static>> {
+ Ok(match self {
+ Input::File(p) => InputReader::File(BufReader::new(File::open(p)?)),
+ Input::Stdin => InputReader::Stdin(std::io::stdin().lock()),
+ })
+ }
+}
+
+#[derive(Debug)]
+pub enum InputReader<'a> {
+ Stdin(StdinLock<'a>),
+ File(BufReader<File>),
+}
+
+impl Read for InputReader<'_> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ match self {
+ InputReader::Stdin(s) => s.read(buf),
+ InputReader::File(p) => p.read(buf),
+ }
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..ab2ff99
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,7 @@
+pub use crate::cli::{Cli, Opts};
+pub use crate::error::{Error, Result};
+pub use crate::input::Input;
+
+mod cli;
+mod error;
+mod input;
diff --git a/src/main.rs b/src/main.rs
index e7a11a9..290ddda 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,10 @@
-fn main() {
- println!("Hello, world!");
+use cat::{Cli, Result};
+
+/// I assumed I should keep this "DIY". I would most likely use some "de facto" external
+/// libraries like `clap`, `serde`, ect.
+
+fn main() -> Result<()> {
+ let cli = Cli::parse()?;
+
+ cli.run()
}