use clap::{Arg, Command};
use color_eyre::Result;
use color_eyre::eyre::eyre;
use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
pub struct CommOptions {
pub file1: String,
pub file2: String,
pub suppress1: bool,
pub suppress2: bool,
pub suppress3: bool,
pub output_delimiter: String,
pub zero_terminated: bool,
}
pub fn parse_options(matches: &clap::ArgMatches) -> Result<CommOptions> {
let suppress1 = matches.get_flag("suppress1");
let suppress2 = matches.get_flag("suppress2");
let suppress3 = matches.get_flag("suppress3");
let output_delimiter = matches.get_one::<String>("output-delimiter")
.cloned()
.unwrap_or_else(|| "\t".to_string());
let zero_terminated = matches.get_flag("zero-terminated");
let args: Vec<String> = matches.get_many::<String>("files")
.map(|vals| vals.cloned().collect())
.unwrap_or_default();
if args.len() != 2 {
return Err(eyre!("comm: missing operand"));
}
Ok(CommOptions {
file1: args[0].clone(),
file2: args[1].clone(),
suppress1,
suppress2,
suppress3,
output_delimiter,
zero_terminated,
})
}
pub fn command() -> Command {
Command::new("comm")
.about("Compare two sorted files line by line")
.arg(Arg::new("suppress1")
.short('1')
.help("Suppress lines unique to FILE1")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("suppress2")
.short('2')
.help("Suppress lines unique to FILE2")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("suppress3")
.short('3')
.help("Suppress lines that appear in both files")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("output-delimiter")
.long("output-delimiter")
.help("Separate columns with STR")
.value_name("STR"))
.arg(Arg::new("zero-terminated")
.short('z')
.long("zero-terminated")
.help("Line delimiter is NUL, not newline")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("files")
.num_args(2)
.required(true)
.help("Two sorted files to compare (use - for stdin)"))
}
pub fn open_file_or_stdin(path: &str, applet_name: &str) -> Result<Box<dyn BufRead>> {
if path == "-" {
Ok(Box::new(BufReader::new(io::stdin())))
} else {
let file = File::open(path)
.map_err(|e| eyre!("{}: cannot open '{}': {}", applet_name, path, e))?;
Ok(Box::new(BufReader::new(file)))
}
}
fn read_line(reader: &mut Box<dyn BufRead>, buf: &mut Vec<u8>, zero_terminated: bool) -> io::Result<bool> {
buf.clear();
let bytes_read = if zero_terminated {
reader.read_until(b'\0', buf)?
} else {
reader.read_until(b'\n', buf)?
};
Ok(bytes_read > 0)
}
fn strip_delimiter(line: &[u8], zero_terminated: bool) -> Vec<u8> {
if line.is_empty() {
return line.to_vec();
}
let delimiter = if zero_terminated { b'\0' } else { b'\n' };
if line.last() == Some(&delimiter) {
line[..line.len() - 1].to_vec()
} else {
line.to_vec()
}
}
fn output_line(line: &[u8], columns: usize, delimiter: &str) -> Result<()> {
for _ in 0..columns {
io::stdout().write_all(delimiter.as_bytes())
.map_err(|e| eyre!("comm: error writing: {}", e))?;
}
io::stdout().write_all(line)
.map_err(|e| eyre!("comm: error writing: {}", e))?;
io::stdout().write_all(b"\n")
.map_err(|e| eyre!("comm: error writing: {}", e))?;
Ok(())
}
fn process_comparison(
reader1: &mut Box<dyn BufRead>,
reader2: &mut Box<dyn BufRead>,
line1: &mut Vec<u8>,
line2: &mut Vec<u8>,
has_line1: &mut bool,
has_line2: &mut bool,
options: &CommOptions,
) -> Result<()> {
let line1_content = strip_delimiter(line1, options.zero_terminated);
let line2_content = strip_delimiter(line2, options.zero_terminated);
match line1_content.cmp(&line2_content) {
std::cmp::Ordering::Less => {
if !options.suppress1 {
output_line(&line1_content, 0, "")?;
}
*has_line1 = read_line(reader1, line1, options.zero_terminated)
.map_err(|e| eyre!("comm: error reading '{}': {}", options.file1, e))?;
}
std::cmp::Ordering::Greater => {
if !options.suppress2 {
output_line(&line2_content, 1, &options.output_delimiter)?;
}
*has_line2 = read_line(reader2, line2, options.zero_terminated)
.map_err(|e| eyre!("comm: error reading '{}': {}", options.file2, e))?;
}
std::cmp::Ordering::Equal => {
if !options.suppress3 {
output_line(&line1_content, 2, &options.output_delimiter)?;
}
*has_line1 = read_line(reader1, line1, options.zero_terminated)
.map_err(|e| eyre!("comm: error reading '{}': {}", options.file1, e))?;
*has_line2 = read_line(reader2, line2, options.zero_terminated)
.map_err(|e| eyre!("comm: error reading '{}': {}", options.file2, e))?;
}
}
Ok(())
}
pub fn run(options: CommOptions) -> Result<()> {
let mut reader1 = open_file_or_stdin(&options.file1, "comm")?;
let mut reader2 = open_file_or_stdin(&options.file2, "comm")?;
let mut line1 = Vec::new();
let mut line2 = Vec::new();
let mut has_line1 = read_line(&mut reader1, &mut line1, options.zero_terminated)
.map_err(|e| eyre!("comm: error reading '{}': {}", options.file1, e))?;
let mut has_line2 = read_line(&mut reader2, &mut line2, options.zero_terminated)
.map_err(|e| eyre!("comm: error reading '{}': {}", options.file2, e))?;
while has_line1 || has_line2 {
if !has_line1 {
if !options.suppress2 {
output_line(&strip_delimiter(&line2, options.zero_terminated), 1, &options.output_delimiter)?;
}
has_line2 = read_line(&mut reader2, &mut line2, options.zero_terminated)
.map_err(|e| eyre!("comm: error reading '{}': {}", options.file2, e))?;
} else if !has_line2 {
if !options.suppress1 {
output_line(&strip_delimiter(&line1, options.zero_terminated), 0, "")?;
}
has_line1 = read_line(&mut reader1, &mut line1, options.zero_terminated)
.map_err(|e| eyre!("comm: error reading '{}': {}", options.file1, e))?;
} else {
process_comparison(
&mut reader1,
&mut reader2,
&mut line1,
&mut line2,
&mut has_line1,
&mut has_line2,
&options,
)?;
}
}
Ok(())
}