From b3a573a1f0382153c72c7f2bfbb44f71e83e164b Mon Sep 17 00:00:00 2001 From: "John C. McCabe-Dansted" Date: Mon, 18 May 2026 19:05:39 +1200 Subject: [PATCH] Warn user `borg list` warns needs name, unless they meant repo-list --- src/borg/archiver/__init__.py | 44 ++++++++++++++++++-- src/borg/testsuite/archiver/help_cmd_test.py | 27 ++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index e697640e7..95400dffc 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -309,7 +309,7 @@ class Archiver( return parser @staticmethod - def _first_toplevel_command_index(args, parser): + def _first_positional_index(args, parser): option_actions = {} for action in parser._actions: for option_string in getattr(action, "option_strings", ()): @@ -319,7 +319,7 @@ class Archiver( while i < len(args): token = args[i] if token == "--": - return None + return i + 1 if i + 1 < len(args) else len(args) if not token.startswith("-") or token == "-": return i @@ -344,7 +344,14 @@ class Archiver( else: i += 2 - return None + return len(args) + + @staticmethod + def _first_toplevel_command_index(args, parser): + index = Archiver._first_positional_index(args, parser) + if index is None or index == len(args): + return None + return index def _legacy_command_hint(self, args, parser): command_index = self._first_toplevel_command_index(args, parser) @@ -422,6 +429,34 @@ class Archiver( ] ) + def _missing_list_name_hint(self, args, parser): + command_index = self._first_toplevel_command_index(args, parser) + if command_index is None or args[command_index] != "list": + return None + + commands = getattr(parser, "_subcommands_action", None) + commands = commands._name_parser_map if commands else {} + list_parser = commands.get("list") + if list_parser is None: + return None + + subcommand_args = args[command_index + 1 :] + positional_index = self._first_positional_index(subcommand_args, list_parser) + if positional_index is None or positional_index != len(subcommand_args): + return None + + prog = self.prog or "borg" + repo_value = self._option_value(args, ("-r", "--repo")) or "REPO" + repo_list_command = shlex.join([prog, "-r", repo_value, "repo-list"]) + return "\n".join( + [ + "borg list NAME lists contents of an archive and needs an archive NAME.", + "If you meant to list archives in a repository, use repo-list:", + repo_list_command, + f"tip: For details of accepted options run: {prog} list --help", + ] + ) + def get_args(self, argv, cmd): """Usually just returns argv, except when dealing with an SSH forced command for borg serve.""" result = self.parse_args(argv[1:]) @@ -469,6 +504,9 @@ class Archiver( legacy_hint = self._legacy_repo_archive_hint(args, parser) if legacy_hint: parser.exit(EXIT_ERROR, legacy_hint + "\n") + legacy_hint = self._missing_list_name_hint(args, parser) + if legacy_hint: + parser.exit(EXIT_ERROR, legacy_hint + "\n") args = parser.parse_args(args or ["-h"]) args = flatten_namespace(args) diff --git a/src/borg/testsuite/archiver/help_cmd_test.py b/src/borg/testsuite/archiver/help_cmd_test.py index a7fa3d0eb..bec69d732 100644 --- a/src/borg/testsuite/archiver/help_cmd_test.py +++ b/src/borg/testsuite/archiver/help_cmd_test.py @@ -121,6 +121,33 @@ def test_borg1_repo_archive_in_repo_shows_borg2_forms_when_repo_is_after_command assert "borg list ::test1" in output +def test_list_without_name_suggests_repo_list(archiver): + ret, output = exec_cmd("list", archiver=archiver.archiver, fork=archiver.FORK_DEFAULT, exe=archiver.EXE) + + assert ret == 2 + assert "borg list NAME lists contents of an archive and needs an archive NAME." in output + assert "If you meant to list archives in a repository, use repo-list:" in output + assert "borg -r REPO repo-list" in output + assert "tip: For details of accepted options run: borg list --help" in output + + +def test_list_without_name_with_repo_suggests_repo_list(archiver): + ret, output = exec_cmd( + "--repo", + archiver.repository_location, + "list", + archiver=archiver.archiver, + fork=archiver.FORK_DEFAULT, + exe=archiver.EXE, + ) + + assert ret == 2 + assert "borg list NAME lists contents of an archive and needs an archive NAME." in output + assert "If you meant to list archives in a repository, use repo-list:" in output + assert f"borg -r {archiver.repository_location} repo-list" in output + assert "tip: For details of accepted options run: borg list --help" in output + + @pytest.mark.parametrize("command, parser", list(get_all_parsers().items())) def test_help_formatting(command, parser): if isinstance(parser.epilog, RstToTextLazy):