Repository: borgbackup/borg

Issue:
Invalid pattern traceback results in a warning return code
## Have you checked borgbackup docs, FAQ, and open GitHub issues?

Yes

## Is this a BUG / ISSUE report or a QUESTION?

BUG / ISSUE

## System information. For client/server mode post info for both machines.

#### Your borg version (borg -V).

1.2.6

#### Operating system (distribution) and version.

Manjaro Linux 23.0.4

#### Hardware / network configuration, and filesystems used.

Lenovo ThinkPad X1

#### How much data is handled by borg?

< 50 MB

#### Full borg commandline that lead to the problem (leave away excludes and passwords)

```bash
borg create --pattern invalid 1.2.borg
```

## Describe the problem you're observing.

When giving Borg an invalid pattern, I receive a traceback with a helpful error message:

```
Traceback (most recent call last):
...
  File "/usr/lib/python3.11/site-packages/borg/patterns.py", line 17, in parse_patternfile_line
    ie_command = parse_inclexcl_command(line, fallback=fallback)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/borg/patterns.py", line 384, in parse_inclexcl_command
    raise argparse.ArgumentTypeError("A pattern/command must start with anyone of: %s" %
argparse.ArgumentTypeError: A pattern/command must start with anyone of: -, !, +, R, r, P, p
```

This is expected (but ugly).

What's not expected is that Borg exits with a return code of 1 (as indicated by `echo $?`), indicating a warning. But given that `borg create` aborted with a traceback, I would expect an error return code instead.

This is relevant because wrapper scripts like borgmatic [consume Borg's return code](https://projects.torsion.org/borgmatic-collective/borgmatic/issues/776).

#### Can you reproduce the problem? If so, describe how. If not, describe troubleshooting steps you took before opening the issue.

Yes, it reproduces simply by running the above command again and inspecting the return code.

#### Include any warning/errors/backtraces from the system logs

```bash
$ borg create --pattern invalid 1.2.borg
Traceback (most recent call last):
  File "/usr/bin/borg", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/usr/lib/python3.11/site-packages/borg/archiver.py", line 5312, in main
    args = archiver.get_args(sys.argv, os.environ.get('SSH_ORIGINAL_COMMAND'))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/borg/archiver.py", line 5104, in get_args
    result = self.parse_args(argv[1:])
             ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/borg/archiver.py", line 5150, in parse_args
    args = parser.parse_args(args or ['-h'])
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/argparse.py", line 1869, in parse_args
    args, argv = self.parse_known_args(args, namespace)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/argparse.py", line 1902, in parse_known_args
    namespace, args = self._parse_known_args(args, namespace)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/argparse.py", line 2096, in _parse_known_args
    positionals_end_index = consume_positionals(start_index)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/argparse.py", line 2073, in consume_positionals
    take_action(action, args)
  File "/usr/lib/python3.11/argparse.py", line 1978, in take_action
    action(self, namespace, argument_values, option_string)
  File "/usr/lib/python3.11/argparse.py", line 1241, in __call__
    subnamespace, arg_strings = parser.parse_known_args(arg_strings, None)
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/argparse.py", line 1902, in parse_known_args
    namespace, args = self._parse_known_args(args, namespace)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/argparse.py", line 2114, in _parse_known_args
    start_index = consume_optional(start_index)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/argparse.py", line 2054, in consume_optional
    take_action(action, args, option_string)
  File "/usr/lib/python3.11/argparse.py", line 1978, in take_action
    action(self, namespace, argument_values, option_string)
  File "/usr/lib/python3.11/site-packages/borg/patterns.py", line 45, in __call__
    parse_patternfile_line(values[0], args.paths, args.patterns, ShellPattern)
  File "/usr/lib/python3.11/site-packages/borg/patterns.py", line 17, in parse_patternfile_line
    ie_command = parse_inclexcl_command(line, fallback=fallback)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/borg/patterns.py", line 384, in parse_inclexcl_command
    raise argparse.ArgumentTypeError("A pattern/command must start with anyone of: %s" %
argparse.ArgumentTypeError: A pattern/command must start with anyone of: -, !, +, R, r, P, p
$ echo $?
1
```


Discussion:

Resolving patch:
diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py
--- a/src/borg/archiver/__init__.py
+++ b/src/borg/archiver/__init__.py
@@ -625,6 +625,17 @@ def main():  # pragma: no cover
                 tb = format_tb(e)
                 print(tb, file=sys.stderr)
             sys.exit(e.exit_code)
+        except argparse.ArgumentTypeError as e:
+            # we might not have logging setup yet, so get out quickly
+            print(str(e), file=sys.stderr)
+            sys.exit(EXIT_ERROR)
+        except Exception:
+            msg = "Local Exception"
+            tb = f"{traceback.format_exc()}\n{sysinfo()}"
+            # we might not have logging setup yet, so get out quickly
+            print(msg, file=sys.stderr)
+            print(tb, file=sys.stderr)
+            sys.exit(EXIT_ERROR)
         try:
             with sig_int:
                 exit_code = archiver.run(args)
diff --git a/src/borg/patterns.py b/src/borg/patterns.py
--- a/src/borg/patterns.py
+++ b/src/borg/patterns.py
@@ -368,7 +368,7 @@ def parse_inclexcl_command(cmd_line_str, fallback=ShellPattern):
 
     cmd = cmd_prefix_map.get(cmd_line_str[0])
     if cmd is None:
-        raise argparse.ArgumentTypeError("A pattern/command must start with anyone of: %s" % ", ".join(cmd_prefix_map))
+        raise argparse.ArgumentTypeError("A pattern/command must start with any of: %s" % ", ".join(cmd_prefix_map))
 
     # remaining text on command-line following the command character
     remainder_str = cmd_line_str[1:].lstrip()


Test patch: