Repository: pcubillos/bibmanager (language: python)

Issue:
bm.loadfile() breaks on mismatched braces that are not part of an entry
The code should ignore mismatched braces when dealing with comments.

Here's a MWE:
```python

text = """@ARTICLE{2010A&A...518L...1P,
   author = {{Pilbratt}, G.~L. and others},
    title = "{Herschel Space Observatory. An ESA facility for far-infrared and submillimetre astronomy}",
  journal = {\aap},
archivePrefix = "arXiv",
   eprint = {1005.5331},
 primaryClass = "astro-ph.IM",
 keywords = {space vehicles, space vehicles: instruments, infrared: general, submillimetre: general},
     year = 2010,
    month = jul,
   volume = 518,
      eid = {L1},
    pages = {L1},
      doi = {10.1051/0004-6361/201014759},
   adsurl = {http://adsabs.harvard.edu/abs/2010A%26A...518L...1P},
  adsnote = {Provided by the SAO/NASA Astrophysics Data System}
}

Any text like this one outside an entry (e.g., [at]article{...}) count as comments
{Riedinger}, J.~R. and {Passvogel}, T. and
       {Crone}, G. and {Doyle}, D. and {Gageur}, U. and {Heras}, A.~M. and
       {Jewell}, C. and {Metcalfe}, L. and {Ott}, S. and {Schmidt}, M.
       }"""

import bibmanager.bib_manager as bm
bibs = bm.loadfile(text=text)
```

```
ValueError: Mismatched braces in line 23:
'       }'
```
Bibtex type `misc` does not require author
The Bibtex entry type "misc" has no required fields (https://en.wikipedia.org/wiki/BibTeX#Entry_types) but `bibm` refuses to take this bibtex file:

```bibtex
@MISC{Nasa_undated-bs,
  title  = "{NASA} {SMD} Annual Report 2018",
  author = "{NASA}"
}
```

```bash
$ bibm merge test.bib                                                                        (py37)
Traceback (most recent call last):
  File "/Users/klay6683/miniconda3/envs/py37/bin/bibm", line 10, in <module>
    sys.exit(main())
  File "/Users/klay6683/miniconda3/envs/py37/lib/python3.7/site-packages/bibmanager/__main__.py", line 748, in main
    args.func(args)
  File "/Users/klay6683/miniconda3/envs/py37/lib/python3.7/site-packages/bibmanager/__main__.py", line 83, in cli_merge
    bm.merge(bibfile=args.bibfile, take=args.take)
  File "/Users/klay6683/miniconda3/envs/py37/lib/python3.7/site-packages/bibmanager/bib_manager/bib_manager.py", line 623, in merge
    new = loadfile(bibfile)
  File "/Users/klay6683/miniconda3/envs/py37/lib/python3.7/site-packages/bibmanager/bib_manager/bib_manager.py", line 499, in loadfile
    bibs = [Bib(entry) for entry in entries]
  File "/Users/klay6683/miniconda3/envs/py37/lib/python3.7/site-packages/bibmanager/bib_manager/bib_manager.py", line 499, in <listcomp>
    bibs = [Bib(entry) for entry in entries]
  File "/Users/klay6683/miniconda3/envs/py37/lib/python3.7/site-packages/bibmanager/bib_manager/bib_manager.py", line 133, in __init__
    raise ValueError(f"Bibtex entry '{self.key}' is missing author, "
ValueError: Bibtex entry 'Nasa_undated-bs' is missing author, title, or year.
```
Note, i'm not saying this is a good bibtex entry, as obviously that NASA report has authors, years etc. But formally speaking `bibm` shouldn't reject this entry, as it's defined by BibTex to not require any field. Of course `bibm` could decide that @MISC entries are out of scope for itself.
issue with importing bibtex entry for book
Hi folks, bibm merge balks at merging a .bib entry for a book if it doesn't have an author.  In my case it has an editor and the bibtex documentation says that the required field is an author OR and editor. 

Best,
R Haynes


Discussion / hints:

Right. I'm going to keep this type of entries out of the scope of bibmanager (at least until somebody comes with a reasonable case needing to use a 'misc' entry for a peer-review article).
Thanks.
But, then you need to implement a stable response to it, b/c ppl will have @misc entries. Your import should not fail, but should simply ignore @misc and tell the user so.
Hi, yes, that's a known 'feature'. Originally, I designed the code to request having author, year, and title (to do all the indexing, sorting, etc.). Actually I couldn't conceive entries could not have an author entry (my bad!).
I will sort this out along with #77 

Sorry about that!

Thanks!

Sincerely,

Dr. Ronald D. Haynes

Professor, Department of Mathematics and Statistics
Chair, MSc and PhD Scientific Computing Programs
Memorial University of Newfoundland

We acknowledge that the lands on which Memorial University’s campuses are situated are in the traditional territories of diverse Indigenous groups, and we acknowledge with respect the diverse histories and cultures of the Beothuk, Mi’kmaq, Innu, and Inuit of this province.
On Dec 23, 2020, 3:43 PM -0330, Patricio Cubillos <notifications@github.com>, wrote:
> Hi, yes, that's a known 'feature'. Originally, I designed the code to request having author, year, and title (to do all the indexing, sorting, etc.). Actually I couldn't conceive entries could not have an author entry (my bad!).
> I will sort this out along with #77
> Sorry about that!
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub, or unsubscribe.


Fix patch:
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index b3443ec..1769fa9 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -2108,3 +2108,170 @@ Updated README.
 
 Updated pypi readme.
 
+
+*****  Thu Dec 31 21:48:43 CET 2020  *****
+
+Moved find_closing_bracket() from browser.py to utils.py.
+Updated docstring and added tests.
+
+
+*****  Sat Jan 2 13:21:39 CET 2021  *****
+
+Re-wrote bm.loadfile() to comply with the bibtex/bst file formatting
+when reading a file/piece of text.
+See section 8 of the Tame the BeaST document:
+http://mirror.easyname.at/ctan/info/bibtex/tamethebeast/ttb_en.pdf
+Updated example and tests.
+
+*****
+
+Renamed bm.loadfile() to read_file().
+Along with the last two commits, this fixes #79
+
+*****
+
+Refactored Bib.__init__() to default title to None, get value from
+booktitle (if there is no title field).
+Skipped no-title test (to be removed).
+
+*****
+
+Refactored Bib.__init__() to check that year field has a correct value.
+Added test.
+
+*****
+
+Updated invalid-month error in Bib.__init__() to output the key of the
+offending entry.
+Updated tests.
+
+*****
+
+Added mock_open() fixture to patch subprocess.run call in pm.open().
+(I would need to be careful of this fixture if I ever need to use
+subprocess.run in another part of the code).
+Added mocks to tests where I need to open a PDF to avoid raising
+'can't open PDF' sort of issues.
+
+*****
+
+Tweaked entry in tests/conftest.py to prevent minor format warning.
+
+
+*****  So 10. Jan 13:45:02 CET 2021  *****
+
+Added removal of files created during testing in test_cli.py
+
+*****
+
+Modified u.cond_split() to ignore lettercase as BibTeX does.
+Added respective tests.
+
+*****
+
+Added in bm.Bib() constructor an exception to avoid crashing when
+the value of the month field is an empty string.
+Added test.
+
+*****
+
+Added get_opening argument to u.find_closing_bracket() function
+to also return the position of the opening bracket when requested.
+Updated and added u.find_closing_bracket() tests.
+
+*****
+
+Tweaked bm.read_file() to ignore @comment entries like BibTeX does.
+Added respective tests.
+
+
+*****  Di 12. Jan 16:56:08 CET 2021  *****
+
+Refactored u.AlwaysPassValidator() class to improve workflow.
+No change in behavior.
+
+*****
+
+Modified u.AlwaysPassValidator() class to display the year of entry
+in the bottom toolbar.
+
+*****
+
+Updated tests for prompt_toolkit >= 3.0.10.
+Added version requirement for tests.
+
+*****
+
+Allowed entries to not have a title.
+Added respective test for bm.search().
+When an entry has no title, display 'NO_TITLE' in browser and search
+bottom toolbars.
+Added test to prevent identifying None titles as duplicates.
+
+
+*****  Mi 13. Jan 15:29:55 CET 2021  *****
+
+Removed title-not-found-error test (no longer needed).
+
+*****
+
+Stored number of entries in the examples/sample.bib file into
+a variable.  To simplify further edits during testing.
+
+*****
+
+Allowed entries to not have a year.
+Added entry with no year into examples/sample.bib for testing.
+Added tests for Bib1 equal or lower than Bib2 when an entry has
+no year.
+
+
+*****  Do 14. Jan 18:29:27 CET 2021  *****
+
+Allowed entries to not have an author.
+Added entry with no author into examples/sample.bib for testing.
+Added tests for Bib1 equal or lower than Bib2 when entries have
+no author.
+
+*****
+
+Modified u.get_authors() to return an empty string when the input
+is None.
+Added respective tests.
+
+*****
+
+Added Exception to pm.guess_name() when entry does not have author
+nor year (cannot guess an appropriate filename).
+Added respective test.
+
+
+*****  Fr 15. Jan 17:09:50 CET 2021  *****
+
+Refactored year parsing in Bib.__init__() to allow years 0--999.
+
+*****
+
+Implemented u.warnings_format() to set a custom warnings format.
+Modified Bib.__init__() to throw a warning instead of an exception
+for a bad year format.
+Added respective tests.
+
+*****
+
+Turned invalid-month exception into a warning.
+Updated respective tests.
+
+*****
+
+Added key argument to u.parse_name() to display when raising warnings
+Turned invalid-author format into too-many-commas warning.
+Attemt to correct the entry when that's the case (guessing what
+BibTeX does).
+Added respective tests.
+
+*****
+
+Bumped bibmanager to version 1.3.1
+The next merge should solve #47 #77 #78 #79
+
diff --git a/bibmanager/VERSION.py b/bibmanager/VERSION.py
index 7d4d91f..cdf89e4 100644
--- a/bibmanager/VERSION.py
+++ b/bibmanager/VERSION.py
@@ -4,4 +4,4 @@
 # bibmanager Version:
 BM_VER =  1  # Major version
 BM_MIN =  3  # Minor version
-BM_REV =  0  # Revision
+BM_REV =  1  # Revision
diff --git a/bibmanager/__main__.py b/bibmanager/__main__.py
index 63a82bf..0da2d86 100644
--- a/bibmanager/__main__.py
+++ b/bibmanager/__main__.py
@@ -163,8 +163,10 @@ def cli_search(args):
         return
 
     for match in matches:
