use std::io::Write;
use anyhow::{bail, Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use doiget_core::refs::{self, Format};
use doiget_core::store::{render, FsStore, Metadata, Store};
use doiget_core::{Ref, Safekey};
use super::fetch::CliExit;
use super::resolve_store_root;
#[allow(clippy::print_stderr)]
fn print_err(args: std::fmt::Arguments<'_>) {
eprintln!("{args}");
}
pub fn run(
ref_: Option<String>,
all: bool,
from_file: Option<Utf8PathBuf>,
_mode: super::output::OutputMode,
) -> Result<()> {
let selectors =
usize::from(ref_.is_some()) + usize::from(all) + usize::from(from_file.is_some());
if selectors == 0 {
bail!("specify a ref, `--all`, or `--from-file <FILE>`");
}
if selectors > 1 {
bail!("`--all`, `--from-file`, and a positional ref are mutually exclusive");
}
let store = FsStore::new(resolve_store_root()?)?;
if all {
return run_all(&store);
}
if let Some(path) = from_file {
return run_from_file(&store, &path);
}
let input = match ref_ {
Some(r) => r,
None => bail!("specify a ref, `--all`, or `--from-file <FILE>`"),
};
let ref_ = Ref::parse(&input).with_context(|| format!("invalid ref: {input}"))?;
let safekey = ref_.safekey();
match store.read(&safekey)? {
Some(m) => write_all(&render::to_bibtex(safekey.as_str(), &m)),
None => bail!("no entry for {input}"),
}
}
fn run_all(store: &FsStore) -> Result<()> {
let entries = store
.list_recent(usize::MAX)
.context("failed to enumerate the store")?;
if entries.is_empty() {
print_err(format_args!(
"bib --all: the store is empty; nothing to export"
));
return Ok(());
}
let mut out = String::new();
let mut seen: Vec<String> = Vec::new();
let mut rendered = 0usize;
for e in &entries {
match store.read(&e.safekey) {
Ok(Some(m)) => push_entry(&mut out, &mut seen, &e.safekey, &m, &mut rendered),
Ok(None) => {}
Err(err) => print_err(format_args!(
"bib --all: skipping {} (read failed: {err})",
e.safekey.as_str()
)),
}
}
write_all(&out)?;
print_err(format_args!("bib --all: exported {rendered} entries"));
Ok(())
}
fn run_from_file(store: &FsStore, path: &Utf8Path) -> Result<()> {
let raw = std::fs::read_to_string(path)
.with_context(|| format!("reading --from-file list: {path}"))?;
let parsed = refs::parse_input(&raw, Format::Auto, Some(path));
let mut out = String::new();
let mut seen: Vec<String> = Vec::new();
let mut rendered = 0usize;
let mut missing = 0usize;
for entry in parsed {
let ref_ = match entry {
Ok(p) => p.ref_,
Err(e) => {
missing += 1;
print_err(format_args!(
"bib --from-file: skipping unparsable entry ({e})"
));
continue;
}
};
let safekey = ref_.safekey();
match store.read(&safekey) {
Ok(Some(m)) => push_entry(&mut out, &mut seen, &safekey, &m, &mut rendered),
Ok(None) => {
missing += 1;
print_err(format_args!(
"bib --from-file: no store entry for {} (skipped)",
ref_.as_input_str()
));
}
Err(err) => {
missing += 1;
print_err(format_args!(
"bib --from-file: read failed for {} ({err}; skipped)",
ref_.as_input_str()
));
}
}
}
write_all(&out)?;
print_err(format_args!(
"bib --from-file: exported {rendered} entries, {missing} missing"
));
if missing > 0 {
let code = missing.min(255) as i32;
return Err(anyhow::Error::new(CliExit(code)));
}
Ok(())
}
fn push_entry(
out: &mut String,
seen: &mut Vec<String>,
safekey: &Safekey,
m: &Metadata,
rendered: &mut usize,
) {
let key = safekey.as_str();
if seen.iter().any(|k| k == key) {
return;
}
seen.push(key.to_string());
if !out.is_empty() {
out.push('\n');
}
out.push_str(&render::to_bibtex(key, m));
*rendered += 1;
}
fn write_all(bib: &str) -> Result<()> {
let stdout = std::io::stdout();
let mut out = stdout.lock();
write!(out, "{bib}").context("failed to write BibTeX to stdout")
}