use async_trait::async_trait;
use super::arg_parser::{OptArg, gnu_getopt};
use super::{Builtin, BuiltinHelper, Context, read_text_file, resolve_path};
use crate::error::Result;
use crate::interpreter::ExecResult;
pub struct Fold;
impl BuiltinHelper for Fold {
const NAME: &'static str = "fold";
}
const TAB_WIDTH: usize = 8;
fn adjust_column(column: usize, c: char, count_bytes: bool) -> usize {
if count_bytes {
return column + c.len_utf8();
}
match c {
'\x08' => column.saturating_sub(1),
'\r' => 0,
'\t' => column + TAB_WIDTH - column % TAB_WIDTH,
_ => column + 1,
}
}
fn fold_text(text: &str, width: usize, break_spaces: bool, count_bytes: bool, out: &mut String) {
let mut line: Vec<char> = Vec::new();
let mut column = 0usize;
for c in text.chars() {
if c == '\n' {
out.extend(line.drain(..));
out.push('\n');
column = 0;
continue;
}
loop {
column = adjust_column(column, c, count_bytes);
if column <= width {
line.push(c);
break;
}
if break_spaces && let Some(blank) = line.iter().rposition(|&b| b == ' ' || b == '\t') {
let rest = line.split_off(blank + 1);
out.extend(line.drain(..));
out.push('\n');
line = rest;
column = line
.iter()
.fold(0, |col, &ch| adjust_column(col, ch, count_bytes));
continue;
}
if line.is_empty() {
line.push(c);
break;
}
out.extend(line.drain(..));
out.push('\n');
column = 0;
}
}
out.extend(line);
}
fn normalize_args(args: &[String]) -> Vec<String> {
let mut out = Vec::with_capacity(args.len());
let mut done = false;
for a in args {
if !done && a == "--" {
done = true;
}
let digits = a.strip_prefix('-').filter(|d| !d.is_empty());
match digits {
Some(d) if !done && d.bytes().all(|b| b.is_ascii_digit()) => {
out.push(format!("-w{d}"));
}
_ => out.push(a.clone()),
}
}
out
}
#[async_trait]
impl Builtin for Fold {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
if let Some(r) = Self::check_help(
ctx.args,
"Usage: fold [OPTION]... [FILE]...\nWrap each input line to fit in specified width.\n\n -b, --bytes\tcount bytes rather than columns\n -s, --spaces\tbreak at spaces\n -w, --width=WIDTH\tuse WIDTH columns instead of 80\n --help\tdisplay this help and exit\n --version\toutput version information and exit\n",
Some("fold (bashkit) 0.1"),
) {
return Ok(r);
}
let args = normalize_args(ctx.args);
let (opts, mut files) = match gnu_getopt(
"fold",
&args,
"bsw:",
&[
("bytes", OptArg::No, 'b'),
("spaces", OptArg::No, 's'),
("width", OptArg::Required, 'w'),
],
true,
1,
) {
Ok(v) => v,
Err(e) => return Ok(e),
};
let mut width: usize = 80;
let mut break_spaces = false;
let mut count_bytes = false;
for o in opts {
match o.key {
'b' => count_bytes = true,
's' => break_spaces = true,
_ => {
let val = o.value.unwrap_or_default();
width = match val.parse::<usize>() {
Ok(w) if w > 0 => w,
_ => {
return Ok(Self::err(format!("invalid number of columns: '{val}'"), 1));
}
};
}
}
}
if files.is_empty() {
files.push("-".to_string());
}
let mut output = String::new();
for file in &files {
if file == "-" {
let stdin = ctx.stdin.map(ToString::to_string).unwrap_or_default();
fold_text(&stdin, width, break_spaces, count_bytes, &mut output);
continue;
}
let path = resolve_path(ctx.cwd, file);
match read_text_file(ctx.fs.as_ref(), &path, "fold").await {
Ok(text) => fold_text(&text, width, break_spaces, count_bytes, &mut output),
Err(_) => {
return Ok(Self::err_path(file, "No such file or directory", 1));
}
}
}
Ok(ExecResult::ok(output))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fs::InMemoryFs;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
async fn run_fold(args: &[&str], stdin: Option<&str>) -> ExecResult {
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/");
let fs = Arc::new(InMemoryFs::new()) as Arc<dyn crate::fs::FileSystem>;
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs,
stdin: crate::builtins::test_stream_opt(stdin),
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
Fold.execute(ctx).await.expect("fold failed")
}
#[tokio::test]
async fn test_fold_default_width() {
let long = "a".repeat(100);
let result = run_fold(&[], Some(&long)).await;
assert_eq!(result.exit_code, 0);
let lines: Vec<&str> = result.stdout.lines().collect();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].len(), 80);
assert_eq!(lines[1].len(), 20);
}
#[tokio::test]
async fn test_fold_custom_width() {
let result = run_fold(&["-w", "10"], Some("abcdefghijklmno")).await;
assert_eq!(result.exit_code, 0);
let lines: Vec<&str> = result.stdout.lines().collect();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0], "abcdefghij");
assert_eq!(lines[1], "klmno");
}
#[tokio::test]
async fn test_fold_break_at_spaces() {
let result = run_fold(&["-w", "15", "-s"], Some("hello world this is a test")).await;
assert_eq!(result.exit_code, 0);
for line in result.stdout.lines() {
assert!(line.len() <= 15, "Line too long: '{}'", line);
}
}
#[tokio::test]
async fn test_fold_short_line_unchanged() {
let result = run_fold(&["-w", "80"], Some("short line\n")).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "short line\n");
}
#[tokio::test]
async fn test_fold_unknown_option() {
let result = run_fold(&["-Q"], Some("hello")).await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("invalid option -- 'Q'"));
}
#[tokio::test]
async fn test_fold_empty_input() {
let result = run_fold(&[], Some("")).await;
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn test_fold_tab_counts_to_next_stop() {
let result = run_fold(&["-w", "4"], Some("\tab\n")).await;
assert_eq!(result.stdout, "\t\nab\n");
}
#[tokio::test]
async fn test_fold_backspace_moves_left() {
let result = run_fold(&["-w", "3"], Some("\x08abcde\n")).await;
assert_eq!(result.stdout, "\x08abc\nde\n");
}
#[tokio::test]
async fn test_fold_spaces_breaks_after_blank() {
let result = run_fold(&["-s", "-w", "6"], Some("ab cd efgh\n")).await;
assert_eq!(result.stdout, "ab cd \nefgh\n");
}
#[tokio::test]
async fn test_fold_obsolete_width() {
let result = run_fold(&["-3"], Some("abcdef\n")).await;
assert_eq!(result.stdout, "abc\ndef\n");
}
#[tokio::test]
async fn test_fold_invalid_width() {
let result = run_fold(&["-w", "0"], Some("a\n")).await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("invalid number of columns: '0'"));
}
}