diff --git a/Cargo.lock b/Cargo.lock index 647a8cd..ad61d98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,8 +222,15 @@ dependencies = [ "libc", "num_cpus", "termcolor", + "textwrap", ] +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + [[package]] name = "strsim" version = "0.10.0" @@ -239,6 +246,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +dependencies = [ + "smawk", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index e442f1d..a228fea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,16 +6,17 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -atty = "0.2.14" -clap = "4.0.29" -clap_complete = "4.0.6" -clap_complete_nushell = "0.1.8" -clap_mangen = "0.2.5" -data-encoding = "2.3.3" +atty = "0.2" +clap = "4.0" +clap_complete = "4.0" +clap_complete_nushell = "0.1" +clap_mangen = "0.2" +data-encoding = "2.3" hostname = { version = "0.3", features = ["set"] } -libc = "0.2.139" -num_cpus = "1.15.0" -termcolor = "1.1.3" +libc = "0.2" +num_cpus = "1.15" +termcolor = "1.1" +textwrap = { version = "0.16", default-features = false, features = ["smawk"] } [profile.release] codegen-units = 1 diff --git a/README.md b/README.md index 2f91523..2f2e97c 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,12 @@ code between applets, making for an overall smaller binary. ## Scope *Shitbox* does not aim to supply an entire system of utilities, but rather a -a subset of the most common Unix shell utilities. Things which are out of scope +subset of the most common Unix shell utilities. Things which are out of scope for the project include: - Shells - Network servers - Kernel module handling utilities +- Anything requiring suid such as `su` or `sudo` The code aims to be portable across **Unix** variants, ie Linux and BSD, but not MacOS or Windows. Development occurs on Linux, so if your OS is more exotic then YMMV. diff --git a/src/cmd/fold/mod.rs b/src/cmd/fold/mod.rs new file mode 100644 index 0000000..df5b2db --- /dev/null +++ b/src/cmd/fold/mod.rs @@ -0,0 +1,132 @@ +use super::Cmd; +use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; +use std::{fs::File, io::{self, BufRead, BufReader}}; +use textwrap::{fill, wrap_algorithms::WrapAlgorithm}; + +#[derive(Debug)] +pub struct Fold { + name: &'static str, + path: Option, +} + +impl Default for Fold { + fn default() -> Self { + Self { + name: "fold", + path: Some(crate::Path::UsrBin), + } + } +} + +impl Cmd for Fold { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> Command { + Command::new(self.name) + .about("Wrap each input line to fit in specified width") + .author("Nathan Fisher") + .after_long_help("With no FILE, or when FILE is -, read standard input") + .args([ + Arg::new("FILE").help("The input file to use").num_args(0..), + Arg::new("BYTES") + .help("Count bytes rather than columns") + .short('b') + .long("bytes") + .action(ArgAction::SetTrue), + Arg::new("WORDS") + .help("Break at spaces") + .short('s') + .long("spaces") + .action(ArgAction::SetTrue), + Arg::new("OPTIMAL") + .help("Optimal fit") + .long_help( + "Uses a look ahad algorithm to avoid unnecessarily short or long lines", + ) + .short('o') + .long("optimal") + .action(ArgAction::SetTrue) + .conflicts_with_all(["BYTES", "WORDS"]), + Arg::new("WIDTH") + .help("Use width columns") + .short('w') + .long("width") + .default_value("80") + .num_args(1) + .value_parser(value_parser!(usize)), + ]) + } + + fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box> { + let Some(matches) = matches else { + return Err(Box::new(io::Error::new(io::ErrorKind::Other, "no input"))); + }; + let files = match matches.get_many::("FILE") { + Some(c) => c.map(String::to_string).collect(), + None => vec!["-".to_string()], + }; + for file in files { + if file.as_str() == "-" { + wrap_stdin(matches)?; + } else { + wrap_file(&file, matches)?; + } + } + if matches.get_flag("BYTES") { + println!(); + } + Ok(()) + } + + fn path(&self) -> Option { + self.path + } +} + +fn wrap_line(line: &str, args: &ArgMatches) { + let width = args.get_one("WIDTH").map(|x| *x).unwrap_or(80); + if args.get_flag("OPTIMAL") { + let line = line.replace('\t', " "); + let opts = textwrap::Options::new(width).wrap_algorithm(WrapAlgorithm::new_optimal_fit()); + println!("{}", fill(line.trim_end(), opts)); + } else if args.get_flag("WORDS") { + let line = line.replace('\t', " "); + let opts = textwrap::Options::new(width).wrap_algorithm(WrapAlgorithm::FirstFit); + println!("{}", fill(line.trim_end(), opts)); + } else if args.get_flag("BYTES") { + for (index, b) in line.as_bytes().iter().enumerate() { + if index % width == 0 { + println!(); + } + print!("{}", *b as char); + } + } else { + let line = line + .chars() + .collect::>() + .chunks(width) + .map(|c| c.iter().collect::()) + .collect::>(); + for line in &line { + println!("{}", line); + } + } +} + +fn wrap_stdin(args: &ArgMatches) -> Result<(), io::Error> { + for line in io::stdin().lock().lines() { + wrap_line(&line?, args); + } + Ok(()) +} + +fn wrap_file(file: &str, args: &ArgMatches) -> Result<(), io::Error> { + let fd = File::open(file)?; + let buf = BufReader::new(fd); + for line in buf.lines() { + wrap_line(&line?, args); + } + Ok(()) +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 535c3cc..9f1b559 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -14,6 +14,7 @@ pub mod dirname; pub mod echo; pub mod factor; pub mod r#false; +pub mod fold; mod getty; pub mod head; pub mod hostname; @@ -35,7 +36,7 @@ pub mod yes; pub use { self::hostname::Hostname, base32::Base32, base64::Base64, basename::Basename, - bootstrap::Bootstrap, dirname::Dirname, echo::Echo, factor::Factor, head::Head, + bootstrap::Bootstrap, dirname::Dirname, echo::Echo, factor::Factor, fold::Fold, head::Head, mountpoint::Mountpoint, nologin::Nologin, nproc::Nproc, r#false::False, r#true::True, rev::Rev, shitbox::Shitbox, sleep::Sleep, yes::Yes, }; @@ -57,6 +58,7 @@ pub fn get(name: &str) -> Option> { "echo" => Some(Box::new(Echo::default())), "factor" => Some(Box::new(Factor::default())), "false" => Some(Box::new(False::default())), + "fold" => Some(Box::new(Fold::default())), "head" => Some(Box::new(Head::default())), "mountpoint" => Some(Box::new(Mountpoint::default())), "nologin" => Some(Box::new(Nologin::default())), @@ -70,7 +72,7 @@ pub fn get(name: &str) -> Option> { } } -pub static COMMANDS: [&'static str; 18] = [ +pub static COMMANDS: [&'static str; 19] = [ "base32", "base64", "basename", @@ -79,6 +81,7 @@ pub static COMMANDS: [&'static str; 18] = [ "echo", "false", "factor", + "fold", "head", "hostname", "mountpoint",