Repository: codespell-project/codespell

Issue:
codespell exits brutally on ill-formed config file
```
$ cat CONFIG 
check-hidden =
$ 
$ codespell --config CONFIG
Traceback (most recent call last):
  File "/home/username/.local/bin/codespell", line 8, in <module>
    sys.exit(_script_main())
             ^^^^^^^^^^^^^^
  File "/path/to/codespell/codespell_lib/_codespell.py", line 1019, in _script_main
    return main(*sys.argv[1:])
           ^^^^^^^^^^^^^^^^^^^
  File "/path/to/codespell/codespell_lib/_codespell.py", line 1024, in main
    options, parser, used_cfg_files = parse_options(args)
                                      ^^^^^^^^^^^^^^^^^^^
  File "/path/to/codespell/codespell_lib/_codespell.py", line 584, in parse_options
    _cfg.read(cfg_file)
  File "/usr/lib/python3.11/configparser.py", line 713, in read
    self._read(fp, filename)
  File "/usr/lib/python3.11/configparser.py", line 1101, in _read
    raise MissingSectionHeaderError(fpname, lineno, line)
configparser.MissingSectionHeaderError: File contains no section headers.
file: 'CONFIG', line: 1
'check-hidden =\n'
$ 
```


Discussion:

Resolving patch:
diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py
--- a/codespell_lib/_codespell.py
+++ b/codespell_lib/_codespell.py
@@ -118,6 +118,7 @@
 EX_OK = 0
 EX_USAGE = 64
 EX_DATAERR = 65
+EX_CONFIG = 78
 
 # OPTIONS:
 #
@@ -586,7 +587,7 @@ def parse_options(
             used_cfg_files.append(cfg_file)
 
     # Use config files
-    config.read(cfg_files)
+    config.read(used_cfg_files)
     if config.has_section("codespell"):
         # Build a "fake" argv list using option name and value.
         cfg_args = []
@@ -1021,7 +1022,14 @@ def _script_main() -> int:
 
 def main(*args: str) -> int:
     """Contains flow control"""
-    options, parser, used_cfg_files = parse_options(args)
+    try:
+        options, parser, used_cfg_files = parse_options(args)
+    except configparser.Error as e:
+        print(
+            f"ERROR: ill-formed config file: {e.message}",
+            file=sys.stderr,
+        )
+        return EX_CONFIG
 
     # Report used config files
     if not options.quiet_level & QuietLevels.CONFIG_FILES:


Test patch:
diff --git a/codespell_lib/tests/test_basic.py b/codespell_lib/tests/test_basic.py
--- a/codespell_lib/tests/test_basic.py
+++ b/codespell_lib/tests/test_basic.py
@@ -13,7 +13,13 @@
 import pytest
 
 import codespell_lib as cs_
-from codespell_lib._codespell import EX_DATAERR, EX_OK, EX_USAGE, uri_regex_def
+from codespell_lib._codespell import (
+    EX_CONFIG,
+    EX_DATAERR,
+    EX_OK,
+    EX_USAGE,
+    uri_regex_def,
+)
 
 
 def test_constants() -> None:
@@ -21,6 +27,7 @@ def test_constants() -> None:
     assert EX_OK == 0
     assert EX_USAGE == 64
     assert EX_DATAERR == 65
+    assert EX_CONFIG == 78
 
 
 class MainWrapper:
@@ -42,7 +49,7 @@ def main(
         assert frame is not None
         capsys = frame.f_locals["capsys"]
         stdout, stderr = capsys.readouterr()
-        assert code in (EX_OK, EX_USAGE, EX_DATAERR)
+        assert code in (EX_OK, EX_USAGE, EX_DATAERR, EX_CONFIG)
         if code == EX_DATAERR:  # have some misspellings
             code = int(stderr.split("\n")[-2])
         elif code == EX_OK and count:
@@ -1028,7 +1035,7 @@ def test_uri_regex_def() -> None:
         assert not uri_regex.findall(boilerplate % uri), uri
 
 
-def test_quiet_option_32(
+def test_quiet_level_32(
     tmp_path: Path,
     tmpdir: pytest.TempPathFactory,
     capsys: pytest.CaptureFixture[str],
@@ -1057,6 +1064,27 @@ def test_quiet_option_32(
     assert "setup.cfg" in stdout
 
 
+def test_ill_formed_ini_config_file(
+    tmp_path: Path,
+    tmpdir: pytest.TempPathFactory,
+    capsys: pytest.CaptureFixture[str],
+) -> None:
+    d = tmp_path / "files"
+    d.mkdir()
+    conf = str(tmp_path / "setup.cfg")
+    with open(conf, "w") as f:
+        # It should contain but lacks a section.
+        f.write("foobar =\n")
+    args = ("--config", conf)
+
+    # Should not raise a configparser.Error exception.
+    result = cs.main(str(d), *args, std=True)
+    assert isinstance(result, tuple)
+    code, _, stderr = result
+    assert code == 78
+    assert "ill-formed config file" in stderr
+
+
 @pytest.mark.parametrize("kind", ("toml", "cfg"))
 def test_config_toml(
     tmp_path: Path,