stash-cli 0.8.0

A local store for pipeline output and ad hoc file snapshots
Documentation
use clap::{ArgAction, Args};
use std::collections::BTreeMap;
use std::fs::File;
use std::io::{self, Write};
use std::path::PathBuf;

use crate::store;

#[derive(Args, Debug, Clone, Default)]
pub(crate) struct PushArgs {
    #[arg(short = 'a', long = "attr", value_name = "key=value", action = ArgAction::Append, help = "Set attribute key=value (repeatable)")]
    pub(crate) attr: Vec<String>,

    #[arg(long, default_value = "null", help = "Where to print the generated entry ID: stdout, stderr, null, 1, 2, or 0")]
    pub(crate) print: String,

    #[arg(help = "Optional file to stash; reads stdin when omitted")]
    pub(crate) file: Option<PathBuf>,
}

#[derive(Args, Debug, Clone)]
pub(crate) struct TeeArgs {
    #[arg(short = 'a', long = "attr", value_name = "key=value", action = ArgAction::Append, help = "Set attribute key=value (repeatable)")]
    pub(crate) attr: Vec<String>,

    #[arg(long, default_value = "null", help = "Where to print the generated entry ID: stdout, stderr, null, 1, 2, or 0")]
    pub(crate) print: String,

    #[arg(long, num_args = 0..=1, default_value_t = true, default_missing_value = "true", help = "Save captured input when an upstream or processing error happens: true or false")]
    pub(crate) save_on_error: bool,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum PrintTarget {
    Stdout,
    Stderr,
    None,
}

fn parse_print_target(value: &str) -> io::Result<PrintTarget> {
    match value {
        "stdout" | "1" => Ok(PrintTarget::Stdout),
        "stderr" | "2" => Ok(PrintTarget::Stderr),
        "null" | "0" => Ok(PrintTarget::None),
        _ => Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "--print must be stdout, stderr, null, 1, 2, or 0",
        )),
    }
}

fn emit_generated_id(
    target: PrintTarget,
    id: &str,
    stdout: Option<&mut dyn Write>,
) -> io::Result<()> {
    match target {
        PrintTarget::Stdout => {
            if let Some(out) = stdout {
                writeln!(out, "{id}")?;
            } else {
                println!("{id}");
            }
        }
        PrintTarget::Stderr => {
            eprintln!("{id}");
        }
        PrintTarget::None => {}
    }
    Ok(())
}

fn parse_meta_flags(values: &[String]) -> io::Result<BTreeMap<String, String>> {
    let mut attrs = BTreeMap::new();
    for value in values {
        let Some((k, v)) = value.split_once('=') else {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "attribute must be key=value",
            ));
        };
        attrs.insert(k.to_string(), v.to_string());
    }
    Ok(attrs)
}

pub(super) fn push_command(args: PushArgs) -> io::Result<()> {
    let mut attrs = parse_meta_flags(&args.attr)?;
    let print_target = parse_print_target(&args.print)?;
    let id = if let Some(path) = args.file {
        let mut file = File::open(&path)?;
        store::add_filename_attr(&path, &mut attrs);
        store::push_from_reader(&mut file, attrs)?
    } else {
        let stdin = io::stdin();
        let mut input = stdin.lock();
        store::push_from_reader(&mut input, attrs)?
    };
    emit_generated_id(print_target, &id, None)?;
    Ok(())
}

pub(super) fn tee_command(args: TeeArgs) -> io::Result<()> {
    let attrs = parse_meta_flags(&args.attr)?;
    let print_target = parse_print_target(&args.print)?;
    let stdin = io::stdin();
    let mut input = stdin.lock();
    let stdout = io::stdout();
    let mut out = stdout.lock();
    match store::tee_from_reader_partial(&mut input, &mut out, attrs, args.save_on_error) {
        Ok(id) => {
            emit_generated_id(print_target, &id, Some(&mut out))?;
            Ok(())
        }
        Err(err) => Err(err),
    }
}