Changelog
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
v0.8.1 - 2026-08-21
Fixed: an
exportwith 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 (aurl, adoi):book,inbook, andproceedingskeeppublisher,techreportkeepsinstitution,bookletkeepshowpublished,manualkeepsorganization,unpublishedkeepsnoteandurl,webpagekeepsurl,periodicalkeepsjournal,jurthesiskeepsschool,glossdefkeepsword/definition, andconferenceis exported likeinproceedings. Previously onlyarticle,inproceedings,incollection,mastersthesis, andphdthesishad a whitelist and every other type fell back toauthor,title,year, so that a minimally exported@bookcarried no publisher and a@techreportno institution. A storeddoiis 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@onlineand types added by abibdeskparser.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/--fieldon the command line – only adds fields on top of that. Refreshing a file exported with--fulltherefore no longer strips it down to the minimal fields, and hand-added fields such as anannotesurvive 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
@stringmacro reference. An ordinary URL is a valid BibDesk macro name, soset_field KEY url ...(and the equivalent assignment in Python) stored an unbraced value, which made the nextsavefail withundefined macro(s) referenced by one or more entriesand madeexportwrite invalid BibTeX. A plainstris now always stored as literal text when it carries a URL scheme (://), and in any field whose name containsurl;MacroStringstill forces a macro reference. [#78, #80]Added:
export(includingexport --update) now warns about every bare@stringmacro reference it keeps for a macro that nothing defines – neither the library (nor thestringsargument ofexport_entries) nor the standard month macros – since such output is not valid BibTeX. Only theexpand_strings=Truepath warned before. [#78, #80]
v0.8.0 - 2026-08-03
Added:
Library.info, a read-writedict-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_infoblock of the.bibfile. 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 aFormatConversionWarning; 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), andset_info KEY VALUE/delete_info KEYmodify it. The%i{Key}format specifier (case-insensitive lookup, empty for a missing key,%i{Key}Ntruncating to N characters) is now implemented on top of this data instead of raisingNotImplementedError. [#69, #70]Fixed: a
@bibdesk_infoblock 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-keydocument_info(inkeys,show,search, thecheckaudits, …), 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 plantdate-added/date-modifiedbookkeeping 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
assetsconfiguration 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.bibfile’s directory, andLibrary.assets(*keys)reports which asset files exist on disk; on the command line, theassetandassetscommands (both taking optional citation keys and--json,assetalso--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 (assetswithout keys reports the library-level classes, and the coverage table over all entries is the explicitbib.assets(*bib)/assets $(bibdeskparser keys)).assetverifies by default that something is on disk at the resolved path – a directory for a directory-valued class, a file otherwise – and raisesFileNotFoundErrorif 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
checkaudits over the asset files.asset_orphans(on by default,--no-orphansto skip) inverts each per-entryassetspattern 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--jsonoutput. [#71, #72]Changed:
Library.rekey(and therekeycommand) 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 eachassetspattern 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 viarename_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 argumentsrename_assets/rename_attachments(CLI:--rename-assets/--no-rename-assets,--rename-attachments/--no-rename-attachments) default to the newrekeyconfiguration table, bothtrue. To keep the previous behavior (rename the key only), setrename_assets = falseandrename_attachments = falsein therekeytable, or pass the--no-*options. [#71, #72]Added:
Library.delete(key, remove_assets=..., remove_attachments=...), backing entry deletion (del) and thedeletecommand (CLI:--remove-assets/--no-remove-assets,--remove-attachments/--no-remove-attachments, defaulting to the newdeleteconfiguration table, bothfalse). 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
exportwithfull=True(--fullon 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 everyLibraryload and every CLI command on such a file previously crashed with a bareError: Invalid filewhen decoding that value. Abdsk-file-Nvalue that is not a base64 binary plist is now read as a path-only attachment: it appears inEntry.fileslike 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 theLibrarymethods. [#65, #68]Changed:
Entry.add_url/Entry.replace_url(and thusLibrary.add_url/Library.replace_urland theadd_url/replace_urlCLI 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 abdsk-url-Nvalue is ASCII by construction (matching BibDesk’s ownabsoluteStringserialization), 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 storeshttps://example.com/m%C3%BCnchen;replace_urlencodes only itsnew_url, matchingold_urlliterally against the stored URLs.importapplies the same normalization to incomingbdsk-url-Nfields. [#64, #67]Added: a new standard
checkaudit,url_encoding, reporting every URL-type value that holds raw non-ASCII characters: any field whose name containsurl(the class exempt from TeX encoding, e.g. theurlfield) plus thebdsk-url-Nlinks. Such characters break a LaTeX export that typesets the value inside\url{...}(whose verbatim catcodes bypassinputenc, printing garbled glyphs) and are what BibDesk drops when reloading abdsk-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')). Storedurlfield 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 nameurl_encodingalso appears in the--jsonoutput. [#64, #67]Added: a
.bibfile that is not a BibDesk database – no BibDesk header, no group@commentblocks, nobdsk-*fields; e.g., a file written byexport– is now recognized as plain BibTeX on load, andsavepreserves that format: no header is synthesized, no date fields are created, comments,@preambleblocks, and@stringdefinitions are kept in place, and every stored field of every entry is written – unmodified entries in their stored field order, so a file created byexportround-trips byte-identically, and modified entries in BibDesk’s field order, in the export layout. All commands that modify the.bibfile (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),@stringexpansion, 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.exportgained amarkerparameter (CLI:--marker/--no-marker, default on) controlling the marker line, which is never written when the output includesbdsk-*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 newFormatConversionWarning(re-exported from the top-level package) naming the trigger. [#62, #63]Added:
export --update FILE KEY...(with the keys optional), via a newupdate=parameter ofLibrary.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@stringblock 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,@preambleblocks, unused definitions – is kept; nothing is ever removed, and entries written by an update never includebdsk-*fields. The--unicode/--expand-strings/--preprintoptions 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--outfileto create a new export) and be plain BibTeX; a BibDesk database is refused. [#62, #63]Changed:
exportnow defaults to the minimal field selection – thefieldsparameter ofLibrary.exportdefaults to"minimal"instead of"full"– and the CLI flag pair--minimal/--no-minimalis renamed to--minimal/--full. To adapt, pass--full(Python:fields="full") for the previous behavior. In particular,export KEY | import other.bib --stdinas a library-to-library transfer now moves a minimal skeleton unless--fullis passed, and theexport-to-edit --stdinround-trip must be spelledexport KEY --full --preprint stored | edit KEY --stdin, since a field missing from the text piped intoedit --stdinis 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 cleanWarning: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
Entryno longer carriesdate-added/date-modified; the fields are stamped when the entry is added to aLibraryin the BibDesk database format, so that adding entries to a plain BibTeX file (directly, or viaimport/add) never plants date bookkeeping there. Adate-addedalready stored on an entry (e.g. preserved byimport) is kept as before, andEntry.date_added/Entry.date_modifiedareNonefor a detached entry. [#62, #63]
v0.6.0 - 2026-07-27
Fixed: the
checkcommand now audits every bare (unbraced) field value for a reference to an undefined@stringmacro, not justjournal. Such a reference renders as the macro name itself (month = septbecomes literalsept,publisher = elsevirbecomeselsevir) and is refused byLibrary.save, so a file could previously passcheckand then be impossible to write (a subsequentbibdeskparser set_field ...failing with anundefined macro(s) referenced by one or more entrieserror naming those bare values); a passingchecknow implies a writable file. Only a value that is a valid macro name is considered, so a bare non-macro value likevolume = 90is not flagged, andkeywords(always literal text) is exempt, matching whatsavescans. The audit name in the--jsonoutput isundefined_macro; an undefined macro that is also not a month (month = sept) is reported by both themonthand theundefined_macroaudit. [#56, #61]Added: two new
checkaudits over the date fields, reporting values that the citation-key specifiers%Yand%mmis-read in silence.yearreports an entry whoseyeardoes 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%Yreduces(about 1984),in press, andn.d.to as well. A value BibDesk reads correctly without being a bare four-digit string still passes:08maps into 1950–2049, and trailing text after the digits (2008a,2001--) is ignored.monthreports an entry whosemonthis anything but a bare reference to one of the twelve standard month macrosjan…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 literal06orJuneis reported although it renders correctly, for the same reason a literaljournalvalue is: a.bststyle typesetsjunasJune,Jun.,Juni, or6, and writing the month out freezes one of those choices into the database. Unlike theyearaudit, themonthaudit inspects the stored value rather than what%mrenders, since%manswers01for 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 therequired_fieldsandempty_fieldsaudits instead. The audit namesyearandmonthalso appear in the--jsonoutput. [#55, #60]Changed:
Library.eval_format_spec(and theeval_format_specCLI command) now always returns the evaluated citation key, even one that equals the entry’s owncrossrefvalue, instead of raisingValueError: evaluating a format is a preview and applies nothing.Library.rekey(and therekeycommand) still refuses to apply such a key. Thecheck --key-formataudit reports such an entry as an ordinary key deviation, naming the generated key, rather than as unevaluable. [#60]Added: two new
checkaudits over every entry.entry_typereports an entry whose type is not one of the recognized entry types (Bogus2026: unrecognized entry type 'bogustype'), andrequired_fieldsreports 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.bibfile deliberately never validates, so nothing had ever looked at either: an@articleholding only adoi, and an entry of a type that does not exist, both passed the gate. A defined-but-empty field counts as missing, soyear = {}is reported by bothrequired_fieldsandempty_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 asdataset) has no required fields on record and is skipped; atypes.NAMEtable inbibdeskparser.tomldeclares either, making the type recognized and supplying therequiredlist to audit against. Neither audit is gated byverify_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@articlewith noyearis not an article, and the fix for one is@misc, which requires nothing. The audit namesentry_typeandrequired_fieldsalso appear in the--jsonoutput. [#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 ajournalkeys asSmith2020, one without ayearasSmithPRA. An entry that renders every specifier of a file-name format empty is filed asa.pdfrather 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@misctalks by theirhowpublishedvenue 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@miscentry, since the entries lacking it could not be keyed at all. This affectsLibrary.rekey,Library.eval_format_spec,Library.import_bibtex,Library.add_file, andLibrary.rename_file, along with therekey,eval_format_spec,import,add,add_file, andrename_fileCLI commands. Thecheck --key-formataudit 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, soKılıçkeyed asKlc,MasłowskiasMasowski, andƏliyevasliyev. 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-argumentrekey(and thecheck --key-formataudit) 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 thesearchCLI command) now finds such a letter by its ASCII spelling at thefoldedmatch level. Its fold handled only decomposable accents plusß, soMølmerwas reachable byMolmeronly at thefuzzylevel, below the default, andbibdeskparser search Molmeron a library full of Mølmer entries returned nothing;Kılıçwas unreachable byKilicat 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_preprintandLibrary.add_doinow 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 (əliyevagainstliyev), which silently disabled thetitle+authoracceptance 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
importcommand now reads its source file from a--file FILEoption instead of a positionalFILEargument, so a positional argument ending in.bibalways names the library, like every other command. This removes the collision where, with adefault_bib_fileconfigured,bibdeskparser import from_paper.bibsilently claimed the snippet as the library and then failed for lack of a source. Migration:bibdeskparser import library.bib entries.bibbecomesbibdeskparser import library.bib --file entries.bib, andbibdeskparser import from_paper.bib(into the default library) becomesbibdeskparser import --file from_paper.bib.Library.import_bibtex, which takes the BibTeX text directly, is unaffected. [#46, #51]Added: a read-only
configCLI command that dumps the resolved configuration – the built-in defaults merged with whatever a discoveredbibdeskparser.tomlsets. Unlikeconfig_path(which reports only the file in effect, and fails when none is found),configshows the effective value of every setting, including the ones a file omits (auto_key.clean,preprint_export, the built-inpreprint_archives, …) and the built-in defaults in full when no file exists. The default text output is TOML-shaped, mirroring what abibdeskparser.tomlwould contain to reproduce the resolved tunable state (an unset value, or anauto_key/auto_filetable without aformat_spec, is omitted);--no-typesrestricts 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--jsonprints the complete state as an object with unset values asnull. TheBIBFILEargument is optional – it only fixes the config-discovery directory, defaulting to the current directory – so the command needs no.bibfile and never fails for a missing configuration file. [#45, #50]Added: a
--key-formatoption on thecheckCLI command, an opt-in audit (off by default) that reports every citation key not matching its expected auto-key format, i.e. every key thateval_format_spec(or single-argumentrekey) 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 PATTERNoption audits against that pattern instead and implies--key-format(combining it with--no-key-formatis an error). A key already matching the format evaluates to itself, so disambiguated sibling keys such asSmithPRA2015andSmithPRA2015aboth 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-specand nothing configured), a single message is reported instead of one failure per entry. The audit namekey_formatalso appears in the--jsonoutput. [#44, #49]Added: a
--filesoption on thecheckCLI command, an opt-in audit (off by default) that reports every linked attachment (bdsk-filepath) that does not resolve to a real path on disk relative to the.bibdirectory. 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, socheck --filescan 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 namefilesalso appears in the--jsonoutput; 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
--usageoption on thebibdeskparsercommand-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--helpoutput. [#47]Changed: running
bibdeskparserwith no command now prints the short usage summary (see--usage) on stderr and exits 2, instead of dumping the entire--helpoutput. [#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 (behindaddandimport --url) needssocksio, and requests (behindadd_preprint,add_doi, andadd_abstract, via thearxivandhabaneropackages) needspysocks. Both helpers are now regular dependencies.Fixed: with
--dry-run, the per-key reports of theadd_abstract,add_preprint, andadd_doiCLI commands (and the preprint report ofadd --add-preprint) now saywould store/would mark known missinginstead of the past-tensestored/marked known missing, which misread as the file having been modified. The JSON reports are unchanged:appliedmarks 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 meaninglesscli.pysource 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.bibfile separated from its attachment tree (e.g. a copy in another directory) flooded stderr with one location-prefixedUserWarningper linked file, on every mutating command.Fixed: importing an entry whose
journalspells 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 lowercasedPhysical review letters, where the library definesprl = "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, soPhys. Rev. Adoes not capturePhysical Review Applied– and on a full match the existing macro is reused, with a warning showing thejournal_macrosalias line that makes the mapping explicit. When the match fails (the colliding macro holds a different journal), the error now prints the exactjournal_macrosconfiguration line for each possible intent – appending the incoming spelling to the alias list (canonical value first), or an entry under a fresh macro name / aninitials.journalexception – as does the error for ajournal_macrosentry that conflicts with an existing@stringdefinition. [#40, #42]Added: a
with_filesargument ofLibrary.keysand a paired--with-files/--without-filesoption on thekeysCLI command, a tri-state filter on file attachments (thebdsk-file-Nfields): the default (None, or neither flag) does not filter,True/--with-fileskeeps only entries with at least one attachment, andFalse/--without-filesonly those with none. This composes with the existingkeysfilters, sobibdeskparser keys --type article --without-fileslists the articles still needing a PDF. [#39, #41]Changed: the
files,urls,groups, andkeywordsCLI 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-flatoption (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 --flatis thus every file the library references, the reverse index for reconciling against a folder of PDFs.fileskeeps its--absolute/--relativeoption;groupsandkeywordsgain a paired--index/--no-indexoption that prints the inverse map instead, from each static group or keyword to the citation keys it contains (--indextakes no keys and does not combine with--flat). Migration: a single-keyfiles KEY/urls KEY/groups KEY/keywords KEYthat previously printed a bare list now prints a one-entryKEY: valuesmap; pass--flatfor the old bare-list output (single-keyfiles --flatkeepsbdsk-file-Nnumeric order). A baregroups/keywords(no key) previously printed the group/keyword catalog and now prints the per-entry map; pass--indexfor the catalog. [#39, #41]Fixed:
Library.add_preprint(and theadd_preprintCLI command) no longer reports a falseno-resultsfor 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ørensenbecames/rensen), poisoning every arXiv query and silently disabling thetitle+authoracceptance 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 foreprint, the false negative had additionally marked the entry as verified-absent, excluding it from future searches. [#36, #38]Changed: the
namesaudit of thecheckcommand now also flags anauthor/editorwhose 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 -DorMeyer, H- D, both of which should renderH.-D.). Such values split cleanly into names, so they passed the old audit, yet they makerenderemit 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_doiand a correspondingadd_doiCLI command, recording the DOI of an existing entry in itsdoifield – 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’seprint(which names the published version of exactly this paper; an arXiv-issued10.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’syearby more than one is rejected as a likely title collision (reported asyear-mismatchfor 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 fordoiin theknown_missingtable ofbibdeskparser.toml(e.g.doi = "No DOI"), the group is maintained exactly asadd_abstract/add_preprintmaintain theirs: members are skipped, a clean no-match marks, storing a DOI unmarks, andoverwrite/--overwritere-audits; entries verified to have no DOI thus also pass the missing-doi audit ofcheckautomatically. [#35]Added:
--group NAMEand--not-group NAMEfilter options on thekeysCLI command, and correspondinggroup/not_grouparguments ofLibrary.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 withbibdeskparser add_preprint --overwrite $(bibdeskparser keys --group "No Eprint"). [#34]Added: a
known_missingtable inbibdeskparser.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 asLibrary.config.known_missing). This replaces the previous empty-field markers, which the BibDesk app silently deletes whenever it saves the.bibfile; 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 asknown-missing;overwrite/--overwritere-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 fordoimakes thecheckcommand accept anarticlewithout adoi. [#34]Added: two new
checkaudits:empty_fieldsflags 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), andknown_missingflags 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 --emptyand theemptyargument ofLibrary.keysare removed (--missing/missingnow also match a defined-but-empty field); themark_emptyarguments and--mark-emptyoptions ofadd_abstract/add_preprint, themark_emptykey of theadd_abstracttable, and theadd_preprinttable (whose only key wasmark_empty) are removed; a defined-but-emptydoino longer suppresses the missing-doicheckproblem (membership in the known-missing group configured fordoidoes instead);set_field KEY FIELD ""is now an error (usedelete_field, or record a verified absence via a known-missing group); andappliedin theadd_abstract/add_preprintresults now means the library was modified (the field, or the known-missing group membership). To migrate a library that used the old markers, runbibdeskparser check: every leftover empty field is reported by the newempty_fieldsaudit. For each reported entry, record the verified absence in a group (bibdeskparser set_group "No Eprint"once to create the group, thenbibdeskparser add_to_group "No Eprint" KEY...), add the matchingknown_missingentry tobibdeskparser.toml, and remove the empty field (bibdeskparser delete_field KEY eprint; likewise forabstractanddoi). Also delete anymark_emptykey andadd_preprinttable frombibdeskparser.toml. [#34]Removed:
Entry.add_abstractandEntry.add_preprint. UseLibrary.add_abstract(key, ...)andLibrary.add_preprint(key, ...)instead: the known-missing group bookkeeping and the PDF-attachment lookup both need the library, so theLibrarymethods are the only public fetching entry points.Entry.add_abstract’spdf_pathargument is gone with it (theLibrarymethod 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_abstractnow returnssource="error"instead of"none", mirroringadd_preprint’smatch="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 therenderCLI command) no longer drops the editors of an entry that has aneditorbut noauthor(e.g. an edited volume; aproceedingsentry 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.); theedited by ...piece of the published-in segment (forinproceedingsand book-family entries) is correspondingly only rendered when the entry also has authors. [#25, #32]Fixed:
Library.render(and therenderCLI command) no longer drops most fields of book-family entries: aninbookentry previously rendered as author/title/year only (droppingpublisher,series,volume,chapter,pages, andbooktitle), and anincollectionentry dropped itseditor,pages,series, andvolume. The book family (book,inbook,incollection, and the previously unhandledproceedings) now renders uniformly asseries Vol. N, publisher, address (year), Chapter N, pp. N1–N2(each piece only if present), prefixed forinbook/incollectionbyIn: *booktitle*and, when there are editors, byedited 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 ofLibrary.render(and therenderCLI 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
checkCLI command, running the standing audits over the library (or, withKEY...arguments, over just the given entries) and reporting every problem found, one per line, followed by aPASS/FAILsummary 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; everyarticlethat is not a preprint has adoi(a defined-but-emptydoimarks an entry verified to have none, and passes); everyjournalfield references a defined@stringmacro (a literal journal value is a problem, unless it is a recognized preprint pseudo-journal likearXiv:2205.15044); everyauthorandeditorfield parses as names; and every@stringmacro 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 withpassed,entries_checked, andproblemsmembers, each problem carrying the audit name, the citation key (nullfor a problem not tied to an entry), and a message. [#31]Fixed: reading
Entry.author/Entry.editorfor an unparseable name field (e.g. a name with too many commas) now raisesbibtexparser’s descriptiveInvalidNameError(aValueErrorsubclass, as documented), instead of anIndexErrorwith 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
journalis 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 amiscorunpublishedentry with aneprintfrom a recognized archive (e.g. arXiv’s own BibTeX export). The built-in archives are arXiv, bioRxiv, medRxiv, ChemRxiv, HAL, and SSRN; a newpreprint_archivestable inbibdeskparser.toml(exposed asLibrary.config.preprint_archives) adds further archives, mapping the archive’s canonical spelling to a URL template for its preprint pages.Library.import_bibtex(andLibrary.add) normalize a preprint-only entry to its canonical stored form: an@unpublishedentry with the pseudo-journal in the archive’s canonical spelling (hal:→HAL:; synthesized from theeprintif absent), theeprint(version suffix stripped) andarchiveprefixfields derived from the pseudo-journal if missing, and thedoiextracted from ahttps://doi.org/...value in theurlfield or (for arXiv) derived as10.48550/arXiv.<identifier>; aurlthat merely restates the archive’s page for the identifier is dropped when the entry carries adoi; the preprint citation-key format (Goerz2205.15044) applies to all archives. Anarchivefield holding the link base derivable from theeprint/archiveprefixis dropped on import (exports regenerate it), for preprint-only and published entries alike. The publication-statusnoterecommended 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@stringmacro (this also catches URLs pasted into thejournalfield).Library.addfor an arXiv query now also records theprimaryclass(the arXiv category). [#30]Added: a
keep_journalsargument toLibrary.import_bibtex(--keep-journals/--no-keep-journalson theimportCLI command; default off), preserving every incomingjournalfield as-is instead of converting it to an@stringmacro reference, and keeping the incoming entry type (preprint-only entries are still recognized for theeprint/archiveprefixderivation and the citation key, and unrecognized archive prefixes are then no error). [#30]Added: a
preprintargument toLibrary.export(--preprinton theexportCLI command), selecting the form a preprint-only entry is exported as, independent of its stored form:"unpublished"or"misc"– the structuredeprint-field forms, for BibTeX styles that render theeprintfield (REVTeX,elsarticle, biblatex);"unpublished"guarantees the entry type’s requirednotefield in minimal exports, writing the storednoteor 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 inurl, for classic styles (plain,unsrt,IEEEtran, …) that would silently drop aneprint– or"stored"for no transformation. Minimal exports reduce to the essential fields of the chosen form, always includingeprint/archiveprefixfor the structured forms and keeping a storednote; an explicit--fieldlist always exports the stored fields. The default is the newpreprint_exportsetting inbibdeskparser.toml("unpublished"unless configured; exposed asLibrary.config.preprint_export). [#30]Changed:
Library.render(and therenderCLI command) now renders a preprint-only entry’s preprint reference (arXiv:2205.15044, with the category tag from a storedprimaryclassappended) 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 anarticlenow include theeprint,archiveprefix, andprimaryclassfields, 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
archiveBibTeX field – the link base that REVTeX’sapsrev4-x/aipnum4-xstyles use for a rendered eprint, defaulting to arXiv’shttps://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 bioRxiveprint), so the eprint hyperlink points at the right server. The base URL is derived from the archive’s URL template inpreprint_archiveswhen it has the form<base>/{id}; a storedarchivefield is always written as-is. [#30]Fixed: rendering an entry whose
archiveprefixis not arXiv (e.g. a HAL or bioRxiv eprint) no longer mislabels the eprint asarXiv:<identifier>with a brokenarxiv.orglink; the eprint segment now names the actual archive in its canonical spelling and links to that archive’s page (an unrecognizedarchiveprefixrenders 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 theadd_preprintCLI command, including viaadd --add-preprint) now also stores the preprint’s arXiv primary category (e.g.quant-ph) in theprimaryclassfield, replacing any existing value (which, underoverwrite/--overwrite, described the replaced identifier); the returned named tuple, the per-key report, and the--jsonoutput gain aprimaryclassmember. An explicitly given identifier (--eprint) still stores onlyeprint/archiveprefix, since without network access the category is unknown, andmark_empty/--mark-emptynow clears a staleprimaryclassalongside the emptiedeprintand the stalearchiveprefix. [#30]Added: read-only
files KEYandurls KEYCLI commands, listing an entry’s file attachments and linked URLs, one per line (corresponding to theEntry.filesandEntry.urlsproperties).filesprints each attachment as an absolute path by default;--relativeprints the stored form, relative to the.bibfile’s directory. [#28]Added: a read-only
pathCLI command, printing the absolute path of the.bibfile being operated on (the givenBIBFILE, or the configureddefault_bib_file; corresponding to theLibrary.pathproperty), and a read-onlyconfig_pathCLI command, printing the absolute path of the discoveredbibdeskparser.tomlconfiguration file in effect for that.bibfile (failing with an error when none is found). [#28]Changed:
Library.exportand theexportCLI command no longer take aformatparameter. 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@stringmacro references are replaced by their values or kept bare with the needed@stringdefinitions prepended, andfields–"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"becomesunicode=False(which now consistently TeX-encodes the@stringdefinitions as well, and re-encodes deterministically instead of exposing the in-memory stored form);format="minimal"becomesfields="minimal"(which now also prepends the needed@stringdefinitions; addexpand_strings=Truefor 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 ofLibrary.save. [#27]Changed:
Library.edit, the underlying editing functions, and theeditCLI command no longer take aformatparameter; editing always uses the default export form. [#27]Changed: the
showandget_fieldCLI commands now render field values:@stringmacro references are replaced by the macro’s value (--no-expand-stringsshows the bare macro name instead; in JSON output, every field value then uniformly becomes a{"macro": ..., "value": ...}object –macroisnullfor a literal value – so a macro reference remains distinguishable from a literal value while all fields share one shape), and--no-unicodeshows TeX-encoded values. [#27]Fixed:
Library.exportno longer turns a literal field value that happens to look like a macro name (aValueString) into a bare@stringmacro reference; the stored literal-vs-macro distinction is now preserved on export. [#27]
v0.3.0 - 2026-07-18
Added: a
createCLI command, creating a new, empty.bibfile that contains only the standard BibDesk header comment (bibdeskparser create new.bib, orbibdeskparser createto bootstrap the configureddefault_bib_file). It is the one command whoseBIBFILEmust not already exist – an existing file is never overwritten; every other command still requires an existing file, and the “bibfile not found” error now suggestscreate. [#24]Changed: the first
save()of a from-scratchLibrary(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 raisesFileExistsErrorif the destination path already exists (a from-scratch library has no baseline timestamp for theStaleFileErrorcheck, so it would previously overwrite silently); to adapt, passforce=Truetosave()to deliberately overwrite an existing file with a from-scratch library. [#24]Fixed:
@stringmacro 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@stringdefinitions and bare macro-reference field values, e.g. from a hand-edited@string{JAN = ...}/month = JAN), lookups inLibrary.strings(reading,in, deleting) match case-insensitively, andLibrary.rename_stringlooks 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 (therename_stringCLI command behaves the same). A hand-edited mixed-case@stringmonth 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 inLibrary.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 baremonth = janfield resolves inLibrary.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 inLibrary.stringsand are never written to the.bibfile; a@stringdefinition 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.keysnow returns atupleof citation keys (instead of the generic set-like view inherited fromcollections.abc.MutableMapping, which did not even display the keys in the REPL) and accepts optionaltypes,has,missing, andemptyfilter arguments, matching the--type/--has/--missing/--emptyoptions of thekeysCLI command (which now delegates to it). To adapt: code that relied on the set operations of the previous view should wrap the result inset(); code that relied on a live view reflecting later mutations should callkeys()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-existsand--no-auto-fileflags, which keep working; the new--auto-filepositive form forces auto-filing even withoutfile_automatically = truein the configuration). [#17]Added:
Entry.add_abstract, aLibrary.add_abstractthat delegates to it after locating the entry’s first attached PDF (paths inEntry.filesare library-relative, so the PDF source is only available through theLibrarymethod), and a correspondingadd_abstractCLI command, fetching the abstract of an existing entry from the best available source – Crossref (via the entry’sdoi), the text of the entry’s first attached PDF (extracted with poppler’spdftotext, when installed), the arXiv API (viaeprint), 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 abovemin_confidence/--min-confidence(defaulthigh) are stored, and the CLI reports unstored candidates in full for manual review. Entries that already have a non-empty abstract are skipped unlessoverwrite/--overwriteis given;mark_empty/--mark-emptystores an emptyabstractfield as an “audited, nothing found” marker. New dependency:pylatexenc.Added: an
add_abstracttable inbibdeskparser.toml, configuring the defaults for themin_confidenceandmark_emptykeyword arguments ofLibrary.add_abstract(and the like-named options of theadd_abstractCLI command); exposed asLibrary.config.add_abstract. An explicit argument or command-line flag always overrides the configured default. [#18]Added:
Entry.add_preprint, a purely delegatingLibrary.add_preprint(likeLibrary.add_url), and a correspondingadd_preprintCLI command, recording the arXiv preprint of an existing entry in itseprintfield (witharchiveprefix = 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. Theeprintfield encodes the audit state: absent = unknown (keys --missing eprint), empty = searched with no preprint found (keys --empty eprint, written bymark_empty/--mark-empty), non-empty = known (mark_emptyalso clears a stalearchiveprefix = arXivalongside the emptiedeprint). Entries with a non-emptyeprintare skipped unlessoverwrite/--overwriteis given; a failed search never modifies the entry. arXiv is the only supported preprint server for now. [#19]Added: an
add_preprinttable inbibdeskparser.toml, configuring the default for themark_emptykeyword argument ofLibrary.add_preprint(and the like-named option of theadd_preprintCLI command); exposed asLibrary.config.add_preprint. An explicit argument or command-line flag always overrides the configured default. [#19]Added: an
add_abstractflag onLibrary.add(--add-abstract/--no-add-abstracton theaddCLI 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’sabstractfield, cleaned and validated the same way. [#20]Added: an
add_preprintflag onLibrary.add(--add-preprint/--no-add-preprinton theaddCLI command; default off) that searches arXiv for a preprint matching the newly added entry, exactly asLibrary.add_preprintdoes (skipped when the entry already has aneprint). [#20]Added: an
addtable inbibdeskparser.toml, configuring the defaults for thefix_uppercase,add_abstract, andadd_preprintkeyword arguments ofLibrary.add(and the like-named options of theaddCLI command); exposed asLibrary.config.add. An explicit argument or command-line flag always overrides the configured default. [#20]
v0.2.1 - 2026-07-14
Added:
Library.import_bibtexand a correspondingimportCLI 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.bibfile – after thorough sanitization: journals are stored as@stringmacro references (resolved against the library’s macros and the newjournal_macrosconfiguration table, or newly created from the journal’s lowercased initials, honoring theinitials.journalexceptions; literalarXiv:...pseudo-journals stay literal and deriveeprint/archiveprefix), proper nouns in sentence-case titles and all configuredprotected_wordsare 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, aurlshadowed by the DOI, …) are dropped, and citation keys are regenerated (keep_keys/--keep-keysto opt out) from the configuredauto_keyformat or built-in defaults (e.g.GoerzPRA2014, orGoerz2205.15044for 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.addand a correspondingaddCLI 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 asimport_bibtex. The CLI command accepts--dry-run(print the entry instead of saving) and--fix-uppercase(repair all-uppercase publisher metadata, also available onimport). This subsumes the functionality of the getbibtex script. New dependencies:habanero,arxiv, andhttpx. [#14]Added: a
journal_macrostable inbibdeskparser.toml, mapping@stringmacro names to the journal name(s) they stand for (a list value declares publisher-spelling aliases of the same journal), and aprotected_wordslist of words that import always brace-protects inside titles; exposed asLibrary.config.journal_macrosandLibrary.config.protected_words. [#14]Added: CLI commands for working with the fields of a single entry:
fields KEYlists the defined field names,get_field KEY FIELDNAMEprints one field value,set_field KEY FIELDNAME VALUEsets a field (with--literal/--macroto force the value to be stored as literal text or as a bare@stringmacro reference), anddelete_field KEY FIELDNAMEremoves a field. These correspond to thedictinterface ofEntry(iteration, indexing, assignment, anddel). [#13]Added: an
author KEYand aneditor KEYCLI command, printing an entry’s authors/editors as structured names (last-name-first; with--json, as objects withfirst/von/last/jrname parts), corresponding to theEntry.authorandEntry.editorproperties. [#13]Added: a
set_type KEY TYPECLI command, changing an entry’s type (corresponding to assigningEntry.entry_type). [#13]Added: the
groupsandkeywordsCLI commands accept an optional entryKEY, listing the groups/keywords of that single entry (corresponding to theEntry.groupsandEntry.keywordsproperties) instead of the library-wide mapping. [#13]Added: filter options on the
keysCLI command:--type TYPEkeeps only entries of the given type(s), and--has FIELD/--missing FIELD/--empty FIELDkeep only entries whereFIELDis defined with a non-empty value, not defined at all, or defined but empty, respectively (all repeatable). [#13]Fixed: assigning a
MacroString(e.g. viaset_field --macro) now normalizes the macro name to BibDesk’s canonical lowercase form, matching how@stringdefinitions are stored; previously a non-canonical name such asPRAwas stored verbatim and then read back as a literalValueStringinstead of aMacroString. [#13]Fixed: the CLI’s top-level
--helpnow shows a complete, un-truncated one-line summary for every subcommand (previously long summaries were cut off with...). [#11]Fixed:
Library.render(and therenderCLI command) now expands@stringmacros in the rendered citation, so a field likejournal = prashows its defined value (e.g.Phys. Rev. A) rather than the bare macro name. [#12]Changed: bump the PyPI
Development Statusclassifier from2 - Pre-Alphato3 - Alpha.
v0.2.0 - 2026-07-13
Added: automatic filing of file attachments, mirroring BibDesk’s AutoFile feature.
Library.rename_filewithout anew_filenamemoves 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_filefiles newly attached files the same way when auto-filing is in effect (file_automatically = truein the configuration, or explicitformat_spec/auto_file_locationarguments, withauto_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/%nspecifier disambiguates against existing files on disk. Therename_fileandadd_fileCLI commands gain the corresponding options. [#10]Added: an
auto_filetable inbibdeskparser.toml, withformat_spec(a single format or a per-type table),location(the directory files are moved into, relative to the.bibfile or absolute),lowercase,clean, andfile_automaticallykeys, exposed asLibrary.config.auto_file. [#10]Added: the file-name context of the format-specifier language: the
%l/%L/%e/%Eoriginal-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_specalso evaluates file-name formats, via a newfilenamekeyword argument that selects the file-name dialect (any non-Nonevalue, including"", does so) and supplies the original-name specifiers%l/%L/%e/%E. Thefilenameneed not exist or be one of the entry’s attachments; the format is evaluated purely, without touching the filesystem. Iffilenameis an attachment’s current path that already matches the format, it evaluates to itself. Theeval_format_specCLI command gains a matching--filenameoption. [#10]Changed:
Library.add_fileandLibrary.rename_filenow return the stored library-relative path of the attachment (previouslyNone),rename_file’snew_filenameargument 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 onrename_filefailing for a target in a nonexistent directory; the return values can be ignored. [#10]Added: automatic citation-key generation. Calling
Library.rekeywithout anew_keygenerates the key from an auto-key format in BibDesk’s format-specifier language (e.g.%a1%c{journal}0%Y%u0), taken from the newauto_keytable ofbibdeskparser.tomlor from the newformat_speckeyword argument. A key that already matches the format is kept unchanged, and a%u/%U/%nspecifier in the format disambiguates collisions, like in BibDesk. TherekeyCLI command correspondingly makesNEW_KEYoptional, adds a--format-spec PATTERNoption, and prints the generated key. [#9]Added:
Library.eval_format_specand a matching read-onlyeval_format_specCLI 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_keytable’sformat_specmay be a table mapping each entry type to its own format (with""as the fallback for unlisted types), so a mixed-type library can namejournalfor articles,booktitlefor conference papers, and so on. Theauto_keysettings are also available and settable asLibrary.config.auto_key(withformat_spec,lowercase, andcleanattributes). [#9]Added: an
initialstable inbibdeskparser.toml, defining per-field exceptions (e.g. journal or conference-proceedings initials) to the acronym that the%cformat specifier builds from a field value. [#9]Added: a “Format Specifiers” reference page documenting the format-specifier language. [#9]
Changed:
Library.rekeynow returns the resulting citation key (previouslyNone). [#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
bibdeskparserCLI, and expanded the top-level--helptext to orient such callers (read-only vs. mutating commands,--json, exit codes). [#8]Added:
Library.editandLibrary.edit_stringsaccept a function foreditor, 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 aValueErrorinstead of prompting interactively. [#8]Added: a
--stdinoption on theeditandedit_stringsCLI commands, reading the full edited text from standard input instead of opening an editor, and a--biboption onstrings, printing the@stringdefinitions as re-parseable@string{name = {value}}lines. Together withexport, these allow non-interactive (e.g., scripted or AI-agent) editing via pipes:export KEY... | ... | edit KEY... --stdinandstrings --bib | ... | edit_strings --stdin. [#8]Changed: the
editandedit_stringsCLI commands now fail immediately with a usage error when invoked without a terminal on standard input and without--stdinor an explicit--editor, instead of blocking on$EDITOR. Non-interactive callers must pass--stdin(piping in the edited text) or--editorwith a command that needs no terminal. [#8]Fixed: newly added entries, new
@stringmacros, and a newly synthesized static-groups@commentblock were appended at the very end of the.bibfile, after BibDesk’s group@commentblocks. They are now written at their canonical position: macros in the alphabetically sorted@stringrun before the first entry, entries before the group@commentblocks, and the static-groups block before the smart-groups block, matching the layout BibDesk itself writes.Added:
Library.searchand a correspondingsearchCLI subcommand: find entries matching a query, ranked best match first. Matching runs against the stored field values (bare@stringmacro 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
bibdeskparsercommand-line tool that exposes the publicLibraryAPI as subcommands (keys,show,render,export,add_to_group,set_string,edit, …). Data-output commands accept--json; the.bibfile argument may be omitted whendefault_bib_fileis configured. [#4]Added: a
default_bib_fileoption inbibdeskparser.toml, naming the.bibfile the command-line tool operates on when none is given. [#4]Added: the
BIBDESKPARSER_CONFIGenvironment variable, naming the user-levelbibdeskparser.tomlin place of the XDG location. Setting it to an empty value disables the user-level configuration entirely. [#7]Added:
MacroString, mirroringValueString, to force a field value to be stored as a bare@stringmacro reference. Both are subtypes ofstr.Added:
Entry.add_url,Entry.replace_url,Entry.remove_urland the correspondingLibrary.add_url,Library.replace_url,Library.remove_urlmethods for managing an entry’s linked URLs.Added: validation and normalization of
Entrytypes: constructing or assigning an entry type now lowercases it and rejects unrecognized types with aValueError. Loading a.bibfile still never validates.Added: validation of the
author/editorfields: assigning an unparseable value raisesValueError.Added: a
UserWarningwhen 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.tomlconfiguration file (searched for next to the.bibfile and in the XDG config location), withverify_typesandverify_fieldsflags to disable entry-type validation and field-appropriateness warnings, andtypes/fieldstables to define custom entry types and fields or extend the built-in ones. The active configuration is exposed as theLibrary.configclass attribute (equally readable from a library instance asbib.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.tomlfile.Changed:
Valuehas been renamed toValueString(breaking: renameValuetoValueStringin your code). Values returned by theEntrydict interface are nowValueString(for literal/braced values) orMacroString(for bare@stringmacro references) instances; both arestrsubclasses and compare as plain strings.Changed:
Entry.urlsis now a read-only tuple (breaking: replace assignment toentry.urlswith the newadd_url/replace_url/remove_urlmethods).Changed: the
keywordsfield is now readable through theEntrydict interface (indexing an entry bykeywordsreturns the comma-joined string). It is still not writable that way, and theEntry.keywordsproperty remains read-only; keywords are edited only through the owningLibrary.Removed: the public
Entry.dirtyproperty (breaking: there is no public replacement; it was an internal detail).
v0.1.0 - 2026-07-07
Initial release.