use clap::{crate_version, Arg, ArgAction, Command};
use std::fs::File;
use std::io::{stdin, BufRead, BufReader, Read};
use std::path::Path;
use uucore::display::Quotable;
use uucore::error::{FromIo, UResult, USimpleError};
use uucore::{format_usage, help_about, help_usage};
const TAB_WIDTH: usize = 8;
const USAGE: &str = help_usage!("fold.md");
const ABOUT: &str = help_about!("fold.md");
mod options {
pub const BYTES: &str = "bytes";
pub const SPACES: &str = "spaces";
pub const WIDTH: &str = "width";
pub const FILE: &str = "file";
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = args.collect_lossy();
let (args, obs_width) = handle_obsolete(&args[..]);
let matches = uu_app().try_get_matches_from(args)?;
let bytes = matches.get_flag(options::BYTES);
let spaces = matches.get_flag(options::SPACES);
let poss_width = match matches.get_one::<String>(options::WIDTH) {
Some(v) => Some(v.clone()),
None => obs_width,
};
let width = match poss_width {
Some(inp_width) => inp_width.parse::<usize>().map_err(|e| {
USimpleError::new(
1,
format!("illegal width value ({}): {}", inp_width.quote(), e),
)
})?,
None => 80,
};
let files = match matches.get_many::<String>(options::FILE) {
Some(v) => v.cloned().collect(),
None => vec!["-".to_owned()],
};
fold(&files, bytes, spaces, width)
}
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.override_usage(format_usage(USAGE))
.about(ABOUT)
.infer_long_args(true)
.arg(
Arg::new(options::BYTES)
.long(options::BYTES)
.short('b')
.help(
"count using bytes rather than columns (meaning control characters \
such as newline are not treated specially)",
)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::SPACES)
.long(options::SPACES)
.short('s')
.help("break lines at word boundaries rather than a hard cut-off")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::WIDTH)
.long(options::WIDTH)
.short('w')
.help("set WIDTH as the maximum line width rather than 80")
.value_name("WIDTH")
.allow_hyphen_values(true),
)
.arg(
Arg::new(options::FILE)
.hide(true)
.action(ArgAction::Append)
.value_hint(clap::ValueHint::FilePath),
)
}
fn handle_obsolete(args: &[String]) -> (Vec<String>, Option<String>) {
for (i, arg) in args.iter().enumerate() {
let slice = &arg;
if slice.starts_with('-') && slice.chars().nth(1).map_or(false, |c| c.is_ascii_digit()) {
let mut v = args.to_vec();
v.remove(i);
return (v, Some(slice[1..].to_owned()));
}
}
(args.to_vec(), None)
}
fn fold(filenames: &[String], bytes: bool, spaces: bool, width: usize) -> UResult<()> {
for filename in filenames {
let filename: &str = filename;
let mut stdin_buf;
let mut file_buf;
let buffer = BufReader::new(if filename == "-" {
stdin_buf = stdin();
&mut stdin_buf as &mut dyn Read
} else {
file_buf = File::open(Path::new(filename)).map_err_context(|| filename.to_string())?;
&mut file_buf as &mut dyn Read
});
if bytes {
fold_file_bytewise(buffer, spaces, width)?;
} else {
fold_file(buffer, spaces, width)?;
}
}
Ok(())
}
fn fold_file_bytewise<T: Read>(mut file: BufReader<T>, spaces: bool, width: usize) -> UResult<()> {
let mut line = String::new();
loop {
if file
.read_line(&mut line)
.map_err_context(|| "failed to read line".to_string())?
== 0
{
break;
}
if line == "\n" {
println!();
line.truncate(0);
continue;
}
let len = line.len();
let mut i = 0;
while i < len {
let width = if len - i >= width { width } else { len - i };
let slice = {
let slice = &line[i..i + width];
if spaces && i + width < len {
match slice.rfind(|c: char| c.is_whitespace() && c != '\r') {
Some(m) => &slice[..=m],
None => slice,
}
} else {
slice
}
};
if slice == "\n" {
break;
}
i += slice.len();
let at_eol = i >= len;
if at_eol {
print!("{slice}");
} else {
println!("{slice}");
}
}
line.truncate(0);
}
Ok(())
}
#[allow(unused_assignments)]
#[allow(clippy::cognitive_complexity)]
fn fold_file<T: Read>(mut file: BufReader<T>, spaces: bool, width: usize) -> UResult<()> {
let mut line = String::new();
let mut output = String::new();
let mut col_count = 0;
let mut last_space = None;
macro_rules! emit_output {
() => {
let consume = match last_space {
Some(i) => i + 1,
None => output.len(),
};
println!("{}", &output[..consume]);
output.replace_range(..consume, "");
col_count = output.len();
last_space = None;
};
}
loop {
if file
.read_line(&mut line)
.map_err_context(|| "failed to read line".to_string())?
== 0
{
break;
}
for ch in line.chars() {
if ch == '\n' {
last_space = None;
emit_output!();
break;
}
if col_count >= width {
emit_output!();
}
match ch {
'\r' => col_count = 0,
'\t' => {
let next_tab_stop = col_count + TAB_WIDTH - col_count % TAB_WIDTH;
if next_tab_stop > width && !output.is_empty() {
emit_output!();
}
col_count = next_tab_stop;
last_space = if spaces { Some(output.len()) } else { None };
}
'\x08' => {
col_count = col_count.saturating_sub(1);
}
_ if spaces && ch.is_whitespace() => {
last_space = Some(output.len());
col_count += 1;
}
_ => col_count += 1,
};
output.push(ch);
}
if !output.is_empty() {
print!("{output}");
output.truncate(0);
}
line.truncate(0);
}
Ok(())
}