diff --git a/README.md b/README.md index 3d998c1..b14ded0 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ code between applets, making for an overall smaller binary. - sync - true - unlink +- wc - which - whoami - yes diff --git a/src/args/mod.rs b/src/args/mod.rs index 9d9d14a..338905a 100644 --- a/src/args/mod.rs +++ b/src/args/mod.rs @@ -1,3 +1,5 @@ +//! Common arguments to be re-used across multiple commands. Using these args +//! instead of implementing from scratch increases consistency across applets use clap::{Arg, ArgAction}; pub fn verbose() -> Arg { diff --git a/src/bitflags/mod.rs b/src/bitflags/mod.rs new file mode 100644 index 0000000..79a4a0b --- /dev/null +++ b/src/bitflags/mod.rs @@ -0,0 +1,62 @@ +//! Minimal bitflags type implementation allowing to check if a set of flags +//! contains a specific flag. The type must implement Copy and Bitand with u32. +use core::ops::BitAnd; + +pub trait BitFlags { + fn contains(&self, rhs: T) -> bool; +} + +impl BitFlags for U +where + U: BitAnd + Copy, + T: BitAnd, + >::Output: PartialEq, +{ + fn contains(&self, rhs: T) -> bool { + *self & rhs != 0 + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[derive(Clone, Copy)] + enum Flags { + A = 0b1, + B = 0b10, + C = 0b100, + } + + impl BitAnd for u32 { + type Output = u32; + + fn bitand(self, rhs: Flags) -> Self::Output { + self & rhs as u32 + } + } + + impl BitAnd for Flags { + type Output = u32; + + fn bitand(self, rhs: u32) -> Self::Output { + self as u32 & rhs + } + } + + impl BitAnd for Flags { + type Output = u32; + + fn bitand(self, rhs: Self) -> Self::Output { + self as u32 & rhs as u32 + } + } + + #[test] + fn contains() { + let num = 0b101; + assert!(num.contains(Flags::A)); + assert!(!num.contains(Flags::B)); + assert!(num.contains(Flags::C)); + } +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 9d11db0..8ddb803 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -45,6 +45,7 @@ mod sleep; mod sync; mod r#true; mod unlink; +mod wc; mod which; mod whoami; mod yes; @@ -104,6 +105,7 @@ pub fn get(name: &str) -> Option> { "sync" => Some(Box::new(sync::Sync::default())), "true" => Some(Box::new(r#true::True::default())), "unlink" => Some(Box::new(unlink::Unlink::default())), + "wc" => Some(Box::new(wc::Wc::default())), "which" => Some(Box::new(which::Which::default())), "whoami" => Some(Box::new(whoami::Whoami::default())), "yes" => Some(Box::new(yes::Yes::default())), @@ -111,7 +113,7 @@ pub fn get(name: &str) -> Option> { } } -pub static COMMANDS: [&str; 40] = [ +pub static COMMANDS: [&str; 41] = [ "base32", "base64", "basename", @@ -149,6 +151,7 @@ pub static COMMANDS: [&str; 40] = [ "sync", "true", "unlink", + "wc", "which", "whoami", "yes", diff --git a/src/cmd/wc/mod.rs b/src/cmd/wc/mod.rs new file mode 100644 index 0000000..fd197dd --- /dev/null +++ b/src/cmd/wc/mod.rs @@ -0,0 +1,246 @@ +use super::Cmd; +use crate::bitflags::BitFlags; +use clap::{Arg, ArgMatches, Command, ArgAction}; +use std::{ + cmp, + error::Error, + fmt::{self, Write}, + fs, + io::{self, stdin, Read}, + ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, AddAssign}, + str::FromStr, +}; + +#[derive(Debug, Default)] +pub struct Wc; + +impl Cmd for Wc { + fn cli(&self) -> clap::Command { + Command::new("wc") + .version(env!("CARGO_PKG_VERSION")) + .author("Nathan Fisher") + .about("Print newline, word, and byte counts for each file") + .args([ + Arg::new("INPUT") + .help("The input file to use") + .num_args(1..), + Arg::new("BYTES") + .help("Print the byte counts") + .short('c') + .long("bytes") + .action(ArgAction::SetTrue), + Arg::new("CHARS") + .help("Print the character counts") + .short('m') + .long("chars") + .action(ArgAction::SetTrue), + Arg::new("LINES") + .help("Print the line counts") + .short('l') + .long("lines") + .action(ArgAction::SetTrue), + Arg::new("MAX") + .help("Print the maximum display width") + .short('L') + .long("max-line-length") + .action(ArgAction::SetTrue), + Arg::new("WORDS") + .help("Print the word counts") + .short('w') + .long("words") + .action(ArgAction::SetTrue), + ]) + } + + fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box> { + let Some(matches) = matches else { + return Err(io::Error::new(io::ErrorKind::Other, "no input").into()); + }; + let mut flags = 0; + for arg in &["LINES", "WORDS", "CHARS", "BYTES", "MAX"] { + if matches.get_flag(arg) { + flags |= arg.parse::()?; + } + } + if flags == 0 { + flags = 0b1011; + } + let files: Vec<_> = match matches.get_many::("INPUT") { + Some(c) => c.map(|x| x.to_owned()).collect(), + None => vec!["-".to_string()], + }; + let mut totals = Values { + name: "Total", + ..Values::default() + }; + files.iter().try_for_each(|f| get_values(f, &mut totals, flags))?; + if files.len() > 1 { + totals.print_values(flags)?; + } + Ok(()) + } + + fn path(&self) -> Option { + Some(crate::Path::UsrBin) + } +} + +fn get_values<'a>(file: &'a str, totals: &mut Values<'a>, flags: u32) -> Result<(), Box> { + let contents = if file == "-" { + let mut buf = String::new(); + stdin().read_to_string(&mut buf)?; + buf + } else { + fs::read_to_string(file)? + }; + let mut f = Values { + name: file, + ..Values::default() + }; + if flags.contains(Flags::Lines) { + f.lines = contents.lines().count(); + } + if flags.contains(Flags::Words) { + f.words = contents.split_whitespace().count(); + } + if flags.contains(Flags::Chars) { + f.chars = contents.chars().count(); + } + if flags.contains(Flags::Bytes) { + f.bytes = contents.len(); + } + if flags.contains(Flags::Max) { + f.max = 0; + contents.lines().into_iter().for_each(|line| { + let max = line.chars().count(); + f.max = cmp::max(max, f.max); + }); + } + *totals += f; + f.print_values(flags) +} + +#[derive(Clone, Copy, Default)] +struct Values<'a> { + name: &'a str, + lines: usize, + words: usize, + chars: usize, + bytes: usize, + max: usize, +} + +impl AddAssign for Values<'_> { + fn add_assign(&mut self, rhs: Self) { + self.lines += rhs.lines; + self.words += rhs.words; + self.chars += rhs.chars; + self.bytes += rhs.bytes; + self.max = cmp::max(self.max, rhs.max); + } +} + +impl Values<'_> { + fn print_values(&self, flags: u32) -> Result<(), Box> { + let mut line = String::new(); + if flags.contains(Flags::Lines) { + write!(line, "\t{}", self.lines)?; + } + if flags.contains(Flags::Words) { + write!(line, "\t{}", self.words)?; + } + if flags.contains(Flags::Chars) { + write!(line, "\t{}", self.chars)?; + } + if flags.contains(Flags::Bytes) { + write!(line, "\t{}", self.bytes)?; + } + if flags.contains(Flags::Max) { + write!(line, "\t{}", self.max)?; + } + if self.name != "-" { + write!(line, "\t{}", self.name).unwrap(); + } + println!("{}", line); + Ok(()) + } +} + +#[derive(Debug)] +pub struct ParseFlagsError; + +impl fmt::Display for ParseFlagsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl Error for ParseFlagsError {} + +#[derive(Clone, Copy)] +enum Flags { + Lines = 0b1, + Words = 0b10, + Chars = 0b100, + Bytes = 0b1000, + Max = 0b10000, +} + +impl FromStr for Flags { + type Err = ParseFlagsError; + + fn from_str(s: &str) -> Result { + match s { + "LINES" => Ok(Self::Lines), + "WORDS" => Ok(Self::Words), + "CHARS" => Ok(Self::Chars), + "BYTES" => Ok(Self::Bytes), + "MAX" => Ok(Self::Max), + _ => Err(ParseFlagsError), + } + } +} + +impl BitAnd for Flags { + type Output = u32; + + fn bitand(self, rhs: u32) -> Self::Output { + self as u32 & rhs + } +} + +impl BitAnd for u32 { + type Output = u32; + + fn bitand(self, rhs: Flags) -> Self::Output { + self & rhs as u32 + } +} + +impl BitAndAssign for u32 { + fn bitand_assign(&mut self, rhs: Flags) { + *self = *self & rhs; + } +} + +impl BitOr for Flags { + type Output = u32; + + fn bitor(self, rhs: Self) -> Self::Output { + self as u32 | rhs as u32 + } +} + +impl BitOr for u32 { + type Output = u32; + + fn bitor(self, rhs: Flags) -> Self::Output { + self | rhs as u32 + } +} + +impl BitOrAssign for u32 { + fn bitor_assign(&mut self, rhs: Flags) { + *self = *self | rhs; + } +} diff --git a/src/lib.rs b/src/lib.rs index 33e682f..6823b71 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ use std::{env, path::PathBuf, process, string::ToString}; pub mod args; +pub mod bitflags; mod cmd; pub mod fs; pub mod math;