Skip to main content

bn/commands/
stdin.rs

1//! Utilities for reading from stdin in pipe-friendly commands.
2//!
3//! Supports the `-` convention: `--description -` reads from stdin.
4//! Also supports reading bean IDs from stdin, one per line.
5
6use std::io::{self, IsTerminal, Read};
7
8use anyhow::{Context, Result};
9
10/// Read all of stdin into a string.
11/// Returns an error if stdin is a terminal (not piped).
12pub fn read_stdin() -> Result<String> {
13    if io::stdin().is_terminal() {
14        anyhow::bail!(
15            "Expected piped input but stdin is a terminal.\n\
16             Use: echo \"content\" | bn ... --description -"
17        );
18    }
19
20    let mut buf = String::new();
21    io::stdin()
22        .read_to_string(&mut buf)
23        .context("Failed to read from stdin")?;
24    Ok(buf)
25}
26
27/// Resolve a value that might be "-" (meaning read from stdin).
28/// If the value is "-", reads stdin. Otherwise returns the value as-is.
29pub fn resolve_stdin_value(value: String) -> Result<String> {
30    if value == "-" {
31        read_stdin()
32    } else {
33        Ok(value)
34    }
35}
36
37/// Resolve an `Option<String>` that might contain "-".
38pub fn resolve_stdin_opt(value: Option<String>) -> Result<Option<String>> {
39    match value {
40        Some(v) if v == "-" => Ok(Some(read_stdin()?)),
41        other => Ok(other),
42    }
43}
44
45/// Read bean IDs from stdin, one per line.
46/// Trims whitespace and skips empty lines.
47pub fn read_ids_from_stdin() -> Result<Vec<String>> {
48    let content = read_stdin()?;
49    let ids: Vec<String> = content
50        .lines()
51        .map(|l| l.trim().to_string())
52        .filter(|l| !l.is_empty())
53        .collect();
54
55    if ids.is_empty() {
56        anyhow::bail!("No bean IDs found on stdin");
57    }
58
59    Ok(ids)
60}