-        title = textwrap.fill(f"Title: {match.title}, {match.year}",
-                              width=78, subsequent_indent='       ')
+        year = '' if match.year is None else f', {match.year}'
+        title = textwrap.fill(
+            f"Title: {match.title}{year}",
+            width=78, subsequent_indent='       ')
         author_format = 'short' if args.verb < 2 else 'long'
         authors = textwrap.fill(
             f"Authors: {match.get_authors(format=author_format)}",
@@ -204,7 +206,7 @@ def cli_export(args):
 
 def cli_cleanup(args):
     """Command-line interface to clean up a bibfile."""
-    bibs = bm.loadfile(args.bibfile)
+    bibs = bm.read_file(args.bibfile)
     if args.ads:
         try:
             updated = am.update(base=bibs)
diff --git a/bibmanager/bib_manager/bib_manager.py b/bibmanager/bib_manager/bib_manager.py
index 1253bf8..175a8c9 100644
--- a/bibmanager/bib_manager/bib_manager.py
+++ b/bibmanager/bib_manager/bib_manager.py
@@ -6,7 +6,7 @@
     'display_bibs',
     'remove_duplicates',
     'filter_field',
-    'loadfile',
+    'read_file',
     'save',
     'load',
     'find',
@@ -28,6 +28,7 @@
 import pickle
 import urllib
 import subprocess
+import warnings
 
 import numpy as np
 import prompt_toolkit
@@ -43,6 +44,7 @@
 from .. import utils as u
 from ..__init__ import __version__
 
+
 # Some constant definitions:
 lexer = prompt_toolkit.lexers.PygmentsLexer(BibTeXLexer)
 
@@ -57,7 +59,7 @@ class Bib(object):
   def __init__(self, entry, pdf=None, freeze=None):
       """
       Create a Bib() object from given entry.  Minimally, entries must
-      contain the author, title, and year keys.
+      contain the author key.
 
       Parameters
       ----------
@@ -71,7 +73,6 @@ def __init__(self, entry, pdf=None, freeze=None):
       Examples
       --------
       >>> import bibmanager.bib_manager as bm
-      >>> from bibmanager.utils import Author
       >>> entry = '''@Misc{JonesEtal2001scipy,
                 author = {Eric Jones and Travis Oliphant and Pearu Peterson},
                 title  = {{SciPy}: Open source scientific tools for {Python}},
@@ -92,6 +93,9 @@ def __init__(self, entry, pdf=None, freeze=None):
           raise ValueError("Mismatched braces in entry.")
       self.content  = entry
       # Defaults:
+      self.authors = None
+      self.title = None
+      self.year = None
       self.month    = 13
       self.adsurl   = None
       self.bibcode  = None
@@ -109,17 +113,28 @@ def __init__(self, entry, pdf=None, freeze=None):
           if key == "title":
               # Title with no braces, tabs, nor linebreak and corrected blanks:
               self.title = " ".join(re.sub("({|})", "", value).split())
+          elif key == "booktitle" and self.title is None:
+              # Only when the entry does not contain a 'title' field:
+              self.title = " ".join(re.sub("({|})", "", value).split())
 
           elif key == "author":
               # Parse authors finding all non-brace-nested 'and' instances:
-              authors, nests = u.cond_split(value.replace("\n"," "), " and ",
-                                  nested=nested, ret_nests=True)
-              self.authors = [u.parse_name(author, nested)
-                              for author,nested in zip(authors,nests)]
+              authors, nests = u.cond_split(
+                  value.replace("\n"," "), " and ",
+                  nested=nested,
+                  ret_nests=True)
+              self.authors = [
+                  u.parse_name(author, nested, self.key)
+                  for author,nested in zip(authors,nests)]
 
           elif key == "year":
-              r = re.search('[0-9]{4}', value)
-              self.year = int(r.group(0))
+              value = re.sub('({|}|")', '', value)
+              if value.isnumeric():
+                  self.year = int(value)
+              else:
+                  warnings.formatwarning = u.warnings_format
+                  warnings.warn(
+                      f"Bad year format value '{value}' for entry '{self.key}'")
 
           elif key == "month":
               value = value.lower().strip()
@@ -131,8 +146,12 @@ def __init__(self, entry, pdf=None, freeze=None):
                   self.month = month
               elif month in months.keys():
                   self.month = months[month]
+              elif month == '':
+                  pass
               else:
-                  raise ValueError(f'Invalid month value ({value})')
+                  warnings.formatwarning = u.warnings_format
+                  warnings.warn(
+                      f"Invalid month value '{value}' for entry '{self.key}'")
 
           elif key == "doi":
               self.doi = value
@@ -149,19 +168,19 @@ def __init__(self, entry, pdf=None, freeze=None):
           elif key == "isbn":
               self.isbn = value.lower().strip()
 
-      for attr in ['authors', 'title', 'year']:
-          if not hasattr(self, attr):
-              raise ValueError(f"Bibtex entry '{self.key}' is missing author, "
-                                "title, or year.")
       # First-author fields used for sorting:
       # Note this differs from Author[0], since fields are 'purified',
       # and 'first' goes only by initials().
-      self.sort_author = u.Sort_author(u.purify(self.authors[0].last),
-                                       u.initials(self.authors[0].first),
-                                       u.purify(self.authors[0].von),
-                                       u.purify(self.authors[0].jr),
-                                       self.year,
-                                       self.month)
+      if self.authors is not None:
+          last = u.purify(self.authors[0].last)
+          first = u.initials(self.authors[0].first)
+          von = u.purify(self.authors[0].von)
+          jr = u.purify(self.authors[0].jr)
+      else:
+          last, first, von, jr = None, None, None, None
+
+      self.sort_author = u.Sort_author(
+          last, first, von, jr, self.year, self.month)
 
   def update_content(self, other):
       """Update the bibtex content of self with that of other."""
@@ -225,6 +244,8 @@ def __contains__(self, author):
       >>> '^Perez' in bib
       False
       """
+      if self.authors is None:
+          return False
       # Check first-author mark:
       if author[0:1] == '^':
           author = author[1:]
@@ -257,20 +278,29 @@ def __lt__(self, other):
       fields are equal, go on to next field to compare.
       """
       s, o = self.sort_author, other.sort_author
-      if s.last != o.last:
-          return s.last < o.last
-      if len(s.first)==1 or len(o.first) == 1:
-          if s.first[0:1] != o.first[0:1]:
-              return s.first < o.first
-      else:
-          if s.first != o.first:
-              return s.first < o.first
-      if s.von != o.von:
-          return s.von < o.von
-      if s.jr != o.jr:
-          return s.jr < o.jr
-      if s.year != o.year:
-          return s.year < o.year
+      if s.last is None and o.last is not None:
+          return False
+      elif s.last is not None and o.last is None:
+          return True
+      elif s.last is not None and o.last is not None:
+          if s.last != o.last:
+              return s.last < o.last
+
+          if len(s.first) == 1 or len(o.first) == 1:
+              if s.first[0:1] != o.first[0:1]:
+                  return s.first < o.first
+          else:
+              if s.first != o.first:
+                  return s.first < o.first
+          if s.von != o.von:
+              return s.von < o.von
+          if s.jr != o.jr:
+              return s.jr < o.jr
+
+      s_year = 9999 if s.year is None else s.year
+      o_year = 9999 if o.year is None else o.year
+      if s_year != o_year:
+          return s_year < o_year
       return s.month < o.month
 
   def __eq__(self, other):
@@ -280,17 +310,24 @@ def __eq__(self, other):
       Evaluate to equal by first initial if one entry has less initials
       than the other.
       """
-      if len(self.sort_author.first)==1 or len(other.sort_author.first)==1:
-          first = self.sort_author.first[0:1] == other.sort_author.first[0:1]
+      s, o = self.sort_author, other.sort_author
+      if s.last is None and o.last is None:
+          return s.year == o.year and s.month == o.month
+      if s.last is None or o.last is None:
+          return False
+
+      if len(s.first) == 1 or len(o.first) == 1:
+          first = s.first[0:1] == o.first[0:1]
       else:
-          first = self.sort_author.first == other.sort_author.first
+          first = s.first == o.first
 
-      return (self.sort_author.last  == other.sort_author.last
-          and first
-          and self.sort_author.von   == other.sort_author.von
-          and self.sort_author.jr    == other.sort_author.jr
-          and self.sort_author.year  == other.sort_author.year
-          and self.sort_author.month == other.sort_author.month)
+      return (
+          s.last == o.last and
+          first and
+          s.von == o.von and
+          s.jr == o.jr and
+          s.year == o.year and
+          s.month == o.month)
 
   def __le__(self, other):
       return self.__lt__(other) or self.__eq__(other)
@@ -478,8 +515,8 @@ def filter_field(bibs, new, field, take):
       new.pop(idx)
 
 
-def loadfile(bibfile=None, text=None):
-    """
+def read_file(bibfile=None, text=None):
+    r"""
     Create a list of Bib() objects from a BibTeX file (.bib file).
 
     Parameters
@@ -498,62 +535,62 @@ def loadfile(bibfile=None, text=None):
     Examples
     --------
     >>> import bibmanager.bib_manager as bm
-    >>> import os
-    >>> bibfile = os.path.expanduser("~") + "/.bibmanager/examples/sample.bib"
-    >>> bibs = bm.loadfile(bibfile)
+    >>> text = (
+    >>>    "@misc{AASteamHendrickson2018aastex62,\n"
+    >>>    "author = {{AAS Journals Team} and {Hendrickson}, Amy},\n"
+    >>>    "title  = {{AASJournals/AASTeX60: Version 6.2 official release}},\n"
+    >>>    "year   = 2018\n"
+    >>>    "}")
+    >>> bibs = bm.read_file(text=text)
     """
     entries = []  # Store Lists of bibtex entries
-    entry   = []  # Store lines in the bibtex
     meta_info = []  # Meta information for each entry
-    parcount = 0  # Braces count (+1 for each '{', -1 for each '}')
 
     # Load a bib file:
-    if bibfile is not None:
-        f = open(bibfile, 'r')
-    elif text is not None:
-        f = text.splitlines()
-    else:
-        raise TypeError("Missing input arguments for loadfile(), at least "
+    if bibfile is None and text is None:
+        raise TypeError("Missing input arguments for read_file(), at least "
                         "bibfile or text must be provided.")
-
-    meta = {'pdf':None, 'freeze':None}
-    for i,line in enumerate(f):
-        # Meta info:
-        if parcount == 0:
+    if bibfile is not None:
+        with open(bibfile, 'r') as f:
+            text = f.read()
+
+    position = 0
+    while True:
+        start_pos = text.find('@', position)
+        if start_pos < 0:
+            break
+        # TBD: bracket_or_parenthesis
+        pos = u.find_closing_bracket(text, start_pos, get_open=True)
+        # Open end:
+        if pos is None:
+            start_line = len(text[:start_pos].splitlines())
+            line = text.splitlines()[start_line].rstrip()
+            raise ValueError(
+                f"Mismatched braces at/after line {start_line}:\n{line}")
+        left_bracket, end_pos = pos
+        # Skip @comment entries
+        if text[start_pos+1:start_pos+left_bracket].lower() == 'comment':
+            position = end_pos
+            continue
+        # Content outside/before entry is comments or meta info:
+        meta = {
+            'pdf': None,
+            'freeze': None,
+        }
+        for line in text[position:start_pos].splitlines():
             if line.lower().startswith('pdf'):
                 meta['pdf'] = line.split()[-1]
             if line.lower().strip() == 'freeze':
                 meta['freeze'] = True
 
-        # New entry:
-        if line.startswith("@") and parcount != 0:
-            raise ValueError(
-                f"Mismatched braces in line {i}:\n'{line.rstrip()}'")
-
-        parcount += u.count(line)
-        # Skip content outside the entries (except when starting a new one):
-        if parcount == 0 and entry == [] and not line.startswith("@"):
-            continue
-
-        if parcount < 0:
-            raise ValueError(
-                f"Mismatched braces in line {i}:\n'{line.rstrip()}'")
-
-        entry.append(line.rstrip())
+        entries.append(text[start_pos:end_pos+1])
+        meta_info.append(meta)
+        position = end_pos
 
-        if parcount == 0 and entry != []:
-            entries.append("\n".join(entry))
-            entry = []
-            meta_info.append(meta)
-            meta = {'pdf':None, 'freeze':None}
-
-    if bibfile is not None:
-        f.close()
-
-    if parcount != 0:
-        raise ValueError("Invalid input, mistmatched braces at end of file.")
-
-    bibs = [Bib(entry, **meta) for entry,meta in zip(entries,meta_info)]
+    bibs = [
+        Bib(entry, **meta)
+        for entry,meta in zip(entries,meta_info)
+    ]
 
     remove_duplicates(bibs, "doi")
     remove_duplicates(bibs, "isbn")
@@ -777,7 +814,7 @@ def merge(bibfile=None, new=None, take="old", base=None):
       bibs = base
 
   if bibfile is not None:
-      new = loadfile(bibfile)
+      new = read_file(bibfile)
   if new is None:
       return
 
@@ -813,7 +850,7 @@ def merge(bibfile=None, new=None, take="old", base=None):
   keep = np.zeros(len(new), bool)
   bm_titles = [bib.title for bib in bibs]
   for i,bib in enumerate(new):
-      if bib.title not in bm_titles:
+      if bib.title not in bm_titles or bib.title is None:
           keep[i] = True
           continue
       idx = bm_titles.index(bib.title)
@@ -881,7 +918,7 @@ def init(bibfile=None, reset_db=True, reset_config=False):
               with u.ignored(OSError):
                   os.remove(bm_file)
       else:
-          bibs = loadfile(bibfile)
+          bibs = read_file(bibfile)
           save(bibs)
           export(bibs, meta=True)
 
@@ -904,7 +941,7 @@ def add_entries(take='ask'):
   newbibs = prompt_toolkit.prompt(
       "Enter a BibTeX entry (press META+ENTER or ESCAPE ENTER when done):\n",
       multiline=True, lexer=lexer, style=style)
-  new = loadfile(text=newbibs)
+  new = read_file(text=newbibs)
 
   if len(new) == 0:
       print("No new entries to add.")
@@ -936,7 +973,7 @@ def edit():
                   "the bib file.")
     # Check edits:
     try:
-        new = loadfile(u.BM_TMP_BIB())
+        new = read_file(u.BM_TMP_BIB())
     finally:
         # Always delete the tmp file:
         os.remove(u.BM_TMP_BIB())
@@ -995,12 +1032,21 @@ def search(authors=None, year=None, title=None, key=None, bibcode=None):
   >>>                              "2017AJ....153....3C"])
   """
   matches = load()
+
   if year is not None:
-      try: # Assume year = [from_year, to_year]
-          matches = [bib for bib in matches if bib.year >= year[0]]
-          matches = [bib for bib in matches if bib.year <= year[1]]
-      except:
+      if isinstance(year, int):
           matches = [bib for bib in matches if bib.year == year]
+      else: # Assume year = [from_year, to_year]
+          matches = [
+              bib for bib in matches
+              if bib.year is not None
+              if bib.year >= year[0]
+          ]
+          matches = [
+              bib for bib in matches
+              if bib.year is not None
+              if bib.year <= year[1]
+          ]
 
   if authors is not None:
       if isinstance(authors, str):
@@ -1016,8 +1062,11 @@ def search(authors=None, year=None, title=None, key=None, bibcode=None):
       elif not isinstance(title, (list, tuple, np.ndarray)):
           raise ValueError("Invalid input format for 'title'.")
       for word in title:
-          matches = [bib for bib in matches
-                     if word.lower() in bib.title.lower()]
+          matches = [
+              bib for bib in matches
+              if bib.title is not None
+              if word.lower() in bib.title.lower()
+          ]
 
   if key is not None:
       if isinstance(key, str):
diff --git a/bibmanager/bib_manager/browser.py b/bibmanager/bib_manager/browser.py
index c3b53a6..7af37e0 100755
--- a/bibmanager/bib_manager/browser.py
+++ b/bibmanager/bib_manager/browser.py
@@ -205,51 +205,6 @@ def apply_transformation(self, transformation_input):
         return Transformation(fragments)
 
 
-def find_closing_bracket(text, start_pos=0):
-    """
-    Find the closing bracket that matches the nearest opening bracket in
-    text starting from start_pos.
-
-    Parameters
-    ----------
-    text: String
-        Text to search through.
-    start_pos: Integer
-        Starting position where to start looking for the brackets.
-    Returns
-    -------
-    i: Integer
-       The absolute position to the cursor position at closing bracket.
-       Returns None if there are no matching brackets.
-
-    Examples
-    --------
-    >>> text = '@ARTICLE{key, author={last_name}, title={The Title}}'
-    >>> pos = find_closing_bracket(text)
-    >>> print(text[:pos+1])
-    @ARTICLE{key, author={last_name}, title={The Title}}
-    >>> pos = find_closing_bracket(text, start_pos=14)
-    >>> print(text[14:pos+1])
-    author={last_name}
-    """
-    left_bracket = text[start_pos:].find('{')
-    if left_bracket < 0:
-        return None
-    start_pos += left_bracket + 1
-    end_pos = len(text)
-
-    stack = 1
-    for index,char in enumerate(text[start_pos:end_pos]):
-        if char == '{':
-            stack += 1
-        elif char == '}':
-            stack -= 1
-
-        if stack == 0:
-            return start_pos + index
-    return None
-
-
 def get_current_key(doc, keys, get_start_end=False, get_expanded=False):
     """
     Get the key for the bibtex entry currently under the cursor.
@@ -270,7 +225,7 @@ def get_current_key(doc, keys, get_start_end=False, get_expanded=False):
         key_end = doc.text.find(',', start_pos)
         key = doc.text[key_start+1:key_end].strip()
         if get_start_end:
-            end_pos = find_closing_bracket(doc.text, start_pos) + 2
+            end_pos = u.find_closing_bracket(doc.text, start_pos) + 2
 
     if not (get_start_end or get_expanded):
         return key
@@ -334,7 +289,9 @@ def get_infobar_text():
         """Get author-year-title of entry under cursor."""
         key = get_current_key(text_field.buffer.document, keys)
         bib = bibs[keys.index(key)]
-        return f"{bib.get_authors('ushort')}{bib.year}: {bib.title}"
+        year = '' if bib.year is None else bib.year
+        title = 'NO_TITLE' if bib.title is None else bib.title
+        return f"{bib.get_authors('ushort')}{year}: {title}"
 
     search_buffer = Buffer(
         completer=WordCompleter(keys),
diff --git a/bibmanager/config_manager/config_manager.py b/bibmanager/config_manager/config_manager.py
index 3a362e7..019a0bc 100644
--- a/bibmanager/config_manager/config_manager.py
+++ b/bibmanager/config_manager/config_manager.py
@@ -249,7 +249,7 @@ def set(key, value):
                     f"database.  Please update to a version >= {pickle_ver}.")
               return
           new_biblio = f'{new_home}/{os.path.basename(u.BM_BIBFILE())}'
-          bm.merge(new=bm.loadfile(new_biblio), take='new')
+          bm.merge(new=bm.read_file(new_biblio), take='new')
 
       # Move (overwrite) database files:
       bm_files = [bm_file for bm_file in bm_files if os.path.isfile(bm_file)]
diff --git a/bibmanager/examples/sample.bib b/bibmanager/examples/sample.bib
index 1cc4ac6..e4f5975 100644
--- a/bibmanager/examples/sample.bib
+++ b/bibmanager/examples/sample.bib
@@ -247,7 +247,6 @@ @Misc{Oliphant2006numpy
 
 @article{vanderWaltEtal2011numpy,
    author = {St{\'{e}}fan van der Walt and S. Chris Colbert and Ga{\"{e}}l Varoquaux},
-    title = {The {NumPy} Array: A Structure for Efficient Numerical Computation},
   journal = {Computing in Science \& Engineering},
    volume = {13},
    number = {},
@@ -258,3 +257,17 @@ @article{vanderWaltEtal2011numpy
 publisher = {IEEE Computer Society},
   address = {Los Alamitos, CA, USA},
 }
+
+@Misc{JonesNoYearScipy,
+  author = {Eric Jones},
+  title  = {{SciPy}: Scientific Python},
+}
+
+@MISC{1978windEnergyReport,
+        title = "{Wind energy systems: Program summary}",
+ howpublished = {Unknown},
+         year = 1978,
+        month = dec,
+       adsurl = {https://ui.adsabs.harvard.edu/abs/1978wes..rept......},
+}
+
diff --git a/bibmanager/pdf_manager/pdf_manager.py b/bibmanager/pdf_manager/pdf_manager.py
index 287218c..d179d27 100644
--- a/bibmanager/pdf_manager/pdf_manager.py
+++ b/bibmanager/pdf_manager/pdf_manager.py
@@ -76,9 +76,13 @@ def guess_name(bib, arxiv=False):
     author = author.encode('ascii', errors='ignore').decode()
     author = re.sub('\W', '', author)
 
-    year = bib.year
+    year = '' if bib.year is None else bib.year
     guess_filename = f"{author}{year}.pdf"
 
+    if author == '' and year == '':
+        raise ValueError(
+            'Could not guess a good filename since entry does not '
+            'have author nor year fields')
     if bib.bibcode is not None:
         journal = re.sub('(\.|&)', '', bib.bibcode[4:9])
         if arxiv and journal.lower() != 'arxiv':
diff --git a/bibmanager/utils/utils.py b/bibmanager/utils/utils.py
index 2efb220..5f9b50e 100644
--- a/bibmanager/utils/utils.py
+++ b/bibmanager/utils/utils.py
@@ -31,6 +31,7 @@
     'nest',
     'cond_split',
     'cond_next',
+    'find_closing_bracket',
     'parse_name',
     'repr_author',
     'purify',
@@ -40,6 +41,7 @@
     'last_char',
     'get_fields',
     'req_input',
+    'warnings_format',
     # Classes:
     'AutoSuggestCompleter',
     'AutoSuggestKeyCompleter',
@@ -52,6 +54,7 @@
 import re
 from contextlib import contextmanager
 from collections import namedtuple
+import warnings
 
 import numpy as np
 from prompt_toolkit.auto_suggest import AutoSuggest, Suggestion
@@ -111,6 +114,7 @@ def BM_PDF():
 Author      = namedtuple("Author",      "last first von jr")
 Sort_author = namedtuple("Sort_author", "last first von jr year month")
 
+
 # Completer keywords:
 search_keywords = ['author:"^"', 'author:""', 'year:',
                    'title:""', 'key:', 'bibcode:']
@@ -367,7 +371,7 @@ def cond_split(text, pattern, nested=None, nlev=-1, ret_nests=False):
 
   # First and last indices of each pattern match:
   bounds = [(m.start(0), m.end(0))
-            for m in re.finditer(pattern, text)
+            for m in re.finditer(pattern, text, re.IGNORECASE)
             if nested[m.start(0)] == nlev]
 
   flat_bounds = [item for sublist in bounds for item in sublist]
@@ -431,8 +435,62 @@ def cond_next(text, pattern, nested, nlev=1):
   return len(text) - 1
 
 
+def find_closing_bracket(text, start_pos=0, get_open=False):
+    """
+    Find the closing bracket that matches the nearest opening bracket in
+    text starting from start_pos.
+
+    Parameters
+    ----------
+    text: String
+        Text to search through.
+    start_pos: Integer
+        Starting position where to start looking for the brackets.
+    get_opening: Bool
+        If True, return a tuple with the position of both
+        opening and closing brackets.
+
+    Returns
+    -------
+    end_pos: Integer
+        The absolute position to the cursor position at closing bracket.
+        Returns None if there are no matching brackets.
+
+    Examples
+    --------
+    >>> import bibmanager.utils as u
+    >>> text = '@ARTICLE{key, author={last_name}, title={The Title}}'
+    >>> end_pos = u.find_closing_bracket(text)
+    >>> print(text[:end_pos+1])
+    @ARTICLE{key, author={last_name}, title={The Title}}
+
+    >>> start_pos = 14
+    >>> end_pos = find_closing_bracket(text, start_pos=start_pos)
+    >>> print(text[start_pos:end_pos+1])
+    author={last_name}
+    """
+    left_bracket = text[start_pos:].find('{')
+    if left_bracket < 0:
+        return None
+    start_pos += left_bracket + 1
+    end_pos = len(text)
+
+    stack = 1
+    for index,char in enumerate(text[start_pos:end_pos]):
+        if char == '{':
+            stack += 1
+        elif char == '}':
+            stack -= 1
+
+        if stack == 0:
+            if get_open:
+                return left_bracket, start_pos + index
+            return start_pos + index
+    return None
+
+
 #@functools.lru_cache(maxsize=1024, typed=False)
-def parse_name(name, nested=None):
+def parse_name(name, nested=None, key=None):
   r"""
   Parse first, last, von, and jr parts from a name, following these rules:
   http://mirror.easyname.at/ctan/info/bibtex/tamethebeast/ttb_en.pdf
@@ -441,14 +499,17 @@ def parse_name(name, nested=None):
   Parameters
   ----------
   name: String
-     A name following the BibTeX format.
+      A name following the BibTeX format.
   nested: 1D integer ndarray
-     Nested level of characters in name.
+      Nested level of characters in name.
+  key: Sting
+      The entry that contains this author name (to display in case of
+      a warning).
 
   Returns
   -------
   author: Author namedtuple
-     Four element tuple with the parsed name.
+      Four element tuple with the parsed name.
 
   Examples
   --------
@@ -476,7 +537,14 @@ def parse_name(name, nested=None):
   name = " ".join(cond_split(name, "~", nested=nested))
   fields, nests = cond_split(name, ",", nested=nested, ret_nests=True)
   if len(fields) > 3:
-      raise ValueError(f"Invalid BibTeX format for author '{name}'.")
+      warnings.formatwarning = warnings_format
+      warning_text = f"Too many commas in name '{name}'"
+      if key is not None:
+          warning_text += f" for entry '{key}'"
+      warnings.warn(warning_text)
+      # LaTeX seems to interpret extra content as middle names:
+      fields = [fields[0], ' '.join(fields[1:])]
+      nests = [nests[0], nest(fields[1])]
 
   # 'First von Last' format:
   if len(fields) == 1:
@@ -704,6 +772,9 @@ def get_authors(authors, format='long'):
   2 author(s): {AAS Journals Team} and {Hendrickson}, A.
   3 author(s): Jones, Eric; Oliphant, Travis; and Peterson, Pearu
   """
+  if authors is None:
+      return ''
+
   if format == "ushort":
       # Remove characters except letters, spaces, dashes, and parentheses:
       last = re.sub("[^\w\s\-\(\)]", "", authors[0].last)
@@ -913,6 +984,11 @@ def req_input(prompt, options):
   return answer
 
 
+def warnings_format(message, category, filename, lineno, file=None, line=None):
+    """Custom format for warnings."""
+    return f'Warning: {message}\n'
+
+
 class AutoSuggestCompleter(AutoSuggest):
     """Give suggestions based on the words in WordCompleter."""
     def get_suggestion(self, buffer, document):
@@ -1139,15 +1215,19 @@ def validate(self, document):
 
         if text in self.keys:
             bib = self.bibs[self.keys.index(text)]
-            self.toolbar_text = f"{bib.get_authors('ushort')}: {bib.title}"
         elif text in self.pdfs:
             bib = self.bibs[self.pdfs.index(text)]
-            self.toolbar_text = f"{bib.get_authors('ushort')}: {bib.title}"
         elif text in self.bibcodes:
             bib = self.bibs[self.bibcodes.index(text)]
-            self.toolbar_text = f"{bib.get_authors('ushort')}: {bib.title}"
         else:
+            bib = None
+
+        if bib is None:
             self.toolbar_text = self.default_toolbar_text
+        else:
+            year = '' if bib.year is None else bib.year
+            title = 'NO_TITLE' if bib.title is None else bib.title
+            self.toolbar_text = f"{bib.get_authors('ushort')}{year}: {title}"
         return True
 
     def bottom_toolbar(self):
diff --git a/setup.py b/setup.py
index 362379c..c81048c 100644
--- a/setup.py
+++ b/setup.py
@@ -75,7 +75,7 @@
                           'packaging>=17.1',
                           'prompt_toolkit>=3.0.5',
                           'pygments>=2.2.0',],
-      tests_require = ['requests-mock',],
+      tests_require = ['requests-mock', 'prompt_toolkit>=3.0.10'],
       license      = "MIT",
       description  = "A BibTeX manager for LaTeX projects",
       long_description = long_description,


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 78aae1a..7b6a69c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -13,6 +13,10 @@
 import bibmanager.utils as u
 
 
+# Number of entries in bibmanager/examples/sample.bib:
+nentries = 19
+
+
 @pytest.fixture
 def mock_input(monkeypatch, request):
     def mock_input(s):
@@ -55,6 +59,14 @@ def mock_call(some_list):
     monkeypatch.setattr('subprocess.call', mock_call)
 
 
+# FINDME: Does not test windows:
+@pytest.fixture
+def mock_open(monkeypatch):
+    def mock_open(*arg, **kwargs):
+        return
+    monkeypatch.setattr('subprocess.run', mock_open)
+
+
 @pytest.fixture
 def mock_home(monkeypatch):
     # Re-define bibmanager HOME:
@@ -88,7 +100,7 @@ def entries():
   title  = {{SciPy}: Open source scientific tools for {Python}},
 }'''
 
-    jones_no_title = '''@Misc{JonesEtal2001scipy,
+    jones_no_title = '''@Misc{JonesNoTitleEtal2001scipy,
   author = {Eric Jones and Travis Oliphant and Pearu Peterson},
   year   = {2001},
 }'''
@@ -159,7 +171,7 @@ def entries():
   adsnote = {Provided by the SAO/NASA Astrophysics Data System}
 }"""
 
-    hunter = """@Article{Hunter2007ieeeMatplotlib,
+    hunter = r"""@Article{Hunter2007ieeeMatplotlib,
   Author    = {{Hunter}, J. D.},
   Title     = {Matplotlib: A 2D graphics environment},
   Journal   = {Computing In Science \& Engineering},
diff --git a/tests/test_bib_manager.py b/tests/test_bib_manager.py
index 729209f..5d78933 100644
--- a/tests/test_bib_manager.py
+++ b/tests/test_bib_manager.py
@@ -11,6 +11,7 @@
 import bibmanager as bibm
 import bibmanager.utils as u
 import bibmanager.bib_manager as bm
+from conftest import nentries
 
 
 def test_Bib_minimal(entries):
@@ -91,29 +92,7 @@ def test_Bib_update_content(entries):
     assert bib1.freeze is True
 
 
-def test_Bib_year_raise(entries):
-    # No year:
-    with pytest.raises(ValueError, match="Bibtex entry 'JonesEtal2001scipy' is"
-                                         " missing author, title, or year."):
-        bib = bm.Bib(entries['jones_no_year'])
-
-
-def test_Bib_title_raise(entries):
-    # No title:
-    with pytest.raises(ValueError, match="Bibtex entry 'JonesEtal2001scipy' is"
-                                         " missing author, title, or year."):
-        bib = bm.Bib(entries['jones_no_title'])
-
-
-def test_Bib_author_raise(entries):
-    # No author:
-    with pytest.raises(ValueError, match="Bibtex entry 'JonesEtal2001scipy' is"
-                                         " missing author, title, or year."):
-        bib = bm.Bib(entries['jones_no_author'])
-
-
-def test_Bib_braces_raise(entries):
-    # Mismatched braces:
+def test_Bib_mismatched_braces_raise(entries):
     with pytest.raises(ValueError, match="Mismatched braces in entry."):
         bib = bm.Bib(entries['jones_braces'])
 
@@ -180,6 +159,7 @@ def test_Bib_published_non_ads():
 
 @pytest.mark.parametrize('month_in, month_out',
     [('', 13),
+     ('month  = {},', 13),
      ('month  = {Jan},', 1),
      ('month  = {1},', 1),
     ])
@@ -193,17 +173,104 @@ def test_Bib_month(month_in, month_out):
     assert bib.month == month_out
 
 
-@pytest.mark.parametrize('month',
-    ['15', 'Tuesday',])
-def test_Bib_month_invalid(month):
-    e = '''@Misc{JonesEtal2001scipy,
+def test_Bib_lower_than_no_author():
+    b1 = bm.Bib('''@MISC{1978windEnergyReport,
+        title = "{Wind energy systems: Program summary}",
+         year = 1978,
+    }''')
+    b2 = bm.Bib('''@Misc{ZJones2001Scipy,
+       author = {Eric ZJones},
+       title  = {SciPy},
+         year = 2001,
+    }''')
+    assert b1 > b2
+
+
+def test_Bib_lower_than_both_no_author():
+    b1 = bm.Bib('''@MISC{1978windEnergyReport,
+        title = "{Wind energy systems: Program summary}",
+         year = 1978,
+    }''')
+    b2 = bm.Bib('''@MISC{1979windEnergyReport,
+        title = "{Wind energy systems: Program summary}",
+         year = 1979,
+    }''')
+    assert b2 > b1
+
+
+def test_Bib_lower_than_no_year():
+    b1 = bm.Bib('''@Misc{JonesEtal2001scipy,
        author = {Eric Jones},
        title  = {SciPy},
        year   = {2001},
-       ''' + f'month = {month}' + ',}'
-    value = f'{month.lower()}'
-    with pytest.raises(ValueError, match=fr"Invalid month value \({value}\)"):
-        bib = bm.Bib(e)
+    }''')
+    b2 = bm.Bib('''@Misc{JonesEtalScipy_noyear,
+       author = {Eric Jones},
+       title  = {SciPy},
+    }''')
+    assert b1 < b2
+
+
+def test_Bib_equal_no_author():
+    b1 = bm.Bib('''@MISC{1978windEnergyReport,
+        title = "{Wind energy systems: Program summary}",
+         year = 1978,
+    }''')
+    b2 = bm.Bib('''@Misc{ZJones2001Scipy,
+       author = {Eric ZJones},
+       title  = {SciPy},
+         year = 2001,
+    }''')
+    assert b1 != b2
+
+
+def test_Bib_equal_both_no_author():
+    b1 = bm.Bib('''@MISC{1978windEnergyReport,
+        title = "{Wind energy systems: Program summary}",
+         year = 1978,
+    }''')
+    b2 = bm.Bib('''@MISC{1978NewWindEnergyReport,
+        title = "{New wind energy systems: Program summary}",
+         year = 1978,
+    }''')
+    assert b2 == b1
+
+
+def test_Bib_not_equal_both_no_author():
+    b1 = bm.Bib('''@MISC{1978windEnergyReport,
+        title = "{Wind energy systems: Program summary}",
+         year = 1978,
+    }''')
+    b2 = bm.Bib('''@MISC{1979NewWindEnergyReport,
+        title = "{New wind energy systems: Program summary}",
+         year = 1979,
+    }''')
+    assert b2 != b1
+
+
+def test_Bib_not_equal_no_year():
+    b1 = bm.Bib('''@Misc{JonesEtal2001scipy,
+       author = {Eric Jones},
+       title  = {SciPy},
+       year   = {2001},
+    }''')
+    b2 = bm.Bib('''@Misc{JonesEtalScipy_noyear,
+       author = {Eric Jones},
+       title  = {SciPy},
+    }''')
+    assert b1 != b2
+
+
+def test_Bib_equal_no_year():
+    b1 = bm.Bib('''@Misc{JonesEtal2001scipy,
+       author = {Eric Jones},
+       title  = {SciPy},
+    }''')
+    b2 = bm.Bib('''@Misc{JonesEtalScipy_noyear,
+       author = {Eric Jones},
+       title  = {SciPy},
+    }''')
+    assert b1 == b2
 
 
 def test_Bib_meta():
@@ -218,6 +285,64 @@ def test_Bib_meta():
     assert bib.meta() == 'freeze\npdf: file.pdf\n'
 
 
+def test_Bib_warning_year():
+    e = '''@Misc{JonesEtal2001scipy,
+       author = {Eric Jones},
+       title  = {SciPy},
+       year   = {200X},
+    }'''
+    with pytest.warns(Warning) as record:
+        bib = bm.Bib(e)
+        assert str(record[0].message) == \
+            "Bad year format value '200X' for entry 'JonesEtal2001scipy'"
+
+
+@pytest.mark.parametrize('month',
+    ['15', 'tuesday',])
+def test_Bib_warning_month(month):
+    e = '''@Misc{JonesEtal2001scipy,
+       author = {Eric Jones},
+       title  = {SciPy},
+       year   = {2001},
+       ''' + f'month = {month}' + ',}'
+    with pytest.warns(Warning) as record:
+        bib = bm.Bib(e)
+        assert str(record[0].message) == \
+            f"Invalid month value '{month}' for entry 'JonesEtal2001scipy'"
+
+
+def test_Bib_warning_authors_comma_typo():
+    e = '''@article{Joint2017ALMAGuide,
+    title = {{ALMA Proposer's Guide}},
+    year = {2017},
+    author = {{Andreani}, P and {Trigo}, M, D, and {Remijan}, A},
+    }'''
+    with pytest.warns(Warning) as record:
+        bib = bm.Bib(e)
+        assert str(record[0].message) == (
+            "Too many commas in name '{Trigo}, M, D,' "
+            "for entry 'Joint2017ALMAGuide'")
+        assert len(bib.authors) == 3
+        # Corrected name:
+        assert bib.authors[1].first == 'M D'
+
+
+def test_Bib_warning_authors_missing_and():
+    e = '''@article{Joint2017ALMAGuide,
+    title = {{ALMA Proposer's Guide}},
+    year = {2017},
+    author = {{Andreani}, P and {Trigo}, M, {Remijan}, A},
+    }'''
+    with pytest.warns(Warning) as record:
+        bib = bm.Bib(e)
+        assert str(record[0].message) == (
+            "Too many commas in name '{Trigo}, M, {Remijan}, A' "
+            "for entry 'Joint2017ALMAGuide'")
+        assert len(bib.authors) == 2
+        # 'Corrected' name (seems to be how bibtex interprets the entry):
+        assert bib.authors[1].first == 'M {Remijan} A'
+
+
 def test_display_bibs(capfd, mock_init):
     e1 = '''@Misc{JonesEtal2001scipy,
        author = {Eric Jones},
@@ -232,10 +357,10 @@ def test_display_bibs(capfd, mock_init):
     bibs = [bm.Bib(e1), bm.Bib(e2)]
     bm.display_bibs(["DATABASE:\n", "NEW:\n"], bibs)
     captured = capfd.readouterr()
-    assert captured.out == '\x1b[0m\x1b[?7h\x1b[0;38;5;248;3m\r\n::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\r\n\x1b[0mDATABASE:\r\n\x1b[0;38;5;34;1;4m@Misc\x1b[0m{\x1b[0;38;5;142mJonesEtal2001scipy\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mauthor\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mEric Jones\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mtitle\x1b[0m  \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mSciPy\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33myear\x1b[0m   \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130m2001\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n    \x1b[0m}\x1b[0m\r\n\x1b[0m\r\n\x1b[0mNEW:\r\n\x1b[0;38;5;34;1;4m@Misc\x1b[0m{\x1b[0;38;5;142mJones2001\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mauthor\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mTravis Oliphant\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mtitle\x1b[0m  \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mtools for Python\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33myear\x1b[0m   \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130m2001\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n    \x1b[0m}\x1b[0m\r\n\x1b[0m\r\n\x1b[0m\x1b[0m'
+    assert captured.out == '\x1b[0m\x1b[?7h\x1b[0;38;5;248;3m\r\n::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\r\n\x1b[0mDATABASE:\r\n\x1b[0;38;5;34;1;4m@Misc\x1b[0m{\x1b[0;38;5;142mJonesEtal2001scipy\x1b[0m,\r\n       \x1b[0;38;5;33mauthor\x1b[0m = \x1b[0;38;5;130m{Eric Jones}\x1b[0m,\r\n       \x1b[0;38;5;33mtitle\x1b[0m  = \x1b[0;38;5;130m{SciPy}\x1b[0m,\r\n       \x1b[0;38;5;33myear\x1b[0m   = \x1b[0;38;5;130m{2001}\x1b[0m,\r\n    }\r\n\r\nNEW:\r\n\x1b[0;38;5;34;1;4m@Misc\x1b[0m{\x1b[0;38;5;142mJones2001\x1b[0m,\r\n       \x1b[0;38;5;33mauthor\x1b[0m = \x1b[0;38;5;130m{Travis Oliphant}\x1b[0m,\r\n       \x1b[0;38;5;33mtitle\x1b[0m  = \x1b[0;38;5;130m{tools for Python}\x1b[0m,\r\n       \x1b[0;38;5;33myear\x1b[0m   = \x1b[0;38;5;130m{2001}\x1b[0m,\r\n    }\r\n\r\n\x1b[0m'
 
 
-def test_display_bibs_with_meta(capfd, mock_init):
+def test_display_bibs_meta_not_shown(capfd, mock_init):
     e1 = '''@Misc{JonesEtal2001scipy,
        author = {Eric Jones},
        title  = {SciPy},
@@ -247,13 +372,26 @@ def test_display_bibs_with_meta(capfd, mock_init):
        year   = {2001},
     }'''
     bibs = [bm.Bib(e1), bm.Bib(e2, freeze=True, pdf='file.pdf')]
-    bm.display_bibs(["DATABASE:\n", "NEW:\n"], bibs)
+    bm.display_bibs(["DATABASE:\n", "NEW:\n"], bibs, meta=False)
     captured = capfd.readouterr()
-    assert captured.out == '\x1b[0m\x1b[?7h\x1b[0;38;5;248;3m\r\n::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\r\n\x1b[0mDATABASE:\r\n\x1b[0;38;5;34;1;4m@Misc\x1b[0m{\x1b[0;38;5;142mJonesEtal2001scipy\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mauthor\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mEric Jones\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mtitle\x1b[0m  \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mSciPy\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33myear\x1b[0m   \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130m2001\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n    \x1b[0m}\x1b[0m\r\n\x1b[0m\r\n\x1b[0mNEW:\r\n\x1b[0;38;5;34;1;4m@Misc\x1b[0m{\x1b[0;38;5;142mJones2001\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mauthor\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mTravis Oliphant\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mtitle\x1b[0m  \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mtools for Python\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33myear\x1b[0m   \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130m2001\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n    \x1b[0m}\x1b[0m\r\n\x1b[0m\r\n\x1b[0m\x1b[0m'
+    assert captured.out == '\x1b[0m\x1b[?7h\x1b[0;38;5;248;3m\r\n::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\r\n\x1b[0mDATABASE:\r\n\x1b[0;38;5;34;1;4m@Misc\x1b[0m{\x1b[0;38;5;142mJonesEtal2001scipy\x1b[0m,\r\n       \x1b[0;38;5;33mauthor\x1b[0m = \x1b[0;38;5;130m{Eric Jones}\x1b[0m,\r\n       \x1b[0;38;5;33mtitle\x1b[0m  = \x1b[0;38;5;130m{SciPy}\x1b[0m,\r\n       \x1b[0;38;5;33myear\x1b[0m   = \x1b[0;38;5;130m{2001}\x1b[0m,\r\n    }\r\n\r\nNEW:\r\n\x1b[0;38;5;34;1;4m@Misc\x1b[0m{\x1b[0;38;5;142mJones2001\x1b[0m,\r\n       \x1b[0;38;5;33mauthor\x1b[0m = \x1b[0;38;5;130m{Travis Oliphant}\x1b[0m,\r\n       \x1b[0;38;5;33mtitle\x1b[0m  = \x1b[0;38;5;130m{tools for Python}\x1b[0m,\r\n       \x1b[0;38;5;33myear\x1b[0m   = \x1b[0;38;5;130m{2001}\x1b[0m,\r\n    }\r\n\r\n\x1b[0m'
+
 
+def test_display_bibs_meta_shown(capfd, mock_init):
+    e1 = '''@Misc{JonesEtal2001scipy,
+       author = {Eric Jones},
+       title  = {SciPy},
+       year   = {2001},
+    }'''
+    e2 = '''@Misc{Jones2001,
+       author = {Travis Oliphant},
+       title  = {tools for Python},
+       year   = {2001},
+    }'''
+    bibs = [bm.Bib(e1), bm.Bib(e2, freeze=True, pdf='file.pdf')]
     bm.display_bibs(["DATABASE:\n", "NEW:\n"], bibs, meta=True)
     captured = capfd.readouterr()
-    assert captured.out == '\x1b[0m\x1b[?7h\x1b[0;38;5;248;3m\r\n::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\r\n\x1b[0mDATABASE:\r\n\x1b[0;38;5;248;3m\x1b[0;38;5;34;1;4m@Misc\x1b[0m{\x1b[0;38;5;142mJonesEtal2001scipy\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mauthor\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mEric Jones\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mtitle\x1b[0m  \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mSciPy\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33myear\x1b[0m   \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130m2001\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n    \x1b[0m}\x1b[0m\r\n\x1b[0m\r\n\x1b[0mNEW:\r\n\x1b[0;38;5;248;3mfreeze\r\npdf: file.pdf\r\n\x1b[0;38;5;34;1;4m@Misc\x1b[0m{\x1b[0;38;5;142mJones2001\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mauthor\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mTravis Oliphant\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mtitle\x1b[0m  \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mtools for Python\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33myear\x1b[0m   \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130m2001\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n    \x1b[0m}\x1b[0m\r\n\x1b[0m\r\n\x1b[0m\x1b[0m'
+    assert captured.out == '\x1b[0m\x1b[?7h\x1b[0;38;5;248;3m\r\n::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\r\n\x1b[0mDATABASE:\r\n\x1b[0;38;5;248;3m\x1b[0;38;5;34;1;4m@Misc\x1b[0m{\x1b[0;38;5;142mJonesEtal2001scipy\x1b[0m,\r\n       \x1b[0;38;5;33mauthor\x1b[0m = \x1b[0;38;5;130m{Eric Jones}\x1b[0m,\r\n       \x1b[0;38;5;33mtitle\x1b[0m  = \x1b[0;38;5;130m{SciPy}\x1b[0m,\r\n       \x1b[0;38;5;33myear\x1b[0m   = \x1b[0;38;5;130m{2001}\x1b[0m,\r\n    }\r\n\r\nNEW:\r\n\x1b[0;38;5;248;3mfreeze\r\npdf: file.pdf\r\n\x1b[0;38;5;34;1;4m@Misc\x1b[0m{\x1b[0;38;5;142mJones2001\x1b[0m,\r\n       \x1b[0;38;5;33mauthor\x1b[0m = \x1b[0;38;5;130m{Travis Oliphant}\x1b[0m,\r\n       \x1b[0;38;5;33mtitle\x1b[0m  = \x1b[0;38;5;130m{tools for Python}\x1b[0m,\r\n       \x1b[0;38;5;33myear\x1b[0m   = \x1b[0;38;5;130m{2001}\x1b[0m,\r\n    }\r\n\r\n\x1b[0m'
 
 
 def test_remove_duplicates_no_duplicates(bibs):
@@ -350,19 +488,19 @@ def test_filter_field_take_ask2(bibs, mock_input, mock_init):
     assert new == []
 
 
-def test_loadfile_bibfile(mock_init):
-    bibs = bm.loadfile(u.ROOT+'examples/sample.bib')
-    assert len(bibs) == 17
+def test_read_file_bibfile(mock_init):
+    bibs = bm.read_file(u.ROOT+'examples/sample.bib')
+    assert len(bibs) == nentries
 
 
-def test_loadfile_text(mock_init):
+def test_read_file_text(mock_init):
     with open(u.ROOT+'examples/sample.bib') as f:
        text = f.read()
-    bibs = bm.loadfile(text=text)
-    assert len(bibs) == 17
+    bibs = bm.read_file(text=text)
+    assert len(bibs) == nentries
 
 
-def test_loadfile_single_line_entry(mock_init):
+def test_read_file_single_line_entry(mock_init):
     text = """@Article{Adams1991ApJ, author = {{Adams}, F.~C.}, title = "{Asymptotic theory for the spatial distribution of protostellar emission}", journal = {\apj}, keywords = {ASYMPTOTIC METHODS, EMISSION SPECTRA, PROTOSTARS, SPATIAL DISTRIBUTION, STAR FORMATION, COMPUTATIONAL ASTROPHYSICS, DENSITY DISTRIBUTION, PRE-MAIN SEQUENCE STARS, STELLAR ENVELOPES, STELLAR LUMINOSITY, TEMPERATURE DISTRIBUTION}, year = 1991, month = dec, volume = 382, pages = {544-554}, doi = {10.1086/170741}, adsurl = {http://adsabs.harvard.edu/abs/1991ApJ...382..544A}, adsnote = {Provided by the SAO/NASA Astrophysics Data System} }
 
 @Misc{JonesOliphantPeterson2001scipy,
@@ -370,17 +508,41 @@ def test_loadfile_single_line_entry(mock_init):
   title  = {{SciPy}: Open source scientific tools for {Python}},
   year   = {2001},
 }"""
-    bibs = bm.loadfile(text=text)
+    bibs = bm.read_file(text=text)
     assert len(bibs) == 2
     assert bibs[0].key == 'Adams1991ApJ'
 
 
-def test_loadfile_meta():
+def test_read_file_ignore_comment(mock_init):
+    text = """
+@comment{Jones2000comment,
+  author = {Eric Jones},
+  title  = {{SciPy}: Open source scientific fools for {Python}},
+  year   = {2000},
+}
+
+@Misc{JonesEtal2001scipy,
+  author = {Eric Jones and Travis Oliphant and Pearu Peterson},
+  title  = {{SciPy}: Open source scientific tools for {Python}},
+  year   = {2001},
+}
+"""
+    bibs = bm.read_file(text=text)
+    assert len(bibs) == 1
+
+
+def test_read_file_ignore_comment_no_commas(mock_init):
+    text = """@Comment{jabref-meta: databaseType:biblatex;}"""
+    bibs = bm.read_file(text=text)
+    assert len(bibs) == 0
+
+
+def test_read_file_meta():
     with open(u.ROOT+'examples/sample.bib') as f:
        text = f.read()
     # prepend meta info before first entry:
     text = 'freeze\npdf: file.pdf\n'+ text
-    bibs = bm.loadfile(text=text)
+    bibs = bm.read_file(text=text)
 
     assert bibs[0].pdf == 'file.pdf'
     assert bibs[0].freeze is True
@@ -388,28 +550,44 @@ def test_loadfile_meta():
     assert bibs[1].freeze is None
 
 
-def test_loadfile_pdf_with_path(tmp_path, mock_init):
+def test_read_file_pdf_with_path(tmp_path, mock_init):
     pdf_path = str(tmp_path) + '/pathed_file.pdf'
     pathlib.Path(pdf_path).touch()
     with open(u.ROOT+'examples/sample.bib') as f:
        text = f.read()
     text = f'pdf: {pdf_path}\n{text}'
-    bibs = bm.loadfile(text=text)
+    bibs = bm.read_file(text=text)
     assert bibs[0].pdf == 'pathed_file.pdf'
     assert 'pathed_file.pdf' in os.listdir(u.BM_PDF())
     assert not os.path.isfile(pdf_path)
 
 
-def test_loadfile_pdf_with_bad_path(tmp_path, mock_init):
+def test_read_file_pdf_with_bad_path(tmp_path, mock_init):
     pdf_path = str(tmp_path) + '/pathed_file.pdf'
     # (no touch)
     with open(u.ROOT+'examples/sample.bib') as f:
        text = f.read()
     text = f'pdf: {pdf_path}\n{text}'
-    bibs = bm.loadfile(text=text)
+    bibs = bm.read_file(text=text)
     assert bibs[0].pdf is None
 
 
+def test_read_file_error_bad_format(mock_init):
+    text = '@this will fail}'
+    with pytest.raises(
+            ValueError,
+            match="Mismatched braces at/after line 0:\n@this will fail}"):
+        bibs = bm.read_file(text=text)
+
+
+def test_read_file_error_open_end(mock_init):
+    text = '@misc{key,\n author={name}'
+    with pytest.raises(
+            ValueError,
+            match="Mismatched braces at/after line 0:\n@misc{key,"):
+        bibs = bm.read_file(text=text)
+
+
 def test_save(bibs, mock_init):
     my_bibs = [bibs["beaulieu_apj"]]
     bm.save(my_bibs)
@@ -518,7 +696,7 @@ def test_export_home(bibs, mock_init):
     with open(u.BM_BIBFILE(), "r") as f:
         lines = f.readlines()
     assert lines[0] == "This file was created by bibmanager\n"
-    loaded_bibs = bm.loadfile(u.BM_BIBFILE())
+    loaded_bibs = bm.read_file(u.BM_BIBFILE())
     assert loaded_bibs == sorted(my_bibs)
 
 
@@ -553,14 +731,14 @@ def test_export_no_meta(mock_init_sample):
 def test_merge_bibfile(capfd, mock_init):
     bm.merge(u.HOME + "examples/sample.bib")
     captured = capfd.readouterr()
-    assert captured.out == "\nMerged 17 new entries.\n"
+    assert captured.out == f"\nMerged {nentries} new entries.\n"
 
 
 def test_merge_bibs(capfd, mock_init):
-    new = bm.loadfile(u.HOME + "examples/sample.bib")
+    new = bm.read_file(u.HOME + "examples/sample.bib")
     bm.merge(new=new)
     captured = capfd.readouterr()
-    assert captured.out == "\nMerged 17 new entries.\n"
+    assert captured.out == f"\nMerged {nentries} new entries.\n"
 
 
 def test_merge_no_new(capfd, bibs, mock_init_sample):
@@ -576,11 +754,26 @@ def test_merge_base(bibs):
     assert merged[1] == bibs['stodden']
 
 
+def test_merge_bibs_no_titles(capfd, mock_init):
+    e1 = """@Misc{Jones2001scipy,
+   author = {Eric Jones},
+   year   = {2001},
+ }"""
+    e2 = """@Misc{Oliphant2001scipy,
+   author = {Travis Oliphant},
+   year   = {2001},
+ }"""
+    bibs = [bm.Bib(e1), bm.Bib(e2)]
+    bm.merge(new=bibs)
+    captured = capfd.readouterr()
+    assert captured.out == "\nMerged 2 new entries.\n"
+
+
 @pytest.mark.parametrize('mock_input', [['n']], indirect=True)
 def test_merge_duplicate_key_ingnore(bibs, mock_init_sample, mock_input):
     bm.merge(new=[bibs['oliphant_dup']])
     loaded_bibs = bm.load()
-    assert len(loaded_bibs) == 17
+    assert len(loaded_bibs) == nentries
     assert bibs['oliphant_dup'] in loaded_bibs
 
 
@@ -588,7 +781,7 @@ def test_merge_duplicate_key_ingnore(bibs, mock_init_sample, mock_input):
 def test_merge_duplicate_key_rename(bibs, mock_init_sample, mock_input):
     bm.merge(new=[bibs['oliphant_dup']])
     loaded_bibs = bm.load()
-    assert len(loaded_bibs) == 18
+    assert len(loaded_bibs) == nentries + 1
     assert 'Oliphant2016numpyb' in [e.key for e in loaded_bibs]
 
 
@@ -596,7 +789,7 @@ def test_merge_duplicate_key_rename(bibs, mock_init_sample, mock_input):
 def test_merge_duplicate_title_ignore(bibs, mock_init_sample, mock_input):
     bm.merge(new=[bibs['no_oliphant']])
     loaded_bibs = bm.load()
-    assert len(loaded_bibs) == 17
+    assert len(loaded_bibs) == nentries
     assert bibs['no_oliphant'] not in loaded_bibs
 
 
@@ -604,7 +797,7 @@ def test_merge_duplicate_title_ignore(bibs, mock_init_sample, mock_input):
 def test_merge_duplicate_title_add(bibs, mock_init_sample, mock_input):
     bm.merge(new=[bibs['no_oliphant']])
     loaded_bibs = bm.load()
-    assert len(loaded_bibs) == 18
+    assert len(loaded_bibs) == nentries + 1
     assert bibs['no_oliphant'] in loaded_bibs
 
 
@@ -633,13 +826,6 @@ def test_add_entries_dry(capfd, mock_init, mock_prompt):
         "\nNo new entries to add.\n")
 
 
-@pytest.mark.parametrize('mock_prompt', [['this will fail}']], indirect=True)
-def test_add_entries_raise(capfd, mock_init, mock_prompt):
-    with pytest.raises(ValueError,
-                       match="Mismatched braces in line 0:\n'this will fail}'"):
-        bm.add_entries('new')
-
-
 # TBD: Can I pass a fixure to the decorator?
 @pytest.mark.parametrize('mock_prompt',
  [['''@Misc{JonesEtal2001scipy,
@@ -660,7 +846,7 @@ def test_edit():
     pass
 
 
-def test_search_author(mock_init_sample):
+def test_search_author_lastname(mock_init_sample):
     matches = bm.search(authors="oliphant")
     assert len(matches) == 2
     keys = [m.key for m in matches]
@@ -707,6 +893,16 @@ def test_search_title_multiple(mock_init_sample):
     assert 'ShowmanEtal2009apjRadGCM' in keys
 
 
+def test_search_title_entry_without_title(mock_init_sample, entries):
+    # Multiple-title querries act with AND logic:
+    bib = bm.Bib(entries['jones_no_title'])
+    bm.merge(new=[bib])
+    matches = bm.search(title=['HD 209458b', 'atmospheric circulation'])
+    assert len(matches) == 1
+    keys = [m.key for m in matches]
+    assert 'ShowmanEtal2009apjRadGCM' in keys
+
+
 def test_search_year_specific(mock_init_sample):
     matches = bm.search(authors="cubillos, p", year=2016)
     assert len(matches) == 1
@@ -801,7 +997,7 @@ def test_prompt_search_extra(mock_init_sample, mock_prompt_session):
 
 
 @pytest.mark.parametrize('mock_prompt_session',
-     [['']], indirect=True)
+    [['']], indirect=True)
 def test_prompt_search_empty_prompt(mock_init_sample, mock_prompt_session):
     keywords = ['key', 'bibcode']
     field = 'bibcode'
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 0cdfa6f..76cb933 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -13,6 +13,7 @@
 import bibmanager.bib_manager as bm
 import bibmanager.config_manager as cm
 import bibmanager.__main__ as cli
+from conftest import nentries
 
 
 # Main help text:
@@ -111,8 +112,8 @@ def test_cli_reset_keep_database(capsys, mock_init_sample):
         "examples",
         "pdf",
         ])
