From 12b42200541801e7280971a688801ea652cc4ee4 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Fri, 28 Nov 2025 22:55:07 +0000 Subject: [PATCH] Read additional args from file with with `@`. --- README.rst | 7 ++++++ codespell_lib/_codespell.py | 23 +++++++++++++++++++- codespell_lib/tests/test_basic.py | 36 +++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index fdd16cc5e5..a9562e8f2b 100644 --- a/README.rst +++ b/README.rst @@ -229,6 +229,13 @@ instead of these invalid entries: .. _tomli: https://pypi.org/project/tomli/ +Reading arguments from file +--------------------------- + +Additional arguments can be read from a file with ``@PATH``. Arguments are +extracted using ``shlex.split()``. + + pre-commit hook --------------- diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py index 94ab65d068..abb72b0e3b 100644 --- a/codespell_lib/_codespell.py +++ b/codespell_lib/_codespell.py @@ -23,6 +23,7 @@ import itertools import os import re +import shlex import sys import textwrap from collections.abc import Iterable, Sequence @@ -388,7 +389,27 @@ def _supports_ansi_colors() -> bool: def parse_options( args: Sequence[str], ) -> tuple[argparse.Namespace, argparse.ArgumentParser, list[str]]: - parser = argparse.ArgumentParser(formatter_class=NewlineHelpFormatter) + # Split lines read from `@PATH` using shlex.split(), otherwise default + # behaviour is to have one arg per line. See: + # https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.convert_arg_line_to_args + class ArgumentParser2(argparse.ArgumentParser): + def convert_arg_line_to_args(self, arg_line: str) -> list[str]: + if sys.platform == "win32": + # On Windows, shlex.split() seems to be messed up by back + # slashes. Temporarily changing them to forward slashes seems + # to make things work better. + arg_line = arg_line.replace("\\", "/") + ret = shlex.split(arg_line) + ret = [p.replace("/", "\\") for p in ret] + else: + ret = shlex.split(arg_line) + return ret + + parser = ArgumentParser2( + formatter_class=NewlineHelpFormatter, + fromfile_prefix_chars="@", + epilog="Use @PATH to read additional arguments from file PATH.", + ) parser.set_defaults(colors=_supports_ansi_colors()) parser.add_argument("--version", action="version", version=VERSION) diff --git a/codespell_lib/tests/test_basic.py b/codespell_lib/tests/test_basic.py index d8f97ea3f1..b0e6da169d 100644 --- a/codespell_lib/tests/test_basic.py +++ b/codespell_lib/tests/test_basic.py @@ -1458,3 +1458,39 @@ def test_stdin(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: code, stdout, _ = result assert stdout == "1: Thsi ==> This\n" assert code == 1 + + +def test_args_from_file( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + import textwrap + + print() + fname1 = tmp_path / "tmp1" + fname2 = tmp_path / "tmp2" + fname3 = tmp_path / "tmp3" + fname_list = tmp_path / "tmp_list" + fname_list.write_text(f"{fname1} {fname2}\n{fname3}") + fname1.write_text("abandonned\ncode") + fname2.write_text("exmaple\n") + fname3.write_text("abilty\n") + print(f"{fname_list=}") + args = ["codespell", f"@{fname_list}"] + print(f"Running: {args=}") + cp = subprocess.run( # noqa: S603 + args, + check=False, + text=True, + capture_output=True, + ) + code = cp.returncode + stdout = cp.stdout + stderr = cp.stderr + print(f"{code=}") + print(f"stdout:\n{textwrap.indent(stdout, ' ')}") + print(f"stderr:\n{textwrap.indent(stderr, ' ')}") + assert "tmp1:1: abandonned ==> abandoned\n" in stdout, f"{stdout=}" + assert "tmp2:1: exmaple ==> example\n" in stdout, f"{stdout=}" + assert "tmp3:1: abilty ==> ability\n" in stdout, f"{stdout=}" + assert code, f"{code=}"