How-to Guides
These short, task-oriented recipes assume you are already familiar with the
basics of bibdeskparser (see the introduction); for background on
why things work this way, see BibDesk’s .bib Format.
All examples operate on the example database shipped in the repository at
tests/Refs/refs.bib; substitute the path to your own library.
How to manage file attachments
Tip
Rather than naming and placing every attachment by hand, you can have
bibdeskparser auto-file them – moving each file into a configured
location and renaming it by a file-name format (BibDesk’s AutoFile
feature). See How to auto-file attachments below.
Linked files are stored with paths relative to the library’s .bib
file, so attaching, replacing, unlinking, and renaming them are
Library operations (Entry.files
itself is a read-only list of the stored relative paths; see
BibDesk’s .bib Format for the background). This
also means the library must have a .bib
path: a from-scratch library must be saved before files can be
attached.
Attach a file with Library.add_file.
The filename may be absolute, or relative to the library’s directory
or to the current working directory (a relative name that exists in
both places is rejected as ambiguous; pass an absolute path instead).
Here, a PDF that was saved next to the .bib file is attached to the
entry for the book it belongs to:
>>> import warnings
>>> from pathlib import Path
>>> from bibdeskparser import Library
>>> bib = Library("tests/Refs/refs.bib")
>>> bib["Shapiro2012"].files # no attachment yet
[]
>>> pdf = Path("tests/Refs/shapiro-brumer-2012.pdf")
>>> _ = pdf.write_bytes(b"%PDF-1.4 fake")
>>> with warnings.catch_warnings():
... warnings.simplefilter("ignore") # no macOS bookmark support here
... _ = bib.add_file("Shapiro2012", "shapiro-brumer-2012.pdf")
>>> bib["Shapiro2012"].files
['shapiro-brumer-2012.pdf']
For a file that exists, a macOS bookmark is created automatically
(with the bibdeskparser[macos] extra installed), so BibDesk can
still locate the file after it is moved or renamed outside of
bibdeskparser. To link a file that does not exist here – say, one
that lives only on another machine – pass
check_that_file_exists=False; the name is then stored as-is,
relative to the library directory, and BibDesk fills in the bookmark
on its next save, once the file appears.
Library.rename_file
renames (or, given a path with a directory component, moves) the file
on disk and
updates every entry that links it, each with a fresh bookmark; it
returns the new stored path (relative to the library directory), as
does add_file:
>>> with warnings.catch_warnings():
... warnings.simplefilter("ignore") # no macOS bookmark support here
... bib.rename_file(
... "Shapiro2012", "shapiro-brumer-2012.pdf", "Shapiro2012.pdf"
... )
'Shapiro2012.pdf'
>>> bib["Shapiro2012"].files
['Shapiro2012.pdf']
>>> Path("tests/Refs/Shapiro2012.pdf").exists()
True
Library.unlink_file
removes an attachment, and
Library.replace_file
swaps one file for another in place. Both require an explicit
remove keyword argument: whether to also delete the (old) file from
the filesystem – moved to the Trash on macOS (with the
bibdeskparser[macos] extra), deleted permanently elsewhere, and
never deleted while another entry still links it:
>>> bib.unlink_file("Shapiro2012", "Shapiro2012.pdf", remove=False)
>>> bib["Shapiro2012"].files
[]
>>> Path("tests/Refs/Shapiro2012.pdf").exists() # remove=False
True
>>> bib.save()
How to auto-file attachments
Instead of naming every attachment by hand, let bibdeskparser
auto-file them (BibDesk’s AutoFile feature): move each file into a
configured location and rename it according to a
file-name format. Configure it once in the
[auto_file] table of bibdeskparser.toml:
[auto_file]
format_spec = "%f{Cite Key}%u0%e" # <citation key><the file's extension>
location = "." # directory, relative to the .bib file
(equivalently, for the current process only,
Library.config.auto_file.format_spec = "%f{Cite Key}%u0%e").
With that in place,
Library.rename_file
without a new filename files an existing attachment, and
Library.eval_format_spec
with a filename previews the generated path without moving anything:
>>> _ = Path("tests/Refs/2018_AAMOP_sola.pdf").write_bytes(
... b"%PDF-1.4 fake"
... )
>>> bib.config.auto_file.format_spec = "%f{Cite Key}%u0%e"
>>> with warnings.catch_warnings():
... warnings.simplefilter("ignore") # no macOS bookmark support here
... rel = bib.add_file("SolaAAMOP2018", "2018_AAMOP_sola.pdf")
>>> rel # attached under its original name
'2018_AAMOP_sola.pdf'
>>> bib.eval_format_spec("SolaAAMOP2018", filename=rel) # preview only
'SolaAAMOP2018.pdf'
>>> with warnings.catch_warnings():
... warnings.simplefilter("ignore") # no macOS bookmark support here
... bib.rename_file("SolaAAMOP2018", rel) # move and rename the file
'SolaAAMOP2018.pdf'
>>> bib["SolaAAMOP2018"].files
['SolaAAMOP2018.pdf']
Re-running rename_file on an already-filed attachment is a no-op (a
name that matches the format is kept), so auto-filing can safely be
applied across a whole library.
By default, add_file attaches a file under its original name, as
above; filing is a separate, on-demand step. Setting
file_automatically = true in [auto_file] makes add_file
auto-file every new attachment immediately. Per call, an explicit
auto_file_location (or format_spec) also enables auto-filing –
here into a subdirectory next to the .bib file:
>>> _ = Path("tests/Refs/simpson_chapter.pdf").write_bytes(
... b"%PDF-1.4 fake"
... )
>>> with warnings.catch_warnings():
... warnings.simplefilter("ignore") # no macOS bookmark support here
... bib.add_file(
... "JuhlARNMRS2020",
... "simpson_chapter.pdf",
... auto_file_location="Papers",
... )
'Papers/JuhlARNMRS2020.pdf'
>>> bib.config.auto_file.format_spec = None # restore the default
>>> bib.save()
(Conversely, auto_file_location="" forces a plain attach even with
file_automatically = true.)
How to define or rename a @string macro (journal abbreviation)
Define a macro through
Library.strings,
then reference it in a field with a bare (unquoted) string; rename it
everywhere it is used with
Library.rename_string.
Here, a long book series title is turned into a macro:
>>> bib.strings["aamop"] = (
... "Advances In Atomic, Molecular, and Optical Physics"
... )
>>> entry = bib["SolaAAMOP2018"]
>>> entry["booktitle"] = "aamop" # macro-shaped str: a bare reference
>>> entry["booktitle"]
'aamop'
>>> bib.rename_string("aamop", "adv_amop") # updates referencing entries
>>> entry["booktitle"]
'adv_amop'
>>> bib.strings["adv_amop"]
'Advances In Atomic, Molecular, and Optical Physics'
How to organize entries into groups
Library.groups is a
dict-like mapping of each group name to the tuple of its members’
citation keys. Create, replace, or delete whole groups through the
mapping interface; add or remove individual keys with
Library.add_to_group /
Library.remove_from_group.
Every affected entry’s
Entry.groups (a
read-only tuple) updates immediately.
>>> bib.groups["To Read"] = () # create an empty group
>>> bib.add_to_group("To Read", "BrifNJP2010", "KochEPJQT2022")
>>> bib.groups["To Read"]
('BrifNJP2010', 'KochEPJQT2022')
>>> bib["BrifNJP2010"].groups
('To Read',)
>>> bib.remove_from_group("To Read", "KochEPJQT2022")
>>> bib.groups["To Read"]
('BrifNJP2010',)
Assigning to a group name replaces its membership wholesale (creating
the group if needed), and del removes the group entirely, dropping it
from every member entry’s .groups:
>>> bib.groups["To Read"] = ("KochJPCM2016", "KochEPJQT2022")
>>> bib["KochJPCM2016"].groups
('To Read',)
>>> del bib.groups["To Read"]
>>> bib["KochJPCM2016"].groups
()
Group values are always tuples, so a group’s membership can never be
mutated in place; the mapping and every entry’s .groups therefore
stay consistent – including when an entry is deleted from the library
or renamed with
Library.rekey, which
update the group data as well. add_to_group requires the group to
exist already (create it first, e.g. with an empty tuple), and all
assigned keys must belong to entries in the library.
How to tag entries with keywords
Library.keywords
works just like .groups, mapping each keyword to the tuple of
citation keys of the entries carrying it, with
Library.add_to_keyword /
Library.remove_from_keyword
for per-key changes and
Entry.keywords as
the read-only per-entry tuple:
>>> bib.add_to_keyword("Review", "BrifNJP2010", "KochEPJQT2022")
>>> bib.keywords["Review"]
('BrifNJP2010', 'KochEPJQT2022')
>>> bib["BrifNJP2010"].keywords
('OCT', 'Coherent Control', 'Review')
>>> bib.remove_from_keyword("Review", "KochEPJQT2022")
>>> bib.keywords["Review"]
('BrifNJP2010',)
>>> del bib.keywords["Review"]
>>> bib["BrifNJP2010"].keywords
('OCT', 'Coherent Control')
Unlike groups, keywords live inside each entry (as its stored
keywords field), so there is no separate creation step:
add_to_keyword creates a keyword the moment the first entry carries
it, and a keyword with no entries simply does not exist (assigning
() is equivalent to deleting it). The keywords field is readable
through the entry’s dict interface (entry["keywords"] returns the
comma-joined string), but deliberately not writable that way
(entry["keywords"] = ... raises KeyError); routing all keyword
edits through the Library is what keeps bib.keywords and every
entry’s .keywords consistent at all times. Since these edits change
the entry’s stored fields, they also bump its date-modified and mark
it as modified since it was loaded.
How to search a library
Library.search
returns the entries matching a query, best match first:
>>> bib = Library("tests/Refs/refs.bib")
>>> [e.key for e in bib.search("krotov monotonic convergence")]
['GoerzSPP2019']
Accented text is found by its accented, accent-stripped, and transliterated spellings alike:
>>> for query in ("Schrödinger", "Schrodinger", "Schroedinger"):
... print([e.key for e in bib.search(query, fields=["title"])])
['WP_Schroedinger']
['WP_Schroedinger']
['WP_Schroedinger']
A bare @string macro reference is found both by the macro’s name and
by its expansion (and fields limits the search, with the pseudo-field
"key" selecting the citation key):
>>> [e.key for e in bib.search("epjd", fields=["journal"], match="exact")]
['Luc-KoenigEPJD2004']
>>> [e.key for e in bib.search("Eur. Phys. J. D", match="exact")]
['Luc-KoenigEPJD2004']
>>> [e.key for e in bib.search("tannor", fields=["key"])]
['Tannor2007', 'TannorBookChapter1991']
The match argument sets the match strictness, from "exact"
(verbatim substring, up to case) through "folded" (accent-insensitive)
and "words" (the default: most of the query’s words occur, in any
order) to "fuzzy" (tolerates small typos); match="regex" instead
treats the query as a regular expression:
>>> bib.search("Universitat Kassel", match="exact")
[]
>>> [e.key for e in bib.search("Universitat Kassel", match="folded")]
['GoerzPhd2015']
>>> bib.search("quantom speed limit", match="fuzzy")[0].key
'GoerzJPB2011'
>>> [
... e.key
... for e in bib.search(
... r"^The quantum speed limit", fields=["title"], match="regex"
... )
... ]
['GoerzJPB2011']
At the "fuzzy" level, two words match when they agree on about 80% of
their letters. In practice this forgives a single typo per word – one
wrong, missing, or extra letter, or one adjacent swap – and along the
way bridges US/UK spellings and the plain-ASCII spelling of a
transliterated name. Because the threshold is a fraction of the word,
shorter words tolerate less: a four-letter word can lose or gain a
letter but not swap one for another, and a three-letter word is
essentially exact-only. A second typo survives only in longer words.
Query word |
Entry word |
Matches? |
Note |
|---|---|---|---|
|
|
yes |
one wrong letter |
|
|
yes |
one missing letter |
|
|
yes |
one extra letter |
|
|
yes |
adjacent swap |
|
|
yes |
US/UK spelling |
|
|
yes |
ASCII vs. transliteration |
|
|
yes |
short word, missing letter |
|
|
no |
short word, wrong letter |
|
|
no |
three-letter word |
A complementary guard rejects word pairs whose lengths differ by more
than two characters, and the "words"/"fuzzy" levels additionally
require at least 70% of the query’s words to match, so a two-word query
tolerates one unmatched word but a three-word query still needs two of
its three.
On the command line, the search subcommand prints the matching keys
one per line, which composes with the other subcommands:
$ bibdeskparser render tests/Refs/refs.bib \
$(bibdeskparser search tests/Refs/refs.bib "tractor atom")
How to find and resolve duplicate citation keys
A .bib file with two entries sharing a key still loads; the
duplicated keys are reported via
Library.duplicate_keys
(and a UserWarning at load time). Give the offending entry a new key
directly in the .bib file, then reload:
>>> with warnings.catch_warnings():
... warnings.simplefilter("ignore") # the duplicate-key warning itself
... dup_bib = Library("tests/Refs/with_duplicates.bib")
>>> dup_bib.duplicate_keys
('GoerzSPP2019',)
How to automatically generate citation keys
Configure an auto-key format (in BibDesk’s
format-specifier language) in your
bibdeskparser.toml, then call
Library.rekey with
just the old key:
[auto_key]
format_spec = "%a1%c{journal}0%Y%u0"
This particular format is a recommended scheme for a journal article:
first author’s last name, the journal’s initials, the full year, and
(only if needed) a disambiguating letter — e.g. GoerzPRA2014. For a
library that mixes entry types, give format_spec a
per-type table instead (naming booktitle
for conference papers, series for books, and so on). The [initials]
table of the configuration handles venues whose initials should not be
the plain acronym (see Venue initials).
>>> bib = Library("tests/Refs/refs.bib")
>>> bib.rekey("GoerzPRA2014")
'GoerzPRA2014'
The format_spec argument overrides the configured format ad hoc; keys
that already match the format are kept unchanged, so regenerating is
idempotent and safe to re-run over many entries (here, one group;
entries lacking a field the format requires, such as author, are
reported with a ValueError):
>>> bib.rekey("GoerzPRA2014", format_spec="%a1%c{journal}0%Y%u0")
'GoerzPRA2014'
>>> for key in bib.groups["My Papers"]:
... _ = bib.rekey(key, format_spec="%a1:%Y%u0")
>>> bib.rekey("Goerz:2014", format_spec="%a1%c{journal}0%Y%u0")
'GoerzNJP2014'
Library.eval_format_spec
evaluates a format for an entry and returns the resulting key without
renaming anything. Since a key that already matches the format evaluates to
itself, this finds all citation keys that do not follow a given
format:
>>> fmt = "%a1:%Y%u0"
>>> [
... key
... for key in bib.groups["My Papers"]
... if bib.eval_format_spec(key, fmt) != key
... ]
['GoerzNJP2014']
On the command line, the same is available as bibdeskparser rekey BIBFILE OLD_KEY (see the CLI reference), which
prints the generated key, and as the read-only bibdeskparser eval_format_spec BIBFILE KEY [FORMAT].
How to add a reference from a DOI, arXiv ID, or search query
Library.add fetches the
metadata from the appropriate online source (Crossref for a DOI or free-text
search, the arXiv API for an arXiv identifier), sanitizes it, and
adds a new entry:
bib.add("10.1103/PhysRevA.89.032334") # a DOI
bib.add("https://arxiv.org/abs/2205.15044") # an arXiv preprint
bib.add("pulser open-source pulse sequences") # best search match
bib.save()
On the command line (CLI reference):
$ bibdeskparser add tests/Refs/refs.bib 10.1103/PhysRevA.89.032334
MuellerPRA2014
$ bibdeskparser add tests/Refs/refs.bib --dry-run some paper title # no write
The new entry follows the library’s conventions automatically: the
journal is stored as an @string macro (see the
[journal_macros] configuration), the title
is brace-protected, and the citation key is generated (e.g.
MuellerPRA2014, or Goerz2205.15044 for a preprint). An entry
whose DOI or arXiv eprint is already in the library is rejected, so
re-adding the same paper is safe.
To also store the paper’s abstract in the new entry, pass
add_abstract=True (--add-abstract on the command line); to also
search arXiv for a matching preprint and record it in the eprint
field, pass add_preprint=True (--add-preprint). See the next two
recipes for filling in abstracts and arXiv identifiers after the
fact. To make either behavior the default, set it once in the
[add] configuration table:
[add]
add_abstract = true
add_preprint = true
How to fill in missing abstracts
Library.add_abstract
fetches the abstract of an existing entry – from Crossref (via the entry’s
doi), the entry’s attached PDF (requires the
poppler pdftotext tool), the
arXiv API (via eprint), or Semantic Scholar – cleans it to
plain-unicode prose, and stores it in the entry’s abstract field:
result = bib.add_abstract("SauvagePRXQ2020")
bib.save()
Each result carries the source it came from and a confidence level;
only a high-confidence abstract (identified by the entry’s
doi/eprint, or confirmed by two independent sources) is stored by
default. On the command line, list the entries that need an abstract,
fill them in bulk, and review what remains
(CLI reference):
$ bibdeskparser keys tests/Refs/refs.bib --type article --missing abstract
TuriniciHAL00640217
SauvagePRXQ2020
Vecheck2022.09.09.507322
KatrukhaNC2017
$ bibdeskparser add_abstract tests/Refs/refs.bib \
SauvagePRXQ2020 Vecheck2022.09.09.507322
SauvagePRXQ2020: stored (crossref, high)
Vecheck2022.09.09.507322: needs review (semanticscholar, medium) [cr-miss]
Quantum biology examines quantum effects in living cells ...
A lower-confidence candidate is reported in full instead of stored;
after checking it (against the PDF or the publisher page), apply it
with set_field, or store whatever the sources found by lowering the
bar with --min-confidence medium:
$ bibdeskparser set_field tests/Refs/refs.bib Vecheck2022.09.09.507322 \
abstract "Quantum biology examines quantum effects in living cells ..."
For an entry whose abstract genuinely cannot be found, store an
empty abstract as an “audited” marker – either explicitly
(set_field KEY abstract "") or with add_abstract --mark-empty.
The entry then no longer shows up in keys --missing abstract (it is
matched by keys --empty abstract instead), so repeated fill-in
passes stay fast and idempotent.
How to fill in missing arXiv identifiers
Library.add_preprint
searches arXiv for the preprint of an existing entry and records its identifier in
the entry’s eprint field (along with archiveprefix = arXiv):
result = bib.add_preprint("WinckelIP2008")
bib.save()
A search result is stored only on a confident match: an arXiv DOI
equal to the entry’s doi, a near-exact title match, or a good title
match corroborated by the first author. On the command line, list the
entries whose preprint status is unknown and fill them in bulk
(CLI reference):
$ bibdeskparser keys tests/Refs/refs.bib --type article --missing eprint
WinckelIP2008
TuriniciHAL00640217
Vecheck2022.09.09.507322
$ bibdeskparser add_preprint tests/Refs/refs.bib --mark-empty \
WinckelIP2008 Vecheck2022.09.09.507322
WinckelIP2008: no preprint found (stored empty marker) [best-ratio=0.42]
Vecheck2022.09.09.507322: no preprint found (stored empty marker) [best-ratio=0.31]
With --mark-empty (or mark_empty = true in the
[add_preprint] configuration table), an entry for
which no preprint is found gets an empty eprint field. Like the
empty-abstract marker of the previous recipe, this records “searched,
nothing found”: the entry moves from keys --missing eprint to
keys --empty eprint, so repeated fill-in passes do not re-query
arXiv for it. Since a non-match can also be a matching failure,
re-audit those markers occasionally:
$ bibdeskparser add_preprint tests/Refs/refs.bib $(bibdeskparser keys \
tests/Refs/refs.bib --empty eprint)
A match that the search rejects as postdated-unverified (an arXiv
submission years after the entry’s publication, without a
corroborating journal reference) is only reported; if reviewing it
shows it really is the paper’s preprint (authors do post old papers
late), record it explicitly, which needs no network access:
$ bibdeskparser add_preprint tests/Refs/refs.bib WinckelIP2008 \
--eprint 2505.01234
The search respects the arXiv API’s rate limit of one request every three seconds, so filling in a large library takes time – let it run.
How to import BibTeX entries from a publisher or another library
Library.import_bibtex
runs any BibTeX snippet – a publisher’s “export citation” download, or
entries from another .bib file – through the same sanitization and
adds the entries:
bib.import_bibtex(text) # text: BibTeX for one or more entries
bib.save()
On the command line, import reads from a file, stdin, or a URL
(CLI reference):
$ bibdeskparser import tests/Refs/refs.bib entries.bib
$ pbpaste | bibdeskparser import tests/Refs/refs.bib --stdin
$ bibdeskparser import tests/Refs/refs.bib --url https://example.com/refs.bib
Since export writes exactly the kind of snippet that
import accepts (including the @string definitions), this also
moves entries between libraries:
$ bibdeskparser export tests/Refs/refs.bib GoerzPRA2014 \
| bibdeskparser import other.bib --stdin
GoerzPRA2014
If anything about a snippet is not acceptable (an undefined macro, an entry whose DOI is already present, …), the whole import is rejected with a list of all problems and the library is left untouched.
How to edit an entry in your text editor
Library.edit opens one
or more entries in $EDITOR as bibtex text and merges back whatever you save:
bib.edit("GoerzQ2022") # a single entry
bib.edit("GrondPRA2009a", "GrondPRA2009b", editor="vim") # several
How to render a bibliography in a specific citation format
Library.render
produces a formatted citation string for one or more citation keys; pass format="markdown"
(default), "tex", or "html". When rendering several entries, style
controls their layout: "default", "paragraphs", "numbered list",
or "itemized list".
>>> bib = Library("tests/Refs/refs.bib")
>>> print(bib.render("Evans1983", format="html"))
L. C. Evans. <a href="https://math.berkeley.edu/~evans/control.course.pdf"><i>An Introduction to Mathematical Optimal Control Theory</i></a> (1983). Lecture Notes, University of California, Berkeley.
>>> print(bib.render("Tannor2007", format="tex"))
D. J. Tannor. \href{https://uscibooks.aip.org/books/introduction-to-quantum-mechanics-a-time-dependent-perspective/}{\textit{Introduction to Quantum Mechanics: A Time-Dependent Perspective}}. University Science Books, Sausalito, California (2007).
>>> print(
... bib.render(
... "GrondPRA2009a", "GrondPRA2009b", style="numbered list"
... )
... )
1. J. Grond, J. Schmiedmayer and U. Hohenester. [*Optimizing number squeezing when splitting a mesoscopic condensate*](https://link.aps.org/doi/10.1103/PhysRevA.79.021603). [Phys. Rev. A **79**, p. 021603](https://doi.org/10.1103/physreva.79.021603) (2009), [arXiv:0806.3877](https://arxiv.org/abs/0806.3877).
2. J. Grond, G. von Winckel, J. Schmiedmayer and U. Hohenester. [*Optimal control of number squeezing in trapped Bose-Einstein condensates*](https://link.aps.org/doi/10.1103/PhysRevA.80.053625). [Phys. Rev. A **80**, p. 053625](https://doi.org/10.1103/physreva.80.053625) (2009), [arXiv:0908.1634](https://arxiv.org/abs/0908.1634).
How to give an AI coding agent access to your library
bibdeskparser ships no dedicated AI integration, because it does not
need one: the command-line tool is the integration
surface.
Any agent that can run shell commands, such as
Claude Code, can inspect and edit
your BibDesk library by calling bibdeskparser. Each invocation is a
one-shot process that loads the .bib file, does its work, and exits,
so there is no server to run and nothing to keep alive.
1. Put the tool on PATH. Install the package into an environment
the agent can reach (see Installation); the simplest way is
uv tool install bibdeskparser. Verify that bibdeskparser runs from
a plain shell:
$ bibdeskparser --version
2. Point it at your library. Set default_bib_file in a
bibdeskparser.toml (see Configuration) so commands
need no path argument:
default_bib_file = "/Users/you/Documents/references.bib"
Otherwise the agent must pass the .bib path as the first argument to
every command.
3. Tell the agent the tool exists. In an environment file the agent
reads at startup (for Claude Code, a CLAUDE.md), describe the tool and
point it at the built-in help. The agent discovers the full command set
itself from --help; you only need a few lines:
## Bibliography
My BibDesk reference database is managed with the `bibdeskparser` CLI
(`default_bib_file` is configured, so commands need no path argument).
Run `bibdeskparser --help` for the command list and
`bibdeskparser COMMAND --help` for a command's arguments. Use `--json`
on read-only commands (`show`, `search`, `keys`, ...) for reliable
parsing. To find entries, prefer `bibdeskparser search`. For free-form
edits, pipe modified `export` output back through `edit --stdin`.
4. Reduce permission prompts (optional). Agents that gate shell
access can be told to allow the tool without prompting. In Claude Code,
add bibdeskparser to the allowlist (Bash(bibdeskparser:*) in
.claude/settings.json).
A few properties make the CLI safe to hand to an agent:
Machine-readable output. Every read-only command accepts
--json, so the agent parses structured data instead of scraping text.Concurrent-edit safety. Mutating commands save in place and refuse to overwrite a
.bibfile that changed on disk since it was read (whether by BibDesk, by you, or by another agent), failing with aStaleFileErrorand exit code 1 rather than clobbering the newer version. Nothing coordinates between agents up front, but no write silently loses another’s changes.Nothing blocks. Every command is usable non-interactively.
editandedit_stringsopen$EDITORonly when run from a terminal; an agent passes--stdininstead and pipes in the edited text (see below). Invoked without a terminal and without--stdin/--editor, they fail fast with a usage error rather than hanging on$EDITOR.
For edits beyond the dedicated mutating commands (changing a title,
fixing an author list, adding an arbitrary field), the agent round-trips
an entry through export and edit --stdin: export prints exactly
the text that edit would show in an editor, so transforming that text
and piping it back applies the change, and piping it back unchanged is
a no-op.
$ bibdeskparser export GoerzQ2022 \
| sed 's/Semi-Automatic/Semiautomatic/' \
| bibdeskparser edit GoerzQ2022 --stdin
The @string macro definitions round-trip the same way, from
strings --bib into edit_strings --stdin. Invalid edited text (an
unparseable block, a reference to an undefined macro) exits with code 1
and the list of problems on stderr, leaving the .bib file untouched.
How to export a minimal BibTeX file for LaTeX
Library.export writes
selected entries as plain bibtex text, stripped of BibDesk-only fields; use
format="minimal" to further restrict each entry to a small,
per-entry-type whitelist of citation-relevant fields (dropping things
like abstract and annote), and outfile= to write straight to a
file:
Note how the entries’ abstract and keywords, and the article
entry’s linked file, are all dropped:
>>> bib.export(
... "GrondPRA2009a", "Evans1983", format="minimal", outfile="paper.bib"
... )
>>> print(Path("paper.bib").read_text())
@article{GrondPRA2009a,
Author = {Grond, Julian and Schmiedmayer, J\"org and Hohenester, Ulrich},
Title = {Optimizing number squeezing when splitting a mesoscopic condensate},
Journal = pra,
Year = {2009},
Doi = {10.1103/physreva.79.021603},
Pages = {021603},
Volume = {79},
}
@unpublished{Evans1983,
Author = {Evans, Lawrence C.},
Title = {An Introduction to Mathematical Optimal Control Theory},
Year = {1983},
}