use std::fs::OpenOptions;
use std::io::Read as _;
use std::io::Write as _;
use std::path::PathBuf;
use std::process::ExitCode;
use bstr::ByteSlice as _;
use clap::Parser;
use itertools::Itertools as _;
#[cfg(unix)]
use nix::sys::signal;
#[derive(Parser, Debug)]
struct Args {
#[arg(long, default_value_t = false)]
fail: bool,
#[arg(long, default_value_t = false, conflicts_with = "fail")]
abort: bool,
#[arg(long, default_value_t = false)]
reverse: bool,
#[arg(long, default_value_t = false)]
uppercase: bool,
#[arg(long, default_value_t = false)]
lowercase: bool,
#[arg(long)]
append: Option<String>,
#[arg(long)]
stdout: Option<String>,
#[arg(long)]
stderr: Option<String>,
#[arg(long)]
tee: Option<PathBuf>,
#[arg(long, default_value_t = false)]
byte_mode: bool,
#[arg(long, value_delimiter = ',')]
line_ranges: Vec<String>,
#[arg(long, default_value_t = false, requires = "line_ranges")]
split_even_length_lines: bool,
}
#[derive(Debug)]
struct LineRange {
first: usize,
last: usize,
}
impl LineRange {
fn from_str(s: &str) -> Self {
let (first, last) = s.split_once('-').unwrap();
Self {
first: first.parse().unwrap(),
last: last.parse().unwrap(),
}
}
fn contains(&self, line_num: usize) -> bool {
line_num >= self.first && line_num <= self.last
}
}
fn split_even_length_line(line: &str) -> String {
let line_len = line.strip_suffix('\n').unwrap_or(line).len();
if line_len.is_multiple_of(2) {
let (first, second) = line.split_at(line_len / 2);
format!("{first}\n{second}")
} else {
line.to_owned()
}
}
fn main() -> ExitCode {
let args: Args = Args::parse();
if let Some(data) = args.stderr {
eprint!("{data}");
}
let stdout = if let Some(data) = args.stdout {
assert!(!args.reverse);
assert!(!args.uppercase);
assert!(!args.lowercase);
assert!(args.append.is_none());
print!("{data}");
data
} else if args.byte_mode {
assert!(!args.reverse);
assert!(args.append.is_none());
let mut stdout = vec![];
#[expect(clippy::unbuffered_bytes)]
for byte in std::io::stdin().bytes() {
let byte = byte.expect("Failed to read from stdin");
let output = if args.uppercase {
byte.to_ascii_uppercase()
} else if args.lowercase {
assert!(!args.uppercase);
byte.to_ascii_lowercase()
} else {
byte
};
stdout.push(output);
std::io::stdout()
.write_all(&[output])
.expect("Failed to write to stdout");
}
stdout
.to_str()
.expect("Output is not a valid UTF-8 string")
.to_owned()
} else {
let line_ranges = if !args.line_ranges.is_empty() {
args.line_ranges
.iter()
.map(|range| LineRange::from_str(range))
.collect_vec()
} else {
vec![LineRange {
first: 1,
last: usize::MAX,
}]
};
let mut input = vec![];
std::io::stdin()
.read_to_end(&mut input)
.expect("Failed to read from stdin");
let mut stdout = input
.lines_with_terminator()
.enumerate()
.map(|(i, line)| {
let line = line
.to_str()
.expect("The input is not valid UTF-8 string")
.to_owned();
let line_num = i + 1;
let in_range = line_ranges
.iter()
.any(|line_range| line_range.contains(line_num));
if !in_range {
return line;
}
let line = if args.reverse {
line.chars().rev().collect()
} else {
line
};
let line = if args.split_even_length_lines {
split_even_length_line(&line)
} else {
line
};
if args.uppercase {
assert!(!args.lowercase);
line.to_uppercase()
} else if args.lowercase {
assert!(!args.uppercase);
line.to_lowercase()
} else {
line
}
})
.join("");
if let Some(line) = args.append {
stdout.push_str(&line);
}
print!("{stdout}");
stdout
};
if let Some(path) = args.tee {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.unwrap();
write!(file, "{stdout}").unwrap();
}
if args.abort {
#[cfg(unix)]
let _ = signal::raise(signal::Signal::SIGTERM);
std::process::abort()
} else if args.fail {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}