use std::io::{self, BufWriter, Write};
#[cfg(unix)]
use std::mem::ManuallyDrop;
#[cfg(unix)]
use std::os::unix::io::FromRawFd;
use std::path::Path;
use std::process;
use coreutils_rs::common::io::{FileData, MmapHints, read_file_with_hints, read_stdin};
use coreutils_rs::common::io_error_msg;
use coreutils_rs::fmt::{FmtConfig, fmt_data};
struct Cli {
width: usize,
goal: Option<usize>,
split_only: bool,
crown_margin: bool,
tagged: bool,
uniform_spacing: bool,
prefix: Option<String>,
files: Vec<String>,
}
fn parse_args() -> Cli {
let mut cli = Cli {
width: 75,
goal: None,
split_only: false,
crown_margin: false,
tagged: false,
uniform_spacing: false,
prefix: None,
files: Vec::new(),
};
let mut args = std::env::args_os().skip(1);
#[allow(clippy::while_let_on_iterator)]
while let Some(arg) = args.next() {
let bytes = arg.as_encoded_bytes();
if bytes == b"--" {
for a in args {
cli.files.push(a.to_string_lossy().into_owned());
}
break;
}
if bytes.starts_with(b"--") {
if bytes.starts_with(b"--width=") {
let val = arg.to_string_lossy();
match val[8..].parse::<usize>() {
Ok(w) => cli.width = w,
Err(_) => {
eprintln!("fmt: invalid width: '{}'", &val[8..]);
process::exit(1);
}
}
continue;
}
if bytes.starts_with(b"--goal=") {
let val = arg.to_string_lossy();
match val[7..].parse::<usize>() {
Ok(g) => cli.goal = Some(g),
Err(_) => {
eprintln!("fmt: invalid goal width: '{}'", &val[7..]);
process::exit(1);
}
}
continue;
}
if bytes.starts_with(b"--prefix=") {
let val = arg.to_string_lossy();
cli.prefix = Some(val[9..].to_string());
continue;
}
match bytes {
b"--crown-margin" => cli.crown_margin = true,
b"--split-only" => cli.split_only = true,
b"--tagged-paragraph" => cli.tagged = true,
b"--uniform-spacing" => cli.uniform_spacing = true,
b"--width" => {
let val = args
.next()
.unwrap_or_else(|| {
eprintln!("fmt: option '--width' requires an argument");
process::exit(1);
})
.to_string_lossy()
.into_owned();
match val.parse::<usize>() {
Ok(w) => cli.width = w,
Err(_) => {
eprintln!("fmt: invalid width: '{}'", val);
process::exit(1);
}
}
}
b"--goal" => {
let val = args
.next()
.unwrap_or_else(|| {
eprintln!("fmt: option '--goal' requires an argument");
process::exit(1);
})
.to_string_lossy()
.into_owned();
match val.parse::<usize>() {
Ok(g) => cli.goal = Some(g),
Err(_) => {
eprintln!("fmt: invalid goal width: '{}'", val);
process::exit(1);
}
}
}
b"--prefix" => {
let val = args
.next()
.unwrap_or_else(|| {
eprintln!("fmt: option '--prefix' requires an argument");
process::exit(1);
})
.to_string_lossy()
.into_owned();
cli.prefix = Some(val);
}
b"--help" => {
print!(
"Usage: fmt [-WIDTH] [OPTION]... [FILE]...\n\
Reformat each paragraph in the FILE(s), writing to standard output.\n\
The option -WIDTH is an abbreviated form of --width=DIGITS.\n\n\
With no FILE, or when FILE is -, read standard input.\n\n\
Mandatory arguments to long options are mandatory for short options too.\n\
\x20 -c, --crown-margin preserve indentation of first two lines\n\
\x20 -p, --prefix=STRING reformat only lines beginning with STRING,\n\
\x20 reattaching the prefix to reformatted lines\n\
\x20 -s, --split-only split long lines, but do not refill\n\
\x20 -t, --tagged-paragraph indentation of first line different from second\n\
\x20 -u, --uniform-spacing one space between words, two after sentences\n\
\x20 -w, --width=WIDTH maximum line width (default of 75 columns)\n\
\x20 -g, --goal=WIDTH goal width (default of 93% of width)\n\
\x20 --help display this help and exit\n\
\x20 --version output version information and exit\n"
);
process::exit(0);
}
b"--version" => {
println!("fmt (fcoreutils) {}", env!("CARGO_PKG_VERSION"));
process::exit(0);
}
_ => {
eprintln!("fmt: unrecognized option '{}'", arg.to_string_lossy());
eprintln!("Try 'fmt --help' for more information.");
process::exit(1);
}
}
} else if bytes.len() > 1 && bytes[0] == b'-' {
let mut i = 1;
while i < bytes.len() {
match bytes[i] {
b'c' => cli.crown_margin = true,
b's' => cli.split_only = true,
b't' => cli.tagged = true,
b'u' => cli.uniform_spacing = true,
b'w' => {
if i + 1 < bytes.len() {
let val = arg.to_string_lossy();
match val[i + 1..].parse::<usize>() {
Ok(w) => cli.width = w,
Err(_) => {
eprintln!("fmt: invalid width: '{}'", &val[i + 1..]);
process::exit(1);
}
}
} else {
let val = args
.next()
.unwrap_or_else(|| {
eprintln!("fmt: option requires an argument -- 'w'");
process::exit(1);
})
.to_string_lossy()
.into_owned();
match val.parse::<usize>() {
Ok(w) => cli.width = w,
Err(_) => {
eprintln!("fmt: invalid width: '{}'", val);
process::exit(1);
}
}
}
break;
}
b'g' => {
if i + 1 < bytes.len() {
let val = arg.to_string_lossy();
match val[i + 1..].parse::<usize>() {
Ok(g) => cli.goal = Some(g),
Err(_) => {
eprintln!("fmt: invalid goal width: '{}'", &val[i + 1..]);
process::exit(1);
}
}
} else {
let val = args
.next()
.unwrap_or_else(|| {
eprintln!("fmt: option requires an argument -- 'g'");
process::exit(1);
})
.to_string_lossy()
.into_owned();
match val.parse::<usize>() {
Ok(g) => cli.goal = Some(g),
Err(_) => {
eprintln!("fmt: invalid goal width: '{}'", val);
process::exit(1);
}
}
}
break;
}
b'p' => {
if i + 1 < bytes.len() {
let val = arg.to_string_lossy();
cli.prefix = Some(val[i + 1..].to_string());
} else {
let val = args
.next()
.unwrap_or_else(|| {
eprintln!("fmt: option requires an argument -- 'p'");
process::exit(1);
})
.to_string_lossy()
.into_owned();
cli.prefix = Some(val);
}
break;
}
b'0'..=b'9' => {
let val = arg.to_string_lossy();
match val[i..].parse::<usize>() {
Ok(w) => cli.width = w,
Err(_) => {
eprintln!("fmt: invalid width: '{}'", &val[i..]);
process::exit(1);
}
}
break;
}
_ => {
eprintln!("fmt: invalid option -- '{}'", bytes[i] as char);
eprintln!("Try 'fmt --help' for more information.");
process::exit(1);
}
}
i += 1;
}
} else {
cli.files.push(arg.to_string_lossy().into_owned());
}
}
cli
}
fn main() {
coreutils_rs::common::reset_sigpipe();
let cli = parse_args();
let goal = cli.goal.unwrap_or((cli.width * 187) / 200);
let config = FmtConfig {
width: cli.width,
goal,
split_only: cli.split_only,
crown_margin: cli.crown_margin,
tagged: cli.tagged,
uniform_spacing: cli.uniform_spacing,
prefix: cli.prefix,
};
let files: Vec<String> = if cli.files.is_empty() {
vec!["-".to_string()]
} else {
cli.files
};
#[cfg(unix)]
let stdout_raw = unsafe { ManuallyDrop::new(std::fs::File::from_raw_fd(1)) };
#[cfg(unix)]
let mut out = BufWriter::with_capacity(256 * 1024, &*stdout_raw);
#[cfg(not(unix))]
let stdout = io::stdout();
#[cfg(not(unix))]
let mut out = BufWriter::with_capacity(256 * 1024, stdout.lock());
let mut had_error = false;
for filename in &files {
let data: FileData = if filename == "-" {
match read_stdin() {
Ok(d) => FileData::Owned(d),
Err(e) => {
eprintln!("fmt: standard input: {}", io_error_msg(&e));
had_error = true;
continue;
}
}
} else {
match read_file_with_hints(Path::new(filename), MmapHints::Lazy) {
Ok(d) => d,
Err(e) => {
eprintln!(
"fmt: cannot open '{}' for reading: {}",
filename,
io_error_msg(&e)
);
had_error = true;
continue;
}
}
};
let result = fmt_data(&data, &mut out, &config);
if let Err(e) = result {
if e.kind() == io::ErrorKind::BrokenPipe {
process::exit(0);
}
eprintln!("fmt: write error: {}", io_error_msg(&e));
had_error = true;
}
}
if let Err(e) = out.flush()
&& e.kind() != io::ErrorKind::BrokenPipe
{
eprintln!("fmt: write error: {}", io_error_msg(&e));
had_error = true;
}
if had_error {
process::exit(1);
}
}
#[cfg(test)]
mod tests {
use std::process::Command;
fn cmd() -> Command {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("ffmt");
Command::new(path)
}
#[test]
fn test_fmt_basic() {
use std::io::Write;
use std::process::Stdio;
let mut child = cmd()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(b"hello world\n")
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
assert_eq!(output.stdout, b"hello world\n");
}
#[test]
fn test_fmt_wrap_long_line() {
use std::io::Write;
use std::process::Stdio;
let long_line = "word ".repeat(50);
let mut child = cmd()
.args(["-w", "40"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(long_line.as_bytes())
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
assert!(
line.len() <= 42,
"line too long: {} chars: '{}'",
line.len(),
line
);
}
}
#[test]
fn test_fmt_empty_input() {
use std::io::Write;
use std::process::Stdio;
let mut child = cmd()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(b"").unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
assert!(output.stdout.is_empty());
}
#[test]
fn test_fmt_preserve_paragraphs() {
use std::io::Write;
use std::process::Stdio;
let mut child = cmd()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(b"paragraph one\n\nparagraph two\n")
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("\n\n"),
"empty line between paragraphs should be preserved"
);
}
#[test]
fn test_fmt_file() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("text.txt");
std::fs::write(&file, "short line\n").unwrap();
let output = cmd().arg(file.to_str().unwrap()).output().unwrap();
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "short line");
}
#[test]
fn test_fmt_nonexistent_file() {
let output = cmd().arg("/nonexistent/file.txt").output().unwrap();
assert!(!output.status.success());
}
#[test]
fn test_fmt_width_option() {
use std::io::Write;
use std::process::Stdio;
let mut child = cmd()
.arg("--width=20")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(b"a b c d e f g h i j k l m n o p q r s\n")
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.lines().count() > 1,
"should wrap into multiple lines"
);
}
#[test]
fn test_fmt_only_whitespace() {
use std::io::Write;
use std::process::Stdio;
let mut child = cmd()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(b" \n \n")
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
}
}