Changelog

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

v0.8.1 - 2026-08-21

  • Fixed: an export with the default minimal field selection no longer drops fields that the entry type requires. Every entry type BibDesk documents now has a field whitelist of its own, holding the type’s required fields together with the optional ones that identify the work (a book’s edition, a report’s number) or locate it (a url, a doi): book, inbook, and proceedings keep publisher, techreport keeps institution, booklet keeps howpublished, manual keeps organization, unpublished keeps note and url, webpage keeps url, periodical keeps journal, jurthesis keeps school, glossdef keeps word/definition, and conference is exported like inproceedings. Previously only article, inproceedings, incollection, mastersthesis, and phdthesis had a whitelist and every other type fell back to author, title, year, so that a minimally exported @book carried no publisher and a @techreport no institution. A stored doi is now exported for every type, including the thesis types. The fallback applies only to entry types outside BibDesk’s own table, i.e. biblatex-only types like @online and types added by a bibdeskparser.toml. [#74, #77]

  • Fixed: export --update FILE (Library.export(update=...)) no longer drops the fields of an entry that its field selection does not cover. A refreshed entry now keeps every field it has in FILE, in FILE’s own field order, with the library’s current value where the library’s entry has the field (and FILE’s value where it does not); the selection – "minimal" by default, and --full/--field on the command line – only adds fields on top of that. Refreshing a file exported with --full therefore no longer strips it down to the minimal fields, and hand-added fields such as an annote survive an update, making the documented “nothing is ever removed” true field by field. [#75, #79]

  • Fixed: setting a field to a URL no longer stores it as a bare @string macro reference. An ordinary URL is a valid BibDesk macro name, so set_field KEY url ... (and the equivalent assignment in Python) stored an unbraced value, which made the next save fail with undefined macro(s) referenced by one or more entries and made export write invalid BibTeX. A plain str is now always stored as literal text when it carries a URL scheme (://), and in any field whose name contains url; MacroString still forces a macro reference. [#78, #80]

  • Added: export (including export --update) now warns about every bare @string macro reference it keeps for a macro that nothing defines – neither the library (nor the strings argument of export_entries) nor the standard month macros – since such output is not valid BibTeX. Only the expand_strings=True path warned before. [#78, #80]

v0.8.0 - 2026-08-03

  • Added: Library.info, a read-write dict-like view of BibDesk’s document info – the key/value metadata that the “Document Info” panel attaches to the database as a whole, stored in the @bibdesk_info block of the .bib file. Keys are matched case-insensitively (preserving their stored spelling and order); values are plain Unicode strings, with the empty string allowed. A mutation regenerates the block in BibDesk’s own layout (deleting the last key removes it from the file) and, on a plain BibTeX file, converts to the database format with a FormatConversionWarning; an unmodified block round-trips byte-for-byte. On the command line, info (read-only) prints the data (all pairs, or the value of a given KEY), and set_info KEY VALUE / delete_info KEY modify it. The %i{Key} format specifier (case-insensitive lookup, empty for a missing key, %i{Key}N truncating to N characters) is now implemented on top of this data instead of raising NotImplementedError. [#69, #70]

  • Fixed: a @bibdesk_info block is now preserved byte-for-byte as long as the document info is not modified. The block has the syntactic shape of an entry, and was previously treated as one: it appeared in the entry API under the pseudo-key document_info (in keys, show, search, the check audits, …), and any save that rewrote the file re-serialized it in entry layout (closing brace fused onto the last field line instead of on its own line) and could plant date-added/date-modified bookkeeping inside it. It is no longer exposed as an entry; a file containing one counts as a BibDesk database for the purpose of plain-format detection. [#69, #70]

  • Added: an assets configuration table declaring the library’s asset files – companion files keyed by citation key (summaries, extracted full texts) or belonging to the library as a whole – as path patterns in the format-specifier language, e.g. summary = "%f{Cite Key}_summary.md" (a trailing slash marks a directory-valued asset, an empty pattern disables a class, and a pattern built only from %i{Key} document info and literal text is library-level). Library.asset(name, key=None) resolves a class to a path relative to the .bib file’s directory, and Library.assets(*keys) reports which asset files exist on disk; on the command line, the asset and assets commands (both taking optional citation keys and --json, asset also --relative). Whether a citation key is required follows from the pattern: a per-entry class needs one and a library-level class refuses one, in both methods (assets without keys reports the library-level classes, and the coverage table over all entries is the explicit bib.assets(*bib) / assets $(bibdeskparser keys)). asset verifies by default that something is on disk at the resolved path – a directory for a directory-valued class, a file otherwise – and raises FileNotFoundError if not; check_that_file_exists=False (CLI: --no-check-exists) resolves without touching the filesystem, which is what a generator needs in order to learn where to write an asset. Patterns are validated when the configuration is loaded; unique (%u/%U/%n), random (%r/%R/%d), and original-name (%l/%L/%e/%E) specifiers are rejected, so resolution is deterministic. See the new “External Assets” documentation page. [#71, #72]

  • Added: two new check audits over the asset files. asset_orphans (on by default, --no-orphans to skip) inverts each per-entry assets pattern into a glob and reports every match on disk that belongs to no entry, e.g. a summary left behind by a delete or named after an old citation key. assets (opt-in via --assets, like --files) reports every resolving asset that is missing from disk. Both audit names appear in the --json output. [#71, #72]

  • Changed: Library.rekey (and the rekey command) now renames the files named after the citation key along with the entry: the entry’s asset files are moved to the paths the new key resolves to (the deepest entry-dependent path component of each assets pattern moves as one unit, so a bundle directory travels whole), and every attachment whose current path matches what the configured auto-file format generates is re-filed under the new key via rename_file (hand-named attachments are left alone; a skipped file – no format for the entry’s type, not following the format, absent from disk, or a target conflict – is reported as a warning, and never fails the rename). The new keyword arguments rename_assets/rename_attachments (CLI: --rename-assets/--no-rename-assets, --rename-attachments/--no-rename-attachments) default to the new rekey configuration table, both true. To keep the previous behavior (rename the key only), set rename_assets = false and rename_attachments = false in the rekey table, or pass the --no-* options. [#71, #72]

  • Added: Library.delete(key, remove_assets=..., remove_attachments=...), backing entry deletion (del) and the delete command (CLI: --remove-assets/--no-remove-assets, --remove-attachments/--no-remove-attachments, defaulting to the new delete configuration table, both false). With removal on, the entry’s asset files and/or attached files are deleted from disk (to the Trash where possible; an attachment still linked from another entry is kept); with removal off (the default), a warning reports any files the deleted entry leaves behind. [#71, #72]

v0.7.0 - 2026-07-28

  • Fixed: reading a file written by export with full=True (--full on the command line) no longer fails. A full export records each attachment as a plain relative path (Bdsk-File-1 = {GrondPRA2009a.pdf}) rather than a base64 binary plist, and every Library load and every CLI command on such a file previously crashed with a bare Error: Invalid file when decoding that value. A bdsk-file-N value that is not a base64 binary plist is now read as a path-only attachment: it appears in Entry.files like any other, round-trips back to the same plain path, and is upgraded to a full base64 attachment (with a fresh bookmark) if renamed or replaced through the Library methods. [#65, #68]

  • Changed: Entry.add_url/Entry.replace_url (and thus Library.add_url/Library.replace_url and the add_url/replace_url CLI commands) now percent-encode their input before storing it, exactly as BibDesk does: every character outside the URL-allowed set is UTF-8 percent-encoded, so a bdsk-url-N value is ASCII by construction (matching BibDesk’s own absoluteString serialization), while an already-encoded URL passes through unchanged (% is left alone, so no double encoding). A raw non-ASCII URL previously stored verbatim could silently disappear when BibDesk reloaded the file. add_url("https://example.com/münchen") now stores https://example.com/m%C3%BCnchen; replace_url encodes only its new_url, matching old_url literally against the stored URLs. import applies the same normalization to incoming bdsk-url-N fields. [#64, #67]

  • Added: a new standard check audit, url_encoding, reporting every URL-type value that holds raw non-ASCII characters: any field whose name contains url (the class exempt from TeX encoding, e.g. the url field) plus the bdsk-url-N links. Such characters break a LaTeX export that typesets the value inside \url{...} (whose verbatim catcodes bypass inputenc, printing garbled glyphs) and are what BibDesk drops when reloading a bdsk-url-N. The message shows the percent-encoded form so the fix is copy-pasteable (Key2020: url contains non-ASCII characters: 'https://example.com/münchen' (use 'https://example.com/m%C3%BCnchen')). Stored url field values are never rewritten automatically (that would break byte-exact round-tripping and diverge from BibDesk, which keeps them verbatim); the audit is the nudge instead. The audit name url_encoding also appears in the --json output. [#64, #67]

  • Added: a .bib file that is not a BibDesk database – no BibDesk header, no group @comment blocks, no bdsk-* fields; e.g., a file written by export – is now recognized as plain BibTeX on load, and save preserves that format: no header is synthesized, no date fields are created, comments, @preamble blocks, and @string definitions are kept in place, and every stored field of every entry is written – unmodified entries in their stored field order, so a file created by export round-trips byte-identically, and modified entries in BibDesk’s field order, in the export layout. All commands that modify the .bib file (set_field, delete, rekey, set_string, import, …) thus work on exported files without converting them. The plain-format options – value encoding (Unicode or TeX-encoded), @string expansion, and preprint export form – are read from the marker line that every export now writes as its first block (%% Created by BibDeskParser (unicode, preprints as unpublished).), or, for a marker-less file, detected from the file’s content; Library.export gained a marker parameter (CLI: --marker/--no-marker, default on) controlling the marker line, which is never written when the output includes bdsk-* fields. A mutation that introduces database-only state – creating or assigning a static group, attaching a file, or adding a URL – converts the library to the BibDesk database format instead, with a new FormatConversionWarning (re-exported from the top-level package) naming the trigger. [#62, #63]

  • Added: export --update FILE KEY... (with the keys optional), via a new update= parameter of Library.export, refreshes a previously exported file from the library, which is only read: every key in FILE that the library knows – or, with KEYs given, exactly those keys, appended if not yet present – is replaced by a fresh export, the @string block is regenerated as the sorted union of the old block and the definitions the rewritten entries need (with the library’s current value for every macro the library defines, which is how corrected journal abbreviations propagate, and the file’s own value for a macro only it defines), and everything else – entries the library does not know, comments, @preamble blocks, unused definitions – is kept; nothing is ever removed, and entries written by an update never include bdsk-* fields. The --unicode/--expand-strings/--preprint options default to the target file’s own recorded or detected options; an explicit flag overrides them and is recorded in the rewritten marker, so it is sticky for subsequent updates. The target must already exist (use --outfile to create a new export) and be plain BibTeX; a BibDesk database is refused. [#62, #63]

  • Changed: export now defaults to the minimal field selection – the fields parameter of Library.export defaults to "minimal" instead of "full" – and the CLI flag pair --minimal/--no-minimal is renamed to --minimal/--full. To adapt, pass --full (Python: fields="full") for the previous behavior. In particular, export KEY | import other.bib --stdin as a library-to-library transfer now moves a minimal skeleton unless --full is passed, and the export-to-edit --stdin round-trip must be spelled export KEY --full --preprint stored | edit KEY --stdin, since a field missing from the text piped into edit --stdin is deleted from the entry. [#62, #63]

  • Changed: the command-line tool now reports warnings raised while a command runs (a group/file/URL command converting a plain BibTeX file, the macOS-bookmark fallback of add_file, a duplicate-citation-key warning on load) as the same clean Warning: lines on stderr it uses for save-time warnings, instead of letting them surface through Python’s location-prefixed warning display. [#62, #63]

  • Changed: a newly constructed Entry no longer carries date-added/date-modified; the fields are stamped when the entry is added to a Library in the BibDesk database format, so that adding entries to a plain BibTeX file (directly, or via import/add) never plants date bookkeeping there. A date-added already stored on an entry (e.g. preserved by import) is kept as before, and Entry.date_added/Entry.date_modified are None for a detached entry. [#62, #63]

v0.6.0 - 2026-07-27

  • Fixed: the check command now audits every bare (unbraced) field value for a reference to an undefined @string macro, not just journal. Such a reference renders as the macro name itself (month = sept becomes literal sept, publisher = elsevir becomes elsevir) and is refused by Library.save, so a file could previously pass check and then be impossible to write (a subsequent bibdeskparser set_field ... failing with an undefined macro(s) referenced by one or more entries error naming those bare values); a passing check now implies a writable file. Only a value that is a valid macro name is considered, so a bare non-macro value like volume = 90 is not flagged, and keywords (always literal text) is exempt, matching what save scans. The audit name in the --json output is undefined_macro; an undefined macro that is also not a month (month = sept) is reported by both the month and the undefined_macro audit. [#56, #61]

  • Added: two new check audits over the date fields, reporting values that the citation-key specifiers %Y and %m mis-read in silence. year reports an entry whose year does not read as a four-digit year (Monroe2008: year 'August, 2008' does not read as a four-digit year (%Y gives '0')), which is what %Y reduces (about 1984), in press, and n.d. to as well. A value BibDesk reads correctly without being a bare four-digit string still passes: 08 maps into 1950–2049, and trailing text after the digits (2008a, 2001--) is ignored. month reports an entry whose month is anything but a bare reference to one of the twelve standard month macros jan … dec, either a literal value (Monroe2008: month is the literal string 'June', not one of the twelve standard month macros (jan ... dec)) or a macro outside the twelve, defined or not (month = sept). A literal 06 or June is reported although it renders correctly, for the same reason a literal journal value is: a .bst style typesets jun as June, Jun., Juni, or 6, and writing the month out freezes one of those choices into the database. Unlike the year audit, the month audit inspects the stored value rather than what %m renders, since %m answers 01 for every value it cannot parse, indistinguishable from a genuine January. Both audits look only at a field the entry defines with a non-empty value; an absent or empty one is reported by the required_fields and empty_fields audits instead. The audit names year and month also appear in the --json output. [#55, #60]

  • Changed: Library.eval_format_spec (and the eval_format_spec CLI command) now always returns the evaluated citation key, even one that equals the entry’s own crossref value, instead of raising ValueError: evaluating a format is a preview and applies nothing. Library.rekey (and the rekey command) still refuses to apply such a key. The check --key-format audit reports such an entry as an ordinary key deviation, naming the generated key, rather than as unevaluable. [#60]

  • Added: two new check audits over every entry. entry_type reports an entry whose type is not one of the recognized entry types (Bogus2026: unrecognized entry type 'bogustype'), and required_fields reports each field the entry’s type lists as required that the entry does not have (NoYear2014: missing required field 'year' for entry type 'article'), one problem per field. Loading a .bib file deliberately never validates, so nothing had ever looked at either: an @article holding only a doi, and an entry of a type that does not exist, both passed the gate. A defined-but-empty field counts as missing, so year = {} is reported by both required_fields and empty_fields. An entry whose type is unrecognized is not additionally audited for required fields, and a recognized type BibDesk does not template (an extended biblatex type such as dataset) has no required fields on record and is skipped; a types.NAME table in bibdeskparser.toml declares either, making the type recognized and supplying the required list to audit against. Neither audit is gated by verify_types/verify_fields, which govern what happens when a type or field is assigned in Python, nor exempted by a known-missing group, which records a verified-absent optional field: an @article with no year is not an article, and the fix for one is @misc, which requires nothing. The audit names entry_type and required_fields also appear in the --json output. [#54, #59]

  • Fixed: a citation-key or file-name format that references a field an entry does not have now renders that field as empty, as BibDesk does, instead of refusing to generate anything. The entry simply gets a shorter result: under %a1%c{journal}0%Y%u0, an entry without a journal keys as Smith2020, one without a year as SmithPRA. An entry that renders every specifier of a file-name format empty is filed as a.pdf rather than under the bare extension .pdf, i.e. the format’s required unique specifier fills the stem instead of the file becoming a hidden one (BibDesk guarantees a non-empty name, but not a non-empty stem). A format can therefore reference a field only some entries carry – keying @misc talks by their howpublished venue while the software and lecture notes in the same library key as author plus year – where previously the venue had to be dropped from the format for every @misc entry, since the entries lacking it could not be keyed at all. This affects Library.rekey, Library.eval_format_spec, Library.import_bibtex, Library.add_file, and Library.rename_file, along with the rekey, eval_format_spec, import, add, add_file, and rename_file CLI commands. The check --key-format audit no longer reports such an entry as unevaluable: it now conforms if its key matches the shortened form, and is otherwise reported like any other deviating key, naming the key the format generates. [#53, #58]

  • Fixed: citation-key generation now folds a non-ASCII Latin letter to its ASCII base letter instead of deleting it. Only letters that Unicode decomposes into a base letter plus a combining mark (ü, ğ, ř) were handled before, along with a hand-written table of ligatures and stroked letters (æ, ß, ø, đ); every other letter was silently dropped, so Kılıç keyed as Klc, Masłowski as Masowski, and Əliyev as liyev. The base letter is now read off the Unicode character name, which covers the whole class rather than the codepoints someone thought to list: dotless ı, ł (whose uppercase Ł was in the table), ĸ, ŋ, ə, and 100+ more. Ligatures and digraphs are spelled out (dž → dz), matching BibDesk, which transliterates before falling back to a lossy conversion. Existing keys are unaffected unless they contain such a letter, in which case single-argument rekey (and the check --key-format audit) will now propose the corrected key. Text in a script with no Latin rendering (Greek, Cyrillic, CJK) is still dropped. [#52, #57]

  • Fixed: Library.search (and the search CLI command) now finds such a letter by its ASCII spelling at the folded match level. Its fold handled only decomposable accents plus ß, so Mølmer was reachable by Molmer only at the fuzzy level, below the default, and bibdeskparser search Molmer on a library full of Mølmer entries returned nothing; Kılıç was unreachable by Kilic at any level, since two unfoldable letters in a short word put it under the fuzzy threshold. Entries affected by this move up the ladder, which changes their rank in a result list. Text in a script with no ASCII spelling is left intact by the fold rather than dropped, so a Cyrillic or CJK value is still found by itself. [#52, #57]

  • Fixed: Library.add_preprint and Library.add_doi now compare an author’s last name correctly when it contains such a letter. The fold behind the comparison covered 24 hand-listed letters, so ə, ŀ, ĸ, ʼn, and the rest of the class went unfolded on the entry side but were stripped on the arXiv/Crossref side (əliyev against liyev), which silently disabled the title+author acceptance rung for those entries and, with a known-missing group configured, could record a false verified-absent. [#52, #57]

v0.5.0 - 2026-07-23

  • Changed: the import command now reads its source file from a --file FILE option instead of a positional FILE argument, so a positional argument ending in .bib always names the library, like every other command. This removes the collision where, with a default_bib_file configured, bibdeskparser import from_paper.bib silently claimed the snippet as the library and then failed for lack of a source. Migration: bibdeskparser import library.bib entries.bib becomes bibdeskparser import library.bib --file entries.bib, and bibdeskparser import from_paper.bib (into the default library) becomes bibdeskparser import --file from_paper.bib. Library.import_bibtex, which takes the BibTeX text directly, is unaffected. [#46, #51]

  • Added: a read-only config CLI command that dumps the resolved configuration – the built-in defaults merged with whatever a discovered bibdeskparser.toml sets. Unlike config_path (which reports only the file in effect, and fails when none is found), config shows the effective value of every setting, including the ones a file omits (auto_key.clean, preprint_export, the built-in preprint_archives, …) and the built-in defaults in full when no file exists. The default text output is TOML-shaped, mirroring what a bibdeskparser.toml would contain to reproduce the resolved tunable state (an unset value, or an auto_key/auto_file table without a format_spec, is omitted); --no-types restricts the dump to those user-tunable settings, while the default additionally lists the resolved entry-type/field data model (documented_types, recognized_entry_types, universal_fields, known_fields), and --json prints the complete state as an object with unset values as null. The BIBFILE argument is optional – it only fixes the config-discovery directory, defaulting to the current directory – so the command needs no .bib file and never fails for a missing configuration file. [#45, #50]

  • Added: a --key-format option on the check CLI command, an opt-in audit (off by default) that reports every citation key not matching its expected auto-key format, i.e. every key that eval_format_spec (or single-argument rekey) would regenerate differently. A preprint-only entry is audited against the arXiv preprint format, every other entry against the configured auto-key format; a --format-spec PATTERN option audits against that pattern instead and implies --key-format (combining it with --no-key-format is an error). A key already matching the format evaluates to itself, so disambiguated sibling keys such as SmithPRA2015 and SmithPRA2015a both pass; an entry that lacks a field the format requires is reported as unevaluable rather than silently skipped; and when no format is available at all (no --format-spec and nothing configured), a single message is reported instead of one failure per entry. The audit name key_format also appears in the --json output. [#44, #49]

  • Added: a --files option on the check CLI command, an opt-in audit (off by default) that reports every linked attachment (bdsk-file path) that does not resolve to a real path on disk relative to the .bib directory. It walks each stored path one component at a time, matching case exactly, so besides a link to a deleted file it also catches a link whose spelling differs only in case from the file on disk, one that works on a case-insensitive filesystem (macOS) but breaks on a case-sensitive one (a collaborator’s machine or a Linux CI job); the plain existence check behind the warning a write-in-place command prints for a missing link cannot detect that case-mismatch class, so check --files can fail a library that would be written without any such warning. It is off by default because attachments may legitimately live only on another machine, so a fresh clone of a library whose PDFs are not under version control would otherwise fail wholesale. The audit name files also appears in the --json output; an attachment whose stored path is empty is flagged, and a link resolving to a directory passes (BibDesk can link folders). [#43, #48]

  • Added: a --usage option on the bibdeskparser command-line tool, printing a short usage summary (the one-line description, the usage line, and the list of command names) as a compact alternative to the full --help output. [#47]

  • Changed: running bibdeskparser with no command now prints the short usage summary (see --usage) on stderr and exits 2, instead of dumping the entire --help output. [#47]

  • Fixed: the network commands no longer fail in an environment that routes traffic through a SOCKS proxy (ALL_PROXY=socks5h://..., e.g. an SSH tunnel). Both HTTP stacks used by the package refuse to even attempt a SOCKS connection without an optional helper package, each with a different error: httpx (behind add and import --url) needs socksio, and requests (behind add_preprint, add_doi, and add_abstract, via the arxiv and habanero packages) needs pysocks. Both helpers are now regular dependencies.

  • Fixed: with --dry-run, the per-key reports of the add_abstract, add_preprint, and add_doi CLI commands (and the preprint report of add --add-preprint) now say would store / would mark known missing instead of the past-tense stored / marked known missing, which misread as the file having been modified. The JSON reports are unchanged: applied marks what a real run would store.

  • Changed: the command-line tool now reports save-time warnings as the same clean Warning: lines on stderr it uses for all other warnings, instead of letting them surface through Python’s warning machinery with a meaningless cli.py source location. Warnings about linked files that do not exist are printed individually only up to five; beyond that, they collapse into a single summary line with the total count and the first missing file. Previously, saving a .bib file separated from its attachment tree (e.g. a copy in another directory) flooded stderr with one location-prefixed UserWarning per linked file, on every mutating command.

  • Fixed: importing an entry whose journal spells out a name the library abbreviates no longer aborts with a macro-name collision (publisher and Google Scholar exports spell journals in full, e.g. Physical Review Letters, or Scholar’s lowercased Physical review letters, where the library defines prl = "Phys. Rev. Lett."). When the derived macro name is taken, the incoming name is compared word by word against the colliding macro’s value – a dot-terminated word matches as a case-insensitive prefix, a bare word must match exactly, so Phys. Rev. A does not capture Physical Review Applied – and on a full match the existing macro is reused, with a warning showing the journal_macros alias line that makes the mapping explicit. When the match fails (the colliding macro holds a different journal), the error now prints the exact journal_macros configuration line for each possible intent – appending the incoming spelling to the alias list (canonical value first), or an entry under a fresh macro name / an initials.journal exception – as does the error for a journal_macros entry that conflicts with an existing @string definition. [#40, #42]

  • Added: a with_files argument of Library.keys and a paired --with-files/--without-files option on the keys CLI command, a tri-state filter on file attachments (the bdsk-file-N fields): the default (None, or neither flag) does not filter, True/--with-files keeps only entries with at least one attachment, and False/--without-files only those with none. This composes with the existing keys filters, so bibdeskparser keys --type article --without-files lists the articles still needing a PDF. [#39, #41]

  • Changed: the files, urls, groups, and keywords CLI commands now share one output shape. Each takes any number of citation keys and prints a map from each citation key to its list of values (attachment paths, URLs, group names, or keywords): with keys, exactly those entries (an entry with none maps to an empty list); with no key, every entry in the library that has at least one value, in library order. A new paired --flat/--no-flat option (default --no-flat) instead prints just the values as a bare list, combined across the selected entries with duplicates removed (its order is unspecified); files --flat is thus every file the library references, the reverse index for reconciling against a folder of PDFs. files keeps its --absolute/--relative option; groups and keywords gain a paired --index/--no-index option that prints the inverse map instead, from each static group or keyword to the citation keys it contains (--index takes no keys and does not combine with --flat). Migration: a single-key files KEY / urls KEY / groups KEY / keywords KEY that previously printed a bare list now prints a one-entry KEY: values map; pass --flat for the old bare-list output (single-key files --flat keeps bdsk-file-N numeric order). A bare groups / keywords (no key) previously printed the group/keyword catalog and now prints the per-entry map; pass --index for the catalog. [#39, #41]

  • Fixed: Library.add_preprint (and the add_preprint CLI command) no longer reports a false no-results for an entry whose title or first-author last name contains a Latin letter that Unicode treats as atomic rather than as base-plus-combining-accent (ø, Ø, ł, Ł, ß, æ, œ, đ, ð, þ, ħ, ı, ŋ, and friends) – the canonical case being any title naming the Mølmer-Sørensen gate. Such letters have no Unicode decomposition, so the old accent-folding left them in place and the ASCII-only tokenizer then split the word on them (sørensen became s/rensen), poisoning every arXiv query and silently disabling the title+author acceptance rung for affected first authors. arXiv indexes title characters literally, so queries now preserve these letters intact, while match comparison folds them to ASCII (so an ASCII-spelled publisher title still matches the unicode arXiv record, and vice versa); letter-producing TeX commands in a raw title (e.g. {\o}) are also decoded before a query is built. With a known-missing group configured for eprint, the false negative had additionally marked the entry as verified-absent, excluding it from future searches. [#36, #38]

  • Changed: the names audit of the check command now also flags an author/editor whose parsed first name has a part that cannot be initialized – a hyphen-separated segment that does not begin with a letter after TeX-to-unicode conversion, such as a quoted nickname (`Eunice') copied into the author list, or a stray hyphen that detaches an initial (Meyer, H -D or Meyer, H- D, both of which should render H.-D.). Such values split cleanly into names, so they passed the old audit, yet they make render emit a bogus initial (Y. K. `. Lee) or silently drop the hyphen (H. D. Meyer); the gate now reports them for manual repair rather than letting the corruption surface only in rendered output. [#33, #37]

  • Added: Library.add_doi and a corresponding add_doi CLI command, recording the DOI of an existing entry in its doi field – either an explicitly given DOI (--doi, validated and normalized to its bare lowercase form, no network access), or one found online: the DOI recorded on arXiv for the entry’s eprint (which names the published version of exactly this paper; an arXiv-issued 10.48550/... DataCite DOI does not count), or otherwise a Crossref search by title and first author. A search result is stored only on a confident match (a near-exact title, or a good title corroborated by the first author’s last name); a title-based match whose publication year differs from the entry’s year by more than one is rejected as a likely title collision (reported as year-mismatch for manual review), and an erratum, corrigendum, retraction, comment, or reply never matches an entry that is not itself such an amendment. Preprint-only entries are skipped (the search would find the published version’s DOI, which does not belong on a preprint reference). With a known-missing group configured for doi in the known_missing table of bibdeskparser.toml (e.g. doi = "No DOI"), the group is maintained exactly as add_abstract/add_preprint maintain theirs: members are skipped, a clean no-match marks, storing a DOI unmarks, and overwrite/--overwrite re-audits; entries verified to have no DOI thus also pass the missing-doi audit of check automatically. [#35]

  • Added: --group NAME and --not-group NAME filter options on the keys CLI command, and corresponding group/not_group arguments of Library.keys, keeping only entries that are (respectively, are not) members of the given static groups (repeatable; group names are matched case-sensitively, and an unknown group name is an error rather than an empty result, so a typo cannot silently select nothing, or everything for --not-group). For example, re-audit the entries recorded as having no preprint with bibdeskparser add_preprint --overwrite $(bibdeskparser keys --group "No Eprint"). [#34]

  • Added: a known_missing table in bibdeskparser.toml, mapping a field name to the name of a BibDesk static group that records, per entry, a verified “searched, this info does not exist” status (e.g. abstract = "No Abstract", eprint = "No Eprint", doi = "No DOI"; exposed as Library.config.known_missing). This replaces the previous empty-field markers, which the BibDesk app silently deletes whenever it saves the .bib file; static groups survive BibDesk saves, BibDesk maintains their membership when citation keys change or entries are deleted, they never appear in exported entries, and they can be managed by drag and drop in BibDesk. With the table configured, Library.add_abstract/Library.add_preprint (and the corresponding CLI commands) skip entries in the field’s group (reported as known-missing; overwrite/--overwrite re-searches), add an entry to the group when a search runs cleanly and finds nothing (creating the group on first use; a failed search never marks), and remove the entry from the group whenever a real value is stored; without the table, none of this bookkeeping happens. Membership in the group configured for doi makes the check command accept an article without a doi. [#34]

  • Added: two new check audits: empty_fields flags every defined-but-empty field on any entry (BibDesk deletes empty fields when saving, so the field would silently disappear the next time the library is saved in BibDesk), and known_missing flags an entry that is a member of a configured known-missing group while actually having a non-empty value in that field (e.g. after a manual edit in BibDesk). [#34]

  • Changed: a defined-but-empty field now counts as missing everywhere, and the empty-field “audited” markers are gone. keys --empty and the empty argument of Library.keys are removed (--missing/missing now also match a defined-but-empty field); the mark_empty arguments and --mark-empty options of add_abstract/add_preprint, the mark_empty key of the add_abstract table, and the add_preprint table (whose only key was mark_empty) are removed; a defined-but-empty doi no longer suppresses the missing-doi check problem (membership in the known-missing group configured for doi does instead); set_field KEY FIELD "" is now an error (use delete_field, or record a verified absence via a known-missing group); and applied in the add_abstract/add_preprint results now means the library was modified (the field, or the known-missing group membership). To migrate a library that used the old markers, run bibdeskparser check: every leftover empty field is reported by the new empty_fields audit. For each reported entry, record the verified absence in a group (bibdeskparser set_group "No Eprint" once to create the group, then bibdeskparser add_to_group "No Eprint" KEY...), add the matching known_missing entry to bibdeskparser.toml, and remove the empty field (bibdeskparser delete_field KEY eprint; likewise for abstract and doi). Also delete any mark_empty key and add_preprint table from bibdeskparser.toml. [#34]

  • Removed: Entry.add_abstract and Entry.add_preprint. Use Library.add_abstract(key, ...) and Library.add_preprint(key, ...) instead: the known-missing group bookkeeping and the PDF-attachment lookup both need the library, so the Library methods are the only public fetching entry points. Entry.add_abstract’s pdf_path argument is gone with it (the Library method locates the entry’s first attached PDF itself). [#34]

  • Changed: when no abstract is found and one of the consulted sources failed (an unreachable API, or an attached PDF that could not be read), Library.add_abstract now returns source="error" instead of "none", mirroring add_preprint’s match="error"; the negative is not conclusive, so it never records a verified absence. Previously a network failure was indistinguishable from a definite “no abstract exists anywhere”. [#34]

v0.4.0 - 2026-07-21

  • Fixed: Library.render (and the render CLI command) no longer drops the editors of an entry that has an editor but no author (e.g. an edited volume; a proceedings entry in particular can never have an author). The editors now render in the authors position, marked with an (ed.)/(eds.) suffix, e.g. E. Andersson and P. Öhberg (eds.); the edited by ... piece of the published-in segment (for inproceedings and book-family entries) is correspondingly only rendered when the entry also has authors. [#25, #32]

  • Fixed: Library.render (and the render CLI command) no longer drops most fields of book-family entries: an inbook entry previously rendered as author/title/year only (dropping publisher, series, volume, chapter, pages, and booktitle), and an incollection entry dropped its editor, pages, series, and volume. The book family (book, inbook, incollection, and the previously unhandled proceedings) now renders uniformly as series Vol. N, publisher, address (year), Chapter N, pp. N1–N2 (each piece only if present), prefixed for inbook/incollection by In: *booktitle* and, when there are editors, by edited by .... [#25, #32]

  • Fixed: TeX commands stored in field values (typically in note, occasionally in a title) no longer leak into the markdown/html output of Library.render (and the render CLI command). \url{...} and \href{...}{...} now render as hyperlinks, \texttt{...} as monospace text (a markdown code span / <code>), \textit{...} and \emph{...} as italics, and \textbf{...} as bold, recursively in their arguments; the escaped characters \&, \%, \$, \#, and \_ lose their backslash, and the TeX non-breaking space ~ becomes a plain space. In particular, a \texttt{...} in a title is no longer mangled by the title-protection brace stripping (previously \texttt{JAX} rendered as \textttJAX); an unrecognized command now passes through verbatim, with its braced argument intact. TeX output (format="tex") is unchanged and passes all TeX markup through. [#25, #32]

  • Added: a read-only check CLI command, running the standing audits over the library (or, with KEY... arguments, over just the given entries) and reporting every problem found, one per line, followed by a PASS/FAIL summary line; the exit code is 0 if all audits pass and 1 otherwise, making the command a pass/fail gate, e.g. for an agent after a batch of edits. The audits: the file parses cleanly (no skipped blocks); no citation key occurs more than once; every article that is not a preprint has a doi (a defined-but-empty doi marks an entry verified to have none, and passes); every journal field references a defined @string macro (a literal journal value is a problem, unless it is a recognized preprint pseudo-journal like arXiv:2205.15044); every author and editor field parses as names; and every @string macro defined in the file is referenced by some entry. In per-key mode, the unused-macros audit is skipped and the duplicate-key audit reports only the given keys, while parse problems are always reported. With --json, the report is an object with passed, entries_checked, and problems members, each problem carrying the audit name, the citation key (null for a problem not tied to an entry), and a message. [#31]

  • Fixed: reading Entry.author/Entry.editor for an unparseable name field (e.g. a name with too many commas) now raises bibtexparser’s descriptive InvalidNameError (a ValueError subclass, as documented), instead of an IndexError with no useful message. [#31]

  • Added: preprint-only entries are now a first-class concept, covering arXiv and other preprint servers (see the new “Preprints” documentation page). An entry is preprint-only if its journal is a pseudo-journal <Archive>:<identifier> (e.g. {arXiv:2205.15044}, {bioRxiv:2022.09.09.507322}, {HAL:hal-00640217}) with a recognized archive, or if it is a misc or unpublished entry with an eprint from a recognized archive (e.g. arXiv’s own BibTeX export). The built-in archives are arXiv, bioRxiv, medRxiv, ChemRxiv, HAL, and SSRN; a new preprint_archives table in bibdeskparser.toml (exposed as Library.config.preprint_archives) adds further archives, mapping the archive’s canonical spelling to a URL template for its preprint pages. Library.import_bibtex (and Library.add) normalize a preprint-only entry to its canonical stored form: an @unpublished entry with the pseudo-journal in the archive’s canonical spelling (hal: → HAL:; synthesized from the eprint if absent), the eprint (version suffix stripped) and archiveprefix fields derived from the pseudo-journal if missing, and the doi extracted from a https://doi.org/... value in the url field or (for arXiv) derived as 10.48550/arXiv.<identifier>; a url that merely restates the archive’s page for the identifier is dropped when the entry carries a doi; the preprint citation-key format (Goerz2205.15044) applies to all archives. An archive field holding the link base derivable from the eprint/archiveprefix is dropped on import (exports regenerate it), for preprint-only and published entries alike. The publication-status note recommended by the documentation (“preprint only”, “submitted to Phys. Rev. Lett.”, “lecture notes”) is never filled in automatically: a missing note is the signal to record the status by hand. A pseudo-journal whose archive is not recognized is rejected as a validation problem instead of being mangled into a nonsense @string macro (this also catches URLs pasted into the journal field). Library.add for an arXiv query now also records the primaryclass (the arXiv category). [#30]

  • Added: a keep_journals argument to Library.import_bibtex (--keep-journals/--no-keep-journals on the import CLI command; default off), preserving every incoming journal field as-is instead of converting it to an @string macro reference, and keeping the incoming entry type (preprint-only entries are still recognized for the eprint/archiveprefix derivation and the citation key, and unrecognized archive prefixes are then no error). [#30]

  • Added: a preprint argument to Library.export (--preprint on the export CLI command), selecting the form a preprint-only entry is exported as, independent of its stored form: "unpublished" or "misc" – the structured eprint-field forms, for BibTeX styles that render the eprint field (REVTeX, elsarticle, biblatex); "unpublished" guarantees the entry type’s required note field in minimal exports, writing the stored note or the text “preprint” (full exports never synthesize it, so a full-export round trip cannot plant a note in a library) – "article" – the pseudo-journal form, with the DOI written as its resolver address in url, for classic styles (plain, unsrt, IEEEtran, …) that would silently drop an eprint – or "stored" for no transformation. Minimal exports reduce to the essential fields of the chosen form, always including eprint/archiveprefix for the structured forms and keeping a stored note; an explicit --field list always exports the stored fields. The default is the new preprint_export setting in bibdeskparser.toml ("unpublished" unless configured; exposed as Library.config.preprint_export). [#30]

  • Changed: Library.render (and the render CLI command) now renders a preprint-only entry’s preprint reference (arXiv:2205.15044, with the category tag from a stored primaryclass appended) in the journal position of the citation, linked to the DOI, the entry’s first URL, or the archive’s page for the identifier, without a separate trailing eprint link. For all other entries (e.g. a published article with a recorded preprint), the eprint segment still renders after the journal reference. [#30]

  • Changed: fields="minimal" exports of an article now include the eprint, archiveprefix, and primaryclass fields, so the bibliography of a published paper keeps its preprint link (rendered by eprint-aware styles like REVTeX, ignored by classic styles). [#30]

  • Added: exports emit the SPIRES-era archive BibTeX field – the link base that REVTeX’s apsrev4-x/aipnum4-x styles use for a rendered eprint, defaulting to arXiv’s https://arxiv.org/abs – whenever the structured eprint fields of a non-arXiv preprint are written (both for preprint-only entries and in full and minimal exports of published entries with e.g. a HAL or bioRxiv eprint), so the eprint hyperlink points at the right server. The base URL is derived from the archive’s URL template in preprint_archives when it has the form <base>/{id}; a stored archive field is always written as-is. [#30]

  • Fixed: rendering an entry whose archiveprefix is not arXiv (e.g. a HAL or bioRxiv eprint) no longer mislabels the eprint as arXiv:<identifier> with a broken arxiv.org link; the eprint segment now names the actual archive in its canonical spelling and links to that archive’s page (an unrecognized archiveprefix renders verbatim, without a link; a missing one still defaults to arXiv). [#30]

  • Changed: a search match applied by Entry.add_preprint/Library.add_preprint (and the add_preprint CLI command, including via add --add-preprint) now also stores the preprint’s arXiv primary category (e.g. quant-ph) in the primaryclass field, replacing any existing value (which, under overwrite/--overwrite, described the replaced identifier); the returned named tuple, the per-key report, and the --json output gain a primaryclass member. An explicitly given identifier (--eprint) still stores only eprint/archiveprefix, since without network access the category is unknown, and mark_empty/--mark-empty now clears a stale primaryclass alongside the emptied eprint and the stale archiveprefix. [#30]

  • Added: read-only files KEY and urls KEY CLI commands, listing an entry’s file attachments and linked URLs, one per line (corresponding to the Entry.files and Entry.urls properties). files prints each attachment as an absolute path by default; --relative prints the stored form, relative to the .bib file’s directory. [#28]

  • Added: a read-only path CLI command, printing the absolute path of the .bib file being operated on (the given BIBFILE, or the configured default_bib_file; corresponding to the Library.path property), and a read-only config_path CLI command, printing the absolute path of the discovered bibdeskparser.toml configuration file in effect for that .bib file (failing with an error when none is found). [#28]

  • Changed: Library.export and the export CLI command no longer take a format parameter. The formerly bundled aspects of the output are now controlled independently: unicode=True|False (--unicode/--no-unicode) selects Unicode or TeX-encoded field values, expand_strings=True|False (--expand-strings/--no-expand-strings) selects whether @string macro references are replaced by their values or kept bare with the needed @string definitions prepended, and fields – "full", "minimal", or a list of field names (--minimal, --field) – selects which fields are exported. To adapt: format="default" becomes no arguments (the defaults); format="raw" becomes unicode=False (which now consistently TeX-encodes the @string definitions as well, and re-encodes deterministically instead of exposing the in-memory stored form); format="minimal" becomes fields="minimal" (which now also prepends the needed @string definitions; add expand_strings=True for a standalone snippet without them, with the references replaced by their values instead of left dangling). [#27]

  • Changed: all exports now use a single layout, previously used only by format="minimal": 4-space indentation, capitalized field names (Author, Bdsk-File-1), a comma after every field, and the closing brace on its own line. Reproducing the byte-exact BibDesk file layout remains the job of Library.save. [#27]

  • Changed: Library.edit, the underlying editing functions, and the edit CLI command no longer take a format parameter; editing always uses the default export form. [#27]

  • Changed: the show and get_field CLI commands now render field values: @string macro references are replaced by the macro’s value (--no-expand-strings shows the bare macro name instead; in JSON output, every field value then uniformly becomes a {"macro": ..., "value": ...} object – macro is null for a literal value – so a macro reference remains distinguishable from a literal value while all fields share one shape), and --no-unicode shows TeX-encoded values. [#27]

  • Fixed: Library.export no longer turns a literal field value that happens to look like a macro name (a ValueString) into a bare @string macro reference; the stored literal-vs-macro distinction is now preserved on export. [#27]

v0.3.0 - 2026-07-18

  • Added: a create CLI command, creating a new, empty .bib file that contains only the standard BibDesk header comment (bibdeskparser create new.bib, or bibdeskparser create to bootstrap the configured default_bib_file). It is the one command whose BIBFILE must not already exist – an existing file is never overwritten; every other command still requires an existing file, and the “bibfile not found” error now suggests create. [#24]

  • Changed: the first save() of a from-scratch Library (constructed without a path) now always synthesizes the standard BibDesk header, even if the library was never modified; previously an unmodified from-scratch library saved as an empty file. It also now raises FileExistsError if the destination path already exists (a from-scratch library has no baseline timestamp for the StaleFileError check, so it would previously overwrite silently); to adapt, pass force=True to save() to deliberately overwrite an existing file with a from-scratch library. [#24]

  • Fixed: @string macro names are now case-insensitive throughout, matching BibDesk’s macro table. Names are normalized to their canonical lowercase form when a file is parsed (both @string definitions and bare macro-reference field values, e.g. from a hand-edited @string{JAN = ...} / month = JAN), lookups in Library.strings (reading, in, deleting) match case-insensitively, and Library.rename_string looks up the old name case-insensitively, normalizes the new name to lowercase instead of rejecting a name containing uppercase letters, and treats a rename onto the same macro (a new name differing at most in case) as a no-op (the rename_string CLI command behaves the same). A hand-edited mixed-case @string month definition now overrides the built-in month macro (and, like any month-macro override, can be deleted while still referenced), and a mixed-case bare reference resolves in Library.render, Library.search, and format specifiers. Note that saving a modified library rewrites hand-edited mixed-case macro names in lowercase (BibDesk preserves the case as written while matching case-insensitively). [#22, #23]

  • Added: the twelve standard BibTeX month macros (jan … dec) are now always defined, expanding to the full English month names (“January” … “December”), matching BibDesk’s built-in standard macros. A bare month = jan field resolves in Library.render, Library.search, and format specifiers, and no longer counts as an undefined macro when saving, editing, or importing. The built-in macros do not appear in Library.strings and are never written to the .bib file; a @string definition of the same name overrides them (and, unlike other in-use macros, can be deleted while still referenced, since references then fall back to the built-in month name). [#21]

  • Changed: Library.keys now returns a tuple of citation keys (instead of the generic set-like view inherited from collections.abc.MutableMapping, which did not even display the keys in the REPL) and accepts optional types, has, missing, and empty filter arguments, matching the --type/--has/--missing/--empty options of the keys CLI command (which now delegates to it). To adapt: code that relied on the set operations of the previous view should wrap the result in set(); code that relied on a live view reflecting later mutations should call keys() again after mutating. [#16]

  • Added: every boolean CLI option that toggles behavior now has an explicit negative form, so that a default can later change without breaking existing invocations: --fix-uppercase/--no-fix-uppercase (import, add), --keep-keys/--no-keep-keys (import), --skip-missing/--no-skip-missing (show), --remove/--no-remove (replace_file, unlink_file), --check-exists/--no-check-exists (add_file, replace_file), and --auto-file/--no-auto-file (add_file) (subsuming the previous --no-check-exists and --no-auto-file flags, which keep working; the new --auto-file positive form forces auto-filing even without file_automatically = true in the configuration). [#17]

  • Added: Entry.add_abstract, a Library.add_abstract that delegates to it after locating the entry’s first attached PDF (paths in Entry.files are library-relative, so the PDF source is only available through the Library method), and a corresponding add_abstract CLI command, fetching the abstract of an existing entry from the best available source – Crossref (via the entry’s doi), the text of the entry’s first attached PDF (extracted with poppler’s pdftotext, when installed), the arXiv API (via eprint), or Semantic Scholar as a last resort – cleaning it to plain-unicode prose (LaTeX/JATS math markup converted to unicode, ligature/hyphenation repair, publisher copyright trailers stripped), and validating it with heuristic garble checks. Each result carries its source and a confidence level (high/medium/low/none); only abstracts at or above min_confidence/--min-confidence (default high) are stored, and the CLI reports unstored candidates in full for manual review. Entries that already have a non-empty abstract are skipped unless overwrite/--overwrite is given; mark_empty/--mark-empty stores an empty abstract field as an “audited, nothing found” marker. New dependency: pylatexenc.

  • Added: an add_abstract table in bibdeskparser.toml, configuring the defaults for the min_confidence and mark_empty keyword arguments of Library.add_abstract (and the like-named options of the add_abstract CLI command); exposed as Library.config.add_abstract. An explicit argument or command-line flag always overrides the configured default. [#18]

  • Added: Entry.add_preprint, a purely delegating Library.add_preprint (like Library.add_url), and a corresponding add_preprint CLI command, recording the arXiv preprint of an existing entry in its eprint field (with archiveprefix = arXiv) – either an explicitly given identifier (--eprint, no network access), or one found by searching the arXiv API by title and first author. A search result is stored only on a confident match (the entry’s DOI, a near-exact title, or a good title corroborated by the first author), and a title-based match postdating the entry’s publication year is rejected unless its journal reference corroborates the year. The eprint field encodes the audit state: absent = unknown (keys --missing eprint), empty = searched with no preprint found (keys --empty eprint, written by mark_empty/--mark-empty), non-empty = known (mark_empty also clears a stale archiveprefix = arXiv alongside the emptied eprint). Entries with a non-empty eprint are skipped unless overwrite/--overwrite is given; a failed search never modifies the entry. arXiv is the only supported preprint server for now. [#19]

  • Added: an add_preprint table in bibdeskparser.toml, configuring the default for the mark_empty keyword argument of Library.add_preprint (and the like-named option of the add_preprint CLI command); exposed as Library.config.add_preprint. An explicit argument or command-line flag always overrides the configured default. [#19]

  • Added: an add_abstract flag on Library.add (--add-abstract/--no-add-abstract on the add CLI command; default off) that stores the abstract returned alongside the fetched metadata (the publisher’s Crossref deposit, or the arXiv summary) in the new entry’s abstract field, cleaned and validated the same way. [#20]

  • Added: an add_preprint flag on Library.add (--add-preprint/--no-add-preprint on the add CLI command; default off) that searches arXiv for a preprint matching the newly added entry, exactly as Library.add_preprint does (skipped when the entry already has an eprint). [#20]

  • Added: an add table in bibdeskparser.toml, configuring the defaults for the fix_uppercase, add_abstract, and add_preprint keyword arguments of Library.add (and the like-named options of the add CLI command); exposed as Library.config.add. An explicit argument or command-line flag always overrides the configured default. [#20]

v0.2.1 - 2026-07-14

  • Added: Library.import_bibtex and a corresponding import CLI command (reading from a file, --stdin, or --url), importing the entries of a BibTeX snippet – anything from a single publisher-provided entry to a complete .bib file – after thorough sanitization: journals are stored as @string macro references (resolved against the library’s macros and the new journal_macros configuration table, or newly created from the journal’s lowercased initials, honoring the initials.journal exceptions; literal arXiv:... pseudo-journals stay literal and derive eprint/archiveprefix), proper nouns in sentence-case titles and all configured protected_words are brace-protected, DOIs are normalized to their bare lowercase form, article page ranges collapse to the first page, non-essential article fields (month, publisher, numpages, issn, a url shadowed by the DOI, …) are dropped, and citation keys are regenerated (keep_keys/--keep-keys to opt out) from the configured auto_key format or built-in defaults (e.g. GoerzPRA2014, or Goerz2205.15044 for arXiv preprints). An entry whose DOI or eprint is already in the library is rejected; any validation problem rejects the whole import (reporting all problems at once) and leaves the library untouched. [#14]

  • Added: Library.add and a corresponding add CLI command, fetching bibliographic data by DOI, arXiv identifier, URL, or free-text search – from Crossref (with DOI content negotiation as the fallback for work types with no BibTeX equivalent) or the arXiv API (respecting its rate limits) – and adding it as a new entry via the same sanitization as import_bibtex. The CLI command accepts --dry-run (print the entry instead of saving) and --fix-uppercase (repair all-uppercase publisher metadata, also available on import). This subsumes the functionality of the getbibtex script. New dependencies: habanero, arxiv, and httpx. [#14]

  • Added: a journal_macros table in bibdeskparser.toml, mapping @string macro names to the journal name(s) they stand for (a list value declares publisher-spelling aliases of the same journal), and a protected_words list of words that import always brace-protects inside titles; exposed as Library.config.journal_macros and Library.config.protected_words. [#14]

  • Added: CLI commands for working with the fields of a single entry: fields KEY lists the defined field names, get_field KEY FIELDNAME prints one field value, set_field KEY FIELDNAME VALUE sets a field (with --literal/--macro to force the value to be stored as literal text or as a bare @string macro reference), and delete_field KEY FIELDNAME removes a field. These correspond to the dict interface of Entry (iteration, indexing, assignment, and del). [#13]

  • Added: an author KEY and an editor KEY CLI command, printing an entry’s authors/editors as structured names (last-name-first; with --json, as objects with first/von/last/jr name parts), corresponding to the Entry.author and Entry.editor properties. [#13]

  • Added: a set_type KEY TYPE CLI command, changing an entry’s type (corresponding to assigning Entry.entry_type). [#13]

  • Added: the groups and keywords CLI commands accept an optional entry KEY, listing the groups/keywords of that single entry (corresponding to the Entry.groups and Entry.keywords properties) instead of the library-wide mapping. [#13]

  • Added: filter options on the keys CLI command: --type TYPE keeps only entries of the given type(s), and --has FIELD/--missing FIELD/--empty FIELD keep only entries where FIELD is defined with a non-empty value, not defined at all, or defined but empty, respectively (all repeatable). [#13]

  • Fixed: assigning a MacroString (e.g. via set_field --macro) now normalizes the macro name to BibDesk’s canonical lowercase form, matching how @string definitions are stored; previously a non-canonical name such as PRA was stored verbatim and then read back as a literal ValueString instead of a MacroString. [#13]

  • Fixed: the CLI’s top-level --help now shows a complete, un-truncated one-line summary for every subcommand (previously long summaries were cut off with ...). [#11]

  • Fixed: Library.render (and the render CLI command) now expands @string macros in the rendered citation, so a field like journal = pra shows its defined value (e.g. Phys. Rev. A) rather than the bare macro name. [#12]

  • Changed: bump the PyPI Development Status classifier from 2 - Pre-Alpha to 3 - Alpha.

v0.2.0 - 2026-07-13

  • Added: automatic filing of file attachments, mirroring BibDesk’s AutoFile feature. Library.rename_file without a new_filename moves an attachment into the configured auto-file location and renames it according to a file-name format in BibDesk’s format-specifier language (the recommended format is %f{Cite Key}%u0%e, naming each file after its entry’s citation key while keeping the real extension); Library.add_file files newly attached files the same way when auto-filing is in effect (file_automatically = true in the configuration, or explicit format_spec/auto_file_location arguments, with auto_file_location="" forcing a plain attach). A file whose name already matches the format is left in place (re-filing is idempotent), and the format’s required %u/%U/%n specifier disambiguates against existing files on disk. The rename_file and add_file CLI commands gain the corresponding options. [#10]

  • Added: an auto_file table in bibdeskparser.toml, with format_spec (a single format or a per-type table), location (the directory files are moved into, relative to the .bib file or absolute), lowercase, clean, and file_automatically keys, exposed as Library.config.auto_file. [#10]

  • Added: the file-name context of the format-specifier language: the %l/%L/%e/%E original-file-name specifiers, / as a directory separator, and file-name oriented sanitization (only : is invalid; spaces and non-ASCII text survive). [#10]

  • Changed: Library.eval_format_spec also evaluates file-name formats, via a new filename keyword argument that selects the file-name dialect (any non-None value, including "", does so) and supplies the original-name specifiers %l/%L/%e/%E. The filename need not exist or be one of the entry’s attachments; the format is evaluated purely, without touching the filesystem. If filename is an attachment’s current path that already matches the format, it evaluates to itself. The eval_format_spec CLI command gains a matching --filename option. [#10]

  • Changed: Library.add_file and Library.rename_file now return the stored library-relative path of the attachment (previously None), rename_file’s new_filename argument is optional (omitting it triggers auto-filing), missing parent directories of a rename target are now created automatically, and a rename may move a file across filesystems. To adapt: existing code needs no changes unless it relied on rename_file failing for a target in a nonexistent directory; the return values can be ignored. [#10]

  • Added: automatic citation-key generation. Calling Library.rekey without a new_key generates the key from an auto-key format in BibDesk’s format-specifier language (e.g. %a1%c{journal}0%Y%u0), taken from the new auto_key table of bibdeskparser.toml or from the new format_spec keyword argument. A key that already matches the format is kept unchanged, and a %u/%U/%n specifier in the format disambiguates collisions, like in BibDesk. The rekey CLI command correspondingly makes NEW_KEY optional, adds a --format-spec PATTERN option, and prints the generated key. [#9]

  • Added: Library.eval_format_spec and a matching read-only eval_format_spec CLI command, evaluating an auto-key format for an entry and returning the key it yields, without renaming anything. A key that already matches the format evaluates to itself, so this identifies the entries whose citation key does not follow a given format. [#9]

  • Added: per-type auto-key formats. The auto_key table’s format_spec may be a table mapping each entry type to its own format (with "" as the fallback for unlisted types), so a mixed-type library can name journal for articles, booktitle for conference papers, and so on. The auto_key settings are also available and settable as Library.config.auto_key (with format_spec, lowercase, and clean attributes). [#9]

  • Added: an initials table in bibdeskparser.toml, defining per-field exceptions (e.g. journal or conference-proceedings initials) to the acronym that the %c format specifier builds from a field value. [#9]

  • Added: a “Format Specifiers” reference page documenting the format-specifier language. [#9]

  • Changed: Library.rekey now returns the resulting citation key (previously None). [#9]

  • Added: a “How to give an AI coding agent access to your library” how-to guide, describing the lightweight path for letting a shell-capable agent (such as Claude Code) drive the bibdeskparser CLI, and expanded the top-level --help text to orient such callers (read-only vs. mutating commands, --json, exit codes). [#8]

  • Added: Library.edit and Library.edit_strings accept a function for editor, as an alternative to an editor command string. The function receives the path of the temporary file and must edit it in place; validation problems then raise a ValueError instead of prompting interactively. [#8]

  • Added: a --stdin option on the edit and edit_strings CLI commands, reading the full edited text from standard input instead of opening an editor, and a --bib option on strings, printing the @string definitions as re-parseable @string{name = {value}} lines. Together with export, these allow non-interactive (e.g., scripted or AI-agent) editing via pipes: export KEY... | ... | edit KEY... --stdin and strings --bib | ... | edit_strings --stdin. [#8]

  • Changed: the edit and edit_strings CLI commands now fail immediately with a usage error when invoked without a terminal on standard input and without --stdin or an explicit --editor, instead of blocking on $EDITOR. Non-interactive callers must pass --stdin (piping in the edited text) or --editor with a command that needs no terminal. [#8]

  • Fixed: newly added entries, new @string macros, and a newly synthesized static-groups @comment block were appended at the very end of the .bib file, after BibDesk’s group @comment blocks. They are now written at their canonical position: macros in the alphabetically sorted @string run before the first entry, entries before the group @comment blocks, and the static-groups block before the smart-groups block, matching the layout BibDesk itself writes.

  • Added: Library.search and a corresponding search CLI subcommand: find entries matching a query, ranked best match first. Matching runs against the stored field values (bare @string macro names intact), the decoded Unicode values, and macro expansions, with accent-insensitive, word-overlap, fuzzy, and regex match levels, optionally limited to specific fields. [#5]

  • Added: a bibdeskparser command-line tool that exposes the public Library API as subcommands (keys, show, render, export, add_to_group, set_string, edit, …). Data-output commands accept --json; the .bib file argument may be omitted when default_bib_file is configured. [#4]

  • Added: a default_bib_file option in bibdeskparser.toml, naming the .bib file the command-line tool operates on when none is given. [#4]

  • Added: the BIBDESKPARSER_CONFIG environment variable, naming the user-level bibdeskparser.toml in place of the XDG location. Setting it to an empty value disables the user-level configuration entirely. [#7]

  • Added: MacroString, mirroring ValueString, to force a field value to be stored as a bare @string macro reference. Both are subtypes of str.

  • Added: Entry.add_url, Entry.replace_url, Entry.remove_url and the corresponding Library.add_url, Library.replace_url, Library.remove_url methods for managing an entry’s linked URLs.

  • Added: validation and normalization of Entry types: constructing or assigning an entry type now lowercases it and rejects unrecognized types with a ValueError. Loading a .bib file still never validates.

  • Added: validation of the author/editor fields: assigning an unparseable value raises ValueError.

  • Added: a UserWarning when assigning a field that is not appropriate for the entry type.

  • Added: a “Bib Entry Types” reference page documenting the supported entry types and fields.

  • Added: support for a bibdeskparser.toml configuration file (searched for next to the .bib file and in the XDG config location), with verify_types and verify_fields flags to disable entry-type validation and field-appropriateness warnings, and types/fields tables to define custom entry types and fields or extend the built-in ones. The active configuration is exposed as the Library.config class attribute (equally readable from a library instance as bib.config), whose attributes – verify_types, verify_fields, config_file, auto_key, and others – can be assigned for in-process overrides that never write back to the configuration file.

  • Added: a “Configuration” reference page documenting the bibdeskparser.toml file.

  • Changed: Value has been renamed to ValueString (breaking: rename Value to ValueString in your code). Values returned by the Entry dict interface are now ValueString (for literal/braced values) or MacroString (for bare @string macro references) instances; both are str subclasses and compare as plain strings.

  • Changed: Entry.urls is now a read-only tuple (breaking: replace assignment to entry.urls with the new add_url/replace_url/remove_url methods).

  • Changed: the keywords field is now readable through the Entry dict interface (indexing an entry by keywords returns the comma-joined string). It is still not writable that way, and the Entry.keywords property remains read-only; keywords are edited only through the owning Library.

  • Removed: the public Entry.dirty property (breaking: there is no public replacement; it was an internal detail).

v0.1.0 - 2026-07-07

Initial release.