diff options
author | Toby Vincent <tobyv13@gmail.com> | 2022-12-01 23:46:05 -0600 |
---|---|---|
committer | Toby Vincent <tobyv13@gmail.com> | 2022-12-01 23:47:13 -0600 |
commit | 2fd31cf87160ae90cb4aa9e319360b4bfab6da40 (patch) | |
tree | 414cd6c5f73a670d3a40124a1df18a8a22dc016c | |
parent | 8e216dfe75eceab48d4c372623be25f304c1f4ba (diff) |
feat: impl reading from file/stdin and writing to stdout
-rw-r--r-- | src/cli.rs | 168 | ||||
-rw-r--r-- | src/error.rs | 32 | ||||
-rw-r--r-- | src/input.rs | 43 | ||||
-rw-r--r-- | src/lib.rs | 7 | ||||
-rw-r--r-- | src/main.rs | 11 |
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() } |