Add fold utility

This commit is contained in:
Nathan Fisher 2023-01-08 11:40:08 -05:00
parent 1e1e909afe
commit 0513cdef2e
5 changed files with 165 additions and 12 deletions

16
Cargo.lock generated
View File

@ -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"

View File

@ -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

View File

@ -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.

132
src/cmd/fold/mod.rs Normal file
View File

@ -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<crate::Path>,
}
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<dyn std::error::Error>> {
let Some(matches) = matches else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "no input")));
};
let files = match matches.get_many::<String>("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<crate::Path> {
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::<Vec<char>>()
.chunks(width)
.map(|c| c.iter().collect::<String>())
.collect::<Vec<String>>();
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(())
}

View File

@ -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<Box<dyn Cmd>> {
"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<Box<dyn Cmd>> {
}
}
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",