use clap::{Arg, Command};
use color_eyre::Result;
use color_eyre::eyre::eyre;
use std::io::{self, BufRead};
use crate::version_compare::version_compare_strings;
#[derive(Debug)]
pub struct KeySpec {
pub field: usize,
}
fn parse_key_spec(key_str: &str) -> Result<KeySpec> {
let field: usize = key_str.parse()
.map_err(|_| eyre!("sort: invalid key specification '{}'", key_str))?;
if field == 0 {
return Err(eyre!("sort: field number must be greater than 0"));
}
Ok(KeySpec { field })
}
fn numeric_cmp(a: &str, b: &str) -> std::cmp::Ordering {
let a_trim = a.trim_start();
let b_trim = b.trim_start();
let a_num = a_trim.chars().take_while(|c| c.is_ascii_digit() || *c == '-').collect::<String>();
let b_num = b_trim.chars().take_while(|c| c.is_ascii_digit() || *c == '-').collect::<String>();
if !a_num.is_empty() && !b_num.is_empty() {
if let (Ok(na), Ok(nb)) = (a_num.parse::<i64>(), b_num.parse::<i64>()) {
let ord = na.cmp(&nb);
if ord != std::cmp::Ordering::Equal {
return ord;
}
}
}
a.cmp(b)
}
fn get_sort_key(line: &str, key_spec: &KeySpec, separator: Option<&str>) -> String {
if let Some(sep) = separator {
let fields: Vec<&str> = line.split(sep).collect();
if key_spec.field <= fields.len() {
fields[key_spec.field - 1].to_string()
} else {
"".to_string()
}
} else {
let fields: Vec<&str> = line.split_whitespace().collect();
if key_spec.field <= fields.len() {
fields[key_spec.field - 1].to_string()
} else {
"".to_string()
}
}
}
pub struct SortOptions {
pub files: Vec<String>,
pub reverse: bool,
pub numeric: bool,
pub version_sort: bool,
pub key: Option<KeySpec>,
pub separator: Option<String>,
pub stable: bool,
pub unique: bool,
}
pub fn parse_options(matches: &clap::ArgMatches) -> Result<SortOptions> {
let files: Vec<String> = matches.get_many::<String>("files")
.map(|vals| vals.cloned().collect())
.unwrap_or_default();
let reverse = matches.get_flag("reverse");
let numeric = matches.get_flag("numeric");
let version_sort = matches.get_flag("version_sort");
let key = if let Some(key_str) = matches.get_one::<String>("key") {
Some(parse_key_spec(key_str)?)
} else {
None
};
let separator = matches.get_one::<String>("separator").cloned();
let stable = matches.get_flag("stable");
let unique = matches.get_flag("unique");
Ok(SortOptions { files, reverse, numeric, version_sort, key, separator, stable, unique })
}
pub fn command() -> Command {
Command::new("sort")
.about("Sort lines of text files")
.arg(Arg::new("reverse")
.short('r')
.long("reverse")
.help("Reverse the sort order")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("numeric")
.short('n')
.long("numeric")
.help("Compare according to string numerical value")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("version_sort")
.short('V')
.long("version-sort")
.help("Natural sort of (version) numbers within text")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("key")
.short('k')
.long("key")
.help("Sort by key/field")
.value_name("POS")
.action(clap::ArgAction::Set))
.arg(Arg::new("separator")
.short('t')
.long("field-separator")
.help("Use SEP instead of non-blank to blank transition")
.value_name("SEP")
.action(clap::ArgAction::Set))
.arg(Arg::new("stable")
.short('s')
.long("stable")
.help("Stabilize sort by disabling last-resort comparison")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("unique")
.short('u')
.long("unique")
.help("Output only the first of an equal run")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("files")
.num_args(0..)
.help("Files to sort (if none, read from stdin)"))
}
fn read_lines(options: &SortOptions) -> Result<Vec<String>> {
let mut lines = Vec::new();
if options.files.is_empty() {
let stdin = io::stdin();
let reader = stdin.lock();
for line_result in reader.lines() {
let line = line_result
.map_err(|e| eyre!("sort: error reading stdin: {}", e))?;
lines.push(line);
}
} else {
for file_path in &options.files {
let file = std::fs::File::open(file_path)
.map_err(|e| eyre!("sort: {}: {}", file_path, e))?;
let reader = io::BufReader::new(file);
for line_result in reader.lines() {
let line = line_result
.map_err(|e| eyre!("sort: error reading {}: {}", file_path, e))?;
lines.push(line);
}
}
}
Ok(lines)
}
fn sort_lines(lines: &mut Vec<String>, options: &SortOptions) {
if let Some(key_spec) = &options.key {
let mut keyed_lines: Vec<(String, String)> = lines.drain(..)
.map(|line| {
let key = get_sort_key(&line, key_spec, options.separator.as_deref());
(key, line)
})
.collect();
if options.stable {
if options.version_sort {
keyed_lines.sort_by(|a, b| version_compare_strings(&a.0, &b.0));
} else if options.numeric {
keyed_lines.sort_by(|a, b| numeric_cmp(&a.0, &b.0));
} else {
keyed_lines.sort_by(|a, b| a.0.cmp(&b.0));
}
} else {
if options.version_sort {
keyed_lines.sort_unstable_by(|a, b| version_compare_strings(&a.0, &b.0));
} else if options.numeric {
keyed_lines.sort_unstable_by(|a, b| numeric_cmp(&a.0, &b.0));
} else {
keyed_lines.sort_unstable_by(|a, b| a.0.cmp(&b.0));
}
}
if options.reverse {
keyed_lines.reverse();
}
if options.unique {
keyed_lines.dedup_by(|a, b| a.0 == b.0);
}
*lines = keyed_lines.into_iter().map(|(_, line)| line).collect();
} else {
if options.version_sort {
if options.stable {
lines.sort_by(|a, b| version_compare_strings(a, b));
} else {
lines.sort_unstable_by(|a, b| version_compare_strings(a, b));
}
} else if options.numeric {
if options.stable {
lines.sort_by(|a, b| numeric_cmp(a, b));
} else {
lines.sort_unstable_by(|a, b| numeric_cmp(a, b));
}
} else {
if options.stable {
lines.sort();
} else {
lines.sort_unstable();
}
}
if options.reverse {
lines.reverse();
}
if options.unique {
lines.dedup();
}
}
}
pub fn run(options: SortOptions) -> Result<()> {
let mut lines = read_lines(&options)?;
sort_lines(&mut lines, &options);
for line in lines {
println!("{}", line);
}
Ok(())
}