doiget_cli/commands/
bib.rs1use std::io::Write;
16
17use anyhow::{bail, Context, Result};
18use camino::{Utf8Path, Utf8PathBuf};
19
20use doiget_core::refs::{self, Format};
21use doiget_core::store::{render, FsStore, Metadata, Store};
22use doiget_core::Safekey;
23
24use super::fetch::CliExit;
25use super::output::print_err;
26use super::resolve_store_root;
27
28pub fn run(
36 ref_: Option<String>,
37 all: bool,
38 from_file: Option<Utf8PathBuf>,
39 _mode: super::output::OutputMode,
40) -> Result<()> {
41 let selectors =
43 usize::from(ref_.is_some()) + usize::from(all) + usize::from(from_file.is_some());
44 if selectors == 0 {
45 bail!("specify a ref, `--all`, or `--from-file <FILE>`");
46 }
47 if selectors > 1 {
48 bail!("`--all`, `--from-file`, and a positional ref are mutually exclusive");
49 }
50
51 let store = FsStore::new(resolve_store_root()?)?;
52
53 if all {
54 return run_all(&store);
55 }
56 if let Some(path) = from_file {
57 return run_from_file(&store, &path);
58 }
59
60 let input = match ref_ {
64 Some(r) => r,
65 None => bail!("specify a ref, `--all`, or `--from-file <FILE>`"),
66 };
67 let ref_ = super::parse_ref_or_exit(&input)?;
68 let safekey = ref_.safekey();
69 match store.read(&safekey)? {
70 Some(m) => write_all(&render::to_bibtex(safekey.as_str(), &m)),
71 None => bail!("no entry for {input}"),
72 }
73}
74
75fn run_all(store: &FsStore) -> Result<()> {
79 let entries = store
81 .list_recent(usize::MAX)
82 .context("failed to enumerate the store")?;
83
84 if entries.is_empty() {
85 print_err(format_args!(
86 "bib --all: the store is empty; nothing to export"
87 ));
88 return Ok(());
89 }
90
91 let mut out = String::new();
92 let mut seen: Vec<String> = Vec::new();
93 let mut rendered = 0usize;
94 for e in &entries {
95 match store.read(&e.safekey) {
98 Ok(Some(m)) => push_entry(&mut out, &mut seen, &e.safekey, &m, &mut rendered),
99 Ok(None) => {}
100 Err(err) => print_err(format_args!(
101 "bib --all: skipping {} (read failed: {err})",
102 e.safekey.as_str()
103 )),
104 }
105 }
106
107 write_all(&out)?;
108 print_err(format_args!("bib --all: exported {rendered} entries"));
109 Ok(())
110}
111
112fn run_from_file(store: &FsStore, path: &Utf8Path) -> Result<()> {
118 let raw = std::fs::read_to_string(path)
119 .with_context(|| format!("reading --from-file list: {path}"))?;
120 let parsed = refs::parse_input(&raw, Format::Auto, Some(path));
123
124 let mut out = String::new();
125 let mut seen: Vec<String> = Vec::new();
126 let mut rendered = 0usize;
127 let mut missing = 0usize;
128 for entry in parsed {
129 let ref_ = match entry {
130 Ok(p) => p.ref_,
131 Err(e) => {
132 missing += 1;
133 print_err(format_args!(
134 "bib --from-file: skipping unparsable entry ({e})"
135 ));
136 continue;
137 }
138 };
139 let safekey = ref_.safekey();
140 match store.read(&safekey) {
144 Ok(Some(m)) => push_entry(&mut out, &mut seen, &safekey, &m, &mut rendered),
145 Ok(None) => {
146 missing += 1;
147 print_err(format_args!(
148 "bib --from-file: no store entry for {} (skipped)",
149 ref_.as_input_str()
150 ));
151 }
152 Err(err) => {
153 missing += 1;
154 print_err(format_args!(
155 "bib --from-file: read failed for {} ({err}; skipped)",
156 ref_.as_input_str()
157 ));
158 }
159 }
160 }
161
162 write_all(&out)?;
163 print_err(format_args!(
164 "bib --from-file: exported {rendered} entries, {missing} missing"
165 ));
166 if missing > 0 {
167 let code = missing.min(255) as i32;
169 return Err(anyhow::Error::new(CliExit(code)));
170 }
171 Ok(())
172}
173
174fn push_entry(
178 out: &mut String,
179 seen: &mut Vec<String>,
180 safekey: &Safekey,
181 m: &Metadata,
182 rendered: &mut usize,
183) {
184 let key = safekey.as_str();
185 if seen.iter().any(|k| k == key) {
186 return;
187 }
188 seen.push(key.to_string());
189 if !out.is_empty() {
190 out.push('\n');
193 }
194 out.push_str(&render::to_bibtex(key, m));
195 *rendered += 1;
196}
197
198fn write_all(bib: &str) -> Result<()> {
203 let stdout = std::io::stdout();
204 let mut out = stdout.lock();
205 write!(out, "{bib}").context("failed to write BibTeX to stdout")
206}