use std::collections::HashMap;
use std::io::{self, BufRead, Write};
use std::process;
use clap::{Parser, ValueEnum};
use crate::display;
use crate::fold;
use crate::output::{CliOutput, OutputArgs};
use consortium::node_set::NodeSet;
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
enum InterpretKeys {
Never,
Always,
#[default]
Auto,
}
#[derive(Parser)]
#[command(name = "molt", version, about)]
pub struct Args {
#[arg(short = 'b', long = "dshbak")]
dshbak: bool,
#[arg(short = 'L', long = "line")]
line: bool,
#[arg(short = 'S', long = "separator", default_value = ":")]
separator: String,
#[arg(short = 'T', long = "tree")]
tree: bool,
#[arg(short = 'G', long = "groupbase")]
groupbase: bool,
#[arg(long = "axis", value_name = "RANGESET")]
axis: Option<String>,
#[arg(long = "interpret-keys", value_name = "WHEN", default_value = "auto")]
interpret_keys: InterpretKeys,
files: Vec<String>,
#[command(flatten)]
output: OutputArgs,
}
pub fn run() {
let args = Args::parse();
let _out = CliOutput::from_args(&args.output);
if let Err(e) = execute(args) {
eprintln!("molt: {e}");
process::exit(1);
}
}
fn execute(args: Args) -> anyhow::Result<()> {
let fold_axes = match &args.axis {
Some(axis) => {
Some(fold::parse_fold_axis(axis).map_err(|e| anyhow::anyhow!("Parse error: {e}"))?)
}
None => None,
};
let fold_axis: Option<&[i64]> = fold_axes.as_deref().filter(|v| !v.is_empty());
let stdout = io::stdout();
let mut out = stdout.lock();
let lines = read_input(&args)?;
if args.dshbak {
run_dshbak(&lines, &args, fold_axis, &mut out)?;
} else if args.line {
run_line_mode(&lines, &args, &mut out)?;
} else {
run_passthrough(&lines, &mut out)?;
}
Ok(())
}
fn read_input(args: &Args) -> anyhow::Result<Vec<String>> {
let mut lines = Vec::new();
if args.files.is_empty() {
let stdin = io::stdin();
for line in stdin.lock().lines() {
lines.push(line?);
}
} else {
for path in &args.files {
let file = std::fs::File::open(path)?;
let reader = io::BufReader::new(file);
for line in reader.lines() {
lines.push(line?);
}
}
}
Ok(lines)
}
fn parse_line<'a>(line: &'a str, separator: &str) -> Option<(&'a str, &'a str)> {
let idx = line.find(separator)?;
let key = line[..idx].trim();
let msg = &line[idx + separator.len()..];
if key.is_empty() {
return None;
}
Some((key, msg))
}
fn expand_key(key: &str, mode: &mut InterpretKeys) -> anyhow::Result<Vec<String>> {
if *mode == InterpretKeys::Never {
return Ok(vec![key.to_string()]);
}
match NodeSet::parse(key) {
Ok(ns) => {
let nodes: Vec<String> = ns.iter().collect();
if nodes.is_empty() {
Ok(vec![key.to_string()])
} else {
Ok(nodes)
}
}
Err(e) => {
if *mode == InterpretKeys::Always {
return Err(anyhow::anyhow!("Parse error: {e}"));
}
*mode = InterpretKeys::Never;
Ok(vec![key.to_string()])
}
}
}
fn run_dshbak(
lines: &[String],
args: &Args,
fold_axis: Option<&[i64]>,
out: &mut impl Write,
) -> anyhow::Result<()> {
let mut interpret = args.interpret_keys;
let mut node_bufs: HashMap<String, Vec<Vec<u8>>> = HashMap::new();
for line in lines {
if let Some((key, msg)) = parse_line(line, &args.separator) {
for node in expand_key(key, &mut interpret)? {
node_bufs
.entry(node)
.or_default()
.push(msg.as_bytes().to_vec());
}
}
}
let mut groups: HashMap<Vec<u8>, Vec<String>> = HashMap::new();
for (node, msgs) in node_bufs {
groups.entry(msgs.join(&b"\n"[..])).or_default().push(node);
}
let mut groups: Vec<(Vec<u8>, Vec<String>)> = groups.into_iter().collect();
for (_buf, names) in groups.iter_mut() {
names.sort();
}
groups.sort_by(|a, b| b.1.len().cmp(&a.1.len()).then_with(|| a.1[0].cmp(&b.1[0])));
for (message, names) in &groups {
let folded = if interpret == InterpretKeys::Never {
names.join(",")
} else {
fold::fold_nodes(names, fold_axis)
};
display::print_gathered_header(&folded, names.len(), out)?;
out.write_all(message)?;
writeln!(out)?;
}
Ok(())
}
fn run_line_mode(lines: &[String], args: &Args, out: &mut impl Write) -> anyhow::Result<()> {
let mut node_output: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
let mut interpret = args.interpret_keys;
for line in lines {
if let Some((key, msg)) = parse_line(line, &args.separator) {
for node in expand_key(key, &mut interpret)? {
node_output.entry(node).or_default().push(msg.to_string());
}
}
}
for (node, msgs) in &node_output {
for msg in msgs {
display::print_line_with_label(node, msg, out)?;
}
}
Ok(())
}
fn run_passthrough(lines: &[String], out: &mut impl Write) -> anyhow::Result<()> {
for line in lines {
writeln!(out, "{line}")?;
}
Ok(())
}