-    bibs = bm.loadfile(bibfile)
-    assert len(bibs) == 17
+    bibs = bm.read_file(bibfile)
+    assert len(bibs) == nentries
 
 
 def test_cli_reset_error(capsys, mock_init):
@@ -130,7 +131,7 @@ def test_cli_merge_default(capsys, mock_init):
     sys.argv = f"bibm merge {bibfile}".split()
     cli.main()
     captured = capsys.readouterr()
-    assert captured.out  == "\nMerged 17 new entries.\n\n" \
+    assert captured.out  == f"\nMerged {nentries} new entries.\n\n" \
                   f"Merged BibTeX file '{bibfile}' into bibmanager database.\n"
 
 
@@ -392,7 +393,7 @@ def test_cli_search_verbosity_three(capfd, mock_init_sample,
     sys.argv = "bibm search -vvv".split()
     cli.main()
     captured = capfd.readouterr()
-    assert captured.out == '''(Press \'tab\' for autocomplete)\n\n\x1b[0m\x1b[?7h\x1b[0;38;5;248;3m\r\n::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\r\n\x1b[0m\x1b[0;38;5;248;3m\x1b[0;38;5;34;1;4m@ARTICLE\x1b[0m{\x1b[0;38;5;142mBurbidgeEtal1957rvmpStellarElementSynthesis\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mauthor\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130m{\x1b[0;38;5;130mBurbidge\x1b[0;38;5;130m}\x1b[0;38;5;130m, E. Margaret and \x1b[0;38;5;130m{\x1b[0;38;5;130mBurbidge\x1b[0;38;5;130m}\x1b[0;38;5;130m, G.~R. and \x1b[0;38;5;130m{\x1b[0;38;5;130mFowler\x1b[0;38;5;130m}\x1b[0;38;5;130m, William A.\r\n        and \x1b[0;38;5;130m{\x1b[0;38;5;130mHoyle\x1b[0;38;5;130m}\x1b[0;38;5;130m, F.\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n        \x1b[0;38;5;33mtitle\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m"\x1b[0;38;5;130m{\x1b[0;38;5;130mSynthesis of the Elements in Stars\x1b[0;38;5;130m}\x1b[0;38;5;130m"\x1b[0m,\x1b[0m\r\n      \x1b[0;38;5;33mjournal\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mReviews of Modern Physics\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n         \x1b[0;38;5;33myear\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;30m1957\x1b[0m,\x1b[0m\r\n        \x1b[0;38;5;33mmonth\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;124mJan\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33mvolume\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130m29\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n        \x1b[0;38;5;33mpages\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130m547-650\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n          \x1b[0;38;5;33mdoi\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130m10.1103/RevModPhys.29.547\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n       \x1b[0;38;5;33madsurl\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mhttps://ui.adsabs.harvard.edu/abs/1957RvMP...29..547B\x1b[0;38;5;130m}\x1b[0m,\x1b[0m\r\n      \x1b[0;38;5;33madsnote\x1b[0m \x1b[0m=\x1b[0m \x1b[0;38;5;130m{\x1b[0;38;5;130mProvided by the SAO/NASA Astrophysics Data System\x1b[0;38;5;130m}\x1b[0m\r\n\x1b[0m}\x1b[0m\r\n\x1b[0m\r\n\x1b[0m\x1b[0m'''
+    assert captured.out == '(Press \'tab\' for autocomplete)\n\n\x1b[0m\x1b[?7h\x1b[0;38;5;248;3m\r\n::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\r\n\x1b[0m\x1b[0;38;5;248;3m\x1b[0;38;5;34;1;4m@ARTICLE\x1b[0m{\x1b[0;38;5;142mBurbidgeEtal1957rvmpStellarElementSynthesis\x1b[0m,\r\n       \x1b[0;38;5;33mauthor\x1b[0m = \x1b[0;38;5;130m{{Burbidge}, E. Margaret and {Burbidge}, G.~R. and {Fowler}, William A.\r\n        and {Hoyle}, F.}\x1b[0m,\r\n        \x1b[0;38;5;33mtitle\x1b[0m = \x1b[0;38;5;130m"{Synthesis of the Elements in Stars}"\x1b[0m,\r\n      \x1b[0;38;5;33mjournal\x1b[0m = \x1b[0;38;5;130m{Reviews of Modern Physics}\x1b[0m,\r\n         \x1b[0;38;5;33myear\x1b[0m = \x1b[0;38;5;30m1957\x1b[0m,\r\n        \x1b[0;38;5;33mmonth\x1b[0m = \x1b[0;38;5;124mJan\x1b[0m,\r\n       \x1b[0;38;5;33mvolume\x1b[0m = \x1b[0;38;5;130m{29}\x1b[0m,\r\n        \x1b[0;38;5;33mpages\x1b[0m = \x1b[0;38;5;130m{547-650}\x1b[0m,\r\n          \x1b[0;38;5;33mdoi\x1b[0m = \x1b[0;38;5;130m{10.1103/RevModPhys.29.547}\x1b[0m,\r\n       \x1b[0;38;5;33madsurl\x1b[0m = \x1b[0;38;5;130m{https://ui.adsabs.harvard.edu/abs/1957RvMP...29..547B}\x1b[0m,\r\n      \x1b[0;38;5;33madsnote\x1b[0m = \x1b[0;38;5;130m{Provided by the SAO/NASA Astrophysics Data System}\x1b[0m\r\n}\r\n\r\n\x1b[0m'
 
 
 @pytest.mark.skip(reason='How in the world can I test this?')
@@ -647,7 +648,8 @@ def test_cli_fetch_keycode_filename(capsys, mock_init_sample, reqs):
 bibm open BurbidgeEtal1957rvmpStellarElementSynthesis\n"""
 
 
-def test_cli_fetch_keycode_open(capsys, mock_init_sample, reqs, mock_call):
+def test_cli_fetch_keycode_open(capsys, mock_init_sample, reqs, mock_call,
+        mock_open):
     sys.argv = "bibm fetch 1957RvMP...29..547B -o".split()
     cli.main()
     captured = capsys.readouterr()
@@ -663,6 +665,7 @@ def test_cli_fetch_keycode_not_in_database(capsys, mock_init_sample, reqs):
 Fetching PDF file from Journal website:
 Saved PDF to: '1957RvMP...00..000B.pdf'.
 (Note that BibTex entry is not in the Bibmanager database)\n"""
+    os.remove('1957RvMP...00..000B.pdf')
 
 
 @pytest.mark.parametrize('mock_prompt_session',
@@ -780,7 +783,8 @@ def test_cli_fetch_invalid_name(capsys, mock_init_sample, reqs):
      'Slipher1913lobAndromedaRarialVelocity',
      '1913LowOB...2...56S',
      'Slipher1913.pdf'])
