Repository: borgbackup/borg

Issue:
do 1.0-maint backports (2)
https://github.com/borgbackup/borg/issues?utf8=%E2%9C%93&q=label%3Abackport%2F1.0-maint%20

do 3 simple/small or 1 bigger/complex backport from there.

---

:moneybag: [there is a bounty for this](https://www.bountysource.com/issues/43626146-do-1-0-maint-backports-2)

borg key crashes
```
$ BORG_REPO=abc borg key
Traceback (most recent call last):
  File "/home/tw/w/borg-env/bin/borg", line 11, in <module>
    load_entry_point('borgbackup', 'console_scripts', 'borg')()
  File "/home/tw/w/borg/src/borg/archiver.py", line 3423, in main
    args = archiver.get_args(sys.argv, os.environ.get('SSH_ORIGINAL_COMMAND'))
  File "/home/tw/w/borg/src/borg/archiver.py", line 3296, in get_args
    result = self.parse_args(argv[1:])
  File "/home/tw/w/borg/src/borg/archiver.py", line 3315, in parse_args
    if args.func == self.do_create:
AttributeError: 'Namespace' object has no attribute 'func'
```


Discussion:

same in 1.0-maint:
``` 
(borg-env)tw@tux:~/w/borg$ BORG_REPO=ccc BORG_PASSPHRASE=1 borg key
Traceback (most recent call last):
  File "/home/tw/w/borg-env/bin/borg", line 11, in <module>
    load_entry_point('borgbackup', 'console_scripts', 'borg')()
  File "/home/tw/w/borg/borg/archiver.py", line 2135, in main
    args = archiver.get_args(sys.argv, os.environ.get('SSH_ORIGINAL_COMMAND'))
  File "/home/tw/w/borg/borg/archiver.py", line 2046, in get_args
    result = self.parse_args(argv[1:])
  File "/home/tw/w/borg/borg/archiver.py", line 2065, in parse_args
    if args.func == self.do_create:
AttributeError: 'Namespace' object has no attribute 'func'
```
Didn't we fix this already once?

Edit: This is a regression. 30a5c5e44 / patterns

Also affects "borg debug".

Resolving patch:
diff --git a/borg/archiver.py b/borg/archiver.py
--- a/borg/archiver.py
+++ b/borg/archiver.py
@@ -6,6 +6,7 @@
 import faulthandler
 import functools
 import inspect
+import itertools
 import os
 import re
 import shlex
@@ -2060,6 +2061,9 @@ def get_args(self, argv, cmd):
         if cmd is not None and result.func == self.do_serve:
             forced_result = result
             argv = shlex.split(cmd)
+            # Drop environment variables (do *not* interpret them) before trying to parse
+            # the borg command line.
+            argv = list(itertools.dropwhile(lambda arg: '=' in arg, argv))
             result = self.parse_args(argv[1:])
             if result.func != forced_result.func:
                 # someone is trying to execute a different borg subcommand, don't do that!
@@ -2075,10 +2079,11 @@ def parse_args(self, args=None):
             args = self.preprocess_args(args)
         parser = self.build_parser(args)
         args = parser.parse_args(args or ['-h'])
-        if args.func == self.do_create:
+        # This works around http://bugs.python.org/issue9351
+        func = getattr(args, 'func', None) or getattr(args, 'fallback_func')
+        if func == self.do_create and not args.paths:
             # need at least 1 path but args.paths may also be populated from patterns
-            if not args.paths:
-                parser.error('Need at least one PATH argument.')
+            parser.error('Need at least one PATH argument.')
         return args
 
     def run(self, args):
diff --git a/borg/helpers.py b/borg/helpers.py
--- a/borg/helpers.py
+++ b/borg/helpers.py
@@ -216,9 +216,9 @@ def prune_within(archives, within):
         hours = int(within[:-1]) * multiplier[within[-1]]
     except (KeyError, ValueError):
         # I don't like how this displays the original exception too:
-        raise argparse.ArgumentTypeError('Unable to parse --within option: "%s"' % within)
+        raise argparse.ArgumentTypeError('Unable to parse --keep-within option: "%s"' % within)
     if hours <= 0:
-        raise argparse.ArgumentTypeError('Number specified using --within option must be positive')
+        raise argparse.ArgumentTypeError('Number specified using --keep-within option must be positive')
     target = datetime.now(timezone.utc) - timedelta(seconds=hours * 3600)
     return [a for a in archives if a.ts > target]
 
diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py
--- a/borg/testsuite/archiver.py
+++ b/borg/testsuite/archiver.py
@@ -1800,6 +1800,13 @@ def test_get_args():
                              'borg init /')
     assert args.func == archiver.do_serve
 
+    # Check that environment variables in the forced command don't cause issues. If the command
+    # were not forced, environment variables would be interpreted by the shell, but this does not
+    # happen for forced commands - we get the verbatim command line and need to deal with env vars.
+    args = archiver.get_args(['borg', 'serve', ],
+                             'BORG_HOSTNAME_IS_UNIQUE=yes borg serve --info')
+    assert args.func == archiver.do_serve
+
 
 class TestBuildFilter:
     def test_basic(self):


Test patch: