use std::io::{self, BufRead, Write};
use std::process;
use clap::Parser;
use crate::output::{CliOutput, OutputArgs};
use consortium::node_set::NodeSet;
use consortium::range_set::RangeSet;
#[derive(Parser)]
#[command(name = "pinch", version, about)]
pub struct Args {
#[arg(short = 'c', long = "count", group = "operation")]
count: bool,
#[arg(short = 'e', long = "expand", group = "operation")]
expand: bool,
#[arg(short = 'f', long = "fold", group = "operation")]
fold: bool,
#[arg(short = 'l', long = "list", action = clap::ArgAction::Count, group = "operation")]
list: u8,
#[arg(short = 'r', long = "regroup", group = "operation")]
regroup: bool,
#[arg(long = "groupsources", group = "operation")]
groupsources: bool,
#[arg(short = 'x', long = "exclude")]
exclude: Vec<String>,
#[arg(short = 'i', long = "intersection")]
intersection: Vec<String>,
#[arg(short = 'X', long = "xor")]
xor: Vec<String>,
#[arg(short = 'a', long = "all")]
all: bool,
#[arg(short = 'g', long = "group")]
group: Vec<String>,
#[arg(short = 'S', long = "separator")]
separator: Option<String>,
#[arg(short = 'R', long = "rangeset")]
rangeset: bool,
#[arg(long = "autostep")]
autostep: Option<String>,
#[arg(long = "split")]
split: Option<usize>,
#[arg(long = "contiguous")]
contiguous: bool,
#[arg(long = "pick")]
pick: Option<usize>,
#[arg(short = 'I', long = "slice", value_name = "RANGESET")]
slice: Option<String>,
#[arg(long = "index", value_name = "NODE")]
index: Option<String>,
#[arg(short = 'O', long = "output-format", value_name = "FORMAT")]
output_format: Option<String>,
nodesets: 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!("pinch: {e}");
process::exit(1);
}
}
fn execute(args: Args) -> anyhow::Result<()> {
let cmdcount = args.count as u32
+ args.expand as u32
+ args.fold as u32
+ u32::from(args.list > 0)
+ args.regroup as u32
+ args.groupsources as u32
+ u32::from(args.index.is_some());
if cmdcount > 1 {
eprintln!("Multiple commands not allowed.");
process::exit(2);
}
if args.index.is_some() && args.pick.is_some() {
eprintln!("--index cannot be combined with --pick");
process::exit(2);
}
let stdout = io::stdout();
let mut out = stdout.lock();
if args.rangeset {
return run_rangeset(&args, &mut out);
}
let mut ns = gather_input(&args)?;
for pat in &args.exclude {
let other = NodeSet::parse(pat)?;
ns.difference_update(&other);
}
for pat in &args.intersection {
let other = NodeSet::parse(pat)?;
ns.intersection_update(&other);
}
for pat in &args.xor {
let other = NodeSet::parse(pat)?;
ns.symmetric_difference_update(&other);
}
if let Some(ref slice_pat) = args.slice {
ns = slice_nodeset(&ns, slice_pat)?;
}
if let Some(n) = args.pick {
ns = pick_random(&ns, n);
}
let fmt = args.output_format.as_deref().unwrap_or("%s");
if let Some(ref node_arg) = args.index {
match NodeSet::parse(node_arg) {
Ok(parsed) if parsed.len() == 1 => {}
Ok(_) => {
eprintln!("ERROR: index() argument must be a single node");
process::exit(1);
}
Err(e) => return Err(e.into()),
}
return match ns.index(node_arg) {
Some(i) => {
writeln!(out, "{}", apply_format(fmt, &i.to_string()))?;
Ok(())
}
None => {
eprintln!("ERROR: '{node_arg}' is not in nodeset");
process::exit(1);
}
};
}
let sep = args.separator.as_deref().unwrap_or(" ");
if args.count {
writeln!(out, "{}", ns.len())?;
} else if args.expand {
let nodes: Vec<String> = ns.iter().collect();
writeln!(out, "{}", nodes.join(sep))?;
} else if let Some(n) = args.split {
let subsets = ns.split(n);
for subset in subsets {
writeln!(out, "{subset}")?;
}
} else if args.contiguous {
let subsets = ns.split(ns.len().max(1));
for subset in subsets {
if !subset.is_empty() {
writeln!(out, "{subset}")?;
}
}
} else {
writeln!(out, "{ns}")?;
}
Ok(())
}
fn gather_input(args: &Args) -> anyhow::Result<NodeSet> {
let mut ns = NodeSet::new();
for pat in &args.nodesets {
let parsed = NodeSet::parse(pat)?;
ns.update(&parsed);
}
if args.nodesets.is_empty() && !is_terminal::is_terminal(io::stdin()) {
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = line?;
let trimmed = line.trim();
if !trimmed.is_empty() {
let parsed = NodeSet::parse(trimmed)?;
ns.update(&parsed);
}
}
}
Ok(ns)
}
fn run_rangeset(args: &Args, out: &mut impl Write) -> anyhow::Result<()> {
let autostep = parse_autostep(&args.autostep);
let mut rs = RangeSet::new();
for pat in &args.nodesets {
let parsed = RangeSet::parse(pat, autostep)?;
rs.update(&parsed);
}
if args.nodesets.is_empty() && !is_terminal::is_terminal(io::stdin()) {
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = line?;
let trimmed = line.trim();
if !trimmed.is_empty() {
let parsed = RangeSet::parse(trimmed, autostep)?;
rs.update(&parsed);
}
}
}
for pat in &args.exclude {
let other = RangeSet::parse(pat, autostep)?;
rs.difference_update(&other);
}
for pat in &args.intersection {
let other = RangeSet::parse(pat, autostep)?;
rs.intersection_update(&other);
}
for pat in &args.xor {
let other = RangeSet::parse(pat, autostep)?;
rs.symmetric_difference_update(&other);
}
if let Some(ref slice_pat) = args.slice {
rs = slice_rangeset(&rs, slice_pat)?;
}
let fmt = args.output_format.as_deref().unwrap_or("%s");
if let Some(ref elem_arg) = args.index {
return match rs.index_str(elem_arg) {
Some(i) => {
writeln!(out, "{}", apply_format(fmt, &i.to_string()))?;
Ok(())
}
None => {
eprintln!("ERROR: {elem_arg} is not in RangeSet");
process::exit(1);
}
};
}
let sep = args.separator.as_deref().unwrap_or(" ");
if args.count {
writeln!(out, "{}", rs.len())?;
} else if args.expand {
let items: Vec<String> = rs.striter().collect();
writeln!(out, "{}", items.join(sep))?;
} else {
writeln!(out, "{rs}")?;
}
Ok(())
}
fn parse_autostep(s: &Option<String>) -> Option<u32> {
let s = s.as_deref()?;
match s {
"auto" => Some(0), _ => s.parse::<u32>().ok(),
}
}
fn slice_nodeset(ns: &NodeSet, slice_pat: &str) -> anyhow::Result<NodeSet> {
let positions = RangeSet::parse(slice_pat, None)?;
let elems: Vec<String> = ns.iter().collect();
let mut sliced = NodeSet::new();
for pos in positions.intiter() {
if pos >= 0 && (pos as usize) < elems.len() {
sliced.update_str(&elems[pos as usize])?;
}
}
Ok(sliced)
}
fn slice_rangeset(rs: &RangeSet, slice_pat: &str) -> anyhow::Result<RangeSet> {
let positions = RangeSet::parse(slice_pat, None)?;
let elems = rs.sorted();
let mut sliced = RangeSet::new();
for pos in positions.intiter() {
if pos >= 0 && (pos as usize) < elems.len() {
sliced.add_str(&elems[pos as usize]);
}
}
Ok(sliced)
}
fn apply_format(fmt: &str, value: &str) -> String {
let mut out = String::new();
let mut chars = fmt.chars();
while let Some(c) = chars.next() {
if c == '%' {
match chars.next() {
Some('s') | Some('d') | Some('i') => out.push_str(value),
Some('%') => out.push('%'),
Some(other) => {
out.push('%');
out.push(other);
}
None => out.push('%'),
}
} else {
out.push(c);
}
}
out
}
fn pick_random(ns: &NodeSet, n: usize) -> NodeSet {
use std::collections::HashSet;
let all: Vec<String> = ns.iter().collect();
if n >= all.len() {
return ns.clone();
}
let mut indices: Vec<usize> = (0..all.len()).collect();
let mut seed: u64 = std::process::id() as u64
^ std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
for i in (1..indices.len()).rev() {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
let j = (seed >> 33) as usize % (i + 1);
indices.swap(i, j);
}
let picked: HashSet<&str> = indices[..n].iter().map(|&i| all[i].as_str()).collect();
let mut result = NodeSet::new();
for node in picked {
let _ = result.update_str(node);
}
result
}