1use std::io::{self, BufRead, Write};
11use std::process;
12
13use clap::Parser;
14
15use crate::output::{CliOutput, OutputArgs};
16use consortium::node_set::NodeSet;
17use consortium::range_set::RangeSet;
18
19#[derive(Parser)]
24#[command(name = "pinch", version, about)]
25pub struct Args {
26 #[arg(short = 'c', long = "count", group = "operation")]
29 count: bool,
30
31 #[arg(short = 'e', long = "expand", group = "operation")]
33 expand: bool,
34
35 #[arg(short = 'f', long = "fold", group = "operation")]
37 fold: bool,
38
39 #[arg(short = 'l', long = "list", action = clap::ArgAction::Count, group = "operation")]
41 list: u8,
42
43 #[arg(short = 'r', long = "regroup", group = "operation")]
45 regroup: bool,
46
47 #[arg(long = "groupsources", group = "operation")]
49 groupsources: bool,
50
51 #[arg(short = 'x', long = "exclude")]
54 exclude: Vec<String>,
55
56 #[arg(short = 'i', long = "intersection")]
58 intersection: Vec<String>,
59
60 #[arg(short = 'X', long = "xor")]
62 xor: Vec<String>,
63
64 #[arg(short = 'a', long = "all")]
67 all: bool,
68
69 #[arg(short = 'g', long = "group")]
71 group: Vec<String>,
72
73 #[arg(short = 'S', long = "separator")]
76 separator: Option<String>,
77
78 #[arg(short = 'R', long = "rangeset")]
80 rangeset: bool,
81
82 #[arg(long = "autostep")]
84 autostep: Option<String>,
85
86 #[arg(long = "split")]
88 split: Option<usize>,
89
90 #[arg(long = "contiguous")]
92 contiguous: bool,
93
94 #[arg(long = "pick")]
96 pick: Option<usize>,
97
98 #[arg(short = 'I', long = "slice", value_name = "RANGESET")]
100 slice: Option<String>,
101
102 #[arg(long = "index", value_name = "NODE")]
104 index: Option<String>,
105
106 #[arg(short = 'O', long = "output-format", value_name = "FORMAT")]
108 output_format: Option<String>,
109
110 nodesets: Vec<String>,
113
114 #[command(flatten)]
115 output: OutputArgs,
116}
117
118pub fn run() {
119 let args = Args::parse();
120 let _out = CliOutput::from_args(&args.output);
121 if let Err(e) = execute(args) {
122 eprintln!("pinch: {e}");
123 process::exit(1);
124 }
125}
126
127fn execute(args: Args) -> anyhow::Result<()> {
128 let cmdcount = args.count as u32
132 + args.expand as u32
133 + args.fold as u32
134 + u32::from(args.list > 0)
135 + args.regroup as u32
136 + args.groupsources as u32
137 + u32::from(args.index.is_some());
138 if cmdcount > 1 {
139 eprintln!("Multiple commands not allowed.");
140 process::exit(2);
141 }
142 if args.index.is_some() && args.pick.is_some() {
143 eprintln!("--index cannot be combined with --pick");
144 process::exit(2);
145 }
146
147 let stdout = io::stdout();
148 let mut out = stdout.lock();
149
150 if args.rangeset {
151 return run_rangeset(&args, &mut out);
152 }
153
154 let mut ns = gather_input(&args)?;
156
157 for pat in &args.exclude {
159 let other = NodeSet::parse(pat)?;
160 ns.difference_update(&other);
161 }
162 for pat in &args.intersection {
163 let other = NodeSet::parse(pat)?;
164 ns.intersection_update(&other);
165 }
166 for pat in &args.xor {
167 let other = NodeSet::parse(pat)?;
168 ns.symmetric_difference_update(&other);
169 }
170
171 if let Some(ref slice_pat) = args.slice {
173 ns = slice_nodeset(&ns, slice_pat)?;
174 }
175
176 if let Some(n) = args.pick {
178 ns = pick_random(&ns, n);
179 }
180
181 let fmt = args.output_format.as_deref().unwrap_or("%s");
182
183 if let Some(ref node_arg) = args.index {
185 match NodeSet::parse(node_arg) {
187 Ok(parsed) if parsed.len() == 1 => {}
188 Ok(_) => {
189 eprintln!("ERROR: index() argument must be a single node");
190 process::exit(1);
191 }
192 Err(e) => return Err(e.into()),
193 }
194 return match ns.index(node_arg) {
195 Some(i) => {
196 writeln!(out, "{}", apply_format(fmt, &i.to_string()))?;
197 Ok(())
198 }
199 None => {
200 eprintln!("ERROR: '{node_arg}' is not in nodeset");
201 process::exit(1);
202 }
203 };
204 }
205
206 let sep = args.separator.as_deref().unwrap_or(" ");
208
209 if args.count {
210 writeln!(out, "{}", ns.len())?;
211 } else if args.expand {
212 let nodes: Vec<String> = ns.iter().collect();
213 writeln!(out, "{}", nodes.join(sep))?;
214 } else if let Some(n) = args.split {
215 let subsets = ns.split(n);
216 for subset in subsets {
217 writeln!(out, "{subset}")?;
218 }
219 } else if args.contiguous {
220 let subsets = ns.split(ns.len().max(1));
222 for subset in subsets {
223 if !subset.is_empty() {
224 writeln!(out, "{subset}")?;
225 }
226 }
227 } else {
228 writeln!(out, "{ns}")?;
230 }
231
232 Ok(())
233}
234
235fn gather_input(args: &Args) -> anyhow::Result<NodeSet> {
237 let mut ns = NodeSet::new();
238
239 for pat in &args.nodesets {
241 let parsed = NodeSet::parse(pat)?;
242 ns.update(&parsed);
243 }
244
245 if args.nodesets.is_empty() && !is_terminal::is_terminal(io::stdin()) {
247 let stdin = io::stdin();
248 for line in stdin.lock().lines() {
249 let line = line?;
250 let trimmed = line.trim();
251 if !trimmed.is_empty() {
252 let parsed = NodeSet::parse(trimmed)?;
253 ns.update(&parsed);
254 }
255 }
256 }
257
258 Ok(ns)
259}
260
261fn run_rangeset(args: &Args, out: &mut impl Write) -> anyhow::Result<()> {
263 let autostep = parse_autostep(&args.autostep);
264
265 let mut rs = RangeSet::new();
266
267 for pat in &args.nodesets {
269 let parsed = RangeSet::parse(pat, autostep)?;
270 rs.update(&parsed);
271 }
272
273 if args.nodesets.is_empty() && !is_terminal::is_terminal(io::stdin()) {
275 let stdin = io::stdin();
276 for line in stdin.lock().lines() {
277 let line = line?;
278 let trimmed = line.trim();
279 if !trimmed.is_empty() {
280 let parsed = RangeSet::parse(trimmed, autostep)?;
281 rs.update(&parsed);
282 }
283 }
284 }
285
286 for pat in &args.exclude {
288 let other = RangeSet::parse(pat, autostep)?;
289 rs.difference_update(&other);
290 }
291 for pat in &args.intersection {
292 let other = RangeSet::parse(pat, autostep)?;
293 rs.intersection_update(&other);
294 }
295 for pat in &args.xor {
296 let other = RangeSet::parse(pat, autostep)?;
297 rs.symmetric_difference_update(&other);
298 }
299
300 if let Some(ref slice_pat) = args.slice {
302 rs = slice_rangeset(&rs, slice_pat)?;
303 }
304
305 let fmt = args.output_format.as_deref().unwrap_or("%s");
306
307 if let Some(ref elem_arg) = args.index {
309 return match rs.index_str(elem_arg) {
310 Some(i) => {
311 writeln!(out, "{}", apply_format(fmt, &i.to_string()))?;
312 Ok(())
313 }
314 None => {
315 eprintln!("ERROR: {elem_arg} is not in RangeSet");
316 process::exit(1);
317 }
318 };
319 }
320
321 let sep = args.separator.as_deref().unwrap_or(" ");
322
323 if args.count {
324 writeln!(out, "{}", rs.len())?;
325 } else if args.expand {
326 let items: Vec<String> = rs.striter().collect();
327 writeln!(out, "{}", items.join(sep))?;
328 } else {
329 writeln!(out, "{rs}")?;
331 }
332
333 Ok(())
334}
335
336fn parse_autostep(s: &Option<String>) -> Option<u32> {
338 let s = s.as_deref()?;
339 match s {
340 "auto" => Some(0), _ => s.parse::<u32>().ok(),
342 }
343}
344
345fn slice_nodeset(ns: &NodeSet, slice_pat: &str) -> anyhow::Result<NodeSet> {
350 let positions = RangeSet::parse(slice_pat, None)?;
351 let elems: Vec<String> = ns.iter().collect();
352 let mut sliced = NodeSet::new();
353 for pos in positions.intiter() {
354 if pos >= 0 && (pos as usize) < elems.len() {
355 sliced.update_str(&elems[pos as usize])?;
356 }
357 }
358 Ok(sliced)
359}
360
361fn slice_rangeset(rs: &RangeSet, slice_pat: &str) -> anyhow::Result<RangeSet> {
363 let positions = RangeSet::parse(slice_pat, None)?;
364 let elems = rs.sorted();
365 let mut sliced = RangeSet::new();
366 for pos in positions.intiter() {
367 if pos >= 0 && (pos as usize) < elems.len() {
368 sliced.add_str(&elems[pos as usize]);
369 }
370 }
371 Ok(sliced)
372}
373
374fn apply_format(fmt: &str, value: &str) -> String {
377 let mut out = String::new();
378 let mut chars = fmt.chars();
379 while let Some(c) = chars.next() {
380 if c == '%' {
381 match chars.next() {
382 Some('s') | Some('d') | Some('i') => out.push_str(value),
383 Some('%') => out.push('%'),
384 Some(other) => {
385 out.push('%');
386 out.push(other);
387 }
388 None => out.push('%'),
389 }
390 } else {
391 out.push(c);
392 }
393 }
394 out
395}
396
397fn pick_random(ns: &NodeSet, n: usize) -> NodeSet {
399 use std::collections::HashSet;
400
401 let all: Vec<String> = ns.iter().collect();
402 if n >= all.len() {
403 return ns.clone();
404 }
405
406 let mut indices: Vec<usize> = (0..all.len()).collect();
408 let mut seed: u64 = std::process::id() as u64
410 ^ std::time::SystemTime::now()
411 .duration_since(std::time::UNIX_EPOCH)
412 .unwrap_or_default()
413 .as_nanos() as u64;
414
415 for i in (1..indices.len()).rev() {
416 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
417 let j = (seed >> 33) as usize % (i + 1);
418 indices.swap(i, j);
419 }
420
421 let picked: HashSet<&str> = indices[..n].iter().map(|&i| all[i].as_str()).collect();
422 let mut result = NodeSet::new();
423 for node in picked {
424 let _ = result.update_str(node);
425 }
426 result
427}