-def test_cli_open_keycode(capsys, mock_init_sample, mock_call, keycode):
+def test_cli_open_keycode(capsys, mock_init_sample, mock_call, mock_open,
+        keycode):
     pathlib.Path(f"{u.BM_PDF()}Slipher1913.pdf").touch()
     sys.argv = f"bibm open {keycode}".split()
     cli.main()
@@ -801,7 +805,7 @@ def test_cli_open_keycode_invalid(capsys, mock_init_sample, mock_call):
      [['key: Slipher1913lobAndromedaRarialVelocity'],
       ['bibcode: 1913LowOB...2...56S'],
       ['pdf: Slipher1913.pdf']], indirect=True)
-def test_cli_open_prompt(capsys, mock_init_sample, mock_call,
+def test_cli_open_prompt(capsys, mock_init_sample, mock_call, mock_open,
         mock_prompt_session):
     pathlib.Path(f"{u.BM_PDF()}Slipher1913.pdf").touch()
     sys.argv = f"bibm open".split()
@@ -958,6 +962,7 @@ def test_cli_pdf_set_missing_pdf(capsys, mock_init_sample):
     assert captured.out == "\nError: Path to PDF file is missing.\n"
     bib = bm.find(bibcode='1957RvMP...29..547B')
     assert bib.pdf is None
+    os.remove('file.pdf')
 
 
 def test_cli_pdf_set_missing_bib(capsys, mock_init_sample):
@@ -969,6 +974,7 @@ def test_cli_pdf_set_missing_bib(capsys, mock_init_sample):
         "\nError: BibTex entry is not in Bibmanager database.\n"
     bib = bm.find(bibcode='1957RvMP...29..547B')
     assert bib.pdf is None
+    os.remove('file.pdf')
 
 
 def test_cli_pdf_set_missing_pdf_file(capsys, mock_init_sample):
@@ -990,6 +996,7 @@ def test_cli_pdf_set_pathed_filename(capsys, mock_init_sample):
     assert captured.out == "\nError: filename must not have a path\n"
     bib = bm.find(bibcode='1957RvMP...29..547B')
     assert bib.pdf is None
+    os.remove('file.pdf')
 
 
 def test_cli_pdf_set_invalid_name(capsys, mock_init_sample):
@@ -1001,6 +1008,7 @@ def test_cli_pdf_set_invalid_name(capsys, mock_init_sample):
         "\nError: Invalid filename, must have a .pdf extension\n"
     bib = bm.find(bibcode='1957RvMP...29..547B')
     assert bib.pdf is None
+    os.remove('file.pdf')
 
 
 @pytest.mark.parametrize('mock_prompt_session',
@@ -1018,6 +1026,7 @@ def test_cli_pdf_set_prompt_missing_pdf(capsys, mock_init_sample,
         "\n\nError: Path to PDF file is missing.\n")
     bib = bm.find(bibcode='1957RvMP...29..547B')
     assert bib.pdf is None
+    os.remove('file.pdf')
 
 
 @pytest.mark.parametrize('mock_prompt_session',
diff --git a/tests/test_latex_manager.py b/tests/test_latex_manager.py
index 7fc88cf..8817dde 100644
--- a/tests/test_latex_manager.py
+++ b/tests/test_latex_manager.py
@@ -156,7 +156,7 @@ def test_build_bib_inplace(mock_init):
     assert "texsample.bib" in files
     # Now check content:
     np.testing.assert_array_equal(missing, np.zeros(0,dtype="U"))
-    bibs = bm.loadfile("texsample.bib")
+    bibs = bm.read_file("texsample.bib")
     assert len(bibs) == 7
     keys = [bib.key for bib in bibs]
     assert "AASteamHendrickson2018aastex62" in keys
@@ -196,7 +196,7 @@ def test_build_bib_missing(capsys, tmp_path, mock_init):
     assert captured.out == "References not found:\nMissingEtal2019\n"
     # Check content:
     np.testing.assert_array_equal(missing, np.array(["MissingEtal2019"]))
-    bibs = bm.loadfile(bibfile)
+    bibs = bm.read_file(bibfile)
     assert len(bibs) == 1
     assert "Astropycollab2013aaAstropy" in bibs[0].key
 
diff --git a/tests/test_pdf_manager.py b/tests/test_pdf_manager.py
index 6516518..afb5893 100644
--- a/tests/test_pdf_manager.py
+++ b/tests/test_pdf_manager.py
@@ -104,6 +104,16 @@ def test_guess_name_bibcode_no_pages():
     assert pm.guess_name(bib) == 'Chase1986_jtt_book.pdf'
 
 
+def test_guess_name_no_author_no_year():
+    bib = bm.Bib('''@ARTICLE{Raftery1995BIC,
+        title = "{Bayesian Model Selection in Social Research}",
+    }''')
+    with pytest.raises(ValueError,
+            match='Could not guess a good filename since entry does not '
+                  'have author nor year fields'):
+        pm.guess_name(bib)
+
+
 @pytest.mark.skip(reason='Is this even testable?')
 def test_open():
     pass
diff --git a/tests/test_utils.py b/tests/test_utils.py
index dbe0e6d..085901a 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -44,10 +44,12 @@ def test_cond_split1():
     assert u.cond_split("{a,b,c,d}", ",") == ["{a,b,c,d}"]
     assert u.cond_split("a,{b,c},d", ",") == ['a', '{b,c}', 'd']
 
+
 def test_cond_split2():
     assert u.cond_split("a and b and c", "and")   == ['a ', ' b ', ' c']
     assert u.cond_split("a and b and c", " and ") == ['a', 'b', 'c']
 
+
 def test_cond_split3():
     assert u.cond_split("a,b,c", ",", nested=[0,0,0,0,0]) == ['a', 'b', 'c']
     assert u.cond_split("a,b,c", ",", nested=[1,1,1,1,1]) == ['a', 'b', 'c']
@@ -58,6 +60,17 @@ def test_cond_split3():
            == ["{P\\'erez}, F.", '{Granger}, B.~E.']
 
 
+@pytest.mark.parametrize('and_texts', (
+    ['and', 'and'],
+    ['And', 'and'],
+    ['AND', 'and'],
+    ['and', 'AND']))
+def test_cond_split_ignore_case(and_texts):
+    assert u.cond_split(
+        f"a {and_texts[0]} b",
+        f" {and_texts[1]} ") == ['a', 'b']
+
+
 def test_cond_next1():
     # Empty string:
     text = ""
@@ -93,6 +106,38 @@ def test_cond_next3():
     assert u.cond_next(text, " and ", nested, nested[0]) == 9
 
 
+def test_find_closing_bracket1():
+    text = '@ARTICLE{key, author={last_name}, title={The Title}}'
+    end_pos = u.find_closing_bracket(text)
+    assert end_pos == 51
+
+
+def test_find_closing_bracket2():
+    text = '@ARTICLE{key, author={last_name}, title={The Title}}'
+    start_pos = 14
+    end_pos = u.find_closing_bracket(text, start_pos=start_pos)
+    assert end_pos == 31
+
+
+def test_find_closing_bracket_tuple():
+    text = '@ARTICLE{key, author={last_name}, title={The Title}}'
+    pos = u.find_closing_bracket(text, get_open=True)
+    assert pos[0] == 8
+    assert pos[1] == 51
+
+
+def test_find_closing_bracket_no_left_bracket():
+    text = 'key, author=last_name}'
+    end_pos = u.find_closing_bracket(text)
+    assert end_pos is None
+
+
+def test_find_closing_bracket_no_right_bracket():
+    text = '@ARTICLE{key, author=last_name'
+    end_pos = u.find_closing_bracket(text)
+    assert end_pos is None
+
+
 def test_parse_name1():
     # 'First Last' format:
     author = u.parse_name('Jones')
@@ -337,7 +382,6 @@ def test_get_authors_single(author_format):
 
 @pytest.mark.parametrize('author_format', ('short','long'))
 def test_get_authors_two(author_format):
-    # Short format:
     assert u.get_authors(author_lists[1], author_format) == \
            '{AAS Journals Team} and {Hendrickson}, A.'
 
@@ -379,6 +423,11 @@ def test_get_authors_ushort_non_ascii():
     assert u.get_authors(authors, 'ushort') == 'Huang (黄新川)'
 
 
+@pytest.mark.parametrize('author_format', ('short','long', 'ushort'))
+def test_get_authors_none(author_format):
+    assert u.get_authors(None, author_format) == ''
+
+
 def test_next_char():
     assert u.next_char('Hello')    == 0
     assert u.next_char('  Hello')  == 2